QT备忘录之十:手搓一套Qt SQL“真·插件”

话接上回。我编译了一个支持Windows XP的Qt 5.6.3静态版本,同时将数据库驱动(Database Drivers)使用-plugin的方式也从qt源码中独立出来管理。实际使用时发现一个问题:QT += sql之后,必须一次性带上所有支持的数据库的库文件(.dll/.so),否则程序无法运行。这并不符合我对“插件”的定义,于是又想手搓一个。【本文的部分调研和数据整理工作借助AI工具完成】

0. 什么是插件?

在软件行业,‌插件(plugin)‌ 是一种遵循特定接口规范、可动态加载的独立软件模块,用于在‌不修改主程序源码‌的前提下,扩展或增强宿主应用程序的功能。其核心本质是通过标准化的‌应用程序接口(API)‌ 与主程序进行交互,实现功能的灵活扩展与模块化部署。其特征应包含:

  • 非侵入性‌:插件无需改动宿主程序的原始代码,即可实现功能新增或行为修改,保障主程序的稳定性与可维护性。
  • 依赖宿主运行‌:插件本身无法独立运行,必须由宿主程序加载、调用并提供运行环境与资源支持。
  • 动态加载机制‌:支持运行时加载与卸载(热插拔),用户可根据需求启用或禁用插件,无需重启主程序。
  • 接口标准化‌:插件与宿主之间通过预定义的接口协议通信,如事件钩子、回调函数、服务注册等,确保兼容性与可扩展性。
  • 模块化设计‌:每个插件通常实现单一功能,多个插件可组合使用,形成高度定制化的应用生态。

1. 关于Qt SQL模块

Qt SQL 模块是 Qt 框架中用于‌统一、安全、高效访问关系型数据库‌的官方组件。它不直接实现数据库引擎,而是通过‌插件化驱动架构‌,为开发者提供一套与数据库类型无关的标准化 API,实现“一次编码,多库兼容”。

‌1.1 三层解耦设计‌

层级 组件 职责 特性
驱动层 QSqlDriver 及插件 实现与具体数据库(SQLite、MySQL 等)的底层通信 动态加载,无需编译进主程序;支持自定义驱动扩展
API 层 QSqlDatabase、QSqlQuery、QSqlRecord 提供统一的连接管理、SQL 执行与结果封装 支持预处理语句、参数绑定、事务控制、错误诊断
模型层 QSqlQueryModel、QSqlTableModel、QSqlRelationalTableModel 将数据库数据映射为 Qt MVC 视图可绑定的模型 无缝集成 QTableView、QListView,支持可编辑与外键关联

‌1.2 核心功能‌

  • ‌数据库连接管理‌(QSqlDatabase)
  • ‌SQL 执行与结果处理‌(QSqlQuery )
  • ‌数据模型
模型类 适用场景 是否可编辑 特点
QSqlQueryModel 任意复杂查询结果展示 ❌ 只读 轻量,无缓存,适合报表
QSqlTableModel 单表增删改查 ✅ 支持 自动映射表结构,支持编辑策略
QSqlRelationalTableModel 带外键关联的表 ✅ 支持 可显示关联表的文本值(如“部门名称”而非 ID)

1.3 存在的问题

  • 不论是官方的动态环境,还是自己编译的静态环境,对于具体数据库的使能,只能在环境编译之前,由configure的参数决定,之后无法变更。
  • 动态编译的库,发布时可以只附带必须的qsql*.dll文件及数据库本身的库文件,例如项目使用mysql,则只需要带qsqlmysql.dll和libmysql.dll即可;而静态编译默认的配置,包括plugin在内全部以静态库形式存在,这就导致了使用此环境编译出来的程序,必须全量带上configure中未被配置为-no-sql的数据库类型。这显然不利于工程管理与发布。

2.  打造(手搓)一套理想的“SQL插件”

首先要对“理想”下一个定义。前文提到,一个“插件”应当具备非侵入性、‌依赖宿主、动态加载、接口标准化‌、模块化的特征。据此,理想的SQL插件应当是这样的:

  • 以独立于宿主程序的形式存在(动态库或独立进程)
  • 按需加载(用时加载,不用不加载)
  • 缺失时不影响宿主程序其他部分功能(宿主程序可以正常启动;可以报错,但不应崩溃)

2.1 Qt插件的生成与使用逻辑

还是那句老话,代码不会骗人。耐下心来,从qtbase\src\plugins\sqldrivers\sqldrivers.pro开始看

# sqldriver.pro
TEMPLATE = subdirs

contains(sql-plugins, psql)	: SUBDIRS += psql
contains(sql-plugins, mysql)	: SUBDIRS += mysql
contains(sql-plugins, odbc)	: SUBDIRS += odbc
contains(sql-plugins, tds)	: SUBDIRS += tds
contains(sql-plugins, oci)	: SUBDIRS += oci
contains(sql-plugins, db2)	: SUBDIRS += db2
contains(sql-plugins, sqlite)	: SUBDIRS += sqlite
contains(sql-plugins, sqlite2)	: SUBDIRS += sqlite2
contains(sql-plugins, ibase)	: SUBDIRS += ibase

这就是一个典型的子项目配置模板实例。不妨选一个具体的子项目来研究,比如qtbase\src\plugins\sqldrivers\mysql\mysql.pro:

# mysql.pro
TARGET = qsqlmysql

SOURCES = main.cpp
OTHER_FILES += mysql.json
include(../../../sql/drivers/mysql/qsql_mysql.pri)

PLUGIN_CLASS_NAME = QMYSQLDriverPlugin
include(../qsqldriverbase.pri)

qtbase\src\sql\drivers\mysql\qsql_mysql.pri:

# qsql_mysql.pri
HEADERS += $$PWD/qsql_mysql_p.h
SOURCES += $$PWD/qsql_mysql.cpp

QMAKE_CXXFLAGS *= $$QT_CFLAGS_MYSQL
LIBS += $$QT_LFLAGS_MYSQL

unix {
    isEmpty(QT_LFLAGS_MYSQL) {
        !contains(LIBS, .*mysqlclient.*):!contains(LIBS, .*mysqld.*) {
            use_libmysqlclient_r:LIBS += -lmysqlclient_r
            else:LIBS += -lmysqlclient
        }
    }
} else {
    !contains(LIBS, .*mysql.*):!contains(LIBS, .*mysqld.*):LIBS += -llibmysql
}

先在LIBS里尝试找到mysql相关的库文件;如果没找到,就使用-l引用mysql默认数据库驱动库,也便是程序启动依赖libmysql.dll/libmysqlclient.so的根本原因。再看qtbase\src\plugins\sqldrivers\qsqldriverbase.pri:

# qsqldriverbase.pri
QT  = core core-private sql-private

PLUGIN_TYPE = sqldrivers
load(qt_plugin)

DEFINES += QT_NO_CAST_TO_ASCII QT_NO_CAST_FROM_ASCII

这里是一个plugin-sqldriver的通用配置。关键在引用的qtbase\mkspecs\features\qt_plugin.prf文件,文件比较长,我尝试做了一些注释:

# qt_plugin.prf
#
#  W A R N I N G
#  -------------
#
# This file is not part of the Qt API.  It exists purely as an
# implementation detail.  It may change from version to version
# without notice, or even be removed.
#
# We mean it.
#


# 加载 Qt 构建路径配置
load(qt_build_paths)

# 检查插件类型(PLUGIN_TYPE)已定义
isEmpty(PLUGIN_TYPE): error("PLUGIN_TYPE (plugins/ subdirectory) needs to be defined.")

# 模板为 lib(动态库/静态库)
# 添加 plugin 配置,表示这是一个 Qt 插件,会链接 Qt 插件支持库
# 输出目录设置为构建目录下的 plugins/$(PLUGIN_TYPE)
TEMPLATE = lib
CONFIG += plugin
DESTDIR = $$MODULE_BASE_OUTDIR/plugins/$$PLUGIN_TYPE

# 仅在 Windows 且构建为共享库(动态插件)时生效:
#   - 若未手动设置 VERSION,则使用当前 Qt 版本号
#   - 添加 skip_target_version_ext,使生成的 DLL 文件名不带版本号后缀
win32:CONFIG(shared, static|shared) {
    isEmpty(VERSION): VERSION = $$QT_VERSION
    CONFIG += skip_target_version_ext
}

# 使插件的构建模式与 Qt 自身的配置保持一致。
# ============================================================================
tool_plugin {
    !build_pass:contains(QT_CONFIG, debug_and_release): CONFIG += release
} else:if(win32|mac):!macx-xcode {
    contains(QT_CONFIG, simulator_and_device): CONFIG += iphonesimulator_and_iphoneos
    contains(QT_CONFIG, debug_and_release): CONFIG += debug_and_release
    contains(QT_CONFIG, build_all): CONFIG += build_all
}

# 添加 relative_qt_rpath 配置,使插件在运行时使用相对于可执行文件的
CONFIG += relative_qt_rpath  # Qt's plugins should be relocatable

# 当构建静态插件(CONFIG += static)或采用“前缀构建”(prefix_build)时,
# 会生成一个 .pri 文件(qt_plugin_${MODULE}.pri),用于在其他项目中自动链接该静态插件。
# 具体操作如下:
#     1. 根据 MODULE(默认取自 TARGET)生成 .pri 文件
#     2. 文件中记录插件类型、扩展关系和类名等信息
#     3. 将 QT_PLUGINS += $$MODULE 写入文件,以便其他项目包含
#     4. 若开启 force_independent,还会生成转发 .pri 文件
#     5. 将生成的 .pri 路径加入 qmake 内部缓存,确保后续 include() 能加载
#     6. 如果是静态构建,将 .pri 文件安装到 mkspecs/modules 目录
CONFIG(static, static|shared)|prefix_build {
    isEmpty(MODULE): MODULE = $$basename(TARGET)

    mod_work_pfx = $$MODULE_QMAKE_OUTDIR/mkspecs/modules
    force_independent: \
        mod_inst_pfx = $$MODULE_QMAKE_OUTDIR/mkspecs/modules-inst
    else: \
        mod_inst_pfx = $$mod_work_pfx
    MODULE_PRI = $$mod_inst_pfx/qt_plugin_$${MODULE}.pri
    MODULE_FWD_PRI = $$mod_work_pfx/qt_plugin_$${MODULE}.pri

    !build_pass {
        # 生成主 .pri 文件内容
        MODULE_PRI_CONT = \
            "QT_PLUGIN.$${MODULE}.TYPE = $$PLUGIN_TYPE" \
            "QT_PLUGIN.$${MODULE}.EXTENDS =$$join(PLUGIN_EXTENDS, " ", " ")" \
            "QT_PLUGIN.$${MODULE}.CLASS_NAME = $$PLUGIN_CLASS_NAME" \
            "QT_PLUGINS += $$MODULE"
        write_file($$MODULE_PRI, MODULE_PRI_CONT)|error("Aborting.")
        MODULE_PRI_FILES = $$MODULE_PRI

        force_independent {
            # 若需要独立转发,则生成转发 .pri 文件
            MODULE_FWD_PRI_CONT = \
                "QT_PLUGIN.$${MODULE}.PATH = $$MODULE_BASE_OUTDIR/plugins" \
                "include($$MODULE_PRI)"
            write_file($$MODULE_FWD_PRI, MODULE_FWD_PRI_CONT)|error("Aborting.")
            touch($$MODULE_FWD_PRI, $$MODULE_PRI)
            MODULE_PRI_FILES += $$MODULE_FWD_PRI
        }

        # 将生成的 .pri 文件路径加入内部缓存,使当前项目能包含它们
        !contains(QMAKE_INTERNAL_INCLUDED_FILES, $$MODULE_FWD_PRI): \ # before the actual include()!
            cache(QMAKE_INTERNAL_INCLUDED_FILES, add transient, MODULE_PRI_FILES)
        include($$MODULE_FWD_PRI)
        for(var, $$list(TYPE EXTENDS CLASS_NAME PATH)): \
            defined(QT_PLUGIN.$${MODULE}.$$var, var): \
                cache(QT_PLUGIN.$${MODULE}.$$var, transient)
        cache(QT_PLUGINS, transient)
    }

    CONFIG(static, static|shared) {
        # 如果是静态构建,将生成的 .pri 安装到 mkspecs/modules 目录,
        # 以便其他项目通过 QT += plugin 自动发现并使用该静态插件。
        pritarget.path = $$[QT_HOST_DATA]/mkspecs/modules
        pritarget.files = $$MODULE_PRI
        INSTALLS += pritarget
    }
}

