Skip to main content

kernel/config/
aggregate.rs

1use std::hash::{Hash, Hasher};
2
3use serde::{Deserialize, Serialize};
4
5use crate::common;
6
7use super::{AggregateKey, ColumnPair, Expression, Filter, FlussoType, join::hash_on};
8
9/// Reduces rows from a related `table` to a single value — a count, sum,
10/// extreme, or existence flag. The `key` connects the tables; `filters`
11/// restrict which rows of `table` count; `via` reaches `table` across the
12/// tables in between.
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct Aggregate {
15    pub table: common::TableName,
16    pub op: AggregateOp,
17    pub key: AggregateKey,
18    /// The declared result type. Fixed for `count` (`long`) and `avg` (`double`)
19    /// and left `None`; required for `sum` / `min` / `max`, whose result mirrors
20    /// the aggregated column and so must be stated to stay database-free.
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub value_type: Option<FlussoType>,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub filters: Option<Vec<Filter>>,
25    /// Over a junction: count each target row once instead of once per
26    /// junction row. Only valid with an [`AggregateKey::Through`] key.
27    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
28    pub distinct: bool,
29    /// The tables between the parent and `table`, in order from the parent.
30    /// Empty for a one-hop aggregate. When set, the [`key`](Self::key) points
31    /// at the last hop's table instead of the parent.
32    #[serde(default, skip_serializing_if = "Vec::is_empty")]
33    pub via: Vec<ViaHop>,
34    /// Extra column equalities between the aggregated rows and the table
35    /// before them (the parent, or the last `via` hop), ANDed with the key.
36    /// Only with an [`AggregateKey::Direct`] key.
37    #[serde(default, skip_serializing_if = "Vec::is_empty")]
38    pub on: Vec<ColumnPair>,
39}
40
41/// One table an aggregate passes through on its way to the aggregated table: a
42/// downward foreign-key hop. `foreign_key` is on `table` and points at the
43/// table before it (the parent, for the first hop).
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ViaHop {
46    /// The table this hop passes through.
47    pub table: common::TableName,
48    /// The column on `table` pointing at the table before it.
49    pub foreign_key: common::ColumnName,
50    /// Which rows of `table` the path passes through.
51    #[serde(default, skip_serializing_if = "Option::is_none")]
52    pub filters: Option<Vec<Filter>>,
53    /// Extra column equalities between `table` and the table before it,
54    /// ANDed with `foreign_key`.
55    #[serde(default, skip_serializing_if = "Vec::is_empty")]
56    pub on: Vec<ColumnPair>,
57}
58
59/// Hashes `on` only when non-empty, so a hop without it keeps its content hash.
60impl Hash for ViaHop {
61    fn hash<H: Hasher>(&self, state: &mut H) {
62        let Self {
63            table,
64            foreign_key,
65            filters,
66            on,
67        } = self;
68        table.hash(state);
69        foreign_key.hash(state);
70        filters.hash(state);
71        hash_on("via_on", on, state);
72    }
73}
74
75/// Hashes `distinct` only when set and `via`/`on` only when non-empty, so a schema
76/// that never opts in keeps the content hash (and so the physical index) it had
77/// before the fields existed.
78impl Hash for Aggregate {
79    fn hash<H: Hasher>(&self, state: &mut H) {
80        let Self {
81            table,
82            op,
83            key,
84            value_type,
85            filters,
86            distinct,
87            via,
88            on,
89        } = self;
90        table.hash(state);
91        op.hash(state);
92        key.hash(state);
93        value_type.hash(state);
94        filters.hash(state);
95        if *distinct {
96            distinct.hash(state);
97        }
98        if !via.is_empty() {
99            "via".hash(state);
100            via.hash(state);
101        }
102        hash_on("on", on, state);
103    }
104}
105
106/// What an aggregate computes. `sum`/`avg`/`min`/`max` fold an [`Expression`]
107/// over the aggregated rows: one column, or arithmetic over the table's columns.
108#[derive(Debug, Clone, Hash, Serialize, Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum AggregateOp {
111    Count,
112    Sum(Expression),
113    Avg(Expression),
114    Min(Expression),
115    Max(Expression),
116    /// Collect the related table's primary keys into a flat scalar array. The
117    /// element type is stated explicitly (the schema names it) so the array's
118    /// mapping is known without touching the database.
119    Ids {
120        element_type: FlussoType,
121    },
122    /// Whether any related row exists: a non-null `boolean`, `false` over zero
123    /// rows. Last, so the derived `Hash` keeps every earlier variant's
124    /// discriminant.
125    Exists,
126}
127
128impl AggregateOp {
129    /// The per-row input `sum`/`avg`/`min`/`max` fold; `None` for the ops that
130    /// read no column.
131    pub fn input(&self) -> Option<&Expression> {
132        match self {
133            Self::Sum(input) | Self::Avg(input) | Self::Min(input) | Self::Max(input) => {
134                Some(input)
135            }
136            Self::Count | Self::Ids { .. } | Self::Exists => None,
137        }
138    }
139}