use crate::{
db::query::plan::expr::ast::{Alias, BinaryOp, Expr, FieldId},
error::InternalError,
model::{entity::EntityModel, field::FieldModel},
value::Value,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum ProjectionSelection {
All,
Fields(Vec<FieldId>),
Exprs(Vec<ProjectionField>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum ProjectionField {
Scalar { expr: Expr, alias: Option<Alias> },
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub(crate) struct ProjectionSpec {
fields: Vec<ProjectionField>,
}
impl ProjectionSpec {
#[must_use]
pub(in crate::db::query::plan) const fn new(fields: Vec<ProjectionField>) -> Self {
Self { fields }
}
#[must_use]
#[cfg(test)]
pub(in crate::db) const fn from_fields_for_test(fields: Vec<ProjectionField>) -> Self {
Self::new(fields)
}
#[must_use]
pub(crate) const fn len(&self) -> usize {
self.fields.len()
}
pub(crate) fn fields(&self) -> std::slice::Iter<'_, ProjectionField> {
self.fields.iter()
}
#[must_use]
pub(in crate::db) fn is_model_identity_for(&self, model: &EntityModel) -> bool {
if self.len() != model.fields().len() {
return false;
}
for (field_model, projected_field) in model.fields().iter().zip(self.fields()) {
if !projected_field.is_identity_field_projection(field_model) {
return false;
}
}
true
}
pub(in crate::db) fn referenced_slots_for(
&self,
model: &EntityModel,
) -> Result<Vec<usize>, InternalError> {
let mut referenced = vec![false; model.fields().len()];
for field in self.fields() {
mark_projection_expr_slots(model, field.expr(), referenced.as_mut_slice())?;
}
Ok(referenced
.into_iter()
.enumerate()
.filter_map(|(slot, required)| required.then_some(slot))
.collect())
}
}
impl ProjectionField {
#[must_use]
pub(crate) const fn expr(&self) -> &Expr {
match self {
Self::Scalar { expr, .. } => expr,
}
}
#[must_use]
pub(in crate::db) fn direct_field_name(&self) -> Option<&str> {
direct_projection_expr_field_name(self.expr())
}
fn is_identity_field_projection(&self, field_model: &FieldModel) -> bool {
match self {
Self::Scalar {
expr: Expr::Field(field_id),
alias: None,
} => field_id.as_str() == field_model.name(),
Self::Scalar { .. } => false,
}
}
}
fn mark_projection_expr_slots(
model: &EntityModel,
expr: &Expr,
referenced: &mut [bool],
) -> Result<(), InternalError> {
expr.try_for_each_tree_expr(&mut |node| match node {
Expr::Field(field_id) => {
let field_name = field_id.as_str();
let slot = model.resolve_field_slot(field_name).ok_or_else(|| {
InternalError::query_invalid_logical_plan(format!(
"projection expression references unknown field '{field_name}'",
))
})?;
referenced[slot] = true;
Ok(())
}
Expr::FieldPath(path) => {
let field_name = path.root().as_str();
let slot = model.resolve_field_slot(field_name).ok_or_else(|| {
InternalError::query_invalid_logical_plan(format!(
"projection expression references unknown field '{field_name}'",
))
})?;
referenced[slot] = true;
Ok(())
}
_ => Ok(()),
})
}
#[must_use]
#[cfg_attr(
not(test),
expect(
clippy::missing_const_for_fn,
reason = "test-only alias traversal keeps the shared helper non-const across the full target matrix"
)
)]
pub(in crate::db) fn direct_projection_expr_field_name(expr: &Expr) -> Option<&str> {
match expr {
Expr::Field(field) => Some(field.as_str()),
#[cfg(test)]
Expr::Alias { expr, .. } => direct_projection_expr_field_name(expr.as_ref()),
Expr::Unary { .. } => None,
Expr::FieldPath(_)
| Expr::Literal(_)
| Expr::FunctionCall { .. }
| Expr::Aggregate(_)
| Expr::Case { .. }
| Expr::Binary { .. } => None,
}
}
#[must_use]
pub(crate) fn collect_unique_direct_projection_slots<'a>(
model: &EntityModel,
field_names: impl IntoIterator<Item = &'a str>,
) -> Option<Vec<usize>> {
let mut field_slots = Vec::new();
for field_name in field_names {
let slot = model.resolve_field_slot(field_name)?;
if field_slots.contains(&slot) {
return None;
}
field_slots.push(slot);
}
Some(field_slots)
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum GroupedOrderExprClass {
CanonicalGroupField,
GroupFieldPlusConstant,
GroupFieldMinusConstant,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum GroupedOrderTermAdmissibility {
Preserves(GroupedOrderExprClass),
PrefixMismatch,
UnsupportedExpression,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum GroupedTopKOrderTermAdmissibility {
Admissible,
NonGroupFieldReference,
UnsupportedExpression,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum GroupedCanonicalOrderShape {
CanonicalGroupField,
GroupFieldPlusConstant,
GroupFieldMinusConstant,
OtherField,
OtherFieldOffset,
Unsupported,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct GroupedOrderExprAnalysis {
canonical_shape: GroupedCanonicalOrderShape,
references_only_group_fields: bool,
contains_aggregate: bool,
contains_non_aggregate_wrapper_fn: bool,
}
impl GroupedOrderExprAnalysis {
fn from_expr(expr: &Expr, group_fields: &[&str], expected_group_field: Option<&str>) -> Self {
match expr {
Expr::Field(field) => Self {
canonical_shape: expected_group_field.map_or(
GroupedCanonicalOrderShape::Unsupported,
|expected_group_field| {
if field.as_str() == expected_group_field {
GroupedCanonicalOrderShape::CanonicalGroupField
} else {
GroupedCanonicalOrderShape::OtherField
}
},
),
references_only_group_fields: group_fields
.iter()
.any(|allowed| *allowed == field.as_str()),
contains_aggregate: false,
contains_non_aggregate_wrapper_fn: false,
},
Expr::Aggregate(_) => Self {
canonical_shape: GroupedCanonicalOrderShape::Unsupported,
references_only_group_fields: true,
contains_aggregate: true,
contains_non_aggregate_wrapper_fn: false,
},
Expr::FieldPath(_) => Self {
canonical_shape: GroupedCanonicalOrderShape::Unsupported,
references_only_group_fields: false,
contains_aggregate: false,
contains_non_aggregate_wrapper_fn: false,
},
Expr::Literal(_) => Self {
canonical_shape: GroupedCanonicalOrderShape::Unsupported,
references_only_group_fields: true,
contains_aggregate: false,
contains_non_aggregate_wrapper_fn: false,
},
Expr::FunctionCall { args, .. } => {
let child = args.iter().fold(
Self {
canonical_shape: GroupedCanonicalOrderShape::Unsupported,
references_only_group_fields: true,
contains_aggregate: false,
contains_non_aggregate_wrapper_fn: false,
},
|current, arg| current.merge_with(Self::from_expr(arg, group_fields, None)),
);
Self {
canonical_shape: GroupedCanonicalOrderShape::Unsupported,
contains_non_aggregate_wrapper_fn: !child.contains_aggregate
|| child.contains_non_aggregate_wrapper_fn,
..child
}
}
Expr::Case {
when_then_arms,
else_expr,
} => when_then_arms.iter().fold(
Self::from_expr(else_expr.as_ref(), group_fields, None),
|current, arm| {
current
.merge_with(Self::from_expr(arm.condition(), group_fields, None))
.merge_with(Self::from_expr(arm.result(), group_fields, None))
},
),
Expr::Binary { op, left, right } => {
let left_expr = left.as_ref();
let right_expr = right.as_ref();
let left = Self::from_expr(left_expr, group_fields, None);
let right = Self::from_expr(right_expr, group_fields, None);
Self {
canonical_shape: classify_grouped_canonical_order_shape(
*op,
left_expr,
right_expr,
expected_group_field,
),
..left.merge_with(right)
}
}
#[cfg(test)]
Expr::Alias { expr, .. } => Self {
canonical_shape: GroupedCanonicalOrderShape::Unsupported,
..Self::from_expr(expr.as_ref(), group_fields, None)
},
Expr::Unary { expr, .. } => Self {
canonical_shape: GroupedCanonicalOrderShape::Unsupported,
..Self::from_expr(expr.as_ref(), group_fields, None)
},
}
}
const fn merge_with(self, other: Self) -> Self {
Self {
canonical_shape: GroupedCanonicalOrderShape::Unsupported,
references_only_group_fields: self.references_only_group_fields
&& other.references_only_group_fields,
contains_aggregate: self.contains_aggregate || other.contains_aggregate,
contains_non_aggregate_wrapper_fn: self.contains_non_aggregate_wrapper_fn
|| other.contains_non_aggregate_wrapper_fn,
}
}
const fn canonical_admissibility(self) -> GroupedOrderTermAdmissibility {
match self.canonical_shape {
GroupedCanonicalOrderShape::CanonicalGroupField => {
GroupedOrderTermAdmissibility::Preserves(GroupedOrderExprClass::CanonicalGroupField)
}
GroupedCanonicalOrderShape::GroupFieldPlusConstant => {
GroupedOrderTermAdmissibility::Preserves(
GroupedOrderExprClass::GroupFieldPlusConstant,
)
}
GroupedCanonicalOrderShape::GroupFieldMinusConstant => {
GroupedOrderTermAdmissibility::Preserves(
GroupedOrderExprClass::GroupFieldMinusConstant,
)
}
GroupedCanonicalOrderShape::OtherField
| GroupedCanonicalOrderShape::OtherFieldOffset => {
GroupedOrderTermAdmissibility::PrefixMismatch
}
GroupedCanonicalOrderShape::Unsupported => {
GroupedOrderTermAdmissibility::UnsupportedExpression
}
}
}
}
fn classify_grouped_canonical_order_shape(
op: BinaryOp,
left: &Expr,
right: &Expr,
expected_group_field: Option<&str>,
) -> GroupedCanonicalOrderShape {
let Some(expected_group_field) = expected_group_field else {
return GroupedCanonicalOrderShape::Unsupported;
};
match (op, left, right) {
(BinaryOp::Add, Expr::Field(field), right)
if field.as_str() == expected_group_field && is_numeric_order_offset_literal(right) =>
{
GroupedCanonicalOrderShape::GroupFieldPlusConstant
}
(BinaryOp::Sub, Expr::Field(field), right)
if field.as_str() == expected_group_field && is_numeric_order_offset_literal(right) =>
{
GroupedCanonicalOrderShape::GroupFieldMinusConstant
}
(BinaryOp::Add | BinaryOp::Sub, Expr::Field(_), right)
if is_numeric_order_offset_literal(right) =>
{
GroupedCanonicalOrderShape::OtherFieldOffset
}
_ => GroupedCanonicalOrderShape::Unsupported,
}
}
#[must_use]
pub(crate) fn classify_grouped_order_term_for_field(
expr: &Expr,
expected_group_field: &str,
) -> GroupedOrderTermAdmissibility {
GroupedOrderExprAnalysis::from_expr(expr, &[], Some(expected_group_field))
.canonical_admissibility()
}
const fn is_numeric_order_offset_literal(expr: &Expr) -> bool {
matches!(
expr,
Expr::Literal(
Value::Int(_)
| Value::Int128(_)
| Value::IntBig(_)
| Value::Uint(_)
| Value::Uint128(_)
| Value::UintBig(_)
| Value::Decimal(_)
| Value::Float32(_)
| Value::Float64(_)
)
)
}
#[must_use]
pub(crate) fn classify_grouped_top_k_order_term(
expr: &Expr,
group_fields: &[&str],
) -> GroupedTopKOrderTermAdmissibility {
let analysis = GroupedOrderExprAnalysis::from_expr(expr, group_fields, None);
if analysis.references_only_group_fields {
if !analysis.contains_aggregate && analysis.contains_non_aggregate_wrapper_fn {
return GroupedTopKOrderTermAdmissibility::UnsupportedExpression;
}
return GroupedTopKOrderTermAdmissibility::Admissible;
}
GroupedTopKOrderTermAdmissibility::NonGroupFieldReference
}
#[must_use]
pub(crate) fn grouped_top_k_order_term_requires_heap(expr: &Expr) -> bool {
GroupedOrderExprAnalysis::from_expr(expr, &[], None).contains_aggregate
}