# 将最终生成的插件安装到 Qt 的全局插件目录(由 QT_INSTALL_PLUGINS 指定)
target.path = $$[QT_INSTALL_PLUGINS]/$$PLUGIN_TYPE
INSTALLS += target

# 调整目标文件名(调用 qt5LibraryTarget 函数)
TARGET = $$qt5LibraryTarget($$TARGET)

# 启用 create_cmake 配置,在构建过程中同时生成 CMake 的配置文件
CONFIG += create_cmake

# 加载其他通用特性文件
#   - qt_targets.prf:定义与 Qt 目标相关的规则(如链接 Qt 库)
#   - qt_common.prf:定义通用的编译/链接标志等
load(qt_targets)
load(qt_common)

# wince: 链接 GUI 相关库
# 添加 QMAKE_LFLAGS_NOUNDEF(通常是 -Wl,--no-undefined 或 /NODEFAULTLIB),
wince: LIBS += $$QMAKE_LIBS_GUI
QMAKE_LFLAGS += $$QMAKE_LFLAGS_NOUNDEF

所以对于mysql数据库,照此规则会生成以下文件:

  • mkspecs\modules\qt_plugin_qsqlmysql.pri
  • plugins\sqldrivers\qsqlmysql.lib
  • plugins\sqldrivers\qsqlmysql.dll(仅动态构建时)

而qsqlmysql.lib/dll又依赖libmysql.dll或者libmysqlclient.so。其他几种数据库的插件也是相同的逻辑。

2.2 将数据库驱动库全部改为动态加载

原理看明白后,就好办了。-l依赖的库,在程序执行时就会检查对应的库文件(.dll/.so),缺失时程序无法运行。因此,只需要取消所有-l,将数据库驱动改为使用动态加载的方式,再同步修改所有调用驱动库的地方。为了方便编码,选用QLibrary来实现动态库加载释放。当然,也可以使用LoadLibrary/dlopen分别编写Windows和linux的代码,只不过那样就需要维护两套代码。

以mysql为例,需要修改qsql_mysql.pri和qsql_mysql.cpp两个文件(都在qtbase\src\sql\drivers\mysql\文件夹中)。

qsql_mysql.pri:注释掉关于-l和LIBS+=的相关配置

# qsql_mysql.pri

HEADERS += $$PWD/qsql_mysql_p.h
SOURCES += $$PWD/qsql_mysql.cpp

# QMAKE_CXXFLAGS *= $$QT_CFLAGS_MYSQL
# LIBS += $$QT_LFLAGS_MYSQL

# unix {
#     isEmpty(QT_LFLAGS_MYSQL) {
#         !contains(LIBS, .*mysqlclient.*):!contains(LIBS, .*mysqld.*) {
#             use_libmysqlclient_r:LIBS += -lmysqlclient_r
#             else:LIBS += -lmysqlclient
#         }
#     }
# } else {
#     !contains(LIBS, .*mysql.*):!contains(LIBS, .*mysqld.*):LIBS += -llibmysql
# }

qsql_mysql.cpp:新增使用QLibrary管理驱动库的函数,并修改所有调用驱动库的地方。具体改动详见代码中注释。

// qsql_mysql.cpp

