#![warn(missing_docs)]
#![forbid(unsafe_code)]
macro_rules! render_returning {
($ctx:expr, $returning:expr, $quote:ident) => {{
if !$returning.is_empty() {
$ctx.sql.push_str(" RETURNING ");
for (i, col) in $returning.iter().enumerate() {
if i > 0 {
$ctx.sql.push_str(", ");
}
$ctx.sql.push_str(&$quote(&col.table));
$ctx.sql.push('.');
$ctx.sql.push_str(&$quote(&col.name));
$ctx.sql.push_str(" AS ");
$ctx.sql.push_str(&$quote(&col.alias()));
}
}
}};
}
macro_rules! render_insert_body {
($ctx:expr, $insert:expr, $quote:ident, $supports_returning:expr, $supports_enum_cast:expr) => {{
$ctx.sql.push_str("INSERT INTO ");
$ctx.sql.push_str(&$quote(&$insert.table));
$ctx.sql.push_str(" (");
for (i, col) in $insert.columns.iter().enumerate() {
if i > 0 {
$ctx.sql.push_str(", ");
}
$ctx.sql.push_str(&$quote(&col.name));
}
$ctx.sql.push(')');
$ctx.sql.push_str(" VALUES ");
for (row_idx, row) in $insert.values.iter().enumerate() {
if row_idx > 0 {
$ctx.sql.push_str(", ");
}
$ctx.sql.push('(');
for (val_idx, value) in row.iter().enumerate() {
if val_idx > 0 {
$ctx.sql.push_str(", ");
}
if matches!(value, nautilus_core::Value::Null) {
$ctx.sql.push_str("NULL");
} else {
let placeholder = $ctx.push_param(value.clone());
$ctx.sql.push_str(&placeholder);
if $supports_enum_cast {
if let nautilus_core::Value::Enum { type_name, .. } = value {
$ctx.sql.push_str("::");
$ctx.sql.push_str(type_name);
}
}
}
}
$ctx.sql.push(')');
}
if $supports_returning {
render_returning!($ctx, $insert.returning, $quote);
}
}};
}
macro_rules! render_update_body {
($ctx:expr, $update:expr, $quote:ident, $render_expr:ident, $supports_returning:expr, $supports_enum_cast:expr) => {{
$ctx.sql.push_str("UPDATE ");
$ctx.sql.push_str(&$quote(&$update.table));
$ctx.sql.push_str(" SET ");
for (i, (col, value)) in $update.assignments.iter().enumerate() {
if i > 0 {
$ctx.sql.push_str(", ");
}
$ctx.sql.push_str(&$quote(&col.name));
$ctx.sql.push_str(" = ");
if matches!(value, nautilus_core::Value::Null) {
$ctx.sql.push_str("NULL");
} else {
let placeholder = $ctx.push_param(value.clone());
$ctx.sql.push_str(&placeholder);
if $supports_enum_cast {
if let nautilus_core::Value::Enum { type_name, .. } = value {
$ctx.sql.push_str("::");
$ctx.sql.push_str(type_name);
}
}
}
}
if let Some(ref filter) = $update.filter {
$ctx.sql.push_str(" WHERE ");
$render_expr($ctx, filter);
}
if $supports_returning {
render_returning!($ctx, $update.returning, $quote);
}
}};
}
macro_rules! render_delete_body {
($ctx:expr, $delete:expr, $quote:ident, $render_expr:ident, $supports_returning:expr) => {{
$ctx.sql.push_str("DELETE FROM ");
$ctx.sql.push_str(&$quote(&$delete.table));
if let Some(ref filter) = $delete.filter {
$ctx.sql.push_str(" WHERE ");
$render_expr($ctx, filter);
}
if $supports_returning {
render_returning!($ctx, $delete.returning, $quote);
}
}};
}
macro_rules! render_select_body_core {
(
$ctx:expr, $select:expr,
$quote:ident, $render_expr:ident,
$distinct_on:expr, $mysql_limit_hack:expr
) => {{
$ctx.sql.push_str("SELECT ");
if !$select.distinct.is_empty() {
if $distinct_on {
$ctx.sql.push_str("DISTINCT ON (");
for (i, col) in $select.distinct.iter().enumerate() {
if i > 0 {
$ctx.sql.push_str(", ");
}
$ctx.sql.push_str(&$quote(col));
}
$ctx.sql.push_str(") ");
} else {
$ctx.sql.push_str("DISTINCT ");
}
}
let join_items: Vec<&nautilus_core::SelectItem> =
$select.joins.iter().flat_map(|j| j.items.iter()).collect();
let has_items = !$select.items.is_empty() || !join_items.is_empty();
if !has_items {
$ctx.sql.push('*');
} else {
let mut first = true;
for item in $select.items.iter().chain(join_items.iter().copied()) {
if !first {
$ctx.sql.push_str(", ");
}
first = false;
match item {
nautilus_core::SelectItem::Column(col) => {
$ctx.sql.push_str(&$quote(&col.table));
$ctx.sql.push('.');
$ctx.sql.push_str(&$quote(&col.name));
$ctx.sql.push_str(" AS ");
$ctx.sql.push_str(&$quote(&col.alias()));
}
nautilus_core::SelectItem::Computed { expr, alias } => {
$ctx.sql.push('(');
$render_expr($ctx, expr);
$ctx.sql.push(')');
$ctx.sql.push_str(" AS ");
$ctx.sql.push_str(&$quote(alias));
}
}
}
}
$ctx.sql.push_str(" FROM ");
$ctx.sql.push_str(&$quote(&$select.table));
for join in &$select.joins {
match join.join_type {
nautilus_core::JoinType::Inner => $ctx.sql.push_str(" INNER JOIN "),
nautilus_core::JoinType::Left => $ctx.sql.push_str(" LEFT JOIN "),
}
$ctx.sql.push_str(&$quote(&join.table));
$ctx.sql.push_str(" ON ");
$render_expr($ctx, &join.on);
}
if let Some(ref filter) = $select.filter {
$ctx.sql.push_str(" WHERE ");
$render_expr($ctx, filter);
}
if !$select.group_by.is_empty() {
$ctx.sql.push_str(" GROUP BY ");
for (i, col) in $select.group_by.iter().enumerate() {
if i > 0 {
$ctx.sql.push_str(", ");
}
$ctx.sql.push_str(&$quote(&col.table));
$ctx.sql.push('.');
$ctx.sql.push_str(&$quote(&col.name));
}
}
if let Some(ref having) = $select.having {
$ctx.sql.push_str(" HAVING ");
$render_expr($ctx, having);
}
let has_col_order = !$select.order_by.is_empty();
let has_expr_order = !$select.order_by_exprs.is_empty();
if has_col_order || has_expr_order {
$ctx.sql.push_str(" ORDER BY ");
let mut first = true;
for order in &$select.order_by {
if !first {
$ctx.sql.push_str(", ");
}
first = false;
$ctx.sql.push_str(&$quote(&order.column));
match order.direction {
nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
}
}
for (expr, dir) in &$select.order_by_exprs {
if !first {
$ctx.sql.push_str(", ");
}
first = false;
$render_expr($ctx, expr);
match dir {
nautilus_core::OrderDir::Asc => $ctx.sql.push_str(" ASC"),
nautilus_core::OrderDir::Desc => $ctx.sql.push_str(" DESC"),
}
}
}
if let Some(take) = $select.take {
$ctx.sql.push_str(" LIMIT ");
$ctx.sql.push_str(&take.unsigned_abs().to_string());
} else if $mysql_limit_hack && $select.skip.is_some() {
$ctx.sql.push_str(" LIMIT 18446744073709551615");
}
if let Some(skip) = $select.skip {
$ctx.sql.push_str(" OFFSET ");
$ctx.sql.push_str(&skip.to_string());
}
}};
}
macro_rules! render_expr_common {
(
$ctx:expr, $expr:expr,
$quote:ident, $render_expr:ident, $render_select_body:ident,
{ $($specific:tt)* }
) => {
match $expr {
nautilus_core::Expr::Column(name) => {
if let Some((table, column)) = name.split_once("__") {
$ctx.sql.push_str(&$quote(table));
$ctx.sql.push('.');
$ctx.sql.push_str(&$quote(column));
} else {
$ctx.sql.push_str(&$quote(name));
}
}
nautilus_core::Expr::Not(inner) => {
$ctx.sql.push_str("NOT (");
$render_expr($ctx, inner);
$ctx.sql.push(')');
}
nautilus_core::Expr::Exists(subquery) => {
$ctx.sql.push_str("EXISTS (");
$render_select_body($ctx, subquery);
$ctx.sql.push(')');
}
nautilus_core::Expr::NotExists(subquery) => {
$ctx.sql.push_str("NOT EXISTS (");
$render_select_body($ctx, subquery);
$ctx.sql.push(')');
}
nautilus_core::Expr::ScalarSubquery(subquery) => {
$ctx.sql.push('(');
$render_select_body($ctx, subquery);
$ctx.sql.push(')');
}
nautilus_core::Expr::IsNull(inner) => {
$ctx.sql.push('(');
$render_expr($ctx, inner);
$ctx.sql.push_str(" IS NULL)");
}
nautilus_core::Expr::IsNotNull(inner) => {
$ctx.sql.push('(');
$render_expr($ctx, inner);
$ctx.sql.push_str(" IS NOT NULL)");
}
nautilus_core::Expr::Literal(s) => {
$ctx.sql.push('\'');
$ctx.sql.push_str(&s.replace('\'', "''"));
$ctx.sql.push('\'');
}
nautilus_core::Expr::List(exprs) => {
for (i, e) in exprs.iter().enumerate() {
if i > 0 { $ctx.sql.push_str(", "); }
$render_expr($ctx, e);
}
}
nautilus_core::Expr::CaseWhen { condition, then } => {
$ctx.sql.push_str("CASE WHEN ");
$render_expr($ctx, condition);
$ctx.sql.push_str(" THEN ");
$render_expr($ctx, then);
$ctx.sql.push_str(" ELSE NULL END");
}
nautilus_core::Expr::Star => {
$ctx.sql.push('*');
}
$($specific)*
}
};
}
mod mysql;
mod postgres;
mod sqlite;
pub use mysql::MysqlDialect;
pub use postgres::PostgresDialect;
pub use sqlite::SqliteDialect;
use nautilus_core::{Delete, Insert, Result, Select, Update, Value};
#[derive(Debug, Clone, PartialEq)]
#[must_use]
pub struct Sql {
pub text: String,
pub params: Vec<Value>,
}
pub trait Dialect {
fn supports_returning(&self) -> bool {
true
}
fn render_select(&self, select: &Select) -> Result<Sql>;
fn render_insert(&self, insert: &Insert) -> Result<Sql>;
fn render_update(&self, update: &Update) -> Result<Sql>;
fn render_delete(&self, delete: &Delete) -> Result<Sql>;
}
pub(crate) fn double_quote_identifier(name: &str) -> String {
format!("\"{}\"", name.replace('"', "\"\""))
}
pub(crate) fn backtick_quote_identifier(name: &str) -> String {
format!("`{}`", name.replace('`', "``"))
}
#[inline]
pub(crate) fn binary_op_sql(op: &nautilus_core::BinaryOp) -> &'static str {
match op {
nautilus_core::BinaryOp::Eq => "=",
nautilus_core::BinaryOp::Ne => "!=",
nautilus_core::BinaryOp::Lt => "<",
nautilus_core::BinaryOp::Le => "<=",
nautilus_core::BinaryOp::Gt => ">",
nautilus_core::BinaryOp::Ge => ">=",
nautilus_core::BinaryOp::And => "AND",
nautilus_core::BinaryOp::Or => "OR",
nautilus_core::BinaryOp::Like => "LIKE",
nautilus_core::BinaryOp::ArrayContains
| nautilus_core::BinaryOp::ArrayContainedBy
| nautilus_core::BinaryOp::ArrayOverlaps
| nautilus_core::BinaryOp::In
| nautilus_core::BinaryOp::NotIn => {
unreachable!(
"binary_op_sql: operator {:?} must be handled by dialect-specific code",
op
)
}
}
}