use crate::{
db::{
query::plan::{AggregateKind, expr::Function},
sql::identifier::split_qualified_identifier,
},
value::Value,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum SqlStatement {
Select(SqlSelectStatement),
Delete(SqlDeleteStatement),
Insert(SqlInsertStatement),
Update(SqlUpdateStatement),
Explain(SqlExplainStatement),
Describe(SqlDescribeStatement),
ShowIndexes(SqlShowIndexesStatement),
ShowColumns(SqlShowColumnsStatement),
ShowEntities(SqlShowEntitiesStatement),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum SqlProjection {
All,
Items(Vec<SqlSelectItem>),
}
impl SqlProjection {
#[must_use]
pub(in crate::db) fn is_already_local_scalar(&self) -> bool {
match self {
Self::All => true,
Self::Items(items) => items.iter().all(SqlSelectItem::is_already_local_projection),
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum SqlSelectItem {
Field(String),
Aggregate(SqlAggregateCall),
Expr(SqlExpr),
}
impl SqlSelectItem {
#[must_use]
pub(crate) fn contains_aggregate(&self) -> bool {
match self {
Self::Field(_) => false,
Self::Aggregate(_) => true,
Self::Expr(expr) => expr.contains_aggregate(),
}
}
#[must_use]
pub(in crate::db) fn is_already_local_projection(&self) -> bool {
match self {
Self::Field(field) => SqlExpr::identifier_is_already_local(field.as_str()),
Self::Aggregate(aggregate) => aggregate.is_already_local_scalar(),
Self::Expr(_) => false,
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SqlExprUnaryOp {
Not,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SqlExprBinaryOp {
Or,
And,
Eq,
Ne,
Lt,
Lte,
Gt,
Gte,
Add,
Sub,
Mul,
Div,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlCaseArm {
pub(crate) condition: SqlExpr,
pub(crate) result: SqlExpr,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum SqlExpr {
Field(String),
FieldPath {
root: String,
segments: Vec<String>,
},
Aggregate(SqlAggregateCall),
Literal(Value),
Param {
index: usize,
},
Membership {
expr: Box<Self>,
values: Vec<Value>,
negated: bool,
},
NullTest {
expr: Box<Self>,
negated: bool,
},
Like {
expr: Box<Self>,
pattern: String,
negated: bool,
casefold: bool,
},
FunctionCall {
function: SqlScalarFunction,
args: Vec<Self>,
},
Unary {
op: SqlExprUnaryOp,
expr: Box<Self>,
},
Binary {
op: SqlExprBinaryOp,
left: Box<Self>,
right: Box<Self>,
},
Case {
arms: Vec<SqlCaseArm>,
else_expr: Option<Box<Self>>,
},
}
impl SqlExpr {
#[must_use]
pub(crate) fn from_select_item(item: &SqlSelectItem) -> Self {
match item {
SqlSelectItem::Field(field) => Self::from_field_identifier(field.clone()),
SqlSelectItem::Aggregate(aggregate) => Self::Aggregate(aggregate.clone()),
SqlSelectItem::Expr(expr) => expr.clone(),
}
}
#[must_use]
pub(crate) fn from_field_identifier(identifier: String) -> Self {
let mut parts = identifier.split('.');
let Some(root) = parts.next() else {
return Self::Field(identifier);
};
let segments = parts.map(str::to_string).collect::<Vec<_>>();
if segments.is_empty() {
return Self::Field(root.to_string());
}
Self::FieldPath {
root: root.to_string(),
segments,
}
}
#[must_use]
pub(crate) fn contains_aggregate(&self) -> bool {
self.any_tree_expr(&mut |expr| matches!(expr, Self::Aggregate(_)))
}
#[must_use]
pub(in crate::db) fn is_already_local_scalar(&self) -> bool {
self.all_tree_expr(&mut |expr| match expr {
Self::Field(field) => Self::identifier_is_already_local(field.as_str()),
Self::Literal(_)
| Self::Param { .. }
| Self::NullTest { .. }
| Self::Like { .. }
| Self::Unary { .. }
| Self::Binary { .. } => true,
Self::Membership { values, .. } => values
.iter()
.all(|value| !matches!(value, Value::List(_) | Value::Map(_))),
Self::FieldPath { .. }
| Self::Aggregate(_)
| Self::FunctionCall { .. }
| Self::Case { .. } => false,
})
}
#[must_use]
pub(in crate::db) fn fields_are_already_local(&self) -> bool {
self.all_tree_expr(&mut |expr| match expr {
Self::Field(field) => Self::identifier_is_already_local(field.as_str()),
Self::FieldPath { .. } => false,
Self::Aggregate(aggregate) => aggregate.is_already_local_scalar(),
Self::Literal(_)
| Self::Param { .. }
| Self::Membership { .. }
| Self::NullTest { .. }
| Self::Like { .. }
| Self::FunctionCall { .. }
| Self::Unary { .. }
| Self::Binary { .. }
| Self::Case { .. } => true,
})
}
#[must_use]
pub(in crate::db) fn contains_omitted_else_case(&self) -> bool {
self.any_tree_expr(
&mut |expr| matches!(expr, Self::Case { else_expr, .. } if else_expr.is_none()),
)
}
pub(in crate::db) fn for_each_tree_expr(&self, visit: &mut impl FnMut(&Self)) {
visit(self);
match self {
Self::Field(_)
| Self::FieldPath { .. }
| Self::Aggregate(_)
| Self::Literal(_)
| Self::Param { .. } => {}
Self::Membership { expr, .. }
| Self::NullTest { expr, .. }
| Self::Like { expr, .. }
| Self::Unary { expr, .. } => expr.for_each_tree_expr(visit),
Self::FunctionCall { args, .. } => {
for arg in args {
arg.for_each_tree_expr(visit);
}
}
Self::Binary { left, right, .. } => {
left.for_each_tree_expr(visit);
right.for_each_tree_expr(visit);
}
Self::Case { arms, else_expr } => {
for arm in arms {
arm.condition.for_each_tree_expr(visit);
arm.result.for_each_tree_expr(visit);
}
if let Some(else_expr) = else_expr.as_ref() {
else_expr.for_each_tree_expr(visit);
}
}
}
}
pub(in crate::db) fn for_each_tree_aggregate(&self, visit: &mut impl FnMut(&SqlAggregateCall)) {
self.for_each_tree_expr(&mut |expr| {
if let Self::Aggregate(aggregate) = expr {
visit(aggregate);
}
});
}
fn identifier_is_already_local(identifier: &str) -> bool {
split_qualified_identifier(identifier).is_none()
}
fn any_tree_expr(&self, predicate: &mut impl FnMut(&Self) -> bool) -> bool {
if predicate(self) {
return true;
}
match self {
Self::Field(_)
| Self::FieldPath { .. }
| Self::Aggregate(_)
| Self::Literal(_)
| Self::Param { .. } => false,
Self::Membership { expr, .. }
| Self::NullTest { expr, .. }
| Self::Like { expr, .. }
| Self::Unary { expr, .. } => expr.any_tree_expr(predicate),
Self::FunctionCall { args, .. } => args.iter().any(|arg| arg.any_tree_expr(predicate)),
Self::Binary { left, right, .. } => {
left.any_tree_expr(predicate) || right.any_tree_expr(predicate)
}
Self::Case { arms, else_expr } => {
arms.iter().any(|arm| {
arm.condition.any_tree_expr(predicate) || arm.result.any_tree_expr(predicate)
}) || else_expr
.as_ref()
.is_some_and(|else_expr| else_expr.any_tree_expr(predicate))
}
}
}
fn all_tree_expr(&self, predicate: &mut impl FnMut(&Self) -> bool) -> bool {
if !predicate(self) {
return false;
}
match self {
Self::Field(_)
| Self::FieldPath { .. }
| Self::Aggregate(_)
| Self::Literal(_)
| Self::Param { .. } => true,
Self::Membership { expr, .. }
| Self::NullTest { expr, .. }
| Self::Like { expr, .. }
| Self::Unary { expr, .. } => expr.all_tree_expr(predicate),
Self::FunctionCall { args, .. } => args.iter().all(|arg| arg.all_tree_expr(predicate)),
Self::Binary { left, right, .. } => {
left.all_tree_expr(predicate) && right.all_tree_expr(predicate)
}
Self::Case { arms, else_expr } => {
arms.iter().all(|arm| {
arm.condition.all_tree_expr(predicate) && arm.result.all_tree_expr(predicate)
}) && else_expr
.as_ref()
.is_none_or(|else_expr| else_expr.all_tree_expr(predicate))
}
}
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SqlAggregateKind {
Count,
Sum,
Avg,
Min,
Max,
}
impl SqlAggregateKind {
#[must_use]
pub(crate) const fn supports_star_input(self) -> bool {
matches!(self, Self::Count)
}
#[must_use]
pub(in crate::db) const fn lowers_shared_field_target_shape(self) -> bool {
!matches!(self, Self::Count)
}
#[must_use]
pub(in crate::db) const fn aggregate_kind(self) -> AggregateKind {
match self {
Self::Count => AggregateKind::Count,
Self::Sum => AggregateKind::Sum,
Self::Avg => AggregateKind::Avg,
Self::Min => AggregateKind::Min,
Self::Max => AggregateKind::Max,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlAggregateCall {
pub(crate) kind: SqlAggregateKind,
pub(crate) input: Option<Box<SqlExpr>>,
pub(crate) filter_expr: Option<Box<SqlExpr>>,
pub(crate) distinct: bool,
}
impl SqlAggregateCall {
#[must_use]
pub(in crate::db) fn is_already_local_scalar(&self) -> bool {
let input_is_local = self
.input
.as_deref()
.is_none_or(SqlExpr::is_already_local_scalar);
input_is_local
&& self
.filter_expr
.as_deref()
.is_none_or(SqlExpr::is_already_local_scalar)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[remain::sorted]
pub(crate) enum SqlScalarFunction {
Abs,
Cbrt,
Ceiling,
Coalesce,
Contains,
EndsWith,
Exp,
Floor,
#[cfg_attr(not(test), allow(dead_code))]
IsEmpty,
#[cfg_attr(not(test), allow(dead_code))]
IsMissing,
#[cfg_attr(not(test), allow(dead_code))]
IsNotEmpty,
#[cfg_attr(not(test), allow(dead_code))]
IsNotNull,
#[cfg_attr(not(test), allow(dead_code))]
IsNull,
Left,
Length,
Ln,
Log,
Log2,
Log10,
Lower,
Ltrim,
Mod,
NullIf,
OctetLength,
Position,
Power,
Replace,
Right,
Round,
Rtrim,
Sign,
Sqrt,
StartsWith,
Substring,
Trim,
Trunc,
Upper,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SqlScalarFunctionCallShape {
BinaryExprArgs,
FieldPlusLiteral,
Position,
Replace,
NumericScaleSpecial,
SharedScalarCall,
Substring,
UnaryExpr,
VariadicExprArgs,
WherePredicateExprPair,
}
impl SqlScalarFunction {
#[must_use]
pub(crate) const fn uses_numeric_scale_special_case(self) -> bool {
matches!(self, Self::Round | Self::Trunc)
}
#[must_use]
pub(in crate::db) const fn planner_function(self) -> Function {
match self {
Self::Abs => Function::Abs,
Self::Cbrt => Function::Cbrt,
Self::Ceiling => Function::Ceiling,
Self::Coalesce => Function::Coalesce,
Self::Contains => Function::Contains,
Self::EndsWith => Function::EndsWith,
Self::Exp => Function::Exp,
Self::Floor => Function::Floor,
Self::IsEmpty => Function::IsEmpty,
Self::IsMissing => Function::IsMissing,
Self::IsNotEmpty => Function::IsNotEmpty,
Self::IsNotNull => Function::IsNotNull,
Self::IsNull => Function::IsNull,
Self::Left => Function::Left,
Self::Length => Function::Length,
Self::Ln => Function::Ln,
Self::Log => Function::Log,
Self::Log10 => Function::Log10,
Self::Log2 => Function::Log2,
Self::Lower => Function::Lower,
Self::Ltrim => Function::Ltrim,
Self::Mod => Function::Mod,
Self::NullIf => Function::NullIf,
Self::OctetLength => Function::OctetLength,
Self::Position => Function::Position,
Self::Power => Function::Power,
Self::Replace => Function::Replace,
Self::Right => Function::Right,
Self::Round => Function::Round,
Self::Rtrim => Function::Rtrim,
Self::Sign => Function::Sign,
Self::StartsWith => Function::StartsWith,
Self::Substring => Function::Substring,
Self::Sqrt => Function::Sqrt,
Self::Trim => Function::Trim,
Self::Trunc => Function::Trunc,
Self::Upper => Function::Upper,
}
}
#[must_use]
pub(in crate::db) const fn non_where_call_shape(self) -> SqlScalarFunctionCallShape {
match self {
Self::Round | Self::Trunc => SqlScalarFunctionCallShape::NumericScaleSpecial,
Self::Coalesce => SqlScalarFunctionCallShape::VariadicExprArgs,
Self::NullIf | Self::Log | Self::Mod | Self::Power => {
SqlScalarFunctionCallShape::BinaryExprArgs
}
Self::Trim
| Self::Ltrim
| Self::Rtrim
| Self::Abs
| Self::Cbrt
| Self::Ceiling
| Self::Exp
| Self::Floor
| Self::Ln
| Self::Log10
| Self::Log2
| Self::Sign
| Self::Sqrt
| Self::IsEmpty
| Self::IsMissing
| Self::IsNotEmpty
| Self::IsNotNull
| Self::IsNull
| Self::Lower
| Self::Upper
| Self::Length
| Self::OctetLength => SqlScalarFunctionCallShape::UnaryExpr,
Self::Left | Self::Right | Self::StartsWith | Self::EndsWith | Self::Contains => {
SqlScalarFunctionCallShape::FieldPlusLiteral
}
Self::Position => SqlScalarFunctionCallShape::Position,
Self::Replace => SqlScalarFunctionCallShape::Replace,
Self::Substring => SqlScalarFunctionCallShape::Substring,
}
}
#[must_use]
pub(in crate::db) const fn where_call_shape(self) -> SqlScalarFunctionCallShape {
match self.non_where_call_shape() {
SqlScalarFunctionCallShape::NumericScaleSpecial => {
SqlScalarFunctionCallShape::NumericScaleSpecial
}
SqlScalarFunctionCallShape::VariadicExprArgs => {
SqlScalarFunctionCallShape::VariadicExprArgs
}
SqlScalarFunctionCallShape::BinaryExprArgs => {
SqlScalarFunctionCallShape::BinaryExprArgs
}
SqlScalarFunctionCallShape::FieldPlusLiteral
if self
.planner_function()
.boolean_text_predicate_kind()
.is_some() =>
{
SqlScalarFunctionCallShape::WherePredicateExprPair
}
SqlScalarFunctionCallShape::UnaryExpr
| SqlScalarFunctionCallShape::FieldPlusLiteral
| SqlScalarFunctionCallShape::Position
| SqlScalarFunctionCallShape::Replace
| SqlScalarFunctionCallShape::Substring => SqlScalarFunctionCallShape::SharedScalarCall,
SqlScalarFunctionCallShape::SharedScalarCall
| SqlScalarFunctionCallShape::WherePredicateExprPair => {
SqlScalarFunctionCallShape::SharedScalarCall
}
}
}
#[must_use]
pub(crate) fn from_identifier(identifier: &str) -> Option<Self> {
const SUPPORTED_SCALAR_FUNCTIONS: [(&str, SqlScalarFunction); 35] = [
("trim", SqlScalarFunction::Trim),
("ltrim", SqlScalarFunction::Ltrim),
("rtrim", SqlScalarFunction::Rtrim),
("round", SqlScalarFunction::Round),
("coalesce", SqlScalarFunction::Coalesce),
("nullif", SqlScalarFunction::NullIf),
("abs", SqlScalarFunction::Abs),
("cbrt", SqlScalarFunction::Cbrt),
("ceil", SqlScalarFunction::Ceiling),
("ceiling", SqlScalarFunction::Ceiling),
("exp", SqlScalarFunction::Exp),
("floor", SqlScalarFunction::Floor),
("ln", SqlScalarFunction::Ln),
("log", SqlScalarFunction::Log),
("log10", SqlScalarFunction::Log10),
("log2", SqlScalarFunction::Log2),
("sign", SqlScalarFunction::Sign),
("sqrt", SqlScalarFunction::Sqrt),
("mod", SqlScalarFunction::Mod),
("power", SqlScalarFunction::Power),
("pow", SqlScalarFunction::Power),
("trunc", SqlScalarFunction::Trunc),
("truncate", SqlScalarFunction::Trunc),
("lower", SqlScalarFunction::Lower),
("upper", SqlScalarFunction::Upper),
("length", SqlScalarFunction::Length),
("octet_length", SqlScalarFunction::OctetLength),
("left", SqlScalarFunction::Left),
("right", SqlScalarFunction::Right),
("starts_with", SqlScalarFunction::StartsWith),
("ends_with", SqlScalarFunction::EndsWith),
("contains", SqlScalarFunction::Contains),
("position", SqlScalarFunction::Position),
("replace", SqlScalarFunction::Replace),
("substring", SqlScalarFunction::Substring),
];
for (name, function) in SUPPORTED_SCALAR_FUNCTIONS {
if identifier.eq_ignore_ascii_case(name) {
return Some(function);
}
}
None
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SqlOrderDirection {
Asc,
Desc,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlOrderTerm {
pub(crate) field: SqlExpr,
pub(crate) direction: SqlOrderDirection,
}
impl SqlOrderTerm {
#[must_use]
pub(in crate::db) fn is_already_local_supported(&self) -> bool {
self.field.fields_are_already_local()
}
#[must_use]
pub(in crate::db) const fn direct_field_name(&self) -> Option<&str> {
match &self.field {
SqlExpr::Field(field) => Some(field.as_str()),
SqlExpr::FieldPath { .. }
| SqlExpr::Aggregate(_)
| SqlExpr::Literal(_)
| SqlExpr::Param { .. }
| SqlExpr::Membership { .. }
| SqlExpr::NullTest { .. }
| SqlExpr::Like { .. }
| SqlExpr::FunctionCall { .. }
| SqlExpr::Unary { .. }
| SqlExpr::Binary { .. }
| SqlExpr::Case { .. } => None,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlSelectStatement {
pub(crate) entity: String,
pub(crate) table_alias: Option<String>,
pub(crate) projection: SqlProjection,
pub(crate) projection_aliases: Vec<Option<String>>,
pub(crate) predicate: Option<SqlExpr>,
pub(crate) distinct: bool,
pub(crate) group_by: Vec<String>,
pub(crate) having: Vec<SqlExpr>,
pub(crate) order_by: Vec<SqlOrderTerm>,
pub(crate) limit: Option<u32>,
pub(crate) offset: Option<u32>,
}
impl SqlSelectStatement {
#[must_use]
pub(in crate::db) fn is_already_local_canonical(&self) -> bool {
if self.table_alias.is_some() {
return false;
}
if !self.projection_aliases.iter().all(Option::is_none) {
return false;
}
if !self.having.is_empty() {
return false;
}
if !self
.group_by
.iter()
.all(|field| SqlExpr::identifier_is_already_local(field.as_str()))
{
return false;
}
if !self.projection.is_already_local_scalar() {
return false;
}
if self
.predicate
.as_ref()
.is_some_and(|predicate| !predicate.is_already_local_scalar())
{
return false;
}
self.order_by
.iter()
.all(SqlOrderTerm::is_already_local_supported)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum SqlReturningProjection {
All,
Fields(Vec<String>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlDeleteStatement {
pub(crate) entity: String,
pub(crate) table_alias: Option<String>,
pub(crate) predicate: Option<SqlExpr>,
pub(crate) order_by: Vec<SqlOrderTerm>,
pub(crate) limit: Option<u32>,
pub(crate) offset: Option<u32>,
pub(crate) returning: Option<SqlReturningProjection>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum SqlInsertSource {
Values(Vec<Vec<Value>>),
Select(Box<SqlSelectStatement>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlInsertStatement {
pub(crate) entity: String,
pub(crate) columns: Vec<String>,
pub(crate) source: SqlInsertSource,
pub(crate) returning: Option<SqlReturningProjection>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlAssignment {
pub(crate) field: String,
pub(crate) value: Value,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlUpdateStatement {
pub(crate) entity: String,
pub(crate) table_alias: Option<String>,
pub(crate) assignments: Vec<SqlAssignment>,
pub(crate) predicate: Option<SqlExpr>,
pub(crate) order_by: Vec<SqlOrderTerm>,
pub(crate) limit: Option<u32>,
pub(crate) offset: Option<u32>,
pub(crate) returning: Option<SqlReturningProjection>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum SqlExplainMode {
Plan,
Execution,
Json,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum SqlExplainTarget {
Select(SqlSelectStatement),
Delete(SqlDeleteStatement),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlExplainStatement {
pub(crate) mode: SqlExplainMode,
pub(crate) verbose: bool,
pub(crate) statement: SqlExplainTarget,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlDescribeStatement {
pub(crate) entity: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlShowIndexesStatement {
pub(crate) entity: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlShowColumnsStatement {
pub(crate) entity: String,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct SqlShowEntitiesStatement;