/****************************************************************************
**
** Copyright (C) 2015 The Qt Company Ltd.
** Contact: http://www.qt.io/licensing/
**
** This file is part of the QtSql module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL21$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see http://www.qt.io/terms-conditions. For further
** information use the contact form at http://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 2.1 or version 3 as published by the Free
** Software Foundation and appearing in the file LICENSE.LGPLv21 and
** LICENSE.LGPLv3 included in the packaging of this file. Please review the
** following information to ensure the GNU Lesser General Public License
** requirements will be met: https://www.gnu.org/licenses/lgpl.html and
** http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
**
** As a special exception, The Qt Company gives you certain additional
** rights. These rights are described in The Qt Company LGPL Exception
** version 1.1, included in the file LGPL_EXCEPTION.txt in this package.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qsql_mysql_p.h"
#include <QtSql/private/qsqldriver_p.h>
#include <qcoreapplication.h>
#include <qvariant.h>
#include <qdatetime.h>
#include <qsqlerror.h>
#include <qsqlfield.h>
#include <qsqlindex.h>
#include <qsqlquery.h>
#include <qsqlrecord.h>
#include <qstringlist.h>
#include <qtextcodec.h>
#include <qvector.h>
#include <qfile.h>
#include <qdebug.h>
#include <qlibrary.h>
#include <qmutex.h>

#ifdef Q_OS_WIN32
# define Q_NO_MYSQL_EMBEDDED
#endif

// ========================================================================
// MySQL function pointer types
// ========================================================================

typedef MYSQL* (*funMySQLInit)(MYSQL*);
typedef MYSQL* (*funMySQLRealConnect)(MYSQL*, const char*, const char*, const char*, const char*, unsigned int, const char*, unsigned long);
typedef int    (*funMySQLSelectDb)(MYSQL*, const char*);
typedef void   (*funMySQLClose)(MYSQL*);
typedef int    (*funMySQLQuery)(MYSQL*, const char*);
typedef int    (*funMySQLRealQuery)(MYSQL*, const char*, unsigned long);
typedef MYSQL_RES* (*funMySQLStoreResult)(MYSQL*);
typedef void   (*funMySQLFreeResult)(MYSQL_RES*);
typedef MYSQL_ROW (*funMySQLFetchRow)(MYSQL_RES*);
typedef unsigned long* (*funMySQLFetchLengths)(MYSQL_RES*);
typedef my_ulonglong (*funMySQLNumRows)(MYSQL_RES*);
typedef unsigned int (*funMySQLNumFields)(MYSQL_RES*);
typedef unsigned int (*funMySQLFieldCount)(MYSQL*);
typedef my_ulonglong (*funMySQLAffectedRows)(MYSQL*);
typedef my_ulonglong (*funMySQLInsertId)(MYSQL*);
typedef unsigned int (*funMySQLErrno)(MYSQL*);
typedef const char* (*funMySQLError)(MYSQL*);
typedef const char* (*funMySQLCharacterSetName)(MYSQL*);
typedef int (*funMySQLSetCharacterSet)(MYSQL*, const char*);
typedef MYSQL_RES* (*funMySQLListTables)(MYSQL*, const char*);
typedef MYSQL_RES* (*funMySQLListFields)(MYSQL*, const char*, const char*);
typedef void (*funMySQLDataSeek)(MYSQL_RES*, my_ulonglong);
typedef MYSQL_FIELD* (*funMySQLFetchFieldDirect)(MYSQL_RES*, unsigned int);
typedef MYSQL_FIELD* (*funMySQLFetchField)(MYSQL_RES*);
typedef void (*funMySQLFieldSeek)(MYSQL_RES*, MYSQL_FIELD_OFFSET);
typedef int (*funMySQLNextResult)(MYSQL*);
typedef my_bool (*funMySQLSslSet)(MYSQL*, const char*, const char*, const char*, const char*, const char*);
typedef int (*funMySQLOptions)(MYSQL*, enum mysql_option, const void*);
typedef my_bool (*funMySQLThreadInit)(void);
typedef void (*funMySQLThreadEnd)(void);
typedef int (*funMySQLServerInit)(int, char**, char**);
typedef void (*funMySQLServerEnd)(void);
typedef unsigned long (*funMySQLGetServerVersion)(MYSQL*);
typedef unsigned long (*funMySQLGetClientVersion)(void);
typedef unsigned long (*funMySQLRealEscapeString)(MYSQL*, char*, const char*, unsigned long);

// Prepared statement functions (optional)
typedef MYSQL_STMT* (*funMySQLStmtInit)(MYSQL*);
typedef int (*funMySQLStmtPrepare)(MYSQL_STMT*, const char*, unsigned long);
typedef int (*funMySQLStmtExecute)(MYSQL_STMT*);
typedef int (*funMySQLStmtFetch)(MYSQL_STMT*);
typedef int (*funMySQLStmtStoreResult)(MYSQL_STMT*);
typedef void (*funMySQLStmtDataSeek)(MYSQL_STMT*, my_ulonglong);
typedef my_ulonglong (*funMySQLStmtNumRows)(MYSQL_STMT*);
typedef my_ulonglong (*funMySQLStmtAffectedRows)(MYSQL_STMT*);
typedef my_ulonglong (*funMySQLStmtInsertId)(MYSQL_STMT*);
typedef int (*funMySQLStmtClose)(MYSQL_STMT*);
typedef int (*funMySQLStmtReset)(MYSQL_STMT*);
typedef my_bool (*funMySQLStmtBindParam)(MYSQL_STMT*, MYSQL_BIND*);
typedef my_bool (*funMySQLStmtBindResult)(MYSQL_STMT*, MYSQL_BIND*);
typedef unsigned long (*funMySQLStmtParamCount)(MYSQL_STMT*);
typedef my_bool (*funMySQLStmtAttrSet)(MYSQL_STMT*, enum enum_stmt_attr_type, const void*);
typedef MYSQL_RES* (*funMySQLStmtResultMetadata)(MYSQL_STMT*);
typedef const char* (*funMySQLStmtError)(MYSQL_STMT*);
typedef unsigned int (*funMySQLStmtErrno)(MYSQL_STMT*);

// --------------------------------------------------------------------
// MySQL function table structure
// --------------------------------------------------------------------
struct MySQLFuncs {
    funMySQLInit                 m_funMySQLInit;
    funMySQLRealConnect          m_funMySQLRealConnect;
    funMySQLSelectDb             m_funMySQLSelectDb;
    funMySQLClose                m_funMySQLClose;
    funMySQLQuery                m_funMySQLQuery;
    funMySQLRealQuery            m_funMySQLRealQuery;
    funMySQLStoreResult          m_funMySQLStoreResult;
    funMySQLFreeResult           m_funMySQLFreeResult;
    funMySQLFetchRow             m_funMySQLFetchRow;
    funMySQLFetchLengths         m_funMySQLFetchLengths;
    funMySQLNumRows              m_funMySQLNumRows;
    funMySQLFieldCount           m_funMySQLFieldCount;
    funMySQLAffectedRows         m_funMySQLAffectedRows;
    funMySQLInsertId             m_funMySQLInsertId;
    funMySQLErrno                m_funMySQLErrno;
    funMySQLError                m_funMySQLError;
    funMySQLCharacterSetName     m_funMySQLCharacterSetName;
    funMySQLSetCharacterSet      m_funMySQLSetCharacterSet;
    funMySQLListTables           m_funMySQLListTables;
    funMySQLListFields           m_funMySQLListFields;
    funMySQLDataSeek             m_funMySQLDataSeek;
    funMySQLFetchFieldDirect     m_funMySQLFetchFieldDirect;
    funMySQLFetchField           m_funMySQLFetchField;
    funMySQLFieldSeek            m_funMySQLFieldSeek;
    funMySQLNextResult           m_funMySQLNextResult;
    funMySQLSslSet               m_funMySQLSslSet;
    funMySQLOptions              m_funMySQLOptions;
    funMySQLThreadInit           m_funMySQLThreadInit;
    funMySQLThreadEnd            m_funMySQLThreadEnd;
    funMySQLServerInit           m_funMySQLServerInit;
    funMySQLServerEnd            m_funMySQLServerEnd;
    funMySQLGetServerVersion     m_funMySQLGetServerVersion;
    funMySQLGetClientVersion     m_funMySQLGetClientVersion;
    funMySQLRealEscapeString     m_funMySQLRealEscapeString;
    funMySQLNumFields            m_funMySQLNumFields;

    // Prepared statement functions (may be null if not supported)
    funMySQLStmtInit             m_funMySQLStmtInit;
    funMySQLStmtPrepare          m_funMySQLStmtPrepare;
    funMySQLStmtExecute          m_funMySQLStmtExecute;
    funMySQLStmtFetch            m_funMySQLStmtFetch;
    funMySQLStmtStoreResult      m_funMySQLStmtStoreResult;
    funMySQLStmtDataSeek         m_funMySQLStmtDataSeek;
    funMySQLStmtNumRows          m_funMySQLStmtNumRows;
    funMySQLStmtAffectedRows     m_funMySQLStmtAffectedRows;
    funMySQLStmtInsertId         m_funMySQLStmtInsertId;
    funMySQLStmtClose            m_funMySQLStmtClose;
    funMySQLStmtReset            m_funMySQLStmtReset;
    funMySQLStmtBindParam        m_funMySQLStmtBindParam;
    funMySQLStmtBindResult       m_funMySQLStmtBindResult;
    funMySQLStmtParamCount       m_funMySQLStmtParamCount;
    funMySQLStmtAttrSet          m_funMySQLStmtAttrSet;
    funMySQLStmtResultMetadata   m_funMySQLStmtResultMetadata;
    funMySQLStmtError            m_funMySQLStmtError;
    funMySQLStmtErrno            m_funMySQLStmtErrno;
};

static MySQLFuncs g_stMySQLFuncs;   // zero-initialized
static QLibrary *g_pMySQLLib = 0;
static int g_iMySQLRefCount = 0;
static QMutex g_mtMySQLMutex;       // protects g_pMySQLLib, g_iMySQLRefCount, g_stMySQLFuncs

// Load the MySQL client library and resolve all symbols (thread-safe)
static bool LoadMySQLibrary()
{
    QMutexLocker locker(&g_mtMySQLMutex);

    if (g_pMySQLLib && g_iMySQLRefCount > 0) {
        ++g_iMySQLRefCount;
        return true;
    }
    if (g_pMySQLLib) {
        // loaded but refCount zero? treat as not loaded
        g_pMySQLLib->unload();
        delete g_pMySQLLib;
        g_pMySQLLib = 0;
        memset(&g_stMySQLFuncs, 0, sizeof(g_stMySQLFuncs));
    }

#if defined(Q_OS_WIN)
    g_pMySQLLib = new QLibrary(QString::fromLatin1("libmysql.dll"));
#else
    g_pMySQLLib = new QLibrary(QString::fromLatin1("libmysqlclient.so"));
#endif
    if (!g_pMySQLLib->load())
    {
        delete g_pMySQLLib;
        g_pMySQLLib = nullptr;
        return false;
    }

    // Resolve all required functions
    g_stMySQLFuncs.m_funMySQLInit               = (funMySQLInit)               g_pMySQLLib->resolve("mysql_init");
    g_stMySQLFuncs.m_funMySQLRealConnect        = (funMySQLRealConnect)        g_pMySQLLib->resolve("mysql_real_connect");
    g_stMySQLFuncs.m_funMySQLSelectDb           = (funMySQLSelectDb)           g_pMySQLLib->resolve("mysql_select_db");
    g_stMySQLFuncs.m_funMySQLClose              = (funMySQLClose)              g_pMySQLLib->resolve("mysql_close");
    g_stMySQLFuncs.m_funMySQLQuery              = (funMySQLQuery)              g_pMySQLLib->resolve("mysql_query");
    g_stMySQLFuncs.m_funMySQLRealQuery          = (funMySQLRealQuery)          g_pMySQLLib->resolve("mysql_real_query");
    g_stMySQLFuncs.m_funMySQLStoreResult        = (funMySQLStoreResult)        g_pMySQLLib->resolve("mysql_store_result");
    g_stMySQLFuncs.m_funMySQLFreeResult         = (funMySQLFreeResult)         g_pMySQLLib->resolve("mysql_free_result");
    g_stMySQLFuncs.m_funMySQLFetchRow           = (funMySQLFetchRow)           g_pMySQLLib->resolve("mysql_fetch_row");
    g_stMySQLFuncs.m_funMySQLFetchLengths       = (funMySQLFetchLengths)       g_pMySQLLib->resolve("mysql_fetch_lengths");
    g_stMySQLFuncs.m_funMySQLNumRows            = (funMySQLNumRows)            g_pMySQLLib->resolve("mysql_num_rows");
    g_stMySQLFuncs.m_funMySQLFieldCount         = (funMySQLFieldCount)         g_pMySQLLib->resolve("mysql_field_count");
    g_stMySQLFuncs.m_funMySQLAffectedRows       = (funMySQLAffectedRows)       g_pMySQLLib->resolve("mysql_affected_rows");
    g_stMySQLFuncs.m_funMySQLInsertId           = (funMySQLInsertId)           g_pMySQLLib->resolve("mysql_insert_id");
    g_stMySQLFuncs.m_funMySQLErrno              = (funMySQLErrno)              g_pMySQLLib->resolve("mysql_errno");
    g_stMySQLFuncs.m_funMySQLError              = (funMySQLError)              g_pMySQLLib->resolve("mysql_error");
    g_stMySQLFuncs.m_funMySQLCharacterSetName   = (funMySQLCharacterSetName)   g_pMySQLLib->resolve("mysql_character_set_name");
    g_stMySQLFuncs.m_funMySQLSetCharacterSet    = (funMySQLSetCharacterSet)    g_pMySQLLib->resolve("mysql_set_character_set");
    g_stMySQLFuncs.m_funMySQLListTables         = (funMySQLListTables)         g_pMySQLLib->resolve("mysql_list_tables");
    g_stMySQLFuncs.m_funMySQLListFields         = (funMySQLListFields)         g_pMySQLLib->resolve("mysql_list_fields");
    g_stMySQLFuncs.m_funMySQLDataSeek           = (funMySQLDataSeek)           g_pMySQLLib->resolve("mysql_data_seek");
    g_stMySQLFuncs.m_funMySQLFetchFieldDirect   = (funMySQLFetchFieldDirect)   g_pMySQLLib->resolve("mysql_fetch_field_direct");
    g_stMySQLFuncs.m_funMySQLFetchField         = (funMySQLFetchField)         g_pMySQLLib->resolve("mysql_fetch_field");
    g_stMySQLFuncs.m_funMySQLFieldSeek          = (funMySQLFieldSeek)          g_pMySQLLib->resolve("mysql_field_seek");
    g_stMySQLFuncs.m_funMySQLNextResult         = (funMySQLNextResult)         g_pMySQLLib->resolve("mysql_next_result");
    g_stMySQLFuncs.m_funMySQLSslSet             = (funMySQLSslSet)             g_pMySQLLib->resolve("mysql_ssl_set");
    g_stMySQLFuncs.m_funMySQLOptions            = (funMySQLOptions)            g_pMySQLLib->resolve("mysql_options");
    g_stMySQLFuncs.m_funMySQLThreadInit         = (funMySQLThreadInit)         g_pMySQLLib->resolve("mysql_thread_init");
    g_stMySQLFuncs.m_funMySQLThreadEnd          = (funMySQLThreadEnd)          g_pMySQLLib->resolve("mysql_thread_end");
    g_stMySQLFuncs.m_funMySQLServerInit         = (funMySQLServerInit)         g_pMySQLLib->resolve("mysql_server_init");
    g_stMySQLFuncs.m_funMySQLServerEnd          = (funMySQLServerEnd)          g_pMySQLLib->resolve("mysql_server_end");
    g_stMySQLFuncs.m_funMySQLGetServerVersion   = (funMySQLGetServerVersion)   g_pMySQLLib->resolve("mysql_get_server_version");
    g_stMySQLFuncs.m_funMySQLGetClientVersion   = (funMySQLGetClientVersion)   g_pMySQLLib->resolve("mysql_get_client_version");
    g_stMySQLFuncs.m_funMySQLRealEscapeString   = (funMySQLRealEscapeString)   g_pMySQLLib->resolve("mysql_real_escape_string");
    g_stMySQLFuncs.m_funMySQLNumFields          = (funMySQLNumFields)   g_pMySQLLib->resolve("mysql_mysql_num_fields");

    // Prepared statement functions (optional, may be null)
    g_stMySQLFuncs.m_funMySQLStmtInit           = (funMySQLStmtInit)           g_pMySQLLib->resolve("mysql_stmt_init");
    g_stMySQLFuncs.m_funMySQLStmtPrepare        = (funMySQLStmtPrepare)        g_pMySQLLib->resolve("mysql_stmt_prepare");
    g_stMySQLFuncs.m_funMySQLStmtExecute        = (funMySQLStmtExecute)        g_pMySQLLib->resolve("mysql_stmt_execute");
    g_stMySQLFuncs.m_funMySQLStmtFetch          = (funMySQLStmtFetch)          g_pMySQLLib->resolve("mysql_stmt_fetch");
    g_stMySQLFuncs.m_funMySQLStmtStoreResult    = (funMySQLStmtStoreResult)    g_pMySQLLib->resolve("mysql_stmt_store_result");
    g_stMySQLFuncs.m_funMySQLStmtDataSeek       = (funMySQLStmtDataSeek)       g_pMySQLLib->resolve("mysql_stmt_data_seek");
    g_stMySQLFuncs.m_funMySQLStmtNumRows        = (funMySQLStmtNumRows)        g_pMySQLLib->resolve("mysql_stmt_num_rows");
    g_stMySQLFuncs.m_funMySQLStmtAffectedRows   = (funMySQLStmtAffectedRows)   g_pMySQLLib->resolve("mysql_stmt_affected_rows");
    g_stMySQLFuncs.m_funMySQLStmtInsertId       = (funMySQLStmtInsertId)       g_pMySQLLib->resolve("mysql_stmt_insert_id");
    g_stMySQLFuncs.m_funMySQLStmtClose          = (funMySQLStmtClose)          g_pMySQLLib->resolve("mysql_stmt_close");
    g_stMySQLFuncs.m_funMySQLStmtReset          = (funMySQLStmtReset)          g_pMySQLLib->resolve("mysql_stmt_reset");
    g_stMySQLFuncs.m_funMySQLStmtBindParam      = (funMySQLStmtBindParam)      g_pMySQLLib->resolve("mysql_stmt_bind_param");
    g_stMySQLFuncs.m_funMySQLStmtBindResult     = (funMySQLStmtBindResult)     g_pMySQLLib->resolve("mysql_stmt_bind_result");
    g_stMySQLFuncs.m_funMySQLStmtParamCount     = (funMySQLStmtParamCount)     g_pMySQLLib->resolve("mysql_stmt_param_count");
    g_stMySQLFuncs.m_funMySQLStmtAttrSet        = (funMySQLStmtAttrSet)        g_pMySQLLib->resolve("mysql_stmt_attr_set");
    g_stMySQLFuncs.m_funMySQLStmtResultMetadata = (funMySQLStmtResultMetadata) g_pMySQLLib->resolve("mysql_stmt_result_metadata");
    g_stMySQLFuncs.m_funMySQLStmtError          = (funMySQLStmtError)          g_pMySQLLib->resolve("mysql_stmt_error");
    g_stMySQLFuncs.m_funMySQLStmtErrno          = (funMySQLStmtErrno)          g_pMySQLLib->resolve("mysql_stmt_errno");

    // Check that all required symbols are present
    if (!g_stMySQLFuncs.m_funMySQLInit || !g_stMySQLFuncs.m_funMySQLRealConnect ||
        !g_stMySQLFuncs.m_funMySQLSelectDb || !g_stMySQLFuncs.m_funMySQLClose ||
        !g_stMySQLFuncs.m_funMySQLQuery || !g_stMySQLFuncs.m_funMySQLRealQuery ||
        !g_stMySQLFuncs.m_funMySQLStoreResult || !g_stMySQLFuncs.m_funMySQLFreeResult ||
        !g_stMySQLFuncs.m_funMySQLFetchRow || !g_stMySQLFuncs.m_funMySQLFetchLengths ||
        !g_stMySQLFuncs.m_funMySQLNumRows || !g_stMySQLFuncs.m_funMySQLFieldCount ||
        !g_stMySQLFuncs.m_funMySQLAffectedRows || !g_stMySQLFuncs.m_funMySQLInsertId ||
        !g_stMySQLFuncs.m_funMySQLErrno || !g_stMySQLFuncs.m_funMySQLError ||
        !g_stMySQLFuncs.m_funMySQLCharacterSetName || !g_stMySQLFuncs.m_funMySQLSetCharacterSet ||
        !g_stMySQLFuncs.m_funMySQLListTables || !g_stMySQLFuncs.m_funMySQLListFields ||
        !g_stMySQLFuncs.m_funMySQLDataSeek || !g_stMySQLFuncs.m_funMySQLFetchFieldDirect ||
        !g_stMySQLFuncs.m_funMySQLFetchField || !g_stMySQLFuncs.m_funMySQLFieldSeek ||
        !g_stMySQLFuncs.m_funMySQLNextResult || !g_stMySQLFuncs.m_funMySQLSslSet ||
        !g_stMySQLFuncs.m_funMySQLOptions || !g_stMySQLFuncs.m_funMySQLGetServerVersion) {
        g_pMySQLLib->unload();
        delete g_pMySQLLib;
        g_pMySQLLib = 0;
        memset(&g_stMySQLFuncs, 0, sizeof(g_stMySQLFuncs));
        return false;
    }

    g_iMySQLRefCount = 1;
    return true;
}

static void UnloadMySQLibrary()
{
    QMutexLocker locker(&g_mtMySQLMutex);

    if (!g_pMySQLLib)
        return;
    --g_iMySQLRefCount;
    if (g_iMySQLRefCount == 0) {
        g_pMySQLLib->unload();
        delete g_pMySQLLib;
        g_pMySQLLib = 0;
        memset(&g_stMySQLFuncs, 0, sizeof(g_stMySQLFuncs));
    }
}

// ========================================================================
// Original Qt MySQL driver code (all API calls replaced with g_stMySQLFuncs)
// ========================================================================

Q_DECLARE_METATYPE(MYSQL_RES*)
Q_DECLARE_METATYPE(MYSQL*)

#if MYSQL_VERSION_ID >= 40108
Q_DECLARE_METATYPE(MYSQL_STMT*)
#endif

#if MYSQL_VERSION_ID >= 40100
#  define Q_CLIENT_MULTI_STATEMENTS CLIENT_MULTI_STATEMENTS
#else
#  define Q_CLIENT_MULTI_STATEMENTS 0
#endif

QT_BEGIN_NAMESPACE

class QMYSQLDriverPrivate : public QSqlDriverPrivate
{
public:
    QMYSQLDriverPrivate() : QSqlDriverPrivate(), mysql(0),
#ifndef QT_NO_TEXTCODEC
        tc(QTextCodec::codecForLocale()),
#else
        tc(0),
#endif
        preparedQuerysEnabled(false) { dbmsType = QSqlDriver::MySqlServer; }
    MYSQL *mysql;
    QTextCodec *tc;
    bool preparedQuerysEnabled;
};

static inline QString toUnicode(QTextCodec *tc, const char *str)
{
#ifdef QT_NO_TEXTCODEC
    Q_UNUSED(tc);
    return QString::fromLatin1(str);
#else
    return tc->toUnicode(str);
#endif
}

static inline QString toUnicode(QTextCodec *tc, const char *str, int length)
{
#ifdef QT_NO_TEXTCODEC
    Q_UNUSED(tc);
    return QString::fromLatin1(str, length);
#else
    return tc->toUnicode(str, length);
#endif
}

static inline QByteArray fromUnicode(QTextCodec *tc, const QString &str)
{
#ifdef QT_NO_TEXTCODEC
    Q_UNUSED(tc);
    return str.toLatin1();
#else
    return tc->fromUnicode(str);
#endif
}

static inline QVariant qDateFromString(const QString &val)
{
#ifdef QT_NO_DATESTRING
    Q_UNUSED(val);
    return QVariant(val);
#else
    if (val.isEmpty())
        return QVariant(QDate());
    return QVariant(QDate::fromString(val, Qt::ISODate));
#endif
}

static inline QVariant qTimeFromString(const QString &val)
{
#ifdef QT_NO_DATESTRING
    Q_UNUSED(val);
    return QVariant(val);
#else
    if (val.isEmpty())
        return QVariant(QTime());
    return QVariant(QTime::fromString(val, Qt::ISODate));
#endif
}

static inline QVariant qDateTimeFromString(QString &val)
{
#ifdef QT_NO_DATESTRING
    Q_UNUSED(val);
    return QVariant(val);
#else
    if (val.isEmpty())
        return QVariant(QDateTime());
    if (val.length() == 14)
        // TIMESTAMPS have the format yyyyMMddhhmmss
        val.insert(4, QLatin1Char('-')).insert(7, QLatin1Char('-')).insert(10,
                    QLatin1Char('T')).insert(13, QLatin1Char(':')).insert(16, QLatin1Char(':'));
    return QVariant(QDateTime::fromString(val, Qt::ISODate));
#endif
}

class QMYSQLResultPrivate : public QObject
{
    Q_OBJECT
public:
    QMYSQLResultPrivate(const QMYSQLDriver* dp, const QMYSQLResult* d) : driver(dp), result(0), q(d),
        rowsAffected(0), hasBlobs(false)
#if MYSQL_VERSION_ID >= 40108
        , stmt(0), meta(0), inBinds(0), outBinds(0)
#endif
        , preparedQuery(false)
        {
            connect(dp, SIGNAL(destroyed()), this, SLOT(driverDestroyed()));
        }

    const QMYSQLDriver* driver;
    MYSQL_RES *result;
    MYSQL_ROW row;
    const QMYSQLResult* q;

    int rowsAffected;

    bool bindInValues();
    void bindBlobs();

    bool hasBlobs;
    struct QMyField
    {
        QMyField()
            : outField(0), nullIndicator(false), bufLength(0ul),
              myField(0), type(QVariant::Invalid)
        {}
        char *outField;
        my_bool nullIndicator;
        ulong bufLength;
        MYSQL_FIELD *myField;
        QVariant::Type type;
    };

    QVector<QMyField> fields;

#if MYSQL_VERSION_ID >= 40108
    MYSQL_STMT* stmt;
    MYSQL_RES* meta;

    MYSQL_BIND *inBinds;
    MYSQL_BIND *outBinds;
#endif

    bool preparedQuery;

private Q_SLOTS:
    void driverDestroyed() { driver = NULL; }
};

#ifndef QT_NO_TEXTCODEC
static QTextCodec* codec(MYSQL* mysql)
{
#if MYSQL_VERSION_ID >= 32321
    if (g_stMySQLFuncs.m_funMySQLCharacterSetName) {
        QTextCodec* heuristicCodec = QTextCodec::codecForName(g_stMySQLFuncs.m_funMySQLCharacterSetName(mysql));
        if (heuristicCodec)
            return heuristicCodec;
    }
#endif
    return QTextCodec::codecForLocale();
}
#endif // QT_NO_TEXTCODEC

static QSqlError qMakeError(const QString& err, QSqlError::ErrorType type,
                            const QMYSQLDriverPrivate* p)
{
    const char *cerr = p->mysql ? g_stMySQLFuncs.m_funMySQLError(p->mysql) : 0;
    QString errorText;
    if (cerr)
        errorText = p->tc ? toUnicode(p->tc, cerr) : QString::fromLatin1(cerr);
    unsigned int errnoVal = p->mysql ? g_stMySQLFuncs.m_funMySQLErrno(p->mysql) : 0;
    return QSqlError(QLatin1String("QMYSQL: ") + err, errorText, type, errnoVal);
}


static QVariant::Type qDecodeMYSQLType(int mysqltype, uint flags)
{
    QVariant::Type type;
    switch (mysqltype) {
    case FIELD_TYPE_TINY :
        type = static_cast<QVariant::Type>((flags & UNSIGNED_FLAG) ? QMetaType::UChar : QMetaType::Char);
        break;
    case FIELD_TYPE_SHORT :
        type = static_cast<QVariant::Type>((flags & UNSIGNED_FLAG) ? QMetaType::UShort : QMetaType::Short);
        break;
    case FIELD_TYPE_LONG :
    case FIELD_TYPE_INT24 :
        type = (flags & UNSIGNED_FLAG) ? QVariant::UInt : QVariant::Int;
        break;
    case FIELD_TYPE_YEAR :
        type = QVariant::Int;
        break;
    case FIELD_TYPE_LONGLONG :
        type = (flags & UNSIGNED_FLAG) ? QVariant::ULongLong : QVariant::LongLong;
        break;
    case FIELD_TYPE_FLOAT :
    case FIELD_TYPE_DOUBLE :
    case FIELD_TYPE_DECIMAL :
#if defined(FIELD_TYPE_NEWDECIMAL)
    case FIELD_TYPE_NEWDECIMAL:
#endif
        type = QVariant::Double;
        break;
    case FIELD_TYPE_DATE :
        type = QVariant::Date;
        break;
    case FIELD_TYPE_TIME :
        type = QVariant::Time;
        break;
    case FIELD_TYPE_DATETIME :
    case FIELD_TYPE_TIMESTAMP :
        type = QVariant::DateTime;
        break;
    case FIELD_TYPE_STRING :
    case FIELD_TYPE_VAR_STRING :
    case FIELD_TYPE_BLOB :
    case FIELD_TYPE_TINY_BLOB :
    case FIELD_TYPE_MEDIUM_BLOB :
    case FIELD_TYPE_LONG_BLOB :
        type = (flags & BINARY_FLAG) ? QVariant::ByteArray : QVariant::String;
        break;
    default:
    case FIELD_TYPE_ENUM :
    case FIELD_TYPE_SET :
        type = QVariant::String;
        break;
    }
    return type;
}

static QSqlField qToField(MYSQL_FIELD *field, QTextCodec *tc)
{
    QSqlField f(toUnicode(tc, field->name),
                qDecodeMYSQLType(int(field->type), field->flags));
    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;
}

#if MYSQL_VERSION_ID >= 40108
// ------------------------------------------------------------------------
// Prepared statement helpers (originally under #if MYSQL_VERSION_ID >= 40108)

static QSqlError qMakeStmtError(const QString& err, QSqlError::ErrorType type,
                            MYSQL_STMT* stmt)
{
    const char *cerr = stmt ? g_stMySQLFuncs.m_funMySQLStmtError(stmt) : 0;
    QString errorText;
    if (cerr)
        errorText = QString::fromLatin1(cerr);
    unsigned int errnoVal = stmt ? g_stMySQLFuncs.m_funMySQLStmtErrno(stmt) : 0;
    return QSqlError(QLatin1String("QMYSQL3: ") + err, errorText, type, errnoVal);
}

static bool qIsBlob(int t)
{
    return t == MYSQL_TYPE_TINY_BLOB
           || t == MYSQL_TYPE_BLOB
           || t == MYSQL_TYPE_MEDIUM_BLOB
           || t == MYSQL_TYPE_LONG_BLOB;
}

static bool qIsInteger(int t)
{
    return t == QMetaType::Char || t == QMetaType::UChar
        || t == QMetaType::Short || t == QMetaType::UShort
        || t == QMetaType::Int || t == QMetaType::UInt
        || t == QMetaType::LongLong || t == QMetaType::ULongLong;
}

void QMYSQLResultPrivate::bindBlobs()
{
    int i;
    MYSQL_FIELD *fieldInfo;
    MYSQL_BIND *bind;

    for(i = 0; i < fields.count(); ++i) {
        fieldInfo = fields.at(i).myField;
        if (qIsBlob(inBinds[i].buffer_type) && meta && fieldInfo) {
            bind = &inBinds[i];
            bind->buffer_length = fieldInfo->max_length;
            delete[] static_cast<char*>(bind->buffer);
            bind->buffer = new char[fieldInfo->max_length];
            fields[i].outField = static_cast<char*>(bind->buffer);
        }
    }
}

bool QMYSQLResultPrivate::bindInValues()
{
    MYSQL_BIND *bind;
    char *field;
    int i = 0;

    if (!meta)
        meta = g_stMySQLFuncs.m_funMySQLStmtResultMetadata(stmt);
    if (!meta)
        return false;

    fields.resize(g_stMySQLFuncs.m_funMySQLNumFields(meta));

    inBinds = new MYSQL_BIND[fields.size()];
    memset(inBinds, 0, fields.size() * sizeof(MYSQL_BIND));

    MYSQL_FIELD *fieldInfo;

    while((fieldInfo = g_stMySQLFuncs.m_funMySQLFetchField(meta))) {
        QMyField &f = fields[i];
        f.myField = fieldInfo;

        f.type = qDecodeMYSQLType(fieldInfo->type, fieldInfo->flags);
        if (qIsBlob(fieldInfo->type)) {
            // the size of a blob-field is available as soon as we call
            // mysql_stmt_store_result()
            // after mysql_stmt_exec() in QMYSQLResult::exec()
            fieldInfo->length = 0;
            hasBlobs = true;
        } else if (qIsInteger(f.type)) {
            fieldInfo->length = 8;
        } else {
            fieldInfo->type = MYSQL_TYPE_STRING;
        }
        bind = &inBinds[i];
        field = new char[fieldInfo->length + 1];
        memset(field, 0, fieldInfo->length + 1);

        bind->buffer_type = fieldInfo->type;
        bind->buffer = field;
        bind->buffer_length = f.bufLength = fieldInfo->length + 1;
        bind->is_null = &f.nullIndicator;
        bind->length = &f.bufLength;
        bind->is_unsigned = fieldInfo->flags & UNSIGNED_FLAG ? 1 : 0;
        f.outField=field;

        ++i;
    }
    return true;
}
#endif

QMYSQLResult::QMYSQLResult(const QMYSQLDriver* db)
: QSqlResult(db)
{
    d = new QMYSQLResultPrivate(db, this);
}

QMYSQLResult::~QMYSQLResult()
{
    cleanup();
    delete d;
}

QVariant QMYSQLResult::handle() const
{
#if MYSQL_VERSION_ID >= 40108
    if(d->preparedQuery)
    return d->meta ? QVariant::fromValue(d->meta) : QVariant::fromValue(d->stmt);
    else
#endif
        return QVariant::fromValue(d->result);
}

void QMYSQLResult::cleanup()
{
    if (d->result)
        g_stMySQLFuncs.m_funMySQLFreeResult(d->result);

    // Must iterate through leftover result sets from multi‑selects or stored procedures
// if this isn't done subsequent queries will fail with "Commands out of sync"
#if MYSQL_VERSION_ID >= 40100
    while (d->driver && d->driver->d_func()->mysql && g_stMySQLFuncs.m_funMySQLNextResult(d->driver->d_func()->mysql) == 0) {
        MYSQL_RES *res = g_stMySQLFuncs.m_funMySQLStoreResult(d->driver->d_func()->mysql);
        if (res)
            g_stMySQLFuncs.m_funMySQLFreeResult(res);
    }
#endif

#if MYSQL_VERSION_ID >= 40108
    if (d->stmt) {
        if (g_stMySQLFuncs.m_funMySQLStmtClose(d->stmt))
            qWarning("QMYSQLResult::cleanup: unable to free statement handle");
        d->stmt = 0;
    }

    if (d->meta) {
        g_stMySQLFuncs.m_funMySQLFreeResult(d->meta);
        d->meta = 0;
    }

    int i;
    for (i = 0; i < d->fields.count(); ++i)
        delete[] d->fields[i].outField;

    if (d->outBinds) {
        delete[] d->outBinds;
        d->outBinds = 0;
    }

    if (d->inBinds) {
        delete[] d->inBinds;
        d->inBinds = 0;
    }
#endif

    d->hasBlobs = false;
    d->fields.clear();
    d->result = NULL;
    d->row = NULL;
    setAt(-1);
    setActive(false);
}

bool QMYSQLResult::fetch(int i)
{
    if(!d->driver)
        return false;
    if (isForwardOnly()) { // fake a forward seek
        if (at() < i) {
            int x = i - at();
            while (--x && fetchNext()) {};
            return fetchNext();
        } else {
            return false;
        }
    }
    if (at() == i)
        return true;
    if (d->preparedQuery) {
#if MYSQL_VERSION_ID >= 40108
        g_stMySQLFuncs.m_funMySQLStmtDataSeek(d->stmt, i);
        int nRC = g_stMySQLFuncs.m_funMySQLStmtFetch(d->stmt);
        if (nRC) {
#ifdef MYSQL_DATA_TRUNCATED
            if (nRC == 1 || nRC == MYSQL_DATA_TRUNCATED)
#else
            if (nRC == 1)
#endif
                setLastError(qMakeStmtError(QCoreApplication::translate("QMYSQLResult",
                         "Unable to fetch data"), QSqlError::StatementError, d->stmt));
            return false;
        }
#else
        return false;
#endif
    } else {
        g_stMySQLFuncs.m_funMySQLDataSeek(d->result, i);
        d->row = g_stMySQLFuncs.m_funMySQLFetchRow(d->result);
        if (!d->row)
            return false;
    }

    setAt(i);
    return true;
}

bool QMYSQLResult::fetchNext()
{
    if(!d->driver)
        return false;
    if (d->preparedQuery) {
#if MYSQL_VERSION_ID >= 40108
        int nRC = g_stMySQLFuncs.m_funMySQLStmtFetch(d->stmt);
        if (nRC) {
#ifdef MYSQL_DATA_TRUNCATED
            if (nRC == 1 || nRC == MYSQL_DATA_TRUNCATED)
#else
            if (nRC == 1)
#endif // MYSQL_DATA_TRUNCATED
                setLastError(qMakeStmtError(QCoreApplication::translate("QMYSQLResult",
                                    "Unable to fetch data"), QSqlError::StatementError, d->stmt));
            return false;
        }
#else
        return false;
#endif
    } else {
        d->row = g_stMySQLFuncs.m_funMySQLFetchRow(d->result);
        if (!d->row)
            return false;
    }
    setAt(at() + 1);
    return true;
}

bool QMYSQLResult::fetchLast()
{
    if(!d->driver)
        return false;
    if (isForwardOnly()) { // fake this since MySQL can't seek on forward only queries
        bool success = fetchNext(); // did we move at all?
        while (fetchNext()) {};
        return success;
    }

    my_ulonglong numRows;
    if (d->preparedQuery) {
#if MYSQL_VERSION_ID >= 40108
        numRows = g_stMySQLFuncs.m_funMySQLStmtNumRows(d->stmt);
#else
        numRows = 0;
#endif
    } else {
        numRows = g_stMySQLFuncs.m_funMySQLNumRows(d->result);
    }
    if (at() == int(numRows))
        return true;
    if (!numRows)
        return false;
    return fetch(numRows - 1);
}

bool QMYSQLResult::fetchFirst()
{
    if (at() == 0)
        return true;

    if (isForwardOnly())
        return (at() == QSql::BeforeFirstRow) ? fetchNext() : false;
    return fetch(0);
}

QVariant QMYSQLResult::data(int field)
{

    if (!isSelect() || field >= d->fields.count()) {
        qWarning("QMYSQLResult::data: column %d out of range", field);
        return QVariant();
    }

    if (!d->driver)
        return QVariant();

    int fieldLength = 0;
    const QMYSQLResultPrivate::QMyField &f = d->fields.at(field);
    QString val;
    if (d->preparedQuery) {
        if (f.nullIndicator)
            return QVariant(f.type);

        if (qIsInteger(f.type)) {
            QVariant variant(f.type, f.outField);
            // we never want to return char variants here, see QTBUG-53397
            if (static_cast<int>(f.type) == QMetaType::UChar)
                return variant.toUInt();
            else if (static_cast<int>(f.type) == QMetaType::Char)
                return variant.toInt();
            return variant;
        }

        if (f.type != QVariant::ByteArray)
            val = toUnicode(d->driver->d_func()->tc, f.outField, f.bufLength);
    } else {
        if (d->row[field] == NULL) {
            // NULL value
            return QVariant(f.type);
        }

        fieldLength = g_stMySQLFuncs.m_funMySQLFetchLengths(d->result)[field];

        if (f.type != QVariant::ByteArray)
            val = toUnicode(d->driver->d_func()->tc, d->row[field], fieldLength);
    }

    switch (static_cast<int>(f.type)) {
    case QVariant::LongLong:
        return QVariant(val.toLongLong());
    case QVariant::ULongLong:
        return QVariant(val.toULongLong());
    case QMetaType::Char:
    case QMetaType::Short:
    case QVariant::Int:
        return QVariant(val.toInt());
    case QMetaType::UChar:
    case QMetaType::UShort:
    case QVariant::UInt:
        return QVariant(val.toUInt());
    case QVariant::Double: {
        QVariant v;
        bool ok=false;
        double dbl = val.toDouble(&ok);
        switch(numericalPrecisionPolicy()) {
            case QSql::LowPrecisionInt32:
                v=QVariant(dbl).toInt();
                break;
            case QSql::LowPrecisionInt64:
                v = QVariant(dbl).toLongLong();
                break;
            case QSql::LowPrecisionDouble:
                v = QVariant(dbl);
                break;
            case QSql::HighPrecision:
            default:
                v = val;
                ok = true;
                break;
        }
        if(ok)
            return v;
        else
            return QVariant();
    }
        return QVariant(val.toDouble());
    case QVariant::Date:
        return qDateFromString(val);
    case QVariant::Time:
        return qTimeFromString(val);
    case QVariant::DateTime:
        return qDateTimeFromString(val);
    case QVariant::ByteArray: {

        QByteArray ba;
        if (d->preparedQuery) {
            ba = QByteArray(f.outField, f.bufLength);
        } else {
            ba = QByteArray(d->row[field], fieldLength);
        }
        return QVariant(ba);
    }
    default:
    case QVariant::String:
        return QVariant(val);
    }
    qWarning("QMYSQLResult::data: unknown data type");
    return QVariant();
}

bool QMYSQLResult::isNull(int field)
{
   if (field < 0 || field >= d->fields.count())
       return true;
   if (d->preparedQuery)
       return d->fields.at(field).nullIndicator;
   else
       return d->row[field] == NULL;
}

bool QMYSQLResult::reset (const QString& query)
{
    if (!driver() || !driver()->isOpen() || driver()->isOpenError() || !d->driver)
        return false;

    d->preparedQuery = false;

    cleanup();

    const QByteArray encQuery(fromUnicode(d->driver->d_func()->tc, query));
    if (g_stMySQLFuncs.m_funMySQLRealQuery(d->driver->d_func()->mysql, encQuery.data(), encQuery.length())) {
        setLastError(qMakeError(QCoreApplication::translate("QMYSQLResult", "Unable to execute query"),
                     QSqlError::StatementError, d->driver->d_func()));
        return false;
    }
    d->result = g_stMySQLFuncs.m_funMySQLStoreResult(d->driver->d_func()->mysql);
    if (!d->result && g_stMySQLFuncs.m_funMySQLFieldCount(d->driver->d_func()->mysql) > 0) {
        setLastError(qMakeError(QCoreApplication::translate("QMYSQLResult", "Unable to store result"),
                    QSqlError::StatementError, d->driver->d_func()));
        return false;
    }
    int numFields = g_stMySQLFuncs.m_funMySQLFieldCount(d->driver->d_func()->mysql);
    setSelect(numFields != 0);
    d->fields.resize(numFields);
    d->rowsAffected = g_stMySQLFuncs.m_funMySQLAffectedRows(d->driver->d_func()->mysql);

    if (isSelect()) {
        for(int i = 0; i < numFields; i++) {
            MYSQL_FIELD* field = g_stMySQLFuncs.m_funMySQLFetchFieldDirect(d->result, i);
            d->fields[i].type = qDecodeMYSQLType(field->type, field->flags);
        }
        setAt(QSql::BeforeFirstRow);
    }
    setActive(true);
    return isActive();
}

int QMYSQLResult::size()
{
    if (d->driver && isSelect())
        if (d->preparedQuery)
#if MYSQL_VERSION_ID >= 40108
            return g_stMySQLFuncs.m_funMySQLStmtNumRows(d->stmt);
#else
            return -1;
#endif
        else
            return int(g_stMySQLFuncs.m_funMySQLNumRows(d->result));
    else
        return -1;
}

int QMYSQLResult::numRowsAffected()
{
    return d->rowsAffected;
}

QVariant QMYSQLResult::lastInsertId() const
{
    if (!isActive() || !d->driver)
        return QVariant();

    if (d->preparedQuery) {
#if MYSQL_VERSION_ID >= 40108
        quint64 id = g_stMySQLFuncs.m_funMySQLStmtInsertId(d->stmt);
        if (id)
            return QVariant(id);
#endif
    } else {
        quint64 id = g_stMySQLFuncs.m_funMySQLInsertId(d->driver->d_func()->mysql);
        if (id)
            return QVariant(id);
    }
    return QVariant();
}

QSqlRecord QMYSQLResult::record() const
{
    QSqlRecord info;
    MYSQL_RES *res;
    if (!isActive() || !isSelect() || !d->driver)
        return info;

#if MYSQL_VERSION_ID >= 40108
    res = d->preparedQuery ? d->meta : d->result;
#else
    res = d->result;
#endif

    if (!g_stMySQLFuncs.m_funMySQLErrno(d->driver->d_func()->mysql)) {
        g_stMySQLFuncs.m_funMySQLFieldSeek(res, 0);
        MYSQL_FIELD* field = g_stMySQLFuncs.m_funMySQLFetchField(res);
        while(field) {
            info.append(qToField(field, d->driver->d_func()->tc));
            field = g_stMySQLFuncs.m_funMySQLFetchField(res);
        }
    }
    g_stMySQLFuncs.m_funMySQLFieldSeek(res, 0);
    return info;
}

bool QMYSQLResult::nextResult()
{
    if(!d->driver)
        return false;
#if MYSQL_VERSION_ID >= 40100
    setAt(-1);
    setActive(false);

    if (d->result && isSelect())
        g_stMySQLFuncs.m_funMySQLFreeResult(d->result);
    d->result = 0;
    setSelect(false);

    for (int i = 0; i < d->fields.count(); ++i)
        delete[] d->fields[i].outField;
    d->fields.clear();

    int status = g_stMySQLFuncs.m_funMySQLNextResult(d->driver->d_func()->mysql);
    if (status > 0) {
        setLastError(qMakeError(QCoreApplication::translate("QMYSQLResult", "Unable to execute next query"),
                     QSqlError::StatementError, d->driver->d_func()));
        return false;
    } else if (status == -1) {
        return false;   // No more result sets
    }

    d->result = g_stMySQLFuncs.m_funMySQLStoreResult(d->driver->d_func()->mysql);
    int numFields = g_stMySQLFuncs.m_funMySQLFieldCount(d->driver->d_func()->mysql);
    if (!d->result && numFields > 0) {
        setLastError(qMakeError(QCoreApplication::translate("QMYSQLResult", "Unable to store next result"),
                     QSqlError::StatementError, d->driver->d_func()));
        return false;
    }

    setSelect(numFields > 0);
    d->fields.resize(numFields);
    d->rowsAffected = g_stMySQLFuncs.m_funMySQLAffectedRows(d->driver->d_func()->mysql);

    if (isSelect()) {
        for (int i = 0; i < numFields; i++) {
            MYSQL_FIELD* field = g_stMySQLFuncs.m_funMySQLFetchFieldDirect(d->result, i);
            d->fields[i].type = qDecodeMYSQLType(field->type, field->flags);
        }
    }

    setActive(true);
    return true;
#else
    return false;
#endif
}

void QMYSQLResult::virtual_hook(int id, void *data)
{
    QSqlResult::virtual_hook(id, data);
}

// ------------------------------------------------------------------------
// Prepared statement implementation (originally under #if MYSQL_VERSION_ID >= 40108)

#if MYSQL_VERSION_ID >= 40108

static MYSQL_TIME *toMySqlDate(QDate date, QTime time, QVariant::Type type)
{
    Q_ASSERT(type == QVariant::Time || type == QVariant::Date
             || type == QVariant::DateTime);

    MYSQL_TIME *myTime = new MYSQL_TIME;
    memset(myTime, 0, sizeof(MYSQL_TIME));

    if (type == QVariant::Time || type == QVariant::DateTime) {
        myTime->hour = time.hour();
        myTime->minute = time.minute();
        myTime->second = time.second();
        myTime->second_part = time.msec() * 1000;
    }
    if (type == QVariant::Date || type == QVariant::DateTime) {
        myTime->year = date.year();
        myTime->month = date.month();
        myTime->day = date.day();
    }

    return myTime;
}

bool QMYSQLResult::prepare(const QString& query)
{
    if(!d->driver)
        return false;
#if MYSQL_VERSION_ID >= 40108
    cleanup();
    if (!d->driver->d_func()->preparedQuerysEnabled || !g_stMySQLFuncs.m_funMySQLStmtInit)
        return QSqlResult::prepare(query);

    int r;

    if (query.isEmpty())
        return false;

    if (!d->stmt)
        d->stmt = g_stMySQLFuncs.m_funMySQLStmtInit(d->driver->d_func()->mysql);
    if (!d->stmt) {
        setLastError(qMakeError(QCoreApplication::translate("QMYSQLResult", "Unable to prepare statement"),
                     QSqlError::StatementError, d->driver->d_func()));
        return false;
    }

    const QByteArray encQuery(fromUnicode(d->driver->d_func()->tc, query));
    r = g_stMySQLFuncs.m_funMySQLStmtPrepare(d->stmt, encQuery.constData(), encQuery.length());
    if (r != 0) {
        setLastError(qMakeStmtError(QCoreApplication::translate("QMYSQLResult",
                     "Unable to prepare statement"), QSqlError::StatementError, d->stmt));
        cleanup();
        return false;
    }

    if (g_stMySQLFuncs.m_funMySQLStmtParamCount(d->stmt) > 0) {
        d->outBinds = new MYSQL_BIND[g_stMySQLFuncs.m_funMySQLStmtParamCount(d->stmt)];
    }

    setSelect(d->bindInValues());
    d->preparedQuery = true;
    return true;
#else
    return false;
#endif
}

bool QMYSQLResult::exec()
{
    if (!d->driver)
        return false;
    if (!d->preparedQuery)
        return QSqlResult::exec();
    if (!d->stmt)
        return false;

    int r = 0;
    MYSQL_BIND* currBind;
    QVector<MYSQL_TIME *> timeVector;
    QVector<QByteArray> stringVector;
    QVector<my_bool> nullVector;

    const QVector<QVariant> values = boundValues();

    r = g_stMySQLFuncs.m_funMySQLStmtReset(d->stmt);
    if (r != 0) {
        setLastError(qMakeStmtError(QCoreApplication::translate("QMYSQLResult",
                     "Unable to reset statement"), QSqlError::StatementError, d->stmt));
        return false;
    }

    if (g_stMySQLFuncs.m_funMySQLStmtParamCount(d->stmt) > 0 &&
        g_stMySQLFuncs.m_funMySQLStmtParamCount(d->stmt) == (uint)values.count()) {

        nullVector.resize(values.count());
        for (int i = 0; i < values.count(); ++i) {
            const QVariant &val = boundValues().at(i);
            void *data = const_cast<void *>(val.constData());

            currBind = &d->outBinds[i];

            nullVector[i] = static_cast<my_bool>(val.isNull());
            currBind->is_null = &nullVector[i];
            currBind->length = 0;
            currBind->is_unsigned = 0;

            switch (val.type()) {
                case QVariant::ByteArray:
                    currBind->buffer_type = MYSQL_TYPE_BLOB;
                    currBind->buffer = const_cast<char *>(val.toByteArray().constData());
                    currBind->buffer_length = val.toByteArray().size();
                    break;

                case QVariant::Time:
                case QVariant::Date:
                case QVariant::DateTime: {
                    MYSQL_TIME *myTime = toMySqlDate(val.toDate(), val.toTime(), val.type());
                    timeVector.append(myTime);

                    currBind->buffer = myTime;
                    switch(val.type()) {
                    case QVariant::Time:
                        currBind->buffer_type = MYSQL_TYPE_TIME;
                        myTime->time_type = MYSQL_TIMESTAMP_TIME;
                        break;
                    case QVariant::Date:
                        currBind->buffer_type = MYSQL_TYPE_DATE;
                        myTime->time_type = MYSQL_TIMESTAMP_DATE;
                        break;
                    case QVariant::DateTime:
                        currBind->buffer_type = MYSQL_TYPE_DATETIME;
                        myTime->time_type = MYSQL_TIMESTAMP_DATETIME;
                        break;
                    default:
                        break;
                    }
                    currBind->buffer_length = sizeof(MYSQL_TIME);
                    currBind->length = 0;
                    break; }
                case QVariant::UInt:
                case QVariant::Int:
                    currBind->buffer_type = MYSQL_TYPE_LONG;
                    currBind->buffer = data;
                    currBind->buffer_length = sizeof(int);
                    currBind->is_unsigned = (val.type() != QVariant::Int);
                break;
                case QVariant::Bool:
                    currBind->buffer_type = MYSQL_TYPE_TINY;
                    currBind->buffer = data;
                    currBind->buffer_length = sizeof(bool);
                    currBind->is_unsigned = false;
                    break;
                case QVariant::Double:
                    currBind->buffer_type = MYSQL_TYPE_DOUBLE;
                    currBind->buffer = data;
                    currBind->buffer_length = sizeof(double);
                    break;
                case QVariant::LongLong:
                case QVariant::ULongLong:
                    currBind->buffer_type = MYSQL_TYPE_LONGLONG;
                    currBind->buffer = data;
                    currBind->buffer_length = sizeof(qint64);
                    currBind->is_unsigned = (val.type() == QVariant::ULongLong);
                    break;
                case QVariant::String:
                default: {
                    QByteArray ba = fromUnicode(d->driver->d_func()->tc, val.toString());
                    stringVector.append(ba);
                    currBind->buffer_type = MYSQL_TYPE_STRING;
                    currBind->buffer = const_cast<char *>(ba.constData());
                    currBind->buffer_length = ba.length();
                    break; }
            }
        }

        r = g_stMySQLFuncs.m_funMySQLStmtBindParam(d->stmt, d->outBinds);
        if (r != 0) {
            setLastError(qMakeStmtError(QCoreApplication::translate("QMYSQLResult",
                         "Unable to bind value"), QSqlError::StatementError, d->stmt));
            qDeleteAll(timeVector);
            return false;
        }
    }
    r = g_stMySQLFuncs.m_funMySQLStmtExecute(d->stmt);

    qDeleteAll(timeVector);

    if (r != 0) {
        setLastError(qMakeStmtError(QCoreApplication::translate("QMYSQLResult",
                     "Unable to execute statement"), QSqlError::StatementError, d->stmt));
        return false;
    }
    //if there is meta-data there is also data
    setSelect(d->meta);

    d->rowsAffected = g_stMySQLFuncs.m_funMySQLStmtAffectedRows(d->stmt);

    if (isSelect()) {
        my_bool update_max_length = true;

        r = g_stMySQLFuncs.m_funMySQLStmtBindResult(d->stmt, d->inBinds);
        if (r != 0) {
            setLastError(qMakeStmtError(QCoreApplication::translate("QMYSQLResult",
                         "Unable to bind outvalues"), QSqlError::StatementError, d->stmt));
            return false;
        }
        if (d->hasBlobs && g_stMySQLFuncs.m_funMySQLStmtAttrSet)
            g_stMySQLFuncs.m_funMySQLStmtAttrSet(d->stmt, STMT_ATTR_UPDATE_MAX_LENGTH, &update_max_length);

        r = g_stMySQLFuncs.m_funMySQLStmtStoreResult(d->stmt);
        if (r != 0) {
            setLastError(qMakeStmtError(QCoreApplication::translate("QMYSQLResult",
                         "Unable to store statement results"), QSqlError::StatementError, d->stmt));
            return false;
        }

        if (d->hasBlobs) {
            // mysql_stmt_store_result() with STMT_ATTR_UPDATE_MAX_LENGTH set to true crashes
            // when called without a preceding call to mysql_stmt_bind_result()
            // in versions < 4.1.8
            d->bindBlobs();
            r = g_stMySQLFuncs.m_funMySQLStmtBindResult(d->stmt, d->inBinds);
            if (r != 0) {
                setLastError(qMakeStmtError(QCoreApplication::translate("QMYSQLResult",
                             "Unable to bind outvalues"), QSqlError::StatementError, d->stmt));
                return false;
            }
        }
        setAt(QSql::BeforeFirstRow);
    }
    setActive(true);
    return true;
}
#endif
/////////////////////////////////////////////////////////

static int qMySqlConnectionCount = 0;
static bool qMySqlInitHandledByUser = false;

static void qLibraryInit()
{
#ifndef Q_NO_MYSQL_EMBEDDED
# if MYSQL_VERSION_ID >= 40000
    if (qMySqlInitHandledByUser || qMySqlConnectionCount > 1)
        return;

# if (MYSQL_VERSION_ID >= 40110 && MYSQL_VERSION_ID < 50000) || MYSQL_VERSION_ID >= 50003
    if (mysql_library_init(0, 0, 0)) {
# else
    if (mysql_server_init(0, 0, 0)) {
    if (g_stMySQLFuncs.m_funMySQLServerInit && !qMySqlInitHandledByUser) {
        if (g_stMySQLFuncs.m_funMySQLServerInit(0, 0, 0)) {
            qWarning("QMYSQLDriver::qServerInit: unable to start server.");
        }
    }
#endif
    if (!LoadMySQLibrary()) {
        qWarning("QMYSQLDriver: Failed to load MySQL client library.");
    }
    qMySqlConnectionCount++;
# endif // MYSQL_VERSION_ID
#endif // Q_NO_MYSQL_EMBEDDED
}

static void qLibraryEnd()
{
    qMySqlConnectionCount--;
#ifndef Q_NO_MYSQL_EMBEDDED
    if (!qMySqlInitHandledByUser && qMySqlConnectionCount == 0) {
        if (g_stMySQLFuncs.m_funMySQLServerEnd)
            g_stMySQLFuncs.m_funMySQLServerEnd();
    }
#endif
    if (qMySqlConnectionCount == 0) {
    UnloadMySQLibrary();
    }
}

QMYSQLDriver::QMYSQLDriver(QObject * parent)
    : QSqlDriver(*new QMYSQLDriverPrivate, parent)
{
    init();
    qLibraryInit();
}

/*!
    Create a driver instance with the open connection handle, \a con.
    The instance's parent (owner) is \a parent.
*/

