Skip to content
This repository has been archived by the owner on Sep 3, 2023. It is now read-only.

Commit

Permalink
v0.2.3 - Added more docs and tests, fixed param binding escape. Added…
Browse files Browse the repository at this point in the history
… named parameters for prepared statements and fixed take_arr_values. Also switched to the lazy_regex crate from lazy_static for compile time checks
  • Loading branch information
bobozaur committed Apr 3, 2022
1 parent 1f1b081 commit 836f777
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 61 deletions.
4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "exasol"
version = "0.2.2"
version = "0.2.3"
edition = "2021"
authors = ["bobozaur"]
description = "Exasol client library implemented in Rust."
Expand All @@ -19,7 +19,7 @@ rsa = "0.5.0"
serde_json = "1.0.74"
serde = {version = "1.0.133", features = ["derive"]}
regex = "1.5.4"
lazy_static = "1.4.0"
lazy-regex = "2.3.0"
thiserror = "1.0.30"
flate2 = { version = "1.0.22", optional = true }

Expand Down
16 changes: 8 additions & 8 deletions src/con_opts.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use crate::error::ConnectionError;
use lazy_static::lazy_static;
use lazy_regex::regex;
use rand::rngs::OsRng;
use rand::seq::SliceRandom;
use rand::thread_rng;
use regex::{Captures, Regex};
use regex::Captures;
use rsa::{PaddingScheme, PublicKey, RsaPublicKey};
use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
Expand All @@ -11,7 +12,6 @@ use std::env;
use std::fmt;
use std::fmt::{Display, Formatter};
use std::net::{SocketAddr, ToSocketAddrs};
use rand::seq::SliceRandom;

