Skip to content

fix(bulk): don't write a length byte for DATE in TYPE_INFO - #450

Closed
nandoxlsm wants to merge 1 commit into
tiberius-rs:mainfrom
nandoxlsm:fix/date-type-info-extra-length-byte
Closed

nandoxlsm wants to merge 1 commit into
tiberius-rs:mainfrom
nandoxlsm:fix/date-type-info-extra-length-byte

Conversation

@nandoxlsm

@nandoxlsm nandoxlsm commented Sep 17, 2026 •

Copy link
Copy Markdown

Fixes the root cause behind #410 and #373: a DATE column makes a bulk load fail with Invalid column type from bcp client for colid <N+1> (error 4816), always blaming the column after the date.

The bug

Encode for VarLenContext writes a length byte for VarLenType::Daten:

#[cfg(feature = "tds73")]
VarLenType::Daten
| VarLenType::Timen
| VarLenType::DatetimeOffsetn
| VarLenType::Datetime2 => {
    dst.put_u8(self.len() as u8);
}

The decoder in the same file already implements the correct rule:

#[cfg(feature = "tds73")]
VarLenType::Timen | VarLenType::DatetimeOffsetn | VarLenType::Datetime2 => {
    src.read_u8().await? as usize
}
#[cfg(feature = "tds73")]
VarLenType::Daten => 3,

Per MS-TDS, TIMENTYPE, DATETIME2NTYPE and DATETIMEOFFSETNTYPE carry a 1-byte SCALE in TYPE_INFO; DATENTYPE carries nothing — it is a fixed 3-byte value. So every DATE column adds one spurious byte to the bulk COLMETADATA, the server reads everything after it shifted by one byte, and the load is rejected naming the next column.

The test

The existing round_trip test cannot catch this: with a single type info, the stray byte is simply left in the buffer and the value still decodes back equal. COLMETADATA is a sequence, so the new test encodes a DATE followed by another column and decodes both — which fails on main and passes with the fix:

test tds::codec::type_info::tests::round_trip_sequence_of_date_and_the_column_after_it ... FAILED   (without the fix)
test tds::codec::type_info::tests::round_trip_sequence_of_date_and_the_column_after_it ... ok       (with it)

cargo test --features "tds73 tokio" --lib → 213 passed, 0 failed. cargo fmt clean.

How it was found

Bulk-loading a 47-column table into Azure SQL where column 4 was DATE: the server reported colid 5. The same table with every column as NVARCHAR(MAX) loaded 313,465 rows without a problem, and changing only that one column to DATETIME2 made the typed load succeed — which is what pointed at the metadata rather than the values.

DATENTYPE carries neither length nor scale in TYPE_INFO — it is a fixed
3-byte value — but the encoder grouped it with TIMENTYPE, DATETIME2NTYPE and
DATETIMEOFFSETNTYPE, which do carry a 1-byte scale. The decoder in the same
file already implements the correct rule (`VarLenType::Daten => 3`, with no
`read_u8`).

Every DATE column therefore added one spurious byte to the bulk COLMETADATA.
The server read the remaining column metadata shifted by one byte and rejected
the load with "Invalid column type from bcp client for colid N+1", blaming the
column after the date (tiberius-rs#410, tiberius-rs#373).

The new test encodes a sequence, which is what COLMETADATA is: a single round
trip cannot catch this, because the stray byte still decodes back to the same
value and only the next column goes wrong.
@MattJackson

MattJackson commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

Thanks @nandoxlsm — your diagnosis lines up with ours. The same fix looks to already be in #443 (in review): DATE is split out of the TIME/DATETIME2/DATETIMEOFFSET scale-byte arm so it emits no length byte, matching MS-TDS. It's covered by a round-trip test that asserts a DATE TYPE_INFO encodes to just its type token with no trailing scale byte — which fails on main and passes with the fix.

Closing as a duplicate of #443 — but thank you for the independent, clear write-up. Following up on #373/#410 there.

@nandoxlsm

Copy link
Copy Markdown
Author

Thanks @MattJackson — checked #443 and you're right, it's the same fix with the same reasoning (and your comment documents the spec sections better than mine did). Happy to defer to it.

One practical note, in case it's useful: #443 targets stack/s4, and main still has the old arm — so anyone hitting #373/#410 today has no fix to pick up, not even from git. If it would help to land the one-liner on main independently while the stack works its way through, mine is isolated and carries nothing else; otherwise I'm happy to leave it closed.

Either way: we hit this against a live Azure SQL with a 47-column table (DATE in position 4, error at colid 5), so if it's ever useful I can verify a release candidate against a real workload once the fix lands.

@MattJackson

Copy link
Copy Markdown
Contributor

I agree, we have a large backlog and its is moving just slowly due the amount of fixes it all contains. Ill speak with @aqrln and others to see if we should just steam ahead on the stack or if we should be picking a few off into small commits. Im hoping that the stack will proceed, but its about 1 a week right now so not the pace we want or need it to be.

@aqrln

aqrln commented Sep 23, 2026

Copy link
Copy Markdown
Member

@MattJackson yeah this is not sustainable, I don't have as much capacity as I hoped for to properly review this.

If you already reviewed this code before, I think we should just go ahead and merge it. We need to ship the new release to resolve the security issues. Let me know if there's anything in particular I should pay closer attention to or that you haven't reviewed yourself before, but otherwise feel free to merge.

@MattJackson

Copy link
Copy Markdown
Contributor

Agreed. I have merged the stack and closed out the issues. Ill review remaining issues and PRs to see whats really open but we should be at a much cleaner state now. Please push a new crate when available, didn't see that as part of any CI @aqrln

@aqrln

aqrln commented Sep 25, 2026

Copy link
Copy Markdown
Member

@MattJackson ah here's where this thread was, I was struggling to find it, it wasn't showing up in the search results somehow. Only just now seeing your response here. I published the crates and opened this issue today: #454

We should connect on Discord or something so it's easier to coordinate. Maybe even create a Discord server for the community.

@MattJackson

Copy link
Copy Markdown
Contributor

Agreed. for now email me (on my profile) and lets connect. Discord sounds good long term, ill add it to list :)

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants