use crate::Value;
pub trait Dialect: Clone + Copy {
fn param(&self, idx: usize) -> String;
fn bool_lit(&self, val: bool) -> &'static str;
fn regex_op(&self) -> &'static str;
fn in_clause(&self, field: &str, values: &[Value], start_idx: usize) -> (String, Vec<Value>);
fn not_in_clause(
&self,
field: &str,
values: &[Value],
start_idx: usize,
) -> (String, Vec<Value>);
fn supports_ilike(&self) -> bool;
fn starts_with_clause(&self, field: &str, idx: usize) -> String;
fn ends_with_clause(&self, field: &str, idx: usize) -> String;
fn contains_clause(&self, field: &str, idx: usize) -> String;
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct Postgres;
impl Dialect for Postgres {
#[inline]
fn param(&self, idx: usize) -> String {
format!("${idx}")
}
#[inline]
fn bool_lit(&self, val: bool) -> &'static str {
if val { "TRUE" } else { "FALSE" }
}
#[inline]
fn regex_op(&self) -> &'static str {
"~"
}
fn in_clause(&self, field: &str, values: &[Value], start_idx: usize) -> (String, Vec<Value>) {
let sql = format!("{field} = ANY(${start_idx})");
(sql, vec![Value::Array(values.to_vec())])
}
fn not_in_clause(
&self,
field: &str,
values: &[Value],
start_idx: usize,
) -> (String, Vec<Value>) {
let sql = format!("{field} != ALL(${start_idx})");
(sql, vec![Value::Array(values.to_vec())])
}
#[inline]
fn supports_ilike(&self) -> bool {
true
}
#[inline]
fn starts_with_clause(&self, field: &str, idx: usize) -> String {
format!("{field} LIKE ${idx} || '%'")
}
#[inline]
fn ends_with_clause(&self, field: &str, idx: usize) -> String {
format!("{field} LIKE '%' || ${idx}")
}
#[inline]
fn contains_clause(&self, field: &str, idx: usize) -> String {
format!("{field} LIKE '%' || ${idx} || '%'")
}
}
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct Sqlite;
impl Dialect for Sqlite {
#[inline]
fn param(&self, idx: usize) -> String {
format!("?{idx}")
}
#[inline]
fn bool_lit(&self, val: bool) -> &'static str {
if val { "1" } else { "0" }
}
#[inline]
fn regex_op(&self) -> &'static str {
"LIKE"
}
fn in_clause(&self, field: &str, values: &[Value], start_idx: usize) -> (String, Vec<Value>) {
let placeholders: Vec<String> = (0..values.len())
.map(|i| format!("?{}", start_idx + i))
.collect();
let sql = format!("{} IN ({})", field, placeholders.join(", "));
(sql, values.to_vec())
}
fn not_in_clause(
&self,
field: &str,
values: &[Value],
start_idx: usize,
) -> (String, Vec<Value>) {
let placeholders: Vec<String> = (0..values.len())
.map(|i| format!("?{}", start_idx + i))
.collect();
let sql = format!("{} NOT IN ({})", field, placeholders.join(", "));
(sql, values.to_vec())
}
#[inline]
fn supports_ilike(&self) -> bool {
false
}
#[inline]
fn starts_with_clause(&self, field: &str, idx: usize) -> String {
format!("{field} LIKE ?{idx} || '%'")
}
#[inline]
fn ends_with_clause(&self, field: &str, idx: usize) -> String {
format!("{field} LIKE '%' || ?{idx}")
}
#[inline]
fn contains_clause(&self, field: &str, idx: usize) -> String {
format!("{field} LIKE '%' || ?{idx} || '%'")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_postgres_params() {
let pg = Postgres;
assert_eq!(pg.param(1), "$1");
assert_eq!(pg.param(10), "$10");
}
#[test]
fn test_sqlite_params() {
let sqlite = Sqlite;
assert_eq!(sqlite.param(1), "?1");
assert_eq!(sqlite.param(10), "?10");
}
#[test]
fn test_postgres_bool() {
let pg = Postgres;
assert_eq!(pg.bool_lit(true), "TRUE");
assert_eq!(pg.bool_lit(false), "FALSE");
}
#[test]
fn test_sqlite_bool() {
let sqlite = Sqlite;
assert_eq!(sqlite.bool_lit(true), "1");
assert_eq!(sqlite.bool_lit(false), "0");
}
#[test]
fn test_postgres_in_clause() {
let pg = Postgres;
let values = vec![Value::String("a".into()), Value::String("b".into())];
let (sql, params) = pg.in_clause("status", &values, 1);
assert_eq!(sql, "status = ANY($1)");
assert_eq!(params.len(), 1); }
#[test]
fn test_sqlite_in_clause() {
let sqlite = Sqlite;
let values = vec![Value::String("a".into()), Value::String("b".into())];
let (sql, params) = sqlite.in_clause("status", &values, 1);
assert_eq!(sql, "status IN (?1, ?2)");
assert_eq!(params.len(), 2); }
#[test]
fn test_postgres_regex_op() {
let pg = Postgres;
assert_eq!(pg.regex_op(), "~");
}
#[test]
fn test_sqlite_regex_op() {
let sqlite = Sqlite;
assert_eq!(sqlite.regex_op(), "LIKE");
}
#[test]
fn test_postgres_supports_ilike() {
let pg = Postgres;
assert!(pg.supports_ilike());
}
#[test]
fn test_sqlite_supports_ilike() {
let sqlite = Sqlite;
assert!(!sqlite.supports_ilike());
}
#[test]
fn test_postgres_not_in_clause() {
let pg = Postgres;
let values = vec![Value::Int(1), Value::Int(2), Value::Int(3)];
let (sql, params) = pg.not_in_clause("id", &values, 1);
assert_eq!(sql, "id != ALL($1)");
assert_eq!(params.len(), 1);
let Value::Array(arr) = ¶ms[0] else {
panic!("expected Value::Array, got {:?}", params[0])
};
assert_eq!(arr.len(), 3);
}
#[test]
fn test_sqlite_not_in_clause() {
let sqlite = Sqlite;
let values = vec![Value::Int(1), Value::Int(2), Value::Int(3)];
let (sql, params) = sqlite.not_in_clause("id", &values, 1);
assert_eq!(sql, "id NOT IN (?1, ?2, ?3)");
assert_eq!(params.len(), 3);
}
#[test]
fn test_sqlite_not_in_clause_with_offset() {
let sqlite = Sqlite;
let values = vec![Value::String("x".into()), Value::String("y".into())];
let (sql, params) = sqlite.not_in_clause("name", &values, 5);
assert_eq!(sql, "name NOT IN (?5, ?6)");
assert_eq!(params.len(), 2);
}
#[test]
fn test_postgres_starts_with_clause() {
let pg = Postgres;
assert_eq!(pg.starts_with_clause("name", 1), "name LIKE $1 || '%'");
assert_eq!(pg.starts_with_clause("title", 5), "title LIKE $5 || '%'");
}
#[test]
fn test_sqlite_starts_with_clause() {
let sqlite = Sqlite;
assert_eq!(sqlite.starts_with_clause("name", 1), "name LIKE ?1 || '%'");
assert_eq!(
sqlite.starts_with_clause("title", 5),
"title LIKE ?5 || '%'"
);
}
#[test]
fn test_postgres_ends_with_clause() {
let pg = Postgres;
assert_eq!(pg.ends_with_clause("name", 1), "name LIKE '%' || $1");
assert_eq!(pg.ends_with_clause("email", 3), "email LIKE '%' || $3");
}
#[test]
fn test_sqlite_ends_with_clause() {
let sqlite = Sqlite;
assert_eq!(sqlite.ends_with_clause("name", 1), "name LIKE '%' || ?1");
assert_eq!(sqlite.ends_with_clause("email", 3), "email LIKE '%' || ?3");
}
#[test]
fn test_postgres_contains_clause() {
let pg = Postgres;
assert_eq!(pg.contains_clause("name", 1), "name LIKE '%' || $1 || '%'");
assert_eq!(
pg.contains_clause("description", 2),
"description LIKE '%' || $2 || '%'"
);
}
#[test]
fn test_sqlite_contains_clause() {
let sqlite = Sqlite;
assert_eq!(
sqlite.contains_clause("name", 1),
"name LIKE '%' || ?1 || '%'"
);
assert_eq!(
sqlite.contains_clause("description", 2),
"description LIKE '%' || ?2 || '%'"
);
}
#[test]
fn test_postgres_in_clause_with_ints() {
let pg = Postgres;
let values = vec![Value::Int(1), Value::Int(2), Value::Int(3)];
let (sql, params) = pg.in_clause("id", &values, 2);
assert_eq!(sql, "id = ANY($2)");
assert_eq!(params.len(), 1);
}
#[test]
fn test_sqlite_in_clause_with_ints() {
let sqlite = Sqlite;
let values = vec![Value::Int(1), Value::Int(2), Value::Int(3)];
let (sql, params) = sqlite.in_clause("id", &values, 2);
assert_eq!(sql, "id IN (?2, ?3, ?4)");
assert_eq!(params.len(), 3);
}
#[test]
fn test_sqlite_in_clause_single_value() {
let sqlite = Sqlite;
let values = vec![Value::Int(42)];
let (sql, params) = sqlite.in_clause("id", &values, 1);
assert_eq!(sql, "id IN (?1)");
assert_eq!(params.len(), 1);
}
#[test]
fn test_postgres_in_clause_single_value() {
let pg = Postgres;
let values = vec![Value::String("only".into())];
let (sql, params) = pg.in_clause("name", &values, 1);
assert_eq!(sql, "name = ANY($1)");
assert_eq!(params.len(), 1);
}
#[test]
fn test_sqlite_in_clause_empty() {
let sqlite = Sqlite;
let values: Vec<Value> = vec![];
let (sql, _params) = sqlite.in_clause("id", &values, 1);
assert_eq!(sql, "id IN ()");
}
#[test]
fn test_postgres_in_clause_empty() {
let pg = Postgres;
let values: Vec<Value> = vec![];
let (sql, params) = pg.in_clause("id", &values, 1);
assert_eq!(sql, "id = ANY($1)");
let Value::Array(arr) = ¶ms[0] else {
panic!("expected Value::Array, got {:?}", params[0])
};
assert!(arr.is_empty());
}
#[test]
fn test_postgres_default() {
let pg = Postgres;
assert_eq!(pg.param(1), "$1");
}
#[test]
fn test_sqlite_default() {
let sqlite = Sqlite;
assert_eq!(sqlite.param(1), "?1");
}
#[test]
fn test_postgres_clone_copy() {
let pg = Postgres;
let pg_clone = pg;
let pg_copy = pg;
assert_eq!(pg_clone.param(1), pg_copy.param(1));
}
#[test]
fn test_sqlite_clone_copy() {
let sqlite = Sqlite;
let sqlite_clone = sqlite;
let sqlite_copy = sqlite;
assert_eq!(sqlite_clone.param(1), sqlite_copy.param(1));
}
#[test]
fn test_postgres_in_clause_high_index() {
let pg = Postgres;
let values = vec![Value::Int(1)];
let (sql, _) = pg.in_clause("id", &values, 100);
assert_eq!(sql, "id = ANY($100)");
}
#[test]
fn test_sqlite_in_clause_high_index() {
let sqlite = Sqlite;
let values = vec![Value::Int(1), Value::Int(2)];
let (sql, _) = sqlite.in_clause("id", &values, 100);
assert_eq!(sql, "id IN (?100, ?101)");
}
#[test]
fn test_sqlite_not_in_clause_high_index() {
let sqlite = Sqlite;
let values = vec![Value::Int(1), Value::Int(2)];
let (sql, _) = sqlite.not_in_clause("id", &values, 50);
assert_eq!(sql, "id NOT IN (?50, ?51)");
}
#[test]
fn test_postgres_not_in_clause_high_index() {
let pg = Postgres;
let values = vec![Value::Int(1)];
let (sql, _) = pg.not_in_clause("id", &values, 99);
assert_eq!(sql, "id != ALL($99)");
}
#[test]
fn test_postgres_debug() {
let pg = Postgres;
let debug_str = format!("{pg:?}");
assert_eq!(debug_str, "Postgres");
}
#[test]
fn test_sqlite_debug() {
let sqlite = Sqlite;
let debug_str = format!("{sqlite:?}");
assert_eq!(debug_str, "Sqlite");
}
#[test]
#[allow(clippy::default_trait_access)]
fn test_postgres_default_trait() {
let pg: Postgres = Default::default();
assert_eq!(pg.param(1), "$1");
}
#[test]
#[allow(clippy::default_trait_access)]
fn test_sqlite_default_trait() {
let sqlite: Sqlite = Default::default();
assert_eq!(sqlite.param(1), "?1");
}
#[test]
#[allow(clippy::clone_on_copy)]
fn test_postgres_clone_explicit() {
let pg = Postgres;
let pg_cloned = pg.clone();
assert_eq!(pg_cloned.param(1), "$1");
}
#[test]
#[allow(clippy::clone_on_copy)]
fn test_sqlite_clone_explicit() {
let sqlite = Sqlite;
let sqlite_cloned = sqlite.clone();
assert_eq!(sqlite_cloned.param(1), "?1");
}
#[test]
fn test_sqlite_not_in_clause_empty() {
let sqlite = Sqlite;
let values: Vec<Value> = vec![];
let (sql, params) = sqlite.not_in_clause("id", &values, 1);
assert_eq!(sql, "id NOT IN ()");
assert!(params.is_empty());
}
#[test]
fn test_postgres_not_in_clause_empty() {
let pg = Postgres;
let values: Vec<Value> = vec![];
let (sql, params) = pg.not_in_clause("id", &values, 1);
assert_eq!(sql, "id != ALL($1)");
assert_eq!(params.len(), 1);
let Value::Array(arr) = ¶ms[0] else {
panic!("expected Value::Array, got {:?}", params[0])
};
assert!(arr.is_empty());
}
}