1#![warn(missing_docs)]
4#![forbid(unsafe_code)]
5
6macro_rules! render_returning {
17 ($ctx:expr, $returning:expr, $quote:ident) => {{
18 if !$returning.is_empty() {
19 $ctx.sql.push_str(" RETURNING ");
20 for (i, col) in $returning.iter().enumerate() {
21 if i > 0 {
22 $ctx.sql.push_str(", ");
23 }
24 $ctx.sql.push_str(&$quote(&col.table));
25 $ctx.sql.push('.');
26 $ctx.sql.push_str(&$quote(&col.name));
27 $ctx.sql.push_str(" AS ");
28 $ctx.sql.push_str(&$quote(&col.alias()));
29 }
30 }
31 }};
32}
33
34macro_rules! render_insert_body {
38 ($ctx:expr, $insert:expr, $quote:ident, $supports_returning:expr, $supports_enum_cast:expr) => {{
39 $ctx.sql.push_str("INSERT INTO ");
40 $ctx.sql.push_str(&$quote(&$insert.table));
41
42 $ctx.sql.push_str(" (");
43 for (i, col) in $insert.columns.iter().enumerate() {
44 if i > 0 {
45 $ctx.sql.push_str(", ");
46 }
47 $ctx.sql.push_str(&$quote(&col.name));
48 }
49 $ctx.sql.push(')');
50
51 $ctx.sql.push_str(" VALUES ");
52 for (row_idx, row) in $insert.values.iter().enumerate() {
53 if row_idx > 0 {
54 $ctx.sql.push_str(", ");
55 }
56 $ctx.sql.push('(');
57 for (val_idx, value) in row.iter().enumerate() {
58 if val_idx > 0 {
59 $ctx.sql.push_str(", ");
60 }
61 if matches!(value, nautilus_core::Value::Null) {
62 $ctx.sql.push_str("NULL");
63 } else {
64 let placeholder = $ctx.push_param(value.clone());
65 $ctx.sql.push_str(&placeholder);
66 if $supports_enum_cast {
67 if let nautilus_core::Value::Enum { type_name, .. } = value {
68 $ctx.sql.push_str("::");
69 $ctx.sql.push_str(type_name);
70 }
71 }
72 }
73 }
74 $ctx.sql.push(')');
75 }
76
77 if $supports_returning {
78 render_returning!($ctx, $insert.returning, $quote);
79 }
80 }};
81}
82
83macro_rules! render_update_body {
88 ($ctx:expr, $update:expr, $quote:ident, $render_expr:ident, $supports_returning:expr, $supports_enum_cast:expr) => {{
89 $ctx.sql.push_str("UPDATE ");
90 $ctx.sql.push_str(&$quote(&$update.table));
91
92 $ctx.sql.push_str(" SET ");
93 for (i, (col, value)) in $update.assignments.iter().enumerate() {
94 if i > 0 {
95 $ctx.sql.push_str(", ");
96 }
97 $ctx.sql.push_str(&$quote(&col.name));
98 $ctx.sql.push_str(" = ");
99 if matches!(value, nautilus_core::Value::Null) {
100 $ctx.sql.push_str("NULL");
101 } else {
102 let placeholder = $ctx.push_param(value.clone());
103 $ctx.sql.push_str(&placeholder);
104 if $supports_enum_cast {
105 if let nautilus_core::Value::Enum { type_name, .. } = value {
106 $ctx.sql.push_str("::");
107 $ctx.sql.push_str(type_name);
108 }
109 }
110 }
111 }
112
113 if let Some(ref filter) = $update.filter {
114 $ctx.sql.push_str(" WHERE ");
115 $render_expr($ctx, filter);
116 }
117
118 if $supports_returning {
119 render_returning!($ctx, $update.returning, $quote);
120 }
121 }};
122}
123
124macro_rules! render_delete_body {
129 ($ctx:expr, $delete:expr, $quote:ident, $render_expr:ident, $supports_returning:expr) => {{
130 $ctx.sql.push_str("DELETE FROM ");
131 $ctx.sql.push_str(&$quote(&$delete.table));
132
133 if let Some(ref filter) = $delete.filter {
134 $ctx.sql.push_str(" WHERE ");
135 $render_expr($ctx, filter);
136 }
137
138 if $supports_returning {
139 render_returning!($ctx, $delete.returning, $quote);
140 }
141 }};
142}
143
144macro_rules! render_select_body_core {
152 (
153 $ctx:expr, $select:expr,
154 $quote:ident, $render_expr:ident,
155 $distinct_on:expr, $mysql_limit_hack:expr
156 ) => {{
157 $ctx.sql.push_str("SELECT ");
158
159 if !$select.distinct.is_empty() {
162 if $distinct_on {
163 $ctx.sql.push_str("DISTINCT ON (");
164 for (i, col) in $select.distinct.iter().enumerate() {
165 if i > 0 {
166 $ctx.sql.push_str(", ");
167 }
168 $ctx.sql.push_str(&$quote(col));
169 }
170 $ctx.sql.push_str(") ");
171 } else {
172 $ctx.sql.push_str("DISTINCT ");
173 }
174 }
175
176 let join_items: Vec<&nautilus_core::SelectItem> =
177 $select.joins.iter().flat_map(|j| j.items.iter()).collect();
178 let has_items = !$select.items.is_empty() || !join_items.is_empty();
179
180 if !has_items {
181 $ctx.sql.push('*');
182 } else {
183 let mut first = true;
184 for item in $select.items.iter().chain(join_items.iter().copied()) {
185 if !first {
186 $ctx.sql.push_str(", ");
187 }
188 first = false;
189 match item {
191 nautilus_core::SelectItem::Column(col) => {
192 $ctx.sql.push_str(&$quote(&col.table));
193 $ctx.sql.push('.');
194 $ctx.sql.push_str(&$quote(&col.name));
195 $ctx.sql.push_str(" AS ");
196 $ctx.sql.push_str(&$quote(&col.alias()));
197 }
198 nautilus_core::SelectItem::Computed { expr, alias } => {
199 $ctx.sql.push('(');
200 $render_expr($ctx, expr);
201 $ctx.sql.push(')');
202 $ctx.sql.push_str(" AS ");
203 $ctx.sql.push_str(&$quote(alias));
204 }
205 }
206 }
207 }
208
209 $ctx.sql.push_str(" FROM ");
210 $ctx.sql.push_str(&$quote(&$select.table));
211
212 for join in &$select.joins {
213 match join.join_type {
214 nautilus_core::JoinType::Inner => $ctx.sql.push_str(" INNER JOIN "),
215 nautilus_core::JoinType::Left => $ctx.sql.push_str(" LEFT JOIN "),
216 }
217 $ctx.sql.push_str(&$quote(&join.table));
218 $ctx.sql.push_str(" ON ");
219 $render_expr($ctx, &join.on);
220 }
221
222 if let Some(ref filter) = $select.filter {
223 $ctx.sql.push_str(" WHERE ");
224 $render_expr($ctx, filter);
225 }
226
227 if !$select.group_by.is_empty() {
228 $ctx.sql.push_str(" GROUP BY ");
229 for (i, col) in $select.group_by.iter().enumerate() {
230 if i > 0 {
231 $ctx.sql.push_str(", ");
232 }
233 $ctx.sql.push_str(&$quote(&col.table));
234 $ctx.sql.push('.');
235 $ctx.sql.push_str(&$quote(&col.name));
236 }
237 }
238
239 if let Some(ref having) = $select.having {
240 $ctx.sql.push_str(" HAVING ");
241 $render_expr($ctx, having);
242 }
243
244 let has_col_order = !$select.order_by.is_empty();
245 let has_expr_order = !$select.order_by_exprs.is_empty();
246 if has_col_order || has_expr_order {
247 $ctx.sql.push_str(" ORDER BY ");
248 let mut first = true;
249 for order in &$select.order_by {
250 if !first {
251 $ctx.sql.push_str(", ");
252 }
253 first = false;
254 $ctx.sql.push_str(&$quote(&order.column));
255 match order.direction {
256 nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
257 nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
258 }
259 }
260 for (expr, dir) in &$select.order_by_exprs {
261 if !first {
262 $ctx.sql.push_str(", ");
263 }
264 first = false;
265 $render_expr($ctx, expr);
266 match dir {
267 nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
268 nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
269 }
270 }
271 }
272
273 if let Some(take) = $select.take {
275 $ctx.sql.push_str(" LIMIT ");
276 $ctx.sql.push_str(&take.unsigned_abs().to_string());
277 } else if $mysql_limit_hack && $select.skip.is_some() {
278 $ctx.sql.push_str(" LIMIT 18446744073709551615");
279 }
280
281 if let Some(skip) = $select.skip {
282 $ctx.sql.push_str(" OFFSET ");
283 $ctx.sql.push_str(&skip.to_string());
284 }
285 }};
286}
287
288macro_rules! render_expr_common {
307 (
308 $ctx:expr, $expr:expr,
309 $quote:ident, $render_expr:ident, $render_select_body:ident,
310 { $($specific:tt)* }
311 ) => {
312 match $expr {
313 nautilus_core::Expr::Column(name) => {
316 if let Some((table, column)) = name.split_once("__") {
317 $ctx.sql.push_str(&$quote(table));
318 $ctx.sql.push('.');
319 $ctx.sql.push_str(&$quote(column));
320 } else {
321 $ctx.sql.push_str(&$quote(name));
322 }
323 }
324 nautilus_core::Expr::Not(inner) => {
325 $ctx.sql.push_str("NOT (");
326 $render_expr($ctx, inner);
327 $ctx.sql.push(')');
328 }
329 nautilus_core::Expr::Exists(subquery) => {
330 $ctx.sql.push_str("EXISTS (");
331 $render_select_body($ctx, subquery);
332 $ctx.sql.push(')');
333 }
334 nautilus_core::Expr::NotExists(subquery) => {
335 $ctx.sql.push_str("NOT EXISTS (");
336 $render_select_body($ctx, subquery);
337 $ctx.sql.push(')');
338 }
339 nautilus_core::Expr::ScalarSubquery(subquery) => {
340 $ctx.sql.push('(');
341 $render_select_body($ctx, subquery);
342 $ctx.sql.push(')');
343 }
344 nautilus_core::Expr::IsNull(inner) => {
345 $ctx.sql.push('(');
346 $render_expr($ctx, inner);
347 $ctx.sql.push_str(" IS NULL)");
348 }
349 nautilus_core::Expr::IsNotNull(inner) => {
350 $ctx.sql.push('(');
351 $render_expr($ctx, inner);
352 $ctx.sql.push_str(" IS NOT NULL)");
353 }
354 nautilus_core::Expr::Literal(s) => {
358 $ctx.sql.push('\'');
359 $ctx.sql.push_str(&s.replace('\'', "''"));
360 $ctx.sql.push('\'');
361 }
362 nautilus_core::Expr::List(exprs) => {
363 for (i, e) in exprs.iter().enumerate() {
364 if i > 0 { $ctx.sql.push_str(", "); }
365 $render_expr($ctx, e);
366 }
367 }
368 nautilus_core::Expr::CaseWhen { condition, then } => {
369 $ctx.sql.push_str("CASE WHEN ");
370 $render_expr($ctx, condition);
371 $ctx.sql.push_str(" THEN ");
372 $render_expr($ctx, then);
373 $ctx.sql.push_str(" ELSE NULL END");
374 }
375 nautilus_core::Expr::Star => {
376 $ctx.sql.push('*');
377 }
378 $($specific)*
379 }
380 };
381}
382
383mod mysql;
384mod postgres;
385mod sqlite;
386
387pub use mysql::MysqlDialect;
388pub use postgres::PostgresDialect;
389pub use sqlite::SqliteDialect;
390
391use nautilus_core::{Delete, Insert, Result, Select, Update, Value};
392
393#[derive(Debug, Clone, PartialEq)]
397#[must_use]
398pub struct Sql {
399 pub text: String,
401 pub params: Vec<Value>,
403}
404
405pub trait Dialect {
409 fn supports_returning(&self) -> bool {
415 true
416 }
417
418 fn render_select(&self, select: &Select) -> Result<Sql>;
420
421 fn render_insert(&self, insert: &Insert) -> Result<Sql>;
423
424 fn render_update(&self, update: &Update) -> Result<Sql>;
426
427 fn render_delete(&self, delete: &Delete) -> Result<Sql>;
429}
430
431pub(crate) fn double_quote_identifier(name: &str) -> String {
435 format!("\"{}\"", name.replace('"', "\"\""))
436}
437
438pub(crate) fn backtick_quote_identifier(name: &str) -> String {
442 format!("`{}`", name.replace('`', "``"))
443}
444
445#[inline]
451pub(crate) fn binary_op_sql(op: &nautilus_core::BinaryOp) -> &'static str {
452 match op {
453 nautilus_core::BinaryOp::Eq => "=",
454 nautilus_core::BinaryOp::Ne => "!=",
455 nautilus_core::BinaryOp::Lt => "<",
456 nautilus_core::BinaryOp::Le => "<=",
457 nautilus_core::BinaryOp::Gt => ">",
458 nautilus_core::BinaryOp::Ge => ">=",
459 nautilus_core::BinaryOp::And => "AND",
460 nautilus_core::BinaryOp::Or => "OR",
461 nautilus_core::BinaryOp::Like => "LIKE",
462 nautilus_core::BinaryOp::ArrayContains
463 | nautilus_core::BinaryOp::ArrayContainedBy
464 | nautilus_core::BinaryOp::ArrayOverlaps
465 | nautilus_core::BinaryOp::In
466 | nautilus_core::BinaryOp::NotIn => {
467 unreachable!(
468 "binary_op_sql: operator {:?} must be handled by dialect-specific code",
469 op
470 )
471 }
472 }
473}