#[cfg(test)]
mod group_membership_tests;
use crate::db::{
QueryError,
query::{
builder::AggregateExpr,
plan::expr::{Expr, FieldPath},
preparation::PreparationWork,
},
schema::SchemaInfo,
};
use icydb_diagnostic_code::DiagnosticExecutionBudgetResource as Resource;
#[derive(Clone, Debug)]
pub(in crate::db::sql::lowering) struct AnalyzedLoweredExpr {
expr: Expr,
analysis: LoweredExprAnalysis,
}
impl AnalyzedLoweredExpr {
#[must_use]
pub(in crate::db::sql::lowering) fn new(expr: Expr) -> Self {
let analysis = analyze_lowered_expr(&expr);
Self { expr, analysis }
}
#[must_use]
pub(in crate::db::sql::lowering) const fn expr(&self) -> &Expr {
&self.expr
}
#[must_use]
pub(in crate::db::sql::lowering) const fn analysis(&self) -> &LoweredExprAnalysis {
&self.analysis
}
#[must_use]
pub(in crate::db::sql::lowering) fn into_expr(self) -> Expr {
self.expr
}
#[must_use]
pub(in crate::db::sql::lowering) fn into_parts(self) -> (Expr, LoweredExprAnalysis) {
(self.expr, self.analysis)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(in crate::db::sql::lowering) enum LoweredExprSourceRef {
Direct(String),
Path(FieldPath),
}
impl LoweredExprSourceRef {
const fn root(&self) -> &str {
match self {
Self::Direct(field) => field.as_str(),
Self::Path(path) => path.root().as_str(),
}
}
}
#[derive(Clone, Debug, Default)]
pub(in crate::db::sql::lowering) struct LoweredExprAnalysis {
aggregate_refs: Vec<AggregateExpr>,
source_refs: Vec<LoweredExprSourceRef>,
first_unknown_field: Option<String>,
}
impl LoweredExprAnalysis {
#[must_use]
pub(in crate::db::sql::lowering) const fn contains_aggregate(&self) -> bool {
!self.aggregate_refs.is_empty()
}
#[must_use]
pub(in crate::db::sql::lowering) const fn aggregate_refs(&self) -> &[AggregateExpr] {
self.aggregate_refs.as_slice()
}
#[must_use]
pub(in crate::db::sql::lowering) const fn references_direct_fields(&self) -> bool {
!self.source_refs.is_empty()
}
#[must_use]
pub(in crate::db::sql::lowering) fn references_only_direct_fields(
&self,
allowed: &[&str],
) -> bool {
self.source_refs.iter().all(|source_ref| match source_ref {
LoweredExprSourceRef::Direct(field) => allowed.contains(&field.as_str()),
LoweredExprSourceRef::Path(_) => false,
})
}
pub(in crate::db::sql::lowering) fn references_only_group_fields(
&self,
group_fields: &[String],
work: &PreparationWork<'_>,
) -> Result<bool, QueryError> {
for source_ref in &self.source_refs {
let mut found = false;
for label in group_fields {
work.charge(
Resource::PredicateExpressionSteps,
1 + (label.len() as u64).saturating_mul(2),
)?;
found = match source_ref {
LoweredExprSourceRef::Direct(field) => !label.contains('.') && label == field,
LoweredExprSourceRef::Path(path) => {
label.split_once('.').is_some_and(|(root, nested)| {
root == path.root().as_str()
&& nested.split('.').eq(path
.path_spec()
.segments()
.iter()
.map(String::as_str))
})
}
};
if found {
break;
}
}
if !found {
return Ok(false);
}
}
Ok(true)
}
#[must_use]
pub(in crate::db::sql::lowering) fn first_unknown_field(&self) -> Option<&str> {
self.first_unknown_field.as_deref()
}
#[must_use]
pub(in crate::db::sql::lowering) fn first_unknown_field_for_schema(
&self,
schema: &SchemaInfo,
) -> Option<&str> {
self.first_unknown_field().or_else(|| {
self.source_refs.iter().find_map(|source_ref| {
let root = source_ref.root();
schema.field_slot_index(root).is_none().then_some(root)
})
})
}
#[must_use]
pub(in crate::db::sql::lowering) const fn source_refs(&self) -> &[LoweredExprSourceRef] {
self.source_refs.as_slice()
}
fn visit_field(&mut self, field: &str) {
self.source_refs
.push(LoweredExprSourceRef::Direct(field.to_string()));
}
fn visit_field_path(&mut self, path: &FieldPath) {
self.source_refs
.push(LoweredExprSourceRef::Path(path.clone()));
}
fn visit_aggregate(&mut self, aggregate: &AggregateExpr) {
self.aggregate_refs.push(aggregate.clone());
}
}
#[must_use]
pub(in crate::db::sql::lowering) fn analyze_lowered_expr(expr: &Expr) -> LoweredExprAnalysis {
let mut analysis = LoweredExprAnalysis {
aggregate_refs: Vec::new(),
source_refs: Vec::new(),
first_unknown_field: None,
};
expr.for_each_tree_expr(&mut |node| match node {
Expr::Field(field) => {
analysis.visit_field(field.as_str());
}
Expr::FieldPath(path) => {
analysis.visit_field_path(path);
}
Expr::Aggregate(aggregate) => {
analysis.visit_aggregate(aggregate);
}
_ => {}
});
analysis
}