use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use ts_rs::TS;
use crate::proto;
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize, TS)]
#[serde(rename_all = "snake_case")]
pub enum WindowAggregate {
Sum,
Avg,
Count,
Min,
Max,
Stddev,
Var,
First,
Last,
Lag,
Lead,
Diff,
Rate,
Ema,
}
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum WindowFrame {
Rows(u32),
Range(f64),
Cumulative,
}
#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize, TS)]
#[serde(rename_all = "snake_case")]
pub enum WindowSortDir {
#[default]
Asc,
Desc,
}
impl std::fmt::Display for WindowSortDir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Asc => "asc",
Self::Desc => "desc",
})
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, TS)]
pub struct WindowSort(pub String, pub WindowSortDir);
#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize, TS)]
pub struct Windows(#[ts(as = "HashMap<String, RawWindowSpec>")] pub HashMap<String, WindowSpec>);
impl std::ops::Deref for Windows {
type Target = HashMap<String, WindowSpec>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for Windows {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
#[serde(try_from = "RawWindowSpec", into = "RawWindowSpec")]
pub struct WindowSpec {
pub column: String,
pub aggregate: WindowAggregate,
pub partition_by: Vec<String>,
pub order_by: Option<WindowSort>,
pub frame: Option<WindowFrame>,
pub offset: Option<u32>,
pub alpha: Option<f64>,
}
#[derive(Clone, Debug, Deserialize, Serialize, TS)]
#[serde(deny_unknown_fields)]
#[ts(rename = "WindowSpec")]
struct RawWindowSpec {
column: String,
aggregate: WindowAggregate,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
partition_by: Vec<String>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
order_by: Option<WindowSort>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
rows: Option<u32>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
range: Option<f64>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
cumulative: Option<bool>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
offset: Option<u32>,
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
alpha: Option<f64>,
}
impl From<WindowSpec> for RawWindowSpec {
fn from(value: WindowSpec) -> Self {
let (rows, range, cumulative) = match value.frame {
Some(WindowFrame::Rows(n)) => (Some(n), None, None),
Some(WindowFrame::Range(x)) => (None, Some(x), None),
Some(WindowFrame::Cumulative) => (None, None, Some(true)),
None => (None, None, None),
};
RawWindowSpec {
column: value.column,
aggregate: value.aggregate,
partition_by: value.partition_by,
order_by: value.order_by,
rows,
range,
cumulative,
offset: value.offset,
alpha: value.alpha,
}
}
}
impl TryFrom<RawWindowSpec> for WindowSpec {
type Error = String;
fn try_from(value: RawWindowSpec) -> Result<Self, Self::Error> {
let frame = match (value.rows, value.range, value.cumulative) {
(None, None, None) => None,
(Some(n), None, None) => Some(WindowFrame::Rows(n)),
(None, Some(x), None) => Some(WindowFrame::Range(x)),
(None, None, Some(true)) => Some(WindowFrame::Cumulative),
(None, None, Some(false)) => {
return Err("`cumulative` must be `true` when present".to_string());
},
_ => {
return Err("`rows`, `range` and `cumulative` are mutually exclusive".to_string());
},
};
Ok(WindowSpec {
column: value.column,
aggregate: value.aggregate,
partition_by: value.partition_by,
order_by: value.order_by,
frame,
offset: value.offset,
alpha: value.alpha,
})
}
}
impl From<WindowAggregate> for proto::WindowAggregate {
fn from(value: WindowAggregate) -> Self {
match value {
WindowAggregate::Sum => Self::Sum,
WindowAggregate::Avg => Self::Avg,
WindowAggregate::Count => Self::Count,
WindowAggregate::Min => Self::Min,
WindowAggregate::Max => Self::Max,
WindowAggregate::Stddev => Self::Stddev,
WindowAggregate::Var => Self::Var,
WindowAggregate::First => Self::First,
WindowAggregate::Last => Self::Last,
WindowAggregate::Lag => Self::Lag,
WindowAggregate::Lead => Self::Lead,
WindowAggregate::Diff => Self::Diff,
WindowAggregate::Rate => Self::Rate,
WindowAggregate::Ema => Self::Ema,
}
}
}
impl From<proto::WindowAggregate> for WindowAggregate {
fn from(value: proto::WindowAggregate) -> Self {
match value {
proto::WindowAggregate::Sum => Self::Sum,
proto::WindowAggregate::Avg => Self::Avg,
proto::WindowAggregate::Count => Self::Count,
proto::WindowAggregate::Min => Self::Min,
proto::WindowAggregate::Max => Self::Max,
proto::WindowAggregate::Stddev => Self::Stddev,
proto::WindowAggregate::Var => Self::Var,
proto::WindowAggregate::First => Self::First,
proto::WindowAggregate::Last => Self::Last,
proto::WindowAggregate::Lag => Self::Lag,
proto::WindowAggregate::Lead => Self::Lead,
proto::WindowAggregate::Diff => Self::Diff,
proto::WindowAggregate::Rate => Self::Rate,
proto::WindowAggregate::Ema => Self::Ema,
}
}
}
impl From<WindowFrame> for proto::window_spec::Frame {
fn from(value: WindowFrame) -> Self {
match value {
WindowFrame::Rows(n) => Self::Rows(n),
WindowFrame::Range(x) => Self::Range(x),
WindowFrame::Cumulative => Self::Cumulative(0),
}
}
}
impl From<proto::window_spec::Frame> for WindowFrame {
fn from(value: proto::window_spec::Frame) -> Self {
match value {
proto::window_spec::Frame::Rows(n) => Self::Rows(n),
proto::window_spec::Frame::Range(x) => Self::Range(x),
proto::window_spec::Frame::Cumulative(_) => Self::Cumulative,
}
}
}
impl From<WindowSort> for proto::window_spec::Order {
fn from(value: WindowSort) -> Self {
proto::window_spec::Order {
column: value.0,
desc: value.1 == WindowSortDir::Desc,
}
}
}
impl From<proto::window_spec::Order> for WindowSort {
fn from(value: proto::window_spec::Order) -> Self {
WindowSort(
value.column,
if value.desc {
WindowSortDir::Desc
} else {
WindowSortDir::Asc
},
)
}
}
impl From<WindowSpec> for proto::WindowSpec {
fn from(value: WindowSpec) -> Self {
proto::WindowSpec {
source: value.column,
op: proto::WindowAggregate::from(value.aggregate) as i32,
partition_by: value.partition_by,
order_by: value.order_by.map(|x| x.into()),
frame: value.frame.map(|x| x.into()),
offset: value.offset,
alpha: value.alpha,
}
}
}
impl From<proto::WindowSpec> for WindowSpec {
fn from(value: proto::WindowSpec) -> Self {
WindowSpec {
column: value.source,
aggregate: proto::WindowAggregate::try_from(value.op)
.unwrap_or(proto::WindowAggregate::Sum)
.into(),
partition_by: value.partition_by,
order_by: value.order_by.map(WindowSort::from),
frame: value.frame.map(|x| x.into()),
offset: value.offset,
alpha: value.alpha,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn spec(frame: Option<WindowFrame>) -> WindowSpec {
WindowSpec {
column: "price".to_string(),
aggregate: WindowAggregate::Sum,
partition_by: vec![],
order_by: None,
frame,
offset: None,
alpha: None,
}
}
#[test]
fn test_frame_roundtrips_flattened() {
for (frame, json) in [
(None, r#"{"column":"price","aggregate":"sum"}"#),
(
Some(WindowFrame::Rows(19)),
r#"{"column":"price","aggregate":"sum","rows":19}"#,
),
(
Some(WindowFrame::Range(5000.0)),
r#"{"column":"price","aggregate":"sum","range":5000.0}"#,
),
(
Some(WindowFrame::Cumulative),
r#"{"column":"price","aggregate":"sum","cumulative":true}"#,
),
] {
assert_eq!(serde_json::to_string(&spec(frame)).unwrap(), json);
assert_eq!(
serde_json::from_str::<WindowSpec>(json).unwrap(),
spec(frame)
);
}
}
#[test]
fn test_frame_rejects_invalid_combinations() {
for json in [
r#"{"column":"price","aggregate":"sum","rows":19,"range":1.0}"#,
r#"{"column":"price","aggregate":"sum","rows":19,"cumulative":true}"#,
r#"{"column":"price","aggregate":"sum","cumulative":false}"#,
r#"{"column":"price","aggregate":"sum","frame":"cumulative"}"#,
r#"{"column":"price","aggregate":"sum","rowz":19}"#,
] {
assert!(serde_json::from_str::<WindowSpec>(json).is_err(), "{json}");
}
}
}