[![Crates.io](https://img.shields.io/crates/v/sqlx-exasol)](https://crates.io/crates/sqlx-exasol) [![Docs.rs](https://img.shields.io/docsrs/sqlx-exasol)](https://docs.rs/sqlx-exasol/latest/sqlx_exasol/) # sqlx-exasol A database driver for Exasol to be used with the Rust [sqlx](https://github.com/launchbadge/sqlx) framework, based on the Exasol [Websocket API](https://github.com/exasol/websocket-api). Inspired by [Py-Exasol](https://github.com/exasol/pyexasol) and based on the (now archived) [rust-exasol](https://github.com/bobozaur/rust-exasol) sync driver. **MSRV**: `1.74` ## Note >The crate's version resembles the `sqlx` version it is based on so that managing dependencies is simpler. > > With that in mind, please favor using a fixed version of `sqlx` and `sqlx-exasol` in `Cargo.toml` to avoid issues, such as: > ```toml > sqlx = "=0.8.2" > sqlx-exasol = "=0.8.2" > ``` ## Crate Features flags * `etl` - enables the usage ETL jobs without TLS encryption. * `etl_native_tls` - enables the `etl` feature and adds TLS encryption through `native-tls`[1](#etl_tls) * `etl_rustls` - enables the `etl` feature and adds TLS encryption through `rustls`[1](#etl_tls) * `compression` - enables compression support (for both connections and ETL jobs) * `uuid` - enables support for the `uuid` crate * `chrono` - enables support for the `chrono` crate types * `rust_decimal` - enables support for the `rust_decimal` type * `migrate` - enables the use of migrations and testing (just like in other `sqlx` drivers). ## Comparison to native sqlx drivers Since the driver is used through `sqlx` and it implements the interfaces there, it can do all the drivers shipped with `sqlx` do, with some caveats: - Limitations - no compile-time query check support[2](#sqlx_limitations) - no `sqlx-cli` support[2](#sqlx_limitations) - no locking migrations support[3](#no_locks) - no column nullability checks[4](#nullable) - apart from migrations, only a single query per statement is allowed (including in fixtures)[5](#single_query) - 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 ## Connection string The connection string is expected to be an URL with the `exa://` scheme, e.g: `exa://sys:exasol@localhost:8563`. ## Examples Using the driver for regular database interactions: ```rust use std::env; use sqlx_exasol::*; let pool = ExaPool::connect(&env::var("DATABASE_URL").unwrap()).await?; let mut con = pool.acquire().await?; sqlx::query("CREATE SCHEMA RUST_DOC_TEST") .execute(&mut *con) .await?; ``` Array-like parameter binding, also featuring the [`ExaIter`] adapter. An important thing to note is that the parameter sets must be of equal length, otherwise an error is thrown: ```rust use std::{collections::HashSet, env}; use sqlx_exasol::*; 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::query("INSERT INTO MY_TABLE VALUES (?, ?)") .bind(¶ms1) .bind(ExaIter::from(¶ms2)) .execute(&mut *con) .await?; ``` An EXPORT - IMPORT ETL data pipe. ```rust use std::env; use futures_util::{ future::{try_join, try_join3, try_join_all}, AsyncReadExt, AsyncWriteExt, TryFutureExt, }; use sqlx_exasol::{etl::*, *}; async fn pipe(mut reader: ExaExport, mut writer: ExaImport) -> anyhow::Result<()> { 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(()) } 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(ExportSource::Table("TEST_ETL")) .build(&mut con1) .await?; // Build IMPORT job let (import_fut, writers) = ImportBuilder::new("TEST_ETL").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()); ``` ## License Licensed under either of * Apache License, Version 2.0, (LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0) * MIT license (LICENSE-MIT or https://opensource.org/licenses/MIT) at your option. ## Contributing Contributions to this repository, unless explicitly stated otherwise, will be considered dual-licensed under MIT and Apache 2.0. Bugs/issues encountered can be opened [here](https://github.com/bobozaur/sqlx-exasol/issues) ## Footnotes 1: There is unfortunately no way to automagically choose a crate's feature flags based on its dependencies feature flags, so the TLS backend has to be manually selected. While nothing prevents you from using, say `native-tls` with `sqlx` and `rustls` with Exasol ETL jobs, it might be best to avoid compiling two different TLS backends. Therefore, consider choosing the `sqlx` and `sqlx-exasol` feature flags in a consistent manner. 2: The `sqlx` API powering the compile-time query checks and the `sqlx-cli` tool is not public. Even if it were, the drivers that are incorporated into `sqlx` are hardcoded in the part of the code that handles the compile-time driver decision logic.
The main problem from what I can gather is that there's no easy way of defining a plugin system in Rust at the moment, hence the hardcoding. 3: 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. 4: 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. 5: I didn't even know this (as I never even thought of doing it), but `sqlx` allows running multiple queries in a single statement. Due to limitations with the websocket API this driver is based on, `sqlx-exasol` can only run one query at a time.
This is only circumvented in migrations through a somewhat limited, convoluted and possibly costly workaround that tries to split queries by `;`, which does not make it applicable for runtime queries at all.