Skip to main content

nautilus_dialect/
lib.rs

1//! SQL dialect renderers for Nautilus ORM.
2
3#![warn(missing_docs)]
4#![forbid(unsafe_code)]
5
6// These macros accept identifier parameters (`$quote`, `$render_expr`) so that
7// each dialect module supplies only the logic that differs between dialects.
8// Free identifiers in macro bodies (types, constants) are resolved at the
9// *definition site* (here in lib.rs), so the required types must be imported
10// below.  Identifier parameters (`$quote:expr`, `$render_expr:ident`) are
11// substituted textually at the call site, which is the intended behaviour.
12
13/// Append `RETURNING col1 AS alias1, ...` when `$returning` is non-empty.
14macro_rules! render_returning {
15    ($ctx:expr, $returning:expr, $quote:expr) => {{
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                crate::push_qualified_identifier(&mut $ctx.sql, &col.table, &col.name, $quote);
23                $ctx.sql.push_str(" AS ");
24                crate::push_column_alias(&mut $ctx.sql, col, $quote);
25            }
26        }
27    }};
28}
29
30/// Render the full body of an INSERT statement into `$ctx`.
31///
32/// `$supports_returning`: when `false` the RETURNING clause is omitted (MySQL).
33macro_rules! render_insert_body {
34    ($ctx:expr, $insert:expr, $quote:expr, $supports_returning:expr, $supports_enum_cast:expr) => {{
35        $ctx.sql.push_str("INSERT INTO ");
36        crate::push_quoted_identifier(&mut $ctx.sql, &$insert.table, $quote);
37
38        $ctx.sql.push_str(" (");
39        for (i, col) in $insert.columns.iter().enumerate() {
40            if i > 0 {
41                $ctx.sql.push_str(", ");
42            }
43            crate::push_quoted_identifier(&mut $ctx.sql, &col.name, $quote);
44        }
45        $ctx.sql.push(')');
46
47        $ctx.sql.push_str(" VALUES ");
48        for (row_idx, row) in $insert.values.iter().enumerate() {
49            if row_idx > 0 {
50                $ctx.sql.push_str(", ");
51            }
52            $ctx.sql.push('(');
53            for (val_idx, value) in row.iter().enumerate() {
54                if val_idx > 0 {
55                    $ctx.sql.push_str(", ");
56                }
57                if matches!(value, nautilus_core::Value::Null) {
58                    $ctx.sql.push_str("NULL");
59                } else {
60                    $ctx.push_param(value.clone());
61                    if $supports_enum_cast {
62                        if let nautilus_core::Value::Enum { type_name, .. } = value {
63                            $ctx.sql.push_str("::");
64                            $ctx.sql.push_str(type_name);
65                        }
66                    }
67                }
68            }
69            $ctx.sql.push(')');
70        }
71
72        if $supports_returning {
73            render_returning!($ctx, $insert.returning, $quote);
74        }
75    }};
76}
77
78/// Render the full body of an UPDATE statement into `$ctx`.
79///
80/// `$render_expr`: the dialect-local expression renderer.
81/// `$supports_returning`: when `false` the RETURNING clause is omitted (MySQL).
82macro_rules! render_update_body {
83    ($ctx:expr, $update:expr, $quote:expr, $render_expr:ident, $supports_returning:expr, $supports_enum_cast:expr) => {{
84        $ctx.sql.push_str("UPDATE ");
85        crate::push_quoted_identifier(&mut $ctx.sql, &$update.table, $quote);
86
87        $ctx.sql.push_str(" SET ");
88        for (i, (col, value)) in $update.assignments.iter().enumerate() {
89            if i > 0 {
90                $ctx.sql.push_str(", ");
91            }
92            crate::push_quoted_identifier(&mut $ctx.sql, &col.name, $quote);
93            $ctx.sql.push_str(" = ");
94            if matches!(value, nautilus_core::Value::Null) {
95                $ctx.sql.push_str("NULL");
96            } else {
97                $ctx.push_param(value.clone());
98                if $supports_enum_cast {
99                    if let nautilus_core::Value::Enum { type_name, .. } = value {
100                        $ctx.sql.push_str("::");
101                        $ctx.sql.push_str(type_name);
102                    }
103                }
104            }
105        }
106
107        if let Some(ref filter) = $update.filter {
108            $ctx.sql.push_str(" WHERE ");
109            $render_expr($ctx, filter);
110        }
111
112        if $supports_returning {
113            render_returning!($ctx, $update.returning, $quote);
114        }
115    }};
116}
117
118/// Render the full body of a DELETE statement into `$ctx`.
119///
120/// `$render_expr`: the dialect-local expression renderer.
121/// `$supports_returning`: when `false` the RETURNING clause is omitted (MySQL).
122macro_rules! render_delete_body {
123    ($ctx:expr, $delete:expr, $quote:expr, $render_expr:ident, $supports_returning:expr) => {{
124        $ctx.sql.push_str("DELETE FROM ");
125        crate::push_quoted_identifier(&mut $ctx.sql, &$delete.table, $quote);
126
127        if let Some(ref filter) = $delete.filter {
128            $ctx.sql.push_str(" WHERE ");
129            $render_expr($ctx, filter);
130        }
131
132        if $supports_returning {
133            render_returning!($ctx, $delete.returning, $quote);
134        }
135    }};
136}
137
138/// Render the full body of a SELECT statement into `$ctx`.
139///
140/// - `$distinct_on`: `true` for PostgreSQL-style `DISTINCT ON (cols)`;
141///   `false` emits plain `SELECT DISTINCT`.
142/// - `$mysql_limit_hack`: `true` inserts a synthetic `LIMIT 18446744073709551615`
143///   when only OFFSET is present (required by MySQL).
144/// - `$render_expr`: the dialect-local expression renderer.
145macro_rules! render_select_body_core {
146    (
147        $ctx:expr, $select:expr,
148        $quote:expr, $render_expr:ident,
149        $distinct_on:expr, $mysql_limit_hack:expr
150    ) => {{
151        $ctx.sql.push_str("SELECT ");
152
153        // DISTINCT handling: Postgres supports DISTINCT ON (cols);
154        // other dialects support only full-row SELECT DISTINCT.
155        if !$select.distinct.is_empty() {
156            if $distinct_on {
157                $ctx.sql.push_str("DISTINCT ON (");
158                for (i, col) in $select.distinct.iter().enumerate() {
159                    if i > 0 {
160                        $ctx.sql.push_str(", ");
161                    }
162                    crate::push_identifier_reference(&mut $ctx.sql, col, $quote);
163                }
164                $ctx.sql.push_str(") ");
165            } else {
166                $ctx.sql.push_str("DISTINCT ");
167            }
168        }
169
170        let has_items =
171            !$select.items.is_empty() || $select.joins.iter().any(|join| !join.items.is_empty());
172
173        if !has_items {
174            $ctx.sql.push('*');
175        } else {
176            let mut first = true;
177            for item in &$select.items {
178                if !first {
179                    $ctx.sql.push_str(", ");
180                }
181                first = false;
182                match item {
183                    nautilus_core::SelectItem::Column(col) => {
184                        crate::push_qualified_identifier(
185                            &mut $ctx.sql,
186                            &col.table,
187                            &col.name,
188                            $quote,
189                        );
190                        $ctx.sql.push_str(" AS ");
191                        crate::push_column_alias(&mut $ctx.sql, col, $quote);
192                    }
193                    nautilus_core::SelectItem::Computed { expr, alias } => {
194                        $ctx.sql.push('(');
195                        $render_expr($ctx, expr);
196                        $ctx.sql.push(')');
197                        $ctx.sql.push_str(" AS ");
198                        crate::push_quoted_identifier(&mut $ctx.sql, alias, $quote);
199                    }
200                }
201            }
202            for join in &$select.joins {
203                for item in &join.items {
204                    if !first {
205                        $ctx.sql.push_str(", ");
206                    }
207                    first = false;
208                    match item {
209                        nautilus_core::SelectItem::Column(col) => {
210                            crate::push_qualified_identifier(
211                                &mut $ctx.sql,
212                                &col.table,
213                                &col.name,
214                                $quote,
215                            );
216                            $ctx.sql.push_str(" AS ");
217                            crate::push_column_alias(&mut $ctx.sql, col, $quote);
218                        }
219                        nautilus_core::SelectItem::Computed { expr, alias } => {
220                            $ctx.sql.push('(');
221                            $render_expr($ctx, expr);
222                            $ctx.sql.push(')');
223                            $ctx.sql.push_str(" AS ");
224                            crate::push_quoted_identifier(&mut $ctx.sql, alias, $quote);
225                        }
226                    }
227                }
228            }
229        }
230
231        $ctx.sql.push_str(" FROM ");
232        crate::push_quoted_identifier(&mut $ctx.sql, &$select.table, $quote);
233
234        for join in &$select.joins {
235            match join.join_type {
236                nautilus_core::JoinType::Inner => $ctx.sql.push_str(" INNER JOIN "),
237                nautilus_core::JoinType::Left => $ctx.sql.push_str(" LEFT JOIN "),
238            }
239            crate::push_quoted_identifier(&mut $ctx.sql, &join.table, $quote);
240            $ctx.sql.push_str(" ON ");
241            $render_expr($ctx, &join.on);
242        }
243
244        if let Some(ref filter) = $select.filter {
245            $ctx.sql.push_str(" WHERE ");
246            $render_expr($ctx, filter);
247        }
248
249        if !$select.group_by.is_empty() {
250            $ctx.sql.push_str(" GROUP BY ");
251            for (i, col) in $select.group_by.iter().enumerate() {
252                if i > 0 {
253                    $ctx.sql.push_str(", ");
254                }
255                crate::push_qualified_identifier(&mut $ctx.sql, &col.table, &col.name, $quote);
256            }
257        }
258
259        if let Some(ref having) = $select.having {
260            $ctx.sql.push_str(" HAVING ");
261            $render_expr($ctx, having);
262        }
263
264        let has_order_items = !$select.order_by_items.is_empty();
265        let has_col_order = !$select.order_by.is_empty();
266        let has_expr_order = !$select.order_by_exprs.is_empty();
267        if has_order_items || has_col_order || has_expr_order {
268            $ctx.sql.push_str(" ORDER BY ");
269            let mut first = true;
270            if has_order_items {
271                for item in &$select.order_by_items {
272                    if !first {
273                        $ctx.sql.push_str(", ");
274                    }
275                    first = false;
276                    match item {
277                        nautilus_core::OrderByItem::Column(order) => {
278                            crate::push_identifier_reference(&mut $ctx.sql, &order.column, $quote);
279                            match order.direction {
280                                nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
281                                nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
282                            }
283                        }
284                        nautilus_core::OrderByItem::Expr(expr, dir) => {
285                            $render_expr($ctx, expr);
286                            match dir {
287                                nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
288                                nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
289                            }
290                        }
291                    }
292                }
293            } else {
294                for order in &$select.order_by {
295                    if !first {
296                        $ctx.sql.push_str(", ");
297                    }
298                    first = false;
299                    crate::push_identifier_reference(&mut $ctx.sql, &order.column, $quote);
300                    match order.direction {
301                        nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
302                        nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
303                    }
304                }
305                for (expr, dir) in &$select.order_by_exprs {
306                    if !first {
307                        $ctx.sql.push_str(", ");
308                    }
309                    first = false;
310                    $render_expr($ctx, expr);
311                    match dir {
312                        nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
313                        nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
314                    }
315                }
316            }
317        }
318
319        // MySQL requires LIMIT whenever OFFSET is present; emit a synthetic max value.
320        if let Some(take) = $select.take {
321            $ctx.sql.push_str(" LIMIT ");
322            crate::push_u32(&mut $ctx.sql, take.unsigned_abs());
323        } else if $mysql_limit_hack && $select.skip.is_some() {
324            $ctx.sql.push_str(" LIMIT 18446744073709551615");
325        }
326
327        if let Some(skip) = $select.skip {
328            $ctx.sql.push_str(" OFFSET ");
329            crate::push_u32(&mut $ctx.sql, skip);
330        }
331    }};
332}
333
334/// Render the `Expr` variants that are **identical** across all SQL dialect renderers.
335///
336/// Eight variants (`Column`, `Not`, `Exists`, `NotExists`, `ScalarSubquery`,
337/// `IsNull`, `IsNotNull`, `Literal`) have the same rendering logic in every
338/// dialect — the only structural difference is which function is called to
339/// quote identifiers and which function recurses for sub-expressions.
340///
341/// The four dialect-specific variants (`Param`, `Binary`, `FunctionCall`,
342/// `Filter`) are provided by the caller as a block of match arms in
343/// `{ $($specific:tt)* }` and are appended after the shared arms.
344///
345/// Parameters:
346/// - `$ctx`: `&mut RenderContext` — mutable render context
347/// - `$expr`: `&Expr` — the expression to render
348/// - `$quote`: local identifier-quoting function
349/// - `$render_expr`: dialect-local recursive expression renderer
350/// - `$render_select_body`: dialect-local subquery renderer
351/// - `{ $($specific:tt)* }`: match arms for dialect-specific variants
352macro_rules! render_expr_common {
353    (
354        $ctx:expr, $expr:expr,
355        $quote:expr, $render_expr:ident, $render_select_body:ident,
356        { $($specific:tt)* }
357    ) => {
358        match $expr {
359            // Split "table__column" into a qualified identifier pair; otherwise
360            // render as a single unqualified identifier.
361            nautilus_core::Expr::Column(name) => {
362                crate::push_identifier_reference(&mut $ctx.sql, name, $quote);
363            }
364            nautilus_core::Expr::Not(inner) => {
365                $ctx.sql.push_str("NOT (");
366                $render_expr($ctx, inner);
367                $ctx.sql.push(')');
368            }
369            nautilus_core::Expr::Exists(subquery) => {
370                $ctx.sql.push_str("EXISTS (");
371                $render_select_body($ctx, subquery);
372                $ctx.sql.push(')');
373            }
374            nautilus_core::Expr::NotExists(subquery) => {
375                $ctx.sql.push_str("NOT EXISTS (");
376                $render_select_body($ctx, subquery);
377                $ctx.sql.push(')');
378            }
379            nautilus_core::Expr::Relation { op, relation } => {
380                let is_exists = matches!(op, nautilus_core::expr::RelationFilterOp::Some);
381                if is_exists {
382                    $ctx.sql.push_str("EXISTS (SELECT * FROM ");
383                } else {
384                    $ctx.sql.push_str("NOT EXISTS (SELECT * FROM ");
385                }
386                crate::push_quoted_identifier(&mut $ctx.sql, &relation.target_table, $quote);
387                $ctx.sql.push_str(" WHERE ");
388                crate::push_qualified_identifier(
389                    &mut $ctx.sql,
390                    &relation.target_table,
391                    &relation.fk_db,
392                    $quote,
393                );
394                $ctx.sql.push_str(" = ");
395                crate::push_qualified_identifier(
396                    &mut $ctx.sql,
397                    &relation.parent_table,
398                    &relation.pk_db,
399                    $quote,
400                );
401                $ctx.sql.push_str(" AND ");
402                if matches!(op, nautilus_core::expr::RelationFilterOp::Every) {
403                    $ctx.sql.push_str("NOT (");
404                    $render_expr($ctx, &relation.filter);
405                    $ctx.sql.push(')');
406                } else {
407                    $render_expr($ctx, &relation.filter);
408                }
409                $ctx.sql.push(')');
410            }
411            nautilus_core::Expr::ScalarSubquery(subquery) => {
412                $ctx.sql.push('(');
413                $render_select_body($ctx, subquery);
414                $ctx.sql.push(')');
415            }
416            nautilus_core::Expr::IsNull(inner) => {
417                $ctx.sql.push('(');
418                $render_expr($ctx, inner);
419                $ctx.sql.push_str(" IS NULL)");
420            }
421            nautilus_core::Expr::IsNotNull(inner) => {
422                $ctx.sql.push('(');
423                $render_expr($ctx, inner);
424                $ctx.sql.push_str(" IS NOT NULL)");
425            }
426            // Emit as a single-quoted SQL string literal with internal
427            // single-quotes escaped by doubling.
428            // Must only be called with trusted, static strings.
429            nautilus_core::Expr::Literal(s) => {
430                crate::push_sql_string_literal(&mut $ctx.sql, s);
431            }
432            nautilus_core::Expr::List(exprs) => {
433                for (i, e) in exprs.iter().enumerate() {
434                    if i > 0 { $ctx.sql.push_str(", "); }
435                    $render_expr($ctx, e);
436                }
437            }
438            nautilus_core::Expr::CaseWhen { condition, then } => {
439                $ctx.sql.push_str("CASE WHEN ");
440                $render_expr($ctx, condition);
441                $ctx.sql.push_str(" THEN ");
442                $render_expr($ctx, then);
443                $ctx.sql.push_str(" ELSE NULL END");
444            }
445            nautilus_core::Expr::Star => {
446                $ctx.sql.push('*');
447            }
448            $($specific)*
449        }
450    };
451}
452
453/// Mutable/owned variant of [`render_returning!`] used by `render_*_owned`.
454macro_rules! render_returning_mut {
455    ($ctx:expr, $returning:expr, $quote:expr) => {{
456        if !$returning.is_empty() {
457            $ctx.sql.push_str(" RETURNING ");
458            for (i, col) in $returning.iter().enumerate() {
459                if i > 0 {
460                    $ctx.sql.push_str(", ");
461                }
462                crate::push_qualified_identifier(&mut $ctx.sql, &col.table, &col.name, $quote);
463                $ctx.sql.push_str(" AS ");
464                crate::push_column_alias(&mut $ctx.sql, col, $quote);
465            }
466        }
467    }};
468}
469
470/// Mutable/owned variant of [`render_insert_body!`] used by `render_*_owned`.
471macro_rules! render_insert_body_mut {
472    ($ctx:expr, $insert:expr, $quote:expr, $supports_returning:expr, $supports_enum_cast:expr) => {{
473        $ctx.sql.push_str("INSERT INTO ");
474        crate::push_quoted_identifier(&mut $ctx.sql, &$insert.table, $quote);
475
476        $ctx.sql.push_str(" (");
477        for (i, col) in $insert.columns.iter().enumerate() {
478            if i > 0 {
479                $ctx.sql.push_str(", ");
480            }
481            crate::push_quoted_identifier(&mut $ctx.sql, &col.name, $quote);
482        }
483        $ctx.sql.push(')');
484
485        $ctx.sql.push_str(" VALUES ");
486        for (row_idx, row) in $insert.values.iter_mut().enumerate() {
487            if row_idx > 0 {
488                $ctx.sql.push_str(", ");
489            }
490            $ctx.sql.push('(');
491            for (val_idx, value) in row.iter_mut().enumerate() {
492                if val_idx > 0 {
493                    $ctx.sql.push_str(", ");
494                }
495                if matches!(value, nautilus_core::Value::Null) {
496                    $ctx.sql.push_str("NULL");
497                } else {
498                    let enum_type_name = if $supports_enum_cast {
499                        if let nautilus_core::Value::Enum { type_name, .. } = value {
500                            Some(type_name.clone())
501                        } else {
502                            None
503                        }
504                    } else {
505                        None
506                    };
507                    $ctx.take_param(value);
508                    if let Some(type_name) = enum_type_name.as_deref() {
509                        $ctx.sql.push_str("::");
510                        $ctx.sql.push_str(type_name);
511                    }
512                }
513            }
514            $ctx.sql.push(')');
515        }
516
517        if $supports_returning {
518            render_returning_mut!($ctx, $insert.returning, $quote);
519        }
520    }};
521}
522
523/// Mutable/owned variant of [`render_update_body!`] used by `render_*_owned`.
524macro_rules! render_update_body_mut {
525    ($ctx:expr, $update:expr, $quote:expr, $render_expr:ident, $supports_returning:expr, $supports_enum_cast:expr) => {{
526        $ctx.sql.push_str("UPDATE ");
527        crate::push_quoted_identifier(&mut $ctx.sql, &$update.table, $quote);
528
529        $ctx.sql.push_str(" SET ");
530        for (i, (col, value)) in $update.assignments.iter_mut().enumerate() {
531            if i > 0 {
532                $ctx.sql.push_str(", ");
533            }
534            crate::push_quoted_identifier(&mut $ctx.sql, &col.name, $quote);
535            $ctx.sql.push_str(" = ");
536            if matches!(value, nautilus_core::Value::Null) {
537                $ctx.sql.push_str("NULL");
538            } else {
539                let enum_type_name = if $supports_enum_cast {
540                    if let nautilus_core::Value::Enum { type_name, .. } = value {
541                        Some(type_name.clone())
542                    } else {
543                        None
544                    }
545                } else {
546                    None
547                };
548                $ctx.take_param(value);
549                if let Some(type_name) = enum_type_name.as_deref() {
550                    $ctx.sql.push_str("::");
551                    $ctx.sql.push_str(type_name);
552                }
553            }
554        }
555
556        if let Some(filter) = $update.filter.as_mut() {
557            $ctx.sql.push_str(" WHERE ");
558            $render_expr($ctx, filter);
559        }
560
561        if $supports_returning {
562            render_returning_mut!($ctx, $update.returning, $quote);
563        }
564    }};
565}
566
567/// Mutable/owned variant of [`render_delete_body!`] used by `render_*_owned`.
568macro_rules! render_delete_body_mut {
569    ($ctx:expr, $delete:expr, $quote:expr, $render_expr:ident, $supports_returning:expr) => {{
570        $ctx.sql.push_str("DELETE FROM ");
571        crate::push_quoted_identifier(&mut $ctx.sql, &$delete.table, $quote);
572
573        if let Some(filter) = $delete.filter.as_mut() {
574            $ctx.sql.push_str(" WHERE ");
575            $render_expr($ctx, filter);
576        }
577
578        if $supports_returning {
579            render_returning_mut!($ctx, $delete.returning, $quote);
580        }
581    }};
582}
583
584/// Mutable/owned variant of [`render_select_body_core!`] used by `render_*_owned`.
585macro_rules! render_select_body_core_mut {
586    (
587        $ctx:expr, $select:expr,
588        $quote:expr, $render_expr:ident,
589        $distinct_on:expr, $mysql_limit_hack:expr
590    ) => {{
591        $ctx.sql.push_str("SELECT ");
592
593        if !$select.distinct.is_empty() {
594            if $distinct_on {
595                $ctx.sql.push_str("DISTINCT ON (");
596                for (i, col) in $select.distinct.iter().enumerate() {
597                    if i > 0 {
598                        $ctx.sql.push_str(", ");
599                    }
600                    crate::push_identifier_reference(&mut $ctx.sql, col, $quote);
601                }
602                $ctx.sql.push_str(") ");
603            } else {
604                $ctx.sql.push_str("DISTINCT ");
605            }
606        }
607
608        let has_items =
609            !$select.items.is_empty() || $select.joins.iter().any(|join| !join.items.is_empty());
610
611        if !has_items {
612            $ctx.sql.push('*');
613        } else {
614            let mut first = true;
615            for item in $select.items.iter_mut() {
616                if !first {
617                    $ctx.sql.push_str(", ");
618                }
619                first = false;
620                match item {
621                    nautilus_core::SelectItem::Column(col) => {
622                        crate::push_qualified_identifier(
623                            &mut $ctx.sql,
624                            &col.table,
625                            &col.name,
626                            $quote,
627                        );
628                        $ctx.sql.push_str(" AS ");
629                        crate::push_column_alias(&mut $ctx.sql, col, $quote);
630                    }
631                    nautilus_core::SelectItem::Computed { expr, alias } => {
632                        $ctx.sql.push('(');
633                        $render_expr($ctx, expr);
634                        $ctx.sql.push(')');
635                        $ctx.sql.push_str(" AS ");
636                        crate::push_quoted_identifier(&mut $ctx.sql, alias, $quote);
637                    }
638                }
639            }
640            for join in $select.joins.iter_mut() {
641                for item in join.items.iter_mut() {
642                    if !first {
643                        $ctx.sql.push_str(", ");
644                    }
645                    first = false;
646                    match item {
647                        nautilus_core::SelectItem::Column(col) => {
648                            crate::push_qualified_identifier(
649                                &mut $ctx.sql,
650                                &col.table,
651                                &col.name,
652                                $quote,
653                            );
654                            $ctx.sql.push_str(" AS ");
655                            crate::push_column_alias(&mut $ctx.sql, col, $quote);
656                        }
657                        nautilus_core::SelectItem::Computed { expr, alias } => {
658                            $ctx.sql.push('(');
659                            $render_expr($ctx, expr);
660                            $ctx.sql.push(')');
661                            $ctx.sql.push_str(" AS ");
662                            crate::push_quoted_identifier(&mut $ctx.sql, alias, $quote);
663                        }
664                    }
665                }
666            }
667        }
668
669        $ctx.sql.push_str(" FROM ");
670        crate::push_quoted_identifier(&mut $ctx.sql, &$select.table, $quote);
671
672        for join in $select.joins.iter_mut() {
673            match join.join_type {
674                nautilus_core::JoinType::Inner => $ctx.sql.push_str(" INNER JOIN "),
675                nautilus_core::JoinType::Left => $ctx.sql.push_str(" LEFT JOIN "),
676            }
677            crate::push_quoted_identifier(&mut $ctx.sql, &join.table, $quote);
678            $ctx.sql.push_str(" ON ");
679            $render_expr($ctx, &mut join.on);
680        }
681
682        if let Some(filter) = $select.filter.as_mut() {
683            $ctx.sql.push_str(" WHERE ");
684            $render_expr($ctx, filter);
685        }
686
687        if !$select.group_by.is_empty() {
688            $ctx.sql.push_str(" GROUP BY ");
689            for (i, col) in $select.group_by.iter().enumerate() {
690                if i > 0 {
691                    $ctx.sql.push_str(", ");
692                }
693                crate::push_qualified_identifier(&mut $ctx.sql, &col.table, &col.name, $quote);
694            }
695        }
696
697        if let Some(having) = $select.having.as_mut() {
698            $ctx.sql.push_str(" HAVING ");
699            $render_expr($ctx, having);
700        }
701
702        let has_order_items = !$select.order_by_items.is_empty();
703        let has_col_order = !$select.order_by.is_empty();
704        let has_expr_order = !$select.order_by_exprs.is_empty();
705        if has_order_items || has_col_order || has_expr_order {
706            $ctx.sql.push_str(" ORDER BY ");
707            let mut first = true;
708            if has_order_items {
709                for item in $select.order_by_items.iter_mut() {
710                    if !first {
711                        $ctx.sql.push_str(", ");
712                    }
713                    first = false;
714                    match item {
715                        nautilus_core::OrderByItem::Column(order) => {
716                            crate::push_identifier_reference(&mut $ctx.sql, &order.column, $quote);
717                            match order.direction {
718                                nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
719                                nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
720                            }
721                        }
722                        nautilus_core::OrderByItem::Expr(expr, dir) => {
723                            $render_expr($ctx, expr);
724                            match *dir {
725                                nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
726                                nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
727                            }
728                        }
729                    }
730                }
731            } else {
732                for order in $select.order_by.iter() {
733                    if !first {
734                        $ctx.sql.push_str(", ");
735                    }
736                    first = false;
737                    crate::push_identifier_reference(&mut $ctx.sql, &order.column, $quote);
738                    match order.direction {
739                        nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
740                        nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
741                    }
742                }
743                for (expr, dir) in $select.order_by_exprs.iter_mut() {
744                    if !first {
745                        $ctx.sql.push_str(", ");
746                    }
747                    first = false;
748                    $render_expr($ctx, expr);
749                    match *dir {
750                        nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
751                        nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
752                    }
753                }
754            }
755        }
756
757        if let Some(take) = $select.take {
758            $ctx.sql.push_str(" LIMIT ");
759            crate::push_u32(&mut $ctx.sql, take.unsigned_abs());
760        } else if $mysql_limit_hack && $select.skip.is_some() {
761            $ctx.sql.push_str(" LIMIT 18446744073709551615");
762        }
763
764        if let Some(skip) = $select.skip {
765            $ctx.sql.push_str(" OFFSET ");
766            crate::push_u32(&mut $ctx.sql, skip);
767        }
768    }};
769}
770
771/// Mutable/owned variant of [`render_expr_common!`] used by `render_*_owned`.
772macro_rules! render_expr_common_mut {
773    (
774        $ctx:expr, $expr:expr,
775        $quote:expr, $render_expr:ident, $render_select_body:ident,
776        { $($specific:tt)* }
777    ) => {
778        match $expr {
779            nautilus_core::Expr::Column(name) => {
780                crate::push_identifier_reference(&mut $ctx.sql, name, $quote);
781            }
782            nautilus_core::Expr::Not(inner) => {
783                $ctx.sql.push_str("NOT (");
784                $render_expr($ctx, inner.as_mut());
785                $ctx.sql.push(')');
786            }
787            nautilus_core::Expr::Exists(subquery) => {
788                $ctx.sql.push_str("EXISTS (");
789                $render_select_body($ctx, subquery.as_mut());
790                $ctx.sql.push(')');
791            }
792            nautilus_core::Expr::NotExists(subquery) => {
793                $ctx.sql.push_str("NOT EXISTS (");
794                $render_select_body($ctx, subquery.as_mut());
795                $ctx.sql.push(')');
796            }
797            nautilus_core::Expr::Relation { op, relation } => {
798                let is_exists = matches!(*op, nautilus_core::expr::RelationFilterOp::Some);
799                if is_exists {
800                    $ctx.sql.push_str("EXISTS (SELECT * FROM ");
801                } else {
802                    $ctx.sql.push_str("NOT EXISTS (SELECT * FROM ");
803                }
804                crate::push_quoted_identifier(&mut $ctx.sql, &relation.target_table, $quote);
805                $ctx.sql.push_str(" WHERE ");
806                crate::push_qualified_identifier(
807                    &mut $ctx.sql,
808                    &relation.target_table,
809                    &relation.fk_db,
810                    $quote,
811                );
812                $ctx.sql.push_str(" = ");
813                crate::push_qualified_identifier(
814                    &mut $ctx.sql,
815                    &relation.parent_table,
816                    &relation.pk_db,
817                    $quote,
818                );
819                $ctx.sql.push_str(" AND ");
820                if matches!(*op, nautilus_core::expr::RelationFilterOp::Every) {
821                    $ctx.sql.push_str("NOT (");
822                    $render_expr($ctx, relation.filter.as_mut());
823                    $ctx.sql.push(')');
824                } else {
825                    $render_expr($ctx, relation.filter.as_mut());
826                }
827                $ctx.sql.push(')');
828            }
829            nautilus_core::Expr::ScalarSubquery(subquery) => {
830                $ctx.sql.push('(');
831                $render_select_body($ctx, subquery.as_mut());
832                $ctx.sql.push(')');
833            }
834            nautilus_core::Expr::IsNull(inner) => {
835                $ctx.sql.push('(');
836                $render_expr($ctx, inner.as_mut());
837                $ctx.sql.push_str(" IS NULL)");
838            }
839            nautilus_core::Expr::IsNotNull(inner) => {
840                $ctx.sql.push('(');
841                $render_expr($ctx, inner.as_mut());
842                $ctx.sql.push_str(" IS NOT NULL)");
843            }
844            nautilus_core::Expr::Literal(s) => {
845                crate::push_sql_string_literal(&mut $ctx.sql, s);
846            }
847            nautilus_core::Expr::List(exprs) => {
848                for (i, e) in exprs.iter_mut().enumerate() {
849                    if i > 0 {
850                        $ctx.sql.push_str(", ");
851                    }
852                    $render_expr($ctx, e);
853                }
854            }
855            nautilus_core::Expr::CaseWhen { condition, then } => {
856                $ctx.sql.push_str("CASE WHEN ");
857                $render_expr($ctx, condition.as_mut());
858                $ctx.sql.push_str(" THEN ");
859                $render_expr($ctx, then.as_mut());
860                $ctx.sql.push_str(" ELSE NULL END");
861            }
862            nautilus_core::Expr::Star => {
863                $ctx.sql.push('*');
864            }
865            $($specific)*
866        }
867    };
868}
869
870mod mysql;
871mod postgres;
872mod render_estimate;
873mod sqlite;
874
875pub use mysql::MysqlDialect;
876pub use postgres::PostgresDialect;
877pub use sqlite::SqliteDialect;
878
879use nautilus_core::{Delete, Insert, Result, Select, Update, Value};
880pub(crate) use render_estimate::{
881    estimate_delete_render, estimate_insert_render, estimate_select_render, estimate_update_render,
882    RenderEstimate,
883};
884
885/// SQL query with bound parameters.
886///
887/// Separates the SQL text from parameter values for use with prepared statements.
888#[derive(Debug, Clone, PartialEq)]
889#[must_use]
890pub struct Sql {
891    /// The SQL query text with parameter placeholders.
892    pub text: String,
893    /// The parameter values to bind to the query.
894    pub params: Vec<Value>,
895}
896
897/// Trait for SQL dialect renderers.
898///
899/// Allows rendering AST queries into dialect-specific SQL strings.
900pub trait Dialect {
901    /// Whether this dialect natively supports the RETURNING clause
902    /// on INSERT, UPDATE, and DELETE statements.
903    ///
904    /// Dialects that return `false` (e.g. MySQL) will have RETURNING
905    /// emulated at the connector layer via separate queries.
906    fn supports_returning(&self) -> bool {
907        true
908    }
909
910    /// Render a SELECT query into SQL.
911    fn render_select(&self, select: &Select) -> Result<Sql>;
912
913    /// Render an owned SELECT query into SQL, allowing dialects to move bound
914    /// values out of the AST instead of cloning them.
915    fn render_select_owned(&self, select: Select) -> Result<Sql> {
916        self.render_select(&select)
917    }
918
919    /// Render an INSERT query into SQL.
920    fn render_insert(&self, insert: &Insert) -> Result<Sql>;
921
922    /// Render an owned INSERT query into SQL, allowing dialects to move bound
923    /// values out of the AST instead of cloning them.
924    fn render_insert_owned(&self, insert: Insert) -> Result<Sql> {
925        self.render_insert(&insert)
926    }
927
928    /// Render an UPDATE query into SQL.
929    fn render_update(&self, update: &Update) -> Result<Sql>;
930
931    /// Render an owned UPDATE query into SQL, allowing dialects to move bound
932    /// values out of the AST instead of cloning them.
933    fn render_update_owned(&self, update: Update) -> Result<Sql> {
934        self.render_update(&update)
935    }
936
937    /// Render a DELETE query into SQL.
938    fn render_delete(&self, delete: &Delete) -> Result<Sql>;
939
940    /// Render an owned DELETE query into SQL, allowing dialects to move bound
941    /// values out of the AST instead of cloning them.
942    fn render_delete_owned(&self, delete: Delete) -> Result<Sql> {
943        self.render_delete(&delete)
944    }
945}
946
947fn push_escaped_identifier(sql: &mut String, name: &str, quote: char) {
948    for ch in name.chars() {
949        if ch == quote {
950            sql.push(quote);
951        }
952        sql.push(ch);
953    }
954}
955
956/// Quote a SQL identifier directly into the SQL buffer.
957pub(crate) fn push_quoted_identifier(sql: &mut String, name: &str, quote: char) {
958    sql.push(quote);
959    push_escaped_identifier(sql, name, quote);
960    sql.push(quote);
961}
962
963/// Quote multiple identifier segments as a single identifier directly into the SQL buffer.
964pub(crate) fn push_quoted_identifier_segments(sql: &mut String, segments: &[&str], quote: char) {
965    sql.push(quote);
966    for segment in segments {
967        push_escaped_identifier(sql, segment, quote);
968    }
969    sql.push(quote);
970}
971
972/// Render `table.column` directly into the SQL buffer.
973pub(crate) fn push_qualified_identifier(sql: &mut String, table: &str, column: &str, quote: char) {
974    push_quoted_identifier(sql, table, quote);
975    sql.push('.');
976    push_quoted_identifier(sql, column, quote);
977}
978
979/// Render a join-safe `table__column` alias directly into the SQL buffer.
980pub(crate) fn push_column_alias(
981    sql: &mut String,
982    column: &nautilus_core::ColumnMarker,
983    quote: char,
984) {
985    push_quoted_identifier_segments(
986        sql,
987        &[column.table.as_ref(), "__", column.name.as_ref()],
988        quote,
989    );
990}
991
992/// Render an identifier reference that may use the `table__column` shorthand.
993///
994/// The split happens only on the first `__`, so mapped column names like
995/// `users__profile__slug` still render as `users.profile__slug`.
996pub(crate) fn push_identifier_reference(sql: &mut String, name: &str, quote: char) {
997    if let Some((table, column)) = name.split_once("__") {
998        push_qualified_identifier(sql, table, column, quote);
999    } else {
1000        push_quoted_identifier(sql, name, quote);
1001    }
1002}
1003
1004/// Render a single-quoted SQL string literal directly into the SQL buffer.
1005pub(crate) fn push_sql_string_literal(sql: &mut String, value: &str) {
1006    sql.push('\'');
1007    for ch in value.chars() {
1008        if ch == '\'' {
1009            sql.push('\'');
1010        }
1011        sql.push(ch);
1012    }
1013    sql.push('\'');
1014}
1015
1016fn push_u64(sql: &mut String, mut value: u64) {
1017    let mut digits = [0_u8; 20];
1018    let mut idx = digits.len();
1019
1020    loop {
1021        idx -= 1;
1022        digits[idx] = b'0' + (value % 10) as u8;
1023        value /= 10;
1024        if value == 0 {
1025            break;
1026        }
1027    }
1028
1029    for digit in &digits[idx..] {
1030        sql.push(char::from(*digit));
1031    }
1032}
1033
1034/// Append a `u32` value directly into the SQL buffer.
1035pub(crate) fn push_u32(sql: &mut String, value: u32) {
1036    push_u64(sql, u64::from(value));
1037}
1038
1039/// Append a `usize` value directly into the SQL buffer.
1040pub(crate) fn push_usize(sql: &mut String, value: usize) {
1041    push_u64(sql, value as u64);
1042}
1043
1044/// Return the SQL operator keyword for a standard scalar binary operation.
1045///
1046/// Call only for the nine scalar operators (Eq through Like).  Composite cases
1047/// (IN/NOT IN, array operators) must be handled separately by each dialect before
1048/// delegating to this helper.
1049#[inline]
1050pub(crate) fn binary_op_sql(op: &nautilus_core::BinaryOp) -> &'static str {
1051    match op {
1052        nautilus_core::BinaryOp::Eq => "=",
1053        nautilus_core::BinaryOp::Ne => "!=",
1054        nautilus_core::BinaryOp::Lt => "<",
1055        nautilus_core::BinaryOp::Le => "<=",
1056        nautilus_core::BinaryOp::Gt => ">",
1057        nautilus_core::BinaryOp::Ge => ">=",
1058        nautilus_core::BinaryOp::And => "AND",
1059        nautilus_core::BinaryOp::Or => "OR",
1060        nautilus_core::BinaryOp::Like => "LIKE",
1061        nautilus_core::BinaryOp::ArrayContains
1062        | nautilus_core::BinaryOp::ArrayContainedBy
1063        | nautilus_core::BinaryOp::ArrayOverlaps
1064        | nautilus_core::BinaryOp::In
1065        | nautilus_core::BinaryOp::NotIn => {
1066            unreachable!(
1067                "binary_op_sql: operator {:?} must be handled by dialect-specific code",
1068                op
1069            )
1070        }
1071    }
1072}