use std::sync::Arc;
use crate::BoxFuture;
use crate::js::{self, Value};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Dialect {
Sqlite,
Postgres,
Mysql,
}
#[derive(Clone, Debug, PartialEq)]
pub enum Param {
Null,
Int(i64),
Float(f64),
Text(String),
}
impl From<i64> for Param {
fn from(n: i64) -> Param {
Param::Int(n)
}
}
impl From<i32> for Param {
fn from(n: i32) -> Param {
Param::Int(i64::from(n))
}
}
impl From<usize> for Param {
fn from(n: usize) -> Param {
Param::Int(n as i64)
}
}
impl From<f64> for Param {
fn from(n: f64) -> Param {
if js::is_integer(n) && n.abs() < 9.007_199_254_740_992e15 { Param::Int(n as i64) } else { Param::Float(n) }
}
}
impl From<&str> for Param {
fn from(s: &str) -> Param {
Param::Text(s.to_string())
}
}
impl From<String> for Param {
fn from(s: String) -> Param {
Param::Text(s)
}
}
impl From<&String> for Param {
fn from(s: &String) -> Param {
Param::Text(s.clone())
}
}
impl<T: Into<Param>> From<Option<T>> for Param {
fn from(v: Option<T>) -> Param {
v.map_or(Param::Null, Into::into)
}
}
#[macro_export]
macro_rules! params {
() => { Vec::<$crate::store::Param>::new() };
($($v:expr),+ $(,)?) => { vec![$($crate::store::Param::from($v)),+] };
}
#[derive(Clone, Debug, PartialEq)]
pub enum Cell {
Null,
Int(i64),
Float(f64),
Text(String),
}
impl Cell {
pub fn to_value(&self) -> Value {
match self {
Cell::Null => Value::Null,
Cell::Int(n) => Value::Number(*n as f64),
Cell::Float(f) => Value::Number(*f),
Cell::Text(s) => Value::String(s.clone()),
}
}
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Row(pub Vec<(String, Cell)>);
static NULL: Cell = Cell::Null;
impl Row {
pub fn get(&self, name: &str) -> &Cell {
self.0.iter().find(|(n, _)| n == name).map_or(&NULL, |(_, c)| c)
}
pub fn is_null(&self, name: &str) -> bool {
matches!(self.get(name), Cell::Null)
}
pub fn num(&self, name: &str) -> f64 {
let n = match self.get(name) {
Cell::Null => 0.0,
Cell::Int(n) => *n as f64,
Cell::Float(f) => *f,
Cell::Text(t) => js::text_number(t),
};
if n.is_finite() { n } else { 0.0 }
}
pub fn int(&self, name: &str) -> i64 {
js::to_i64(self.num(name))
}
pub fn opt_int(&self, name: &str) -> Option<i64> {
if self.is_null(name) { None } else { Some(self.int(name)) }
}
pub fn text(&self, name: &str) -> String {
match self.get(name) {
Cell::Null => "null".into(),
Cell::Int(n) => n.to_string(),
Cell::Float(f) => js::format_number(*f),
Cell::Text(t) => t.clone(),
}
}
pub fn text_or(&self, name: &str, fallback: &str) -> String {
if self.is_null(name) { fallback.to_string() } else { self.text(name) }
}
pub fn to_value(&self) -> Value {
let mut o = js::Object::new();
for (name, cell) in &self.0 {
o.set(name.clone(), cell.to_value());
}
Value::Object(o)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DbError(pub String);
impl std::fmt::Display for DbError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for DbError {}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Hold {
Transaction,
Exclusive,
}
pub trait Db: Send + Sync {
fn dialect(&self) -> Dialect;
fn all<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<Vec<Row>, DbError>>;
fn run<'a>(&'a self, sql: &'a str, params: Vec<Param>) -> BoxFuture<'a, Result<u64, DbError>>;
fn hold(&self, kind: Hold) -> BoxFuture<'_, Result<Arc<dyn Db>, DbError>>;
fn finish(&self, _ok: bool) -> BoxFuture<'_, Result<(), DbError>> {
Box::pin(async { Ok(()) })
}
fn metered(&self) -> bool {
false
}
fn close(&self) -> BoxFuture<'_, ()> {
Box::pin(async {})
}
fn prefix(&self) -> Option<&str> {
None
}
}
pub fn fill_placeholders(
sql: &str,
params: &[Param],
mysql: bool,
fill: impl Fn(&Param) -> String,
) -> Result<String, DbError> {
let mut out = String::with_capacity(sql.len() + params.len() * 8);
let mut n = 0;
let mut quote: Option<char> = None;
for ch in sql.chars() {
if let Some(q) = quote {
if ch == q {
quote = None;
out.push(if mysql && ch == '"' { '`' } else { ch });
} else if mysql && q == '\'' && ch == '\\' {
out.push_str("\\\\");
} else if mysql && q == '"' && ch == '`' {
out.push_str("``");
} else {
out.push(ch);
}
} else if ch == '\'' || ch == '"' || (mysql && ch == '`') {
quote = Some(ch);
out.push(if mysql && ch == '"' { '`' } else { ch });
} else if ch == '?' {
let p = params
.get(n)
.ok_or_else(|| DbError("Runlight: a statement has more placeholders than values".into()))?;
out.push_str(&fill(p));
n += 1;
} else {
out.push(ch);
}
}
if n != params.len() {
return Err(DbError("Runlight: a statement has more values than placeholders".into()));
}
Ok(out)
}
pub fn postgres_literal(p: &Param) -> String {
match p {
Param::Null => "NULL".into(),
Param::Int(n) => format!("'{n}'"),
Param::Float(f) => format!("'{}'", js::format_number(*f)),
Param::Text(s) => {
let mut out = String::with_capacity(s.len() + 3);
out.push_str("E'");
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'\'' => out.push_str("''"),
_ => out.push(c),
}
}
out.push('\'');
out
}
}
}
pub fn mysql_literal(p: &Param) -> String {
match p {
Param::Null => "NULL".into(),
Param::Int(n) => n.to_string(),
Param::Float(f) => js::format_number(*f),
Param::Text(s) => {
let mut out = String::with_capacity(s.len() + 2);
out.push('\'');
for c in s.chars() {
match c {
'\0' => out.push_str("\\0"),
'\x08' => out.push_str("\\b"),
'\t' => out.push_str("\\t"),
'\x1a' => out.push_str("\\Z"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'"' => out.push_str("\\\""),
'\'' => out.push_str("''"),
'\\' => out.push_str("\\\\"),
_ => out.push(c),
}
}
out.push('\'');
out
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn placeholders_are_filled_outside_quotes() {
let sql = r#"SELECT "key", 'a?b\c' FROM t WHERE x = ? AND y LIKE ? ESCAPE '\'"#;
let params = vec![Param::Int(3), Param::Text("o'k\\".into())];
assert_eq!(
fill_placeholders(sql, ¶ms, true, mysql_literal).unwrap(),
r#"SELECT `key`, 'a?b\\c' FROM t WHERE x = 3 AND y LIKE 'o''k\\' ESCAPE '\\'"#
);
assert_eq!(
fill_placeholders(sql, ¶ms, false, postgres_literal).unwrap(),
r#"SELECT "key", 'a?b\c' FROM t WHERE x = '3' AND y LIKE E'o''k\\' ESCAPE '\'"#
);
assert!(fill_placeholders("?", &[], false, postgres_literal).is_err());
}
}