@@ -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