diff options
-rw-r--r-- | src/plugins/sqldrivers/psql/qsql_psql.cpp | 304 | ||||
-rw-r--r-- | src/sql/doc/snippets/code/doc_src_sql-driver.cpp | 32 | ||||
-rw-r--r-- | src/sql/doc/snippets/code/doc_src_sql-driver.qdoc | 12 | ||||
-rw-r--r-- | src/sql/doc/src/sql-driver.qdoc | 39 | ||||
-rw-r--r-- | src/sql/kernel/qsqlfield.cpp | 6 | ||||
-rw-r--r-- | src/sql/kernel/qsqlquery.cpp | 8 | ||||
-rw-r--r-- | src/sql/kernel/qsqlresult.cpp | 12 | ||||
-rw-r--r-- | tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp | 209 |
8 files changed, 590 insertions, 32 deletions
diff --git a/src/plugins/sqldrivers/psql/qsql_psql.cpp b/src/plugins/sqldrivers/psql/qsql_psql.cpp index ab9aa7f176..6de7797c19 100644 --- a/src/plugins/sqldrivers/psql/qsql_psql.cpp +++ b/src/plugins/sqldrivers/psql/qsql_psql.cpp @@ -125,6 +125,14 @@ inline void qPQfreemem(void *buffer) PQfreemem(buffer); } +/* Missing declaration of PGRES_SINGLE_TUPLE for PSQL below 9.2 */ +#if !defined PG_VERSION_NUM || PG_VERSION_NUM-0 < 90200 +static const int PGRES_SINGLE_TUPLE = 9; +#endif + +typedef int StatementId; +static const StatementId InvalidStatementId = 0; + class QPSQLResultPrivate; class QPSQLResult: public QSqlResult @@ -143,6 +151,7 @@ protected: bool fetch(int i) override; bool fetchFirst() override; bool fetchLast() override; + bool fetchNext() override; QVariant data(int i) override; bool isNull(int field) override; bool reset (const QString &query) override; @@ -164,7 +173,9 @@ public: pro(QPSQLDriver::Version6), sn(0), pendingNotifyCheck(false), - hasBackslashEscape(false) + hasBackslashEscape(false), + stmtCount(0), + currentStmtId(InvalidStatementId) { dbmsType = QSqlDriver::PostgreSQL; } PGconn *connection; @@ -174,10 +185,19 @@ public: QStringList seid; mutable bool pendingNotifyCheck; bool hasBackslashEscape; + int stmtCount; + StatementId currentStmtId; void appendTables(QStringList &tl, QSqlQuery &t, QChar type); - PGresult * exec(const char * stmt) const; - PGresult * exec(const QString & stmt) const; + PGresult *exec(const char *stmt); + PGresult *exec(const QString &stmt); + StatementId sendQuery(const QString &stmt); + bool setSingleRowMode() const; + PGresult *getResult(StatementId stmtId) const; + void finishQuery(StatementId stmtId); + void discardResults() const; + StatementId generateStatementId(); + void checkPendingNotifications() const; QPSQLDriver::Protocol getPSQLVersion(); bool setEncodingUtf8(); void setDatestyle(); @@ -202,20 +222,89 @@ void QPSQLDriverPrivate::appendTables(QStringList &tl, QSqlQuery &t, QChar type) } } -PGresult * QPSQLDriverPrivate::exec(const char * stmt) const +PGresult *QPSQLDriverPrivate::exec(const char *stmt) { - Q_Q(const QPSQLDriver); + // PQexec() silently discards any prior query results that the application didn't eat. PGresult *result = PQexec(connection, stmt); - if (seid.size() && !pendingNotifyCheck) { - pendingNotifyCheck = true; - QMetaObject::invokeMethod(const_cast<QPSQLDriver*>(q), "_q_handleNotification", Qt::QueuedConnection, Q_ARG(int,0)); + currentStmtId = result ? generateStatementId() : InvalidStatementId; + checkPendingNotifications(); + return result; +} + +PGresult *QPSQLDriverPrivate::exec(const QString &stmt) +{ + return exec((isUtf8 ? stmt.toUtf8() : stmt.toLocal8Bit()).constData()); +} + +StatementId QPSQLDriverPrivate::sendQuery(const QString &stmt) +{ + // Discard any prior query results that the application didn't eat. + // This is required for PQsendQuery() + discardResults(); + const int result = PQsendQuery(connection, + (isUtf8 ? stmt.toUtf8() : stmt.toLocal8Bit()).constData()); + currentStmtId = result ? generateStatementId() : InvalidStatementId; + return currentStmtId; +} + +bool QPSQLDriverPrivate::setSingleRowMode() const +{ + // Activates single-row mode for last sent query, see: + // https://www.postgresql.org/docs/9.2/static/libpq-single-row-mode.html + // This method should be called immediately after the sendQuery() call. +#if defined PG_VERSION_NUM && PG_VERSION_NUM-0 >= 90200 + return PQsetSingleRowMode(connection) > 0; +#else + return false; +#endif +} + +PGresult *QPSQLDriverPrivate::getResult(StatementId stmtId) const +{ + // Make sure the results of stmtId weren't discaded. This might + // happen for forward-only queries if somebody executed another + // SQL query on the same db connection. + if (stmtId != currentStmtId) { + // If you change the following warning, remember to update it + // on sql-driver.html page too. + qWarning("QPSQLDriver::getResult: Query results lost - " + "probably discarded on executing another SQL query."); + return nullptr; } + PGresult *result = PQgetResult(connection); + checkPendingNotifications(); return result; } -PGresult * QPSQLDriverPrivate::exec(const QString & stmt) const +void QPSQLDriverPrivate::finishQuery(StatementId stmtId) +{ + if (stmtId != InvalidStatementId && stmtId == currentStmtId) { + discardResults(); + currentStmtId = InvalidStatementId; + } +} + +void QPSQLDriverPrivate::discardResults() const +{ + while (PGresult *result = PQgetResult(connection)) + PQclear(result); +} + +StatementId QPSQLDriverPrivate::generateStatementId() { - return exec(isUtf8 ? stmt.toUtf8().constData() : stmt.toLocal8Bit().constData()); + int stmtId = ++stmtCount; + if (stmtId <= 0) + stmtId = stmtCount = 1; + return stmtId; +} + +void QPSQLDriverPrivate::checkPendingNotifications() const +{ + Q_Q(const QPSQLDriver); + if (seid.size() && !pendingNotifyCheck) { + pendingNotifyCheck = true; + QMetaObject::invokeMethod(const_cast<QPSQLDriver*>(q), "_q_handleNotification", Qt::QueuedConnection, Q_ARG(int,0)); + } } class QPSQLResultPrivate : public QSqlResultPrivate @@ -227,6 +316,8 @@ public: : QSqlResultPrivate(q, drv), result(0), currentSize(-1), + canFetchMoreRows(false), + stmtId(InvalidStatementId), preparedQueriesEnabled(false) { } @@ -235,6 +326,8 @@ public: PGresult *result; int currentSize; + bool canFetchMoreRows; + StatementId stmtId; bool preparedQueriesEnabled; QString preparedStmtId; @@ -257,21 +350,45 @@ static QSqlError qMakeError(const QString& err, QSqlError::ErrorType type, bool QPSQLResultPrivate::processResults() { Q_Q(QPSQLResult); - if (!result) + if (!result) { + q->setSelect(false); + q->setActive(false); + currentSize = -1; + canFetchMoreRows = false; + if (stmtId != drv_d_func()->currentStmtId) { + q->setLastError(qMakeError(QCoreApplication::translate("QPSQLResult", + "Query results lost - probably discarded on executing " + "another SQL query."), QSqlError::StatementError, drv_d_func(), result)); + } return false; - + } int status = PQresultStatus(result); - if (status == PGRES_TUPLES_OK) { + switch (status) { + case PGRES_TUPLES_OK: q->setSelect(true); q->setActive(true); - currentSize = PQntuples(result); + currentSize = q->isForwardOnly() ? -1 : PQntuples(result); + canFetchMoreRows = false; return true; - } else if (status == PGRES_COMMAND_OK) { + case PGRES_SINGLE_TUPLE: + q->setSelect(true); + q->setActive(true); + currentSize = -1; + canFetchMoreRows = true; + return true; + case PGRES_COMMAND_OK: q->setSelect(false); q->setActive(true); currentSize = -1; + canFetchMoreRows = false; return true; + default: + break; } + q->setSelect(false); + q->setActive(false); + currentSize = -1; + canFetchMoreRows = false; q->setLastError(qMakeError(QCoreApplication::translate("QPSQLResult", "Unable to create query"), QSqlError::StatementError, drv_d_func(), result)); return false; @@ -361,9 +478,13 @@ void QPSQLResult::cleanup() Q_D(QPSQLResult); if (d->result) PQclear(d->result); - d->result = 0; + d->result = nullptr; + if (d->stmtId != InvalidStatementId) + d->drv_d_func()->finishQuery(d->stmtId); + d->stmtId = InvalidStatementId; setAt(QSql::BeforeFirstRow); d->currentSize = -1; + d->canFetchMoreRows = false; setActive(false); } @@ -374,23 +495,117 @@ bool QPSQLResult::fetch(int i) return false; if (i < 0) return false; - if (i >= d->currentSize) - return false; if (at() == i) return true; + + if (isForwardOnly()) { + if (i < at()) + return false; + bool ok = true; + while (ok && i > at()) + ok = fetchNext(); + return ok; + } + + if (i >= d->currentSize) + return false; setAt(i); return true; } bool QPSQLResult::fetchFirst() { + Q_D(const QPSQLResult); + if (!isActive()) + return false; + if (at() == 0) + return true; + + if (isForwardOnly()) { + if (at() == QSql::BeforeFirstRow) { + // First result has been already fetched by exec() or + // nextResult(), just check it has at least one row. + if (d->result && PQntuples(d->result) > 0) { + setAt(0); + return true; + } + } + return false; + } + return fetch(0); } bool QPSQLResult::fetchLast() { Q_D(const QPSQLResult); - return fetch(PQntuples(d->result) - 1); + if (!isActive()) + return false; + + if (isForwardOnly()) { + // Cannot seek to last row in forwardOnly mode, so we have to use brute force + int i = at(); + if (i == QSql::AfterLastRow) + return false; + if (i == QSql::BeforeFirstRow) + i = 0; + while (fetchNext()) + ++i; + setAt(i); + return true; + } + + return fetch(d->currentSize - 1); +} + +bool QPSQLResult::fetchNext() +{ + Q_D(QPSQLResult); + if (!isActive()) + return false; + + const int currentRow = at(); // Small optimalization + if (currentRow == QSql::BeforeFirstRow) + return fetchFirst(); + if (currentRow == QSql::AfterLastRow) + return false; + + if (isForwardOnly()) { + if (!d->canFetchMoreRows) + return false; + PQclear(d->result); + d->result = d->drv_d_func()->getResult(d->stmtId); + if (!d->result) { + setLastError(qMakeError(QCoreApplication::translate("QPSQLResult", + "Unable to get result"), QSqlError::StatementError, d->drv_d_func(), d->result)); + d->canFetchMoreRows = false; + return false; + } + int status = PQresultStatus(d->result); + switch (status) { + case PGRES_SINGLE_TUPLE: + // Fetched next row of current result set + Q_ASSERT(PQntuples(d->result) == 1); + Q_ASSERT(d->canFetchMoreRows); + setAt(currentRow + 1); + return true; + case PGRES_TUPLES_OK: + // In single-row mode PGRES_TUPLES_OK means end of current result set + Q_ASSERT(PQntuples(d->result) == 0); + d->canFetchMoreRows = false; + return false; + default: + setLastError(qMakeError(QCoreApplication::translate("QPSQLResult", + "Unable to get result"), QSqlError::StatementError, d->drv_d_func(), d->result)); + d->canFetchMoreRows = false; + return false; + } + } + + if (currentRow + 1 >= d->currentSize) + return false; + setAt(currentRow + 1); + return true; } QVariant QPSQLResult::data(int i) @@ -400,10 +615,11 @@ QVariant QPSQLResult::data(int i) qWarning("QPSQLResult::data: column %d out of range", i); return QVariant(); } + const int currentRow = isForwardOnly() ? 0 : at(); int ptype = PQftype(d->result, i); QVariant::Type type = qDecodePSQLType(ptype); - const char *val = PQgetvalue(d->result, at(), i); - if (PQgetisnull(d->result, at(), i)) + const char *val = PQgetvalue(d->result, currentRow, i); + if (PQgetisnull(d->result, currentRow, i)) return QVariant(type); switch (type) { case QVariant::Bool: @@ -488,8 +704,9 @@ QVariant QPSQLResult::data(int i) bool QPSQLResult::isNull(int field) { Q_D(const QPSQLResult); - PQgetvalue(d->result, at(), field); - return PQgetisnull(d->result, at(), field); + const int currentRow = isForwardOnly() ? 0 : at(); + PQgetvalue(d->result, currentRow, field); + return PQgetisnull(d->result, currentRow, field); } bool QPSQLResult::reset (const QString& query) @@ -500,7 +717,18 @@ bool QPSQLResult::reset (const QString& query) return false; if (!driver()->isOpen() || driver()->isOpenError()) return false; - d->result = d->drv_d_func()->exec(query); + + d->stmtId = d->drv_d_func()->sendQuery(query); + if (d->stmtId == InvalidStatementId) { + setLastError(qMakeError(QCoreApplication::translate("QPSQLResult", + "Unable to send query"), QSqlError::StatementError, d->drv_d_func())); + return false; + } + + if (isForwardOnly()) + setForwardOnly(d->drv_d_func()->setSingleRowMode()); + + d->result = d->drv_d_func()->getResult(d->stmtId); return d->processResults(); } @@ -546,10 +774,17 @@ QSqlRecord QPSQLResult::record() const f.setName(QString::fromUtf8(PQfname(d->result, i))); else f.setName(QString::fromLocal8Bit(PQfname(d->result, i))); - QSqlQuery qry(driver()->createResult()); - if (qry.exec(QStringLiteral("SELECT relname FROM pg_class WHERE pg_class.oid = %1") - .arg(PQftable(d->result, i))) && qry.next()) { - f.setTableName(qry.value(0).toString()); + + // WARNING: We cannot execute any other SQL queries on + // the same db connection while forward-only mode is active + // (this would discard all results of forward-only query). + // So we just skip this... + if (!isForwardOnly()) { + QSqlQuery qry(driver()->createResult()); + if (qry.exec(QStringLiteral("SELECT relname FROM pg_class WHERE pg_class.oid = %1") + .arg(PQftable(d->result, i))) && qry.next()) { + f.setTableName(qry.value(0).toString()); + } } int ptype = PQftype(d->result, i); f.setType(qDecodePSQLType(ptype)); @@ -668,8 +903,17 @@ bool QPSQLResult::exec() else stmt = QString::fromLatin1("EXECUTE %1 (%2)").arg(d->preparedStmtId, params); - d->result = d->drv_d_func()->exec(stmt); + d->stmtId = d->drv_d_func()->sendQuery(stmt); + if (d->stmtId == InvalidStatementId) { + setLastError(qMakeError(QCoreApplication::translate("QPSQLResult", + "Unable to send query"), QSqlError::StatementError, d->drv_d_func())); + return false; + } + if (isForwardOnly()) + setForwardOnly(d->drv_d_func()->setSingleRowMode()); + + d->result = d->drv_d_func()->getResult(d->stmtId); return d->processResults(); } @@ -996,7 +1240,7 @@ QSqlResult *QPSQLDriver::createResult() const bool QPSQLDriver::beginTransaction() { - Q_D(const QPSQLDriver); + Q_D(QPSQLDriver); if (!isOpen()) { qWarning("QPSQLDriver::beginTransaction: Database not open"); return false; diff --git a/src/sql/doc/snippets/code/doc_src_sql-driver.cpp b/src/sql/doc/snippets/code/doc_src_sql-driver.cpp index 7983386642..e7dfbd90d5 100644 --- a/src/sql/doc/snippets/code/doc_src_sql-driver.cpp +++ b/src/sql/doc/snippets/code/doc_src_sql-driver.cpp @@ -95,3 +95,35 @@ QSqlDatabase: available drivers: QMYSQL //! [34] column.contains(QRegularExpression("pattern")); //! [34] + + +//! [36] +QSqlQuery query(db); +query.setForwardOnly(true); +query.exec("SELECT * FROM table"); +while (query.next()) { + // Handle changes in every iteration of the loop + QVariant v = query.result()->handle(); + if (qstrcmp(v.typeName(), "PGresult*") == 0) { + PGresult *handle = *static_cast<PGresult **>(v.data()); + if (handle != 0) { + // Do something... + } + } +} +//! [36] + + +//! [37] +int value; +QSqlQuery query1(db); +query1.setForwardOnly(true); +query1.exec("select * FROM table1"); +while (query1.next()) { + value = query1.value(0).toInt(); + if (value == 1) { + QSqlQuery query2(db); + query2.exec("update table2 set col=2"); // WRONG: This will discard all results of + } // query1, and cause the loop to quit +} +//! [37] diff --git a/src/sql/doc/snippets/code/doc_src_sql-driver.qdoc b/src/sql/doc/snippets/code/doc_src_sql-driver.qdoc index 04ea30915d..d127bdf8a5 100644 --- a/src/sql/doc/snippets/code/doc_src_sql-driver.qdoc +++ b/src/sql/doc/snippets/code/doc_src_sql-driver.qdoc @@ -225,3 +225,15 @@ cd $QTDIR/qtbase/src/plugins/sqldrivers qmake -- OCI_INCDIR=/usr/include/oracle/10.1.0.3/client OCI_LIBDIR=/usr/lib/oracle/10.1.0.3/client/lib "OCI_LIBS=-Wl,-rpath,/usr/lib/oracle/10.1.0.3/client/lib -lclntsh -lnnz10" make sub-oci //! [33] + + +//! [35] +QSqlDatabase: QPSQL driver not loaded +QSqlDatabase: available drivers: QSQLITE QMYSQL QMYSQL3 QODBC QODBC3 QPSQL QPSQL7 +Could not create database object +//! [35] + + +//! [38] +QPSQLDriver::getResult: Query results lost - probably discarded on executing another SQL query. +//! [38] diff --git a/src/sql/doc/src/sql-driver.qdoc b/src/sql/doc/src/sql-driver.qdoc index 025cd43ef7..70205d5552 100644 --- a/src/sql/doc/src/sql-driver.qdoc +++ b/src/sql/doc/src/sql-driver.qdoc @@ -380,6 +380,45 @@ Binary Large Objects are supported through the \c BYTEA field type in PostgreSQL server versions >= 7.1. + \section3 QPSQL Forward-only query support + + To use forward-only queries, you must build the QPSQL plugin with + PostreSQL client library version 9.2 or later. If the plugin is + built with an older version, then forward-only mode will not be + available - calling QSqlQuery::setForwardOnly() with \c true will + have no effect. + + \warning If you build the QPSQL plugin with PostgreSQL version 9.2 or later, + then you must distribute your application with libpq version 9.2 or later. + Otherwise, loading the QPSQL plugin will fail with the following message: + + \snippet code/doc_src_sql-driver.qdoc 35 + + While navigating the results in forward-only mode, the handle of + QSqlResult may change. Applications that use the low-level handle of + SQL result must get a new handle after each call to any of QSqlResult + fetch functions. Example: + + \snippet code/doc_src_sql-driver.cpp 36 + + While reading the results of a forward-only query with PostgreSQL, + the database connection cannot be used to execute other queries. + This is a limitation of libpq library. Example: + + \snippet code/doc_src_sql-driver.cpp 37 + + This problem will not occur if query1 and query2 use different + database connections, or if we execute query2 after the while loop. + + \note Some methods of QSqlDatabase like tables(), primaryIndex() + implicity execute SQL queries, so these also cannot be used while + navigating the results of forward-only query. + + \note QPSQL will print the following warning if it detects a loss of + query results: + + \snippet code/doc_src_sql-driver.qdoc 38 + \section3 How to Build the QPSQL Plugin on Unix and \macos You need the PostgreSQL client library and headers installed. diff --git a/src/sql/kernel/qsqlfield.cpp b/src/sql/kernel/qsqlfield.cpp index a258d44df7..782ab0d71c 100644 --- a/src/sql/kernel/qsqlfield.cpp +++ b/src/sql/kernel/qsqlfield.cpp @@ -588,7 +588,6 @@ void QSqlField::setAutoValue(bool autoVal) \sa tableName() */ - void QSqlField::setTableName(const QString &table) { detach(); @@ -598,9 +597,12 @@ void QSqlField::setTableName(const QString &table) /*! Returns the tableName of the field. + \note When using the QPSQL driver, due to limitations in the libpq library, + the \c tableName() field is not populated in a QSqlField resulting + from a QSqlRecord obtained by QSqlQuery::record() of a forward-only query. + \sa setTableName() */ - QString QSqlField::tableName() const { return d->table; diff --git a/src/sql/kernel/qsqlquery.cpp b/src/sql/kernel/qsqlquery.cpp index b89d20976f..628bfa1880 100644 --- a/src/sql/kernel/qsqlquery.cpp +++ b/src/sql/kernel/qsqlquery.cpp @@ -878,6 +878,14 @@ bool QSqlQuery::isForwardOnly() const \note Calling setForwardOnly after execution of the query will result in unexpected results at best, and crashes at worst. + \note To make sure the forward-only query completed successfully, + the application should check lastError() for an error not only after + executing the query, but also after navigating the query results. + + \warning PostgreSQL: While navigating the query results in forward-only + mode, do not execute any other SQL command on the same database + connection. This will cause the query results to be lost. + \sa isForwardOnly(), next(), seek(), QSqlResult::setForwardOnly() */ void QSqlQuery::setForwardOnly(bool forward) diff --git a/src/sql/kernel/qsqlresult.cpp b/src/sql/kernel/qsqlresult.cpp index f79c1c71cd..cdb1379502 100644 --- a/src/sql/kernel/qsqlresult.cpp +++ b/src/sql/kernel/qsqlresult.cpp @@ -565,6 +565,14 @@ bool QSqlResult::isForwardOnly() const \note Calling setForwardOnly after execution of the query will result in unexpected results at best, and crashes at worst. + \note To make sure the forward-only query completed successfully, + the application should check lastError() for an error not only after + executing the query, but also after navigating the query results. + + \warning PostgreSQL: While navigating the query results in forward-only + mode, do not execute any other SQL command on the same database + connection. This will cause the query results to be lost. + \sa isForwardOnly(), fetchNext(), QSqlQuery::setForwardOnly() */ void QSqlResult::setForwardOnly(bool forward) @@ -1002,6 +1010,10 @@ bool QSqlResult::nextResult() \warning The handle can be NULL if the result was not executed yet. + \warning PostgreSQL: in forward-only mode, the handle of QSqlResult can change + after calling fetch(), fetchFirst(), fetchLast(), fetchNext(), fetchPrevious(), + nextResult(). + The handle returned here is database-dependent, you should query the type name of the variant before accessing it. diff --git a/tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp b/tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp index 1935bb6501..664ba9a9dc 100644 --- a/tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp +++ b/tests/auto/sql/kernel/qsqlquery/tst_qsqlquery.cpp @@ -94,6 +94,10 @@ private slots: // forwardOnly mode need special treatment void forwardOnly_data() { generic_data(); } void forwardOnly(); + void forwardOnlyMultipleResultSet_data() { generic_data(); } + void forwardOnlyMultipleResultSet(); + void psql_forwardOnlyQueryResultsLost_data() { generic_data("QPSQL"); } + void psql_forwardOnlyQueryResultsLost(); // bug specific tests void tds_bitField_data() { generic_data("QTDS"); } @@ -1359,6 +1363,8 @@ void tst_QSqlQuery::forwardOnly() QVERIFY( q.isForwardOnly() ); QVERIFY( q.at() == QSql::BeforeFirstRow ); QVERIFY_SQL( q, exec( "select * from " + qtest + " order by id" ) ); + if (!q.isForwardOnly()) + QSKIP("DBMS doesn't support forward-only queries"); QVERIFY( q.at() == QSql::BeforeFirstRow ); QVERIFY( q.first() ); QCOMPARE( q.at(), 0 ); @@ -1432,6 +1438,209 @@ void tst_QSqlQuery::forwardOnly() QCOMPARE( q.at(), int( QSql::AfterLastRow ) ); } +void tst_QSqlQuery::forwardOnlyMultipleResultSet() +{ + QFETCH(QString, dbName); + QSqlDatabase db = QSqlDatabase::database(dbName); + CHECK_DATABASE(db); + + QSqlDriver::DbmsType dbType = tst_Databases::getDatabaseType(db); + + if (!db.driver()->hasFeature(QSqlDriver::MultipleResultSets)) + QSKIP("DBMS doesn't support multiple result sets"); + + QSqlQuery q(db); + q.setForwardOnly(true); + QVERIFY_SQL(q, exec("select id, t_varchar from " + qtest + " order by id;" // 1. + "select id, t_varchar, t_char from " + qtest + " where id<4 order by id;" // 2. + "update " + qtest + " set t_varchar='VarChar555' where id=5;" // 3. + "select * from " + qtest + " order by id;" // 4. + "select * from " + qtest + " where id=5 order by id;" // 5. + "select * from " + qtest + " where id=-1 order by id;" // 6. + "select * from " + qtest + " order by id")); // 7. + + if (!q.isForwardOnly()) + QSKIP("DBMS doesn't support forward-only queries"); + + // 1. Result set with 2 columns and 5 rows + QVERIFY(q.at() == QSql::BeforeFirstRow); + QCOMPARE(q.isActive(), true); + QCOMPARE(q.isSelect(), true); + + // Test record() of first result set + QSqlRecord record = q.record(); + QCOMPARE(record.count(), 2); + QCOMPARE(record.indexOf("id"), 0); + QCOMPARE(record.indexOf("t_varchar"), 1); + if (dbType != QSqlDriver::PostgreSQL) { // tableName() is not available in forward-only mode of QPSQL + QCOMPARE(record.field(0).tableName(), qtest); // BUG: This fails for Microsoft SQL Server 2016 (QODBC), need fix + QCOMPARE(record.field(1).tableName(), qtest); + } + + // Test navigation + QVERIFY(q.first()); + QCOMPARE(q.at(), 0); + QCOMPARE(q.value(0).toInt(), 1); + + QVERIFY(q.next()); + QCOMPARE(q.at(), 1); + QCOMPARE(q.value(0).toInt(), 2); + + QVERIFY(q.seek(3)); + QCOMPARE(q.at(), 3); + QCOMPARE(q.value(0).toInt(), 4); + + QTest::ignoreMessage(QtWarningMsg, "QSqlQuery::seek: cannot seek backwards in a forward only query"); + QVERIFY(q.first() == false); + QCOMPARE(q.at(), 3); + QCOMPARE(q.value(0).toInt(), 4); + + QTest::ignoreMessage(QtWarningMsg, "QSqlQuery::seek: cannot seek backwards in a forward only query"); + QVERIFY(q.previous() == false); + QCOMPARE(q.at(), 3); + QCOMPARE(q.value(0).toInt(), 4); + + QTest::ignoreMessage(QtWarningMsg, "QSqlQuery::seek: cannot seek backwards in a forward only query"); + QVERIFY(q.seek(1) == false); + QCOMPARE(q.at(), 3); + QCOMPARE(q.value(0).toInt(), 4); + + QVERIFY(q.last()); + QCOMPARE(q.at(), 4); + + // Try move after last row + QVERIFY(q.next() == false); + QCOMPARE(q.at(), QSql::AfterLastRow); + QCOMPARE(q.isActive(), true); + + // 2. Result set with 3 columns and 3 rows + QVERIFY(q.nextResult()); + QVERIFY(q.at() == QSql::BeforeFirstRow); + QCOMPARE(q.isActive(), true); + QCOMPARE(q.isSelect(), true); + + // Test record() of second result set + record = q.record(); + QCOMPARE(record.count(), 3); + QCOMPARE(record.indexOf("id"), 0); + QCOMPARE(record.indexOf("t_varchar"), 1); + QCOMPARE(record.indexOf("t_char"), 2); + + // Test iteration + QVERIFY(q.at() == QSql::BeforeFirstRow); + int index = 0; + while (q.next()) { + QCOMPARE(q.at(), index); + QCOMPARE(q.value(0).toInt(), index+1); + index++; + } + QVERIFY(q.at() == QSql::AfterLastRow); + QCOMPARE(index, 3); + + // 3. Update statement + QVERIFY(q.nextResult()); + QVERIFY(q.at() == QSql::BeforeFirstRow); + QCOMPARE(q.isActive(), true); + QCOMPARE(q.isSelect(), false); + QCOMPARE(q.numRowsAffected(), 1); + + // 4. Result set with 5 rows + QVERIFY(q.nextResult()); + QVERIFY(q.at() == QSql::BeforeFirstRow); + QCOMPARE(q.isActive(), true); + QCOMPARE(q.isSelect(), true); + + // Test forward seek(n) + QVERIFY(q.seek(2)); + QCOMPARE(q.at(), 2); + QCOMPARE(q.value(0).toInt(), 3); + + // Test value(string) + QCOMPARE(q.value("id").toInt(), 3); + QCOMPARE(q.value("t_varchar").toString(), "VarChar3"); + QCOMPARE(q.value("t_char").toString().trimmed(), "Char3"); + + // Next 2 rows of current result set will be + // discarded by next call of nextResult() + + // 5. Result set with 1 row + QVERIFY(q.nextResult()); + QVERIFY(q.at() == QSql::BeforeFirstRow); + QCOMPARE(q.isActive(), true); + QCOMPARE(q.isSelect(), true); + QVERIFY(q.first()); + QCOMPARE(q.at(), 0); + QCOMPARE(q.value(0).toInt(), 5); + QVERIFY(q.next() == false); + QVERIFY(q.at() == QSql::AfterLastRow); + + // 6. Result set without rows + QVERIFY(q.nextResult()); + QVERIFY(q.at() == QSql::BeforeFirstRow); + QCOMPARE(q.isActive(), true); + QCOMPARE(q.isSelect(), true); + QVERIFY(q.next() == false); + + // 7. Result set with 5 rows + QVERIFY(q.nextResult()); + QVERIFY(q.at() == QSql::BeforeFirstRow); + QCOMPARE(q.isActive(), true); + QCOMPARE(q.isSelect(), true); + + // Just skip it, so we move after last result set. + QVERIFY(q.nextResult() == false); + QVERIFY(q.at() == QSql::BeforeFirstRow); + QCOMPARE(q.isActive(), false); + + // See if we can execute another query + QVERIFY_SQL(q, exec("select id from " + qtest + " where id=5")); + QVERIFY(q.at() == QSql::BeforeFirstRow); + QCOMPARE(q.isActive(), true); + QCOMPARE(q.isSelect(), true); + QVERIFY(q.first()); + QCOMPARE(q.at(), 0); + QCOMPARE(q.value("id").toInt(), 5); + QCOMPARE(q.record().count(), 1); + QCOMPARE(q.record().indexOf("id"), 0); +} + +void tst_QSqlQuery::psql_forwardOnlyQueryResultsLost() +{ + QFETCH(QString, dbName); + QSqlDatabase db = QSqlDatabase::database(dbName); + CHECK_DATABASE(db); + + QSqlQuery q1(db); + q1.setForwardOnly(true); + QVERIFY_SQL(q1, exec("select * from " + qtest + " where id<=3 order by id")); + if (!q1.isForwardOnly()) + QSKIP("DBMS doesn't support forward-only queries"); + + // Read first row of q1 + QVERIFY(q1.next()); + QCOMPARE(q1.at(), 0); + QCOMPARE(q1.value(0).toInt(), 1); + + // Executing another query on the same db connection + // will cause the query results of q1 to be lost. + QSqlQuery q2(db); + q2.setForwardOnly(true); + QVERIFY_SQL(q2, exec("select * from " + qtest + " where id>3 order by id")); + + QTest::ignoreMessage(QtWarningMsg, "QPSQLDriver::getResult: Query results lost - " + "probably discarded on executing another SQL query."); + + // Reading next row of q1 will not possible. + QVERIFY(!q1.next()); + QCOMPARE(q1.at(), QSql::AfterLastRow); + QVERIFY(q1.lastError().type() != QSqlError::NoError); + + // See if we can read rows from q2 + QVERIFY(q2.seek(1)); + QCOMPARE(q2.at(), 1); + QCOMPARE(q2.value(0).toInt(), 5); +} + void tst_QSqlQuery::query_exec() { QFETCH( QString, dbName ); |