From ca09bc8d7a96b95d40bec9638f63467a24dfc0c2 Mon Sep 17 00:00:00 2001 From: Thierry Bastian Date: Tue, 17 Dec 2024 15:27:09 +0100 Subject: [PATCH] Add support for PostgreSQL prepared statements with pgBouncer Since 2023, pgBouncer supports prepared statements but only when prepared/executed/removed at the protocol level. So to support this, You need to call the appropriate functions from libpq. it does not change the behavior otherwise. You can note that to free statements, libpq only has the function in v17. Prior versions of postgresql will still use DEALLOCATE. Fixes: QTBUG-132303 Pick-to: 6.9 Change-Id: I2456820bbea318e1715ae46617bf4d137815ca54 Reviewed-by: Christian Ehrlicher --- src/plugins/sqldrivers/psql/qsql_psql.cpp | 109 +++++++++++++--------- src/plugins/sqldrivers/psql/qsql_psql_p.h | 3 + 2 files changed, 70 insertions(+), 42 deletions(-) diff --git a/src/plugins/sqldrivers/psql/qsql_psql.cpp b/src/plugins/sqldrivers/psql/qsql_psql.cpp index f0a730e614e..3d6027048e7 100644 --- a/src/plugins/sqldrivers/psql/qsql_psql.cpp +++ b/src/plugins/sqldrivers/psql/qsql_psql.cpp @@ -4,6 +4,7 @@ #include "qsql_psql_p.h" #include +#include #include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -386,8 +388,12 @@ static QMetaType qDecodePSQLType(int t) void QPSQLResultPrivate::deallocatePreparedStmt() { if (drv_d_func()) { +#if defined(LIBPQ_HAS_CLOSE_PREPARED) + PGresult *result = PQclosePrepared(drv_d_func()->connection, preparedStmtId.toUtf8()); +#else const QString stmt = QStringLiteral("DEALLOCATE ") + preparedStmtId; PGresult *result = drv_d_func()->exec(stmt); +#endif if (PQresultStatus(result) != PGRES_COMMAND_OK) { const QString msg = QString::fromUtf8(PQerrorMessage(drv_d_func()->connection)); @@ -820,22 +826,23 @@ void QPSQLResult::virtual_hook(int id, void *data) QSqlResult::virtual_hook(id, data); } -static QString qCreateParamString(const QList &boundValues, const QSqlDriver *driver) +static QList qCreateParamArray(const QList &boundValues, const QPSQLDriver *driver) { if (boundValues.isEmpty()) - return QString(); + return {}; - QString params; + QList params; + params.reserve(boundValues.size()); QSqlField f; for (const QVariant &val : boundValues) { - f.setMetaType(val.metaType()); - if (QSqlResultPrivate::isVariantNull(val)) - f.clear(); - else + QByteArray bval; + if (!QSqlResultPrivate::isVariantNull(val)) { + f.setMetaType(val.metaType()); f.setValue(val); - if (!params.isNull()) - params.append(", "_L1); - params.append(driver->formatValue(f)); + if (QString strval = driver->formatValue(f); !strval.isNull()) + bval = strval.toUtf8(); + } + params.append(bval); } return params; } @@ -859,9 +866,8 @@ bool QPSQLResult::prepare(const QString &query) d->deallocatePreparedStmt(); const QString stmtId = qMakePreparedStmtId(); - const QString stmt = QStringLiteral("PREPARE %1 AS ").arg(stmtId).append(d->positionalToNamedBinding(query)); - - PGresult *result = d->drv_d_func()->exec(stmt); + PGresult *result = PQprepare(d->drv_d_func()->connection, stmtId.toUtf8(), + d->positionalToNamedBinding(query).toUtf8(), 0, nullptr); if (PQresultStatus(result) != PGRES_COMMAND_OK) { setLastError(qMakeError(QCoreApplication::translate("QPSQLResult", @@ -884,24 +890,27 @@ bool QPSQLResult::exec() cleanup(); - QString stmt; - const QString params = qCreateParamString(boundValues(), driver()); - if (params.isEmpty()) - stmt = QStringLiteral("EXECUTE %1").arg(d->preparedStmtId); - else - stmt = QStringLiteral("EXECUTE %1 (%2)").arg(d->preparedStmtId, params); + const QList params = qCreateParamArray(boundValues(), static_cast(driver())); + QVarLengthArray pgParams; + pgParams.reserve(params.size()); + for (const QByteArray ¶m : params) + pgParams.emplace_back(param.constBegin()); - d->stmtId = d->drv_d_func()->sendQuery(stmt); - if (d->stmtId == InvalidStatementId) { + d->result = PQexecPrepared(d->drv_d_func()->connection, d->preparedStmtId.toUtf8(), pgParams.size(), + pgParams.data(), nullptr, nullptr, 0); + + const auto status = PQresultStatus(d->result); + if (status != PGRES_COMMAND_OK && status != PGRES_TUPLES_OK) { + d->stmtId = InvalidStatementId; setLastError(qMakeError(QCoreApplication::translate("QPSQLResult", - "Unable to send query"), QSqlError::StatementError, d->drv_d_func())); + "Unable to send query"), QSqlError::StatementError, d->drv_d_func(), d->result)); return false; } + d->stmtId = d->drv_d_func()->currentStmtId = d->drv_d_func()->generateStatementId(); if (isForwardOnly()) setForwardOnly(d->drv_d_func()->setSingleRowMode()); - d->result = d->drv_d_func()->getResult(d->stmtId); if (!isForwardOnly()) { // Fetch all result sets right away while (PGresult *nextResultSet = d->drv_d_func()->getResult(d->stmtId)) @@ -1440,19 +1449,34 @@ QSqlRecord QPSQLDriver::record(const QString &tablename) const return info; } -template + +template +inline QString autoQuoteResult(QAnyStringView str) +{ + return forPreparedStatement ? str.toString() : (u'\'' + str.toString() + u'\''); +} + +template inline void assignSpecialPsqlFloatValue(FloatType val, QString *target) { if (qIsNaN(val)) - *target = QStringLiteral("'NaN'"); + *target = autoQuoteResult(u"NaN"); else if (qIsInf(val)) - *target = (val < 0) ? QStringLiteral("'-Infinity'") : QStringLiteral("'Infinity'"); + *target = autoQuoteResult((val < 0) ? u"-Infinity" : u"Infinity"); } +QString QPSQLDriver::formatValue(const QSqlField &field, bool trimStrings) const +{ + return formatValue(field, trimStrings); +} + +template QString QPSQLDriver::formatValue(const QSqlField &field, bool trimStrings) const { Q_D(const QPSQLDriver); - const auto nullStr = [](){ return QStringLiteral("NULL"); }; + const auto nullStr = [](){ return forPreparedStatement ? + QString{} : QStringLiteral("NULL"); }; + QString r; if (field.isNull()) { r = nullStr(); @@ -1461,11 +1485,10 @@ QString QPSQLDriver::formatValue(const QSqlField &field, bool trimStrings) const case QMetaType::QDateTime: { const auto dt = field.value().toDateTime(); if (dt.isValid()) { - // even though the documentation (https://www.postgresql.org/docs/current/datatype-datetime.html) - // states that any time zone indication for 'timestamp without tz' columns will be ignored, - // it is stored as the correct utc timestamp - so we can pass the utc offset here - r = QStringLiteral("TIMESTAMP WITH TIME ZONE ") + u'\'' + - dt.toOffsetFromUtc(dt.offsetFromUtc()).toString(Qt::ISODateWithMs) + u'\''; + // the datetime needs to be in UTC format + // Anyway the DB stores it that way for timestamptz + // for timestamp (without tz), we store as UTC too and the server will ignore the tz info + r = autoQuoteResult(dt.toUTC().toString(Qt::ISODateWithMs)); } else { r = nullStr(); } @@ -1474,15 +1497,19 @@ QString QPSQLDriver::formatValue(const QSqlField &field, bool trimStrings) const case QMetaType::QTime: { const auto t = field.value().toTime(); if (t.isValid()) - r = u'\'' + QLocale::c().toString(t, u"hh:mm:ss.zzz") + u'\''; + r = autoQuoteResult(t.toString(Qt::ISODateWithMs)); else r = nullStr(); break; } case QMetaType::QString: - r = QSqlDriver::formatValue(field, trimStrings); - if (d->hasBackslashEscape) - r.replace(u'\\', "\\\\"_L1); + if (forPreparedStatement) { + r = field.value().toString(); + } else { + r = QSqlDriver::formatValue(field, trimStrings); + if (d->hasBackslashEscape) + r.replace(u'\\', "\\\\"_L1); + } break; case QMetaType::Bool: if (field.value().toBool()) @@ -1498,24 +1525,22 @@ QString QPSQLDriver::formatValue(const QSqlField &field, bool trimStrings) const #else unsigned char *data = PQescapeBytea((const unsigned char*)ba.constData(), ba.size(), &len); #endif - r += u'\''; - r += QLatin1StringView((const char*)data); - r += u'\''; + r = autoQuoteResult(QLatin1StringView((const char*)data)); qPQfreemem(data); break; } case QMetaType::Float: - assignSpecialPsqlFloatValue(field.value().toFloat(), &r); + assignSpecialPsqlFloatValue(field.value().toFloat(), &r); if (r.isEmpty()) r = QSqlDriver::formatValue(field, trimStrings); break; case QMetaType::Double: - assignSpecialPsqlFloatValue(field.value().toDouble(), &r); + assignSpecialPsqlFloatValue(field.value().toDouble(), &r); if (r.isEmpty()) r = QSqlDriver::formatValue(field, trimStrings); break; case QMetaType::QUuid: - r = u'\'' + field.value().toString() + u'\''; + r = autoQuoteResult(field.value().toString()); break; default: r = QSqlDriver::formatValue(field, trimStrings); diff --git a/src/plugins/sqldrivers/psql/qsql_psql_p.h b/src/plugins/sqldrivers/psql/qsql_psql_p.h index 4ff83bee21c..0439c26ccce 100644 --- a/src/plugins/sqldrivers/psql/qsql_psql_p.h +++ b/src/plugins/sqldrivers/psql/qsql_psql_p.h @@ -84,6 +84,9 @@ public: QString escapeIdentifier(const QString &identifier, IdentifierType type) const override; QString formatValue(const QSqlField &field, bool trimStrings) const override; + template + QString formatValue(const QSqlField &field, bool trimStrings = false) const; + bool subscribeToNotification(const QString &name) override; bool unsubscribeFromNotification(const QString &name) override; QStringList subscribedToNotifications() const override;