1#![warn(missing_docs)]
4#![forbid(unsafe_code)]
5
6macro_rules! render_returning {
15 ($ctx:expr, $returning:expr, $quote:ident) => {{
16 if !$returning.is_empty() {
17 $ctx.sql.push_str(" RETURNING ");
18 for (i, col) in $returning.iter().enumerate() {
19 if i > 0 {
20 $ctx.sql.push_str(", ");
21 }
22 $ctx.sql.push_str(&$quote(&col.table));
23 $ctx.sql.push('.');
24 $ctx.sql.push_str(&$quote(&col.name));
25 $ctx.sql.push_str(" AS ");
26 $ctx.sql.push_str(&$quote(&col.alias()));
27 }
28 }
29 }};
30}
31
32macro_rules! render_insert_body {
36 ($ctx:expr, $insert:expr, $quote:ident, $supports_returning:expr, $supports_enum_cast:expr) => {{
37 $ctx.sql.push_str("INSERT INTO ");
38 $ctx.sql.push_str(&$quote(&$insert.table));
39
40 $ctx.sql.push_str(" (");
41 for (i, col) in $insert.columns.iter().enumerate() {
42 if i > 0 {
43 $ctx.sql.push_str(", ");
44 }
45 $ctx.sql.push_str(&$quote(&col.name));
46 }
47 $ctx.sql.push(')');
48
49 $ctx.sql.push_str(" VALUES ");
50 for (row_idx, row) in $insert.values.iter().enumerate() {
51 if row_idx > 0 {
52 $ctx.sql.push_str(", ");
53 }
54 $ctx.sql.push('(');
55 for (val_idx, value) in row.iter().enumerate() {
56 if val_idx > 0 {
57 $ctx.sql.push_str(", ");
58 }
59 if matches!(value, nautilus_core::Value::Null) {
60 $ctx.sql.push_str("NULL");
61 } else {
62 let placeholder = $ctx.push_param(value.clone());
63 $ctx.sql.push_str(&placeholder);
64 if $supports_enum_cast {
65 if let nautilus_core::Value::Enum { type_name, .. } = value {
66 $ctx.sql.push_str("::");
67 $ctx.sql.push_str(type_name);
68 }
69 }
70 }
71 }
72 $ctx.sql.push(')');
73 }
74
75 if $supports_returning {
76 render_returning!($ctx, $insert.returning, $quote);
77 }
78 }};
79}
80
81macro_rules! render_update_body {
86 ($ctx:expr, $update:expr, $quote:ident, $render_expr:ident, $supports_returning:expr, $supports_enum_cast:expr) => {{
87 $ctx.sql.push_str("UPDATE ");
88 $ctx.sql.push_str(&$quote(&$update.table));
89
90 $ctx.sql.push_str(" SET ");
91 for (i, (col, value)) in $update.assignments.iter().enumerate() {
92 if i > 0 {
93 $ctx.sql.push_str(", ");
94 }
95 $ctx.sql.push_str(&$quote(&col.name));
96 $ctx.sql.push_str(" = ");
97 if matches!(value, nautilus_core::Value::Null) {
98 $ctx.sql.push_str("NULL");
99 } else {
100 let placeholder = $ctx.push_param(value.clone());
101 $ctx.sql.push_str(&placeholder);
102 if $supports_enum_cast {
103 if let nautilus_core::Value::Enum { type_name, .. } = value {
104 $ctx.sql.push_str("::");
105 $ctx.sql.push_str(type_name);
106 }
107 }
108 }
109 }
110
111 if let Some(ref filter) = $update.filter {
112 $ctx.sql.push_str(" WHERE ");
113 $render_expr($ctx, filter);
114 }
115
116 if $supports_returning {
117 render_returning!($ctx, $update.returning, $quote);
118 }
119 }};
120}
121
122macro_rules! render_delete_body {
127 ($ctx:expr, $delete:expr, $quote:ident, $render_expr:ident, $supports_returning:expr) => {{
128 $ctx.sql.push_str("DELETE FROM ");
129 $ctx.sql.push_str(&$quote(&$delete.table));
130
131 if let Some(ref filter) = $delete.filter {
132 $ctx.sql.push_str(" WHERE ");
133 $render_expr($ctx, filter);
134 }
135
136 if $supports_returning {
137 render_returning!($ctx, $delete.returning, $quote);
138 }
139 }};
140}
141
142macro_rules! render_select_body_core {
150 (
151 $ctx:expr, $select:expr,
152 $quote:ident, $render_expr:ident,
153 $distinct_on:expr, $mysql_limit_hack:expr
154 ) => {{
155 $ctx.sql.push_str("SELECT ");
156
157 if !$select.distinct.is_empty() {
160 if $distinct_on {
161 $ctx.sql.push_str("DISTINCT ON (");
162 for (i, col) in $select.distinct.iter().enumerate() {
163 if i > 0 {
164 $ctx.sql.push_str(", ");
165 }
166 crate::push_identifier_reference(&mut $ctx.sql, col, $quote);
167 }
168 $ctx.sql.push_str(") ");
169 } else {
170 $ctx.sql.push_str("DISTINCT ");
171 }
172 }
173
174 let join_items: Vec<&nautilus_core::SelectItem> =
175 $select.joins.iter().flat_map(|j| j.items.iter()).collect();
176 let has_items = !$select.items.is_empty() || !join_items.is_empty();
177
178 if !has_items {
179 $ctx.sql.push('*');
180 } else {
181 let mut first = true;
182 for item in $select.items.iter().chain(join_items.iter().copied()) {
183 if !first {
184 $ctx.sql.push_str(", ");
185 }
186 first = false;
187 match item {
188 nautilus_core::SelectItem::Column(col) => {
189 $ctx.sql.push_str(&$quote(&col.table));
190 $ctx.sql.push('.');
191 $ctx.sql.push_str(&$quote(&col.name));
192 $ctx.sql.push_str(" AS ");
193 $ctx.sql.push_str(&$quote(&col.alias()));
194 }
195 nautilus_core::SelectItem::Computed { expr, alias } => {
196 $ctx.sql.push('(');
197 $render_expr($ctx, expr);
198 $ctx.sql.push(')');
199 $ctx.sql.push_str(" AS ");
200 $ctx.sql.push_str(&$quote(alias));
201 }
202 }
203 }
204 }
205
206 $ctx.sql.push_str(" FROM ");
207 $ctx.sql.push_str(&$quote(&$select.table));
208
209 for join in &$select.joins {
210 match join.join_type {
211 nautilus_core::JoinType::Inner => $ctx.sql.push_str(" INNER JOIN "),
212 nautilus_core::JoinType::Left => $ctx.sql.push_str(" LEFT JOIN "),
213 }
214 $ctx.sql.push_str(&$quote(&join.table));
215 $ctx.sql.push_str(" ON ");
216 $render_expr($ctx, &join.on);
217 }
218
219 if let Some(ref filter) = $select.filter {
220 $ctx.sql.push_str(" WHERE ");
221 $render_expr($ctx, filter);
222 }
223
224 if !$select.group_by.is_empty() {
225 $ctx.sql.push_str(" GROUP BY ");
226 for (i, col) in $select.group_by.iter().enumerate() {
227 if i > 0 {
228 $ctx.sql.push_str(", ");
229 }
230 $ctx.sql.push_str(&$quote(&col.table));
231 $ctx.sql.push('.');
232 $ctx.sql.push_str(&$quote(&col.name));
233 }
234 }
235
236 if let Some(ref having) = $select.having {
237 $ctx.sql.push_str(" HAVING ");
238 $render_expr($ctx, having);
239 }
240
241 let has_order_items = !$select.order_by_items.is_empty();
242 let has_col_order = !$select.order_by.is_empty();
243 let has_expr_order = !$select.order_by_exprs.is_empty();
244 if has_order_items || has_col_order || has_expr_order {
245 $ctx.sql.push_str(" ORDER BY ");
246 let mut first = true;
247 if has_order_items {
248 for item in &$select.order_by_items {
249 if !first {
250 $ctx.sql.push_str(", ");
251 }
252 first = false;
253 match item {
254 nautilus_core::OrderByItem::Column(order) => {
255 crate::push_identifier_reference(&mut $ctx.sql, &order.column, $quote);
256 match order.direction {
257 nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
258 nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
259 }
260 }
261 nautilus_core::OrderByItem::Expr(expr, dir) => {
262 $render_expr($ctx, expr);
263 match dir {
264 nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
265 nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
266 }
267 }
268 }
269 }
270 } else {
271 for order in &$select.order_by {
272 if !first {
273 $ctx.sql.push_str(", ");
274 }
275 first = false;
276 crate::push_identifier_reference(&mut $ctx.sql, &order.column, $quote);
277 match order.direction {
278 nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
279 nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
280 }
281 }
282 for (expr, dir) in &$select.order_by_exprs {
283 if !first {
284 $ctx.sql.push_str(", ");
285 }
286 first = false;
287 $render_expr($ctx, expr);
288 match dir {
289 nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
290 nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
291 }
292 }
293 }
294 }
295
296 if let Some(take) = $select.take {
298 $ctx.sql.push_str(" LIMIT ");
299 $ctx.sql.push_str(&take.unsigned_abs().to_string());
300 } else if $mysql_limit_hack && $select.skip.is_some() {
301 $ctx.sql.push_str(" LIMIT 18446744073709551615");
302 }
303
304 if let Some(skip) = $select.skip {
305 $ctx.sql.push_str(" OFFSET ");
306 $ctx.sql.push_str(&skip.to_string());
307 }
308 }};
309}
310
311macro_rules! render_expr_common {
330 (
331 $ctx:expr, $expr:expr,
332 $quote:ident, $render_expr:ident, $render_select_body:ident,
333 { $($specific:tt)* }
334 ) => {
335 match $expr {
336 nautilus_core::Expr::Column(name) => {
339 crate::push_identifier_reference(&mut $ctx.sql, name, $quote);
340 }
341 nautilus_core::Expr::Not(inner) => {
342 $ctx.sql.push_str("NOT (");
343 $render_expr($ctx, inner);
344 $ctx.sql.push(')');
345 }
346 nautilus_core::Expr::Exists(subquery) => {
347 $ctx.sql.push_str("EXISTS (");
348 $render_select_body($ctx, subquery);
349 $ctx.sql.push(')');
350 }
351 nautilus_core::Expr::NotExists(subquery) => {
352 $ctx.sql.push_str("NOT EXISTS (");
353 $render_select_body($ctx, subquery);
354 $ctx.sql.push(')');
355 }
356 nautilus_core::Expr::Relation { op, relation } => {
357 let is_exists = matches!(op, nautilus_core::expr::RelationFilterOp::Some);
358 if is_exists {
359 $ctx.sql.push_str("EXISTS (SELECT * FROM ");
360 } else {
361 $ctx.sql.push_str("NOT EXISTS (SELECT * FROM ");
362 }
363 $ctx.sql.push_str(&$quote(&relation.target_table));
364 $ctx.sql.push_str(" WHERE ");
365 crate::push_identifier_reference(
366 &mut $ctx.sql,
367 &format!("{}__{}", relation.target_table, relation.fk_db),
368 $quote,
369 );
370 $ctx.sql.push_str(" = ");
371 crate::push_identifier_reference(
372 &mut $ctx.sql,
373 &format!("{}__{}", relation.parent_table, relation.pk_db),
374 $quote,
375 );
376 $ctx.sql.push_str(" AND ");
377 if matches!(op, nautilus_core::expr::RelationFilterOp::Every) {
378 $ctx.sql.push_str("NOT (");
379 $render_expr($ctx, &relation.filter);
380 $ctx.sql.push(')');
381 } else {
382 $render_expr($ctx, &relation.filter);
383 }
384 $ctx.sql.push(')');
385 }
386 nautilus_core::Expr::ScalarSubquery(subquery) => {
387 $ctx.sql.push('(');
388 $render_select_body($ctx, subquery);
389 $ctx.sql.push(')');
390 }
391 nautilus_core::Expr::IsNull(inner) => {
392 $ctx.sql.push('(');
393 $render_expr($ctx, inner);
394 $ctx.sql.push_str(" IS NULL)");
395 }
396 nautilus_core::Expr::IsNotNull(inner) => {
397 $ctx.sql.push('(');
398 $render_expr($ctx, inner);
399 $ctx.sql.push_str(" IS NOT NULL)");
400 }
401 nautilus_core::Expr::Literal(s) => {
405 $ctx.sql.push('\'');
406 $ctx.sql.push_str(&s.replace('\'', "''"));
407 $ctx.sql.push('\'');
408 }
409 nautilus_core::Expr::List(exprs) => {
410 for (i, e) in exprs.iter().enumerate() {
411 if i > 0 { $ctx.sql.push_str(", "); }
412 $render_expr($ctx, e);
413 }
414 }
415 nautilus_core::Expr::CaseWhen { condition, then } => {
416 $ctx.sql.push_str("CASE WHEN ");
417 $render_expr($ctx, condition);
418 $ctx.sql.push_str(" THEN ");
419 $render_expr($ctx, then);
420 $ctx.sql.push_str(" ELSE NULL END");
421 }
422 nautilus_core::Expr::Star => {
423 $ctx.sql.push('*');
424 }
425 $($specific)*
426 }
427 };
428}
429
430mod mysql;
431mod postgres;
432mod sqlite;
433
434pub use mysql::MysqlDialect;
435pub use postgres::PostgresDialect;
436pub use sqlite::SqliteDialect;
437
438use nautilus_core::{Delete, Insert, Result, Select, Update, Value};
439
440#[derive(Debug, Clone, PartialEq)]
444#[must_use]
445pub struct Sql {
446 pub text: String,
448 pub params: Vec<Value>,
450}
451
452pub trait Dialect {
456 fn supports_returning(&self) -> bool {
462 true
463 }
464
465 fn render_select(&self, select: &Select) -> Result<Sql>;
467
468 fn render_insert(&self, insert: &Insert) -> Result<Sql>;
470
471 fn render_update(&self, update: &Update) -> Result<Sql>;
473
474 fn render_delete(&self, delete: &Delete) -> Result<Sql>;
476}
477
478pub(crate) fn double_quote_identifier(name: &str) -> String {
482 format!("\"{}\"", name.replace('"', "\"\""))
483}
484
485pub(crate) fn backtick_quote_identifier(name: &str) -> String {
489 format!("`{}`", name.replace('`', "``"))
490}
491
492pub(crate) fn push_identifier_reference<F>(sql: &mut String, name: &str, quote: F)
497where
498 F: Fn(&str) -> String,
499{
500 if let Some((table, column)) = name.split_once("__") {
501 sql.push_str("e(table));
502 sql.push('.');
503 sql.push_str("e(column));
504 } else {
505 sql.push_str("e(name));
506 }
507}
508
509#[inline]
515pub(crate) fn binary_op_sql(op: &nautilus_core::BinaryOp) -> &'static str {
516 match op {
517 nautilus_core::BinaryOp::Eq => "=",
518 nautilus_core::BinaryOp::Ne => "!=",
519 nautilus_core::BinaryOp::Lt => "<",
520 nautilus_core::BinaryOp::Le => "<=",
521 nautilus_core::BinaryOp::Gt => ">",
522 nautilus_core::BinaryOp::Ge => ">=",
523 nautilus_core::BinaryOp::And => "AND",
524 nautilus_core::BinaryOp::Or => "OR",
525 nautilus_core::BinaryOp::Like => "LIKE",
526 nautilus_core::BinaryOp::ArrayContains
527 | nautilus_core::BinaryOp::ArrayContainedBy
528 | nautilus_core::BinaryOp::ArrayOverlaps
529 | nautilus_core::BinaryOp::In
530 | nautilus_core::BinaryOp::NotIn => {
531 unreachable!(
532 "binary_op_sql: operator {:?} must be handled by dialect-specific code",
533 op
534 )
535 }
536 }
537}