1#![warn(missing_docs)]
4#![forbid(unsafe_code)]
5
6macro_rules! render_returning_mut {
20 ($ctx:expr, $returning:expr, $quote:expr) => {{
21 if !$returning.is_empty() {
22 $ctx.sql.push_str(" RETURNING ");
23 for (i, col) in $returning.iter().enumerate() {
24 if i > 0 {
25 $ctx.sql.push_str(", ");
26 }
27 crate::push_qualified_identifier(&mut $ctx.sql, &col.table, &col.name, $quote);
28 $ctx.sql.push_str(" AS ");
29 crate::push_column_alias(&mut $ctx.sql, col, $quote);
30 }
31 }
32 }};
33}
34
35macro_rules! render_insert_body_mut {
37 ($ctx:expr, $insert:expr, $quote:expr, $supports_returning:expr, $supports_enum_cast:expr) => {{
38 $ctx.sql.push_str("INSERT INTO ");
39 crate::push_quoted_identifier(&mut $ctx.sql, &$insert.table, $quote);
40
41 $ctx.sql.push_str(" (");
42 for (i, col) in $insert.columns.iter().enumerate() {
43 if i > 0 {
44 $ctx.sql.push_str(", ");
45 }
46 crate::push_quoted_identifier(&mut $ctx.sql, &col.name, $quote);
47 }
48 $ctx.sql.push(')');
49
50 $ctx.sql.push_str(" VALUES ");
51 for (row_idx, row) in $insert.values.iter_mut().enumerate() {
52 if row_idx > 0 {
53 $ctx.sql.push_str(", ");
54 }
55 $ctx.sql.push('(');
56 for (val_idx, value) in row.iter_mut().enumerate() {
57 if val_idx > 0 {
58 $ctx.sql.push_str(", ");
59 }
60 if matches!(value, nautilus_core::Value::Null) {
61 $ctx.sql.push_str("NULL");
62 } else {
63 let cast_type_name = if $supports_enum_cast {
64 if let nautilus_core::Value::Enum { type_name, .. }
65 | nautilus_core::Value::Composite { type_name, .. } = value
66 {
67 Some(type_name.clone())
68 } else {
69 None
70 }
71 } else {
72 None
73 };
74 $ctx.take_param(value);
75 if let Some(type_name) = cast_type_name.as_deref() {
76 $ctx.sql.push_str("::");
77 crate::push_quoted_identifier(&mut $ctx.sql, type_name, $quote);
78 }
79 }
80 }
81 $ctx.sql.push(')');
82 }
83
84 if $supports_returning {
85 render_returning_mut!($ctx, $insert.returning, $quote);
86 }
87 }};
88}
89
90macro_rules! render_update_body_mut {
92 ($ctx:expr, $update:expr, $quote:expr, $render_expr:ident, $supports_returning:expr, $supports_enum_cast:expr) => {{
93 $ctx.sql.push_str("UPDATE ");
94 crate::push_quoted_identifier(&mut $ctx.sql, &$update.table, $quote);
95
96 $ctx.sql.push_str(" SET ");
97 for (i, (col, value)) in $update.assignments.iter_mut().enumerate() {
98 if i > 0 {
99 $ctx.sql.push_str(", ");
100 }
101 crate::push_quoted_identifier(&mut $ctx.sql, &col.name, $quote);
102 $ctx.sql.push_str(" = ");
103 if matches!(value, nautilus_core::Value::Null) {
104 $ctx.sql.push_str("NULL");
105 } else {
106 let cast_type_name = if $supports_enum_cast {
107 if let nautilus_core::Value::Enum { type_name, .. }
108 | nautilus_core::Value::Composite { type_name, .. } = value
109 {
110 Some(type_name.clone())
111 } else {
112 None
113 }
114 } else {
115 None
116 };
117 $ctx.take_param(value);
118 if let Some(type_name) = cast_type_name.as_deref() {
119 $ctx.sql.push_str("::");
120 crate::push_quoted_identifier(&mut $ctx.sql, type_name, $quote);
121 }
122 }
123 }
124
125 if let Some(filter) = $update.filter.as_mut() {
126 $ctx.sql.push_str(" WHERE ");
127 $render_expr($ctx, filter);
128 }
129
130 if $supports_returning {
131 render_returning_mut!($ctx, $update.returning, $quote);
132 }
133 }};
134}
135
136macro_rules! render_delete_body_mut {
138 ($ctx:expr, $delete:expr, $quote:expr, $render_expr:ident, $supports_returning:expr) => {{
139 $ctx.sql.push_str("DELETE FROM ");
140 crate::push_quoted_identifier(&mut $ctx.sql, &$delete.table, $quote);
141
142 if let Some(filter) = $delete.filter.as_mut() {
143 $ctx.sql.push_str(" WHERE ");
144 $render_expr($ctx, filter);
145 }
146
147 if $supports_returning {
148 render_returning_mut!($ctx, $delete.returning, $quote);
149 }
150 }};
151}
152
153macro_rules! render_select_body_core_mut {
155 (
156 $ctx:expr, $select:expr,
157 $quote:expr, $render_expr:ident,
158 $distinct_on:expr, $mysql_limit_hack:expr
159 ) => {{
160 $ctx.sql.push_str("SELECT ");
161
162 if !$select.distinct.is_empty() {
163 if $distinct_on {
164 $ctx.sql.push_str("DISTINCT ON (");
165 for (i, col) in $select.distinct.iter().enumerate() {
166 if i > 0 {
167 $ctx.sql.push_str(", ");
168 }
169 crate::push_identifier_reference(&mut $ctx.sql, col, $quote);
170 }
171 $ctx.sql.push_str(") ");
172 } else {
173 $ctx.sql.push_str("DISTINCT ");
174 }
175 }
176
177 let has_items =
178 !$select.items.is_empty() || $select.joins.iter().any(|join| !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_mut() {
185 if !first {
186 $ctx.sql.push_str(", ");
187 }
188 first = false;
189 match item {
190 nautilus_core::SelectItem::Column(col) => {
191 crate::push_qualified_identifier(
192 &mut $ctx.sql,
193 &col.table,
194 &col.name,
195 $quote,
196 );
197 $ctx.sql.push_str(" AS ");
198 crate::push_column_alias(&mut $ctx.sql, col, $quote);
199 }
200 nautilus_core::SelectItem::Computed { expr, alias } => {
201 $ctx.sql.push('(');
202 $render_expr($ctx, expr);
203 $ctx.sql.push(')');
204 $ctx.sql.push_str(" AS ");
205 crate::push_quoted_identifier(&mut $ctx.sql, alias, $quote);
206 }
207 }
208 }
209 for join in $select.joins.iter_mut() {
210 for item in join.items.iter_mut() {
211 if !first {
212 $ctx.sql.push_str(", ");
213 }
214 first = false;
215 match item {
216 nautilus_core::SelectItem::Column(col) => {
217 crate::push_qualified_identifier(
218 &mut $ctx.sql,
219 &col.table,
220 &col.name,
221 $quote,
222 );
223 $ctx.sql.push_str(" AS ");
224 crate::push_column_alias(&mut $ctx.sql, col, $quote);
225 }
226 nautilus_core::SelectItem::Computed { expr, alias } => {
227 $ctx.sql.push('(');
228 $render_expr($ctx, expr);
229 $ctx.sql.push(')');
230 $ctx.sql.push_str(" AS ");
231 crate::push_quoted_identifier(&mut $ctx.sql, alias, $quote);
232 }
233 }
234 }
235 }
236 }
237
238 $ctx.sql.push_str(" FROM ");
239 crate::push_quoted_identifier(&mut $ctx.sql, &$select.table, $quote);
240
241 for join in $select.joins.iter_mut() {
242 match join.join_type {
243 nautilus_core::JoinType::Inner => $ctx.sql.push_str(" INNER JOIN "),
244 nautilus_core::JoinType::Left => $ctx.sql.push_str(" LEFT JOIN "),
245 }
246 crate::push_quoted_identifier(&mut $ctx.sql, &join.table, $quote);
247 $ctx.sql.push_str(" ON ");
248 $render_expr($ctx, &mut join.on);
249 }
250
251 if let Some(filter) = $select.filter.as_mut() {
252 $ctx.sql.push_str(" WHERE ");
253 $render_expr($ctx, filter);
254 }
255
256 if !$select.group_by.is_empty() {
257 $ctx.sql.push_str(" GROUP BY ");
258 for (i, col) in $select.group_by.iter().enumerate() {
259 if i > 0 {
260 $ctx.sql.push_str(", ");
261 }
262 crate::push_qualified_identifier(&mut $ctx.sql, &col.table, &col.name, $quote);
263 }
264 }
265
266 if let Some(having) = $select.having.as_mut() {
267 $ctx.sql.push_str(" HAVING ");
268 $render_expr($ctx, having);
269 }
270
271 let has_order_items = !$select.order_by_items.is_empty();
272 let has_col_order = !$select.order_by.is_empty();
273 let has_expr_order = !$select.order_by_exprs.is_empty();
274 if has_order_items || has_col_order || has_expr_order {
275 $ctx.sql.push_str(" ORDER BY ");
276 let mut first = true;
277 if has_order_items {
278 for item in $select.order_by_items.iter_mut() {
279 if !first {
280 $ctx.sql.push_str(", ");
281 }
282 first = false;
283 match item {
284 nautilus_core::OrderByItem::Column(order) => {
285 crate::push_identifier_reference(&mut $ctx.sql, &order.column, $quote);
286 match order.direction {
287 nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
288 nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
289 }
290 }
291 nautilus_core::OrderByItem::Expr(expr, dir) => {
292 $render_expr($ctx, expr);
293 match *dir {
294 nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
295 nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
296 }
297 }
298 }
299 }
300 } else {
301 for order in $select.order_by.iter() {
302 if !first {
303 $ctx.sql.push_str(", ");
304 }
305 first = false;
306 crate::push_identifier_reference(&mut $ctx.sql, &order.column, $quote);
307 match order.direction {
308 nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
309 nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
310 }
311 }
312 for (expr, dir) in $select.order_by_exprs.iter_mut() {
313 if !first {
314 $ctx.sql.push_str(", ");
315 }
316 first = false;
317 $render_expr($ctx, expr);
318 match *dir {
319 nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
320 nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
321 }
322 }
323 }
324 }
325
326 if let Some(take) = $select.take {
327 $ctx.sql.push_str(" LIMIT ");
328 crate::push_u32(&mut $ctx.sql, take.unsigned_abs());
329 } else if $mysql_limit_hack && $select.skip.is_some() {
330 $ctx.sql.push_str(" LIMIT 18446744073709551615");
331 }
332
333 if let Some(skip) = $select.skip {
334 $ctx.sql.push_str(" OFFSET ");
335 crate::push_u32(&mut $ctx.sql, skip);
336 }
337 }};
338}
339
340macro_rules! render_expr_common_mut {
342 (
343 $ctx:expr, $expr:expr,
344 $quote:expr, $render_expr:ident, $render_select_body:ident,
345 { $($specific:tt)* }
346 ) => {
347 match $expr {
348 nautilus_core::Expr::Column(name) => {
349 crate::push_identifier_reference(&mut $ctx.sql, name, $quote);
350 }
351 nautilus_core::Expr::Not(inner) => {
352 $ctx.sql.push_str("NOT (");
353 $render_expr($ctx, inner.as_mut());
354 $ctx.sql.push(')');
355 }
356 nautilus_core::Expr::Exists(subquery) => {
357 $ctx.sql.push_str("EXISTS (");
358 $render_select_body($ctx, subquery.as_mut());
359 $ctx.sql.push(')');
360 }
361 nautilus_core::Expr::NotExists(subquery) => {
362 $ctx.sql.push_str("NOT EXISTS (");
363 $render_select_body($ctx, subquery.as_mut());
364 $ctx.sql.push(')');
365 }
366 nautilus_core::Expr::Relation { op, relation } => {
367 let is_exists = matches!(*op, nautilus_core::expr::RelationFilterOp::Some);
368 if is_exists {
369 $ctx.sql.push_str("EXISTS (SELECT * FROM ");
370 } else {
371 $ctx.sql.push_str("NOT EXISTS (SELECT * FROM ");
372 }
373 crate::push_quoted_identifier(&mut $ctx.sql, &relation.target_table, $quote);
374 $ctx.sql.push_str(" WHERE ");
375 crate::push_qualified_identifier(
376 &mut $ctx.sql,
377 &relation.target_table,
378 &relation.fk_db,
379 $quote,
380 );
381 $ctx.sql.push_str(" = ");
382 crate::push_qualified_identifier(
383 &mut $ctx.sql,
384 &relation.parent_table,
385 &relation.pk_db,
386 $quote,
387 );
388 $ctx.sql.push_str(" AND ");
389 if matches!(*op, nautilus_core::expr::RelationFilterOp::Every) {
390 $ctx.sql.push_str("NOT (");
391 $render_expr($ctx, relation.filter.as_mut());
392 $ctx.sql.push(')');
393 } else {
394 $render_expr($ctx, relation.filter.as_mut());
395 }
396 $ctx.sql.push(')');
397 }
398 nautilus_core::Expr::ScalarSubquery(subquery) => {
399 $ctx.sql.push('(');
400 $render_select_body($ctx, subquery.as_mut());
401 $ctx.sql.push(')');
402 }
403 nautilus_core::Expr::IsNull(inner) => {
404 $ctx.sql.push('(');
405 $render_expr($ctx, inner.as_mut());
406 $ctx.sql.push_str(" IS NULL)");
407 }
408 nautilus_core::Expr::IsNotNull(inner) => {
409 $ctx.sql.push('(');
410 $render_expr($ctx, inner.as_mut());
411 $ctx.sql.push_str(" IS NOT NULL)");
412 }
413 nautilus_core::Expr::Literal(s) => {
414 crate::push_sql_string_literal(&mut $ctx.sql, s.as_str());
415 }
416 nautilus_core::Expr::List(exprs) => {
417 for (i, e) in exprs.iter_mut().enumerate() {
418 if i > 0 {
419 $ctx.sql.push_str(", ");
420 }
421 $render_expr($ctx, e);
422 }
423 }
424 nautilus_core::Expr::CaseWhen { condition, then } => {
425 $ctx.sql.push_str("CASE WHEN ");
426 $render_expr($ctx, condition.as_mut());
427 $ctx.sql.push_str(" THEN ");
428 $render_expr($ctx, then.as_mut());
429 $ctx.sql.push_str(" ELSE NULL END");
430 }
431 nautilus_core::Expr::Star => {
432 $ctx.sql.push('*');
433 }
434 $($specific)*
435 }
436 };
437}
438
439mod mysql;
440mod postgres;
441mod render_estimate;
442mod sqlite;
443
444pub use mysql::MysqlDialect;
445pub use postgres::PostgresDialect;
446pub use sqlite::SqliteDialect;
447
448use nautilus_core::{Delete, Insert, Result, Select, Update, Value};
449pub(crate) use render_estimate::{
450 estimate_delete_render, estimate_insert_render, estimate_select_render, estimate_update_render,
451 RenderEstimate,
452};
453
454#[derive(Debug, Clone, PartialEq)]
458#[must_use]
459pub struct Sql {
460 pub text: String,
462 pub params: Vec<Value>,
464}
465
466pub trait Dialect {
470 fn supports_returning(&self) -> bool {
476 true
477 }
478
479 fn render_select_owned(&self, select: Select) -> Result<Sql>;
483
484 fn render_select(&self, select: &Select) -> Result<Sql> {
489 self.render_select_owned(select.clone())
490 }
491
492 fn render_insert_owned(&self, insert: Insert) -> Result<Sql>;
495
496 fn render_insert(&self, insert: &Insert) -> Result<Sql> {
499 self.render_insert_owned(insert.clone())
500 }
501
502 fn render_update_owned(&self, update: Update) -> Result<Sql>;
505
506 fn render_update(&self, update: &Update) -> Result<Sql> {
509 self.render_update_owned(update.clone())
510 }
511
512 fn render_delete_owned(&self, delete: Delete) -> Result<Sql>;
515
516 fn render_delete(&self, delete: &Delete) -> Result<Sql> {
519 self.render_delete_owned(delete.clone())
520 }
521}
522
523fn push_escaped_identifier(sql: &mut String, name: &str, quote: char) {
524 for ch in name.chars() {
525 if ch == quote {
526 sql.push(quote);
527 }
528 sql.push(ch);
529 }
530}
531
532pub(crate) fn push_quoted_identifier(sql: &mut String, name: &str, quote: char) {
534 sql.push(quote);
535 push_escaped_identifier(sql, name, quote);
536 sql.push(quote);
537}
538
539pub(crate) fn push_quoted_identifier_segments(sql: &mut String, segments: &[&str], quote: char) {
541 sql.push(quote);
542 for segment in segments {
543 push_escaped_identifier(sql, segment, quote);
544 }
545 sql.push(quote);
546}
547
548pub(crate) fn push_qualified_identifier(sql: &mut String, table: &str, column: &str, quote: char) {
550 push_quoted_identifier(sql, table, quote);
551 sql.push('.');
552 push_quoted_identifier(sql, column, quote);
553}
554
555pub(crate) fn push_column_alias(
557 sql: &mut String,
558 column: &nautilus_core::ColumnMarker,
559 quote: char,
560) {
561 push_quoted_identifier_segments(
562 sql,
563 &[column.table.as_ref(), "__", column.name.as_ref()],
564 quote,
565 );
566}
567
568pub(crate) fn push_identifier_reference(sql: &mut String, name: &str, quote: char) {
573 if let Some((table, column)) = name.split_once("__") {
574 push_qualified_identifier(sql, table, column, quote);
575 } else {
576 push_quoted_identifier(sql, name, quote);
577 }
578}
579
580pub(crate) fn push_composite_field_reference(
582 sql: &mut String,
583 table: &str,
584 column: &str,
585 field: &str,
586 quote: char,
587) {
588 sql.push('(');
589 push_qualified_identifier(sql, table, column, quote);
590 sql.push(')');
591 sql.push('.');
592 push_quoted_identifier(sql, field, quote);
593}
594
595fn push_json_path_key(sql: &mut String, key: &str) {
596 sql.push_str("$.\"");
597 for ch in key.chars() {
598 match ch {
599 '"' | '\\' => {
600 sql.push('\\');
601 sql.push(ch);
602 }
603 other => sql.push(other),
604 }
605 }
606 sql.push('"');
607}
608
609pub(crate) fn push_json_object_path_literal(sql: &mut String, key: &str) {
611 let mut path = String::with_capacity(key.len() + 4);
612 push_json_path_key(&mut path, key);
613 push_sql_string_literal(sql, &path);
614}
615
616pub(crate) fn push_sql_string_literal(sql: &mut String, value: &str) {
618 sql.push('\'');
619 for ch in value.chars() {
620 if ch == '\'' {
621 sql.push('\'');
622 }
623 sql.push(ch);
624 }
625 sql.push('\'');
626}
627
628fn push_u64(sql: &mut String, mut value: u64) {
629 let mut digits = [0_u8; 20];
630 let mut idx = digits.len();
631
632 loop {
633 idx -= 1;
634 digits[idx] = b'0' + (value % 10) as u8;
635 value /= 10;
636 if value == 0 {
637 break;
638 }
639 }
640
641 for digit in &digits[idx..] {
642 sql.push(char::from(*digit));
643 }
644}
645
646pub(crate) fn push_u32(sql: &mut String, value: u32) {
648 push_u64(sql, u64::from(value));
649}
650
651pub(crate) fn push_usize(sql: &mut String, value: usize) {
653 push_u64(sql, value as u64);
654}
655
656#[inline]
662pub(crate) fn binary_op_sql(op: &nautilus_core::BinaryOp) -> &'static str {
663 match op {
664 nautilus_core::BinaryOp::Eq => "=",
665 nautilus_core::BinaryOp::Ne => "!=",
666 nautilus_core::BinaryOp::Lt => "<",
667 nautilus_core::BinaryOp::Le => "<=",
668 nautilus_core::BinaryOp::Gt => ">",
669 nautilus_core::BinaryOp::Ge => ">=",
670 nautilus_core::BinaryOp::And => "AND",
671 nautilus_core::BinaryOp::Or => "OR",
672 nautilus_core::BinaryOp::Like => "LIKE",
673 nautilus_core::BinaryOp::ArrayContains
674 | nautilus_core::BinaryOp::ArrayContainedBy
675 | nautilus_core::BinaryOp::ArrayOverlaps
676 | nautilus_core::BinaryOp::In
677 | nautilus_core::BinaryOp::NotIn => {
678 unreachable!(
679 "binary_op_sql: operator {:?} must be handled by dialect-specific code",
680 op
681 )
682 }
683 }
684}