// Convenience alias
type ConResult<T> = std::result::Result<T, ConnectionError>;
Expand Down Expand Up @@ -367,19 +367,19 @@ impl ConOpts {
/// Connection to all nodes will then be attempted in a random order
/// until one is successful or all failed.
pub(crate) fn parse_dsn(&self) -> ConResult<Vec<String>> {
lazy_static! {
static ref RE: Regex = Regex::new(r"(?x)
let re = regex!(
r"(?x)
^(.+?) # Hostname prefix
(?:(\d+)\.\.(\d+)(.*?))? # Optional range and hostname suffix (e.g. hostname1..4.com)
(?:/([0-9A-Fa-f]+))? # Optional fingerprint (e.g. hostname1..4.com/135a1d2dce102de866f58267521f4232153545a075dc85f8f7596f57e588a181)
(?::(\d+)?)?$ # Optional port (e.g. hostname1..4.com:8564)
").unwrap();
}
"
);

self.0
.dsn
.as_deref()
.and_then(|dsn| RE.captures(dsn))
.and_then(|dsn| re.captures(dsn))
.ok_or(ConnectionError::InvalidDSN)
.and_then(|cap| {
// Parse capture groups from regex
Expand Down
96 changes: 86 additions & 10 deletions src/connection.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use lazy_regex::regex;
use regex::Captures;
use std::borrow::Cow;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt::{Debug, Formatter};
Expand Down Expand Up @@ -149,7 +152,65 @@ impl Connection {

/// Creates a prepared statement of type [PreparedStatement].
/// The prepared statement can then be executed with the provided data.
///
/// Unique named parameters are supported to aid in using map-like types as data rows
/// when executing the prepared statement. Using just `?` results in the parameter name
/// being the empty string.
///
/// The names must be unique as the associated values get consumed, therefore a duplicate
/// name won't result in a value on look-up.
///
/// For sequence-like types, parameter names are ignored and discarded.
///
/// ```
/// use exasol::{connect, QueryResult, PreparedStatement};
/// use exasol::error::Result;
/// use serde_json::json;
/// use std::env;
///
/// let dsn = env::var("EXA_DSN").unwrap();
/// let schema = env::var("EXA_SCHEMA").unwrap();
/// let user = env::var("EXA_USER").unwrap();
/// let password = env::var("EXA_PASSWORD").unwrap();
///
/// let mut exa_con = connect(&dsn, &schema, &user, &password).unwrap();
/// let prepared_stmt: PreparedStatement = exa_con.prepare("SELECT 1 FROM (SELECT 1) TMP WHERE 1 = ?").unwrap();
/// let data = vec![vec![json!(1)]];
/// prepared_stmt.execute(&data).unwrap();
/// ```
///
/// ```
/// use exasol::{connect, QueryResult, PreparedStatement};
/// use exasol::error::Result;
/// use serde_json::json;
/// use serde::Serialize;
/// use std::env;
///
/// let dsn = env::var("EXA_DSN").unwrap();
/// let schema = env::var("EXA_SCHEMA").unwrap();
/// let user = env::var("EXA_USER").unwrap();
/// let password = env::var("EXA_PASSWORD").unwrap();
///
/// let mut exa_con = connect(&dsn, &schema, &user, &password).unwrap();
/// let prepared_stmt: PreparedStatement = exa_con.prepare("INSERT INTO EXA_RUST_TEST VALUES(?col1, ?col2, ?col3)").unwrap();
///
/// #[derive(Serialize, Clone)]
/// struct Data {
/// col1: String,
/// col2: String,
/// col3: u8
/// }
///
/// let data_item = Data { col1: "t".to_owned(), col2: "y".to_owned(), col3: 10 };
/// let vec_data = vec![data_item.clone(), data_item.clone(), data_item];
///
/// prepared_stmt.execute(vec_data).unwrap();
/// ```
///
/// String literals resembling a parameter can be escaped, if, for any reason, needed:
/// (Exasol doesn't really seem to like combining parameters with literals in prepared statements)
///
/// ``` should_panic
/// # use exasol::{connect, QueryResult, PreparedStatement};
/// # use exasol::error::Result;
/// # use serde_json::json;
Expand All @@ -161,16 +222,16 @@ impl Connection {
/// # let password = env::var("EXA_PASSWORD").unwrap();
/// #
/// let mut exa_con = connect(&dsn, &schema, &user, &password).unwrap();
/// let prepared_stmt: PreparedStatement = exa_con.prepare("SELECT 1 FROM (SELECT 1) TMP WHERE 1 = ?").unwrap();
/// let data = vec![vec![json!(1)]];
/// let prepared_stmt: PreparedStatement = exa_con.prepare("INSERT INTO EXA_RUST_TEST VALUES('\\?col1', ?col2, ?col3)").unwrap();
/// let data = vec![json!(["test", 1])];
/// prepared_stmt.execute(&data).unwrap();
/// ```
#[inline]
pub fn prepare<T>(&mut self, query: T) -> Result<PreparedStatement>
pub fn prepare<'a, T>(&mut self, query: T) -> Result<PreparedStatement>
where
T: AsRef<str> + Serialize,
T: Serialize + Into<Cow<'a, str>>,
{
(*self.con).borrow_mut().prepare(&self.con, &query)
(*self.con).borrow_mut().prepare(&self.con, query)
}

/// Ping the server and wait for a Pong frame
Expand Down Expand Up @@ -435,17 +496,32 @@ impl ConnectionImpl {
}

/// Creates a prepared statement
pub(crate) fn prepare<T>(
pub(crate) fn prepare<'a, T>(
&mut self,
con_impl: &Rc<RefCell<ConnectionImpl>>,
query: &T,
query: T,
) -> Result<PreparedStatement>
where
T: AsRef<str> + Serialize,
T: Serialize + Into<Cow<'a, str>>,
{
let payload = json!({"command": "createPreparedStatement", "sqlText": query});
let re = regex!(r"\\(\?\w*)|[?\w]\?\w*|\?\w*\?|\?(\w*)");
let mut col_names = Vec::new();
let cow_q = query.into();
let x = cow_q.as_ref();

let q = re.replace_all(x, |cap: &Captures| {
cap.get(2)
.map(|m| {
col_names.push(m.as_str().to_owned());
"?"
})
.or_else(|| cap.get(1).map(|m| &x[m.range()]))
.unwrap_or(&x[cap.get(0).unwrap().range()])
});

let payload = json!({"command": "createPreparedStatement", "sqlText": q});
self.get_resp_data(payload)
.and_then(|r| r.try_to_prepared_stmt(con_impl))
.and_then(|r| r.try_to_prepared_stmt(con_impl, col_names))
}

/// Closes a result set
Expand Down
19 changes: 9 additions & 10 deletions src/params.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::error::{BindError, DriverError, Result};
use lazy_static::lazy_static;
use regex::{Captures, Regex};
use lazy_regex::regex;
use regex::Captures;
use serde::Serialize;
use serde_json::{Map, Value};
use std::collections::HashMap;
Expand Down Expand Up @@ -120,10 +120,7 @@ fn parametrize_query(query: &str, val: Value) -> BindResult {
/// Bind map elements to the query
#[inline]
fn do_param_binding(query: &str, map: HashMap<String, String>) -> BindResult {
lazy_static! {
static ref RE: Regex = Regex::new(r"\\(:\w+)|[:\w]:\w+|:\w+:|:(\w+)").unwrap();
}

let re = regex!(r"\\(:\w+)|[:\w]:\w+|:\w+:|:(\w+)");
let mut result = Ok(String::new()); // Will store errors here

// Capture group 1 is Some only when an escaped parameter construct
Expand All @@ -134,7 +131,7 @@ fn do_param_binding(query: &str, map: HashMap<String, String>) -> BindResult {
//
// Otherwise, the entire match is returned as-is, as it represents
// a regex match that we purposely ignore.
let q = RE.replace_all(query, |cap: &Captures| {
let q = re.replace_all(query, |cap: &Captures| {
cap.get(2)
.map(|m| match map.get(m.as_str()) {
Some(k) => k.as_str(),
Expand All @@ -143,14 +140,15 @@ fn do_param_binding(query: &str, map: HashMap<String, String>) -> BindResult {
""
}
})
.or_else(|| cap.get(1).map(|m|&query[m.range()]))
.or_else(|| cap.get(1).map(|m| &query[m.range()]))
.unwrap_or(&query[cap.get(0).unwrap().range()])
});

result.and(Ok(q.into_owned()))
}

/// Generates a `HashMap<String, String>` of the params SQL representation.
/// Generates a `HashMap<String, String>` of the params SQL representation,
/// where the key is the column name and the value is the SQL param.
#[inline]
fn gen_map_params(params: Map<String, Value>) -> HashMap<String, String> {
params
Expand All @@ -159,7 +157,8 @@ fn gen_map_params(params: Map<String, Value>) -> HashMap<String, String> {
.collect()
}

/// Generates a `Vec<String>` of the params SQL representation.
/// Generates a `HashMap<String, String>` of the params SQL representation,
/// where the key is the index and the value is the SQL param.
#[inline]
fn gen_seq_params(params: Vec<Value>) -> HashMap<String, String> {
params
Expand Down
64 changes: 37 additions & 27 deletions src/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ impl QueryResult {
pub(crate) fn from_de(
qr: QueryResultDe,
con_rc: &Rc<RefCell<ConnectionImpl>>,
lc: bool
lc: bool,
) -> Self {
match qr {
QueryResultDe::ResultSet { result_set } => {
Expand Down Expand Up @@ -205,7 +205,11 @@ where

/// Method that generates the [ResultSet] struct based on [ResultSetDe].
/// It will also remap column names to their lowercase representation if needed.
pub(crate) fn from_de(mut rs: ResultSetDe, con_rc: &Rc<RefCell<ConnectionImpl>>, lc: bool) -> Self {
pub(crate) fn from_de(
mut rs: ResultSetDe,
con_rc: &Rc<RefCell<ConnectionImpl>>,
lc: bool,
) -> Self {
// Set column names as lowercase if needed
match lc {
false => (),
Expand Down Expand Up @@ -328,35 +332,40 @@ pub struct PreparedStatement {
statement_handle: usize,
parameter_data: Option<ParameterData>,
connection: Rc<RefCell<ConnectionImpl>>,
column_names: Vec<String>,
}

impl PreparedStatement {
/// Method that generates the [PreparedStatement] struct based on [PreparedStatementDe].
pub(crate) fn from_de(
prep_stmt: PreparedStatementDe,
con_rc: &Rc<RefCell<ConnectionImpl>>,
col_names: Vec<String>,
) -> Self {
Self {
statement_handle: prep_stmt.statement_handle,
parameter_data: prep_stmt.parameter_data,
connection: Rc::clone(con_rc),
column_names: col_names,
}
}

/// Executes the prepared statement with the given data.
/// Data must implement [IntoIterator] and [Serialize].
/// Each `Item` of the iterator will represent a data row.
///
/// If `Item` is map-like, columns are re-ordered based on
/// the expected order by Exasol.
/// If `Item` is map-like, columns are uniquely re-ordered based on
/// the expected order given through the named parameters.
///
/// If `Item` is sequence-like, the needed amount of columns is
/// taken from the data.
/// taken from the data. Parameter names are ignored.
///
/// Excess data is discarded.
///
/// # Errors
///
/// Missing column names in map-like types or insufficient columns
/// in sequence-like types results in errors.
/// Missing parameter names in map-like types (which can also be caused by duplication)
/// or insufficient columns in sequence-like types results in errors.
///
/// ```
/// # use exasol::{connect, QueryResult};
Expand All @@ -373,30 +382,29 @@ impl PreparedStatement {
/// use serde::Serialize;
///
/// let mut exa_con = connect(&dsn, &schema, &user, &password).unwrap();
/// let prep_stmt = exa_con.prepare("INSERT INTO EXA_RUST_TEST VALUES(?, ?, ?)").unwrap();
/// let prep_stmt = exa_con.prepare("INSERT INTO EXA_RUST_TEST VALUES(?col1, ?col2, ?col3)").unwrap();
///
/// let json_data = json!(
/// [
/// ["a", "b", 1],
/// ["c", "d", 2],
/// ["e", "f", 3],
/// ["a", "b", 1, "x"],
/// ["c", "d", 2, "x"],
/// ["e", "f", 3, "x"],
/// ]
/// );
///
/// prep_stmt.execute(json_data.as_array().unwrap()).unwrap();
/// #
/// # #[derive(Serialize, Clone)]
/// # struct Data {
/// # col1: String,
/// # col2: String,
/// # col3: u8
/// # }
/// #
/// # let data_item = Data { col1: "t".to_owned(), col2: "y".to_owned(), col3: 10 };
/// # let vec_data = vec![data_item.clone(), data_item.clone(), data_item];
/// #
/// # // prep_stmt.execute(vec_data).unwrap();
/// #
/// prep_stmt.execute(json_data.as_array().unwrap()).unwrap();
///
/// #[derive(Serialize, Clone)]
/// struct Data {
/// col1: String,
/// col2: String,
/// col3: u8
/// }
///
/// let data_item = Data { col1: "t".to_owned(), col2: "y".to_owned(), col3: 10 };
/// let vec_data = vec![data_item.clone(), data_item.clone(), data_item];
///
/// prep_stmt.execute(vec_data).unwrap();
/// ```
pub fn execute<T, S>(&self, data: T) -> Result<QueryResult>
where
Expand All @@ -408,10 +416,12 @@ impl PreparedStatement {
None => (&0, [].as_slice()),
};

let col_names = columns
let col_names = self
.column_names
.iter()
.map(|c| c.name.as_str())
.map(|c| c.as_str())
.collect::<Vec<&str>>();

let col_major_data = to_col_major(&col_names, data).map_err(DriverError::DataError)?;

let payload = json!({
Expand Down
Loading

0 comments on commit 836f777

Please sign in to comment.