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}