1 unstable release
0.1.0 | Aug 22, 2021 |
---|
#25 in #odbc
Used in rs-odbc
10KB
185 lines
RS-ODBC
Rust implementation of the ODBC API that looks and feels like ODBC but is safe
Description
Main design goal of this crate is that the exposed API should look as close as possible to the original ODBC API while providing type safety wherever possible. This crate prevents most of the safety issues inherent to C code and moves most of the application errors to compile time. ODBC state transitions FSM is implemented inside Rust's type system so that many of the invalid handle errors are prevented as compile errors.
Why this crate
1. Known API
If you have already worked with the ODBC API you will feel at home using this crate, i.e. you don't have to learn yet another API. With this crate you are getting a well known, highly used and standardized API. The level of abstraction over the original ODBC API is minimal so that you can use the original ODBC documentation. Translating existing ODBC applications or examples from C to Rust is very straightforward.
2. Safe API
For most applications you will never have to resort to using raw pointers. Crates which expose custom high-level API wrappers around ODBC will most likely force you to fall back to using the raw API when you are required to use ODBC features that are not expressible through the API they provide. This will introduce unnecessary safety risks for your application unless those crates are built on top of this crate.
3. Complete API
This crate is designed to be fully ODBC compliant so there should be no low-level ODBC feature that cannot be expressed through this crate. However, it is possible that a particular feature may not have been implemented yet. If you notice that a feature is missing, you are encouraged to open an issue requiring the feature.
Installation
To be able to use this library you have to have ODBC Driver Manager installed and configured on your host OS.
This library dynamically links against the odbc32.dll
on Windows or against libodbc.so
(unixODBC) on Linux
and OS-X. To enable static linking of native libraries use the cargo static
feature.
Cargo features
static
Enables static linking of native libraries. If static linking is enabled user must define RS_ODBC_LINK_SEARCH
environment variable which contains path to static libraries this crate will link against. For unixODBC, user
should provide both libodbc.a
and libltdl.a
under this path. Static linking is not supported for Windows.
API differences
- ODBC functions are implemented as methods or associated functions on handles. Therefore,
providing handle identifier(e.g.
SQL_HANDLE_STMT
) as an argument becomes unnecessary
C ODBC eample | Rust ODBC example |
---|---|
|
|
-
Most of the ODBC handle methods return
SQLRETURN
as per standard, but some will return a tuple(Result<<succ_handle_type>, <err_handle_type>>, SQLRETURN)
(e.g. SQLDriverConnect). Returning handles makes it possible to implement the ODBC state transition FSM inside the Rust's type system -
ODBC functions which take pointer and it's length take reference to a slice instead. Slice references prevent the possibility of the application writer to write/read past the end of the allocation unit.
C ODBC eample | Rust ODBC example |
---|---|
|
|
- ODBC version is defined at the point when environment handle is allocated. Usually, this should be the first step in your ODBC application but in Rust it is handled by the type system
C ODBC eample | Rust ODBC example |
---|---|
|
|
- Disconnecting and freeing handles is done automatically at the end of scope
Uninitialized variables
When using ODBC functions(such as SQLGetEnvAttr
) that take mutable references which are written to, but are never read from
by the driver or the DM, it is unnecessary to initialize those variables since they will be initialized during the call to the
ODBC function in question. To circumvent the unnecessary initialization, many of the ODBC functions exposed through Rust allow
for the usage of both initialized and uninitialized variables (via MaybeUninit
).
use core::mem::MaybeUninit;
use rs_odbc::api::Allocate;
use rs_odbc::env::{self, SQL_ATTR_CONNECTION_POOLING, SQL_OV_ODBC3_80};
use rs_odbc::handle::{SQLHENV, SQL_NULL_HANDLE};
fn main() {
let (env, _) = SQLHENV::SQLAllocHandle(&SQL_NULL_HANDLE);
let env: SQLHENV<SQL_OV_ODBC3_80> = env.unwrap();
let mut value = env::SQL_CP_ONE_PER_HENV; // Initialized to default value
let _ = env.SQLGetEnvAttr(SQL_ATTR_CONNECTION_POOLING, Some(&mut value), None);
// Confirm value was modified by the driver
assert_ne!(env::SQL_CP_ONE_PER_HENV, value);
let mut value = MaybeUninit::uninit(); // Variable is uninitialized
let _ = env.SQLGetEnvAttr(SQL_ATTR_CONNECTION_POOLING, Some(&mut value), None);
// Value initialized by the driver
match unsafe { value.assume_init() } {
env::SQL_CP_ONE_PER_DRIVER => println!("SQL_CP_ONE_PER_DRIVER"),
env::SQL_CP_ONE_PER_HENV => println!("SQL_CP_ONE_PER_HENV"),
env::SQL_CP_DRIVER_AWARE => println!("SQL_CP_DRIVER_AWARE"),
env::SQL_CP_OFF => println!("SQL_CP_OFF"),
_ => panic!("Driver returned unknown value"),
}
}
The use of uninitialized variables is highly discouraged because their use is usually a micro optimization that will have no measurable
effect on the performance of your code and introduce a potential for unexpected UB if not careful(such as partially initialized variables).
If some ODBC function is only able to receive uninitialized arguments, users are encouraged to use MaybeUninit::new
or MaybeUninit::zeroed
to minimize the risk of UB.
Thread safety
All handles are Send
, however, at the moment, only SQLHENV
is Sync
since sharing references to other handles across threads is considered to be an anti-pattern.
Obviously, to cancel a function running on a connection or statement handle on another thread one must be able to share a handle reference across threads.
Since the operation of canceling is defined by the ODBC standard to always be a thread safe operation, for this specific scenario, from your original handle,
you can derive a handle that implements the Sync
trait such as WeakSQLHSTMT
or RefSQLHSTMT
. Handles prefixed with Ref
are allocated from a reference
to your original handle, while ones that are prefixed Weak
are allocated from your original handle wrapped in an Arc
.
// TODO: Add code example
If there is a use-case where you would like to be able to share handles other than SQLHENV
among threads, please open an issue describing your use-case.
Unsafe API
There are cases where it's not possible to ensure safety through the type system. In these rare cases you can allocate UnsafeSQLHSTMT
and UnsafeSQLHDESC
which implement additional unsafe API which makes some of the statement functions unsafe
// TODO:
Testing
Integration tests use dockerized environment which has database and ODBC driver already set up.
Testing environment can be set up with docker-compose up -d
Tests are executed with docker exec -t rs-odbc sh -lc 'cargo test'
- use
RUSTFLAGS=-Awarnings
to silence compiler warnings which make compile tests fail
Dependencies
~1.5MB
~38K SLoC