Skip to content

Make the ODBC fetch path 2.3-3.9x cheaper on CPU (beats msodbcsql18 on varchar(max)/nvarchar) - #215

Draft
Saurabh Singh (saurabh500) wants to merge 18 commits into
mainfrom
dev/saurabh/perf-on-main
Draft

Make the ODBC fetch path 2.3-3.9x cheaper on CPU (beats msodbcsql18 on varchar(max)/nvarchar)#215
Saurabh Singh (saurabh500) wants to merge 18 commits into
mainfrom
dev/saurabh/perf-on-main

Conversation

@saurabh500

@saurabh500 Saurabh Singh (saurabh500) commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Fixes #185

Performance work on the mssql-odbc fetch path, measured against msodbcsql18 on the
same local SQL Server.

Result

Datatype benchmark, 5000 rows, single column, CPU median of 25 repetitions.
Ratio < 1.0 means mssql-odbc is faster.

Type before after msodbcsql18 ratio was
VarcharMax 5.30 ms 3.49 ms 8.79 ms 0.40x 0.65x
Guid 3.44 2.91 3.70 0.79x 0.82x
Float 3.43 2.88 3.49 0.83x 0.98x
BigInt 2.68 2.50 2.40 1.04x 0.96x
NVarchar 9.08 6.78 6.32 1.07x 1.12x
Varchar 5.77 4.00 3.57 1.12x 1.56x
Int 3.01 2.60 2.20 1.18x 1.36x
Decimal 4.39 3.67 2.77 1.32x 1.46x
DateTime2 7.52 7.20 4.79 1.50x 1.39x

Every type improved in absolute terms. Three beat the native driver and BigInt is at
parity. The biggest mover is Varchar, 1.56x -> 1.12x.

DateTime2 is the one type whose ratio did not improve; its formatter was out of scope for
this round.

On measurement

Wall-clock from this harness is unusable -- cv runs 18-101% across types, and the native
driver's own VarcharMax median swung 32% between two consecutive runs. CPU medians are
usable at cv 3-15%. Every number above is CPU median over 25 repetitions.

That noise floor is why the two commits here carry very different claims: the packet-size
change is a 35% effect with p=0.00 and survives any amount of drift, while the
ParserContext memoization is below resolution and is committed on correctness grounds
with no performance claim. BigInt's cv reached 33% on our side, so read its 1.04x as
"parity", not as a regression from 0.96x.

Changes

Default the ODBC driver to 32KB TDS packets. Fetch CPU is dominated by the number
of socket reads, not the bytes they move. Each async read costs roughly 35 us of CPU,
mostly the tokio reactor round-trip on Windows IOCP. Reading 5000 varchar(100) rows
took 63 socket reads at 8000-byte packets and 14 at 32768; the criterion case dropped
35.4% (4.15 ms -> 2.64 ms, p=0.00). This is why the large-payload types moved and the
small fixed-width ones did not.

apply_connection_params now always sets packet_size, so an unspecified PacketSize=
takes the driver default instead of falling through to the ClientContext default. Only
the ODBC driver changes -- the TDS default stays at 8000 for the JS, Python and CLI front
ends.

The cost is a larger per-connection read buffer, which the transport sizes at 2x the
packet size: 16 KB -> 64 KB per connection. That is a fixed buffer, not per-row
materialization.

Render decimals without intermediate allocations. DecimalParts implements Display
directly instead of building a String via to_decimal_string. Decimal went from 2.18x
to 1.46x on its own, before the packet change.

Borrow string payloads in SQLGetData. as_utf8_str returns Cow<'_, str> so UTF-8
and single-byte-ASCII payloads are read in place. This removes a per-value allocation but
is not a measurable win on its own, and the PR does not claim one -- see below.

Memoize the row-loop ParserContext. next_row_cursor rebuilt one per row: an Arc
clone and drop, an awaited resolve_cell_decryptor, and an O(columns) scan for
crypto_metadata. None of it changes within a result set. Cached and validated by
Arc::ptr_eq against current_metadata, so every reassignment of that field invalidates
it for free. Strictly less work per row, but below both benchmarks' resolution -- no
performance claim.
Benchmark cases for varchar in odbc_glue, including a 1-byte varchar_short case
so payload size can be varied independently of column type.

How the cost was attributed