QMYSQLDriver::QMYSQLDriver(MYSQL * con, QObject * parent)
    : QSqlDriver(*new QMYSQLDriverPrivate, parent)
{
    Q_D(QMYSQLDriver);
    init();
    if (con) {
        d->mysql = (MYSQL *) con;
#ifndef QT_NO_TEXTCODEC
        d->tc = codec(con);
#endif
        setOpen(true);
        setOpenError(false);
        if (qMySqlConnectionCount == 1)
            qMySqlInitHandledByUser = true;
    } else {
        qLibraryInit();
    }
}

void QMYSQLDriver::init()
{
    Q_D(QMYSQLDriver);
    d->mysql = 0;
    qMySqlConnectionCount++;
}

QMYSQLDriver::~QMYSQLDriver()
{
    qMySqlConnectionCount--;
    if (qMySqlConnectionCount == 0 && !qMySqlInitHandledByUser)
        qLibraryEnd();
}

bool QMYSQLDriver::hasFeature(DriverFeature f) const
{
    Q_D(const QMYSQLDriver);
    switch (f) {
    case Transactions:
// CLIENT_TRANSACTION should be defined in all recent mysql client libs > 3.23.34
#ifdef CLIENT_TRANSACTIONS
        if (d->mysql) {
            if ((d->mysql->server_capabilities & CLIENT_TRANSACTIONS) == CLIENT_TRANSACTIONS)
                return true;
        }
#endif
        return false;
    case NamedPlaceholders:
    case BatchOperations:
    case SimpleLocking:
    case EventNotifications:
    case FinishQuery:
    case CancelQuery:
        return false;
    case QuerySize:
    case BLOB:
    case LastInsertId:
    case Unicode:
    case LowPrecisionNumbers:
        return true;
    case PreparedQueries:
    case PositionalPlaceholders:
#if MYSQL_VERSION_ID >= 40108
        return d->preparedQuerysEnabled && g_stMySQLFuncs.m_funMySQLStmtInit != 0;
#else
        return false;
#endif
    case MultipleResultSets:
#if MYSQL_VERSION_ID >= 40100
        return true;
#else
        return false;
#endif
    }
    return false;
}

static void setOptionFlag(uint &optionFlags, const QString &opt)
{
    if (opt == QLatin1String("CLIENT_COMPRESS"))
        optionFlags |= CLIENT_COMPRESS;
    else if (opt == QLatin1String("CLIENT_FOUND_ROWS"))
        optionFlags |= CLIENT_FOUND_ROWS;
    else if (opt == QLatin1String("CLIENT_IGNORE_SPACE"))
        optionFlags |= CLIENT_IGNORE_SPACE;
    else if (opt == QLatin1String("CLIENT_INTERACTIVE"))
        optionFlags |= CLIENT_INTERACTIVE;
    else if (opt == QLatin1String("CLIENT_NO_SCHEMA"))
        optionFlags |= CLIENT_NO_SCHEMA;
    else if (opt == QLatin1String("CLIENT_ODBC"))
        optionFlags |= CLIENT_ODBC;
    else if (opt == QLatin1String("CLIENT_SSL"))
        qWarning("QMYSQLDriver: SSL_KEY, SSL_CERT and SSL_CA should be used instead of CLIENT_SSL.");
    else
        qWarning("QMYSQLDriver::open: Unknown connect option '%s'", opt.toLocal8Bit().constData());
}

bool QMYSQLDriver::open(const QString& db,
                         const QString& user,
                         const QString& password,
                         const QString& host,
                         int port,
                         const QString& connOpts)
{
    Q_D(QMYSQLDriver);
    if (isOpen())
        close();

    /* This is a hack to get MySQL's stored procedure support working.
       Since a stored procedure _may_ return multiple result sets,
       we have to enable CLIEN_MULTI_STATEMENTS here, otherwise _any_
       stored procedure call will fail.
    */
    unsigned int optionFlags = Q_CLIENT_MULTI_STATEMENTS;
    const QStringList opts(connOpts.split(QLatin1Char(';'), QString::SkipEmptyParts));
    QString unixSocket;
    QString sslCert;
    QString sslCA;
    QString sslKey;
    QString sslCAPath;
    QString sslCipher;
#if MYSQL_VERSION_ID >= 50000
    my_bool reconnect=false;
    uint connectTimeout = 0;
    uint readTimeout = 0;
    uint writeTimeout = 0;
#endif

    // extract the real options from the string
    for (int i = 0; i < opts.count(); ++i) {
        QString tmp(opts.at(i).simplified());
        int idx;
        if ((idx = tmp.indexOf(QLatin1Char('='))) != -1) {
            QString val = tmp.mid(idx + 1).simplified();
            QString opt = tmp.left(idx).simplified();
            if (opt == QLatin1String("UNIX_SOCKET"))
                unixSocket = val;
#if MYSQL_VERSION_ID >= 50000
            else if (opt == QLatin1String("MYSQL_OPT_RECONNECT")) {
                if (val == QLatin1String("TRUE") || val == QLatin1String("1") || val.isEmpty())
                    reconnect = true;
            } else if (opt == QLatin1String("MYSQL_OPT_CONNECT_TIMEOUT")) {
                connectTimeout = val.toInt();
            } else if (opt == QLatin1String("MYSQL_OPT_READ_TIMEOUT")) {
                readTimeout = val.toInt();
            } else if (opt == QLatin1String("MYSQL_OPT_WRITE_TIMEOUT")) {
                writeTimeout = val.toInt();
            }
#endif
            else if (opt == QLatin1String("SSL_KEY"))
                sslKey = val;
            else if (opt == QLatin1String("SSL_CERT"))
                sslCert = val;
            else if (opt == QLatin1String("SSL_CA"))
                sslCA = val;
            else if (opt == QLatin1String("SSL_CAPATH"))
                sslCAPath = val;
            else if (opt == QLatin1String("SSL_CIPHER"))
                sslCipher = val;
            else if (val == QLatin1String("TRUE") || val == QLatin1String("1"))
                setOptionFlag(optionFlags, tmp.left(idx).simplified());
            else
                qWarning("QMYSQLDriver::open: Illegal connect option value '%s'",
                         tmp.toLocal8Bit().constData());
        } else {
            setOptionFlag(optionFlags, tmp);
        }
    }

    if (!g_stMySQLFuncs.m_funMySQLInit) {
        setLastError(QSqlError(QLatin1String("QMYSQL: MySQL library not loaded"),
                               QString(), QSqlError::ConnectionError));
        setOpenError(true);
        return false;
    }

    if (!(d->mysql = g_stMySQLFuncs.m_funMySQLInit((MYSQL*) 0))) {
        setLastError(qMakeError(tr("Unable to allocate a MYSQL object"),
                     QSqlError::ConnectionError, d));
        setOpenError(true);
        return false;
    }

    if (!sslKey.isNull() || !sslCert.isNull() || !sslCA.isNull() ||
        !sslCAPath.isNull() || !sslCipher.isNull()) {
       g_stMySQLFuncs.m_funMySQLSslSet(d->mysql,
                        sslKey.isNull() ? static_cast<const char *>(0)
                                        : QFile::encodeName(sslKey).constData(),
                        sslCert.isNull() ? static_cast<const char *>(0)
                                         : QFile::encodeName(sslCert).constData(),
                        sslCA.isNull() ? static_cast<const char *>(0)
                                       : QFile::encodeName(sslCA).constData(),
                        sslCAPath.isNull() ? static_cast<const char *>(0)
                                           : QFile::encodeName(sslCAPath).constData(),
                        sslCipher.isNull() ? static_cast<const char *>(0)
                                           : sslCipher.toLocal8Bit().constData());
    }

#if MYSQL_VERSION_ID >= 50100
    if (connectTimeout != 0 && g_stMySQLFuncs.m_funMySQLOptions)
        g_stMySQLFuncs.m_funMySQLOptions(d->mysql, MYSQL_OPT_CONNECT_TIMEOUT, &connectTimeout);
    if (readTimeout != 0 && g_stMySQLFuncs.m_funMySQLOptions)
        g_stMySQLFuncs.m_funMySQLOptions(d->mysql, MYSQL_OPT_READ_TIMEOUT, &readTimeout);
    if (writeTimeout != 0 && g_stMySQLFuncs.m_funMySQLOptions)
        g_stMySQLFuncs.m_funMySQLOptions(d->mysql, MYSQL_OPT_WRITE_TIMEOUT, &writeTimeout);

#endif
    MYSQL *mysql = g_stMySQLFuncs.m_funMySQLRealConnect(d->mysql,
                                      host.isNull() ? static_cast<const char *>(0)
                                                    : host.toLocal8Bit().constData(),
                                      user.isNull() ? static_cast<const char *>(0)
                                                    : user.toLocal8Bit().constData(),
                                      password.isNull() ? static_cast<const char *>(0)
                                                        : password.toLocal8Bit().constData(),
                                      db.isNull() ? static_cast<const char *>(0)
                                                  : db.toLocal8Bit().constData(),
                                      (port > -1) ? port : 0,
                                      unixSocket.isNull() ? static_cast<const char *>(0)
                                                          : unixSocket.toLocal8Bit().constData(),
                                      optionFlags);

    if (mysql == d->mysql) {
        if (!db.isEmpty() && g_stMySQLFuncs.m_funMySQLSelectDb(d->mysql, db.toLocal8Bit().constData())) {
            setLastError(qMakeError(tr("Unable to open database '%1'").arg(db), QSqlError::ConnectionError, d));
            g_stMySQLFuncs.m_funMySQLClose(d->mysql);
            setOpenError(true);
            return false;
        }
        if (reconnect && g_stMySQLFuncs.m_funMySQLOptions)
#if MYSQL_VERSION_ID >= 50100
        if (reconnect)
            g_stMySQLFuncs.m_funMySQLOptions(d->mysql, MYSQL_OPT_RECONNECT, &reconnect);
#endif
    } else {
        setLastError(qMakeError(tr("Unable to connect"),
                     QSqlError::ConnectionError, d));
        g_stMySQLFuncs.m_funMySQLClose(d->mysql);
        d->mysql = NULL;
        setOpenError(true);
        return false;
    }

#if (MYSQL_VERSION_ID >= 40113 && MYSQL_VERSION_ID < 50000) || MYSQL_VERSION_ID >= 50007
    // force the communication to be utf8
    if (g_stMySQLFuncs.m_funMySQLSetCharacterSet)
        g_stMySQLFuncs.m_funMySQLSetCharacterSet(d->mysql, "utf8");

#endif
#ifndef QT_NO_TEXTCODEC
    d->tc = codec(d->mysql);
#endif

    d->preparedQuerysEnabled = (g_stMySQLFuncs.m_funMySQLStmtInit != 0);

#ifndef QT_NO_THREAD
    if (g_stMySQLFuncs.m_funMySQLThreadInit)
        g_stMySQLFuncs.m_funMySQLThreadInit();
#endif

    setOpen(true);
    setOpenError(false);
    return true;
}

