tdsql
tdsql is an async Microsoft SQL Server client for Rust, built on the
tds-driver TDS driver.
You connect once and run statements on that connection. Queries return rows, and you read values out of them as ordinary Rust types:
use ;
async
Installation
[]
= "0.8.0"
= { = "1", = ["macros", "rt-multi-thread"] }
Connecting
Build a Config in code, or parse a connection string:
use ;
# async
Config::from_jdbc_string parses JDBC-style strings too. A named instance
without a port, such as Server=host\SQLEXPRESS, is found through SQL Server
Browser, and connecting gives up after 15 seconds unless
Config::connect_timeout says otherwise.
A Client is one connection, and it runs one statement at a time. For
concurrency, use a pool such as bb8 or
deadpool and give each task its own
connection.
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 the value to
the type you ask for:
# use Row;
#
A value is never silently truncated to fit the type you ask for, and reading a
NULL into anything but an Option is an error. Which Rust types each SQL
Server type reads as is listed in Types.
Streaming large results
query gives you nothing until the last row has arrived. query_stream hands
you each row as soon as the server sends it, so memory use stays flat however
big the result is:
# use Client;
# async
The stream borrows the client, so nothing else can run on the connection while
you hold it. Drop it early and the query is cancelled on the server. RowStream
is also a futures::Stream, if you'd rather use combinators.
Cancelling
Stop waiting for a request and it's cancelled on the server too. Drop its
future, let tokio::time::timeout or select! give up on it, or drop a stream
early. The server stops working on it and lets go of any locks it took, and the
connection stays usable:
# use Duration;
# use Client;
# async
Cancelling stops a statement, not the transaction around it, and commits and rollbacks always run. Put statements that must stand or fall together in a transaction.
Named parameters and stored procedures
Command takes named parameters. Stored procedures are called over RPC, which
sends each parameter by name, so nothing gets pasted into an EXEC string:
# use ;
# use Decimal;
# async
Text queries can use named parameters too. Names match whatever their case, so
@Id in the SQL finds a parameter bound as id. To send SQL NULL, bind
None:
# use ;
# async
Transactions
client.transaction() starts a transaction, and everything you run through the
handle it returns is part of it. Call commit() to keep the work. If you drop
the handle without committing, the transaction rolls back, so returning early
with ? is safe:
# use Client;
# async
You can pass a &mut Transaction to other functions, so one unit of work can
span several of them:
use ;
async
# async
You can also choose an isolation level, and use savepoints. A savepoint works like a nested transaction: rolling it back undoes its own work and leaves the outer transaction open.
# use ;
# async
If the server ends the transaction itself, after a deadlock for instance, any
further statement on the handle fails with Error::TransactionAborted rather
than running outside the transaction.
Writing helpers that work either way
A helper that takes &mut Client can't be called inside a transaction, and one
that takes &mut Transaction can't be called outside one. Take
&mut impl Executor instead, and it works with both:
# use ;
async
# async
Executor has the same statement methods as Client, so you can make an
existing helper generic by changing nothing but its signature. Its
transaction() starts a transaction on a Client and a savepoint on a
Transaction, so a helper can open a scope of its own without knowing whether
it's already inside one.
Raw batches
A statement with parameters runs through sp_executesql. Some statements, like
CREATE PROCEDURE, CREATE VIEW and CREATE TRIGGER, have to come first in a
batch of their own, so they can't take parameters at all. batch sends SQL
exactly as you wrote it and returns every row of every result set:
# use Client;
# async
Since the SQL goes over untouched, never build it from untrusted input. Values belong in parameters.
Multiple result sets
Most statements return a single result set, which is why query returns a plain
Vec<Row>. When a batch or procedure returns several, use query_dataset.
They come back in the order the server sent them:
# use ;
# async
Errors
Everything that can fail returns an Error, so you can match on what went
wrong:
use ;
# async
When the server rejects a statement, the error's message names the statement and repeats what the server said, so printing it is usually all you need. The same details are available as fields, for handling a specific server error without parsing the message:
# use Client;
# async
Error is Send + Sync + 'static, so ? works with anyhow, eyre and
similar crates.
Blocking client
If your program isn't async, turn on the blocking feature:
[]
= { = "0.8.0", = ["blocking"] }
tdsql::blocking::Client is the async client without the async and .await.
Typed rows, Command, transactions, savepoints and the Executor trait all work
the same way, and Row, DataSet and Error are shared between the two.
use Client;
use Config;
Each blocking client runs its own small runtime, so don't use one from async
code. In async code, use tdsql::Client.
Types
SQL Server types read as and bind from the Rust types you would expect: the
integer types, f64, bool, String, Vec<u8>, rust_decimal::Decimal,
uuid::Uuid and the chrono date and time types. Import those three crates
through tdsql so the versions always match:
use Utc;
use Uuid;
use Decimal;
The full mapping, and how time zones, datetime columns and NULL parameters
are handled, is in docs/types.md.
Learn more
examples/has runnable programs: a basic query, a stored procedure, a transaction and the blocking client.- docs/types.md covers the type mapping in detail.
- docs/how-it-works.md explains what happens on the connection: cancellation, streaming, rollback on drop, batches and the blocking runtime.
- docs.rs/tdsql has the API reference.
License
MIT. See LICENSE.