Three measurements, each ruling out a candidate:

  1. Glue vs TDS. fetch_getdata_varchar (5.21 ms) minus tds_fetch_column_varchar
    (5.19 ms) is about 4 ns/row. SQLGetData and the entire ODBC layer are free for
    strings; all of the cost is below them in TDS.
  2. Per-value vs per-byte. A 1-byte varchar costs 378 ns/row against 306 for int, so
    per-value string overhead is only 72 ns. The other 452 ns/row looked per-byte.
  3. Per-byte vs per-read. It was not per-byte. Payload size and packet count scale
    together, so varying payload alone cannot separate them. Holding payload fixed and
    varying packet size isolated the cost to the socket read itself.

Rejected alternatives

Enlarging the read buffer instead of the wire packet size. shift_data_to_front memmoves
the remainder on every packet boundary, so an 8x buffer with 8000-byte packets cut reads
63 -> 42 but ran slower: 4.47 ms vs 2.64 ms. Reverted.

Follow-up

The 35 us per async read is the structural reason the native driver still wins on the
remaining types. Bigger packets mitigate it; they do not remove it. The real fix is
taking the tokio reactor out of the socket read for the blocking ODBC path.

Validation

  • cargo bfmt clean
  • cargo bclippy clean
  • cargo nextest --workspace --lib: 2184 passed, 7 failed -- the 7 are pre-existing
    expired-certificate fixtures, unchanged from the base commit

NetworkTransport already holds a whole decrypted TDS packet, so for most
columns every byte the decoder needs is present before it is asked for.
The cursor still paid a boxed resume_row_into future, a cancellation and
timeout composition, and pause-state churn per column to wait for bytes
sitting in L1.

Add SliceReader, a sans-I/O TdsPacketReader over an in-memory slice, and
drive the existing decoder over the buffered bytes with a single poll.
Reusing decode_into keeps one implementation of the wire format. PLP and
Always Encrypted columns still take the async path, and any shortfall or
error falls back with the read buffer untouched.

Measured on the datatype benchmark (CPU median, 9 reps): Int 10.00 to
6.97 ms, BigInt 10.18 to 7.12 ms, Guid 10.31 to 8.02 ms, Varchar 11.41 to
9.45 ms, NVarchar 13.72 to 11.6 ms. VarcharMax is unchanged, confirming
PLP is excluded. A TDS-only bench isolates the win: the cursor path drops
12.08 to 9.62 ms while whole-row decode stays flat as a control.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
…rsions

The ODBC layer called block_on for every SQLFetch/SQLGetData even though the
transport usually already holds the whole decrypted TDS packet. drive_read polls
the future once first and only falls back to the executor when it is genuinely
pending; on Pending it resumes the same pinned future rather than restarting, so
bytes already consumed are not decoded twice.

Boxing TdsClient inside DbcState keeps the per-row move in and out of the
connection mutex to a pointer.

Nine character conversions were missing, so Decimal and DateTime2 failed with
HYC00 rather than merely being slow. value_text now formats Decimal/Numeric,
Date, Time, DateTime2, DateTime, SmallDateTime, DateTimeOffset, Money,
SmallMoney, bytes as hex, and Xml.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Optimizes unbound ODBC row fetching while expanding scalar-to-text conversions and performance measurement.

Changes:

  • Adds buffered synchronous column decoding and cheaper row/client state handling.
  • Adds missing ODBC character conversions.
  • Introduces cross-platform ODBC performance benchmarks and runners.

