From 29fe7317286062ed250912172a984abbc1bf081d Mon Sep 17 00:00:00 2001 From: ispyisail Date: Sun, 27 Sep 2026 14:00:07 +1300 Subject: [PATCH] Fix read-only queries stopping after the first row Since #1046, QETSql::execReadOnly() runs a query with PRAGMA query_only set and switches it off before returning. Switching it off aborts a statement SQLite is still stepping through ("abort due to ROLLBACK"), and QSQLITE has already stepped to the first row by then. A query that produces its rows as it goes -- a UNION ALL without ORDER BY -- therefore came back with its first row only and no error. A sorted query was not affected, because SQLite has read every row before returning the first. Every query from the SQL box of a table, a saved query and the scripting qet.query() goes through here. The checked run is now finished before query_only is switched off, and a query that passed is run again for the caller. SQLite refuses a write at its first step, so passing that step is what proves a statement reads only; the second run is of a statement already shown to be read-only. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01G2d2Zi8BfrYRPX88zhaoFG --- sources/dataBase/sqlreadonly.cpp | 7 +++++++ tests/qttest/tst_sqlreadonly.cpp | 17 +++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/sources/dataBase/sqlreadonly.cpp b/sources/dataBase/sqlreadonly.cpp index 95996bde5..f61aabbea 100644 --- a/sources/dataBase/sqlreadonly.cpp +++ b/sources/dataBase/sqlreadonly.cpp @@ -86,9 +86,16 @@ QSqlQuery execReadOnly(const QSqlDatabase &db, const QString &query, QString *er QSqlQuery result(db); const bool ok = result.exec(query); + //Switching query_only off aborts a statement SQLite is still + //stepping through -- an unsorted UNION ALL then ends after its + //first row. So the checked run is finished first, and a statement + //that passed is run again for the caller: SQLite refuses a write + //at its first step, so passing that step proves it read-only. + result.finish(); QSqlQuery(db).exec(QStringLiteral("PRAGMA query_only = OFF")); if (ok) { + result.exec(query); return result; } diff --git a/tests/qttest/tst_sqlreadonly.cpp b/tests/qttest/tst_sqlreadonly.cpp index a96735fd9..7159e1c28 100644 --- a/tests/qttest/tst_sqlreadonly.cpp +++ b/tests/qttest/tst_sqlreadonly.cpp @@ -40,6 +40,7 @@ #include #include +#include #include class TstSqlReadOnly : public QObject @@ -63,6 +64,7 @@ class TstSqlReadOnly : public QObject void doesNotExecuteWhatItRefuses(); void refusedQueryCannotBeRunAgain(); void acceptedQueryRunAgainStillReads(); + void returnsEveryRowOfACompoundSelect(); void leavesTheConnectionWritable(); private: @@ -242,6 +244,21 @@ void TstSqlReadOnly::acceptedQueryRunAgainStillReads() QCOMPARE(n, 2); } +void TstSqlReadOnly::returnsEveryRowOfACompoundSelect() +{ + // Switching query_only off aborts a statement SQLite is still + // stepping through, and an unsorted UNION ALL is one: it came back + // with its first row only. + QSqlQuery q = QETSql::execReadOnly( + m_db, "SELECT uuid FROM element UNION ALL SELECT uuid FROM element"); + QStringList uuids; + while (q.next()) { + uuids << q.value(0).toString(); + } + QVERIFY(!q.lastError().isValid()); + QCOMPARE(uuids, QStringList({"a", "b", "a", "b"})); +} + void TstSqlReadOnly::leavesTheConnectionWritable() { // query_only must not outlive the call, whatever its outcome: the