SQL/PSQL: Handle jsonb operators in prepared queries

Add an option to disable handling of positional binding so jsonb
operators are not screwed up

[ChangeLog][QtSql][QSqlQuery] Add setEnablePositionalBinding() to be
able to disable positional binding.

Fixes: QTBUG-96636
Change-Id: I428a9d3b10274b97292ab86a74d9b3971d6f10e9
Reviewed-by: Andy Shaw <andy.shaw@qt.io>
This commit is contained in:
Christian Ehrlicher 2023-02-26 20:32:21 +01:00
parent 4a7023f5b4
commit e532933a2a
7 changed files with 146 additions and 3 deletions

View File

@ -247,14 +247,14 @@ void QPSQLDriverPrivate::checkPendingNotifications() const
}
}
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;

View File

@ -1269,6 +1269,34 @@ QSql::NumericalPrecisionPolicy QSqlQuery::numericalPrecisionPolicy() const
return d->sqlResult->numericalPrecisionPolicy();
}
/*!
Enables or disables the positional binding for this query.
This is useful when the query contains a '?' which must not be handled
as a positional binding parameter but, for example, as a JSON operator
for a Postgres database.
This function will have no effect when the database has native
support for positional bindings with question marks.
\since 6.7
\sa positionalBindingEnabled()
*/
void QSqlQuery::enablePositionalBinding(bool enable)
{
d->sqlResult->enablePositionalBinding(enable);
}
/*!
Returns true when the positional binding is currently enabled.
\since 6.7
\sa enablePositionalBinding()
*/
bool QSqlQuery::positionalBindingEnabled() const
{
return d->sqlResult->positionalBindingEnabled();
}
/*!
\since 4.3.2

View File

@ -69,6 +69,9 @@ public:
void setNumericalPrecisionPolicy(QSql::NumericalPrecisionPolicy precisionPolicy);
QSql::NumericalPrecisionPolicy numericalPrecisionPolicy() const;
void enablePositionalBinding(bool enable);
bool positionalBindingEnabled() const;
bool seek(int i, bool relative = false);
bool next();
bool previous();

View File

@ -37,6 +37,9 @@ static bool qIsAlnum(QChar ch)
QString QSqlResultPrivate::positionalToNamedBinding(const QString &query) const
{
if (!positionalBindingEnabled)
return query;
const qsizetype n = query.size();
QString result;
@ -92,6 +95,7 @@ QString QSqlResultPrivate::namedToPositionalBinding(const QString &query)
int count = 0;
qsizetype i = 0;
bool ignoreBraces = (sqldriver->dbmsType() == QSqlDriver::PostgreSQL);
const bool qmarkNotationSupported = (sqldriver->dbmsType() != QSqlDriver::PostgreSQL);
while (i < n) {
QChar ch = query.at(i);
@ -115,10 +119,16 @@ QString QSqlResultPrivate::namedToPositionalBinding(const QString &query)
int pos = i + 2;
while (pos < n && qIsAlnum(query.at(pos)))
++pos;
// if question mark notation is not supported we have to use
// the native binding. fieldSerial() should be renamed
// to toNativeBinding() and used unconditionally here
if (qmarkNotationSupported)
result += u'?';
else
result += fieldSerial(count);
QString holder(query.mid(i, pos - i));
indexes[holder].append(count++);
holders.append(QHolder(holder, i));
result += u'?';
i = pos;
} else {
if (ch == u'\'' || ch == u'"' || ch == u'`')
@ -1004,6 +1014,23 @@ QSql::NumericalPrecisionPolicy QSqlResult::numericalPrecisionPolicy() const
return d->precisionPolicy;
}
/*! \internal
*/
void QSqlResult::enablePositionalBinding(bool enable)
{
Q_D(QSqlResult);
d->positionalBindingEnabled = enable;
}
/*! \internal
*/
bool QSqlResult::positionalBindingEnabled() const
{
Q_D(const QSqlResult);
return d->positionalBindingEnabled;
}
/*! \internal
*/
bool QSqlResult::nextResult()

View File

@ -102,6 +102,8 @@ protected:
virtual void detachFromResultSet();
virtual void setNumericalPrecisionPolicy(QSql::NumericalPrecisionPolicy policy);
QSql::NumericalPrecisionPolicy numericalPrecisionPolicy() const;
void enablePositionalBinding(bool enable);
bool positionalBindingEnabled() const;
virtual bool nextResult();
void resetBindCount(); // HACK

View File

@ -98,6 +98,7 @@ public:
bool active = false;
bool isSel = false;
bool forwardOnly = false;
bool positionalBindingEnabled = true;
static bool isVariantNull(const QVariant &variant);
};

View File

@ -77,6 +77,9 @@ private slots:
void psql_forwardOnlyQueryResultsLost_data() { generic_data("QPSQL"); }
void psql_forwardOnlyQueryResultsLost();
void positionalBindingEnabled_data() { generic_data(); }
void positionalBindingEnabled();
// Bug-specific tests:
void oci_nullBlob_data() { generic_data("QOCI"); }
void oci_nullBlob();
@ -254,6 +257,9 @@ private slots:
void ibaseTimeStampTzArray_data() { generic_data("QIBASE"); }
void ibaseTimeStampTzArray();
void psqlJsonOperator_data() { generic_data("QPSQL"); }
void psqlJsonOperator();
// Double addDatabase() with same name leaves system in a state that breaks
// invalidQuery() if run later; so put this one last !
void prematureExec_data() { generic_data(); }
@ -5048,5 +5054,81 @@ void tst_QSqlQuery::ibase_executeBlock()
QCOMPARE(qry.value(0).toInt(), 4);
}
void tst_QSqlQuery::positionalBindingEnabled()
{
QFETCH(QString, dbName);
QSqlDatabase db = QSqlDatabase::database(dbName);
QSqlDriver::DbmsType dbType = tst_Databases::getDatabaseType(db);
CHECK_DATABASE(db);
TableScope ts(db, "positionalBinding", __FILE__);
const QString &tableName = ts.tableName();
QSqlQuery qry(db);
QVERIFY_SQL(qry, exec("CREATE TABLE " + tableName + " (integer_col integer)"));
QVERIFY_SQL(qry, exec("INSERT INTO " + tableName + "(integer_col) VALUES(42)"));
qry.enablePositionalBinding(true);
QVERIFY_SQL(qry, prepare("SELECT integer_col FROM " + tableName + " WHERE integer_col = :integer_val"));
qry.bindValue(":integer_val", 42);
QVERIFY_SQL(qry, exec());
QVERIFY_SQL(qry, next());
QCOMPARE(qry.value(0).toInt(), 42);
QVERIFY_SQL(qry, prepare("SELECT integer_col FROM " + tableName + " WHERE integer_col = ?"));
qry.bindValue(0, 42);
QVERIFY_SQL(qry, exec());
QVERIFY_SQL(qry, next());
QCOMPARE(qry.value(0).toInt(), 42);
qry.enablePositionalBinding(false);
QVERIFY_SQL(qry, prepare("SELECT integer_col FROM " + tableName + " WHERE integer_col = :integer_val"));
qry.bindValue(":integer_val", 42);
QVERIFY_SQL(qry, exec());
QVERIFY_SQL(qry, next());
QCOMPARE(qry.value(0).toInt(), 42);
// the next query will only work when the underlying database support question mark notation natively
if (dbType == QSqlDriver::PostgreSQL) {
QVERIFY(!qry.prepare("SELECT integer_col FROM " + tableName + " WHERE integer_col = ?"));
qry.bindValue(0, 42);
QVERIFY(!qry.exec());
QVERIFY(!qry.next());
} else {
QVERIFY_SQL(qry, prepare("SELECT integer_col FROM " + tableName + " WHERE integer_col = ?"));
qry.bindValue(0, 42);
QVERIFY_SQL(qry, exec());
QVERIFY_SQL(qry, next());
QCOMPARE(qry.value(0).toInt(), 42);
}
}
void tst_QSqlQuery::psqlJsonOperator()
{
QFETCH(QString, dbName);
QSqlDatabase db = QSqlDatabase::database(dbName);
CHECK_DATABASE(db);
TableScope ts(db, "qTableName", __FILE__);
const QString &tableName = ts.tableName();
QSqlQuery qry(db);
qry.enablePositionalBinding(false); // don't allow / handle '?' as placeholder
QVERIFY_SQL(qry, exec("CREATE TABLE " + tableName + " (integer_col integer, json_col jsonb)"));
QVERIFY_SQL(qry, exec("INSERT INTO " + tableName + "(integer_col, json_col) VALUES(42, '{\"a\": [1, 2]}')"));
QVERIFY_SQL(qry, exec("INSERT INTO " + tableName + "(integer_col, json_col) VALUES(43, '{\"b\": [3, 4]}')"));
QVERIFY_SQL(qry, prepare("SELECT integer_col, json_col FROM " + tableName + " WHERE json_col @? '$.a[*] ? (@ == 1)' and integer_col = :int"));
qry.bindValue(":int", 42);
QVERIFY_SQL(qry, exec());
QVERIFY_SQL(qry, next());
QCOMPARE(qry.value(0).toInt(), 42);
QCOMPARE(qry.value(1).toByteArray(), "{\"a\": [1, 2]}");
QVERIFY_SQL(qry, prepare("SELECT integer_col, json_col FROM " + tableName + " WHERE json_col ? 'b' and integer_col = :int"));
qry.bindValue(":int", 43);
QVERIFY_SQL(qry, exec());
QVERIFY_SQL(qry, next());
QCOMPARE(qry.value(0).toInt(), 43);
QCOMPARE(qry.value(1).toByteArray(), "{\"b\": [3, 4]}");
}
QTEST_MAIN(tst_QSqlQuery)
#include "tst_qsqlquery.moc"