Skip to content

Commit 2b7f667

Browse files
adwk67claude
andcommitted
fix: a zero-length SQLGetData buffer reports truncation instead of consuming the column
write_wchar/write_char/write_binary shared one branch for a null target and a zero-length buffer, answering SQL_SUCCESS with nothing written — and sql_get_data marks a column done on any non-SUCCESS_WITH_INFO return, so the standard length-probe idiom (call with BufferLength 0 to read the indicator, then call again with a real buffer) got SQL_NO_DATA on the second call and the value was unrecoverable. The two cases are now split the way write_utf16 already ruled: null target stays a pure length query (SUCCESS), a non-null target with no room is total truncation (SUCCESS_WITH_INFO + 01004) and the cursor stays resumable. The same split reaches SQLFetch's bound columns through the shared writers, so a BufferLength 0 binding now reports 01004 too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 45ecd39 commit 2b7f667

3 files changed

Lines changed: 351 additions & 15 deletions

File tree

CHANGELOG.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3085,4 +3085,30 @@ call `SQLCloseCursor` or `SQLFreeStmt(SQL_CLOSE)` first, as it already must for
30853085
function returns changed; the previously-unguarded path now returns one
30863086
(`HY000`) instead of aborting.
30873087

3088+
- **`SQLGetData` with a zero-length buffer no longer consumes the column.**
3089+
`write_wchar`, `write_char` and `write_binary` shared one branch for a null
3090+
`target_value_ptr` (a pure length query) and a non-null one with
3091+
`buffer_length` 0 (the standard "how large a buffer do I need" probe): both
3092+
returned plain `SQL_SUCCESS`. `SQLGetData`'s own `cursor.done` is derived
3093+
from that return value — anything other than `SQL_SUCCESS_WITH_INFO` marks a
3094+
chunkable column exhausted — so the probe's `SQL_SUCCESS` silently closed the
3095+
column, and the documented follow-up call with a buffer sized from the
3096+
reported length got `SQL_NO_DATA` instead of the value. The spec's own step
3097+
5 already draws this line: "If the data buffer supplied is too small to hold
3098+
the null-termination character, SQLGetData returns SQL_SUCCESS_WITH_INFO and
3099+
SQLSTATE 01004" — a zero-length buffer is always too small to hold it, so
3100+
it is the same case as any other partial write, not a length query. The
3101+
three writers now split the same way `write_utf16` already does elsewhere in
3102+
this crate: null target stays `SQL_SUCCESS`, non-null target with no room
3103+
(`buffer_length <= 0`, or `< 2` for `SQL_C_WCHAR`'s two-byte terminator)
3104+
becomes `SQL_SUCCESS_WITH_INFO` with `01004` and the cursor left resumable.
3105+
3106+
**For driver authors:** the same three writers also serve `SQLFetch`'s
3107+
bound-column path (`SQLBindCol` with `BufferLength` 0), which now reports
3108+
`SQL_SUCCESS_WITH_INFO`/`01004` for that row instead of silently discarding
3109+
the value — the same shared-branch bug, on a call shape no test in this
3110+
crate previously exercised. A `SQLGetData` call with a non-zero
3111+
`BufferLength`, or any bound column with a real `BufferLength`, is
3112+
unaffected.
3113+
30883114
[Unreleased]: https://github.com/stackabletech/stackable-odbc-core/commits/HEAD

src/column_value.rs

Lines changed: 122 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -895,15 +895,37 @@ unsafe fn write_wchar(
895895
unsafe { std::ptr::write_unaligned(len_ind_ptr, total_bytes) };
896896
}
897897

898-
if target_ptr.is_null() || buf_len <= 0 {
898+
// A null target is not something SQLGetData's own spec sanctions — its
899+
// Arguments section is explicit that "TargetValuePtr cannot be NULL."
900+
// The case that actually reaches this branch comes from this function's
901+
// *other* caller: `sql_fetch`'s bound-column loop (`ffi/fetch.rs`)
902+
// legitimately passes a null data pointer when `SQL_DESC_DATA_PTR` is
903+
// null but `SQL_DESC_INDICATOR_PTR` is not — the indicator-only binding
904+
// the spec allows ("An application can unbind the data buffer for a
905+
// column but still have a length/indicator buffer bound for the
906+
// column"), which `collect_bindings` deliberately keeps and
907+
// `fetch_writes_the_indicator_of_an_indicator_only_binding` pins. That
908+
// caller still wants the length written to `len_ind_ptr` above, with
909+
// nothing written to a buffer that does not exist, so this returns
910+
// SUCCESS rather than treating the null pointer as an error this deep in
911+
// the call stack; `SQLGetData`'s own `target_value_ptr` null case is
912+
// `HY009`, deliberately left unchecked at the FFI boundary (see
913+
// `sql_get_data`'s doc comment) rather than enforced here.
914+
if target_ptr.is_null() {
899915
return Ok((SqlReturn::SUCCESS, 0));
900916
}
901917

902-
// The null terminator is one UTF-16 code unit, so a buffer of fewer than
903-
// two bytes cannot hold it. Writing one anyway would overrun the caller's
904-
// buffer. Spec: "If the data buffer supplied is too small to hold the
905-
// null-termination character, SQLGetData returns SQL_SUCCESS_WITH_INFO
906-
// and SQLSTATE 01004."
918+
// A non-null target with fewer than two bytes of room — including
919+
// exactly zero, the standard "how big a buffer do I need" probe — cannot
920+
// hold even the one-UTF-16-code-unit null terminator. That is total
921+
// truncation, not a length query: the application supplied somewhere to
922+
// write and nothing was written there. Spec: "If the data buffer
923+
// supplied is too small to hold the null-termination character,
924+
// SQLGetData returns SQL_SUCCESS_WITH_INFO and SQLSTATE 01004." Reporting
925+
// plain SUCCESS here (as a shared branch with the null-target case above
926+
// used to) made SQLGetData indistinguishable from "this column is fully
927+
// delivered," which permanently stranded the data behind a `buf_len == 0`
928+
// probe: `cursor.done` is derived from this return value.
907929
if buf_len < 2 {
908930
return Ok((SqlReturn::SUCCESS_WITH_INFO, 0));
909931
}
@@ -957,10 +979,21 @@ unsafe fn write_char(
957979
unsafe { std::ptr::write_unaligned(len_ind_ptr, total_bytes) };
958980
}
959981

960-
if target_ptr.is_null() || buf_len <= 0 {
982+
// A null target here is the bound-column caller's indicator-only
983+
// binding, not something SQLGetData's own spec permits — see
984+
// `write_wchar`'s full reasoning.
985+
if target_ptr.is_null() {
961986
return Ok((SqlReturn::SUCCESS, 0));
962987
}
963988

989+
// A non-null target with no room in it — including exactly zero, the
990+
// standard length-probe — cannot hold even the one-byte null terminator,
991+
// which is total truncation (SUCCESS_WITH_INFO / 01004), not a length
992+
// query. See `write_wchar`'s identical split.
993+
if buf_len <= 0 {
994+
return Ok((SqlReturn::SUCCESS_WITH_INFO, 0));
995+
}
996+
964997
let out_ptr = target_ptr.cast::<u8>();
965998
let capacity = (buf_len as usize).saturating_sub(1); // reserve one for null terminator
966999
let copy_count = bytes.len().min(capacity);
@@ -1001,10 +1034,23 @@ unsafe fn write_binary(
10011034
unsafe { std::ptr::write_unaligned(len_ind_ptr, total_bytes) };
10021035
}
10031036

1004-
if target_ptr.is_null() || buf_len <= 0 {
1037+
// A null target here is the bound-column caller's indicator-only
1038+
// binding, not something SQLGetData's own spec permits — see
1039+
// `write_wchar`'s full reasoning.
1040+
if target_ptr.is_null() {
10051041
return Ok((SqlReturn::SUCCESS, 0));
10061042
}
10071043

1044+
// A non-null target with no room in it — including exactly zero, the
1045+
// standard length-probe — cannot hold any of the data, which is total
1046+
// truncation (SUCCESS_WITH_INFO / 01004), not a length query. Binary
1047+
// reserves no terminator, so unlike the two character writers there is no
1048+
// extra "room for one more byte" boundary; `buf_len <= 0` is the whole
1049+
// condition. See `write_wchar`'s identical split.
1050+
if buf_len <= 0 {
1051+
return Ok((SqlReturn::SUCCESS_WITH_INFO, 0));
1052+
}
1053+
10081054
let out_ptr = target_ptr.cast::<u8>();
10091055
let copy_count = data.len().min(buf_len as usize);
10101056

@@ -1888,7 +1934,10 @@ mod tests {
18881934

18891935
#[test]
18901936
fn wchar_zero_length_buffer_reports_size_and_writes_nothing() {
1891-
// buf_len == 0 is a length query: report the byte count, write nothing.
1937+
// A non-null target with buf_len == 0 has no room for even the null
1938+
// terminator, which is total truncation (SUCCESS_WITH_INFO / 01004),
1939+
// not a length query. Only a null target is a length query
1940+
// (SUCCESS) — see `write_utf16`'s identical split.
18921941
let mut buf = [0xAAu8; 4];
18931942
let mut ind: isize = 0;
18941943
let ret = unsafe {
@@ -1900,11 +1949,74 @@ mod tests {
19001949
&mut ind,
19011950
)
19021951
};
1903-
assert_eq!(ret.unwrap(), SqlReturn::SUCCESS);
1952+
assert_eq!(ret.unwrap(), SqlReturn::SUCCESS_WITH_INFO);
19041953
assert_eq!(ind, 10); // 5 chars * 2 bytes, still reported
19051954
assert_eq!(buf, [0xAA; 4], "wrote into a zero-length buffer");
19061955
}
19071956

1957+
#[test]
1958+
fn wchar_null_target_with_zero_length_is_a_pure_length_query() {
1959+
// A null target pointer stays SUCCESS regardless of buf_len. Not
1960+
// something SQLGetData's own spec sanctions directly — its Arguments
1961+
// section says "TargetValuePtr cannot be NULL" — but this writer is
1962+
// shared with `sql_fetch`'s bound-column loop, where a null
1963+
// `SQL_DESC_DATA_PTR` paired with a live indicator pointer is the
1964+
// spec-legal indicator-only binding (see `write_wchar`'s doc
1965+
// comment on this branch for the full reasoning).
1966+
let mut ind: isize = 0;
1967+
let ret = unsafe {
1968+
write_column_value(
1969+
&ColumnValue::String("hello".into()),
1970+
CDataType::WChar,
1971+
std::ptr::null_mut(),
1972+
0,
1973+
&mut ind,
1974+
)
1975+
};
1976+
assert_eq!(ret.unwrap(), SqlReturn::SUCCESS);
1977+
assert_eq!(ind, 10);
1978+
}
1979+
1980+
#[test]
1981+
fn char_zero_length_buffer_reports_size_and_writes_nothing() {
1982+
// The write_char sibling of the wchar case above.
1983+
let mut buf = [0xAAu8; 4];
1984+
let mut ind: isize = 0;
1985+
let ret = unsafe {
1986+
write_column_value(
1987+
&ColumnValue::String("hello".into()),
1988+
CDataType::Char,
1989+
buf.as_mut_ptr() as *mut c_void,
1990+
0,
1991+
&mut ind,
1992+
)
1993+
};
1994+
assert_eq!(ret.unwrap(), SqlReturn::SUCCESS_WITH_INFO);
1995+
assert_eq!(ind, 5);
1996+
assert_eq!(buf, [0xAA; 4], "wrote into a zero-length buffer");
1997+
}
1998+
1999+
#[test]
2000+
fn binary_zero_length_buffer_reports_size_and_writes_nothing() {
2001+
// The write_binary sibling of the wchar case above. Binary has no
2002+
// null terminator, so the "no room to make progress" condition is
2003+
// simply buf_len <= 0 rather than needing 2 bytes.
2004+
let mut buf = [0xAAu8; 4];
2005+
let mut ind: isize = 0;
2006+
let ret = unsafe {
2007+
write_column_value(
2008+
&ColumnValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]),
2009+
CDataType::Binary,
2010+
buf.as_mut_ptr() as *mut c_void,
2011+
0,
2012+
&mut ind,
2013+
)
2014+
};
2015+
assert_eq!(ret.unwrap(), SqlReturn::SUCCESS_WITH_INFO);
2016+
assert_eq!(ind, 4);
2017+
assert_eq!(buf, [0xAA; 4], "wrote into a zero-length buffer");
2018+
}
2019+
19082020
#[test]
19092021
fn wchar_buffer_holding_only_the_null_terminator_is_written() {
19102022
// Two bytes is exactly one UTF-16 code unit: room for the terminator

0 commit comments

Comments
 (0)