Skip to content

Commit 8ee3212

Browse files
committed
Add disconnected RowSet class for bulk row fetching
RowSet fetches up to N rows from a Statement's result set directly into a contiguous buffer at construction time. After construction, the RowSet is completely independent of the source Statement. This enables zero-copy N-row prefetch for the ODBC driver: allocate a RowSet with the desired batch size and access rows by index, without per-row memcpy from the Statement's internal buffer. Addresses asfernandes#42 (REQ-3).
1 parent 4df476e commit 8ee3212

4 files changed

Lines changed: 360 additions & 0 deletions

File tree

src/fb-cpp/RowSet.cpp

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2026 F.D.Castel
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
#include "RowSet.h"
26+
#include "Client.h"
27+
#include "Statement.h"
28+
29+
using namespace fbcpp;
30+
using namespace fbcpp::impl;
31+
32+
33+
RowSet::RowSet(Statement& statement, unsigned maxRows)
34+
{
35+
assert(statement.isValid());
36+
assert(statement.getResultSetHandle());
37+
38+
auto& attachment = statement.getAttachment();
39+
auto status = attachment.getClient().newStatus();
40+
StatusWrapper statusWrapper{attachment.getClient(), status.get()};
41+
42+
auto outMetadata = statement.getOutputMetadata();
43+
messageLength = outMetadata->getMessageLength(&statusWrapper);
44+
45+
buffer.resize(static_cast<std::size_t>(maxRows) * messageLength);
46+
47+
auto resultSet = statement.getResultSetHandle();
48+
49+
for (unsigned i = 0; i < maxRows; ++i)
50+
{
51+
auto* dest = buffer.data() + static_cast<std::size_t>(i) * messageLength;
52+
53+
if (resultSet->fetchNext(&statusWrapper, dest) != fb::IStatus::RESULT_OK)
54+
break;
55+
56+
++count;
57+
}
58+
59+
buffer.resize(static_cast<std::size_t>(count) * messageLength);
60+
}

src/fb-cpp/RowSet.h

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2026 F.D.Castel
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
#ifndef FBCPP_ROWSET_H
26+
#define FBCPP_ROWSET_H
27+
28+
#include "fb-api.h"
29+
#include "SmartPtrs.h"
30+
#include "Exception.h"
31+
#include <cassert>
32+
#include <cstddef>
33+
#include <vector>
34+
35+
36+
///
37+
/// fb-cpp namespace.
38+
///
39+
namespace fbcpp
40+
{
41+
class Statement;
42+
43+
///
44+
/// @brief A disconnected buffer of rows fetched from a Statement's result set.
45+
///
46+
/// Rows are fetched into a contiguous buffer at construction time. After
47+
/// construction the RowSet is independent of its source Statement and can
48+
/// be used, moved, or destroyed freely.
49+
///
50+
class RowSet final
51+
{
52+
public:
53+
///
54+
/// @brief Fetches up to `maxRows` rows from the current result set of
55+
/// `statement`.
56+
///
57+
/// The statement must have an open result set (i.e. `execute()` was
58+
/// called and it is a SELECT-type statement). Rows are fetched via
59+
/// `IResultSet::fetchNext()` directly into the internal buffer.
60+
///
61+
/// @param statement The statement with an open result set.
62+
/// @param maxRows Maximum number of rows to fetch.
63+
///
64+
explicit RowSet(Statement& statement, unsigned maxRows);
65+
66+
RowSet(RowSet&& o) noexcept = default;
67+
RowSet& operator=(RowSet&& o) noexcept = default;
68+
69+
RowSet(const RowSet&) = delete;
70+
RowSet& operator=(const RowSet&) = delete;
71+
72+
public:
73+
///
74+
/// @brief Returns the number of rows actually fetched.
75+
///
76+
unsigned getCount() const noexcept
77+
{
78+
return count;
79+
}
80+
81+
///
82+
/// @brief Returns the message length (in bytes) of each row.
83+
///
84+
unsigned getMessageLength() const noexcept
85+
{
86+
return messageLength;
87+
}
88+
89+
///
90+
/// @brief Returns a pointer to the raw data of the row at `index`.
91+
/// @param index Zero-based row index (must be < getCount()).
92+
///
93+
const std::byte* getRow(unsigned index) const
94+
{
95+
assert(index < count);
96+
return buffer.data() + static_cast<std::size_t>(index) * messageLength;
97+
}
98+
99+
///
100+
/// @brief Returns a mutable pointer to the raw data of the row at `index`.
101+
/// @param index Zero-based row index (must be < getCount()).
102+
///
103+
std::byte* getRow(unsigned index)
104+
{
105+
assert(index < count);
106+
return buffer.data() + static_cast<std::size_t>(index) * messageLength;
107+
}
108+
109+
///
110+
/// @brief Returns the entire contiguous buffer containing all fetched rows.
111+
///
112+
const std::vector<std::byte>& getBuffer() const noexcept
113+
{
114+
return buffer;
115+
}
116+
117+
private:
118+
unsigned count = 0;
119+
unsigned messageLength = 0;
120+
std::vector<std::byte> buffer;
121+
};
122+
} // namespace fbcpp
123+
124+
#endif // FBCPP_ROWSET_H

src/fb-cpp/fb-cpp.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
#include "Transaction.h"
3131
#include "Descriptor.h"
3232
#include "Statement.h"
33+
#include "RowSet.h"
3334
#include "Batch.h"
3435
#include "Blob.h"
3536
#include "EventListener.h"

src/test/RowSet.cpp

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/*
2+
* MIT License
3+
*
4+
* Copyright (c) 2026 F.D.Castel
5+
*
6+
* Permission is hereby granted, free of charge, to any person obtaining a copy
7+
* of this software and associated documentation files (the "Software"), to deal
8+
* in the Software without restriction, including without limitation the rights
9+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10+
* copies of the Software, and to permit persons to whom the Software is
11+
* furnished to do so, subject to the following conditions:
12+
*
13+
* The above copyright notice and this permission notice shall be included in all
14+
* copies or substantial portions of the Software.
15+
*
16+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22+
* SOFTWARE.
23+
*/
24+
25+
#include "TestUtil.h"
26+
#include "fb-cpp/RowSet.h"
27+
#include "fb-cpp/Statement.h"
28+
#include "fb-cpp/Transaction.h"
29+
#include <cstring>
30+
31+
32+
BOOST_AUTO_TEST_SUITE(RowSetSuite)
33+
34+
BOOST_AUTO_TEST_CASE(fetchRowsIntoRowSet)
35+
{
36+
const auto database = getTempFile("RowSet-fetchRowsIntoRowSet.fdb");
37+
38+
Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true)};
39+
FbDropDatabase attachmentDrop{attachment};
40+
41+
Transaction transaction{attachment};
42+
43+
Statement ddl{attachment, transaction, "create table t (col integer)"};
44+
ddl.execute(transaction);
45+
transaction.commitRetaining();
46+
47+
Statement insert{attachment, transaction, "insert into t (col) values (?)"};
48+
for (int i = 1; i <= 5; ++i)
49+
{
50+
insert.setInt32(0, i);
51+
insert.execute(transaction);
52+
}
53+
54+
Statement select{attachment, transaction, "select col from t order by col"};
55+
BOOST_REQUIRE(select.execute(transaction));
56+
57+
// The first row (1) was fetched by execute(). Fetch remaining rows into RowSet.
58+
RowSet rowSet{select, 10};
59+
60+
BOOST_CHECK_EQUAL(rowSet.getCount(), 4u);
61+
BOOST_CHECK(rowSet.getMessageLength() > 0);
62+
BOOST_CHECK_EQUAL(
63+
rowSet.getBuffer().size(), static_cast<std::size_t>(rowSet.getCount()) * rowSet.getMessageLength());
64+
65+
// Verify row data by reading int32 values from the raw buffer using
66+
// the output descriptor offsets.
67+
const auto& descriptors = select.getOutputDescriptors();
68+
BOOST_REQUIRE_EQUAL(descriptors.size(), 1u);
69+
70+
for (unsigned i = 0; i < rowSet.getCount(); ++i)
71+
{
72+
const auto* row = rowSet.getRow(i);
73+
std::int32_t value;
74+
std::memcpy(&value, row + descriptors[0].offset, sizeof(value));
75+
BOOST_CHECK_EQUAL(value, static_cast<std::int32_t>(i + 2));
76+
}
77+
}
78+
79+
BOOST_AUTO_TEST_CASE(fetchFewerRowsThanMaxRows)
80+
{
81+
const auto database = getTempFile("RowSet-fetchFewerRowsThanMaxRows.fdb");
82+
83+
Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true)};
84+
FbDropDatabase attachmentDrop{attachment};
85+
86+
Transaction transaction{attachment};
87+
88+
Statement ddl{attachment, transaction, "create table t (col integer)"};
89+
ddl.execute(transaction);
90+
transaction.commitRetaining();
91+
92+
Statement insert{attachment, transaction, "insert into t (col) values (?)"};
93+
for (int i = 1; i <= 3; ++i)
94+
{
95+
insert.setInt32(0, i);
96+
insert.execute(transaction);
97+
}
98+
99+
Statement select{attachment, transaction, "select col from t order by col"};
100+
BOOST_REQUIRE(select.execute(transaction));
101+
102+
// execute() fetched row 1. Request 100 rows but only 2 remain.
103+
RowSet rowSet{select, 100};
104+
105+
BOOST_CHECK_EQUAL(rowSet.getCount(), 2u);
106+
BOOST_CHECK_EQUAL(
107+
rowSet.getBuffer().size(), static_cast<std::size_t>(rowSet.getCount()) * rowSet.getMessageLength());
108+
}
109+
110+
BOOST_AUTO_TEST_CASE(rowSetIsDisconnectedFromStatement)
111+
{
112+
const auto database = getTempFile("RowSet-isDisconnectedFromStatement.fdb");
113+
114+
Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true)};
115+
FbDropDatabase attachmentDrop{attachment};
116+
117+
Transaction transaction{attachment};
118+
119+
Statement ddl{attachment, transaction, "create table t (col integer)"};
120+
ddl.execute(transaction);
121+
transaction.commitRetaining();
122+
123+
Statement insert{attachment, transaction, "insert into t (col) values (?)"};
124+
for (int i = 1; i <= 3; ++i)
125+
{
126+
insert.setInt32(0, i);
127+
insert.execute(transaction);
128+
}
129+
130+
Statement select{attachment, transaction, "select col from t order by col"};
131+
BOOST_REQUIRE(select.execute(transaction));
132+
133+
RowSet rowSet{select, 10};
134+
BOOST_CHECK_EQUAL(rowSet.getCount(), 2u);
135+
136+
// Free the statement; the RowSet data is still valid.
137+
select.free();
138+
139+
const auto& descriptors_copy = rowSet.getBuffer();
140+
BOOST_CHECK(!descriptors_copy.empty());
141+
BOOST_CHECK_EQUAL(rowSet.getCount(), 2u);
142+
}
143+
144+
BOOST_AUTO_TEST_CASE(moveConstructor)
145+
{
146+
const auto database = getTempFile("RowSet-moveConstructor.fdb");
147+
148+
Attachment attachment{CLIENT, database, AttachmentOptions().setCreateDatabase(true)};
149+
FbDropDatabase attachmentDrop{attachment};
150+
151+
Transaction transaction{attachment};
152+
153+
Statement ddl{attachment, transaction, "create table t (col integer)"};
154+
ddl.execute(transaction);
155+
transaction.commitRetaining();
156+
157+
Statement insert{attachment, transaction, "insert into t (col) values (?)"};
158+
for (int i = 1; i <= 3; ++i)
159+
{
160+
insert.setInt32(0, i);
161+
insert.execute(transaction);
162+
}
163+
164+
Statement select{attachment, transaction, "select col from t order by col"};
165+
BOOST_REQUIRE(select.execute(transaction));
166+
167+
RowSet rowSet1{select, 10};
168+
const auto count = rowSet1.getCount();
169+
170+
RowSet rowSet2{std::move(rowSet1)};
171+
BOOST_CHECK_EQUAL(rowSet2.getCount(), count);
172+
BOOST_CHECK_EQUAL(rowSet1.getCount(), 0u);
173+
}
174+
175+
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)