sqlx_exasol/
lib.rs

1#![cfg_attr(not(test), warn(unused_crate_dependencies))]
2//! A database driver for Exasol to be used with the Rust [sqlx](https://github.com/launchbadge/sqlx) framework.
3//!
4//! ## Crate Features flags
5//! * `etl` - enables the usage ETL jobs without TLS encryption.
6//! * `etl_native_tls` - enables the `etl` feature and adds TLS encryption through
7//!   `native-tls`<sup>[1](#etl_tls)</sup>
8//! * `etl_rustls` - enables the `etl` feature and adds TLS encryption through
9//!   `rustls`<sup>[1](#etl_tls)</sup>
10//! * `compression` - enables compression support (for both connections and ETL jobs)
11//! * `uuid` - enables support for the `uuid` crate
12//! * `chrono` - enables support for the `chrono` crate types
13//! * `rust_decimal` - enables support for the `rust_decimal` type
14//! * `migrate` - enables the use of migrations and testing (just like in other `sqlx` drivers).
15//!
16//! ## Comparison to native sqlx drivers
17//! Since the driver is used through `sqlx` and it implements the interfaces there, it can do all
18//! the drivers shipped with `sqlx` do, with some caveats:
19//! - Limitations
20//!     - no compile-time query check support<sup>[2](#sqlx_limitations)</sup>
21//!     - no `sqlx-cli` support<sup>[2](#sqlx_limitations)</sup>
22//!     - no locking migrations support<sup>[3](#no_locks)</sup>
23//!     - no column nullability checks<sup>[4](#nullable)</sup>
24//!
25//! - Additions
26//!     - array-like parameter binding in queries, thanks to the columnar nature of the Exasol
27//!       database
28//!     - performant & parallelizable ETL IMPORT/EXPORT jobs in CSV format through HTTP Transport
29//!       (see the [etl] module for more details)
30//!
31//! ## Connection string
32//! The connection string is expected to be an URL with the `exa://` scheme, e.g:
33//! `exa://sys:exasol@localhost:8563`.
34//!
35//! Connection options:
36//! - `access-token`: Use an access token for login instead of credentials
37//! - `refresh-token`: Use a refresh token for login instead of credentials
38//! - `protocol-version`: Select a specific protocol version to use
39//! - `ssl-mode`: Select a specifc SSL behavior. See: [`ExaSslMode`]
40//! - `ssl-ca`: Use a certain certificate authority
41//! - `ssl-cert`: Use a certain certificate
42//! - `ssl-key`: Use a specific SSL key
43//! - `statement-cache-capacity`: Set the capacity of the LRU prepared statements cache
44//! - `fetch-size`: Sets the size of data chunks when retrieving result sets
45//! - `query-timeout`: The query timeout amount, in seconds. 0 means no timeout
46//! - `compression`: Boolean representing whether use compression
47//! - `feedback-interval`: Interval at which Exasol sends keep-alive Pong frames
48//!
49//! [`ExaConnectOptions`] can also be constructed in code through its builder method,
50//! which returns a [`ExaConnectOptionsBuilder`].
51//!
52//! ## Supported Rust datatypes
53//! - [`bool`]
54//! - [`u8`], [`u16`], [`u32`], [`u64`], [`u128`]
55//! - [`i8`], [`i16`], [`i32`], [`i64`], [`i128`]
56//! - [`f32`], [`f64`]
57//! - [`str`], [`String`], [`std::borrow::Cow<str>`]
58//! - `chrono` feature: [`chrono::DateTime<Utc>`], [`chrono::DateTime<Utc>`],
59//!   [`chrono::NaiveDateTime`], [`chrono::NaiveDate`], [`chrono::Duration`], [`Months`] (analog of
60//!   [`chrono::Months`])
61//! - `uuid` feature: [`uuid::Uuid`]
62//! - `rust_decimal` feature: [`rust_decimal::Decimal`]
63//!
64//! ## Supported Exasol datatypes:
65//! All Exasol datatypes are supported in some way, also depdending on the additional types used
66//! through feature flags.
67//!
68//! The [GEOMETRY](https://docs.exasol.com/db/latest/sql_references/geospatialdata/geospatialdata_overview.htm) type does not have
69//! a correspondent Rust datatype. One could be introduced in future versions of the driver, but for
70//! now they can be encoded/decoded to [`String`].
71//!
72//! ## HTTP Transport
73//! Functionality that allows performant data import/export by creation of one-shot HTTP servers
74//! to which Exasol connects to (at most one per node), thus balancing the load.
75//!
76//! The data is always in `CSV` format and job configuration can be done through the
77//! [`etl::ImportBuilder`] and [`etl::ExportBuilder`] structs. The workers implement
78//! [`futures_io::AsyncWrite`] and [`futures_io::AsyncRead`] respectively, providing great
79//! flexibility in terms of how the data is processed.
80//!
81//! The general flow of an ETL job is:
82//! - build the job through [`etl::ImportBuilder`] or [`etl::ExportBuilder`]
83//! - concurrently wait on the query execution future (typically from the main thread) and on worker
84//!   operations (async tasks can be spawned in multi-threaded runtimes to further parallelize the
85//!   workload).
86//! - when all the workers are done (readers reach EOF, while writers require an explicit `close()`)
87//!   the job ends and the query execution future returns.
88//! - an error/timeout issue results in the query execution future or a worker throwing an error,
89//!   therefore consider joining the tasks and aborting them if an error is thrown somewhere.
90//!
91//! ## Examples
92//! Using the driver for regular database interactions:
93//! ```rust,no_run
94//! use std::env;
95//!
96//! use sqlx_exasol::*;
97//!
98//! # async {
99//! #
100//! let pool = ExaPool::connect(&env::var("DATABASE_URL").unwrap()).await?;
101//! let mut con = pool.acquire().await?;
102//!
103//! sqlx::query("CREATE SCHEMA RUST_DOC_TEST")
104//!     .execute(&mut *con)
105//!     .await?;
106//! #
107//! # let res: anyhow::Result<()> = Ok(());
108//! # res
109//! # };
110//! ```
111//!
112//! Array-like parameter binding, also featuring the [`ExaIter`] adapter.
113//! An important thing to note is that the parameter sets must be of equal length,
114//! otherwise an error is thrown:
115//! ```rust,no_run
116//! use std::{collections::HashSet, env};
117//!
118//! use sqlx_exasol::*;
119//!
120//! # async {
121//! #
122//! let pool = ExaPool::connect(&env::var("DATABASE_URL").unwrap()).await?;
123//! let mut con = pool.acquire().await?;
124//!
125//! let params1 = vec![1, 2, 3];
126//! let params2 = HashSet::from([1, 2, 3]);
127//!
128//! sqlx::query("INSERT INTO MY_TABLE VALUES (?, ?)")
129//!     .bind(&params1)
130//!     .bind(ExaIter::from(&params2))
131//!     .execute(&mut *con)
132//!     .await?;
133//! #
134//! # let res: anyhow::Result<()> = Ok(());
135//! # res
136//! # };
137//! ```
138//!
139//! An EXPORT - IMPORT ETL data pipe.
140//! ```rust,no_run
141//! # #[cfg(feature = "etl")] {
142//! use std::env;
143//!
144//! use futures_util::{
145//!     future::{try_join, try_join3, try_join_all},
146//!     AsyncReadExt, AsyncWriteExt, TryFutureExt,
147//! };
148//! use sqlx_exasol::{etl::*, *};
149//!
150//! async fn pipe(mut reader: ExaExport, mut writer: ExaImport) -> anyhow::Result<()> {
151//!     let mut buf = vec![0; 5120].into_boxed_slice();
152//!     let mut read = 1;
153//!
154//!     while read > 0 {
155//!         // Readers return EOF when there's no more data.
156//!         read = reader.read(&mut buf).await?;
157//!         // Write data to Exasol
158//!         writer.write_all(&buf[..read]).await?;
159//!     }
160//!
161//!     // Writes, unlike readers, MUST be closed to signal we won't send more data to Exasol
162//!     writer.close().await?;
163//!     Ok(())
164//! }
165//!
166//! # async {
167//! #
168//! let pool = ExaPool::connect(&env::var("DATABASE_URL").unwrap()).await?;
169//! let mut con1 = pool.acquire().await?;
170//! let mut con2 = pool.acquire().await?;
171//!
172//! // Build EXPORT job
173//! let (export_fut, readers) = ExportBuilder::new(ExportSource::Table("TEST_ETL"))
174//!     .build(&mut con1)
175//!     .await?;
176//!
177//! // Build IMPORT job
178//! let (import_fut, writers) = ImportBuilder::new("TEST_ETL").build(&mut con2).await?;
179//!
180//! // Use readers and writers in some futures
181//! let transport_futs = std::iter::zip(readers, writers).map(|(r, w)| pipe(r, w));
182//!
183//! // Execute the EXPORT and IMPORT query futures along with the worker futures
184//! let (export_res, import_res, _) = try_join3(
185//!     export_fut.map_err(From::from),
186//!     import_fut.map_err(From::from),
187//!     try_join_all(transport_futs),
188//! )
189//! .await?;
190//!
191//! assert_eq!(export_res.rows_affected(), import_res.rows_affected());
192//! #
193//! # let res: anyhow::Result<()> = Ok(());
194//! # res
195//! # }};
196//! ```
197//!
198//! ## Footnotes
199//! <a name= etl_tls>1</a>: There is unfortunately no way to automagically choose a crate's feature
200//! flags based on its dependencies feature flags, so the TLS backend has to be manually selected.
201//! While nothing prevents you from using, say `native-tls` with `sqlx` and `rustls` with Exasol ETL
202//! jobs, it might be best to avoid compiling two different TLS backends. Therefore, consider
203//! choosing the `sqlx` and `sqlx-exasol` feature flags in a consistent manner.
204//!
205//! <a name="sqlx_limitations">2</a>: The `sqlx` API powering the compile-time query checks and the
206//! `sqlx-cli` tool is not public. Even if it were, the drivers that are incorporated into `sqlx`
207//! are hardcoded in the part of the code that handles the compile-time driver decision logic.
208//! <br>The main problem from what I can gather is that there's no easy way of defining a plugin
209//! system in Rust at the moment, hence the hardcoding.
210//!
211//! <a name="no_locks">3</a>: Exasol has no advisory or database locks and simple, unnested,
212//! transactions are unfortunately not enough to define a mechanism so that concurrent migrations do
213//! not collide. This does **not** pose a problem when migrations are run sequentially or do not act
214//! on the same database objects.
215//!
216//! <a name="nullable">4</a>: Exasol does not provide the information of whether a column is
217//! nullable or not, so the driver cannot implicitly decide whether a `NULL` value can go into a
218//! certain database column or not until it actually tries.
219
220/// Gets rid of unused dependencies warning from dev-dependencies.
221mod arguments;
222mod column;
223mod connection;
224mod database;
225mod error;
226#[cfg(feature = "migrate")]
227mod migrate;
228mod options;
229mod query_result;
230mod responses;
231mod row;
232mod statement;
233#[cfg(feature = "migrate")]
234mod testing;
235mod transaction;
236mod type_info;
237mod types;
238mod value;
239
240pub use arguments::ExaArguments;
241pub use column::ExaColumn;
242#[cfg(feature = "etl")]
243pub use connection::etl;
244pub use connection::ExaConnection;
245pub use database::Exasol;
246pub use options::{ExaConnectOptions, ExaConnectOptionsBuilder, ExaSslMode, ProtocolVersion};
247pub use query_result::ExaQueryResult;
248pub use responses::{ExaAttributes, ExaDatabaseError, SessionInfo};
249pub use row::ExaRow;
250use sqlx_core::{
251    executor::Executor, impl_acquire, impl_column_index_for_row, impl_column_index_for_statement,
252    impl_into_arguments_for_arguments,
253};
254pub use statement::ExaStatement;
255pub use transaction::ExaTransactionManager;
256pub use type_info::ExaTypeInfo;
257pub use types::ExaIter;
258#[cfg(feature = "chrono")]
259pub use types::Months;
260pub use value::{ExaValue, ExaValueRef};
261
262/// An alias for [`Pool`][sqlx_core::pool::Pool], specialized for Exasol.
263pub type ExaPool = sqlx_core::pool::Pool<Exasol>;
264
265/// An alias for [`PoolOptions`][sqlx_core::pool::PoolOptions], specialized for Exasol.
266pub type ExaPoolOptions = sqlx_core::pool::PoolOptions<Exasol>;
267
268/// An alias for [`Executor<'_, Database = Exasol>`][Executor].
269pub trait ExaExecutor<'c>: Executor<'c, Database = Exasol> {}
270impl<'c, T: Executor<'c, Database = Exasol>> ExaExecutor<'c> for T {}
271
272impl_into_arguments_for_arguments!(ExaArguments);
273impl_acquire!(Exasol, ExaConnection);
274impl_column_index_for_row!(ExaRow);
275impl_column_index_for_statement!(ExaStatement);
276
277// ###################
278// ##### Aliases #####
279// ###################
280type SqlxError = sqlx_core::Error;
281type SqlxResult<T> = sqlx_core::Result<T>;