Skip to content

Oversized decimal in SQLGetData reports SQLSTATE 07006 instead of 22003 #281

Description

Describe the bug

An oversized decimal/numeric value converted through SQLGetData into a numeric C type reports SQLSTATE 07006 ("restricted data type attribute violation") instead of 22003 ("numeric value out of range").

07006 means the requested C type is not a legal target for this SQL type. But decimalSQL_C_SLONG is a legal pairing — it is this particular value that will not fit. An application branching on SQLSTATE to distinguish "I asked for the wrong type" from "this row had a value too big for the buffer I gave you" gets the wrong answer, and the first is not retryable per-row while the second is.

The root cause is that numeric_source in mssql-odbc/src/api/fetch_convert.rs returns Option, so two distinct conditions collapse into the same None:

ColumnValues::Decimal(d) | ColumnValues::Numeric(d) => {
    let m = i128::try_from(d.magnitude()?).ok()?;   // <- both failures return None
    ...
}

and the sole caller maps every None to 07006:

numeric_source(value).ok_or(ConvError::Restricted)   // fetch_convert.rs:255

None legitimately means 07006 for a binary or GUID column, which has no numeric interpretation at all. For a decimal column it means the value overflowed. Swapping the error at the Decimal arm alone is not enough — numeric_source needs to distinguish the two, e.g. by returning Result<NumericSource, ConvError> or by having the decimal arm signal out-of-range separately.

The same function already returns 22003 for the analogous character-source case (fetch_convert.rs:250), so fixing this also makes the function self-consistent.

This is reachable from the wire, not only through FFI. Two failure modes reach the ?:

  1. d.magnitude() returns None for more than four significant words. After Guard decimal magnitude reassembly against 128-bit shift overflow #237 the wire decoder caps the count at 4, so this one is FFI-only.
  2. i128::try_from fails for a four-word magnitude above i128::MAX. The wire decoder validates the word count, not that the magnitude fits the column's declared precision, so a 17-byte payload — a legal length — carrying a magnitude in (i128::MAX, u128::MAX] is accepted by the decoder and fails here. A valid decimal(38, s) never exceeds 10^38 - 1 ≈ 2^126.2, so this is a malformed or hostile payload, but it is one a server can send.

Case 2 also means the string and typed paths disagree about the same value: SQL_C_CHAR renders it, SQL_C_SLONG reports the conversion as illegal.

Steps to reproduce

Verified against mssql-odbc at 39532382 with a temporary unit test (since case 2 needs a malformed payload a real server will not normally send):

use mssql_tds::datatypes::decoder::DecimalParts;

// A 17-byte wire payload of all 0xFF: 4 words, legal length, magnitude u128::MAX.
let d = DecimalParts {
    is_positive: true,
    scale: 0,
    precision: 38,
    int_parts: vec![-1, -1, -1, -1],
};

let mut out: i64 = 0;
let mut ind: SqlLen = 0;
let err = unsafe {
    convert_integer_c(
        &ColumnValues::Decimal(d),
        SQL_C_SBIGINT,
        (&mut out as *mut i64).cast(),
        &mut ind,
    )
};

Observed output:

magnitude       = Some(340282366920938463463374607431768211455)
i128::try_from  = false
Display         = 340282366920938463463374607431768211455
convert result  = Err(Restricted)      // -> SQLSTATE 07006

Equivalently over a live connection: have a server send a decimal(38, 0) column whose 16-byte magnitude exceeds i128::MAX, then SQLGetData that column into SQL_C_SBIGINT.

Expected behavior

SQLGetData returns SQL_ERROR with SQLSTATE 22003, "Numeric value out of range" — the conversion is legal, the value does not fit.

07006 should remain reserved for source/target pairings that are genuinely illegal, such as a binary or GUID column into a numeric C type.

Actual behavior

SQLGetData returns SQL_ERROR with SQLSTATE 07006, "Restricted data type attribute violation", implying decimal cannot be fetched as an integer at all.

Internally: ConvError::Restricted rather than ConvError::OutOfRange.

Version

mssql-odbc 0.1.0, commit 39532382 (branch for #237)

Affected crate

mssql-odbc

Note: mssql-odbc is not in the "Affected crate" dropdown in .github/ISSUE_TEMPLATE/bug_report.yml, which still lists only mssql-tds, mssql-js, mssql-tds-cli, mssql-mock-tds, and mssql-py-core. Worth adding separately.

Additional context

Came out of review discussion on #237 (comment). The 07006 behavior arrived with #217; #237 made numeric_source the single shared entry point for both the string and typed paths, so the fix is now in one place rather than two.

Notes for whoever picks this up:

  • Not a drive-by change. A returned SQLSTATE is observable behavior an application can branch on, which is why it was deliberately kept out of Guard decimal magnitude reassembly against 128-bit shift overflow #237. It deserves its own PR.
  • One unit test pins the current behavior: decimal_limbs_are_reassembled_and_bounded in fetch_convert.rs asserts ConvError::Restricted for vec![1, 0, 0, 0, 1]. It will need updating, and it is the right place to add a case for the wire-reachable i128::MAX boundary above, which nothing currently covers.
  • I grepped for 07006 across mssql-odbc: no ODBC e2e test pins it for the decimal path, so the blast radius is that one unit test. bind_param.rs's unsupported_conversion_returns_07006 is a different code path and unaffected.
  • Worth checking msodbcsql parity while in here — the pipeline already runs a parity comparison against the reference driver, so it can confirm which SQLSTATE msodbcsql returns for the same input rather than reasoning from the spec alone.
  • Consider whether the decoder should also range-check a decimal magnitude against its declared precision, which would close case 2 at the wire instead of at the conversion. That is arguably a separate mssql-tds issue, and is related in spirit to mssql-tds: SQL_VARIANT narrows a u32 data length to u8, desynchronizing the token stream #280.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions