@@ -72,7 +72,12 @@ use crate::{
7272/// - `HY009` Invalid argument value — (driver-manager-handled; not returned here)
7373/// - `HY010` Function sequence error — (driver-manager-handled; not returned here)
7474/// - `HY013` Memory management error — (driver-manager-handled; not returned here)
75- /// - `HY021` Inconsistent descriptor information — (driver-manager-handled; not returned here)
75+ /// - `HY021` Inconsistent descriptor information — **returned by this driver**. The row
76+ /// carries no `(DM)` marker, and `SQLSetDescRec`'s "Consistency Checks" section says when
77+ /// the check runs: "This check is always performed when **SQLBindParameter** or
78+ /// **SQLBindCol** is called". Both halves of the binding are checked before either
79+ /// descriptor is written, so a rejected bind leaves neither changed
80+ /// (`crate::descriptor::consistency_check`).
7681/// - `HY090` Invalid string or buffer length — (driver-manager-handled; not returned here)
7782/// - `HY104` Invalid precision or scale value — a driver-returned code (the spec does not mark it
7883/// (DM)): `column_size` and `decimal_digits` are stored verbatim without range validation.
@@ -356,6 +361,11 @@ pub unsafe fn sql_num_params<B: Backend>(
356361/// - 01000: General warning — (driver-manager-handled; not returned here).
357362/// - 07009: Invalid descriptor index — returned when `parameter_number` is 0 or exceeds
358363/// the number of parameter markers in the prepared statement.
364+ /// - 21S01: Insert value list does not match column list — not returned here. The row is
365+ /// about an `INSERT` whose parameter count differs from the target table's column count,
366+ /// which needs the data source's catalog: core parses the statement only far enough to
367+ /// count `?` markers (`count_params`) and never resolves a table. A backend that describes
368+ /// parameters itself is where this would originate.
359369/// - 08S01: Communication link failure — not applicable (no backend query).
360370/// - HY000: General error — returned for unexpected failures.
361371/// - HY001: Memory allocation error — not applicable; Rust allocation panics are caught by `panic_safe`.
@@ -1303,6 +1313,13 @@ unsafe fn dae_nts_byte_count(c_type: Option<odbc_sys::CDataType>, data_ptr: *con
13031313/// - 01000: General warning — (driver-manager-handled; not returned here).
13041314/// - 01004: String data, right truncated — not applicable; data is accumulated without
13051315/// truncation.
1316+ /// - 07006: Restricted data type attribute violation — not returned here. The pairing is
1317+ /// fixed at `SQLBindParameter`, which refuses the C-to-SQL combinations core cannot convert
1318+ /// before the query runs (`crate::binary_convert`, `crate::numeric_convert`), so a chunk
1319+ /// arriving here is already of a pairing that was accepted.
1320+ /// - 08S01: Communication link failure — not returned here. `SQLPutData` accumulates into a
1321+ /// buffer on the statement handle and makes no backend call at all; the link is next touched
1322+ /// by the `SQLParamData` that completes the execution, which is where this arrives.
13061323/// - 22001: String data, right truncation — not applicable; no target column size check
13071324/// at this stage.
13081325/// - 22003: Numeric value out of range — not applicable; type conversion happens at execute
@@ -1318,9 +1335,17 @@ unsafe fn dae_nts_byte_count(c_type: Option<odbc_sys::CDataType>, data_ptr: *con
13181335/// - HY000: General error — returned for unexpected failures.
13191336/// - HY001: Memory allocation error — not applicable; Rust allocation panics are caught by
13201337/// `panic_safe`.
1321- /// - HY009: Invalid use of null pointer — returned when `data_ptr` is null but
1322- /// `str_len_or_ind` is neither `SQL_NULL_DATA` nor `SQL_DEFAULT_PARAM`, the two values
1323- /// that carry the whole parameter and so need no buffer.
1338+ /// - HY008: Operation canceled — not returned here. This call makes no fallible backend call,
1339+ /// so there is no error for a cancellation to be reported through, and the asynchronous
1340+ /// clause is inapplicable: core never returns `SQL_STILL_EXECUTING`. A `SQLCancel` during a
1341+ /// data-at-execution sequence discards the sequence; the following `SQLParamData` is what
1342+ /// reports it.
1343+ /// - HY009: Invalid use of null pointer — (DM) the row is driver-manager-marked. Core keeps a
1344+ /// guard anyway, because unixODBC does not always run it and the alternative is
1345+ /// dereferencing a null pointer, and the guard matches the clause exactly: "(DM) The
1346+ /// argument DataPtr was a null pointer, and the argument StrLen_or_Ind was not 0,
1347+ /// SQL_DEFAULT_PARAM, or SQL_NULL_DATA." A null pointer with a length of 0 is a legal
1348+ /// zero-length put and is accepted.
13241349/// - HY010: Function sequence error — returned when no data-at-execution is in progress
13251350/// (no prior `SQL_NEED_DATA` from `SQLExecute`/`SQLExecDirectW`), or when
13261351/// `SQLParamData` has not yet been called to identify the current parameter.
@@ -1463,9 +1488,16 @@ pub unsafe fn sql_put_data<B: Backend>(
14631488 return Ok ( SqlReturn :: SUCCESS ) ;
14641489 }
14651490
1466- // Spec HY009: data_ptr must not be null (unless SQL_NULL_DATA or
1467- // SQL_DEFAULT_PARAM, both handled above).
1468- if data_ptr. is_null ( ) {
1491+ // A refusal to dereference a null pointer, not a spec check: the
1492+ // row is `(DM)`-marked and belongs to the Driver Manager. It is
1493+ // kept because unixODBC does not always run it, and it is written
1494+ // to match the clause exactly — "(DM) The argument DataPtr was a
1495+ // null pointer, and the argument StrLen_or_Ind was not 0,
1496+ // SQL_DEFAULT_PARAM, or SQL_NULL_DATA". The other two values are
1497+ // handled above, so only the zero remains, and a null pointer with
1498+ // a length of zero is a legal zero-length put that this guard used
1499+ // to refuse.
1500+ if data_ptr. is_null ( ) && str_len_or_ind != 0 {
14691501 return Err ( OdbcError :: general (
14701502 "DataPtr is null" ,
14711503 SqlState :: invalid_use_of_null_pointer ( ) ,
@@ -1490,9 +1522,17 @@ pub unsafe fn sql_put_data<B: Backend>(
14901522 str_len_or_ind as usize
14911523 } ;
14921524
1493- // SAFETY: caller guarantees data_ptr is valid for byte_count bytes.
1494- let data = std:: slice:: from_raw_parts ( data_ptr as * const u8 , byte_count) ;
1495- dae. buffer . extend_from_slice ( data) ;
1525+ if byte_count > 0 {
1526+ // SAFETY: caller guarantees data_ptr is valid for byte_count
1527+ // bytes, and it is non-null: the guard above admits a null only
1528+ // with a length of zero, which this branch excludes.
1529+ // `from_raw_parts` on a null pointer is undefined behaviour
1530+ // even at length zero.
1531+ let data = std:: slice:: from_raw_parts ( data_ptr as * const u8 , byte_count) ;
1532+ dae. buffer . extend_from_slice ( data) ;
1533+ }
1534+ // A zero-length put is still a put: it is what distinguishes an
1535+ // empty value from a parameter nobody supplied.
14961536 dae. put_state = PutDataState :: Data ;
14971537
14981538 Ok ( SqlReturn :: SUCCESS )
@@ -1548,6 +1588,10 @@ pub unsafe fn sql_put_data<B: Backend>(
15481588/// backend.
15491589/// - 23000: Integrity constraint violation — propagated from backend.
15501590/// - 24000: Invalid cursor state — propagated from backend.
1591+ /// - 22026: String data, length mismatch — not returned here. The row's condition opens with
1592+ /// "The SQL_NEED_LONG_DATA_LEN information type in `SQLGetInfo` was 'Y'", and core answers
1593+ /// `"N"` for it (`default_get_info`), so the driver never asked the application to declare a
1594+ /// long parameter's length in advance and has nothing to compare against.
15511595/// - 40001: Serialization failure — propagated from backend.
15521596/// - 40003: Statement completion unknown — propagated from backend.
15531597/// - 42000: Syntax error or access violation — propagated from backend.
@@ -2492,6 +2536,64 @@ mod tests {
24922536 }
24932537 }
24942538
2539+ /// The spec's clause, read as written: "(DM) The argument DataPtr was a
2540+ /// null pointer, and the argument StrLen_or_Ind was **not** 0,
2541+ /// SQL_DEFAULT_PARAM, or SQL_NULL_DATA." A null pointer with a length of
2542+ /// zero is therefore a legal zero-length put, and core's own guard — which
2543+ /// exists to avoid dereferencing a null pointer, not to enforce a `(DM)`
2544+ /// row — must not be stricter than the clause it stands in for.
2545+ #[ test]
2546+ fn put_data_accepts_a_null_pointer_with_a_zero_length ( ) {
2547+ unsafe {
2548+ let ( env, conn, stmt) = connected_stmt ( ) ;
2549+ let mut indicator: isize = SQL_DATA_AT_EXEC ;
2550+ start_dae_loop (
2551+ stmt,
2552+ CDataType :: Char ,
2553+ SqlDataType :: VARCHAR ,
2554+ & raw mut indicator,
2555+ ) ;
2556+
2557+ assert_eq ! (
2558+ sql_put_data:: <MockBackend >( stmt, std:: ptr:: null_mut( ) , 0 ) ,
2559+ SqlReturn :: SUCCESS ,
2560+ ) ;
2561+ assert ! (
2562+ dae_buffer( stmt) . is_empty( ) ,
2563+ "a zero-length put appends nothing" ,
2564+ ) ;
2565+
2566+ cleanup ( env, conn, stmt) ;
2567+ }
2568+ }
2569+
2570+ /// The half of the clause that stays: a null pointer with a real length is
2571+ /// still HY009, because there is nothing to read those bytes from.
2572+ #[ test]
2573+ fn put_data_rejects_a_null_pointer_with_a_real_length ( ) {
2574+ unsafe {
2575+ let ( env, conn, stmt) = connected_stmt ( ) ;
2576+ let mut indicator: isize = SQL_DATA_AT_EXEC ;
2577+ start_dae_loop (
2578+ stmt,
2579+ CDataType :: Char ,
2580+ SqlDataType :: VARCHAR ,
2581+ & raw mut indicator,
2582+ ) ;
2583+
2584+ assert_eq ! (
2585+ sql_put_data:: <MockBackend >( stmt, std:: ptr:: null_mut( ) , 3 ) ,
2586+ SqlReturn :: ERROR ,
2587+ ) ;
2588+ with_handle :: < MockBackend , StatementHandle < MockBackend > , _ > ( stmt, |h| {
2589+ let rec = h. diagnostics . get ( 0 ) . expect ( "record 1 exists" ) ;
2590+ assert_eq ! ( rec. sqlstate. as_str( ) , "HY009" ) ;
2591+ } ) ;
2592+
2593+ cleanup ( env, conn, stmt) ;
2594+ }
2595+ }
2596+
24952597 /// `CStr::from_ptr` scans without a bound, so a buffer whose terminator is
24962598 /// missing is read past its own allocation — the only such scan in the
24972599 /// crate, since `utf16_to_string` caps its own at `MAX_NTS_SCAN`.
0 commit comments