[Data/ODBC] Empty parameterized SELECT reports one row and causes ActiveRecord Query to return a null record
Describe the bug
With the ODBC connector, Poco::Data::Statement::execute() returns 1 for a parameterized SELECT that produces an empty result set.
No value is extracted, but the positive return value causes Poco::ActiveRecord::Query::execute() to treat the query as successful. It creates an ActiveRecord from the unchanged default object, resulting in a null record inside the returned vector.
The problem is caused by the affected-row fallback in StatementImpl: ODBC initializes _affectedRowCount from the number of bound input rows, which is 1 for a scalar parameter. When the result set is empty, this binding count is returned as though one row had been extracted.
To Reproduce
The following SSCCE uses an empty SQL Server temporary table:
#include "Poco/Data/Keywords.h"
#include "Poco/Data/ODBC/Connector.h"
#include "Poco/Data/Session.h"
#include "Poco/Data/Statement.h"
#include "Poco/Types.h"
#include <iostream>
#include <string>
using namespace Poco::Data::Keywords;
int main()
{
Poco::Data::ODBC::Connector::registerConnector();
Poco::Data::Session session("ODBC", "<SQL Server connection string>");
session
<< "CREATE TABLE #poco_empty_result_test "
"(id BIGINT PRIMARY KEY, name VARCHAR(256) NOT NULL)",
now;
Poco::Int64 id = -1;
std::string name = "missing";
Poco::Data::Statement statement(session);
statement
<< "SELECT id FROM #poco_empty_result_test WHERE name = ?",
into(id),
bind(name),
limit(1);
const auto rows = statement.execute();
std::cout << "rows=" << rows << '\n';
std::cout << "id=" << id << '\n';
Poco::Data::ODBC::Connector::unregisterConnector();
}The table is empty, so the query cannot match any row.
Actual output:
rows=1
id=-1The issue also affects ActiveRecord queries. Query::execute() evaluates the incorrect result as true, clones the unchanged default record and calls ActRec::withContext(). Because the record has no valid primary key, the resulting pointer is null.
Expected behavior
Statement::execute() should return the number of rows actually extracted from a result-set statement:
rows=0
id=-1An equivalent ActiveRecord query should return an empty vector, not a vector containing one null record.
Please add relevant environment information:
- OS Type and Version: Windows 11, build 26100
- POCO Version: 1.15.2
- Third-party product: Microsoft SQL Server Express with Microsoft ODBC Driver 17 for SQL Server
Additional context
The relevant execution sequence is:
ODBCStatementImpl::doBind()initializes_affectedRowCountusing the first binding'snumOfRowsHandled().- A scalar input binding therefore sets
_affectedRowCountto1. - The
SELECTextracts zero rows. StatementImpl::executeWithLimit()falls back toaffectedRowCount()whenever the extraction count is zero.- It consequently returns
1, even though the statement returned an empty result set.
The same fallback exists in executeWithoutLimit().
A possible fix is to use the affected-row fallback only for statements that do not return columns:
-if (count == 0)
+if (count == 0 && columnsReturned() == 0)This condition should be applied in both StatementImpl::executeWithLimit() and StatementImpl::executeWithoutLimit().
Source: pocoproject/poco