void QMYSQLDriver::close()
{
    Q_D(QMYSQLDriver);
    if (isOpen()) {
#ifndef QT_NO_THREAD
        if (g_stMySQLFuncs.m_funMySQLThreadEnd)
#endif
            g_stMySQLFuncs.m_funMySQLThreadEnd();
        g_stMySQLFuncs.m_funMySQLClose(d->mysql);
        d->mysql = NULL;
        setOpen(false);
        setOpenError(false);
    }
}

QSqlResult *QMYSQLDriver::createResult() const
{
    return new QMYSQLResult(this);
}

QStringList QMYSQLDriver::tables(QSql::TableType type) const
{
    Q_D(const QMYSQLDriver);
    QStringList tl;
#if MYSQL_VERSION_ID >= 40100
    if(g_stMySQLFuncs.m_funMySQLGetServerVersion(d->mysql) < 50000)
    {
#endif
    if (!isOpen())
        return tl;
    if (!(type & QSql::Tables))
        return tl;

    MYSQL_RES* tableRes = g_stMySQLFuncs.m_funMySQLListTables(d->mysql, NULL);
    MYSQL_ROW row;
    int i = 0;
    while (tableRes) {
        g_stMySQLFuncs.m_funMySQLDataSeek(tableRes, i);
        row = g_stMySQLFuncs.m_funMySQLFetchRow(tableRes);
        if (!row)
            break;
        tl.append(toUnicode(d->tc, row[0]));
        i++;
    }
    g_stMySQLFuncs.m_funMySQLFreeResult(tableRes);
#if MYSQL_VERSION_ID >= 40100
    } else {
        QSqlQuery q(createResult());
        if(type & QSql::Tables) {
            QString sql = QLatin1String("select table_name from information_schema.tables where table_schema = '") + QLatin1String(d->mysql->db) + QLatin1String("' and table_type = 'BASE TABLE'");
            q.exec(sql);

            while(q.next())
                tl.append(q.value(0).toString());
        }
        if(type & QSql::Views) {
            QString sql = QLatin1String("select table_name from information_schema.tables where table_schema = '") + QLatin1String(d->mysql->db) + QLatin1String("' and table_type = 'VIEW'");
            q.exec(sql);

            while(q.next())
                tl.append(q.value(0).toString());
        }
    }
#endif
    return tl;
}

