A database driver for Exasol to be used with the Rust [sqlx](https://github.com/launchbadge/sqlx) framework.
Based on `sqlx` version `0.9.0-alpha.1`.
## Features flags
- `etl` - Add support for ETL jobs.
- `compression` - Add compression support (for both connections and ETL jobs).
- `any` - Add support for the `Any` database driver, which can proxy to a database driver at
runtime.
- `derive` - Add support for the derive family macros, those are `FromRow`, `Type`, `Encode`,
`Decode`.
- `macros` - Add support for the `query*!` macros, which allows compile-time checked queries.
- `migrate` - Add support for the migration management and `migrate!` macro, which allow
compile-time embedded migrations.
- `uuid` - Add support for UUID.
- `chrono` - Add support for date and time types from `chrono`.
- `time` - Add support for date and time types from `time` crate (alternative to `chrono`, which
is preferred by `query!` macro, if both enabled).
- `bigdecimal` - Add support for `BigDecimal` from the `bigdecimal` crate.
- `rust_decimal` - Add support for `Decimal` from the `rust_decimal` crate.
- `geo-types` - Add support for `Geometry` and its variants from the `geo-types` crate.
- `json` - Add support for `Json<T>` as well as `serde_json::Value` and `serde_json::RawValue`.
## Supported types
See the [`types`] module.
## Comparison to native sqlx drivers
The driver re-exports all `sqlx` public API and implements the exposed traits. As a result,
it can do all the drivers shipped with `sqlx` do, with some caveats:
- Limitations
- separate CLI utility (`sqlx-exasol` instead of `sqlx`)
- compile time query macros cannot work along the ones from `sqlx` within the same crate
- no locking migrations support<sup>[1](#no_locks)</sup>
- no column nullability checks<sup>[2](#nullable)</sup>
- Additions
- array-like parameter binding in queries, thanks to the columnar nature of the Exasol
database
- performant & parallelizable ETL IMPORT/EXPORT jobs in CSV format through HTTP Transport
## Compile-time query checks
The driver now supports compile-time query validation and can be used alongside
`sqlx` within the same crate. Note however that derive proc-macros from `sqlx` are
database agnostic and thus `sqlx-exasol` just re-exports them as-is.
## CLI utility
The driver uses its own CLI utility (built on the same `sqlx-cli` library):
```sh
cargo install sqlx-exasol-cli
# Usage is exactly the same as sqlx-cli
sqlx-exasol database create
sqlx-exasol database drop
sqlx-exasol migrate add <name>
sqlx-exasol migrate run
cargo sqlx-exasol prepare
```
## Connection string
The connection string is expected to be an URL with the `exa://` scheme, e.g:
`exa://sys:exasol@localhost:8563`.
See [`ExaConnectOptions`] for a list of supported connection string parameters.
## HTTP Transport
Functionality that allows performant data import/export by creation of one-shot HTTP servers
to which Exasol connects to (at most one per node), thus balancing the load.
The data is always in `CSV` format and job configuration can be done through the
[`ImportBuilder`](etl::ImportBuilder) and [`ExportBuilder`](etl::ExportBuilder) structs.
The workers implement `AsyncWrite` and `AsyncRead` respectively, providing great flexibility in
terms of how the data is processed.
The general flow of an ETL job is:
- build the job through [`ImportBuilder`](etl::ImportBuilder) or
[`ExportBuilder`](etl::ExportBuilder)
- concurrently wait on the query execution future (typically from the main thread) and on worker
operations (async tasks can be spawned in multi-threaded runtimes to further parallelize the
workload).
- when all the workers are done (readers reach EOF, while writers require an explicit `close()`)
the job ends and the query execution future returns.
- an error/timeout issue results in the query execution future or a worker throwing an error,
therefore consider joining the tasks and aborting them if an error is thrown somewhere.
## Examples
Using the driver for regular database interactions:
```rust,no_run
use std::env;
use sqlx_exasol::{error::*, *};
# async {
#
let pool = ExaPool::connect(&env::var("DATABASE_URL").unwrap()).await?;
let mut con = pool.acquire().await?;
sqlx_exasol::query("CREATE SCHEMA RUST_DOC_TEST")
.execute(&mut *con)
.await?;
#
# let res: Result<(), BoxDynError> = Ok(());
# res
# };
```
Array-like parameter binding, also featuring the [`crate::types::ExaIter`] adapter.
An important thing to note is that the parameter sets must be of equal length,
otherwise an error is thrown:
```rust,no_run
use std::{collections::HashSet, env};
use sqlx_exasol::{error::*, *};
# async {
#
let pool = ExaPool::connect(&env::var("DATABASE_URL").unwrap()).await?;
let mut con = pool.acquire().await?;
let params1 = vec![1, 2, 3];
let params2 = HashSet::from([1, 2, 3]);
sqlx_exasol::query("INSERT INTO MY_TABLE VALUES (?, ?)")
.bind(¶ms1)
.bind(types::ExaIter::new(params2.iter()))
.execute(&mut *con)
.await?;
#
# let res: Result<(), BoxDynError> = Ok(());
# res
# };
```
An EXPORT - IMPORT ETL data pipe.
```rust,no_run
# #[cfg(feature = "etl")] {
use std::env;
use futures_util::{
future::{try_join, try_join3, try_join_all},
AsyncReadExt, AsyncWriteExt, TryFutureExt,
};
use sqlx_exasol::{error::*, etl::*, *};
async fn pipe(mut reader: ExaExport, mut writer: ExaImport) -> Result<(), BoxDynError> {
let mut buf = vec![0; 5120].into_boxed_slice();
let mut read = 1;
while read > 0 {
// Readers return EOF when there's no more data.
read = reader.read(&mut buf).await?;
// Write data to Exasol
writer.write_all(&buf[..read]).await?;
}
// Writes, unlike readers, MUST be closed to signal we won't send more data to Exasol
writer.close().await?;
Ok(())
}
# async {
#
let pool = ExaPool::connect(&env::var("DATABASE_URL").unwrap()).await?;
let mut con1 = pool.acquire().await?;
let mut con2 = pool.acquire().await?;
// Build EXPORT job
let (export_fut, readers) = ExportBuilder::new_from_table("TEST_ETL", None)
.build(&mut con1)
.await?;
// Build IMPORT job
let (import_fut, writers) = ImportBuilder::new("TEST_ETL", None)
.build(&mut con2)
.await?;
// Use readers and writers in some futures
let transport_futs = std::iter::zip(readers, writers).map(|(r, w)| pipe(r, w));
// Execute the EXPORT and IMPORT query futures along with the worker futures
let (export_res, import_res, _) = try_join3(
export_fut.map_err(From::from),
import_fut.map_err(From::from),
try_join_all(transport_futs),
)
.await?;
assert_eq!(export_res.rows_affected(), import_res.rows_affected());
#
# let res: Result<(), BoxDynError> = Ok(());
# res
# }};
```
## Footnotes
<a name="no_locks">1</a>: Exasol has no advisory or database locks and simple, unnested,
transactions are unfortunately not enough to define a mechanism so that concurrent migrations do
not collide. This does **not** pose a problem when migrations are run sequentially or do not act
on the same database objects.
<a name="nullable">2</a>: Exasol does not provide the information of whether a column is
nullable or not, so the driver cannot implicitly decide whether a `NULL` value can go into a
certain database column or not until it actually tries.