tdsql
tdsql is an ergonomic async Microsoft SQL Server client for Rust, built on the
tiberius-ng TDS driver.
Connect once, then run statements against the connection. Queries hand back rows directly, and values come out typed:
use ;
async
Installation
[]
= "0.1"
= { = "1", = ["macros", "rt-multi-thread"] }
Connecting
Build the configuration fluently, or parse a connection string:
use ;
# async
Config also accepts a JDBC-style string via Config::from_jdbc_string. The
password is redacted from the Debug output, so logging a config is safe.
One Client is one connection, and it is reused for every statement. Because
TDS has no MARS, a connection carries one in-flight request at a time — for
concurrency, use a pool such as bb8 or
deadpool and check out a connection per
task.
Queries
Parameters are positional: @P1 is the first, @P2 the second.
# use Client;
# async
Reading values
get takes a column name or a zero-based position, and converts to the type you
ask for:
# use Row;
#
Integers widen (a tinyint reads fine as i64) but never narrow, so a value is
never silently truncated. Reading a NULL into a non-optional type is an error
rather than a default value.
Columns keep their order, and SELECT a, a stays addressable by position.
Named parameters and stored procedures
Command carries named parameters. A stored procedure is sent as a real RPC, so
its parameters are genuinely named on the wire rather than pasted into an EXEC
string:
# use ;
# use Decimal;
# async
A text batch may also use named parameters; they are rewritten to the positional form the protocol expects:
# use ;
# async
Binding None sends SQL NULL:
use Command;
let cmd = query
.param
.param;
DDL and raw batches
Parameterised statements are sent as an RPC, and some statements — CREATE
PROCEDURE, CREATE VIEW, CREATE TRIGGER — must be the first statement of
their own batch, so they cannot go that route. batch sends SQL verbatim:
# use Client;
# async
Because the SQL is sent verbatim, never interpolate untrusted input into it — use parameters for values.
Multiple result sets
Most commands return one result set, which is why query hands back a plain
Vec<Row>. When a batch or procedure genuinely returns several, use
query_dataset and index them in the order the server sent them:
# use ;
# async
Errors
Every fallible call returns Error, so failures can be matched rather than
string-compared:
use ;
# async
Error is Send + Sync + 'static, so it also works with anyhow, eyre and
friends via ?.
Type mapping
| SQL Server | DataValue |
Reads as |
|---|---|---|
tinyint |
TinyInt |
u8, i16, i32, i64 |
smallint |
SmallInt |
i16, i32, i64 |
int |
Int |
i32, i64 |
bigint |
BigInt |
i64 |
real, float |
Float |
f64 |
decimal, numeric, money |
Decimal |
rust_decimal::Decimal |
bit |
Bool |
bool |
char, varchar, nchar, nvarchar, text, xml |
Text |
String |
binary, varbinary, image |
Binary |
Vec<u8> |
uniqueidentifier |
Guid |
uuid::Uuid |
date |
Date |
chrono::NaiveDate |
time |
Time |
chrono::NaiveTime |
datetime, datetime2, smalldatetime |
DateTime |
chrono::NaiveDateTime |
datetimeoffset |
DateTimeOffset |
chrono::DateTime<FixedOffset> |
any NULL |
Null |
Option<T> |
Any column reads as DataValue if you would rather match on it yourself.
Known limitation: untyped NULLs
Binding DataValue::Null (or None) sends an untyped NULL, which travels as a
NULL int. SQL Server coerces that for most targets, but a column that needs a
specific type may reject it. Bind a typed Option<T> when that matters.
Examples
Runnable examples live in examples/. The
tests directory holds integration tests that exercise the full type
mapping against a live server.
License
MIT — see LICENSE.