use std::collections::HashMap;
use serde::Deserialize;
use serde_json::{json, Value};
use crate::client::Client;
use crate::error::{Error, Result};
use crate::params::Param;
use crate::response::Response;
const NO_SERVER_PARAMS: &str = "this server did not grant server-side parameters (SERVER_PARAMS \
was not in the granted feature set), so this client will not bind `?` placeholders on this \
connection. It will not render the values into the statement text instead: escaping and binding \
are not the same guarantee. Upgrade the server, or build the statement yourself";
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct Rows {
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub columns: Vec<String>,
#[serde(default, deserialize_with = "crate::serde_null::or_default")]
pub rows: Vec<Vec<String>>,
}
impl Rows {
pub fn len(&self) -> usize {
self.rows.len()
}
pub fn is_empty(&self) -> bool {
self.rows.is_empty()
}
pub fn get(&self, row: usize, column: &str) -> Option<&str> {
let index = self.columns.iter().position(|c| c == column)?;
self.rows.get(row)?.get(index).map(String::as_str)
}
pub fn maps(&self) -> Vec<HashMap<&str, &str>> {
self.rows
.iter()
.map(|row| {
self.columns
.iter()
.zip(row.iter())
.map(|(c, v)| (c.as_str(), v.as_str()))
.collect()
})
.collect()
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Statement {
pub sql: String,
pub params: Vec<Param>,
}
impl Statement {
pub fn new(sql: impl Into<String>) -> Self {
Self {
sql: sql.into(),
params: Vec::new(),
}
}
pub fn with_params(sql: impl Into<String>, params: impl Into<Vec<Param>>) -> Self {
Self {
sql: sql.into(),
params: params.into(),
}
}
}
impl From<&str> for Statement {
fn from(sql: &str) -> Self {
Statement::new(sql)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
pub struct TransactionResult {
#[serde(default)]
pub statements: i64,
#[serde(default)]
pub committed_writes: i64,
#[serde(default)]
pub discarded_writes: i64,
#[serde(default, rename = "transaction")]
pub outcome: String,
#[serde(default)]
pub nested_depth: Option<i64>,
}
impl Client {
pub fn execute(&mut self, sql: &str) -> Result<Response> {
let body = self.sql_body(sql, &[])?;
self.send(json!({"Sql": {"Exec": body}}))
}
pub fn execute_params(&mut self, sql: &str, params: &[Param]) -> Result<Response> {
let body = self.sql_body(sql, params)?;
self.send(json!({"Sql": {"Exec": body}}))
}
pub fn query(&mut self, sql: &str) -> Result<Rows> {
let body = self.sql_body(sql, &[])?;
let response = self.send(json!({"Sql": {"Query": body}}))?;
rows_of(&response, "query")
}
pub fn query_params(&mut self, sql: &str, params: &[Param]) -> Result<Rows> {
let body = self.sql_body(sql, params)?;
let response = self.send(json!({"Sql": {"Query": body}}))?;
rows_of(&response, "query")
}
fn sql_body(&self, sql: &str, params: &[Param]) -> Result<Value> {
if params.is_empty() {
return Ok(json!({"sql": sql}));
}
if !self.server_params_granted() {
return Err(Error::feature_refusal(NO_SERVER_PARAMS));
}
for (index, param) in params.iter().enumerate() {
param.validate(index)?;
}
Ok(json!({"sql": sql, "params": params}))
}
pub fn transaction(&mut self, statements: &[Statement]) -> Result<TransactionResult> {
let (script, params) = build_transaction_script(statements)?;
let response = self.execute_params(&script, ¶ms)?;
response.decode("transaction")
}
pub fn begin(&mut self) -> Result<TransactionResult> {
if !self.session_txn_granted() {
return Err(Error::feature_refusal(
"this server did not grant session transactions (SESSION_TXN was not in the \
granted feature set), so begin/commit/rollback cannot open a transaction on this connection. \
Use transaction(...) to send the whole unit as one request",
));
}
self.transaction_control("BEGIN")
}
pub fn commit(&mut self) -> Result<TransactionResult> {
self.transaction_control("COMMIT")
}
pub fn rollback(&mut self) -> Result<TransactionResult> {
self.transaction_control("ROLLBACK")
}
pub fn with_transaction<T>(
&mut self,
work: impl FnOnce(&mut Client) -> Result<T>,
) -> Result<T> {
self.begin()?;
match work(self) {
Ok(value) => {
self.commit()?;
Ok(value)
}
Err(error) => {
if self.in_transaction() {
let _ = self.rollback();
}
Err(error)
}
}
}
fn transaction_control(&mut self, keyword: &str) -> Result<TransactionResult> {
match self.execute(keyword) {
Ok(response) => {
let result: TransactionResult = response.decode(keyword)?;
self.set_txn_open(keyword == "BEGIN" || result.nested_depth.is_some());
Ok(result)
}
Err(error) => {
if keyword != "BEGIN" && !error.was_refused_locally() {
self.set_txn_open(false);
}
Err(error)
}
}
}
}
fn rows_of(response: &Response, what: &str) -> Result<Rows> {
let data = response.expect("Rows", what)?;
serde_json::from_value(data.clone())
.map_err(|e| Error::protocol(format!("malformed Rows payload: {e}")))
}
pub(crate) fn build_transaction_script(statements: &[Statement]) -> Result<(String, Vec<Param>)> {
if statements.is_empty() {
return Err(Error::invalid("a transaction needs at least one statement"));
}
let mut parts = Vec::with_capacity(statements.len());
let mut params = Vec::new();
for statement in statements {
let text = statement.sql.trim().trim_end_matches(';').trim();
if text.is_empty() {
return Err(Error::invalid(
"every statement in a transaction must be non-empty SQL",
));
}
let first = text
.split_whitespace()
.next()
.unwrap_or_default()
.to_ascii_uppercase();
if matches!(first.as_str(), "BEGIN" | "START" | "COMMIT" | "ROLLBACK") {
return Err(Error::invalid(format!(
"transaction() brackets the script itself — remove the `{first}` statement"
)));
}
parts.push(text.to_string());
params.extend(statement.params.iter().cloned());
}
Ok((format!("BEGIN; {}; COMMIT", parts.join("; ")), params))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::params;
#[test]
fn a_script_keeps_its_placeholders_and_flattens_its_parameters() {
let (script, params) = build_transaction_script(&[
Statement::with_params("INSERT INTO t VALUES (?, ?)", params![1, "ada"]),
Statement::with_params("UPDATE t SET name = ? WHERE id = ?", params!["bob", 1]),
])
.unwrap();
assert_eq!(
script,
"BEGIN; INSERT INTO t VALUES (?, ?); UPDATE t SET name = ? WHERE id = ?; COMMIT"
);
assert_eq!(params.len(), 4);
assert_eq!(params[1], Param::Text("ada".into()));
assert_eq!(params[2], Param::Text("bob".into()));
}
#[test]
fn trailing_semicolons_and_whitespace_do_not_break_the_script() {
let (script, _) =
build_transaction_script(&[Statement::new(" INSERT INTO t VALUES (1) ; ")]).unwrap();
assert_eq!(script, "BEGIN; INSERT INTO t VALUES (1); COMMIT");
}
#[test]
fn caller_supplied_transaction_control_is_refused() {
for sql in ["BEGIN", "begin", "COMMIT", "ROLLBACK", "START TRANSACTION"] {
let err = build_transaction_script(&[Statement::new(sql)]).unwrap_err();
assert_eq!(err.kind, crate::ErrorKind::InvalidArgument);
}
}
#[test]
fn an_empty_transaction_is_refused() {
assert!(build_transaction_script(&[]).is_err());
assert!(build_transaction_script(&[Statement::new(" ; ")]).is_err());
}
#[test]
fn rows_can_be_read_by_column_name() {
let rows = Rows {
columns: vec!["id".into(), "name".into()],
rows: vec![vec!["1".into(), "ada".into()]],
};
assert_eq!(rows.get(0, "name"), Some("ada"));
assert_eq!(rows.get(0, "missing"), None);
assert_eq!(rows.maps()[0]["id"], "1");
assert_eq!(rows.len(), 1);
assert!(!rows.is_empty());
}
}