Reviewed changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
mssql-tds/src/io/token_stream.rs Shares row metadata via Arc.
mssql-tds/src/io/slice_reader.rs Adds in-memory packet reader.
mssql-tds/src/io.rs Registers slice reader module.
mssql-tds/src/connection/transport/network_transport.rs Adds buffered decode fast path.
mssql-tds/src/connection/tds_client.rs Uses fast path for cursor columns.
mssql-tds/Cargo.toml Registers attribution benchmark.
mssql-tds/benches/sync_decoder.rs Removes empty benchmark file.
mssql-tds/benches/odbc_split.rs Measures TDS cursor overhead.
mssql-odbc/tests/perf/run_perf.sh Adds Unix benchmark runner.
mssql-odbc/tests/perf/run_perf.ps1 Adds Windows benchmark runner.
mssql-odbc/tests/perf/README.md Documents performance suite.
mssql-odbc/tests/perf/lib/perf_fixture.cpp Implements benchmark fixtures.
mssql-odbc/tests/perf/include/perf_fixture.h Declares benchmark utilities.
mssql-odbc/tests/perf/CMakeLists.txt Configures benchmark builds.
mssql-odbc/tests/perf/benches/fetch_bench.cpp Benchmarks row fetching.
mssql-odbc/tests/perf/benches/exec_bench.cpp Benchmarks execution paths.
mssql-odbc/tests/perf/benches/datatype_bench.cpp Benchmarks type conversions.
mssql-odbc/tests/perf/benches/connect_bench.cpp Benchmarks connection operations.
mssql-odbc/src/handles/dbc.rs Boxes the TDS client.
mssql-odbc/src/api/value_text.rs Adds scalar text rendering.
mssql-odbc/src/api/util.rs Adds synchronous-first future driving.
mssql-odbc/src/api/more_results.rs Updates boxed-client tests.
mssql-odbc/src/api/mod.rs Registers value conversion module.
mssql-odbc/src/api/get_data.rs Uses new rendering and read driver.
mssql-odbc/src/api/fetch.rs Uses synchronous-first fetching.
mssql-odbc/src/api/exec_direct.rs Updates boxed-client tests.
mssql-odbc/src/api/exec_common.rs Propagates boxed client ownership.
mssql-odbc/src/api/driver_connect.rs Boxes newly connected clients.
mssql-odbc/.gitignore Ignores benchmark artifacts.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +53 to +57
/// Renders nanoseconds-since-midnight as `HH:MM:SS[.f{scale}]`, matching the
/// column's declared fractional-second scale.
fn format_time(time_nanoseconds: u64, scale: u8) -> String {
let secs = time_nanoseconds / NANOS_PER_SEC;
let nanos = time_nanoseconds % NANOS_PER_SEC;
Comment on lines +1561 to +1567
// Boxed so the borrow of `reader` can be released explicitly below;
// a stack-pinned future would hold it until the end of this scope.
let mut decode = Box::pin(decoder.decode_into(&mut reader, metadata, col, writer));
let polled = decode
.as_mut()
.poll(&mut Context::from_waker(Waker::noop()));
drop(decode);
/// `datetime` counts 1/300 s ticks, which ODBC renders with millisecond
/// precision, so the tick count is converted to whole milliseconds.
fn format_datetime(v: &SqlDateTime) -> String {
let millis = (u64::from(v.time) * 1000).div_ceil(300);
Comment on lines +1578 to +1583
match consumed {
Some(count) => {
self.tds_read_buffer.consume_bytes(count);
true
}
None => false,
Comment on lines +183 to +190
do {
std::string drain_err;
if (DrainRows(stmt_, &drain_err) < 0) {
error_ = drain_err;
CloseCursor(stmt_);
return false;
}
} while (SQLMoreResults(stmt_) == SQL_SUCCESS);
odbc_split measures the TDS work behind a fetch loop. This runs the same
query through SQLFetch/SQLGetData, so the difference is the ODBC glue:
handle mutexes, diagnostics bookkeeping, and text rendering.

The driver's entry points are called directly rather than through the
Driver Manager, so odbc32.dll's own per-call cost is not attributed to us
and an edit-measure cycle takes seconds instead of minutes. This needs
`rlib` alongside `cdylib`; the shipped DLL is unaffected.

Measured on the 5000-row INT query: 10.00 ms for the TDS path against
11.52 ms end to end, putting the ODBC layer at ~1.5 ms.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
Wall time cannot resolve anything useful here. Each iteration executes a
real query, so ~4 ms of the ~11 ms is server wait, and that wait varies
enough that two runs of an unmodified binary differed by 4.35% with
p = 0.04. The changes worth making in the ODBC layer are smaller than that.

Counting process CPU removes the server wait by construction.
GetProcessTimes is quantized to the ~15.6 ms scheduler tick and made things
worse (CI +/-6.7%); QueryProcessCycleTime counts cycles and lands at
+/-0.37%, with an unmodified-binary control now drifting only ~1.3%.

The result also agrees with the C++ harness to 0.07% (6.975 ms against
6.970 ms), which is a useful check that the two harnesses measure the
same thing.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
SQLFetch spent most of its CPU on `receive_row_header`, which for a ROW
token reads exactly one byte and then builds a pause state. Reaching that
byte cost a boxed `#[async_trait]` future, cancellation/timeout
composition and two `Instant::now()` calls per row.

Add `try_receive_row_header_buffered`, the row-header counterpart to the
existing `try_decode_column_buffered`: it serves ROW and NBCROW from
bytes already in the read buffer and returns `None` — consuming nothing —
for anything else, so DONE and short buffers still take the async path.

Splitting the bench into fetch-only and fetch+getdata showed the row
advance, not column materialization, was the dominant cost: 6.15 ms of
7.41 ms CPU. Measured over 5000 rows: fetch+getdata 7.41 -> 6.08 ms CPU
(-17.9%, p=0.00), fetch-only 6.15 -> 5.45 ms (-11.4%).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
`SQLGetData` decodes exactly one column per call, but `DefaultRowWriter`
holds its values in a `Vec`, so every pull paid a heap allocation and free
to carry a single value.

Add `SingleValueWriter`, which keeps that value inline, and use it for both
the buffered and async cursor paths. The macro generating its writes keeps
the 23 typed setters from becoming 23 hand-written stubs.

Measured over 5000 rows: fetch+getdata 6.08 -> 5.47 ms CPU (-10.0%,
p=0.00). Combined with the buffered row header, 7.41 -> 5.47 ms (-26%).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
Every `SQLFetch` boxed a fresh `RowPauseState` and every `SQLGetData` freed
it again by dereferencing, so positioning on a row cost a heap allocation
and free per row.

Store it inline in `ActiveRowReadState` instead. `PlpPauseState` stays
boxed, so the enum grows to roughly 64 bytes — paid once per connection,
not per row, and the state is reached through `&mut TdsClient` so no future
grows with it.

Measured over 5000 rows: fetch+getdata 5.47 -> 4.98 ms CPU (-9.1%, p=0.00).
Cumulative with the buffered row header and stack-slot column decode,
7.41 -> 4.98 ms (-33%).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
The cursor and column state machines are ~8.5 KB each. Taking one by
value copied that much across the drive_read call boundary on every
SQLFetch and SQLGetData; pinning in the caller's frame passes a pointer
instead. Cuts ODBC glue cost per row from 436ns to 202ns.

Also adds TDS-direct cases to the attribution bench so the ODBC glue can
be measured by subtraction within a single run.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
drain_active_plp held an 8 KiB stack array across an await. It is
reached from next_row_cursor and read_row_column, so rustc folded the
whole array into those state machines: both futures measured ~8.5 KB and
were rebuilt and moved on every single row, for a path that only runs
when a caller abandons a partially read PLP column.

Moving the buffer to the heap drops next_row_cursor from 8520 to 928
bytes and read_row_column from 8464 to 432. Fetch cost falls 20% on the
ODBC path and 33% on the TDS column path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
Three per-row costs on the cursor path:

- next_row_cursor and read_row_column carried info-level #[instrument]
  spans, so a 5000-row fetch built 10000 spans. tracing is built with the
  log feature here, so each span also pays a log facade dispatch even
  with no subscriber installed. Per-row spans are log spam regardless of
  cost; the surrounding execute/fetch spans still cover the operation.
- resume_row_loop emitted an info-level "Row Received" event for every row.
- try_decode_column_buffered boxed its decode future purely to end a
  borrow early. Pinning in a narrower scope does the same thing without
  a malloc and free per column.

Fetch cost falls 6.7% on the ODBC path and 13.6% on the TDS column path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
DecimalParts now implements Display directly instead of building a
String via to_decimal_string. The scale/sign handling moves into the
formatter and a magnitude() helper replaces the duplicated i128
conversion.

Measured on the datatype benchmark (5000 rows, CPU median): Decimal
fetch goes from 2.18x to 1.46x of msodbcsql18.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
as_utf8_str returns Cow<'_, str> so UTF-8 and single-byte-ASCII
payloads are read in place; to_utf8_string delegates to it. The ODBC
text path is restructured around that borrow: write_string_result
splits into copy_string_out and finish_string_result so the diagnostic
work happens after the borrow ends.

This removes a per-value allocation from the SQLGetData path. It is
not a measurable win on its own -- SQLGetData plus the whole ODBC glue
costs only ~4 ns/row over raw TDS column reads -- but it is the correct
shape and it stops the copy from growing with row width.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
Three cases: fetch_only_varchar and fetch_getdata_varchar through the
ODBC handles, and tds_fetch_column_varchar straight against TdsClient.
Comparing the last two isolates how much the ODBC layer adds over raw
column reads.

tds_fetch_column_varchar_short selects a 1-byte varchar so payload size
can be varied independently of column type, which separates per-value
cost from per-byte cost.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
Fetch CPU is dominated by the number of socket reads, not the bytes
they move: each async read costs roughly 35us of CPU, mostly the tokio
reactor round-trip. Reading 5000 varchar(100) rows took 63 socket reads
at 8000-byte packets and 14 at 32768, and the criterion case dropped
35.4% (4.15ms -> 2.64ms, p=0.00). Payload-only experiments could not
see this because packet count scales with payload; varying packet size
at fixed payload separates the two.

apply_connection_params now always sets packet_size so an unspecified
PacketSize= takes the driver default rather than the ClientContext one.
Only the ODBC driver changes; the TDS default stays at 8000 for the JS,
Python and CLI front ends.

Cost is a larger per-connection read buffer, which the transport sizes
at 2x the packet size: 16KB -> 64KB per connection. That is a fixed
buffer, not per-row materialization.

Datatype benchmark, 5000 rows, CPU median vs msodbcsql18:

  VarcharMax  5.30ms -> 3.67ms   0.65x -> 0.37x
  NVarchar    9.08ms -> 6.87ms   1.12x -> 0.90x
  Varchar     5.77ms -> 4.66ms   1.56x -> 1.34x
  Decimal     4.39ms -> 3.90ms   1.46x -> 1.43x

Enlarging the read buffer instead was tried and is worse:
shift_data_to_front memmoves the remainder on every packet boundary, so
an 8x buffer with 8000-byte packets cut reads to 42 but ran at 4.47ms.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790
next_row_cursor rebuilt a ParserContext on every row: an Arc clone and
drop of the column metadata, an awaited resolve_cell_decryptor, and --
for the common unencrypted case -- an O(columns) scan of the metadata
looking for crypto_metadata. None of that changes within a result set.

Cache it on the client and validate with Arc::ptr_eq against
current_metadata, the same shape current_decryptor already uses. That
field is reassigned in several places (new result set, error, batch
end); keying on pointer identity means each of those invalidates the
cache without needing to touch those sites.

This is strictly less work per row, but it is below the resolution of
both benchmarks: criterion put tds_fetch_column at -3.6% with p=0.08
while the ODBC cases in the same run -- which go through an unchanged
driver DLL and therefore cannot have moved -- drifted -6% to -20%. No
performance claim is made for it.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: f0a91aef-805f-4f6b-bb35-0c3617130790

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 1 comment.

Suppressed comments (5)

mssql-odbc/src/api/value_text.rs:85

  • Using ceiling division misrenders valid SQL datetime ticks. One 1/300-second tick should render as .003, but this computes 4 ms and emits .004 (similarly for other one-third-ms values). Round to the nearest millisecond, matching SQL Server's .000/.003/.007 representation.
    let millis = (u64::from(v.time) * 1000).div_ceil(300);

mssql-odbc/src/api/driver_connect.rs:414

  • SQLSetConnectAttr(SQL_ATTR_PACKET_SIZE, ...) stores the requested value in DbcState::packet_size (set_connect_attr.rs:186), but this fallback only consults the parsed connection string. Thus an application that sets 4096 through the attribute and omits PacketSize= still negotiates 32768, even though SQLGetConnectAttr reports 4096. Pass the DBC state's packet size into this mapping as the fallback, with an explicit connection-string value taking precedence.
    let size = params.packet_size.unwrap_or(DEFAULT_PACKET_SIZE);
    context.packet_size =
        u16::try_from(size.clamp(MIN_PACKET_SIZE, MAX_PACKET_SIZE)).unwrap_or(u16::MAX);

mssql-odbc/tests/perf/run_perf.sh:198

  • The PR states that wall-clock measurements are unusable and bases its results on CPU medians, but this comparison reads real_time, i.e. wall time, from Google Benchmark JSON. This makes the runner's ratios use the noisy metric the PR explicitly rejects; read cpu_time instead.
        out[name] = b["real_time"] * SCALE.get(b.get("time_unit", "ns"), 1)

mssql-odbc/tests/perf/run_perf.ps1:205

  • The PR's reported results use CPU medians because wall time is too noisy, but the PowerShell comparison selects Google Benchmark's real_time. The printed ratios therefore measure the rejected wall-clock metric; select cpu_time here.
            $map[$name] = $b.real_time * (Get-TimeScale $b.time_unit)

mssql-odbc/tests/perf/README.md:69

  • This registration example points to a library name the crate does not build. The crate is named msodbcsql18, and both the build script and the root README use libmsodbcsql18.so; following this example leaves the runner unable to copy/find the driver.
Driver      = /home/<user>/.odbc-dev/libmssql_odbc.so

ColumnValues::Date(d) => Some(format_date(i64::from(d.get_days()) - DAYS_0001_TO_UNIX)),
ColumnValues::Time(t) => Some(format_time(t.time_nanoseconds, t.scale)),
ColumnValues::DateTime2(v) => Some(format_datetime2(v)),
ColumnValues::DateTime(v) => Some(format_datetime(v)),
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.

mssql-odbc: fetch path is significantly slower than msodbcsql18

3 participants