Skip to content

fix(bq_driver): populate row array size across page boundaries in SQLFetch - #1661

Open
sachinpro wants to merge 2 commits into
mainfrom
fix_row_array_pagination
Open

sachinpro wants to merge 2 commits into
mainfrom
fix_row_array_pagination

Conversation

@sachinpro

Copy link
Copy Markdown
Collaborator

Summary

When an ODBC application configures block cursor / array fetching via SQL_ATTR_ROW_ARRAY_SIZE (e.g., 5000 rows), SQLFetch and SQLFetchScroll previously only returned rows from the current ResultSet page. If a query result set spanned multiple pages or streams (e.g., where each page returns 1000 rows), SQLFetch stopped at the end of the first page instead of continuing to fetch across page boundaries to satisfy the requested array size.

This PR introduces a multi-page aware WriteRowset(StatementHandle&, ...) that iterates across pages via FetchNextResultSet until the requested rowset size is populated or no further data remains (SQL_NO_DATA).

Key Changes

  • google/cloud/odbc/bq_driver/internal/odbc_sql_fetch.h & odbc_sql_fetch.cc:
    • Implemented overloaded WriteRowset(StatementHandle& stmt_handle, int const rowset_size, DescriptorHandle& ard, DescriptorHandle& ird).
    • Automatically invokes FetchNextResultSet(stmt_handle) when reaching the end of the current page until row_counter == rowset_size or EOF.
    • Correctly updates array_status_ptr (SQL_ROW_SUCCESS / SQL_ROW_NOROW), rows_processed_ptr, and cursor position.
    • Suppressed error logging on expected SQL_NO_DATA when reaching end of data during pagination.
  • google/cloud/odbc/bq_driver/odbc_sql_results.cc:
    • Updated SQLFetchInternal and SQLFetchScrollInternal to delegate to WriteRowset(handle, ...).
  • google/cloud/odbc/integration_tests/odbc_driver_tests/statement_test.cc:
    • Added integration test TEST_P(HTAPIParameterizedTest, SQLExecDirect_pagination_with_row_array_size) to verify that 3000 rows across page boundaries are fetched in 1 fetch call when ROW_ARRAY_SIZE is set to 5000.

@sachinpro
sachinpro requested a review from a team as a code owner September 12, 2026 00:27
@sachinpro
sachinpro marked this pull request as draft September 12, 2026 00:30
@sachinpro
sachinpro force-pushed the fix_row_array_pagination branch 2 times, most recently from 55c684f to fbce21a Compare September 12, 2026 00:57
@sachinpro
sachinpro force-pushed the fix_row_array_pagination branch from fbce21a to 8433b4f Compare September 12, 2026 01:11
@sachinpro
sachinpro marked this pull request as ready for review September 12, 2026 01:24
@sachinpro
sachinpro requested a review from logachev September 12, 2026 01:24
}
LOG(ERROR) << "WriteRowset::FetchNextResultSet:: "
<< next_page_status.message;
if (row_counter == 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we return error only if row_count == 0? What if we fail to load second page?

if (updated_rs.rows.empty()) {
break;
}
updated_rs.cursor++;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why moving cursor without doing anything? Won't we miss 1 row of results due to this?

@sachinpro sachinpro Sep 17, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We initialize the cursor by "-1" since in ODBC, broadly speaking, subsequent operations after SQLFetch on the current row (such as SQLGetData) expect the cursor to point to the active/last fetched row.

Ref: "When the result set is created, the cursor is positioned before the start of the result set."

updated_rs.cursor++;
}

ResultSet& current_rs = stmt_handle.GetResultSet();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need 3 different vars? Why can't we just use original result_set?
Also we call GetResultSet() for each row which seems exceesive. We can initialize it prior to the 'while' loop & just update as needed.

return status_record;
}

if (row_status_ptr) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: do we need this check? Is it allowed to be null?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is null by default. SQL_ATTR_ROW_STATUS_PTR is optional.

}

ResultSet& final_rs = stmt_handle.GetResultSet();
if (row_counter > 0) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need this? Can you add comment?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we initialize array with SQL_ROW_NOROW & update it as we progress instead?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in the latest commit.

@sachinpro
sachinpro force-pushed the fix_row_array_pagination branch from 8433b4f to 504f41a Compare September 17, 2026 18:51
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