feat: add packet_size configuration for LOGIN7 message - #400
Conversation
Add the ability to configure the TDS packet size in the LOGIN7 message. Larger packet sizes can significantly improve bulk insert performance by reducing network round-trips and protocol overhead. The default packet size remains 4096 bytes for backwards compatibility. Valid values are 512 to 32767 bytes. The server may negotiate a different size than requested. Example usage: ```rust let mut config = Config::new(); config.packet_size(32767); // Request 32KB packets ``` Performance testing showed that increasing packet size from 4KB to 16KB improved bulk insert throughput by ~40% (from 104K to 178K rows/sec for a 19.3M row dataset).
…erts Use a fork of Tiberius that supports configurable TDS packet size. Increasing from default 4KB to 32KB (server negotiates to 16KB on Linux) provides a significant performance improvement for bulk insert operations. Benchmark results (PG→MSSQL, 19.3M rows): - Before (4KB packets): ~186s, 104K rows/sec - After (16KB packets): ~108s, 178K rows/sec - Improvement: 42% faster The fork adds packet_size configuration to Tiberius Config and LoginMessage. PR submitted upstream: tiberius-rs/tiberius#400 Once the upstream PR is merged, we can switch back to the official tiberius crate. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…erts (#63) * perf: use Tiberius fork with 32KB packet size for 42% faster bulk inserts Use a fork of Tiberius that supports configurable TDS packet size. Increasing from default 4KB to 32KB (server negotiates to 16KB on Linux) provides a significant performance improvement for bulk insert operations. Benchmark results (PG→MSSQL, 19.3M rows): - Before (4KB packets): ~186s, 104K rows/sec - After (16KB packets): ~108s, 178K rows/sec - Improvement: 42% faster The fork adds packet_size configuration to Tiberius Config and LoginMessage. PR submitted upstream: tiberius-rs/tiberius#400 Once the upstream PR is merged, we can switch back to the official tiberius crate. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: extract TDS packet size to named constant Address Copilot review comments: - Add TDS_MAX_PACKET_SIZE constant (32767 bytes) with documentation - Clarify that SQL Server on Linux negotiates to 16KB - Document the 42% performance improvement vs default 4KB 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
|
Adding some cross-references in case they help prioritization:
This PR is the LOGIN7 piece of that same throughput story. The 42% improvement in the table above came from our high-throughput data migration tool (Rust → Postgres), where the 4KB default was the dominant bottleneck on the MSSQL read side. With 16KB packets the Stack Overflow 2010 dataset (19.3M rows across 10 tables) goes from ~104K to ~178K rows/sec end-to-end, putting Rust + tiberius at parity with go-mssqldb on the same workload — the only remaining difference between the two drivers here is configurability. The change is minimal (3 files, +32/-0), backwards compatible (default unchanged), and the server-negotiated value still flows through the existing |
Mirrors tiberius-rs/tiberius#400, adding the client-side range validation tasks.md flagged as missing from the original PR. Adds `Config::packet_size(size)`/`Config::get_packet_size()`, threaded through `Connection::login` into `LoginMessage::packet_size` (LOGIN7's `PacketSize` field), matching upstream's plumbing. Unlike upstream, `Config::packet_size` returns `crate::Result<()>` and rejects anything outside 512..=32767 - the valid range per the TDS LOGIN7 packet's PacketSize field, and the same range the original PR's own doc comment claimed but never enforced. This isn't just a documentation nicety: the actual wire-framing code (`Connection::write_all`/`send`) computes `context.packet_size() as usize - HEADER_BYTES`, an unchecked subtraction that would underflow for a packet size smaller than `HEADER_BYTES` (8); 512 is comfortably clear of that, but nothing stopped a caller from passing e.g. `4` without validation. The setting only affects the requested size in LOGIN7 - the server's ENVCHANGE response can still negotiate a different value, which `Context::set_packet_size` already applies before any wire framing happens. Added 7 unit tests for the validation boundaries (defaults, min, max, one below, one above, zero) and a manual live integration test (`connect_with_custom_packet_size`, built directly rather than through `#[test_on_runtimes]` since there's no connection-string equivalent for this setting) confirming a connection with a non-default packet size still works end-to-end. Verified: - cargo check: default, --features=rustls, --features=vendored-openssl, --no-default-features - cargo clippy --all-targets -- -D warnings, for default and rustls - cargo fmt --check - cargo test --lib (176 passed) - cargo test --doc against a live server (27 passed) - Live integration tests over rustls: 301 passed (98 bulk + 203 query, including the new packet-size test) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0156Di1tRRLsJK8ctU1AmAJr
Mirrors tiberius-rs/tiberius#400, adding the client-side range validation tasks.md flagged as missing from the original PR. Adds `Config::packet_size(size)`/`Config::get_packet_size()`, threaded through `Connection::login` into `LoginMessage::packet_size` (LOGIN7's `PacketSize` field), matching upstream's plumbing. Unlike upstream, `Config::packet_size` returns `crate::Result<()>` and rejects anything outside 512..=32767 - the valid range per the TDS LOGIN7 packet's PacketSize field, and the same range the original PR's own doc comment claimed but never enforced. This isn't just a documentation nicety: the actual wire-framing code (`Connection::write_all`/`send`) computes `context.packet_size() as usize - HEADER_BYTES`, an unchecked subtraction that would underflow for a packet size smaller than `HEADER_BYTES` (8); 512 is comfortably clear of that, but nothing stopped a caller from passing e.g. `4` without validation. The setting only affects the requested size in LOGIN7 - the server's ENVCHANGE response can still negotiate a different value, which `Context::set_packet_size` already applies before any wire framing happens. Added 7 unit tests for the validation boundaries (defaults, min, max, one below, one above, zero) and a manual live integration test (`connect_with_custom_packet_size`, built directly rather than through `#[test_on_runtimes]` since there's no connection-string equivalent for this setting) confirming a connection with a non-default packet size still works end-to-end. Verified: - cargo check: default, --features=rustls, --features=vendored-openssl, --no-default-features - cargo clippy --all-targets -- -D warnings, for default and rustls - cargo fmt --check - cargo test --lib (176 passed) - cargo test --doc against a live server (27 passed) - Live integration tests over rustls: 301 passed (98 bulk + 203 query, including the new packet-size test) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0156Di1tRRLsJK8ctU1AmAJr
Summary
Add the ability to configure the TDS packet size in the LOGIN7 message. Larger packet sizes can significantly improve bulk insert performance by reducing network round-trips and protocol overhead.
Motivation
While working on a high-throughput data migration tool, we discovered that the default 4KB packet size was a significant bottleneck for bulk insert operations. Benchmarking showed:
This is a 42% improvement in bulk insert performance simply by increasing the packet size.
For comparison, Go's
go-mssqldbdriver also defaults to 4KB packets, but exposes configuration. After this change, Rust/tiberius performance matches or exceeds Go for bulk operations.Changes
packet_size: Option<u32>field toConfigstructpacket_size(&mut self, size: u32)setter method with documentationget_packet_size(&self) -> Option<u32>getter methodLoginMessagein the connection flowpacket_size(&mut self, size: u32)setter toLoginMessageExample Usage
Technical Notes
Test Plan
cargo test --features rustls)🤖 Generated with Claude Code