Skip to content

Add disconnected RowSet class with typed Row access#46

Merged
asfernandes merged 10 commits into
asfernandes:mainfrom
fdcastel:issue-42-req3
Apr 3, 2026
Merged

Add disconnected RowSet class with typed Row access#46
asfernandes merged 10 commits into
asfernandes:mainfrom
fdcastel:issue-42-req3

Conversation

@fdcastel
Copy link
Copy Markdown
Contributor

@fdcastel fdcastel commented Mar 12, 2026

Addresses REQ-3 from #42 per @asfernandes feedback.

The original REQ-3 proposed a fetchNext(void* buffer) overload. This approach was rejected and instead requested a "disconnected RowSet (a buffer of rows, disconnected after fetched) class."

The RowSet feature is critical for the ODBC driver's N-row prefetch pattern (SQL_ATTR_ROW_ARRAY_SIZE > 1), which eliminates per-row fetchNext() + memcpy overhead.

After several iterations based on reviewer feedback, the current design introduces two new classes:

  • Row — a lightweight, non-owning view of a single row's message buffer with typed accessors.
  • RowSet — a disconnected buffer of rows fetched from a Statement's result set.

Statement is refactored to use Row internally, eliminating ~700 lines of duplicated reading/conversion logic.

Design

Row — lightweight non-owning view

Row is a value-type view over a raw message buffer. It holds four pointers: message, descriptors, numericConverter, and calendarConverter. All typed read logic (isNull, getBool, getInt32, getString, get<T>, etc.) lives in Row.

Row is used in two places:

  • Inside Statement as a private outRow member, pointing to the statement's own outMessage buffer. All ~30 public getters in Statement now delegate to outRow. The pointer is stable because outMessage is sized once during construction and never reallocated.
  • Inside RowSet::getRow(i) as a transient view over the i-th row in the fetched buffer.

This design satisfies the constraint of eliminating duplicated non-trivial getters while keeping Row independent of both Statement and RowSet as classes.

RowSet — disconnected row buffer

RowSet is a move-only class that:

  1. Fetches at construction time: Takes a Statement& with an open result set and a maxRows count. Calls IResultSet::fetchNext() directly into a contiguous internal buffer, up to maxRows times.
  2. Is disconnected after construction: Owns all row data, descriptors, and converters independently. The source Statement can be freed or reused without affecting the RowSet.
  3. Provides both typed and raw access: getRow(index) returns a Row for typed column reading. getRawRow(index) returns a std::span<const std::byte> for zero-copy scenarios.

API

// Lightweight non-owning typed view of a single row
class Row final
{
public:
    Row() = default;
    Row(const std::byte* message, const std::vector<Descriptor>& descriptors,
        NumericConverter& numericConverter, CalendarConverter& calendarConverter);

    bool isNull(unsigned index);
    std::optional<bool> getBool(unsigned index);
    std::optional<std::int32_t> getInt32(unsigned index);
    std::optional<std::string> getString(unsigned index);
    // ... all other typed accessors from Statement ...

    template <typename T> T get(unsigned index);
    template <Aggregate T> T get();
    template <TupleLike T> T get();
    template <VariantLike V> V get(unsigned index);
};

// Disconnected row buffer with typed and raw access
class RowSet final
{
public:
    explicit RowSet(Statement& statement, unsigned maxRows);

    unsigned getCount() const noexcept;
    unsigned getMessageLength() const noexcept;

    Row getRow(unsigned index);                            // typed access
    std::span<const std::byte> getRawRow(unsigned index) const;  // raw access
    const std::vector<std::byte>& getRawBuffer() const noexcept;
};

// Statement: public API unchanged — getters now delegate to private outRow
class Statement
{
    // All existing getters preserved with identical signatures.
    // Internally: assert(isValid()); return outRow.getXxx(index);
private:
    Row outRow;  // new private member, points into outMessage
};

ODBC driver usage (N-row prefetch)

Statement select{attachment, transaction, sql, options};
select.execute(transaction);

while (true)
{
    RowSet batch{select, rowArraySize};
    if (batch.getCount() == 0)
        break;

    for (unsigned i = 0; i < batch.getCount(); ++i)
    {
        // Typed access
        auto row = batch.getRow(i);
        auto name = row.getString(0);
        auto age  = row.getInt32(1);

        // Zero-copy raw access for direct ODBC buffer filling
        auto raw = batch.getRawRow(i);
        std::memcpy(odbcRow, raw.data(), raw.size());
    }
}

