use crate::expr::Expr;
use crate::expr::arithmetic::Numeric;
use crate::expr::node::{AggOp, ExprNode};
use crate::expr::window::WindowBuilder;
use crate::model::Model;
use crate::query::field::FieldRef;
use std::marker::PhantomData;
pub(crate) mod sealed {
pub trait Sealed {}
}
pub trait KindEvidence: sealed::Sealed {}
pub struct ValueAgg;
pub struct MetadataAgg;
pub struct OrderedSetAgg;
pub struct HypotheticalSetAgg;
impl sealed::Sealed for ValueAgg {}
impl sealed::Sealed for MetadataAgg {}
impl sealed::Sealed for OrderedSetAgg {}
impl sealed::Sealed for HypotheticalSetAgg {}
impl KindEvidence for ValueAgg {}
impl KindEvidence for MetadataAgg {}
impl KindEvidence for OrderedSetAgg {}
impl KindEvidence for HypotheticalSetAgg {}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub struct AggregateExpr<Out, K = ValueAgg> {
pub(crate) node: ExprNode,
pub(crate) _out: PhantomData<fn() -> Out>,
pub(crate) _kind: PhantomData<fn() -> K>,
}
impl<Out, K> Clone for AggregateExpr<Out, K> {
fn clone(&self) -> Self {
AggregateExpr {
node: self.node.clone(),
_out: PhantomData,
_kind: PhantomData,
}
}
}
impl<Out, K> std::fmt::Debug for AggregateExpr<Out, K> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("AggregateExpr")
.field("node", &self.node)
.finish()
}
}
impl<Out, K: KindEvidence> AggregateExpr<Out, K> {
pub(crate) fn from_node(node: ExprNode) -> Self {
AggregateExpr {
node,
_out: PhantomData,
_kind: PhantomData,
}
}
pub(crate) fn unary_agg(
op: AggOp,
column: &'static str,
cast_to: Option<&'static str>,
) -> Self {
AggregateExpr::from_node(ExprNode::Aggregate {
op,
arg: Box::new(ExprNode::Field { column }),
arg2: None,
filter: None,
cast_to,
distinct: false,
window: None,
order_by: Vec::new(),
within_group_order_by: Vec::new(),
})
}
pub(crate) fn binary_agg(
op: AggOp,
y_column: &'static str,
x_column: &'static str,
cast_to: Option<&'static str>,
) -> Self {
AggregateExpr::from_node(ExprNode::Aggregate {
op,
arg: Box::new(ExprNode::Field { column: y_column }),
arg2: Some(Box::new(ExprNode::Field { column: x_column })),
filter: None,
cast_to,
distinct: false,
window: None,
order_by: Vec::new(),
within_group_order_by: Vec::new(),
})
}
pub(crate) fn ordered_set(
op: AggOp,
arg: ExprNode,
target: crate::query::order::OrderExpr,
cast_to: Option<&'static str>,
) -> Self {
AggregateExpr::from_node(ExprNode::Aggregate {
op,
arg: Box::new(arg),
arg2: None,
filter: None,
cast_to,
distinct: false,
window: None,
order_by: Vec::new(),
within_group_order_by: vec![target],
})
}
}
impl<Out> AggregateExpr<Out, ValueAgg> {
pub fn filter(mut self, cond: Expr<bool>) -> Self {
if let ExprNode::Aggregate { filter, .. } = &mut self.node {
*filter = Some(Box::new(cond.node));
}
self
}
#[must_use = "AggregateExpr is a value — dropping discards the DISTINCT flag"]
pub fn distinct(mut self) -> Self {
if let ExprNode::Aggregate { distinct, .. } = &mut self.node {
*distinct = true;
}
self
}
#[must_use = "AggregateExpr is a value — dropping discards the window spec"]
pub fn over<F>(mut self, f: F) -> Self
where
F: FnOnce(WindowBuilder) -> WindowBuilder,
{
if let ExprNode::Aggregate { window, .. } = &mut self.node {
*window = Some(f(WindowBuilder::new()).build());
}
self
}
#[must_use = "AggregateExpr is a value — dropping discards the ORDER BY"]
pub fn order_by(mut self, ord: crate::query::order::OrderExpr) -> Self {
if let ExprNode::Aggregate { order_by, .. } = &mut self.node {
order_by.push(ord);
}
self
}
}
impl<Out> AggregateExpr<Out, OrderedSetAgg> {
#[must_use = "AggregateExpr is a value — dropping discards the FILTER clause"]
pub fn filter(mut self, cond: Expr<bool>) -> Self {
if let ExprNode::Aggregate { filter, .. } = &mut self.node {
*filter = Some(Box::new(cond.node));
}
self
}
#[must_use = "AggregateExpr is a value — dropping discards the WITHIN GROUP target"]
pub fn within_group_order_by(mut self, target: crate::query::order::OrderExpr) -> Self {
if let ExprNode::Aggregate {
within_group_order_by,
..
} = &mut self.node
{
*within_group_order_by = vec![target];
}
self
}
}
impl<Out> AggregateExpr<Out, HypotheticalSetAgg> {
#[must_use = "AggregateExpr is a value — dropping discards the FILTER clause"]
pub fn filter(mut self, cond: Expr<bool>) -> Self {
if let ExprNode::Aggregate { filter, .. } = &mut self.node {
*filter = Some(Box::new(cond.node));
}
self
}
#[must_use = "AggregateExpr is a value — dropping discards the WITHIN GROUP target"]
pub fn within_group_order_by(mut self, target: crate::query::order::OrderExpr) -> Self {
if let ExprNode::Aggregate {
within_group_order_by,
..
} = &mut self.node
{
*within_group_order_by = vec![target];
}
self
}
}
impl<M: Model, V> FieldRef<M, V> {
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn count(self) -> AggregateExpr<i64> {
AggregateExpr::unary_agg(AggOp::Count, self.column(), None)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn count_star(self) -> AggregateExpr<i64> {
AggregateExpr::unary_agg(AggOp::CountStar, self.column(), None)
}
}
impl<M: Model, V: Numeric> FieldRef<M, V> {
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn sum(self) -> AggregateExpr<V> {
AggregateExpr::unary_agg(AggOp::Sum, self.column(), Some(<V as Numeric>::SUM_CAST))
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn avg(self) -> AggregateExpr<f64> {
AggregateExpr::unary_agg(AggOp::Avg, self.column(), Some(<V as Numeric>::AVG_CAST))
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn stddev_pop(self) -> AggregateExpr<f64> {
AggregateExpr::unary_agg(AggOp::StddevPop, self.column(), Some("DOUBLE PRECISION"))
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn stddev_samp(self) -> AggregateExpr<f64> {
AggregateExpr::unary_agg(AggOp::StddevSamp, self.column(), Some("DOUBLE PRECISION"))
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn stddev(self) -> AggregateExpr<f64> {
AggregateExpr::unary_agg(AggOp::Stddev, self.column(), Some("DOUBLE PRECISION"))
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn var_pop(self) -> AggregateExpr<f64> {
AggregateExpr::unary_agg(AggOp::VarPop, self.column(), Some("DOUBLE PRECISION"))
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn var_samp(self) -> AggregateExpr<f64> {
AggregateExpr::unary_agg(AggOp::VarSamp, self.column(), Some("DOUBLE PRECISION"))
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn variance(self) -> AggregateExpr<f64> {
AggregateExpr::unary_agg(AggOp::Variance, self.column(), Some("DOUBLE PRECISION"))
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn covar_pop<V2: Numeric>(self, x: FieldRef<M, V2>) -> AggregateExpr<f64> {
AggregateExpr::binary_agg(
AggOp::CovarPop,
self.column(),
x.column(),
Some("DOUBLE PRECISION"),
)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn covar_samp<V2: Numeric>(self, x: FieldRef<M, V2>) -> AggregateExpr<f64> {
AggregateExpr::binary_agg(
AggOp::CovarSamp,
self.column(),
x.column(),
Some("DOUBLE PRECISION"),
)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn corr<V2: Numeric>(self, x: FieldRef<M, V2>) -> AggregateExpr<f64> {
AggregateExpr::binary_agg(
AggOp::Corr,
self.column(),
x.column(),
Some("DOUBLE PRECISION"),
)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn regr_avgx<V2: Numeric>(self, x: FieldRef<M, V2>) -> AggregateExpr<f64> {
AggregateExpr::binary_agg(
AggOp::RegrAvgx,
self.column(),
x.column(),
Some("DOUBLE PRECISION"),
)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn regr_avgy<V2: Numeric>(self, x: FieldRef<M, V2>) -> AggregateExpr<f64> {
AggregateExpr::binary_agg(
AggOp::RegrAvgy,
self.column(),
x.column(),
Some("DOUBLE PRECISION"),
)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn regr_count<V2: Numeric>(self, x: FieldRef<M, V2>) -> AggregateExpr<i64> {
AggregateExpr::binary_agg(AggOp::RegrCount, self.column(), x.column(), Some("BIGINT"))
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn regr_intercept<V2: Numeric>(self, x: FieldRef<M, V2>) -> AggregateExpr<f64> {
AggregateExpr::binary_agg(
AggOp::RegrIntercept,
self.column(),
x.column(),
Some("DOUBLE PRECISION"),
)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn regr_r2<V2: Numeric>(self, x: FieldRef<M, V2>) -> AggregateExpr<f64> {
AggregateExpr::binary_agg(
AggOp::RegrR2,
self.column(),
x.column(),
Some("DOUBLE PRECISION"),
)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn regr_slope<V2: Numeric>(self, x: FieldRef<M, V2>) -> AggregateExpr<f64> {
AggregateExpr::binary_agg(
AggOp::RegrSlope,
self.column(),
x.column(),
Some("DOUBLE PRECISION"),
)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn regr_sxx<V2: Numeric>(self, x: FieldRef<M, V2>) -> AggregateExpr<f64> {
AggregateExpr::binary_agg(
AggOp::RegrSxx,
self.column(),
x.column(),
Some("DOUBLE PRECISION"),
)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn regr_sxy<V2: Numeric>(self, x: FieldRef<M, V2>) -> AggregateExpr<f64> {
AggregateExpr::binary_agg(
AggOp::RegrSxy,
self.column(),
x.column(),
Some("DOUBLE PRECISION"),
)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn regr_syy<V2: Numeric>(self, x: FieldRef<M, V2>) -> AggregateExpr<f64> {
AggregateExpr::binary_agg(
AggOp::RegrSyy,
self.column(),
x.column(),
Some("DOUBLE PRECISION"),
)
}
}
impl<M: Model, V> FieldRef<M, V>
where
V: crate::query::field::IntoFilterValue,
{
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn min(self) -> AggregateExpr<V> {
AggregateExpr::unary_agg(AggOp::Min, self.column(), None)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn max(self) -> AggregateExpr<V> {
AggregateExpr::unary_agg(AggOp::Max, self.column(), None)
}
}
impl<M: Model, V> FieldRef<M, V> {
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn array_agg(self) -> AggregateExpr<Vec<V>> {
AggregateExpr::unary_agg(AggOp::ArrayAgg, self.column(), None)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn json_agg(self) -> AggregateExpr<serde_json::Value> {
AggregateExpr::unary_agg(AggOp::JsonAgg, self.column(), None)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn json_object_agg<V2>(self, value: FieldRef<M, V2>) -> AggregateExpr<serde_json::Value> {
AggregateExpr::binary_agg(AggOp::JsonObjectAgg, self.column(), value.column(), None)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn jsonb_object_agg<V2>(self, value: FieldRef<M, V2>) -> AggregateExpr<serde_json::Value> {
AggregateExpr::binary_agg(AggOp::JsonbObjectAgg, self.column(), value.column(), None)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn grouping(self) -> AggregateExpr<i32, MetadataAgg> {
AggregateExpr::unary_agg(AggOp::Grouping, self.column(), None)
}
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn grouping_of(columns: &[&'static str]) -> AggregateExpr<i32, MetadataAgg> {
assert!(
!columns.is_empty(),
"djogi::grouping_of: column list must be non-empty — Postgres rejects GROUPING() with no args"
);
let args: Vec<crate::expr::node::ExprNode> = columns
.iter()
.map(|&col| {
crate::ident::assert_plain_ident(col, "GROUPING column");
crate::expr::node::ExprNode::Field { column: col }
})
.collect();
AggregateExpr::from_node(crate::expr::node::ExprNode::GroupingVariadic { args })
}
impl<M: Model, V: crate::expr::arithmetic::Numeric> FieldRef<M, V> {
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn percentile_cont(self, p: f64) -> AggregateExpr<f64, OrderedSetAgg> {
let target = self.asc();
AggregateExpr::ordered_set(
AggOp::PercentileCont,
ExprNode::Literal(crate::query::condition::FilterValue::F64(p)),
target,
Some("DOUBLE PRECISION"),
)
}
}
impl<M: Model, V> FieldRef<M, V>
where
V: crate::query::field::IntoFilterValue,
{
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn percentile_disc(self, p: f64) -> AggregateExpr<V, OrderedSetAgg> {
let target = self.asc();
AggregateExpr::ordered_set(
AggOp::PercentileDisc,
ExprNode::Literal(crate::query::condition::FilterValue::F64(p)),
target,
None,
)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn mode(self) -> AggregateExpr<V, OrderedSetAgg> {
let target = self.asc();
AggregateExpr::ordered_set(AggOp::Mode, ExprNode::Field { column: "" }, target, None)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn rank_of(self, value: V) -> AggregateExpr<i64, HypotheticalSetAgg> {
let target = self.asc();
let arg = ExprNode::Literal(value.into_filter_value());
AggregateExpr::ordered_set(AggOp::HypotheticalRank, arg, target, Some("BIGINT"))
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn dense_rank_of(self, value: V) -> AggregateExpr<i64, HypotheticalSetAgg> {
let target = self.asc();
let arg = ExprNode::Literal(value.into_filter_value());
AggregateExpr::ordered_set(AggOp::HypotheticalDenseRank, arg, target, Some("BIGINT"))
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn percent_rank_of(self, value: V) -> AggregateExpr<f64, HypotheticalSetAgg> {
let target = self.asc();
let arg = ExprNode::Literal(value.into_filter_value());
AggregateExpr::ordered_set(
AggOp::HypotheticalPercentRank,
arg,
target,
Some("DOUBLE PRECISION"),
)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn cume_dist_of(self, value: V) -> AggregateExpr<f64, HypotheticalSetAgg> {
let target = self.asc();
let arg = ExprNode::Literal(value.into_filter_value());
AggregateExpr::ordered_set(
AggOp::HypotheticalCumeDist,
arg,
target,
Some("DOUBLE PRECISION"),
)
}
}
impl<M: Model> FieldRef<M, String> {
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn string_agg(self, sep: impl Into<String>) -> AggregateExpr<String> {
AggregateExpr::from_node(ExprNode::Aggregate {
op: AggOp::StringAgg(sep.into()),
arg: Box::new(ExprNode::Field {
column: self.column(),
}),
arg2: None,
filter: None,
cast_to: None,
distinct: false,
window: None,
order_by: Vec::new(),
within_group_order_by: Vec::new(),
})
}
}
impl<M: Model> FieldRef<M, bool> {
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn bool_and(self) -> AggregateExpr<bool> {
AggregateExpr::unary_agg(AggOp::BoolAnd, self.column(), None)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn bool_or(self) -> AggregateExpr<bool> {
AggregateExpr::unary_agg(AggOp::BoolOr, self.column(), None)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn every(self) -> AggregateExpr<bool> {
AggregateExpr::unary_agg(AggOp::Every, self.column(), None)
}
}
mod integer_column_seal {
pub trait Sealed {}
impl Sealed for i16 {}
impl Sealed for i32 {}
impl Sealed for i64 {}
}
pub trait IntegerColumn: integer_column_seal::Sealed {}
impl IntegerColumn for i16 {}
impl IntegerColumn for i32 {}
impl IntegerColumn for i64 {}
impl<M: Model, V: IntegerColumn> FieldRef<M, V> {
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn bit_and(self) -> AggregateExpr<V> {
AggregateExpr::unary_agg(AggOp::BitAnd, self.column(), None)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn bit_or(self) -> AggregateExpr<V> {
AggregateExpr::unary_agg(AggOp::BitOr, self.column(), None)
}
#[must_use = "aggregates are lazy — dropping one silently omits the column"]
pub fn bit_xor(self) -> AggregateExpr<V> {
AggregateExpr::unary_agg(AggOp::BitXor, self.column(), None)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::ModelDescriptor;
use crate::expr::sql::emit_expr;
use crate::pg::accumulator::SqlAccumulator;
use crate::query::portable::SqlEmitContext;
struct Txn;
impl crate::model::__sealed::Sealed for Txn {}
#[allow(clippy::manual_async_fn)]
impl crate::model::Model for Txn {
type Pk = i64;
type Fields = ();
fn table_name() -> &'static str {
"txns"
}
fn pk_value(&self) -> &i64 {
unreachable!()
}
fn descriptor() -> &'static ModelDescriptor {
unreachable!()
}
fn get(
_ctx: &mut crate::context::DjogiContext,
_id: i64,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unreachable!() }
}
fn create(
_ctx: &mut crate::context::DjogiContext,
_v: Self,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send {
async { unreachable!() }
}
fn save<'ctx>(
&'ctx mut self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send + 'ctx
{
async { unreachable!() }
}
fn delete(
self,
_ctx: &mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<(), crate::DjogiError>> + Send {
async { unreachable!() }
}
fn refresh_from_db<'ctx>(
&'ctx self,
_ctx: &'ctx mut crate::context::DjogiContext,
) -> impl std::future::Future<Output = Result<Self, crate::DjogiError>> + Send + 'ctx
{
async { unreachable!() }
}
}
#[test]
fn emit_sum_field() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.sum();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = qb.sql();
assert_eq!(sql.trim(), "SUM(amount)", "got: {sql}");
}
#[test]
fn emit_count_field() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.count();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = qb.sql();
assert_eq!(sql.trim(), "COUNT(amount)", "got: {sql}");
}
#[test]
fn emit_count_star() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.count_star();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = qb.sql();
assert_eq!(sql.trim(), "COUNT(*)", "got: {sql}");
}
#[test]
fn emit_avg_field() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.avg();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = qb.sql();
assert_eq!(sql.trim(), "AVG(amount)", "got: {sql}");
}
#[test]
fn emit_min_max_field() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &f.min().node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(qb.sql().trim(), "MIN(amount)");
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &f.max().node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(qb.sql().trim(), "MAX(amount)");
}
#[test]
fn emit_aggregate_with_filter() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let g: FieldRef<Txn, i64> = FieldRef::new("amount");
let cond = f.as_expr().lt(Expr::literal(0i64));
let agg = g.count().filter(cond);
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = qb.sql();
assert!(
sql.contains("COUNT(amount) FILTER (WHERE amount < $1)"),
"got: {sql}"
);
}
#[test]
fn filter_overwrites_previous_filter() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let g: FieldRef<Txn, i64> = FieldRef::new("amount");
let a = f.as_expr().lt(Expr::literal(0i64));
let b = g.as_expr().gt(Expr::literal(100i64));
let h: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = h.count().filter(a).filter(b);
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = qb.sql();
assert!(sql.contains("amount > $1"), "got: {sql}");
assert!(
!sql.contains("amount < "),
"first filter should be gone: {sql}"
);
}
#[test]
fn emit_array_agg() {
let f: FieldRef<Txn, String> = FieldRef::new("tag");
let agg = f.array_agg();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = qb.sql();
assert_eq!(sql.trim(), "ARRAY_AGG(tag)", "got: {sql}");
}
#[test]
fn emit_json_agg() {
let f: FieldRef<Txn, String> = FieldRef::new("tag");
let agg = f.json_agg();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = qb.sql();
assert_eq!(sql.trim(), "JSONB_AGG(tag)", "got: {sql}");
}
#[test]
fn emit_string_agg_binds_separator() {
let f: FieldRef<Txn, String> = FieldRef::new("tag");
let agg = f.string_agg(", ");
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = qb.sql();
assert!(sql.contains("STRING_AGG(tag, $1)"), "got: {sql}");
}
#[test]
fn emit_bool_and() {
let f: FieldRef<Txn, bool> = FieldRef::new("active");
let agg = f.bool_and();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = qb.sql();
assert_eq!(sql.trim(), "BOOL_AND(active)", "got: {sql}");
}
#[test]
fn emit_bool_or() {
let f: FieldRef<Txn, bool> = FieldRef::new("active");
let agg = f.bool_or();
let mut qb = SqlAccumulator::new("");
emit_expr(&mut qb, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = qb.sql();
assert_eq!(sql.trim(), "BOOL_OR(active)", "got: {sql}");
}
#[test]
fn sum_distinct_emits_sum_distinct() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.sum().distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert!(
acc.sql().contains("SUM(DISTINCT amount)"),
"got: {}",
acc.sql()
);
}
#[test]
fn count_distinct_emits_count_distinct() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.count().distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert!(
acc.sql().contains("COUNT(DISTINCT amount)"),
"got: {}",
acc.sql()
);
}
#[test]
fn avg_distinct_emits_avg_distinct() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.avg().distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert!(
acc.sql().contains("AVG(DISTINCT amount)"),
"got: {}",
acc.sql()
);
}
#[test]
fn min_distinct_emits_min_distinct() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.min().distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert!(
acc.sql().contains("MIN(DISTINCT amount)"),
"got: {}",
acc.sql()
);
}
#[test]
fn max_distinct_emits_max_distinct() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.max().distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert!(
acc.sql().contains("MAX(DISTINCT amount)"),
"got: {}",
acc.sql()
);
}
#[test]
fn array_agg_distinct_emits_array_agg_distinct() {
let f: FieldRef<Txn, String> = FieldRef::new("tag");
let agg = f.array_agg().distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert!(
acc.sql().contains("ARRAY_AGG(DISTINCT tag)"),
"got: {}",
acc.sql()
);
}
#[test]
fn json_agg_distinct_emits_jsonb_agg_distinct() {
let f: FieldRef<Txn, String> = FieldRef::new("tag");
let agg = f.json_agg().distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert!(
acc.sql().contains("JSONB_AGG(DISTINCT tag)"),
"got: {}",
acc.sql()
);
}
#[test]
fn bool_and_distinct_emits_bool_and_distinct() {
let f: FieldRef<Txn, bool> = FieldRef::new("active");
let agg = f.bool_and().distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert!(
acc.sql().contains("BOOL_AND(DISTINCT active)"),
"got: {}",
acc.sql()
);
}
#[test]
fn bool_or_distinct_emits_bool_or_distinct() {
let f: FieldRef<Txn, bool> = FieldRef::new("active");
let agg = f.bool_or().distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert!(
acc.sql().contains("BOOL_OR(DISTINCT active)"),
"got: {}",
acc.sql()
);
}
#[test]
fn count_star_distinct_rejected_at_fetch() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let mut agg = f.count_star();
if let ExprNode::Aggregate {
ref mut distinct, ..
} = agg.node
{
*distinct = true;
}
let result = crate::expr::sql::check_aggregate_legality(&agg.node);
assert!(
result.is_err(),
"expected UnsupportedAggregate error for COUNT(DISTINCT *)"
);
let err = result.unwrap_err();
assert!(
matches!(err, crate::DjogiError::UnsupportedAggregate { .. }),
"expected UnsupportedAggregate variant, got: {err:?}"
);
}
#[test]
fn string_agg_distinct_without_order_by_rejected_at_fetch() {
let f: FieldRef<Txn, String> = FieldRef::new("tag");
let mut agg = f.string_agg(", ");
if let ExprNode::Aggregate {
ref mut distinct, ..
} = agg.node
{
*distinct = true;
}
let result = crate::expr::sql::check_aggregate_legality(&agg.node);
assert!(
result.is_err(),
"expected UnsupportedAggregate error for STRING_AGG(DISTINCT ...) with no ORDER BY"
);
let err = result.unwrap_err();
assert!(
matches!(err, crate::DjogiError::UnsupportedAggregate { .. }),
"expected UnsupportedAggregate variant, got: {err:?}"
);
}
#[test]
fn count_star_with_order_by_rejected_at_fetch() {
let f: FieldRef<Txn, i64> = FieldRef::new("id");
let agg = f.count_star().order_by(f.asc());
let result = crate::expr::sql::check_aggregate_legality(&agg.node);
assert!(
result.is_err(),
"expected UnsupportedAggregate error for COUNT(*) with ORDER BY"
);
let err = result.unwrap_err();
assert!(
matches!(err, crate::DjogiError::UnsupportedAggregate { .. }),
"expected UnsupportedAggregate variant, got: {err:?}"
);
}
#[test]
fn grouping_bare_accepted_at_fetch() {
let f: FieldRef<Txn, i64> = FieldRef::new("region_id");
let agg = f.grouping();
let result = crate::expr::sql::check_aggregate_legality(&agg.node);
assert!(
result.is_ok(),
"bare GROUPING(col) should pass legality, got: {result:?}"
);
}
#[test]
fn order_by_appends_to_aggregate_node() {
let f: FieldRef<Txn, i64> = FieldRef::new("id");
let agg = f.array_agg().order_by(f.asc());
if let ExprNode::Aggregate { order_by, .. } = &agg.node {
assert_eq!(
order_by.len(),
1,
"single .order_by call should append exactly one OrderExpr"
);
} else {
panic!("AggregateExpr did not wrap an Aggregate node");
}
}
#[test]
fn multiple_order_by_calls_append_keys() {
let f: FieldRef<Txn, i64> = FieldRef::new("id");
let g: FieldRef<Txn, i64> = FieldRef::new("rank");
let agg = f.array_agg().order_by(g.desc()).order_by(f.asc());
if let ExprNode::Aggregate { order_by, .. } = &agg.node {
assert_eq!(
order_by.len(),
2,
"chained .order_by calls should append in order"
);
} else {
panic!("AggregateExpr did not wrap an Aggregate node");
}
}
#[test]
fn array_agg_with_order_by_emits_clause() {
let f: FieldRef<Txn, i64> = FieldRef::new("id");
let agg = f.array_agg().order_by(f.asc());
let mut acc = SqlAccumulator::new("");
crate::expr::sql::emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.contains("ARRAY_AGG(id ORDER BY id ASC"),
"expected ARRAY_AGG with ORDER BY clause, got: {sql}"
);
assert!(sql.ends_with(')'), "aggregate must close its parens: {sql}");
}
#[test]
fn array_agg_distinct_with_order_by_emits_distinct_and_order_by() {
let f: FieldRef<Txn, i64> = FieldRef::new("id");
let agg = f.array_agg().distinct().order_by(f.asc());
let mut acc = SqlAccumulator::new("");
crate::expr::sql::emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.contains("ARRAY_AGG(DISTINCT id ORDER BY id ASC"),
"expected ARRAY_AGG(DISTINCT ... ORDER BY ...), got: {sql}"
);
}
#[test]
fn multiple_order_by_keys_emit_comma_separated() {
let f: FieldRef<Txn, i64> = FieldRef::new("id");
let g: FieldRef<Txn, i64> = FieldRef::new("rank");
let agg = f.array_agg().order_by(g.desc()).order_by(f.asc());
let mut acc = SqlAccumulator::new("");
crate::expr::sql::emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.contains("ARRAY_AGG(id ORDER BY rank DESC, id ASC"),
"expected multi-key ORDER BY with comma separator, got: {sql}"
);
}
#[test]
fn string_agg_with_order_by_emits_after_separator() {
let f: FieldRef<Txn, String> = FieldRef::new("name");
let g: FieldRef<Txn, i64> = FieldRef::new("rank");
let agg = f.string_agg(", ").order_by(g.asc());
let mut acc = SqlAccumulator::new("");
crate::expr::sql::emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.starts_with("STRING_AGG(name, $") && sql.contains(" ORDER BY rank ASC"),
"expected STRING_AGG(col, $sep ORDER BY ...), got: {sql}"
);
}
#[test]
fn string_agg_distinct_with_order_by_is_now_accepted() {
let f: FieldRef<Txn, String> = FieldRef::new("tag");
let g: FieldRef<Txn, i64> = FieldRef::new("rank");
let agg = f.string_agg(", ").distinct().order_by(g.asc());
let result = crate::expr::sql::check_aggregate_legality(&agg.node);
assert!(
result.is_ok(),
"STRING_AGG(DISTINCT ... ORDER BY ...) should be accepted, got: {result:?}"
);
}
#[test]
fn string_agg_distinct_with_order_by_emits_correct_sql() {
let f: FieldRef<Txn, String> = FieldRef::new("name");
let g: FieldRef<Txn, i64> = FieldRef::new("rank");
let agg = f.string_agg(", ").distinct().order_by(g.asc());
let mut acc = SqlAccumulator::new("");
crate::expr::sql::emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.starts_with("STRING_AGG(DISTINCT name, $") && sql.contains(" ORDER BY rank ASC"),
"expected STRING_AGG(DISTINCT col, $sep ORDER BY ...), got: {sql}"
);
}
#[test]
fn order_by_with_filter_and_distinct_compose() {
let f: FieldRef<Txn, i64> = FieldRef::new("id");
let g: FieldRef<Txn, i64> = FieldRef::new("rank");
let agg = f
.array_agg()
.distinct()
.filter(g.as_expr().gt(Expr::literal(0i64)))
.order_by(f.asc());
let mut acc = SqlAccumulator::new("");
crate::expr::sql::emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.contains("ARRAY_AGG(DISTINCT id ORDER BY id ASC")
&& sql.contains(") FILTER (WHERE rank > "),
"expected DISTINCT + ORDER BY + FILTER composition, got: {sql}"
);
}
#[test]
fn over_empty_closure_stores_window_spec() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.sum().over(|w| w);
if let ExprNode::Aggregate { window, .. } = &agg.node {
assert!(
window.is_some(),
"over(|w| w) should set window to Some(..)"
);
} else {
panic!("AggregateExpr did not wrap an Aggregate node");
}
}
#[test]
fn over_empty_closure_emits_over_parens_via_terminal() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.sum().over(|w| w);
let mut acc = SqlAccumulator::new("");
crate::query::sql::emit_aggregate_with_window_and_cast(&mut acc, &agg.node)
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.contains("SUM(amount) OVER ()"),
"expected OVER () from empty window spec, got: {sql}"
);
}
#[test]
fn over_with_partition_emits_partition_clause_via_terminal() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let p: FieldRef<Txn, i64> = FieldRef::new("org_id");
let agg = f.sum().over(|w| w.partition_by(p));
let mut acc = SqlAccumulator::new("");
crate::query::sql::emit_aggregate_with_window_and_cast(&mut acc, &agg.node)
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(sql.contains("OVER (PARTITION BY org_id)"), "got: {sql}");
}
#[test]
fn over_with_order_by_emits_order_clause_via_terminal() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let o: FieldRef<Txn, i64> = FieldRef::new("created_at");
let agg = f.count().over(|w| w.order_by(o));
let mut acc = SqlAccumulator::new("");
crate::query::sql::emit_aggregate_with_window_and_cast(&mut acc, &agg.node)
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(sql.contains("OVER (ORDER BY created_at ASC)"), "got: {sql}");
}
#[test]
fn over_with_rows_frame_emits_frame_clause_via_terminal() {
use crate::expr::window::FrameBound;
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let o: FieldRef<Txn, i64> = FieldRef::new("created_at");
let agg = f.sum().over(|w| {
w.order_by(o)
.rows(FrameBound::Preceding(3), FrameBound::CurrentRow)
});
let mut acc = SqlAccumulator::new("");
crate::query::sql::emit_aggregate_with_window_and_cast(&mut acc, &agg.node)
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.contains("ROWS BETWEEN $1 PRECEDING AND CURRENT ROW"),
"got: {sql}"
);
}
#[test]
fn over_replaces_previous_window_spec_last_call_wins() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let p1: FieldRef<Txn, i64> = FieldRef::new("org_id");
let p2: FieldRef<Txn, i64> = FieldRef::new("dept_id");
let agg = f
.sum()
.over(|w| w.partition_by(p1))
.over(|w| w.partition_by(p2));
let mut acc = SqlAccumulator::new("");
crate::query::sql::emit_aggregate_with_window_and_cast(&mut acc, &agg.node)
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(sql.contains("PARTITION BY dept_id"), "got: {sql}");
assert!(!sql.contains("org_id"), "first spec should be gone: {sql}");
}
#[test]
fn no_over_call_preserves_default_over_empty_via_terminal() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.count();
let mut acc = SqlAccumulator::new("");
crate::query::sql::emit_aggregate_with_window_and_cast(&mut acc, &agg.node)
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.contains("COUNT(amount) OVER ()"),
"default should be OVER (), got: {sql}"
);
}
#[test]
fn bit_and_emits_bit_and_keyword() {
let f: FieldRef<Txn, i32> = FieldRef::new("flags");
let agg = f.bit_and();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "BIT_AND(flags)");
}
#[test]
fn bit_or_emits_bit_or_keyword() {
let f: FieldRef<Txn, i32> = FieldRef::new("flags");
let agg = f.bit_or();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "BIT_OR(flags)");
}
#[test]
fn bit_xor_emits_bit_xor_keyword() {
let f: FieldRef<Txn, i64> = FieldRef::new("checksum_part");
let agg = f.bit_xor();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "BIT_XOR(checksum_part)");
}
#[test]
fn bit_aggregates_compose_with_distinct() {
let f: FieldRef<Txn, i32> = FieldRef::new("flags");
let agg = f.bit_and().distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "BIT_AND(DISTINCT flags)");
}
#[test]
fn bit_aggregates_compose_with_filter() {
let f: FieldRef<Txn, i32> = FieldRef::new("flags");
let g: FieldRef<Txn, i64> = FieldRef::new("active_at");
let agg = f.bit_or().filter(g.as_expr().gt(Expr::literal(0i64)));
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.contains("BIT_OR(flags)") && sql.contains(" FILTER (WHERE active_at > "),
"expected BIT_OR + FILTER composition, got: {sql}"
);
}
#[test]
fn bit_aggregates_compose_with_order_by() {
let f: FieldRef<Txn, i32> = FieldRef::new("flags");
let agg = f.bit_and().distinct().order_by(f.asc());
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "BIT_AND(DISTINCT flags ORDER BY flags ASC)");
}
#[test]
fn grouping_emits_grouping_keyword() {
let f: FieldRef<Txn, String> = FieldRef::new("region");
let agg = f.grouping();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "GROUPING(region)");
}
#[test]
fn grouping_returns_i32_aggregate() {
let f: FieldRef<Txn, String> = FieldRef::new("region");
let _: AggregateExpr<i32, MetadataAgg> = f.grouping();
}
#[test]
fn grouping_works_on_any_column_type() {
let f_str: FieldRef<Txn, String> = FieldRef::new("region");
let f_i64: FieldRef<Txn, i64> = FieldRef::new("dept_id");
let f_bool: FieldRef<Txn, bool> = FieldRef::new("is_active");
let _: AggregateExpr<i32, MetadataAgg> = f_str.grouping();
let _: AggregateExpr<i32, MetadataAgg> = f_i64.grouping();
let _: AggregateExpr<i32, MetadataAgg> = f_bool.grouping();
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "GROUPING aggregate must not carry modifiers")]
fn direct_ir_grouping_distinct_debug_asserts() {
let f: FieldRef<Txn, String> = FieldRef::new("region");
let mut agg = f.grouping();
if let ExprNode::Aggregate {
ref mut distinct, ..
} = agg.node
{
*distinct = true;
}
let _ = crate::expr::sql::check_aggregate_legality(&agg.node);
}
#[test]
fn grouping_of_two_args_emits_comma_separated() {
let agg = crate::grouping_of(&["region", "dept"]);
match &agg.node {
crate::expr::node::ExprNode::GroupingVariadic { args } => {
assert_eq!(args.len(), 2);
for (a, expected) in args.iter().zip(["region", "dept"]) {
match a {
crate::expr::node::ExprNode::Field { column } => {
assert_eq!(*column, expected)
}
_ => panic!("expected Field, got {a:?}"),
}
}
}
other => panic!("expected GroupingVariadic, got {other:?}"),
}
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root()).expect("emit");
assert_eq!(acc.sql(), "GROUPING(region, dept)");
}
#[test]
fn grouping_of_three_args_emits_comma_separated() {
let agg = crate::grouping_of(&["region", "dept", "product"]);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root()).expect("emit");
assert_eq!(acc.sql(), "GROUPING(region, dept, product)");
}
#[test]
#[should_panic(expected = "GROUPING() with no args")]
fn grouping_of_empty_panics() {
let _ = crate::grouping_of(&[]);
}
#[test]
fn grouping_of_legality_accepted() {
let agg = crate::grouping_of(&["region", "dept"]);
assert!(crate::expr::sql::check_aggregate_legality(&agg.node).is_ok());
}
#[test]
fn json_object_agg_emits_json_object_agg_key_value() {
let f_key: FieldRef<Txn, i64> = FieldRef::new("id");
let f_val: FieldRef<Txn, String> = FieldRef::new("name");
let agg = f_key.json_object_agg(f_val);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "JSON_OBJECT_AGG(id, name)");
}
#[test]
fn jsonb_object_agg_emits_jsonb_object_agg_key_value() {
let f_key: FieldRef<Txn, i64> = FieldRef::new("id");
let f_val: FieldRef<Txn, String> = FieldRef::new("name");
let agg = f_key.jsonb_object_agg(f_val);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "JSONB_OBJECT_AGG(id, name)");
}
#[test]
fn json_object_agg_key_first_value_second() {
let f_key: FieldRef<Txn, String> = FieldRef::new("k");
let f_val: FieldRef<Txn, i64> = FieldRef::new("v");
let agg = f_key.jsonb_object_agg(f_val);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "JSONB_OBJECT_AGG(k, v)");
}
#[test]
fn json_object_agg_alias_pair_emit_distinct_keywords() {
let f_k1: FieldRef<Txn, i64> = FieldRef::new("k");
let f_v1: FieldRef<Txn, String> = FieldRef::new("v");
let f_k2: FieldRef<Txn, i64> = FieldRef::new("k");
let f_v2: FieldRef<Txn, String> = FieldRef::new("v");
let mut acc1 = SqlAccumulator::new("");
let mut acc2 = SqlAccumulator::new("");
emit_expr(
&mut acc1,
&f_k1.json_object_agg(f_v1).node,
SqlEmitContext::root(),
)
.expect("aggregate expression should lower to SQL");
emit_expr(
&mut acc2,
&f_k2.jsonb_object_agg(f_v2).node,
SqlEmitContext::root(),
)
.expect("aggregate expression should lower to SQL");
assert_eq!(acc1.sql(), "JSON_OBJECT_AGG(k, v)");
assert_eq!(acc2.sql(), "JSONB_OBJECT_AGG(k, v)");
assert_ne!(acc1.sql(), acc2.sql());
}
#[test]
fn json_object_agg_returns_serde_value() {
let f_k: FieldRef<Txn, i64> = FieldRef::new("k");
let f_v: FieldRef<Txn, String> = FieldRef::new("v");
let _: AggregateExpr<serde_json::Value> = f_k.json_object_agg(f_v);
let f_k2: FieldRef<Txn, String> = FieldRef::new("k");
let f_v2: FieldRef<Txn, i64> = FieldRef::new("v");
let _: AggregateExpr<serde_json::Value> = f_k2.jsonb_object_agg(f_v2);
}
#[test]
fn regr_slope_emits_regr_slope_y_x() {
let f_y: FieldRef<Txn, f64> = FieldRef::new("price");
let f_x: FieldRef<Txn, f64> = FieldRef::new("hours");
let agg = f_y.regr_slope(f_x);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "REGR_SLOPE(price, hours)");
}
#[test]
fn regr_intercept_emits_regr_intercept_y_x() {
let f_y: FieldRef<Txn, f64> = FieldRef::new("price");
let f_x: FieldRef<Txn, f64> = FieldRef::new("hours");
let agg = f_y.regr_intercept(f_x);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "REGR_INTERCEPT(price, hours)");
}
#[test]
fn regr_r2_emits_regr_r2_y_x() {
let f_y: FieldRef<Txn, f64> = FieldRef::new("price");
let f_x: FieldRef<Txn, f64> = FieldRef::new("hours");
let agg = f_y.regr_r2(f_x);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "REGR_R2(price, hours)");
}
#[test]
fn regr_count_emits_regr_count_y_x() {
let f_y: FieldRef<Txn, f64> = FieldRef::new("price");
let f_x: FieldRef<Txn, f64> = FieldRef::new("hours");
let agg = f_y.regr_count(f_x);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "REGR_COUNT(price, hours)");
}
#[test]
fn regr_count_returns_i64_aggregate() {
let f_y: FieldRef<Txn, f64> = FieldRef::new("y");
let f_x: FieldRef<Txn, f64> = FieldRef::new("x");
let _: AggregateExpr<i64> = f_y.regr_count(f_x);
}
#[test]
fn regr_avgx_avgy_emit_distinct_keywords() {
let f_y: FieldRef<Txn, f64> = FieldRef::new("y");
let f_x: FieldRef<Txn, f64> = FieldRef::new("x");
let mut acc1 = SqlAccumulator::new("");
emit_expr(&mut acc1, &f_y.regr_avgx(f_x).node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc1.sql(), "REGR_AVGX(y, x)");
let f_y2: FieldRef<Txn, f64> = FieldRef::new("y");
let f_x2: FieldRef<Txn, f64> = FieldRef::new("x");
let mut acc2 = SqlAccumulator::new("");
emit_expr(
&mut acc2,
&f_y2.regr_avgy(f_x2).node,
SqlEmitContext::root(),
)
.expect("aggregate expression should lower to SQL");
assert_eq!(acc2.sql(), "REGR_AVGY(y, x)");
}
#[test]
fn regr_sxx_sxy_syy_emit_distinct_keywords() {
let f_y: FieldRef<Txn, f64> = FieldRef::new("y");
let f_x: FieldRef<Txn, f64> = FieldRef::new("x");
let mut acc1 = SqlAccumulator::new("");
emit_expr(&mut acc1, &f_y.regr_sxx(f_x).node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc1.sql(), "REGR_SXX(y, x)");
let f_y2: FieldRef<Txn, f64> = FieldRef::new("y");
let f_x2: FieldRef<Txn, f64> = FieldRef::new("x");
let mut acc2 = SqlAccumulator::new("");
emit_expr(&mut acc2, &f_y2.regr_sxy(f_x2).node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc2.sql(), "REGR_SXY(y, x)");
let f_y3: FieldRef<Txn, f64> = FieldRef::new("y");
let f_x3: FieldRef<Txn, f64> = FieldRef::new("x");
let mut acc3 = SqlAccumulator::new("");
emit_expr(&mut acc3, &f_y3.regr_syy(f_x3).node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc3.sql(), "REGR_SYY(y, x)");
}
#[test]
fn regr_family_non_count_returns_f64() {
let f_y: FieldRef<Txn, i64> = FieldRef::new("y");
let f_x_i32: FieldRef<Txn, i32> = FieldRef::new("x");
let f_x_f64: FieldRef<Txn, f64> = FieldRef::new("x");
let _: AggregateExpr<f64> = f_y.regr_avgx(f_x_i32);
let _: AggregateExpr<f64> = FieldRef::<Txn, i64>::new("y").regr_avgy(f_x_f64);
let _: AggregateExpr<f64> =
FieldRef::<Txn, f32>::new("y").regr_intercept(FieldRef::<Txn, f64>::new("x"));
let _: AggregateExpr<f64> =
FieldRef::<Txn, f32>::new("y").regr_r2(FieldRef::<Txn, f64>::new("x"));
let _: AggregateExpr<f64> =
FieldRef::<Txn, f32>::new("y").regr_slope(FieldRef::<Txn, f64>::new("x"));
let _: AggregateExpr<f64> =
FieldRef::<Txn, f32>::new("y").regr_sxx(FieldRef::<Txn, f64>::new("x"));
let _: AggregateExpr<f64> =
FieldRef::<Txn, f32>::new("y").regr_sxy(FieldRef::<Txn, f64>::new("x"));
let _: AggregateExpr<f64> =
FieldRef::<Txn, f32>::new("y").regr_syy(FieldRef::<Txn, f64>::new("x"));
}
#[test]
fn covar_pop_emits_covar_pop_y_x() {
let f_y: FieldRef<Txn, f64> = FieldRef::new("revenue");
let f_x: FieldRef<Txn, f64> = FieldRef::new("cost");
let agg = f_y.covar_pop(f_x);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "COVAR_POP(revenue, cost)");
}
#[test]
fn covar_samp_emits_covar_samp_y_x() {
let f_y: FieldRef<Txn, i64> = FieldRef::new("revenue");
let f_x: FieldRef<Txn, i64> = FieldRef::new("cost");
let agg = f_y.covar_samp(f_x);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "COVAR_SAMP(revenue, cost)");
}
#[test]
fn corr_emits_corr_y_x() {
let f_y: FieldRef<Txn, f64> = FieldRef::new("score_a");
let f_x: FieldRef<Txn, f64> = FieldRef::new("score_b");
let agg = f_y.corr(f_x);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "CORR(score_a, score_b)");
}
#[test]
fn covar_pop_composes_with_distinct() {
let f_y: FieldRef<Txn, f64> = FieldRef::new("revenue");
let f_x: FieldRef<Txn, f64> = FieldRef::new("cost");
let agg = f_y.covar_pop(f_x).distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "COVAR_POP(DISTINCT revenue, cost)");
}
#[test]
fn binary_aggregates_y_first_x_second_argument_order() {
let f_a: FieldRef<Txn, f64> = FieldRef::new("a_col");
let f_b: FieldRef<Txn, f64> = FieldRef::new("b_col");
let agg_ab = f_a.covar_pop(f_b);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg_ab.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "COVAR_POP(a_col, b_col)");
let f_a2: FieldRef<Txn, f64> = FieldRef::new("a_col");
let f_b2: FieldRef<Txn, f64> = FieldRef::new("b_col");
let agg_ba = f_b2.covar_pop(f_a2);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg_ba.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "COVAR_POP(b_col, a_col)");
}
#[test]
fn binary_aggregates_return_f64() {
let f_y_i64: FieldRef<Txn, i64> = FieldRef::new("y");
let f_x_i32: FieldRef<Txn, i32> = FieldRef::new("x");
let f_x_f64: FieldRef<Txn, f64> = FieldRef::new("x_f64");
let f_x_i64: FieldRef<Txn, i64> = FieldRef::new("x");
let _: AggregateExpr<f64> = f_y_i64.covar_pop(f_x_i32);
let _: AggregateExpr<f64> = FieldRef::<Txn, i64>::new("y").covar_samp(f_x_f64);
let _: AggregateExpr<f64> = FieldRef::<Txn, i64>::new("y").corr(f_x_i64);
}
#[test]
fn unary_aggregates_have_no_arg2_after_t5_infrastructure() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.sum();
if let ExprNode::Aggregate { arg2, .. } = &agg.node {
assert!(
arg2.is_none(),
"unary aggregates must leave arg2 empty after the T5 IR change"
);
} else {
panic!("AggregateExpr did not wrap an Aggregate node");
}
let f_s: FieldRef<Txn, String> = FieldRef::new("tag");
let agg_s = f_s.string_agg(", ");
if let ExprNode::Aggregate { arg2, .. } = &agg_s.node {
assert!(
arg2.is_none(),
"STRING_AGG carries its separator inline; arg2 must stay empty"
);
} else {
panic!("AggregateExpr did not wrap an Aggregate node");
}
}
#[test]
fn stddev_pop_emits_stddev_pop() {
let f: FieldRef<Txn, f64> = FieldRef::new("score");
let agg = f.stddev_pop();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "STDDEV_POP(score)");
}
#[test]
fn stddev_samp_emits_stddev_samp() {
let f: FieldRef<Txn, f64> = FieldRef::new("score");
let agg = f.stddev_samp();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "STDDEV_SAMP(score)");
}
#[test]
fn stddev_alias_emits_bare_stddev() {
let f: FieldRef<Txn, f64> = FieldRef::new("score");
let agg = f.stddev();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "STDDEV(score)");
}
#[test]
fn var_pop_emits_var_pop() {
let f: FieldRef<Txn, i64> = FieldRef::new("score");
let agg = f.var_pop();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "VAR_POP(score)");
}
#[test]
fn var_samp_emits_var_samp() {
let f: FieldRef<Txn, i64> = FieldRef::new("score");
let agg = f.var_samp();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "VAR_SAMP(score)");
}
#[test]
fn variance_alias_emits_bare_variance() {
let f: FieldRef<Txn, i64> = FieldRef::new("score");
let agg = f.variance();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "VARIANCE(score)");
}
#[test]
fn stats_aggregates_compose_with_distinct() {
let f: FieldRef<Txn, f64> = FieldRef::new("score");
let agg = f.stddev_pop().distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "STDDEV_POP(DISTINCT score)");
}
#[test]
fn stddev_alias_pair_emit_distinct_keywords() {
let f1: FieldRef<Txn, f64> = FieldRef::new("score");
let f2: FieldRef<Txn, f64> = FieldRef::new("score");
let mut acc1 = SqlAccumulator::new("");
let mut acc2 = SqlAccumulator::new("");
emit_expr(&mut acc1, &f1.stddev_samp().node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
emit_expr(&mut acc2, &f2.stddev().node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc1.sql(), "STDDEV_SAMP(score)");
assert_eq!(acc2.sql(), "STDDEV(score)");
assert_ne!(acc1.sql(), acc2.sql());
}
#[test]
fn variance_alias_pair_emit_distinct_keywords() {
let f1: FieldRef<Txn, i64> = FieldRef::new("score");
let f2: FieldRef<Txn, i64> = FieldRef::new("score");
let mut acc1 = SqlAccumulator::new("");
let mut acc2 = SqlAccumulator::new("");
emit_expr(&mut acc1, &f1.var_samp().node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
emit_expr(&mut acc2, &f2.variance().node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc1.sql(), "VAR_SAMP(score)");
assert_eq!(acc2.sql(), "VARIANCE(score)");
assert_ne!(acc1.sql(), acc2.sql());
}
#[test]
fn stats_aggregates_return_f64() {
let f_i16: FieldRef<Txn, i16> = FieldRef::new("score16");
let f_i32: FieldRef<Txn, i32> = FieldRef::new("score32");
let f_i64: FieldRef<Txn, i64> = FieldRef::new("score64");
let f_f32: FieldRef<Txn, f32> = FieldRef::new("score_f32");
let f_f64: FieldRef<Txn, f64> = FieldRef::new("score_f64");
let _: AggregateExpr<f64> = f_i16.stddev_pop();
let _: AggregateExpr<f64> = f_i32.stddev_samp();
let _: AggregateExpr<f64> = f_i64.stddev();
let _: AggregateExpr<f64> = f_f32.var_pop();
let _: AggregateExpr<f64> = f_f64.var_samp();
let _: AggregateExpr<f64> = f_i32.variance();
}
#[test]
fn every_emits_every_keyword() {
let f: FieldRef<Txn, bool> = FieldRef::new("active");
let agg = f.every();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "EVERY(active)");
}
#[test]
fn every_distinct_emits_every_distinct() {
let f: FieldRef<Txn, bool> = FieldRef::new("active");
let agg = f.every().distinct();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "EVERY(DISTINCT active)");
}
#[test]
fn every_returns_bool_aggregate() {
let f: FieldRef<Txn, bool> = FieldRef::new("active");
let _: AggregateExpr<bool> = f.every();
}
#[test]
fn bit_and_returns_field_value_type() {
let f16: FieldRef<Txn, i16> = FieldRef::new("flags16");
let f32: FieldRef<Txn, i32> = FieldRef::new("flags32");
let f64: FieldRef<Txn, i64> = FieldRef::new("flags64");
let _: AggregateExpr<i16> = f16.bit_and();
let _: AggregateExpr<i32> = f32.bit_or();
let _: AggregateExpr<i64> = f64.bit_xor();
}
#[test]
fn percentile_cont_emits_within_group() {
let f: FieldRef<Txn, f64> = FieldRef::new("ms");
let agg = f.percentile_cont(0.5);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.starts_with("PERCENTILE_CONT($")
&& sql.contains(") WITHIN GROUP (ORDER BY ms ASC)"),
"expected PERCENTILE_CONT($n) WITHIN GROUP (ORDER BY ms ASC), got: {sql}"
);
}
#[test]
fn percentile_disc_emits_within_group() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.percentile_disc(0.95);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.starts_with("PERCENTILE_DISC($")
&& sql.contains(") WITHIN GROUP (ORDER BY amount ASC)"),
"got: {sql}"
);
}
#[test]
fn mode_emits_within_group_no_args() {
let f: FieldRef<Txn, String> = FieldRef::new("category");
let agg = f.mode();
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
assert_eq!(acc.sql(), "MODE() WITHIN GROUP (ORDER BY category ASC)");
}
#[test]
fn percentile_cont_returns_f64_regardless_of_column_type() {
let f_i16: FieldRef<Txn, i16> = FieldRef::new("a");
let f_i64: FieldRef<Txn, i64> = FieldRef::new("b");
let f_f64: FieldRef<Txn, f64> = FieldRef::new("c");
let _: AggregateExpr<f64, OrderedSetAgg> = f_i16.percentile_cont(0.5);
let _: AggregateExpr<f64, OrderedSetAgg> = f_i64.percentile_cont(0.5);
let _: AggregateExpr<f64, OrderedSetAgg> = f_f64.percentile_cont(0.5);
}
#[test]
fn percentile_disc_returns_column_type() {
let f_i64: FieldRef<Txn, i64> = FieldRef::new("amount");
let f_str: FieldRef<Txn, String> = FieldRef::new("category");
let _: AggregateExpr<i64, OrderedSetAgg> = f_i64.percentile_disc(0.5);
let _: AggregateExpr<String, OrderedSetAgg> = f_str.percentile_disc(0.5);
}
#[test]
fn mode_returns_column_type() {
let f_i64: FieldRef<Txn, i64> = FieldRef::new("amount");
let f_str: FieldRef<Txn, String> = FieldRef::new("category");
let _: AggregateExpr<i64, OrderedSetAgg> = f_i64.mode();
let _: AggregateExpr<String, OrderedSetAgg> = f_str.mode();
}
#[test]
fn within_group_order_by_overrides_default_target() {
let f: FieldRef<Txn, f64> = FieldRef::new("score");
let other: FieldRef<Txn, f64> = FieldRef::new("response_time_ms");
let agg = f.percentile_cont(0.95).within_group_order_by(other.desc());
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.contains(") WITHIN GROUP (ORDER BY response_time_ms DESC)"),
"expected DESC override on same-type compatible column, got: {sql}"
);
}
#[test]
fn mode_with_filter_accepted_at_fetch() {
let f: FieldRef<Txn, String> = FieldRef::new("category");
let g: FieldRef<Txn, i64> = FieldRef::new("active");
let agg = f.mode().filter(g.as_expr().gt(Expr::literal(0i64)));
let result = crate::expr::sql::check_aggregate_legality(&agg.node);
assert!(
result.is_ok(),
"MODE with FILTER should pass legality, got: {result:?}"
);
}
#[test]
fn percentile_cont_with_filter_emits_filter_after_within_group() {
let f: FieldRef<Txn, f64> = FieldRef::new("ms");
let g: FieldRef<Txn, i64> = FieldRef::new("active");
let agg = f
.percentile_cont(0.5)
.filter(g.as_expr().gt(Expr::literal(0i64)));
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.contains(") WITHIN GROUP (ORDER BY ms ASC) FILTER (WHERE active > "),
"expected WITHIN GROUP then FILTER, got: {sql}"
);
}
#[test]
fn percentile_cont_within_group_target_pins_via_default_asc() {
let f: FieldRef<Txn, f64> = FieldRef::new("ms");
let agg = f.percentile_cont(0.5);
if let ExprNode::Aggregate {
within_group_order_by,
..
} = &agg.node
{
assert_eq!(within_group_order_by.len(), 1);
} else {
panic!("expected Aggregate node");
}
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "ordered-set / hypothetical-set aggregate must carry")]
fn direct_ir_ordered_set_without_within_group_debug_asserts() {
let f: FieldRef<Txn, f64> = FieldRef::new("ms");
let mut agg = f.percentile_cont(0.5);
if let ExprNode::Aggregate {
within_group_order_by,
..
} = &mut agg.node
{
within_group_order_by.clear();
}
let _ = crate::expr::sql::check_aggregate_legality(&agg.node);
}
#[test]
fn rank_of_emits_within_group() {
let f: FieldRef<Txn, i64> = FieldRef::new("salary");
let agg = f.rank_of(7_500);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.starts_with("RANK($") && sql.contains(") WITHIN GROUP (ORDER BY salary ASC)"),
"got: {sql}"
);
}
#[test]
fn dense_rank_of_emits_within_group() {
let f: FieldRef<Txn, i64> = FieldRef::new("salary");
let agg = f.dense_rank_of(7_500);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.starts_with("DENSE_RANK($") && sql.contains(") WITHIN GROUP (ORDER BY salary ASC)"),
"got: {sql}"
);
}
#[test]
fn percent_rank_of_emits_within_group() {
let f: FieldRef<Txn, f64> = FieldRef::new("ms");
let agg = f.percent_rank_of(100.0);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.starts_with("PERCENT_RANK($") && sql.contains(") WITHIN GROUP (ORDER BY ms ASC)"),
"got: {sql}"
);
}
#[test]
fn cume_dist_of_emits_within_group() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let agg = f.cume_dist_of(500);
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.starts_with("CUME_DIST($") && sql.contains(") WITHIN GROUP (ORDER BY amount ASC)"),
"got: {sql}"
);
}
#[test]
fn rank_of_returns_i64() {
let f: FieldRef<Txn, i64> = FieldRef::new("salary");
let _: AggregateExpr<i64, HypotheticalSetAgg> = f.rank_of(7_500);
let _: AggregateExpr<i64, HypotheticalSetAgg> = f.dense_rank_of(7_500);
}
#[test]
fn percent_rank_of_returns_f64() {
let f: FieldRef<Txn, i64> = FieldRef::new("amount");
let _: AggregateExpr<f64, HypotheticalSetAgg> = f.percent_rank_of(500);
let _: AggregateExpr<f64, HypotheticalSetAgg> = f.cume_dist_of(500);
}
#[test]
fn hypothetical_set_with_filter_accepted_at_fetch() {
let f: FieldRef<Txn, i64> = FieldRef::new("salary");
let g: FieldRef<Txn, i64> = FieldRef::new("active");
let agg = f.rank_of(7_500).filter(g.as_expr().gt(Expr::literal(0i64)));
let result = crate::expr::sql::check_aggregate_legality(&agg.node);
assert!(
result.is_ok(),
"FILTER must be accepted on hypothetical-set RANK, got: {result:?}"
);
}
#[test]
fn hypothetical_rank_within_group_override_works() {
let f: FieldRef<Txn, i64> = FieldRef::new("salary");
let other: FieldRef<Txn, i64> = FieldRef::new("base_salary");
let agg = f.rank_of(7_500).within_group_order_by(other.desc());
let mut acc = SqlAccumulator::new("");
emit_expr(&mut acc, &agg.node, SqlEmitContext::root())
.expect("aggregate expression should lower to SQL");
let sql = acc.sql().to_string();
assert!(
sql.contains(") WITHIN GROUP (ORDER BY base_salary DESC)"),
"expected DESC override on different column, got: {sql}"
);
}
#[cfg(debug_assertions)]
#[test]
#[should_panic(expected = "ordered-set / hypothetical-set aggregate must not carry")]
fn direct_ir_hypothetical_set_order_by_debug_asserts() {
let f: FieldRef<Txn, i64> = FieldRef::new("salary");
let ordering: FieldRef<Txn, i64> = FieldRef::new("salary");
let mut agg = f.rank_of(7_500);
if let ExprNode::Aggregate { order_by, .. } = &mut agg.node {
order_by.push(ordering.asc());
}
let _ = crate::expr::sql::check_aggregate_legality(&agg.node);
}
}