QSqlIndex QMYSQLDriver::primaryIndex(const QString& tablename) const
{
    QSqlIndex idx;
    if (!isOpen())
        return idx;

    QSqlQuery i(createResult());
    QString stmt(QLatin1String("show index from %1;"));
    QSqlRecord fil = record(tablename);
    i.exec(stmt.arg(tablename));
    while (i.isActive() && i.next()) {
        if (i.value(2).toString() == QLatin1String("PRIMARY")) {
            idx.append(fil.field(i.value(4).toString()));
            idx.setCursorName(i.value(0).toString());
            idx.setName(i.value(2).toString());
        }
    }

    return idx;
}

QSqlRecord QMYSQLDriver::record(const QString& tablename) const
{
    Q_D(const QMYSQLDriver);
    QString table=tablename;
    if(isIdentifierEscaped(table, QSqlDriver::TableName))
        table = stripDelimiters(table, QSqlDriver::TableName);

    QSqlRecord info;
    if (!isOpen())
        return info;
    MYSQL_RES* r = g_stMySQLFuncs.m_funMySQLListFields(d->mysql, table.toLocal8Bit().constData(), 0);
    if (!r) {
        return info;
    }
    MYSQL_FIELD* field;

    while ((field = g_stMySQLFuncs.m_funMySQLFetchField(r)))
        info.append(qToField(field, d->tc));
    g_stMySQLFuncs.m_funMySQLFreeResult(r);
    return info;
}

