use crate::{
db::{QueryError, query::plan::expr::Expr},
value::Value,
};
pub trait ValueProjectionExpr {
fn field(&self) -> &str;
fn sql_label(&self) -> String;
fn apply_value(&self, value: Value) -> Result<Value, QueryError>;
}
#[must_use]
pub(in crate::db) fn render_scalar_projection_expr_sql_label(expr: &Expr) -> String {
render_scalar_projection_expr_sql_label_with_parent(expr, None, false)
}
fn render_scalar_projection_expr_sql_label_with_parent(
expr: &Expr,
parent_op: Option<crate::db::query::plan::expr::BinaryOp>,
is_right_child: bool,
) -> String {
match expr {
Expr::Field(field) => field.as_str().to_string(),
Expr::Literal(value) => render_scalar_projection_literal(value),
Expr::FunctionCall { function, args } => {
let rendered_args = args
.iter()
.map(|arg| render_scalar_projection_expr_sql_label_with_parent(arg, None, false))
.collect::<Vec<_>>()
.join(", ");
format!("{}({rendered_args})", function.sql_label())
}
Expr::Case {
when_then_arms,
else_expr,
} => render_case_projection_expr_sql_label(when_then_arms, else_expr.as_ref()),
Expr::Binary { op, left, right } => {
let left = render_scalar_projection_expr_sql_label_with_parent(
left.as_ref(),
Some(*op),
false,
);
let right = render_scalar_projection_expr_sql_label_with_parent(
right.as_ref(),
Some(*op),
true,
);
let rendered = format!("{left} {} {right}", binary_op_sql_label(*op));
if binary_expr_requires_parentheses(*op, parent_op, is_right_child) {
format!("({rendered})")
} else {
rendered
}
}
Expr::Aggregate(aggregate) => {
let kind = aggregate.kind().sql_label();
let distinct = if aggregate.is_distinct() {
"DISTINCT "
} else {
""
};
if let Some(input_expr) = aggregate.input_expr() {
let input =
render_scalar_projection_expr_sql_label_with_parent(input_expr, None, false);
return format!("{kind}({distinct}{input})");
}
format!("{kind}({distinct}*)")
}
#[cfg(test)]
Expr::Alias { expr, .. } => render_scalar_projection_expr_sql_label_with_parent(
expr.as_ref(),
parent_op,
is_right_child,
),
Expr::Unary { op, expr } => {
let rendered =
render_scalar_projection_expr_sql_label_with_parent(expr.as_ref(), None, false);
match op {
crate::db::query::plan::expr::UnaryOp::Not => format!("NOT {rendered}"),
}
}
}
}
fn render_case_projection_expr_sql_label(
when_then_arms: &[crate::db::query::plan::expr::CaseWhenArm],
else_expr: &Expr,
) -> String {
let mut rendered = String::from("CASE");
for arm in when_then_arms {
rendered.push_str(" WHEN ");
rendered.push_str(
render_scalar_projection_expr_sql_label_with_parent(arm.condition(), None, false)
.as_str(),
);
rendered.push_str(" THEN ");
rendered.push_str(
render_scalar_projection_expr_sql_label_with_parent(arm.result(), None, false).as_str(),
);
}
rendered.push_str(" ELSE ");
rendered.push_str(
render_scalar_projection_expr_sql_label_with_parent(else_expr, None, false).as_str(),
);
rendered.push_str(" END");
rendered
}
const fn binary_expr_requires_parentheses(
op: crate::db::query::plan::expr::BinaryOp,
parent_op: Option<crate::db::query::plan::expr::BinaryOp>,
is_right_child: bool,
) -> bool {
let Some(parent_op) = parent_op else {
return false;
};
let precedence = binary_op_precedence(op);
let parent_precedence = binary_op_precedence(parent_op);
precedence < parent_precedence || (is_right_child && precedence == parent_precedence)
}
const fn binary_op_precedence(op: crate::db::query::plan::expr::BinaryOp) -> u8 {
match op {
crate::db::query::plan::expr::BinaryOp::Or => 0,
crate::db::query::plan::expr::BinaryOp::And => 1,
crate::db::query::plan::expr::BinaryOp::Eq
| crate::db::query::plan::expr::BinaryOp::Ne
| crate::db::query::plan::expr::BinaryOp::Lt
| crate::db::query::plan::expr::BinaryOp::Lte
| crate::db::query::plan::expr::BinaryOp::Gt
| crate::db::query::plan::expr::BinaryOp::Gte => 2,
crate::db::query::plan::expr::BinaryOp::Add
| crate::db::query::plan::expr::BinaryOp::Sub => 3,
crate::db::query::plan::expr::BinaryOp::Mul
| crate::db::query::plan::expr::BinaryOp::Div => 4,
}
}
const fn binary_op_sql_label(op: crate::db::query::plan::expr::BinaryOp) -> &'static str {
match op {
crate::db::query::plan::expr::BinaryOp::Or => "OR",
crate::db::query::plan::expr::BinaryOp::And => "AND",
crate::db::query::plan::expr::BinaryOp::Eq => "=",
crate::db::query::plan::expr::BinaryOp::Ne => "!=",
crate::db::query::plan::expr::BinaryOp::Lt => "<",
crate::db::query::plan::expr::BinaryOp::Lte => "<=",
crate::db::query::plan::expr::BinaryOp::Gt => ">",
crate::db::query::plan::expr::BinaryOp::Gte => ">=",
crate::db::query::plan::expr::BinaryOp::Add => "+",
crate::db::query::plan::expr::BinaryOp::Sub => "-",
crate::db::query::plan::expr::BinaryOp::Mul => "*",
crate::db::query::plan::expr::BinaryOp::Div => "/",
}
}
fn render_scalar_projection_literal(value: &Value) -> String {
match value {
Value::Null => "NULL".to_string(),
Value::Text(text) => format!("'{}'", text.replace('\'', "''")),
Value::Int(value) => value.to_string(),
Value::Int128(value) => value.to_string(),
Value::IntBig(value) => value.to_string(),
Value::Uint(value) => value.to_string(),
Value::Uint128(value) => value.to_string(),
Value::UintBig(value) => value.to_string(),
Value::Decimal(value) => value.to_string(),
Value::Float32(value) => value.to_string(),
Value::Float64(value) => value.to_string(),
Value::Bool(value) => value.to_string().to_uppercase(),
other => format!("{other:?}"),
}
}