1use crate::ast::{BinaryOp, Criteria, GroupOp, UnaryOp};
7use crate::functions::{Aggregate, Function};
8use crate::value::Value;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12#[non_exhaustive]
13pub enum Dialect {
14 Postgres,
16 MySql,
18 SqlServer,
20 Sqlite,
22}
23
24#[derive(Clone, Debug, PartialEq)]
26#[non_exhaustive]
27pub enum SqlParam {
28 Value(Value),
30 Named(String),
32}
33
34#[derive(Clone, Debug, PartialEq)]
36pub struct SqlQuery {
37 pub text: String,
39 pub params: Vec<SqlParam>,
41}
42
43#[derive(Clone, Debug, PartialEq, Eq)]
45#[non_exhaustive]
46pub enum SqlError {
47 Unsupported(String),
49}
50
51impl std::fmt::Display for SqlError {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 match self {
54 Self::Unsupported(what) => write!(f, "{what} cannot be translated to SQL"),
55 }
56 }
57}
58
59impl std::error::Error for SqlError {}
60
61pub fn to_sql(criteria: &Criteria, dialect: Dialect) -> Result<SqlQuery, SqlError> {
72 let mut w = Writer { dialect, params: Vec::new() };
73 let text = w.node(criteria)?;
74 Ok(SqlQuery { text, params: w.params })
75}
76
77struct Writer {
78 dialect: Dialect,
79 params: Vec<SqlParam>,
80}
81
82impl Writer {
83 fn bind(&mut self, p: SqlParam) -> String {
84 self.params.push(p);
85 let n = self.params.len();
86 match self.dialect {
87 Dialect::Postgres => format!("${n}"),
88 Dialect::MySql => "?".to_owned(),
89 Dialect::SqlServer => format!("@p{n}"),
90 Dialect::Sqlite => format!("?{n}"),
91 }
92 }
93
94 fn ident(&self, name: &str) -> String {
95 match self.dialect {
96 Dialect::Postgres | Dialect::Sqlite => format!("\"{}\"", name.replace('"', "\"\"")),
97 Dialect::MySql => format!("`{}`", name.replace('`', "``")),
98 Dialect::SqlServer => format!("[{}]", name.replace(']', "]]")),
99 }
100 }
101
102 fn term(&mut self, c: &Criteria) -> Result<String, SqlError> {
104 let s = self.node(c)?;
105 Ok(match c {
106 Criteria::Value(_)
107 | Criteria::Property(_)
108 | Criteria::Parameter(_)
109 | Criteria::Function(..)
110 | Criteria::Aggregate(_) => s,
111 _ => format!("({s})"),
112 })
113 }
114
115 fn node(&mut self, c: &Criteria) -> Result<String, SqlError> {
116 Ok(match c {
117 Criteria::Value(Value::Null) => "NULL".to_owned(),
118 Criteria::Value(v) => self.bind(SqlParam::Value(v.clone())),
119 Criteria::Parameter(p) => self.bind(SqlParam::Named(p.clone())),
120 Criteria::Property(path) => {
121 if path.first().is_some_and(|p| p == "^") {
122 return Err(SqlError::Unsupported("a parent row reference".into()));
123 }
124 path.iter().map(|p| self.ident(p)).collect::<Vec<_>>().join(".")
125 }
126 Criteria::Unary(UnaryOp::Not, a) => format!("NOT {}", self.term(a)?),
127 Criteria::Unary(UnaryOp::Minus, a) => format!("-{}", self.term(a)?),
128 Criteria::Unary(UnaryOp::BitNot, a) => format!("~{}", self.term(a)?),
129 Criteria::Binary(BinaryOp::Eq, a, b) if **b == Criteria::Value(Value::Null) => {
130 format!("{} IS NULL", self.term(a)?)
131 }
132 Criteria::Binary(BinaryOp::Ne, a, b) if **b == Criteria::Value(Value::Null) => {
133 format!("{} IS NOT NULL", self.term(a)?)
134 }
135 Criteria::Binary(op, a, b) => {
136 let symbol = match op {
137 BinaryOp::Add => "+",
138 BinaryOp::Sub => "-",
139 BinaryOp::Mul => "*",
140 BinaryOp::Div => "/",
141 BinaryOp::Rem => "%",
142 BinaryOp::Eq => "=",
143 BinaryOp::Ne => "<>",
144 BinaryOp::Lt => "<",
145 BinaryOp::Le => "<=",
146 BinaryOp::Gt => ">",
147 BinaryOp::Ge => ">=",
148 BinaryOp::Like => "LIKE",
149 BinaryOp::BitAnd => "&",
150 BinaryOp::BitOr => "|",
151 BinaryOp::BitXor => "^",
152 };
153 format!("{} {symbol} {}", self.term(a)?, self.term(b)?)
155 }
156 Criteria::Group(op, items) => {
157 let word = if *op == GroupOp::And { " AND " } else { " OR " };
158 let parts = items
159 .iter()
160 .map(|i| match i {
161 Criteria::Binary(
163 BinaryOp::Eq
164 | BinaryOp::Ne
165 | BinaryOp::Lt
166 | BinaryOp::Le
167 | BinaryOp::Gt
168 | BinaryOp::Ge
169 | BinaryOp::Like,
170 ..,
171 )
172 | Criteria::In(..)
173 | Criteria::Between(..)
174 | Criteria::IsNull(_) => self.node(i),
175 other => self.term(other),
176 })
177 .collect::<Result<Vec<_>, _>>()?;
178 parts.join(word)
179 }
180 Criteria::In(a, items) => {
181 let value = self.term(a)?;
182 let list = items.iter().map(|i| self.node(i)).collect::<Result<Vec<_>, _>>()?;
183 format!("{value} IN ({})", list.join(", "))
184 }
185 Criteria::Between(a, lo, hi) => {
186 let (a, lo, hi) = (self.term(a)?, self.term(lo)?, self.term(hi)?);
187 format!("{a} BETWEEN {lo} AND {hi}")
188 }
189 Criteria::IsNull(a) => format!("{} IS NULL", self.term(a)?),
190 Criteria::Function(f, args) => self.function(*f, args)?,
191 Criteria::Aggregate(call) => {
192 if call.collection.is_some() || call.condition.is_some() {
193 return Err(SqlError::Unsupported("an aggregate over a nested collection".into()));
194 }
195 let name = match call.function {
196 Aggregate::Sum => "SUM",
197 Aggregate::Count => "COUNT",
198 Aggregate::Avg => "AVG",
199 Aggregate::Min => "MIN",
200 Aggregate::Max => "MAX",
201 other => return Err(SqlError::Unsupported(format!("the aggregate {}", other.name()))),
202 };
203 match call.args.first() {
204 Some(a) => format!("{name}({})", self.node(a)?),
205 None => format!("{name}(*)"),
206 }
207 }
208 Criteria::Custom(name, _) => {
209 return Err(SqlError::Unsupported(format!("the custom function {name}")));
210 }
211 })
212 }
213
214 fn call(&mut self, name: &str, args: &[Criteria]) -> Result<String, SqlError> {
215 let list = args.iter().map(|a| self.node(a)).collect::<Result<Vec<_>, _>>()?;
216 Ok(format!("{name}({})", list.join(", ")))
217 }
218
219 fn like(&mut self, args: &[Criteria], before: bool, after: bool) -> Result<String, SqlError> {
221 let text = self.term(&args[0])?;
222 let Criteria::Value(Value::Text(part)) = &args[1] else {
223 return Err(SqlError::Unsupported("a text search with a computed pattern".into()));
224 };
225 let escaped = part.replace('\\', "\\\\").replace('%', "\\%").replace('_', "\\_");
226 let pattern = format!("{}{escaped}{}", if before { "%" } else { "" }, if after { "%" } else { "" });
227 let bound = self.bind(SqlParam::Value(Value::Text(pattern)));
228 Ok(format!("{text} LIKE {bound} ESCAPE '\\'"))
229 }
230
231 fn date_part(&mut self, part: &str, arg: &Criteria) -> Result<String, SqlError> {
232 let v = self.node(arg)?;
233 Ok(match self.dialect {
234 Dialect::Postgres => format!("EXTRACT({part} FROM {v})"),
235 Dialect::MySql => format!("{part}({v})"),
236 Dialect::SqlServer => format!("DATEPART({part}, {v})"),
237 Dialect::Sqlite => {
238 let code = match part {
239 "YEAR" => "%Y",
240 "MONTH" => "%m",
241 _ => "%d",
242 };
243 format!("CAST(strftime('{code}', {v}) AS INTEGER)")
244 }
245 })
246 }
247
248 fn function(&mut self, f: Function, args: &[Criteria]) -> Result<String, SqlError> {
249 use Function as F;
250 Ok(match f {
251 F::Upper => self.call("UPPER", args)?,
252 F::Lower => self.call("LOWER", args)?,
253 F::Trim => self.call("TRIM", args)?,
254 F::Abs => self.call("ABS", args)?,
255 F::Round => self.call("ROUND", args)?,
256 F::Floor => self.call("FLOOR", args)?,
257 F::Ceiling => self.call("CEILING", args)?,
258 F::Power => self.call("POWER", args)?,
259 F::Sqrt | F::Sqr => self.call("SQRT", args)?,
260 F::Replace => self.call("REPLACE", args)?,
261 F::Concat => match self.dialect {
262 Dialect::Sqlite => {
263 let parts = args.iter().map(|a| self.term(a)).collect::<Result<Vec<_>, _>>()?;
264 parts.join(" || ")
265 }
266 _ => self.call("CONCAT", args)?,
267 },
268 F::Len => match self.dialect {
269 Dialect::SqlServer => self.call("LEN", args)?,
270 Dialect::MySql => self.call("CHAR_LENGTH", args)?,
271 _ => self.call("LENGTH", args)?,
272 },
273 F::IsNull if args.len() == 2 => self.call("COALESCE", args)?,
274 F::IsNull => format!("{} IS NULL", self.term(&args[0])?),
275 F::IsNullOrEmpty => {
276 let a = self.term(&args[0])?;
277 format!("({a} IS NULL OR {a} = '')")
278 }
279 F::Iif => {
280 let mut s = String::from("CASE");
281 let mut i = 0;
282 while i + 1 < args.len() {
283 let (cond, value) = (self.node(&args[i])?, self.node(&args[i + 1])?);
284 s.push_str(&format!(" WHEN {cond} THEN {value}"));
285 i += 2;
286 }
287 let last = self.node(&args[args.len() - 1])?;
288 format!("{s} ELSE {last} END")
289 }
290 F::Contains => self.like(args, true, true)?,
291 F::StartsWith => self.like(args, false, true)?,
292 F::EndsWith => self.like(args, true, false)?,
293 F::GetYear => self.date_part("YEAR", &args[0])?,
294 F::GetMonth => self.date_part("MONTH", &args[0])?,
295 F::GetDay => self.date_part("DAY", &args[0])?,
296 other => return Err(SqlError::Unsupported(format!("the function {}", other.name()))),
297 })
298 }
299}