use sea_query::{IntoIden, PostgresQueryBuilder, Query, SimpleExpr};
use crate::types::Interval;
pub fn time_bucket(interval: &Interval, column: impl IntoIden + Clone) -> SimpleExpr {
let iden = column.into_iden();
let col_name = iden.to_string();
SimpleExpr::Custom(format!("time_bucket('{interval}', \"{col_name}\")"))
}
pub fn time_bucket_gapfill(interval: &Interval, column: impl IntoIden + Clone) -> SimpleExpr {
let iden = column.into_iden();
let col_name = iden.to_string();
SimpleExpr::Custom(format!("time_bucket_gapfill('{interval}', \"{col_name}\")"))
}
pub fn first(value_col: impl IntoIden + Clone, time_col: impl IntoIden + Clone) -> SimpleExpr {
let value_name = value_col.into_iden().to_string();
let time_name = time_col.into_iden().to_string();
SimpleExpr::Custom(format!("first(\"{value_name}\", \"{time_name}\")"))
}
pub fn last(value_col: impl IntoIden + Clone, time_col: impl IntoIden + Clone) -> SimpleExpr {
let value_name = value_col.into_iden().to_string();
let time_name = time_col.into_iden().to_string();
SimpleExpr::Custom(format!("last(\"{value_name}\", \"{time_name}\")"))
}
pub fn locf(inner: SimpleExpr) -> SimpleExpr {
match inner {
SimpleExpr::Custom(sql) => SimpleExpr::Custom(format!("locf({sql})")),
other => {
let rendered = Query::select().expr(other).to_string(PostgresQueryBuilder);
let expr_str = rendered.strip_prefix("SELECT ").unwrap_or(&rendered);
SimpleExpr::Custom(format!("locf({expr_str})"))
}
}
}
pub fn histogram(column: impl IntoIden + Clone, min: f64, max: f64, buckets: i32) -> SimpleExpr {
let col_name = column.into_iden().to_string();
SimpleExpr::Custom(format!(
"histogram(\"{col_name}\", {min}, {max}, {buckets})"
))
}
pub fn interpolate(inner: SimpleExpr) -> SimpleExpr {
match inner {
SimpleExpr::Custom(sql) => SimpleExpr::Custom(format!("interpolate({sql})")),
other => {
let rendered = Query::select().expr(other).to_string(PostgresQueryBuilder);
let expr_str = rendered.strip_prefix("SELECT ").unwrap_or(&rendered);
SimpleExpr::Custom(format!("interpolate({expr_str})"))
}
}
}
pub fn time_bucket_with_origin(
interval: &Interval,
column: impl IntoIden + Clone,
origin: &str,
) -> SimpleExpr {
let col_name = column.into_iden().to_string();
let origin_escaped = origin.replace('\'', "''");
SimpleExpr::Custom(format!(
"time_bucket('{interval}', \"{col_name}\", origin => '{origin_escaped}')"
))
}
pub fn time_bucket_with_offset(
interval: &Interval,
column: impl IntoIden + Clone,
offset: &Interval,
) -> SimpleExpr {
let col_name = column.into_iden().to_string();
SimpleExpr::Custom(format!(
"time_bucket('{interval}', \"{col_name}\", INTERVAL '{offset}')"
))
}
pub fn time_bucket_tz(
interval: &Interval,
column: impl IntoIden + Clone,
timezone: &str,
) -> SimpleExpr {
let col_name = column.into_iden().to_string();
let tz_escaped = timezone.replace('\'', "''");
SimpleExpr::Custom(format!(
"time_bucket('{interval}', \"{col_name}\", timezone => '{tz_escaped}')"
))
}
#[cfg(test)]
mod tests {
use super::*;
use sea_query::Alias;
fn custom_sql(expr: &SimpleExpr) -> &str {
match expr {
SimpleExpr::Custom(s) => s.as_str(),
_ => panic!("Expected SimpleExpr::Custom"),
}
}
#[test]
fn test_time_bucket_sql() {
let expr = time_bucket(&Interval::Hours(1), Alias::new("time"));
assert_eq!(custom_sql(&expr), "time_bucket('1 hours', \"time\")");
}
#[test]
fn test_time_bucket_gapfill_sql() {
let expr = time_bucket_gapfill(&Interval::Minutes(30), Alias::new("timestamp"));
assert_eq!(
custom_sql(&expr),
"time_bucket_gapfill('30 minutes', \"timestamp\")"
);
}
#[test]
fn test_first_sql() {
let expr = first(Alias::new("value"), Alias::new("time"));
assert_eq!(custom_sql(&expr), "first(\"value\", \"time\")");
}
#[test]
fn test_last_sql() {
let expr = last(Alias::new("value"), Alias::new("time"));
assert_eq!(custom_sql(&expr), "last(\"value\", \"time\")");
}
#[test]
fn test_locf_with_custom_expr() {
let inner = SimpleExpr::Custom("AVG(\"value\")".to_string());
let expr = locf(inner);
assert_eq!(custom_sql(&expr), "locf(AVG(\"value\"))");
}
#[test]
fn test_histogram_sql() {
let expr = histogram(Alias::new("temperature"), 0.0, 100.0, 10);
assert_eq!(custom_sql(&expr), "histogram(\"temperature\", 0, 100, 10)");
}
#[test]
fn test_interpolate_with_custom_expr() {
let inner = SimpleExpr::Custom("AVG(\"value\")".to_string());
let expr = interpolate(inner);
assert_eq!(custom_sql(&expr), "interpolate(AVG(\"value\"))");
}
#[test]
fn test_time_bucket_with_origin_sql() {
let expr = time_bucket_with_origin(
&Interval::Hours(1),
Alias::new("time"),
"2024-01-01 00:00:00+00",
);
assert_eq!(
custom_sql(&expr),
"time_bucket('1 hours', \"time\", origin => '2024-01-01 00:00:00+00')"
);
}
#[test]
fn test_time_bucket_with_offset_sql() {
let expr = time_bucket_with_offset(
&Interval::Hours(1),
Alias::new("time"),
&Interval::Minutes(30),
);
assert_eq!(
custom_sql(&expr),
"time_bucket('1 hours', \"time\", INTERVAL '30 minutes')"
);
}
#[test]
fn test_time_bucket_tz_sql() {
let expr = time_bucket_tz(&Interval::Days(1), Alias::new("time"), "US/Eastern");
assert_eq!(
custom_sql(&expr),
"time_bucket('1 days', \"time\", timezone => 'US/Eastern')"
);
}
#[test]
fn test_time_bucket_various_intervals() {
let cases = vec![
(Interval::Seconds(30), "time_bucket('30 seconds', \"ts\")"),
(Interval::Minutes(5), "time_bucket('5 minutes', \"ts\")"),
(Interval::Days(1), "time_bucket('1 days', \"ts\")"),
(Interval::Weeks(1), "time_bucket('1 weeks', \"ts\")"),
(Interval::Months(1), "time_bucket('1 months', \"ts\")"),
];
for (interval, expected) in cases {
let expr = time_bucket(&interval, Alias::new("ts"));
assert_eq!(custom_sql(&expr), expected);
}
}
}