Skip to main content

perspective_client/virtual_server/
features.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::borrow::Cow;
14
15use indexmap::IndexMap;
16use serde::{Deserialize, Serialize};
17use ts_rs::TS;
18
19use crate::config::{GroupRollupMode, SplitRollupMode};
20use crate::proto::get_features_resp::{AggregateArgs, AggregateOptions, ColumnTypeOptions};
21use crate::proto::{ColumnType, GetFeaturesResp, WindowAggregateArgs};
22
23/// Describes the capabilities supported by a virtual server handler.
24///
25/// This struct is returned by
26/// [`VirtualServerHandler::get_features`](super::VirtualServerHandler::get_features)
27/// to inform clients about which operations are available.
28#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, TS)]
29pub struct Features<'a> {
30    /// Whether group-by aggregation is supported.
31    #[serde(default)]
32    #[ts(optional, as = "Option<_>")]
33    pub group_by: bool,
34
35    /// Which `group_by_rollup_mode` options are supported
36    #[serde(default)]
37    #[ts(optional, as = "Option<_>")]
38    pub group_rollup_mode: Vec<GroupRollupMode>,
39
40    /// Whether split-by (pivot) operations are supported.
41    #[serde(default)]
42    #[ts(optional, as = "Option<_>")]
43    pub split_by: bool,
44
45    /// Which `split_rollup_mode` options are supported. Empty (the default)
46    /// means `["flat"]` - a server must opt in to `"rollup"` explicitly, as
47    /// it requires emitting subtotal and grand-total column groups.
48    #[serde(default)]
49    #[ts(optional, as = "Option<_>")]
50    pub split_rollup_mode: Vec<SplitRollupMode>,
51
52    /// Available filter operators per column type.
53    #[serde(default)]
54    #[ts(optional, as = "Option<_>")]
55    pub filter_ops: IndexMap<ColumnType, Vec<Cow<'a, str>>>,
56
57    /// Available aggregate functions per column type.
58    #[serde(default)]
59    #[ts(optional, as = "Option<_>")]
60    pub aggregates: IndexMap<ColumnType, Vec<AggSpec<'a>>>,
61
62    /// Whether sorting is supported.
63    #[serde(default)]
64    #[ts(optional, as = "Option<_>")]
65    pub sort: bool,
66
67    /// Whether computed expressions are supported.
68    #[serde(default)]
69    #[ts(optional, as = "Option<_>")]
70    pub expressions: bool,
71
72    /// Available window aggregates per column type.
73    #[serde(default)]
74    #[ts(optional, as = "Option<_>")]
75    pub window_aggregates: IndexMap<ColumnType, Vec<WindowAggSpec<'a>>>,
76
77    /// Whether update callbacks are supported.
78    #[serde(default)]
79    #[ts(optional, as = "Option<_>")]
80    pub on_update: bool,
81
82    /// The data store has no reliable natural row order
83    #[serde(default)]
84    #[ts(optional, as = "Option<_>")]
85    pub unordered: bool,
86
87    /// Whether a `Table` can be derived from a `View`, which requires
88    /// [`VirtualServerHandler::view_make_table`](super::VirtualServerHandler::view_make_table).
89    #[serde(default)]
90    #[ts(optional, as = "Option<_>")]
91    pub view_derivations: bool,
92
93    /// Whether `View::expand`, `View::collapse` and `View::set_depth` are
94    /// supported, which requires the handler's `view_expand`,
95    /// `view_collapse` and `view_set_depth`.
96    #[serde(default)]
97    #[ts(optional, as = "Option<_>")]
98    pub expand_collapse: bool,
99
100    /// Whether `Table::update` is supported.
101    #[serde(default)]
102    #[ts(optional, as = "Option<_>")]
103    pub update: bool,
104}
105
106/// Specification for a window aggregate.
107#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
108pub struct WindowAggSpec<'a> {
109    pub name: Cow<'a, str>,
110
111    /// Frame kinds this aggregate accepts (`rows`, `range`, `cumulative`).
112    /// Empty means it takes no frame at all.
113    #[serde(default)]
114    #[ts(optional, as = "Option<_>")]
115    pub frames: Vec<Cow<'a, str>>,
116
117    /// Takes a row offset, as `lag` / `lead` / `diff` do.
118    #[serde(default)]
119    #[ts(optional, as = "Option<_>")]
120    pub offset: bool,
121
122    /// Takes a smoothing factor, as `ema` does.
123    #[serde(default)]
124    #[ts(optional, as = "Option<_>")]
125    pub alpha: bool,
126
127    /// The output column type. `None` means the source column's type, which
128    /// is what `min` / `max` / `lag` do.
129    #[serde(default)]
130    #[serde(skip_serializing_if = "Option::is_none")]
131    #[ts(optional)]
132    pub result_type: Option<ColumnType>,
133}
134
135impl<'a> From<&'a str> for WindowAggSpec<'a> {
136    /// A bare name, taking no frame and no arguments.
137    fn from(name: &'a str) -> Self {
138        WindowAggSpec {
139            name: Cow::Borrowed(name),
140            frames: vec![],
141            offset: false,
142            alpha: false,
143            result_type: None,
144        }
145    }
146}
147
148/// Specification for an aggregate function.
149///
150/// Aggregates can either take no additional arguments ([`AggSpec::Single`])
151/// or require column type arguments ([`AggSpec::Multiple`]).
152#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
153#[serde(untagged)]
154pub enum AggSpec<'a> {
155    /// An aggregate function with no additional arguments.
156    Single(Cow<'a, str>),
157    /// An aggregate function that requires column type arguments.
158    Multiple(Cow<'a, str>, Vec<ColumnType>),
159}
160
161impl<'a> From<Features<'a>> for GetFeaturesResp {
162    fn from(value: Features<'a>) -> GetFeaturesResp {
163        GetFeaturesResp {
164            group_by: value.group_by,
165            group_rollup_mode: value
166                .group_rollup_mode
167                .iter()
168                .map(|x| crate::proto::GroupRollupMode::from(*x) as i32)
169                .collect(),
170            split_rollup_mode: value
171                .split_rollup_mode
172                .iter()
173                .map(|x| crate::proto::SplitRollupMode::from(*x) as i32)
174                .collect(),
175            split_by: value.split_by,
176            expressions: value.expressions,
177            on_update: value.on_update,
178            sort: value.sort,
179            unordered: value.unordered,
180            view_derivations: value.view_derivations,
181            expand_collapse: value.expand_collapse,
182            update: value.update,
183            window_aggregates: value
184                .window_aggregates
185                .iter()
186                .map(|(ty, aggs)| {
187                    (
188                        *ty as u32,
189                        crate::proto::get_features_resp::WindowAggregateOptions {
190                            options: aggs
191                                .iter()
192                                .map(|x| WindowAggregateArgs {
193                                    name: x.name.to_string(),
194                                    frames: x.frames.iter().map(|f| f.to_string()).collect(),
195                                    offset: x.offset,
196                                    alpha: x.alpha,
197                                    result_type: x.result_type.map(|t| t as i32),
198                                })
199                                .collect(),
200                        },
201                    )
202                })
203                .collect(),
204            aggregates: value
205                .aggregates
206                .iter()
207                .map(|(dtype, aggs)| {
208                    (*dtype as u32, AggregateOptions {
209                        aggregates: aggs
210                            .iter()
211                            .map(|agg| match agg {
212                                AggSpec::Single(cow) => AggregateArgs {
213                                    name: cow.to_string(),
214                                    args: vec![],
215                                },
216                                AggSpec::Multiple(cow, column_types) => AggregateArgs {
217                                    name: cow.to_string(),
218                                    args: column_types.iter().map(|x| *x as i32).collect(),
219                                },
220                            })
221                            .collect(),
222                    })
223                })
224                .collect(),
225            filter_ops: value
226                .filter_ops
227                .iter()
228                .map(|(ty, options)| {
229                    (*ty as u32, ColumnTypeOptions {
230                        options: options.iter().map(|x| (*x).to_string()).collect(),
231                    })
232                })
233                .collect(),
234        }
235    }
236}
237
238impl From<&GetFeaturesResp> for Features<'static> {
239    fn from(value: &GetFeaturesResp) -> Features<'static> {
240        Features {
241            group_by: value.group_by,
242            group_rollup_mode: value
243                .group_rollup_mode
244                .iter()
245                .filter_map(|x| crate::proto::GroupRollupMode::try_from(*x).ok())
246                .map(Into::into)
247                .collect(),
248            split_by: value.split_by,
249            split_rollup_mode: value
250                .split_rollup_mode
251                .iter()
252                .filter_map(|x| crate::proto::SplitRollupMode::try_from(*x).ok())
253                .map(Into::into)
254                .collect(),
255            filter_ops: by_column_type(&value.filter_ops, |x| {
256                x.options.iter().cloned().map(Cow::Owned).collect()
257            }),
258            aggregates: by_column_type(&value.aggregates, |x| {
259                x.aggregates
260                    .iter()
261                    .map(|agg| {
262                        let name = Cow::Owned(agg.name.clone());
263                        if agg.args.is_empty() {
264                            AggSpec::Single(name)
265                        } else {
266                            AggSpec::Multiple(
267                                name,
268                                agg.args
269                                    .iter()
270                                    .filter_map(|t| ColumnType::try_from(*t).ok())
271                                    .collect(),
272                            )
273                        }
274                    })
275                    .collect()
276            }),
277            sort: value.sort,
278            expressions: value.expressions,
279            window_aggregates: by_column_type(&value.window_aggregates, |x| {
280                x.options
281                    .iter()
282                    .map(|w| WindowAggSpec {
283                        name: Cow::Owned(w.name.clone()),
284                        frames: w.frames.iter().cloned().map(Cow::Owned).collect(),
285                        offset: w.offset,
286                        alpha: w.alpha,
287                        result_type: w.result_type.and_then(|t| ColumnType::try_from(t).ok()),
288                    })
289                    .collect()
290            }),
291            on_update: value.on_update,
292            unordered: value.unordered,
293            view_derivations: value.view_derivations,
294            expand_collapse: value.expand_collapse,
295            update: value.update,
296        }
297    }
298}
299
300/// Re-keys a wire map by [`ColumnType`], in `ColumnType` order.
301fn by_column_type<'a, T: 'a, U>(
302    map: impl IntoIterator<Item = (&'a u32, &'a T)>,
303    f: impl Fn(&T) -> U,
304) -> IndexMap<ColumnType, U> {
305    let mut entries = map
306        .into_iter()
307        .filter_map(|(ty, x)| Some((ColumnType::try_from(*ty as i32).ok()?, f(x))))
308        .collect::<Vec<_>>();
309
310    entries.sort_by_key(|(ty, _)| *ty as i32);
311    entries.into_iter().collect()
312}