Impact

  • Statement public API unchanged: All existing Statement getters continue to work identically.
  • No duplication: All reading/conversion logic lives exclusively in Row.
  • Zero-copy N-row prefetch: RowSet eliminates the per-row fetchNext() + memcpy pattern for the ODBC driver.

@asfernandes
Copy link
Copy Markdown
Owner

It will be difficult to review and merge if a PR includes things present in others not yet merged.

@fdcastel
Copy link
Copy Markdown
Contributor Author

It will be difficult to review and merge if a PR includes things present in others not yet merged.

Agreed. I mistakenly assumed it would be easier for you to review the three PRs sequentially, and that the other two would be trivial.

I’ll rebase this one out on its own.

P.S.: Please let me know if you’d like me to do the same for #45 as well.

@fdcastel
Copy link
Copy Markdown
Contributor Author

Rebased onto latest main.

Comment thread src/fb-cpp/RowSet.h
@fdcastel
Copy link
Copy Markdown
Contributor Author

2nd attempt.


Row — shared, non-owning typed view

Row is a lightweight, non-owning view of a single row's message buffer. It carries pointers to the raw data, column descriptors, and the numeric/calendar converters needed to interpret the bytes. All typed read logic (getBool, getInt32, getString, get<T>, etc.) lives in Row.

  • Statement::currentRow() returns a Row over the statement's current output buffer.
  • RowSet::getRow(i) returns a Row over the i-th fetched row.
  • Statement's individual getters (getInt32, getString, etc.) now delegate to currentRow().

This eliminates ~900 lines of duplicated reading/conversion logic from Statement.

RowSet — disconnected row buffer

RowSet is a move-only class that:

  1. Fetches at construction time: Takes a Statement& with an open result set and a maxRows count. Calls IResultSet::fetchNext() directly into a contiguous internal buffer, up to maxRows times.
  2. Is disconnected after construction: Owns all row data, descriptors, and converters independently. The source Statement can be freed or reused without affecting the RowSet.
  3. Provides typed row access: getRow(index) returns a Row for typed column reading. getRawRow(index) returns a raw const std::byte* for zero-copy scenarios.

API

// Lightweight non-owning typed view of a single row
class Row final
{
public:
    Row(const std::byte* message, const std::vector<Descriptor>& descriptors,
        NumericConverter& numericConverter, CalendarConverter& calendarConverter);

    bool isNull(unsigned index);
    std::optional<bool> getBool(unsigned index);
    std::optional<std::int32_t> getInt32(unsigned index);
    std::optional<std::string> getString(unsigned index);
    // ... all other typed accessors from Statement ...

    template <typename T> T get(unsigned index);
    template <Aggregate T> T get();
    template <TupleLike T> T get();
    template <VariantLike V> V get(unsigned index);
};

// Disconnected row buffer with typed access
class RowSet final
{
public:
    explicit RowSet(Statement& statement, unsigned maxRows);

    unsigned getCount() const noexcept;
    unsigned getMessageLength() const noexcept;

    Row getRow(unsigned index);                    // typed access
    const std::byte* getRawRow(unsigned index);    // raw access

    const std::vector<std::byte>& getBuffer() const noexcept;
};

// Statement gains currentRow()
class Statement
{
public:
    Row currentRow();   // view of current output buffer
    // existing getters now delegate to currentRow()
};

ODBC driver usage (N-row prefetch with typed access)

Statement select{attachment, transaction, sql, options};
select.execute(transaction);

RowSet batch{select, 64};

for (unsigned i = 0; i < batch.getCount(); ++i)
{
    auto row = batch.getRow(i);
    auto name = row.getString(0);
    auto age = row.getInt32(1);
}

Changes

  • New file: Row.h — shared typed row view extracted from Statement's reading logic.
  • Modified: Statement.h — getters delegate to currentRow(), ~900 lines of duplicated logic removed.
  • New files: RowSet.h, RowSet.cpp — disconnected row buffer with descriptors/converters for typed access.
  • Modified: fb-cpp.h — added #include "Row.h".
  • Modified: src/test/RowSet.cpp — tests use typed Row access instead of raw buffer manipulation; also verifies typed access survives statement free.

Impact

  • Statement API unchanged: All existing Statement getters continue to work identically.
  • RowSet provides both typed and raw access: getRow(i) for typed, getRawRow(i) for zero-copy.
  • Significant code deduplication: reading logic now lives in one place (Row).

