#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DslOp {
Eq,
Ne,
Gt,
Lt,
Ge,
Le,
}
impl DslOp {
pub fn as_sql(self) -> &'static str {
match self {
DslOp::Eq => "=",
DslOp::Ne => "!=",
DslOp::Gt => ">",
DslOp::Lt => "<",
DslOp::Ge => ">=",
DslOp::Le => "<=",
}
}
}
#[derive(Debug, Clone)]
pub struct DslCondition {
pub column: String,
pub op: DslOp,
pub literal: String,
}
#[derive(Debug, Clone, Default)]
pub struct QueryFragment {
pub table: Option<String>,
pub columns: Vec<String>,
pub conditions: Vec<DslCondition>,
pub order_column: Option<String>,
pub order_desc: bool,
pub limit: Option<u64>,
}
pub fn is_safe_ident(name: &str) -> bool {
let mut chars = name.chars();
matches!(chars.next(), Some(c) if c.is_ascii_alphabetic() || c == '_')
&& name.len() <= 64
&& chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn sql_string_literal(value: &str) -> String {
format!("'{}'", value.replace('\'', "''"))
}
impl QueryFragment {
pub fn to_sql(&self) -> String {
let columns = if self.columns.is_empty() {
"*".to_string()
} else {
self.columns
.iter()
.map(|c| {
assert!(is_safe_ident(c), "unsafe column identifier: {c}");
c.clone()
})
.collect::<Vec<_>>()
.join(", ")
};
let table = self.table.as_deref().unwrap_or("(?)");
assert!(
table == "(?)" || is_safe_ident(table),
"unsafe table identifier: {table}"
);
let mut sql = format!("SELECT {columns} FROM {table}");
if !self.conditions.is_empty() {
let clauses: Vec<String> = self
.conditions
.iter()
.map(|c| {
assert!(is_safe_ident(&c.column), "unsafe column: {}", c.column);
format!("{} {} {}", c.column, c.op.as_sql(), c.literal)
})
.collect();
sql.push_str(" WHERE ");
sql.push_str(&clauses.join(" AND "));
}
if let Some(order) = &self.order_column {
assert!(is_safe_ident(order), "unsafe order column: {order}");
let dir = if self.order_desc { "DESC" } else { "ASC" };
sql.push_str(&format!(" ORDER BY {order} {dir}"));
}
if let Some(limit) = self.limit {
sql.push_str(&format!(" LIMIT {limit}"));
}
sql
}
}
fn unescape_rust_string(inner: &str) -> String {
let mut out = String::with_capacity(inner.len());
let mut chars = inner.chars();
while let Some(c) = chars.next() {
if c != '\\' {
out.push(c);
continue;
}
match chars.next() {
Some('n') => out.push('\n'),
Some('r') => out.push('\r'),
Some('t') => out.push('\t'),
Some('0') => out.push('\0'),
Some('\\') => out.push('\\'),
Some('"') => out.push('"'),
Some('\'') => out.push('\''),
Some('u') => {
let mut hex = String::new();
let mut closed = false;
if chars.next() == Some('{') {
for hc in chars.by_ref() {
if hc == '}' {
closed = true;
break;
}
hex.push(hc);
}
}
match (
closed,
u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32),
) {
(true, Some(ch)) => out.push(ch),
_ => {
out.push_str("\\u{");
out.push_str(&hex);
if closed {
out.push('}');
}
}
}
}
Some(other) => {
out.push('\\');
out.push(other);
}
None => out.push('\\'),
}
}
out
}
pub fn literal_from_token(token: &str) -> String {
let trimmed = token.trim();
let bytes = trimmed.as_bytes();
let is_quoted = bytes.len() >= 2
&& ((bytes[0] == b'"' && bytes[bytes.len() - 1] == b'"')
|| (bytes[0] == b'\'' && bytes[bytes.len() - 1] == b'\''));
if is_quoted {
let inner = &trimmed[1..trimmed.len() - 1];
let unescaped = unescape_rust_string(inner);
format!("'{}'", unescaped.replace('\'', "''"))
} else {
trimmed.to_string()
}
}
#[macro_export]
#[doc(hidden)]
macro_rules! __dsl_value {
($v:literal) => {
$crate::database::query_dsl::literal_from_token(stringify!($v))
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! __dsl_op {
(==) => {
$crate::database::query_dsl::DslOp::Eq
};
(!=) => {
$crate::database::query_dsl::DslOp::Ne
};
(>) => {
$crate::database::query_dsl::DslOp::Gt
};
(<) => {
$crate::database::query_dsl::DslOp::Lt
};
(>=) => {
$crate::database::query_dsl::DslOp::Ge
};
(<=) => {
$crate::database::query_dsl::DslOp::Le
};
}
#[macro_export]
#[doc(hidden)]
macro_rules! __dsl_conditions {
($builder:ident, $col:ident $op:tt $val:literal) => {
$builder.add_condition(
stringify!($col),
$crate::__dsl_op!($op),
$crate::__dsl_value!($val),
);
};
($builder:ident, $col:ident $op:tt $val:literal, $($rest:tt)+) => {
$builder.add_condition(
stringify!($col),
$crate::__dsl_op!($op),
$crate::__dsl_value!($val),
);
$crate::__dsl_conditions!($builder, $($rest)+);
};
}
#[macro_export]
macro_rules! q {
(select [$($col:ident),+ $(,)?] from $table:ident
$(where $($wcol:ident $wop:tt $wval:literal),+ $(,)?)?
$(order by $ocol:ident $odir:ident)?
$(limit $lim:literal)?
$(,)?
) => {{
#[allow(unused_mut)]
let mut fragment = $crate::database::query_dsl::QueryFragment {
table: Some(stringify!($table).to_string()),
columns: vec![$(stringify!($col).to_string()),+],
..Default::default()
};
$( $crate::__dsl_conditions!(fragment, $($wcol $wop $wval),+); )?
$( fragment.order_column = Some(stringify!($ocol).to_string());
fragment.order_desc = match stringify!($odir) { "desc" => true, _ => false }; )?
$( fragment.limit = Some($lim); )?
fragment
}};
(select * from $table:ident
$(where $($wcol:ident $wop:tt $wval:literal),+ $(,)?)?
$(order by $ocol:ident $odir:ident)?
$(limit $lim:literal)?
$(,)?
) => {{
#[allow(unused_mut)]
let mut fragment = $crate::database::query_dsl::QueryFragment {
table: Some(stringify!($table).to_string()),
..Default::default()
};
$( $crate::__dsl_conditions!(fragment, $($wcol $wop $wval),+); )?
$( fragment.order_column = Some(stringify!($ocol).to_string());
fragment.order_desc = match stringify!($odir) { "desc" => true, _ => false }; )?
$( fragment.limit = Some($lim); )?
fragment
}};
}
impl QueryFragment {
pub fn add_condition(&mut self, column: &str, op: DslOp, literal: String) {
self.conditions.push(DslCondition {
column: column.to_string(),
op,
literal,
});
}
}