Summary
With SQL_ATTR_ROW_ARRAY_SIZE > 1, column-wise binding (the default, SQL_ATTR_ROW_BIND_TYPE = SQL_BIND_BY_COLUMN) and a SQL_ATTR_ROW_BIND_OFFSET_PTR set, every row of a rowset is written to the same address: the bound address plus the offset, with no per-row stride. SQLFetch returns SQL_SUCCESS, the rows-fetched counter says 4 and the status array holds four SQL_ROW_SUCCESS, but the application's arrays contain only the last row of the rowset, in their first (shifted) element; the other elements are untouched. Silent data loss.
Row-wise binding with the same offset pointer works. Column-wise binding without an offset pointer works.
Environment
Windows x64, master 35fae3e (with #303 applied), Firebird 5.0.3, Microsoft driver manager. The code involved is platform-independent.
Reproduction
BlockCursorTest.ColumnWiseBindingWithOffset in #305, currently GTEST_SKIPped with this defect as the reason. Shape: six-row UNION ALL, SQL_ATTR_ROW_ARRAY_SIZE = 4, SQLBindCol(1, SQL_C_SLONG, values, 0, indicators), offset = 8 × sizeof(SQLLEN) bytes (64 on x64, so the rows should land at values[16..19] and indicators[8..11]).
Observed after the first SQLFetch:
rows_fetched = 4, status = 0,0,0,0
values[16] = 4 rows 1..4 were all written here
values[17..19] = -1 untouched
indicators[8] = 4, indicators[9..11] untouched
Second fetch with the offset changed to 0 (expected values[0..1] = 5, 6): values[0] = 6, values[1] untouched.
Cause
OdbcStatement::getSchemaFetchData() selects the row-wise fetch branch whenever a bind-offset pointer is set, not only for row-wise binding:
bool getSchemaFetchData(){ return applicationRowDescriptor->headBindType || applicationRowDescriptor->headBindOffsetPtr; }
That branch of fetchData() (and of its twin in sqlFetchScrollCursorStatic()) uses one byte offset for every column and advances it by headBindType per row:
bindOffsetPtrTmp += rowBindType; // 0 for SQL_BIND_BY_COLUMN
Column-wise arrays need the other branch, where returnDataFromExtendedFetch() steps each column by its own element size (sizeColumnExtendedFetch * row) and the indicators by sizeof(SQLLEN). That branch never adds the application's offset, though, and it reuses the aliased headBindOffsetPtr as its row counter, so it cannot simply be selected when an offset pointer is present.
Expected
The offset is added to every bound data and indicator address, and the rows still step by element size (ODBC, SQL_ATTR_ROW_BIND_OFFSET_PTR).
Suggested fix
Take the row-wise branch only when headBindType != 0. In the column-wise branch, read the application's offset once at the start of the fetch, start the row counter at 0, and add the offset to both bases in returnDataFromExtendedFetch(): bindOffsetPtrTo = offset + sizeColumnExtendedFetch * row, bindOffsetPtrIndTo = offset + row * sizeof(SQLLEN). Same change in the static-cursor twin. Then unskip the test in #305.
Related: #299 is the same class of defect on the parameter side.
Summary
With
SQL_ATTR_ROW_ARRAY_SIZE> 1, column-wise binding (the default,SQL_ATTR_ROW_BIND_TYPE = SQL_BIND_BY_COLUMN) and aSQL_ATTR_ROW_BIND_OFFSET_PTRset, every row of a rowset is written to the same address: the bound address plus the offset, with no per-row stride.SQLFetchreturnsSQL_SUCCESS, the rows-fetched counter says 4 and the status array holds fourSQL_ROW_SUCCESS, but the application's arrays contain only the last row of the rowset, in their first (shifted) element; the other elements are untouched. Silent data loss.Row-wise binding with the same offset pointer works. Column-wise binding without an offset pointer works.
Environment
Windows x64, master
35fae3e(with #303 applied), Firebird 5.0.3, Microsoft driver manager. The code involved is platform-independent.Reproduction
BlockCursorTest.ColumnWiseBindingWithOffsetin #305, currentlyGTEST_SKIPped with this defect as the reason. Shape: six-rowUNION ALL,SQL_ATTR_ROW_ARRAY_SIZE = 4,SQLBindCol(1, SQL_C_SLONG, values, 0, indicators), offset = 8 ×sizeof(SQLLEN)bytes (64 on x64, so the rows should land atvalues[16..19]andindicators[8..11]).Observed after the first
SQLFetch:Second fetch with the offset changed to 0 (expected
values[0..1] = 5, 6):values[0] = 6,values[1]untouched.Cause
OdbcStatement::getSchemaFetchData()selects the row-wise fetch branch whenever a bind-offset pointer is set, not only for row-wise binding:That branch of
fetchData()(and of its twin insqlFetchScrollCursorStatic()) uses one byte offset for every column and advances it byheadBindTypeper row:bindOffsetPtrTmp += rowBindType; // 0 for SQL_BIND_BY_COLUMNColumn-wise arrays need the other branch, where
returnDataFromExtendedFetch()steps each column by its own element size (sizeColumnExtendedFetch * row) and the indicators bysizeof(SQLLEN). That branch never adds the application's offset, though, and it reuses the aliasedheadBindOffsetPtras its row counter, so it cannot simply be selected when an offset pointer is present.Expected
The offset is added to every bound data and indicator address, and the rows still step by element size (ODBC,
SQL_ATTR_ROW_BIND_OFFSET_PTR).Suggested fix
Take the row-wise branch only when
headBindType != 0. In the column-wise branch, read the application's offset once at the start of the fetch, start the row counter at 0, and add the offset to both bases inreturnDataFromExtendedFetch():bindOffsetPtrTo = offset + sizeColumnExtendedFetch * row,bindOffsetPtrIndTo = offset + row * sizeof(SQLLEN). Same change in the static-cursor twin. Then unskip the test in #305.Related: #299 is the same class of defect on the parameter side.