@fdcastel
Copy link
Copy Markdown
Contributor Author

Rebased onto latest main.

@fdcastel
Copy link
Copy Markdown
Contributor Author

Rebased onto latest main.

@fdcastel
Copy link
Copy Markdown
Contributor Author

@asfernandes! The work on the Firebird ODBC driver is moving along nicely. I think we’re getting close to the point where we’ll need this PR in order to fully use fb-cpp with it.

Do you have any thoughts, suggestions, or concerns about this?

P.S. If you’re short on time and can’t review this yet, that’s totally fine. 👍

Comment thread src/fb-cpp/Row.h Outdated
Comment thread src/fb-cpp/RowSet.cpp Outdated
Comment thread src/fb-cpp/RowSet.cpp Outdated
Comment thread src/fb-cpp/RowSet.h Outdated
Comment thread src/fb-cpp/RowSet.h Outdated
Comment thread src/test/RowSet.cpp
Comment thread src/fb-cpp/Statement.h Outdated
Comment thread src/fb-cpp/Statement.h Outdated
@fdcastel fdcastel force-pushed the issue-42-req3 branch 3 times, most recently from 14a42fe to c7fa972 Compare April 1, 2026 15:32
@asfernandes
Copy link
Copy Markdown
Owner

@fdcastel I did a general architecture refactor in https://github.com/asfernandes/fb-cpp/tree/issue-42-req3 in top of your branch. Please check it and if ok, I'd like to commit it to your PR to continue the review there.

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Apr 2, 2026

I Can't review it now, but, Sure! Be my guest!

You can add new commits here. Or open a new branch / PR. I'll review tomorrow morning.

@asfernandes
Copy link
Copy Markdown
Owner

I pushed the changes here.

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Apr 2, 2026

I pushed the changes here.

Looks great! Two considerations:

  1. We switched back from RowSet* to raw pointers. If you’re comfortable with that, I am as well.
  2. The move assignment operator (RowSet& operator=(RowSet&&)) hasn’t been tested yet.

I can take care of both if you’d like (with some guidance, of course 😉)

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Apr 2, 2026

For reference, I'm thinking something along the lines of:

// ODBC driver bulk fetch pattern
Statement select{attachment, transaction, sql, options};
select.execute(transaction);

while (true)
{
    RowSet batch{select, rowArraySize};
    if (batch.getCount() == 0)
        break;

    for (unsigned i = 0; i < batch.getCount(); ++i)
    {
        // Option A: typed access via Row
        auto row = batch.getRow(i);
        auto val = row.getInt32(0);

        // Option B: zero-copy raw access for ODBC buffer filling
        auto rawRow = batch.getRawRow(i);
        std::memcpy(odbcBuffer + i * rowSize, rawRow.data(), rawRow.size());
    }
}

This eliminates the per-row fetchNext() + memcpy pattern and supports SQL_ATTR_ROW_ARRAY_SIZE > 1.

Suggestions welcome!

@fdcastel fdcastel changed the title Add disconnected RowSet class for bulk row fetching Add disconnected RowSet class with typed Row access Apr 2, 2026
@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Apr 2, 2026

@asfernandes I went ahead and updated the PR description to reflect the latest changes. Feel free to comment.

@asfernandes
Copy link
Copy Markdown
Owner

I pushed the changes here.

Looks great! Two considerations:

  1. We switched back from RowSet* to raw pointers. If you’re comfortable with that, I am as well.

?

  1. The move assignment operator (RowSet& operator=(RowSet&&)) hasn’t been tested yet.

Please.

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Apr 2, 2026

  1. We switched back from RowSet* to raw pointers. If you’re comfortable with that, I am as well.

?

Forget it, brain fart. I messed up the branches when comparing 🤦🏻‍♂️. Lack of morning coffee and all.

  1. The move assignment operator (RowSet& operator=(RowSet&&)) hasn’t been tested yet.

Please.

Working on it.

@fdcastel
Copy link
Copy Markdown
Contributor Author

fdcastel commented Apr 2, 2026

Done!

  • Added moveAssignment test case in RowSet.cpp.
  • Also enhanced the existing moveConstructor test to verify typed access works after the move.

@asfernandes asfernandes merged commit 0b9de71 into asfernandes:main Apr 3, 2026
4 checks passed
@asfernandes
Copy link
Copy Markdown
Owner

Released as 0.0.4.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants