use crate::db::sql::lowering::{
LoweredBaseQueryShape, LoweredSqlCommand, LoweredSqlCommandInner, PreparedSqlStatement,
SqlLoweringError,
};
#[cfg(test)]
use crate::{db::query::intent::Query, traits::EntityKind};
use crate::{
db::{
predicate::MissingRowPolicy,
query::{
builder::aggregate::{avg, count, count_by, max_by, min_by, sum},
intent::StructuralQuery,
plan::{AggregateKind, FieldSlot, resolve_aggregate_target_field_slot},
},
sql::parser::{
SqlAggregateCall, SqlAggregateKind, SqlExplainMode, SqlProjection, SqlSelectItem,
SqlSelectStatement, SqlStatement,
},
},
model::entity::EntityModel,
};
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum SqlGlobalAggregateTerminal {
CountRows,
CountField { field: String, distinct: bool },
SumField { field: String, distinct: bool },
AvgField { field: String, distinct: bool },
MinField(String),
MaxField(String),
}
#[cfg(test)]
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) enum TypedSqlGlobalAggregateTerminal {
CountRows,
CountField {
target_slot: FieldSlot,
distinct: bool,
},
SumField {
target_slot: FieldSlot,
distinct: bool,
},
AvgField {
target_slot: FieldSlot,
distinct: bool,
},
MinField(FieldSlot),
MaxField(FieldSlot),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PreparedSqlScalarAggregateDomain {
ExistingRows,
ProjectionField,
NumericField,
ScalarExtremaValue,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PreparedSqlScalarAggregateOrderingRequirement {
None,
FieldOrder,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PreparedSqlScalarAggregateRowSource {
ExistingRows,
ProjectedField,
NumericField,
ExtremalWinnerField,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PreparedSqlScalarAggregateEmptySetBehavior {
Zero,
Null,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PreparedSqlScalarAggregateDescriptorShape {
CountRows,
CountField,
SumField,
AvgField,
MinField,
MaxField,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PreparedSqlScalarAggregateRuntimeDescriptor {
CountRows,
CountField,
NumericField { kind: AggregateKind },
ExtremalWinnerField { kind: AggregateKind },
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct PreparedSqlScalarAggregateStrategy {
target_slot: Option<FieldSlot>,
distinct_input: bool,
domain: PreparedSqlScalarAggregateDomain,
ordering_requirement: PreparedSqlScalarAggregateOrderingRequirement,
row_source: PreparedSqlScalarAggregateRowSource,
empty_set_behavior: PreparedSqlScalarAggregateEmptySetBehavior,
descriptor_shape: PreparedSqlScalarAggregateDescriptorShape,
}
impl PreparedSqlScalarAggregateStrategy {
const fn new(
target_slot: Option<FieldSlot>,
distinct_input: bool,
domain: PreparedSqlScalarAggregateDomain,
ordering_requirement: PreparedSqlScalarAggregateOrderingRequirement,
row_source: PreparedSqlScalarAggregateRowSource,
empty_set_behavior: PreparedSqlScalarAggregateEmptySetBehavior,
descriptor_shape: PreparedSqlScalarAggregateDescriptorShape,
) -> Self {
Self {
target_slot,
distinct_input,
domain,
ordering_requirement,
row_source,
empty_set_behavior,
descriptor_shape,
}
}
#[cfg(test)]
pub(in crate::db::sql::lowering) fn from_typed_terminal(
terminal: &TypedSqlGlobalAggregateTerminal,
) -> Self {
match terminal {
TypedSqlGlobalAggregateTerminal::CountRows => Self::new(
None,
false,
PreparedSqlScalarAggregateDomain::ExistingRows,
PreparedSqlScalarAggregateOrderingRequirement::None,
PreparedSqlScalarAggregateRowSource::ExistingRows,
PreparedSqlScalarAggregateEmptySetBehavior::Zero,
PreparedSqlScalarAggregateDescriptorShape::CountRows,
),
TypedSqlGlobalAggregateTerminal::CountField {
target_slot,
distinct,
} => Self::new(
Some(target_slot.clone()),
*distinct,
PreparedSqlScalarAggregateDomain::ProjectionField,
PreparedSqlScalarAggregateOrderingRequirement::None,
PreparedSqlScalarAggregateRowSource::ProjectedField,
PreparedSqlScalarAggregateEmptySetBehavior::Zero,
PreparedSqlScalarAggregateDescriptorShape::CountField,
),
TypedSqlGlobalAggregateTerminal::SumField {
target_slot,
distinct,
} => Self::new(
Some(target_slot.clone()),
*distinct,
PreparedSqlScalarAggregateDomain::NumericField,
PreparedSqlScalarAggregateOrderingRequirement::None,
PreparedSqlScalarAggregateRowSource::NumericField,
PreparedSqlScalarAggregateEmptySetBehavior::Null,
PreparedSqlScalarAggregateDescriptorShape::SumField,
),
TypedSqlGlobalAggregateTerminal::AvgField {
target_slot,
distinct,
} => Self::new(
Some(target_slot.clone()),
*distinct,
PreparedSqlScalarAggregateDomain::NumericField,
PreparedSqlScalarAggregateOrderingRequirement::None,
PreparedSqlScalarAggregateRowSource::NumericField,
PreparedSqlScalarAggregateEmptySetBehavior::Null,
PreparedSqlScalarAggregateDescriptorShape::AvgField,
),
TypedSqlGlobalAggregateTerminal::MinField(target_slot) => Self::new(
Some(target_slot.clone()),
false,
PreparedSqlScalarAggregateDomain::ScalarExtremaValue,
PreparedSqlScalarAggregateOrderingRequirement::FieldOrder,
PreparedSqlScalarAggregateRowSource::ExtremalWinnerField,
PreparedSqlScalarAggregateEmptySetBehavior::Null,
PreparedSqlScalarAggregateDescriptorShape::MinField,
),
TypedSqlGlobalAggregateTerminal::MaxField(target_slot) => Self::new(
Some(target_slot.clone()),
false,
PreparedSqlScalarAggregateDomain::ScalarExtremaValue,
PreparedSqlScalarAggregateOrderingRequirement::FieldOrder,
PreparedSqlScalarAggregateRowSource::ExtremalWinnerField,
PreparedSqlScalarAggregateEmptySetBehavior::Null,
PreparedSqlScalarAggregateDescriptorShape::MaxField,
),
}
}
fn from_lowered_terminal_with_model(
model: &'static EntityModel,
terminal: &SqlGlobalAggregateTerminal,
) -> Result<Self, SqlLoweringError> {
let resolve_target_slot = |field: &str| {
resolve_aggregate_target_field_slot(model, field).map_err(SqlLoweringError::from)
};
match terminal {
SqlGlobalAggregateTerminal::CountRows => Ok(Self::new(
None,
false,
PreparedSqlScalarAggregateDomain::ExistingRows,
PreparedSqlScalarAggregateOrderingRequirement::None,
PreparedSqlScalarAggregateRowSource::ExistingRows,
PreparedSqlScalarAggregateEmptySetBehavior::Zero,
PreparedSqlScalarAggregateDescriptorShape::CountRows,
)),
SqlGlobalAggregateTerminal::CountField { field, distinct } => {
let target_slot = resolve_target_slot(field.as_str())?;
Ok(Self::new(
Some(target_slot),
*distinct,
PreparedSqlScalarAggregateDomain::ProjectionField,
PreparedSqlScalarAggregateOrderingRequirement::None,
PreparedSqlScalarAggregateRowSource::ProjectedField,
PreparedSqlScalarAggregateEmptySetBehavior::Zero,
PreparedSqlScalarAggregateDescriptorShape::CountField,
))
}
SqlGlobalAggregateTerminal::SumField { field, distinct } => {
let target_slot = resolve_target_slot(field.as_str())?;
Ok(Self::new(
Some(target_slot),
*distinct,
PreparedSqlScalarAggregateDomain::NumericField,
PreparedSqlScalarAggregateOrderingRequirement::None,
PreparedSqlScalarAggregateRowSource::NumericField,
PreparedSqlScalarAggregateEmptySetBehavior::Null,
PreparedSqlScalarAggregateDescriptorShape::SumField,
))
}
SqlGlobalAggregateTerminal::AvgField { field, distinct } => {
let target_slot = resolve_target_slot(field.as_str())?;
Ok(Self::new(
Some(target_slot),
*distinct,
PreparedSqlScalarAggregateDomain::NumericField,
PreparedSqlScalarAggregateOrderingRequirement::None,
PreparedSqlScalarAggregateRowSource::NumericField,
PreparedSqlScalarAggregateEmptySetBehavior::Null,
PreparedSqlScalarAggregateDescriptorShape::AvgField,
))
}
SqlGlobalAggregateTerminal::MinField(field) => {
let target_slot = resolve_target_slot(field.as_str())?;
Ok(Self::new(
Some(target_slot),
false,
PreparedSqlScalarAggregateDomain::ScalarExtremaValue,
PreparedSqlScalarAggregateOrderingRequirement::FieldOrder,
PreparedSqlScalarAggregateRowSource::ExtremalWinnerField,
PreparedSqlScalarAggregateEmptySetBehavior::Null,
PreparedSqlScalarAggregateDescriptorShape::MinField,
))
}
SqlGlobalAggregateTerminal::MaxField(field) => {
let target_slot = resolve_target_slot(field.as_str())?;
Ok(Self::new(
Some(target_slot),
false,
PreparedSqlScalarAggregateDomain::ScalarExtremaValue,
PreparedSqlScalarAggregateOrderingRequirement::FieldOrder,
PreparedSqlScalarAggregateRowSource::ExtremalWinnerField,
PreparedSqlScalarAggregateEmptySetBehavior::Null,
PreparedSqlScalarAggregateDescriptorShape::MaxField,
))
}
}
}
#[must_use]
pub(crate) const fn target_slot(&self) -> Option<&FieldSlot> {
self.target_slot.as_ref()
}
#[must_use]
pub(crate) const fn is_distinct(&self) -> bool {
self.distinct_input
}
#[cfg(test)]
#[must_use]
pub(crate) const fn domain(&self) -> PreparedSqlScalarAggregateDomain {
self.domain
}
#[cfg(test)]
#[must_use]
pub(crate) const fn descriptor_shape(&self) -> PreparedSqlScalarAggregateDescriptorShape {
self.descriptor_shape
}
#[must_use]
pub(crate) const fn runtime_descriptor(&self) -> PreparedSqlScalarAggregateRuntimeDescriptor {
match self.descriptor_shape {
PreparedSqlScalarAggregateDescriptorShape::CountRows => {
PreparedSqlScalarAggregateRuntimeDescriptor::CountRows
}
PreparedSqlScalarAggregateDescriptorShape::CountField => {
PreparedSqlScalarAggregateRuntimeDescriptor::CountField
}
PreparedSqlScalarAggregateDescriptorShape::SumField => {
PreparedSqlScalarAggregateRuntimeDescriptor::NumericField {
kind: AggregateKind::Sum,
}
}
PreparedSqlScalarAggregateDescriptorShape::AvgField => {
PreparedSqlScalarAggregateRuntimeDescriptor::NumericField {
kind: AggregateKind::Avg,
}
}
PreparedSqlScalarAggregateDescriptorShape::MinField => {
PreparedSqlScalarAggregateRuntimeDescriptor::ExtremalWinnerField {
kind: AggregateKind::Min,
}
}
PreparedSqlScalarAggregateDescriptorShape::MaxField => {
PreparedSqlScalarAggregateRuntimeDescriptor::ExtremalWinnerField {
kind: AggregateKind::Max,
}
}
}
}
#[must_use]
pub(crate) const fn aggregate_kind(&self) -> AggregateKind {
match self.descriptor_shape {
PreparedSqlScalarAggregateDescriptorShape::CountRows
| PreparedSqlScalarAggregateDescriptorShape::CountField => AggregateKind::Count,
PreparedSqlScalarAggregateDescriptorShape::SumField => AggregateKind::Sum,
PreparedSqlScalarAggregateDescriptorShape::AvgField => AggregateKind::Avg,
PreparedSqlScalarAggregateDescriptorShape::MinField => AggregateKind::Min,
PreparedSqlScalarAggregateDescriptorShape::MaxField => AggregateKind::Max,
}
}
#[must_use]
pub(crate) fn projected_field(&self) -> Option<&str> {
self.target_slot().map(FieldSlot::field)
}
#[cfg(test)]
#[must_use]
pub(crate) const fn ordering_requirement(
&self,
) -> PreparedSqlScalarAggregateOrderingRequirement {
self.ordering_requirement
}
#[cfg(test)]
#[must_use]
pub(crate) const fn row_source(&self) -> PreparedSqlScalarAggregateRowSource {
self.row_source
}
#[cfg(test)]
#[must_use]
pub(crate) const fn empty_set_behavior(&self) -> PreparedSqlScalarAggregateEmptySetBehavior {
self.empty_set_behavior
}
}
#[derive(Clone, Debug)]
pub(crate) struct LoweredSqlGlobalAggregateCommand {
pub(in crate::db::sql::lowering) query: LoweredBaseQueryShape,
pub(in crate::db::sql::lowering) terminal: SqlGlobalAggregateTerminal,
}
enum LoweredSqlAggregateShape {
CountRows,
CountField {
field: String,
distinct: bool,
},
FieldTarget {
kind: SqlAggregateKind,
field: String,
distinct: bool,
},
}
#[cfg(test)]
#[derive(Debug)]
pub(crate) struct SqlGlobalAggregateCommand<E: EntityKind> {
query: Query<E>,
terminal: TypedSqlGlobalAggregateTerminal,
}
#[cfg(test)]
impl<E: EntityKind> SqlGlobalAggregateCommand<E> {
#[must_use]
pub(crate) const fn query(&self) -> &Query<E> {
&self.query
}
#[cfg(test)]
#[must_use]
pub(crate) const fn terminal(&self) -> &TypedSqlGlobalAggregateTerminal {
&self.terminal
}
#[must_use]
pub(crate) fn prepared_scalar_strategy(&self) -> PreparedSqlScalarAggregateStrategy {
PreparedSqlScalarAggregateStrategy::from_typed_terminal(&self.terminal)
}
}
#[derive(Clone, Debug)]
pub(crate) struct SqlGlobalAggregateCommandCore {
query: StructuralQuery,
terminal: SqlGlobalAggregateTerminal,
}
impl SqlGlobalAggregateCommandCore {
#[must_use]
pub(in crate::db) const fn query(&self) -> &StructuralQuery {
&self.query
}
pub(in crate::db) fn prepared_scalar_strategy_with_model(
&self,
model: &'static EntityModel,
) -> Result<PreparedSqlScalarAggregateStrategy, SqlLoweringError> {
PreparedSqlScalarAggregateStrategy::from_lowered_terminal_with_model(model, &self.terminal)
}
}
pub(in crate::db) fn is_sql_global_aggregate_statement(statement: &SqlStatement) -> bool {
let SqlStatement::Select(statement) = statement else {
return false;
};
is_sql_global_aggregate_select(statement)
}
fn is_sql_global_aggregate_select(statement: &SqlSelectStatement) -> bool {
if statement.distinct || !statement.group_by.is_empty() || !statement.having.is_empty() {
return false;
}
lower_global_aggregate_terminal(statement.projection.clone()).is_ok()
}
pub(crate) fn bind_lowered_sql_explain_global_aggregate_structural(
lowered: &LoweredSqlCommand,
model: &'static EntityModel,
consistency: MissingRowPolicy,
) -> Option<(SqlExplainMode, SqlGlobalAggregateCommandCore)> {
let LoweredSqlCommandInner::ExplainGlobalAggregate { mode, command } = &lowered.0 else {
return None;
};
Some((
*mode,
bind_lowered_sql_global_aggregate_command_structural(model, command.clone(), consistency),
))
}
#[cfg(test)]
pub(crate) fn compile_sql_global_aggregate_command<E: EntityKind>(
sql: &str,
consistency: MissingRowPolicy,
) -> Result<SqlGlobalAggregateCommand<E>, SqlLoweringError> {
let statement = crate::db::sql::parser::parse_sql(sql)?;
let prepared = crate::db::sql::lowering::prepare_sql_statement(statement, E::MODEL.name())?;
compile_sql_global_aggregate_command_from_prepared::<E>(prepared, consistency)
}
#[cfg(test)]
pub(crate) fn compile_sql_global_aggregate_command_from_prepared<E: EntityKind>(
prepared: PreparedSqlStatement,
consistency: MissingRowPolicy,
) -> Result<SqlGlobalAggregateCommand<E>, SqlLoweringError> {
let SqlStatement::Select(statement) = prepared.statement else {
return Err(SqlLoweringError::unsupported_select_projection());
};
bind_lowered_sql_global_aggregate_command::<E>(
lower_global_aggregate_select_shape(statement)?,
consistency,
)
}
pub(in crate::db) fn compile_sql_global_aggregate_command_core_from_prepared(
prepared: PreparedSqlStatement,
model: &'static EntityModel,
consistency: MissingRowPolicy,
) -> Result<SqlGlobalAggregateCommandCore, SqlLoweringError> {
let SqlStatement::Select(statement) = prepared.statement else {
return Err(SqlLoweringError::unsupported_select_projection());
};
Ok(bind_lowered_sql_global_aggregate_command_structural(
model,
lower_global_aggregate_select_shape(statement)?,
consistency,
))
}
#[cfg(test)]
fn bind_lowered_sql_global_aggregate_terminal<E: EntityKind>(
terminal: SqlGlobalAggregateTerminal,
) -> Result<TypedSqlGlobalAggregateTerminal, SqlLoweringError> {
let resolve_target_slot = |field: &str| {
resolve_aggregate_target_field_slot(E::MODEL, field).map_err(SqlLoweringError::from)
};
match terminal {
SqlGlobalAggregateTerminal::CountRows => Ok(TypedSqlGlobalAggregateTerminal::CountRows),
SqlGlobalAggregateTerminal::CountField { field, distinct } => {
Ok(TypedSqlGlobalAggregateTerminal::CountField {
target_slot: resolve_target_slot(field.as_str())?,
distinct,
})
}
SqlGlobalAggregateTerminal::SumField { field, distinct } => {
Ok(TypedSqlGlobalAggregateTerminal::SumField {
target_slot: resolve_target_slot(field.as_str())?,
distinct,
})
}
SqlGlobalAggregateTerminal::AvgField { field, distinct } => {
Ok(TypedSqlGlobalAggregateTerminal::AvgField {
target_slot: resolve_target_slot(field.as_str())?,
distinct,
})
}
SqlGlobalAggregateTerminal::MinField(field) => Ok(
TypedSqlGlobalAggregateTerminal::MinField(resolve_target_slot(field.as_str())?),
),
SqlGlobalAggregateTerminal::MaxField(field) => Ok(
TypedSqlGlobalAggregateTerminal::MaxField(resolve_target_slot(field.as_str())?),
),
}
}
pub(in crate::db::sql::lowering) fn lower_global_aggregate_select_shape(
statement: SqlSelectStatement,
) -> Result<LoweredSqlGlobalAggregateCommand, SqlLoweringError> {
let SqlSelectStatement {
projection,
projection_aliases: _,
predicate,
distinct,
group_by,
having,
order_by,
limit,
offset,
entity: _,
} = statement;
if distinct {
return Err(SqlLoweringError::unsupported_select_distinct());
}
if !group_by.is_empty() {
return Err(SqlLoweringError::unsupported_select_group_by());
}
if !having.is_empty() {
return Err(SqlLoweringError::unsupported_select_having());
}
let terminal = lower_global_aggregate_terminal(projection)?;
Ok(LoweredSqlGlobalAggregateCommand {
query: LoweredBaseQueryShape {
predicate,
order_by,
limit,
offset,
},
terminal,
})
}
#[cfg(test)]
pub(in crate::db::sql::lowering) fn bind_lowered_sql_global_aggregate_command<E: EntityKind>(
lowered: LoweredSqlGlobalAggregateCommand,
consistency: MissingRowPolicy,
) -> Result<SqlGlobalAggregateCommand<E>, SqlLoweringError> {
let terminal = bind_lowered_sql_global_aggregate_terminal::<E>(lowered.terminal)?;
Ok(SqlGlobalAggregateCommand {
query: Query::from_inner(crate::db::sql::lowering::apply_lowered_base_query_shape(
StructuralQuery::new(E::MODEL, consistency),
lowered.query,
)),
terminal,
})
}
fn bind_lowered_sql_global_aggregate_command_structural(
model: &'static EntityModel,
lowered: LoweredSqlGlobalAggregateCommand,
consistency: MissingRowPolicy,
) -> SqlGlobalAggregateCommandCore {
SqlGlobalAggregateCommandCore {
query: crate::db::sql::lowering::apply_lowered_base_query_shape(
StructuralQuery::new(model, consistency),
lowered.query,
),
terminal: lowered.terminal,
}
}
fn lower_global_aggregate_terminal(
projection: SqlProjection,
) -> Result<SqlGlobalAggregateTerminal, SqlLoweringError> {
let SqlProjection::Items(items) = projection else {
return Err(SqlLoweringError::unsupported_select_projection());
};
if items.len() != 1 {
return Err(SqlLoweringError::unsupported_select_projection());
}
let Some(SqlSelectItem::Aggregate(aggregate)) = items.into_iter().next() else {
return Err(SqlLoweringError::unsupported_select_projection());
};
match lower_sql_aggregate_shape(aggregate)? {
LoweredSqlAggregateShape::CountRows => Ok(SqlGlobalAggregateTerminal::CountRows),
LoweredSqlAggregateShape::CountField { field, distinct } => {
Ok(SqlGlobalAggregateTerminal::CountField { field, distinct })
}
LoweredSqlAggregateShape::FieldTarget {
kind: SqlAggregateKind::Sum,
field,
distinct,
} => Ok(SqlGlobalAggregateTerminal::SumField { field, distinct }),
LoweredSqlAggregateShape::FieldTarget {
kind: SqlAggregateKind::Avg,
field,
distinct,
} => Ok(SqlGlobalAggregateTerminal::AvgField { field, distinct }),
LoweredSqlAggregateShape::FieldTarget {
kind: SqlAggregateKind::Min,
field,
..
} => Ok(SqlGlobalAggregateTerminal::MinField(field)),
LoweredSqlAggregateShape::FieldTarget {
kind: SqlAggregateKind::Max,
field,
..
} => Ok(SqlGlobalAggregateTerminal::MaxField(field)),
LoweredSqlAggregateShape::FieldTarget {
kind: SqlAggregateKind::Count,
..
} => Err(SqlLoweringError::unsupported_select_projection()),
}
}
fn lower_sql_aggregate_shape(
call: SqlAggregateCall,
) -> Result<LoweredSqlAggregateShape, SqlLoweringError> {
match (call.kind, call.field, call.distinct) {
(SqlAggregateKind::Count, None, false) => Ok(LoweredSqlAggregateShape::CountRows),
(SqlAggregateKind::Count, Some(field), distinct) => {
Ok(LoweredSqlAggregateShape::CountField { field, distinct })
}
(
kind @ (SqlAggregateKind::Sum
| SqlAggregateKind::Avg
| SqlAggregateKind::Min
| SqlAggregateKind::Max),
Some(field),
distinct,
) => Ok(LoweredSqlAggregateShape::FieldTarget {
kind,
field,
distinct,
}),
_ => Err(SqlLoweringError::unsupported_select_projection()),
}
}
pub(in crate::db::sql::lowering) fn grouped_projection_aggregate_calls(
projection: &SqlProjection,
group_by_fields: &[String],
) -> Result<Vec<SqlAggregateCall>, SqlLoweringError> {
if group_by_fields.is_empty() {
return Err(SqlLoweringError::unsupported_select_group_by());
}
let SqlProjection::Items(items) = projection else {
return Err(SqlLoweringError::unsupported_select_group_by());
};
let mut projected_group_fields = Vec::<String>::new();
let mut aggregate_calls = Vec::<SqlAggregateCall>::new();
let mut seen_aggregate = false;
for item in items {
match item {
SqlSelectItem::Field(field) => {
if seen_aggregate {
return Err(SqlLoweringError::unsupported_select_group_by());
}
projected_group_fields.push(field.clone());
}
SqlSelectItem::Aggregate(aggregate) => {
seen_aggregate = true;
aggregate_calls.push(aggregate.clone());
}
SqlSelectItem::TextFunction(_)
| SqlSelectItem::Arithmetic(_)
| SqlSelectItem::Round(_) => {
return Err(SqlLoweringError::unsupported_select_group_by());
}
}
}
if aggregate_calls.is_empty() || projected_group_fields.as_slice() != group_by_fields {
return Err(SqlLoweringError::unsupported_select_group_by());
}
Ok(aggregate_calls)
}
pub(in crate::db::sql::lowering) fn lower_aggregate_call(
call: SqlAggregateCall,
) -> Result<crate::db::query::builder::AggregateExpr, SqlLoweringError> {
match lower_sql_aggregate_shape(call)? {
LoweredSqlAggregateShape::CountRows => Ok(count()),
LoweredSqlAggregateShape::CountField {
field,
distinct: false,
} => Ok(count_by(field)),
LoweredSqlAggregateShape::CountField {
field,
distinct: true,
} => Ok(count_by(field).distinct()),
LoweredSqlAggregateShape::FieldTarget {
kind: SqlAggregateKind::Sum,
field,
distinct: false,
} => Ok(sum(field)),
LoweredSqlAggregateShape::FieldTarget {
kind: SqlAggregateKind::Sum,
field,
distinct: true,
} => Ok(sum(field).distinct()),
LoweredSqlAggregateShape::FieldTarget {
kind: SqlAggregateKind::Avg,
field,
distinct: false,
} => Ok(avg(field)),
LoweredSqlAggregateShape::FieldTarget {
kind: SqlAggregateKind::Avg,
field,
distinct: true,
} => Ok(avg(field).distinct()),
LoweredSqlAggregateShape::FieldTarget {
kind: SqlAggregateKind::Min,
field,
..
} => Ok(min_by(field)),
LoweredSqlAggregateShape::FieldTarget {
kind: SqlAggregateKind::Max,
field,
..
} => Ok(max_by(field)),
LoweredSqlAggregateShape::FieldTarget {
kind: SqlAggregateKind::Count,
..
} => Err(SqlLoweringError::unsupported_select_projection()),
}
}
pub(in crate::db::sql::lowering) fn resolve_having_aggregate_index(
target: &SqlAggregateCall,
grouped_projection_aggregates: &[SqlAggregateCall],
) -> Result<usize, SqlLoweringError> {
let mut matched = grouped_projection_aggregates
.iter()
.enumerate()
.filter_map(|(index, aggregate)| (aggregate == target).then_some(index));
let Some(index) = matched.next() else {
return Err(SqlLoweringError::unsupported_select_having());
};
if matched.next().is_some() {
return Err(SqlLoweringError::unsupported_select_having());
}
Ok(index)
}