QVariant QMYSQLDriver::handle() const
{
    Q_D(const QMYSQLDriver);
    return QVariant::fromValue(d->mysql);
}

bool QMYSQLDriver::beginTransaction()
{
    Q_D(QMYSQLDriver);
#ifndef CLIENT_TRANSACTIONS
    return false;
#endif
    if (!isOpen()) {
        qWarning("QMYSQLDriver::beginTransaction: Database not open");
        return false;
    }
    if (g_stMySQLFuncs.m_funMySQLQuery(d->mysql, "BEGIN WORK")) {
        setLastError(qMakeError(tr("Unable to begin transaction"),
                                QSqlError::StatementError, d));
        return false;
    }
    return true;
}

bool QMYSQLDriver::commitTransaction()
{
    Q_D(QMYSQLDriver);
#ifndef CLIENT_TRANSACTIONS
    return false;
#endif
    if (!isOpen()) {
        qWarning("QMYSQLDriver::commitTransaction: Database not open");
        return false;
    }
    if (g_stMySQLFuncs.m_funMySQLQuery(d->mysql, "COMMIT")) {
        setLastError(qMakeError(tr("Unable to commit transaction"),
                                QSqlError::StatementError, d));
        return false;
    }
    return true;
}

bool QMYSQLDriver::rollbackTransaction()
{
    Q_D(QMYSQLDriver);
#ifndef CLIENT_TRANSACTIONS
    return false;
#endif
    if (!isOpen()) {
        qWarning("QMYSQLDriver::rollbackTransaction: Database not open");
        return false;
    }
    if (g_stMySQLFuncs.m_funMySQLQuery(d->mysql, "ROLLBACK")) {
        setLastError(qMakeError(tr("Unable to rollback transaction"),
                                QSqlError::StatementError, d));
        return false;
    }
    return true;
}

QString QMYSQLDriver::formatValue(const QSqlField &field, bool trimStrings) const
{
    Q_D(const QMYSQLDriver);
    QString r;
    if (field.isNull()) {
        r = QStringLiteral("NULL");
    } else {
        switch(field.type()) {
        case QVariant::Double:
            r = QString::number(field.value().toDouble(), 'g', field.precision());
            break;
        case QVariant::String:
            // Escape '\' characters
            r = QSqlDriver::formatValue(field, trimStrings);
            r.replace(QLatin1String("\\"), QLatin1String("\\\\"));
            break;
        case QVariant::ByteArray:
            if (isOpen() && g_stMySQLFuncs.m_funMySQLRealEscapeString) {
                const QByteArray ba = field.value().toByteArray();
                // buffer has to be at least length*2+1 bytes
                char* buffer = new char[ba.size() * 2 + 1];
                int escapedSize = int(g_stMySQLFuncs.m_funMySQLRealEscapeString(d->mysql, buffer,
                                      ba.data(), ba.size()));
                r.reserve(escapedSize + 3);
                r.append(QLatin1Char('\'')).append(toUnicode(d->tc, buffer)).append(QLatin1Char('\''));
                delete[] buffer;
                break;
            } else {
                qWarning("QMYSQLDriver::formatValue: Database not open or mysql_real_escape_string unavailable");
            }
            // fall through
        default:
            r = QSqlDriver::formatValue(field, trimStrings);
        }
    }
    return r;
}

QString QMYSQLDriver::escapeIdentifier(const QString &identifier, IdentifierType) const
{
    QString res = identifier;
    if(!identifier.isEmpty() && !identifier.startsWith(QLatin1Char('`')) && !identifier.endsWith(QLatin1Char('`')) ) {
        res.prepend(QLatin1Char('`')).append(QLatin1Char('`'));
        res.replace(QLatin1Char('.'), QLatin1String("`.`"));
    }
    return res;
}

bool QMYSQLDriver::isIdentifierEscaped(const QString &identifier, IdentifierType type) const
{
    Q_UNUSED(type);
    return identifier.size() > 2
        && identifier.startsWith(QLatin1Char('`')) //left delimited
        && identifier.endsWith(QLatin1Char('`')); //right delimited
}

QT_END_NAMESPACE

#include "qsql_mysql.moc"

其他数据库驱动也类似这样进行修改。我做完mysql相关的修改之后,其他数据库的修改使用AI工具执行,我做代码审核。提示词大致如下:

请分析qsql_**.pri、qsql_**.h、qsql_**.cpp三个文件,执行以下下修改:

1. 去除所有-l

2. ** sql相关的库改用QLibrary加载、释放,库函数的调用改为使用函数指针,函数形参请以lib**.h(数据库驱动的原始主头文件)为准,切莫发挥创造

3. 加载与卸载库的地方加锁、计数,保证安全

4. typedef 函数指针,以fun为前缀

5. 使用一个static结构体对象管理函数指针,结构体成员变量以m_fun为前缀

6. 解析函数指针时,不使用宏

7. 函数、变量命名参考qsql_mysql.cpp(驼峰,不使用下划线)

8. 不使用c++11及更新的语法

9. 形成完整文件返回

考虑到sqlite的便利性,保留其使用静态库链接,即此后使用此环境编译的所有工程默认带有sqlite功能。

2.3 重新编译Qt插件的技巧

各种数据库驱动相关的代码与配置修改完成后,如果之前生成环境的影子文件夹还在,则不需要重新configure再完全nmake(这是一件非常耗时的工作)。经过一些调研和试验,总结技巧如下:

  • 使用Qt Creator,创建一个临时的Qt版本,选择使用影子文件夹\qtbase\bin\qmake.exe,此版本会提示-prefix构建未安装。

  • 创建一个临时Kit,选用上述临时Qt版本,并配好相关的编译器。

  • 使用上述Kit启动qt.pro。此处由于加载整个qt工程,可能需要花数分钟时间,具体取决于计算机性能;过程中可能发生类似“假死”的现象。
  • 找到这个plugins下的sqldrivers.pro,执行qmake,重新构建,即可在影子文件夹\qtbase\plugins\sqldrivers\生成新的库文件。

  • 将这些.lib文件拷贝到对应的Qt工作环境\plugins\sqldrivers\中替换原有文件即可。【注意:.prl文件不要替换】

3. 验证

使用更新后的环境重新编译之前的测试工程,其在windows XP可正常运行,且不依赖各种数据库驱动库文件。

 

二零二六年七月一日至五日 顾毅写于厦门