refactor: Moved simplification of time range expressions to parser

pull/24376/head
Stuart Carnie 2023-06-02 09:50:01 +10:00
parent 8c02f81456
commit d5719f9be2
No known key found for this signature in database
GPG Key ID: 848D9C9718D78B4F
13 changed files with 905 additions and 774 deletions

1
Cargo.lock generated
View File

@ -2490,6 +2490,7 @@ dependencies = [
"chrono-tz",
"insta",
"nom",
"num-integer",
"num-traits",
"once_cell",
"paste",

View File

@ -10,6 +10,7 @@ nom = { version = "7", default-features = false, features = ["std"] }
once_cell = "1"
chrono = { version = "0.4", default-features = false, features = ["std"] }
chrono-tz = { version = "0.8" }
num-integer = { version = "0.1", default-features = false, features = ["i128", "std"] }
num-traits = "0.2"
workspace-hack = { version = "0.1", path = "../workspace-hack" }

View File

@ -2,7 +2,8 @@ use crate::common::ws0;
use crate::identifier::unquoted_identifier;
use crate::internal::{expect, Error, ParseError, ParseResult};
use crate::keywords::keyword;
use crate::literal::literal_regex;
use crate::literal::{literal_regex, Duration};
use crate::timestamp::Timestamp;
use crate::{
identifier::{identifier, Identifier},
literal::Literal,
@ -633,6 +634,53 @@ fn reduce_expr(expr: Expr, remainder: Vec<(BinaryOperator, Expr)>) -> Expr {
})
}
/// Trait for converting a type to a [`Expr::Literal`] expression.
pub trait LiteralExpr {
/// Convert the receiver to a literal expression.
fn lit(self) -> Expr;
}
/// Convert `v` to a literal expression.
pub fn lit<T: LiteralExpr>(v: T) -> Expr {
v.lit()
}
impl LiteralExpr for Literal {
fn lit(self) -> Expr {
Expr::Literal(self)
}
}
impl LiteralExpr for Duration {
fn lit(self) -> Expr {
Expr::Literal(Literal::Duration(self))
}
}
impl LiteralExpr for i64 {
fn lit(self) -> Expr {
Expr::Literal(Literal::Integer(self))
}
}
impl LiteralExpr for f64 {
fn lit(self) -> Expr {
Expr::Literal(Literal::Float(self))
}
}
impl LiteralExpr for String {
fn lit(self) -> Expr {
Expr::Literal(Literal::String(self))
}
}
impl LiteralExpr for Timestamp {
fn lit(self) -> Expr {
Expr::Literal(Literal::Timestamp(self))
}
}
#[cfg(test)]
mod test {
use super::*;

View File

@ -17,7 +17,7 @@
)]
// Workaround for "unused crate" lint false positives.
use workspace_hack as _;
// use workspace_hack as _;
use crate::common::{statement_terminator, ws0};
use crate::internal::Error as InternalError;
@ -51,6 +51,8 @@ pub mod show_tag_values;
pub mod simple_from_clause;
pub mod statement;
pub mod string;
pub mod time_range;
pub mod timestamp;
pub mod visit;
pub mod visit_mut;

View File

@ -4,8 +4,9 @@ use crate::common::ws0;
use crate::internal::{map_error, map_fail, ParseResult};
use crate::keywords::keyword;
use crate::string::{regex, single_quoted_string, Regex};
use crate::timestamp::Timestamp;
use crate::{impl_tuple_clause, write_escaped};
use chrono::{DateTime, FixedOffset};
use chrono::{NaiveDateTime, Offset};
use nom::branch::alt;
use nom::bytes::complete::tag;
use nom::character::complete::{char, digit0, digit1};
@ -55,7 +56,7 @@ pub enum Literal {
Regex(Regex),
/// A timestamp identified in a time range expression of a conditional expression.
Timestamp(DateTime<FixedOffset>),
Timestamp(Timestamp),
}
impl From<String> for Literal {
@ -353,6 +354,17 @@ pub(crate) fn literal_regex(i: &str) -> ParseResult<&str, Literal> {
map(regex, Literal::Regex)(i)
}
/// Returns `nanos` as a timestamp.
pub fn nanos_to_timestamp(nanos: i64) -> Timestamp {
let (secs, nsec) = num_integer::div_mod_floor(nanos, NANOS_PER_SEC);
Timestamp::from_utc(
NaiveDateTime::from_timestamp_opt(secs, nsec as u32)
.expect("unable to convert duration to timestamp"),
chrono::Utc.fix(),
)
}
#[cfg(test)]
mod test {
use super::*;
@ -572,4 +584,18 @@ mod test {
let (_, got) = number("+ 501").unwrap();
assert_matches!(got, Number::Integer(v) if v == 501);
}
#[test]
fn test_nanos_to_timestamp() {
let ts = nanos_to_timestamp(0);
assert_eq!(ts.to_rfc3339(), "1970-01-01T00:00:00+00:00");
// infallible
let ts = nanos_to_timestamp(i64::MAX);
assert_eq!(ts.timestamp_nanos(), i64::MAX);
// let ts = nanos_to_timestamp(i64::MIN);
// This line panics with an arithmetic overflow.
// assert_eq!(ts.timestamp_nanos(), i64::MIN);
}
}

View File

@ -0,0 +1,542 @@
//! Process InfluxQL time range expressions
//!
use crate::expression::walk::{walk_expression, Expression};
use crate::expression::{lit, Binary, BinaryOperator, ConditionalExpression, Expr, VarRef};
use crate::functions::is_now_function;
use crate::literal::{nanos_to_timestamp, Duration, Literal};
use crate::timestamp::{parse_timestamp, Timestamp};
use std::ops::ControlFlow;
/// Result type for operations that could result in an [`ExprError`].
pub type ExprResult = Result<Expr, ExprError>;
#[allow(dead_code)]
fn split_cond(
cond: &ConditionalExpression,
) -> (Option<ConditionalExpression>, Option<ConditionalExpression>) {
// search the tree for an expression involving `time`.
let no_time = walk_expression(cond, &mut |e| {
if let Expression::Conditional(cond) = e {
if is_time_field(cond) {
return ControlFlow::Break(());
}
}
ControlFlow::Continue(())
})
.is_continue();
if no_time {
return (None, None);
}
unimplemented!()
}
/// Simplifies an InfluxQL duration `expr` to a nanosecond interval represented as an `i64`.
pub fn duration_expr_to_nanoseconds(expr: &Expr) -> Result<i64, ExprError> {
let ctx = ReduceContext::default();
match reduce_expr(&ctx, expr)? {
Expr::Literal(Literal::Duration(v)) => Ok(*v),
Expr::Literal(Literal::Float(v)) => Ok(v as i64),
Expr::Literal(Literal::Integer(v)) => Ok(v),
_ => error::expr("invalid duration expression"),
}
}
/// Represents an error that occurred whilst simplifying an InfluxQL expression.
#[derive(Debug)]
pub enum ExprError {
/// An error in the expression that can be resolved by the client.
Expression(String),
/// An internal error that signals a bug.
Internal(String),
}
/// Helper functions for creating errors.
mod error {
use super::ExprError;
pub(crate) fn expr<T>(s: impl Into<String>) -> Result<T, ExprError> {
Err(map::expr(s))
}
pub(crate) mod map {
use super::*;
pub(crate) fn expr(s: impl Into<String>) -> ExprError {
ExprError::Expression(s.into())
}
}
}
/// Context used when simplifying InfluxQL time range expressions.
#[derive(Default, Debug, Clone, Copy)]
pub struct ReduceContext {
/// The value for the `now()` function.
pub now: Option<Timestamp>,
/// The timezone to evaluate literal timestamp strings.
pub tz: Option<chrono_tz::Tz>,
}
#[allow(dead_code)]
fn reduce(
ctx: &ReduceContext,
cond: &ConditionalExpression,
) -> Result<ConditionalExpression, ExprError> {
Ok(match cond {
ConditionalExpression::Expr(expr) => {
ConditionalExpression::Expr(Box::new(reduce_expr(ctx, expr)?))
}
ConditionalExpression::Binary(_) => unimplemented!(),
ConditionalExpression::Grouped(_) => unimplemented!(),
})
}
/// Simplify the time range expression.
pub fn reduce_time_expr(ctx: &ReduceContext, expr: &Expr) -> ExprResult {
match reduce_expr(ctx, expr)? {
expr @ Expr::Literal(Literal::Timestamp(_)) => Ok(expr),
Expr::Literal(Literal::String(ref s)) => {
parse_timestamp_expr(s, ctx.tz).map_err(map_expr_err(expr))
}
Expr::Literal(Literal::Duration(v)) => Ok(lit(nanos_to_timestamp(*v))),
Expr::Literal(Literal::Float(v)) => Ok(lit(nanos_to_timestamp(v as i64))),
Expr::Literal(Literal::Integer(v)) => Ok(lit(nanos_to_timestamp(v))),
_ => error::expr("invalid time range expression"),
}
}
fn reduce_expr(ctx: &ReduceContext, expr: &Expr) -> ExprResult {
match expr {
Expr::Binary(ref v) => reduce_binary_expr(ctx, v).map_err(map_expr_err(expr)),
Expr::Call (call) if is_now_function(call.name.as_str()) => ctx.now.map(lit).ok_or_else(|| ExprError::Internal("unable to resolve now".into())),
Expr::Call (call) => {
error::expr(
format!("invalid function call '{}'", call.name),
)
}
Expr::Nested(expr) => reduce_expr(ctx, expr),
Expr::Literal(val) => match val {
Literal::Integer(_) |
Literal::Float(_) |
Literal::String(_) |
Literal::Timestamp(_) |
Literal::Duration(_) => Ok(Expr::Literal(val.clone())),
_ => error::expr(format!(
"found literal '{val}', expected duration, float, integer, or timestamp string"
)),
},
Expr::VarRef { .. } | Expr::BindParameter(_) | Expr::Wildcard(_) | Expr::Distinct(_) => error::expr(format!(
"found symbol '{expr}', expected now() or a literal duration, float, integer and timestamp string"
)),
}
}
fn reduce_binary_expr(ctx: &ReduceContext, expr: &Binary) -> ExprResult {
let lhs = reduce_expr(ctx, &expr.lhs)?;
let op = expr.op;
let rhs = reduce_expr(ctx, &expr.rhs)?;
match lhs {
Expr::Literal(Literal::Duration(v)) => reduce_binary_lhs_duration(ctx, v, op, rhs),
Expr::Literal(Literal::Integer(v)) => reduce_binary_lhs_integer(ctx, v, op, rhs),
Expr::Literal(Literal::Float(v)) => reduce_binary_lhs_float(v, op, rhs),
Expr::Literal(Literal::Timestamp(v)) => reduce_binary_lhs_timestamp(ctx, v, op, rhs),
Expr::Literal(Literal::String(v)) => reduce_binary_lhs_string(ctx, v, op, rhs),
_ => Ok(Expr::Binary(Binary {
lhs: Box::new(lhs),
op,
rhs: Box::new(rhs),
})),
}
}
/// Reduce `duration OP expr`.
///
/// ```text
/// duration = duration ( ADD | SUB ) ( duration | NOW() )
/// duration = duration ( MUL | DIV ) ( float | integer )
/// timestamp = duration ADD string
/// timestamp = duration ADD timestamp
/// ```
fn reduce_binary_lhs_duration(
ctx: &ReduceContext,
lhs: Duration,
op: BinaryOperator,
rhs: Expr,
) -> ExprResult {
match rhs {
Expr::Literal(ref val) => match val {
// durations may be added and subtracted from other durations
Literal::Duration(Duration(v)) => match op {
BinaryOperator::Add => Ok(lit(Duration(
lhs.checked_add(*v)
.ok_or_else(|| error::map::expr("overflow"))?,
))),
BinaryOperator::Sub => Ok(lit(Duration(
lhs.checked_sub(*v)
.ok_or_else(|| error::map::expr("overflow"))?,
))),
_ => error::expr(format!("found operator '{op}', expected +, -")),
},
// durations may only be scaled by float literals
Literal::Float(v) => {
reduce_binary_lhs_duration(ctx, lhs, op, Expr::Literal(Literal::Integer(*v as i64)))
}
Literal::Integer(v) => match op {
BinaryOperator::Mul => Ok(lit(Duration(*lhs * *v))),
BinaryOperator::Div => Ok(lit(Duration(*lhs / *v))),
_ => error::expr(format!("found operator '{op}', expected *, /")),
},
// A timestamp may be added to a duration
Literal::Timestamp(v) if matches!(op, BinaryOperator::Add) => {
Ok(lit(*v + chrono::Duration::nanoseconds(*lhs)))
}
Literal::String(v) => {
reduce_binary_lhs_duration(ctx, lhs, op, parse_timestamp_expr(v, ctx.tz)?)
}
// This should not occur, as acceptable literals are validated in `reduce_expr`.
_ => Err(ExprError::Internal(format!(
"unexpected literal '{rhs}' for duration expression"
))),
},
_ => error::expr("invalid duration expression"),
}
}
/// Reduce `integer OP expr`.
///
/// ```text
/// integer = integer ( ADD | SUB | MUL | DIV | MOD | BitwiseAND | BitwiseOR | BitwiseXOR ) integer
/// float = integer as float OP float
/// timestamp = integer as timestamp OP duration
/// ```
fn reduce_binary_lhs_integer(
ctx: &ReduceContext,
lhs: i64,
op: BinaryOperator,
rhs: Expr,
) -> ExprResult {
match rhs {
Expr::Literal(Literal::Float(_)) => reduce_binary_lhs_float(lhs as f64, op, rhs),
Expr::Literal(Literal::Integer(v)) => Ok(lit(op.reduce(lhs, v))),
Expr::Literal(Literal::Duration(_)) => {
reduce_binary_lhs_timestamp(ctx, nanos_to_timestamp(lhs), op, rhs)
}
Expr::Literal(Literal::String(v)) => {
reduce_binary_lhs_duration(ctx, Duration(lhs), op, parse_timestamp_expr(&v, ctx.tz)?)
}
_ => error::expr("invalid integer expression"),
}
}
/// Reduce `float OP expr`.
///
/// ```text
/// float = float ( ADD | SUB | MUL | DIV | MOD ) ( float | integer)
/// ```
fn reduce_binary_lhs_float(lhs: f64, op: BinaryOperator, rhs: Expr) -> ExprResult {
Ok(lit(match rhs {
Expr::Literal(Literal::Float(v)) => op
.try_reduce(lhs, v)
.ok_or_else(|| error::map::expr("invalid operator for float expression"))?,
Expr::Literal(Literal::Integer(v)) => op
.try_reduce(lhs, v)
.ok_or_else(|| error::map::expr("invalid operator for float expression"))?,
_ => return error::expr("invalid float expression"),
}))
}
/// Reduce `timestamp OP expr`.
///
/// The right-hand `expr` must be of a type that can be
/// coalesced to a duration, which includes a `duration`, `integer` or a
/// `string`. A `string` is parsed as a timestamp an interpreted as
/// the number of nanoseconds from the Unix epoch.
///
/// ```text
/// timestamp = timestamp ( ADD | SUB ) ( duration | integer | string | timestamp )
/// ```
fn reduce_binary_lhs_timestamp(
ctx: &ReduceContext,
lhs: Timestamp,
op: BinaryOperator,
rhs: Expr,
) -> ExprResult {
match rhs {
Expr::Literal(Literal::Duration(d)) => match op {
BinaryOperator::Add => Ok(lit(lhs + chrono::Duration::nanoseconds(*d))),
BinaryOperator::Sub => Ok(lit(lhs - chrono::Duration::nanoseconds(*d))),
_ => error::expr(format!(
"invalid operator '{op}' for timestamp and duration: expected +, -"
)),
},
Expr::Literal(Literal::Integer(_))
// NOTE: This is a slight deviation from InfluxQL, for which the only valid binary
// operator for two timestamps is subtraction. By converting the timestamp to a
// duration and calling this function recursively, we permit the addition operator.
| Expr::Literal(Literal::Timestamp(_))
| Expr::Literal(Literal::String(_)) => {
reduce_binary_lhs_timestamp(ctx, lhs, op, expr_to_duration(ctx, rhs)?)
}
_ => error::expr(format!(
"invalid expression '{rhs}': expected duration, integer or timestamp string"
)),
}
}
fn expr_to_duration(ctx: &ReduceContext, expr: Expr) -> ExprResult {
Ok(lit(match expr {
Expr::Literal(Literal::Duration(v)) => v,
Expr::Literal(Literal::Integer(v)) => Duration(v),
Expr::Literal(Literal::Timestamp(v)) => Duration(v.timestamp_nanos()),
Expr::Literal(Literal::String(v)) => {
Duration(parse_timestamp_nanos(&v, ctx.tz)?.timestamp_nanos())
}
_ => return error::expr(format!("unable to cast {expr} to duration")),
}))
}
/// Reduce `string OP expr`.
///
/// If `expr` is a string, concatenates the two values and returns a new string.
/// If `expr` is a duration, integer or timestamp, the left-hand
/// string is parsed as a timestamp and the expression evaluated as
/// `timestamp OP expr`
fn reduce_binary_lhs_string(
ctx: &ReduceContext,
lhs: String,
op: BinaryOperator,
rhs: Expr,
) -> ExprResult {
match rhs {
Expr::Literal(Literal::String(ref s)) => match op {
// concatenate the two strings
BinaryOperator::Add => Ok(lit(lhs + s)),
_ => reduce_binary_lhs_timestamp(ctx, parse_timestamp_nanos(&lhs, ctx.tz)?, op, rhs),
},
Expr::Literal(Literal::Duration(_))
| Expr::Literal(Literal::Timestamp(_))
| Expr::Literal(Literal::Integer(_)) => {
reduce_binary_lhs_timestamp(ctx, parse_timestamp_nanos(&lhs, ctx.tz)?, op, rhs)
}
_ => error::expr(format!(
"found '{rhs}', expected duration, integer or timestamp string"
)),
}
}
/// Returns true if the conditional expression is a single node that
/// refers to the `time` column.
///
/// In a conditional expression, this comparison is case-insensitive per the [Go implementation][go]
///
/// [go]: https://github.com/influxdata/influxql/blob/1ba470371ec093d57a726b143fe6ccbacf1b452b/ast.go#L5751-L5753
fn is_time_field(cond: &ConditionalExpression) -> bool {
if let ConditionalExpression::Expr(expr) = cond {
if let Expr::VarRef(VarRef { ref name, .. }) = **expr {
name.eq_ignore_ascii_case("time")
} else {
false
}
} else {
false
}
}
fn parse_timestamp_nanos(s: &str, tz: Option<chrono_tz::Tz>) -> Result<Timestamp, ExprError> {
parse_timestamp(s, tz)
.ok_or_else(|| error::map::expr(format!("'{s}' is not a valid timestamp")))
}
/// Parse s as a timestamp in the specified timezone and return the timestamp
/// as a literal timestamp expression.
fn parse_timestamp_expr(s: &str, tz: Option<chrono_tz::Tz>) -> ExprResult {
Ok(Expr::Literal(Literal::Timestamp(parse_timestamp_nanos(
s, tz,
)?)))
}
fn map_expr_err(expr: &Expr) -> impl Fn(ExprError) -> ExprError + '_ {
move |err| {
error::map::expr(format!(
"invalid expression \"{expr}\": {}",
match err {
ExprError::Expression(str) | ExprError::Internal(str) => str,
}
))
}
}
#[cfg(test)]
mod test {
use crate::expression::ConditionalExpression;
use crate::time_range::{
duration_expr_to_nanoseconds, reduce_time_expr, split_cond, ExprError, ExprResult,
ReduceContext,
};
use crate::timestamp::Timestamp;
use chrono::{NaiveDate, NaiveDateTime, NaiveTime, Offset, Utc};
use test_helpers::assert_error;
#[ignore]
#[test]
fn test_split_cond() {
let cond: ConditionalExpression = "time > now() - 1h".parse().unwrap();
let (cond, time) = split_cond(&cond);
println!("{cond:?}, {time:?}");
}
#[test]
fn test_rewrite_time_expression_no_timezone() {
fn process_expr(s: &str) -> ExprResult {
let cond: ConditionalExpression =
s.parse().expect("unexpected error parsing expression");
let ctx = ReduceContext {
now: Some(Timestamp::from_utc(
NaiveDateTime::new(
NaiveDate::from_ymd_opt(2004, 4, 9).unwrap(),
NaiveTime::from_hms_opt(12, 13, 14).unwrap(),
),
Utc.fix(),
)),
tz: None,
};
reduce_time_expr(&ctx, cond.expr().unwrap())
}
macro_rules! assert_expr {
($S: expr, $EXPECTED: expr) => {
let expr = process_expr($S).unwrap();
assert_eq!(expr.to_string(), $EXPECTED);
};
}
//
// Valid literals
//
// Duration
assert_expr!("1d", "1970-01-02T00:00:00+00:00");
// Single integer interpreted as a Unix nanosecond epoch
assert_expr!("1157082310000000000", "2006-09-01T03:45:10+00:00");
// Single float interpreted as a Unix nanosecond epoch
assert_expr!("1157082310000000000.0", "2006-09-01T03:45:10+00:00");
// Single string interpreted as a timestamp
assert_expr!(
"'2004-04-09 02:33:45.123456789'",
"2004-04-09T02:33:45.123456789+00:00"
);
// now
assert_expr!("now()", "2004-04-09T12:13:14+00:00");
//
// Expressions
//
// now() OP expr
assert_expr!("now() - 5m", "2004-04-09T12:08:14+00:00");
assert_expr!("(now() - 5m)", "2004-04-09T12:08:14+00:00");
assert_expr!("now() - 5m - 60m", "2004-04-09T11:08:14+00:00");
assert_expr!("now() - 500", "2004-04-09T12:13:13.999999500+00:00");
assert_expr!("now() - (5m + 60m)", "2004-04-09T11:08:14+00:00");
// expr OP now()
assert_expr!("5m + now()", "2004-04-09T12:18:14+00:00");
// duration OP expr
assert_expr!("1w3d + 1d", "1970-01-12T00:00:00+00:00");
assert_expr!("1w3d - 1d", "1970-01-10T00:00:00+00:00");
// string OP expr
assert_expr!("'2004-04-09' - '2004-04-08'", "1970-01-02T00:00:00+00:00");
assert_expr!("'2004-04-09' + '02:33:45'", "2004-04-09T02:33:45+00:00");
// integer OP expr
assert_expr!("1157082310000000000 - 1s", "2006-09-01T03:45:09+00:00");
// nested evaluation order
assert_expr!("now() - (6m - (1m * 5))", r#"2004-04-09T12:12:14+00:00"#);
// Fallible
use super::ExprError::Expression;
assert_error!(process_expr("foo + 1"), Expression(ref s) if s == "invalid expression \"foo + 1\": found symbol 'foo', expected now() or a literal duration, float, integer and timestamp string");
assert_error!(process_expr("5m - now()"), Expression(ref s) if s == "invalid expression \"5m - now()\": unexpected literal '2004-04-09T12:13:14+00:00' for duration expression");
assert_error!(process_expr("'2004-04-09' + false"), Expression(ref s) if s == "invalid expression \"'2004-04-09' + false\": found literal 'false', expected duration, float, integer, or timestamp string");
assert_error!(process_expr("1s * 1s"), Expression(ref s) if s == "invalid expression \"1000ms * 1000ms\": found operator '*', expected +, -");
assert_error!(process_expr("1s + 0.5"), Expression(ref s) if s == "invalid expression \"1000ms + 0.5\": found operator '+', expected *, /");
assert_error!(process_expr("'2004-04-09T'"), Expression(ref s) if s == "invalid expression \"'2004-04-09T'\": '2004-04-09T' is not a valid timestamp");
assert_error!(process_expr("now() * 1"), Expression(ref s) if s == "invalid expression \"now() * 1\": invalid operator '*' for timestamp and duration: expected +, -");
assert_error!(process_expr("'2' + now()"), Expression(ref s) if s == "invalid expression \"'2' + now()\": '2' is not a valid timestamp");
assert_error!(process_expr("'2' + '3'"), Expression(ref s) if s == "invalid expression \"'2' + '3'\": '23' is not a valid timestamp");
assert_error!(process_expr("'2' + '3' + 10s"), Expression(ref s) if s == "invalid expression \"'2' + '3' + 10s\": '23' is not a valid timestamp");
}
#[test]
fn test_rewrite_time_expression_with_timezone() {
fn process_expr(s: &str) -> ExprResult {
let cond: ConditionalExpression =
s.parse().expect("unexpected error parsing expression");
let ctx = ReduceContext {
now: None,
tz: Some(chrono_tz::Australia::Hobart),
};
reduce_time_expr(&ctx, cond.expr().unwrap())
}
macro_rules! assert_expr {
($S: expr, $EXPECTED: expr) => {
let expr = process_expr($S).unwrap();
assert_eq!(expr.to_string(), $EXPECTED);
};
}
assert_expr!(
"'2004-04-09 10:05:00.123456789'",
"2004-04-09T10:05:00.123456789+10:00"
);
assert_expr!("'2004-04-09'", "2004-04-09T00:00:00+10:00");
assert_expr!(
"'2004-04-09T10:05:00.123456789Z'",
"2004-04-09T20:05:00.123456789+10:00"
);
}
#[test]
fn test_expr_to_duration() {
fn parse(s: &str) -> Result<i64, ExprError> {
let expr = s
.parse::<ConditionalExpression>()
.unwrap()
.expr()
.unwrap()
.clone();
duration_expr_to_nanoseconds(&expr)
}
let cases = vec![
("10s", 10_000_000_000_i64),
("10s + 1d", 86_410_000_000_000),
("5d10ms", 432_000_010_000_000),
("-2d10ms", -172800010000000),
("-2d10ns", -172800000000010),
];
for (interval_str, exp) in cases {
let got = parse(interval_str).unwrap();
assert_eq!(got, exp, "Actual: {got:?}");
}
}
}

View File

@ -1,9 +1,12 @@
use crate::plan::error;
//! Parse InfluxQL timestamp strings.
//!
use chrono::{DateTime, FixedOffset, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone};
use datafusion::common::Result;
/// Represents an InfluxQL timestamp.
pub type Timestamp = DateTime<FixedOffset>;
/// Parse the timestamp string and return a DateTime in UTC.
fn parse_timestamp_utc(s: &str) -> Result<DateTime<FixedOffset>> {
fn parse_timestamp_utc(s: &str) -> Option<Timestamp> {
// 1a. Try a date time format string with nanosecond precision and then without
// https://github.com/influxdata/influxql/blob/1ba470371ec093d57a726b143fe6ccbacf1b452b/ast.go#L3661
NaiveDateTime::parse_from_str(s, "%Y-%m-%d %H:%M:%S%.f")
@ -20,11 +23,11 @@ fn parse_timestamp_utc(s: &str) -> Result<DateTime<FixedOffset>> {
.map(|nd| nd.and_time(NaiveTime::default())),
)
.map(|ts| DateTime::from_utc(ts, chrono::Utc.fix()))
.map_err(|_| error::map::query("invalid timestamp string"))
.ok()
}
/// Parse the timestamp string and return a DateTime in the specified timezone.
fn parse_timestamp_tz(s: &str, tz: chrono_tz::Tz) -> Result<DateTime<FixedOffset>> {
fn parse_timestamp_tz(s: &str, tz: chrono_tz::Tz) -> Option<Timestamp> {
// 1a. Try a date time format string with nanosecond precision
// https://github.com/influxdata/influxql/blob/1ba470371ec093d57a726b143fe6ccbacf1b452b/ast.go#L3661
tz.datetime_from_str(s, "%Y-%m-%d %H:%M:%S%.f")
@ -51,7 +54,7 @@ fn parse_timestamp_tz(s: &str, tz: chrono_tz::Tz) -> Result<DateTime<FixedOffset
.ok_or(())
})
.map(|ts| ts.with_timezone(&ts.offset().fix()))
.map_err(|_| error::map::query("invalid timestamp string"))
.ok()
}
/// Parse the string and return a `DateTime` using a fixed offset.
@ -60,7 +63,7 @@ fn parse_timestamp_tz(s: &str, tz: chrono_tz::Tz) -> Result<DateTime<FixedOffset
///
/// [`ToTimeLiteral`]: https://github.com/influxdata/influxql/blob/1ba470371ec093d57a726b143fe6ccbacf1b452b/ast.go#L3654-L3655
///
pub fn parse_timestamp(s: &str, tz: Option<chrono_tz::Tz>) -> Result<DateTime<FixedOffset>> {
pub fn parse_timestamp(s: &str, tz: Option<chrono_tz::Tz>) -> Option<Timestamp> {
match tz {
Some(tz) => parse_timestamp_tz(s, tz),
// We could have mapped None => Utc and called parse_timestamp_tz, however,

View File

@ -7,7 +7,6 @@ license.workspace = true
[dependencies]
arrow = { workspace = true, features = ["prettyprint"] }
chrono = { version = "0.4", default-features = false }
chrono-tz = { version = "0.8" }
datafusion = { workspace = true }
datafusion_util = { path = "../datafusion_util" }
@ -25,6 +24,7 @@ thiserror = "1.0"
workspace-hack = { version = "0.1", path = "../workspace-hack" }
[dev-dependencies] # In alphabetical order
chrono = { version = "0.4", default-features = false }
test_helpers = { path = "../test_helpers" }
assert_matches = "1"
insta = { version = "1", features = ["yaml"] }

View File

@ -19,6 +19,7 @@ pub(crate) fn not_implemented<T>(feature: impl Into<String>) -> Result<T> {
/// making them convenient to use with functions like `map_err`.
pub(crate) mod map {
use datafusion::common::DataFusionError;
use influxdb_influxql_parser::time_range::ExprError;
use thiserror::Error;
#[derive(Debug, Error)]
@ -47,6 +48,14 @@ pub(crate) mod map {
DataFusionError::NotImplemented(feature.into())
}
/// Map an [`ExprError`] to a DataFusion error.
pub(crate) fn expr_error(err: ExprError) -> DataFusionError {
match err {
ExprError::Expression(s) => query(s),
ExprError::Internal(s) => internal(s),
}
}
#[cfg(test)]
mod test {
use crate::plan::error::map::PlannerError;

View File

@ -1,588 +1,16 @@
//! APIs for transforming InfluxQL [expressions][influxdb_influxql_parser::expression::Expr].
use crate::plan::error;
use crate::plan::timestamp::parse_timestamp;
use crate::plan::util::binary_operator_to_df_operator;
use datafusion::common::{DataFusionError, Result, ScalarValue};
use datafusion::logical_expr::{binary_expr, lit, now, BinaryExpr, Expr as DFExpr, Operator};
use influxdb_influxql_parser::expression::{Binary, BinaryOperator, Call};
use influxdb_influxql_parser::functions::is_now_function;
use influxdb_influxql_parser::{expression::Expr, literal::Literal};
use datafusion::common::{Result, ScalarValue};
use datafusion::logical_expr::{lit, Expr as DFExpr};
use influxdb_influxql_parser::expression::Expr;
use influxdb_influxql_parser::time_range::duration_expr_to_nanoseconds;
type ExprResult = Result<DFExpr>;
/// Transform an InfluxQL expression, to a DataFusion logical [`Expr`][DFExpr],
/// applying rules specific to time-range expressions. When possible, literal values are folded.
///
/// ## NOTEs
///
/// The rules applied to this transformation are determined from
/// the Go InfluxQL parser and treated as the source of truth in the
/// absence of an official specification. Most of the implementation
/// is sourced from the [`getTimeRange`][] and [`Reduce`][] functions.
///
/// A [time-range][] expression is determined when either the left or right
/// hand side of a [`ConditionalExpression`][influxdb_influxql_parser::expression::ConditionalExpression]
/// has a single node that refers to a `time` field. Whilst most of InfluxQL
/// performs comparisons of fields using case-sensitive matches, this is a
/// case-insensitive match, per the [`conditionExpr`][conditionExpr] function.
///
/// Binary expressions, where the left and right hand sides are strings, are
/// treated as a string concatenation operation. All other expressions are
/// treated as arithmetic expressions.
///
/// Literal values interpreted as follows:
///
/// * single-quoted strings are interpreted as timestamps when either the left or right
/// hand side of the binary expression is numeric.
/// * integer and float values as nanosecond offsets from the Unix epoch.
/// * The Go implementation may interpret a number as a timestamp or duration,
/// depending on context, however, in reality both are just offsets from the Unix epoch.
///
/// [time range]: https://docs.influxdata.com/influxdb/v1.8/query_language/explore-data/#absolute-time
/// [`getTimeRange`]: https://github.com/influxdata/influxql/blob/1ba470371ec093d57a726b143fe6ccbacf1b452b/ast.go#L5788-L5791
/// [`Reduce`]: https://github.com/influxdata/influxql/blob/1ba470371ec093d57a726b143fe6ccbacf1b452b/ast.go#L4850-L4852
/// [conditionExpr]: https://github.com/influxdata/influxql/blob/1ba470371ec093d57a726b143fe6ccbacf1b452b/ast.go#L5751-L5756
/// [`TZ`]: https://docs.influxdata.com/influxdb/v1.8/query_language/explore-data/#the-time-zone-clause
pub(in crate::plan) fn time_range_to_df_expr(expr: &Expr, tz: Option<chrono_tz::Tz>) -> ExprResult {
let df_expr = reduce_expr(expr, tz)?;
// Attempt to coerce the final expression into a timestamp
Ok(match df_expr {
// timestamp literals require no transformation and Call has
// already been validated as a now() function call.
DFExpr::Literal(ScalarValue::TimestampNanosecond(..))
| DFExpr::ScalarFunction { .. }
| DFExpr::BinaryExpr { .. } => df_expr,
DFExpr::Literal(ScalarValue::Utf8(Some(s))) => {
parse_timestamp_df_expr(&s, tz).map_err(map_expr_err(expr))?
}
DFExpr::Literal(ScalarValue::IntervalMonthDayNano(Some(d))) => {
DFExpr::Literal(ScalarValue::TimestampNanosecond(Some(d as i64), None))
}
DFExpr::Literal(ScalarValue::Float64(Some(v))) => {
DFExpr::Literal(ScalarValue::TimestampNanosecond(Some(v as i64), None))
}
DFExpr::Literal(ScalarValue::Int64(Some(v))) => {
DFExpr::Literal(ScalarValue::TimestampNanosecond(Some(v), None))
}
_ => return error::query("invalid time range expression"),
})
}
/// Simplifies `expr` to an InfluxQL duration and returns a DataFusion interval.
///
/// Returns an error if `expr` is not a duration expression.
pub(super) fn expr_to_df_interval_dt(expr: &Expr) -> ExprResult {
let ns = duration_expr_to_nanoseconds(expr)?;
let ns = duration_expr_to_nanoseconds(expr).map_err(error::map::expr_error)?;
Ok(lit(ScalarValue::new_interval_mdn(0, 0, ns)))
}
/// Reduces an InfluxQL duration `expr` to a nanosecond interval.
pub(super) fn duration_expr_to_nanoseconds(expr: &Expr) -> Result<i64> {
let df_expr = reduce_expr(expr, None)?;
match df_expr {
DFExpr::Literal(ScalarValue::IntervalMonthDayNano(Some(v))) => Ok(v as i64),
DFExpr::Literal(ScalarValue::Float64(Some(v))) => Ok(v as i64),
DFExpr::Literal(ScalarValue::Int64(Some(v))) => Ok(v),
_ => error::query("invalid duration expression"),
}
}
fn map_expr_err(expr: &Expr) -> impl Fn(DataFusionError) -> DataFusionError + '_ {
move |err| {
error::map::query(format!(
"invalid expression \"{}\": {}",
expr,
match err {
DataFusionError::Plan(str) => str,
_ => err.to_string(),
}
))
}
}
fn reduce_expr(expr: &Expr, tz: Option<chrono_tz::Tz>) -> ExprResult {
match expr {
Expr::Binary(v) => reduce_binary_expr(v, tz).map_err(map_expr_err(expr)),
Expr::Call (Call { name, .. }) => {
if !is_now_function(name) {
return error::query(
format!("invalid function call '{name}'"),
);
}
Ok(now())
}
Expr::Nested(expr) => reduce_expr(expr, tz),
Expr::Literal(val) => match val {
Literal::Integer(v) => Ok(lit(*v)),
Literal::Float(v) => Ok(lit(*v)),
Literal::String(v) => Ok(lit(v.clone())),
Literal::Timestamp(v) => Ok(lit(ScalarValue::TimestampNanosecond(
Some(v.timestamp_nanos()),
None,
))),
Literal::Duration(v) => Ok(lit(ScalarValue::new_interval_mdn(0, 0, **v))),
_ => error::query(format!(
"found literal '{val}', expected duration, float, integer, or timestamp string"
)),
},
Expr::VarRef { .. } | Expr::BindParameter(_) | Expr::Wildcard(_) | Expr::Distinct(_) => error::query(format!(
"found symbol '{expr}', expected now() or a literal duration, float, integer and timestamp string"
)),
}
}
fn reduce_binary_expr(expr: &Binary, tz: Option<chrono_tz::Tz>) -> ExprResult {
let lhs = reduce_expr(&expr.lhs, tz)?;
let op = expr.op;
let rhs = reduce_expr(&expr.rhs, tz)?;
match lhs {
DFExpr::Literal(ScalarValue::IntervalMonthDayNano(Some(v))) => {
reduce_binary_lhs_duration_df_expr(v, op, &rhs, tz)
}
DFExpr::Literal(ScalarValue::Int64(Some(v))) => {
reduce_binary_lhs_integer_df_expr(v, op, &rhs, tz)
}
DFExpr::Literal(ScalarValue::Float64(Some(v))) => {
reduce_binary_lhs_float_df_expr(v, op, &rhs)
}
DFExpr::Literal(ScalarValue::TimestampNanosecond(Some(v), _)) => {
reduce_binary_lhs_timestamp_df_expr(v, op, &rhs, tz)
}
DFExpr::ScalarFunction { .. } => {
reduce_binary_scalar_df_expr(&lhs, op, &expr_to_interval_df_expr(&rhs, tz)?)
}
DFExpr::Literal(ScalarValue::Utf8(Some(v))) => {
reduce_binary_lhs_string_df_expr(&v, op, &rhs, tz)
}
_ => Ok(DFExpr::BinaryExpr(BinaryExpr {
left: Box::new(lhs),
op: binary_operator_to_df_operator(op),
right: Box::new(rhs),
})),
}
}
/// Reduce `duration OP expr`.
///
/// ```text
/// duration = duration ( ADD | SUB ) ( duration | NOW() )
/// duration = duration ( MUL | DIV ) ( float | integer )
/// timestamp = duration ADD string
/// timestamp = duration ADD timestamp
/// ```
fn reduce_binary_lhs_duration_df_expr(
lhs: i128,
op: BinaryOperator,
rhs: &DFExpr,
tz: Option<chrono_tz::Tz>,
) -> Result<DFExpr> {
match rhs {
DFExpr::Literal(val) => match val {
// durations may be added and subtracted from other durations
ScalarValue::IntervalMonthDayNano(Some(d)) => match op {
BinaryOperator::Add => {
Ok(lit(ScalarValue::new_interval_mdn(0, 0, (lhs + *d) as i64)))
}
BinaryOperator::Sub => {
Ok(lit(ScalarValue::new_interval_mdn(0, 0, (lhs - *d) as i64)))
}
_ => error::query(format!("found operator '{op}', expected +, -")),
},
// durations may only be scaled by float literals
ScalarValue::Float64(Some(v)) => {
reduce_binary_lhs_duration_df_expr(lhs, op, &lit(*v as i64), tz)
}
ScalarValue::Int64(Some(v)) => match op {
BinaryOperator::Mul => {
Ok(lit(ScalarValue::new_interval_mdn(0, 0, lhs as i64 * *v)))
}
BinaryOperator::Div => {
Ok(lit(ScalarValue::new_interval_mdn(0, 0, lhs as i64 / *v)))
}
_ => error::query(format!("found operator '{op}', expected *, /")),
},
// A timestamp may be added to a duration
ScalarValue::TimestampNanosecond(Some(v), _) if matches!(op, BinaryOperator::Add) => {
Ok(lit(ScalarValue::TimestampNanosecond(
Some(*v + lhs as i64),
None,
)))
}
ScalarValue::Utf8(Some(s)) => {
reduce_binary_lhs_duration_df_expr(lhs, op, &parse_timestamp_df_expr(s, tz)?, tz)
}
// This should not occur, as all the DataFusion literal values created by this process
// are handled above.
_ => error::internal(format!(
"unexpected DataFusion literal '{rhs}' for duration expression"
)),
},
DFExpr::ScalarFunction { .. } => reduce_binary_scalar_df_expr(
&expr_to_interval_df_expr(&lit(ScalarValue::new_interval_mdn(0, 0, lhs as i64)), tz)?,
op,
rhs,
),
_ => error::query("invalid duration expression"),
}
}
/// Reduce `integer OP expr`.
///
/// ```text
/// integer = integer ( ADD | SUB | MUL | DIV | MOD | BitwiseAND | BitwiseOR | BitwiseXOR ) integer
/// float = integer as float OP float
/// timestamp = integer as timestamp OP duration
/// ```
fn reduce_binary_lhs_integer_df_expr(
lhs: i64,
op: BinaryOperator,
rhs: &DFExpr,
tz: Option<chrono_tz::Tz>,
) -> ExprResult {
match rhs {
DFExpr::Literal(ScalarValue::Float64(Some(_))) => {
reduce_binary_lhs_float_df_expr(lhs as f64, op, rhs)
}
DFExpr::Literal(ScalarValue::Int64(Some(v))) => Ok(lit(op.reduce(lhs, *v))),
DFExpr::Literal(ScalarValue::IntervalMonthDayNano(Some(_))) => {
reduce_binary_lhs_timestamp_df_expr(lhs, op, rhs, tz)
}
DFExpr::ScalarFunction { .. } | DFExpr::Literal(ScalarValue::TimestampNanosecond(..)) => {
reduce_binary_lhs_duration_df_expr(lhs.into(), op, rhs, tz)
}
DFExpr::Literal(ScalarValue::Utf8(Some(s))) => {
reduce_binary_lhs_duration_df_expr(lhs.into(), op, &parse_timestamp_df_expr(s, tz)?, tz)
}
_ => error::query("invalid integer expression"),
}
}
/// Reduce `float OP expr`.
///
/// ```text
/// float = float ( ADD | SUB | MUL | DIV | MOD ) ( float | integer)
/// ```
fn reduce_binary_lhs_float_df_expr(lhs: f64, op: BinaryOperator, rhs: &DFExpr) -> ExprResult {
Ok(lit(match rhs {
DFExpr::Literal(ScalarValue::Float64(Some(rhs))) => op
.try_reduce(lhs, *rhs)
.ok_or_else(|| error::map::query("invalid operator for float expression"))?,
DFExpr::Literal(ScalarValue::Int64(Some(rhs))) => op
.try_reduce(lhs, *rhs)
.ok_or_else(|| error::map::query("invalid operator for float expression"))?,
_ => return error::query("invalid float expression"),
}))
}
/// Reduce `timestamp OP expr`.
///
/// The right-hand `expr` must be of a type that can be
/// coalesced to a duration, which includes a `duration`, `integer` or a
/// `string`. A `string` is parsed as a timestamp an interpreted as
/// the number of nanoseconds from the Unix epoch.
///
/// ```text
/// timestamp = timestamp ( ADD | SUB ) ( duration | integer | string | timestamp )
/// ```
fn reduce_binary_lhs_timestamp_df_expr(
lhs: i64,
op: BinaryOperator,
rhs: &DFExpr,
tz: Option<chrono_tz::Tz>,
) -> ExprResult {
match rhs {
DFExpr::Literal(ScalarValue::IntervalMonthDayNano(Some(d))) => match op {
BinaryOperator::Add => Ok(lit(ScalarValue::TimestampNanosecond(Some(lhs + *d as i64), None))),
BinaryOperator::Sub => Ok(lit(ScalarValue::TimestampNanosecond(Some(lhs - *d as i64), None))),
_ => error::query(
format!("invalid operator '{op}' for timestamp and duration: expected +, -"),
),
}
DFExpr::Literal(ScalarValue::Int64(_))
// NOTE: This is a slight deviation from InfluxQL, for which the only valid binary
// operator for two timestamps is subtraction. By converting the timestamp to a
// duration and calling this function recursively, we permit the addition operator.
| DFExpr::Literal(ScalarValue::TimestampNanosecond(..))
| DFExpr::Literal(ScalarValue::Utf8(_)) => reduce_binary_lhs_timestamp_df_expr(
lhs,
op,
&expr_to_interval_df_expr(rhs, tz)?,
tz,
),
_ => error::query(
format!("invalid expression '{rhs}': expected duration, integer or timestamp string"),
),
}
}
/// Reduce `expr ( + | - ) expr`.
///
/// This API is called when either the left or right hand expression is
/// a scalar function and ensures the operator is either addition or subtraction.
fn reduce_binary_scalar_df_expr(lhs: &DFExpr, op: BinaryOperator, rhs: &DFExpr) -> ExprResult {
match op {
BinaryOperator::Add => Ok(binary_expr(lhs.clone(), Operator::Plus, rhs.clone())),
BinaryOperator::Sub => Ok(binary_expr(lhs.clone(), Operator::Minus, rhs.clone())),
_ => error::query(format!("found operator '{op}', expected +, -")),
}
}
/// Converts `rhs` to a DataFusion interval literal.
fn expr_to_interval_df_expr(expr: &DFExpr, tz: Option<chrono_tz::Tz>) -> ExprResult {
Ok(lit(ScalarValue::new_interval_mdn(
0,
0,
match expr {
DFExpr::Literal(ScalarValue::IntervalMonthDayNano(Some(d))) => *d as i64,
DFExpr::Literal(ScalarValue::Int64(Some(v))) => *v,
DFExpr::Literal(ScalarValue::TimestampNanosecond(Some(v), _)) => *v,
DFExpr::Literal(ScalarValue::Utf8(Some(s))) => parse_timestamp_nanos(s, tz)?,
_ => return error::query(format!("unable to cast '{expr}' to duration")),
},
)))
}
/// Reduce `string OP expr`.
///
/// If `expr` is a string, concatenates the two values and returns a new string.
/// If `expr` is a duration, integer or timestamp, the left-hand
/// string is parsed as a timestamp and the expression evaluated as
/// `timestamp OP expr`
fn reduce_binary_lhs_string_df_expr(
lhs: &str,
op: BinaryOperator,
rhs: &DFExpr,
tz: Option<chrono_tz::Tz>,
) -> ExprResult {
match rhs {
DFExpr::Literal(ScalarValue::Utf8(Some(s))) => match op {
// concatenate the two strings
BinaryOperator::Add => Ok(lit(lhs.to_string() + s)),
_ => reduce_binary_lhs_timestamp_df_expr(parse_timestamp_nanos(lhs, tz)?, op, rhs, tz),
},
DFExpr::Literal(ScalarValue::IntervalMonthDayNano(_))
| DFExpr::Literal(ScalarValue::TimestampNanosecond(..))
| DFExpr::Literal(ScalarValue::Int64(_)) => {
reduce_binary_lhs_timestamp_df_expr(parse_timestamp_nanos(lhs, tz)?, op, rhs, tz)
}
_ => error::query(format!(
"found '{rhs}', expected duration, integer or timestamp string"
)),
}
}
fn parse_timestamp_nanos(s: &str, tz: Option<chrono_tz::Tz>) -> Result<i64> {
parse_timestamp(s, tz)
.map(|ts| ts.timestamp_nanos())
.map_err(|_| error::map::query(format!("'{s}' is not a valid timestamp")))
}
/// Parse s as a timestamp in the specified timezone and return the timestamp
/// as a literal timestamp expression.
fn parse_timestamp_df_expr(s: &str, tz: Option<chrono_tz::Tz>) -> ExprResult {
Ok(lit(ScalarValue::TimestampNanosecond(
Some(parse_timestamp_nanos(s, tz)?),
None,
)))
}
#[cfg(test)]
mod test {
use super::*;
use influxdb_influxql_parser::expression::ConditionalExpression;
use test_helpers::assert_error;
#[test]
fn test_rewrite_time_expression_no_timezone() {
fn process_expr(s: &str) -> ExprResult {
let cond: ConditionalExpression =
s.parse().expect("unexpected error parsing expression");
time_range_to_df_expr(cond.expr().unwrap(), None)
}
macro_rules! assert_expr {
($S: expr, $EXPECTED: expr) => {
let expr = process_expr($S).unwrap();
assert_eq!(expr.to_string(), $EXPECTED);
};
}
//
// Valid literals
//
// Duration
assert_expr!("1d", "TimestampNanosecond(86400000000000, None)");
// Single integer interpreted as a Unix nanosecond epoch
assert_expr!(
"1157082310000000000",
"TimestampNanosecond(1157082310000000000, None)"
);
// Single float interpreted as a Unix nanosecond epoch
assert_expr!(
"1157082310000000000.0",
"TimestampNanosecond(1157082310000000000, None)"
);
// Single string interpreted as a timestamp
assert_expr!(
"'2004-04-09 02:33:45.123456789'",
"TimestampNanosecond(1081478025123456789, None)"
);
// now
assert_expr!("now()", "now()");
//
// Expressions
//
// now() OP expr
assert_expr!(
"now() - 5m",
r#"now() - IntervalMonthDayNano("300000000000")"#
);
assert_expr!(
"(now() - 5m)",
r#"now() - IntervalMonthDayNano("300000000000")"#
);
assert_expr!(
"now() - 5m - 60m",
r#"now() - IntervalMonthDayNano("300000000000") - IntervalMonthDayNano("3600000000000")"#
);
assert_expr!("now() - 500", r#"now() - IntervalMonthDayNano("500")"#);
assert_expr!(
"now() - (5m + 60m)",
r#"now() - IntervalMonthDayNano("3900000000000")"#
);
// expr OP now()
assert_expr!(
"5m - now()",
r#"IntervalMonthDayNano("300000000000") - now()"#
);
assert_expr!(
"5m + now()",
r#"IntervalMonthDayNano("300000000000") + now()"#
);
// duration OP expr
assert_expr!("1w3d + 1d", "TimestampNanosecond(950400000000000, None)");
assert_expr!("1w3d - 1d", "TimestampNanosecond(777600000000000, None)");
// string OP expr
assert_expr!(
"'2004-04-09' - '2004-04-08'",
"TimestampNanosecond(86400000000000, None)"
);
assert_expr!(
"'2004-04-09' + '02:33:45'",
"TimestampNanosecond(1081478025000000000, None)"
);
// integer OP expr
assert_expr!(
"1157082310000000000 - 1s",
"TimestampNanosecond(1157082309000000000, None)"
);
// nested evaluation order
assert_expr!(
"now() - (6m - (1m * 5))",
r#"now() - IntervalMonthDayNano("60000000000")"#
);
// Fallible
use DataFusionError::Plan;
assert_error!(process_expr("foo + 1"), Plan(ref s) if s == "invalid expression \"foo + 1\": found symbol 'foo', expected now() or a literal duration, float, integer and timestamp string");
assert_error!(process_expr("'2004-04-09' + false"), Plan(ref s) if s == "invalid expression \"'2004-04-09' + false\": found literal 'false', expected duration, float, integer, or timestamp string");
assert_error!(process_expr("1s * 1s"), Plan(ref s) if s == "invalid expression \"1000ms * 1000ms\": found operator '*', expected +, -");
assert_error!(process_expr("1s + 0.5"), Plan(ref s) if s == "invalid expression \"1000ms + 0.5\": found operator '+', expected *, /");
assert_error!(process_expr("'2004-04-09T'"), Plan(ref s) if s == "invalid expression \"'2004-04-09T'\": '2004-04-09T' is not a valid timestamp");
assert_error!(process_expr("now() + now()"), Plan(ref s) if s == "invalid expression \"now() + now()\": unable to cast 'now()' to duration");
assert_error!(process_expr("now() * 1"), Plan(ref s) if s == "invalid expression \"now() * 1\": found operator '*', expected +, -");
assert_error!(process_expr("'2' + now()"), Plan(ref s) if s == "invalid expression \"'2' + now()\": found 'now()', expected duration, integer or timestamp string");
assert_error!(process_expr("'2' + '3'"), Plan(ref s) if s == "invalid expression \"'2' + '3'\": '23' is not a valid timestamp");
assert_error!(process_expr("'2' + '3' + 10s"), Plan(ref s) if s == "invalid expression \"'2' + '3' + 10s\": '23' is not a valid timestamp");
}
#[test]
fn test_rewrite_time_expression_with_timezone() {
fn process_expr(s: &str) -> ExprResult {
let cond: ConditionalExpression =
s.parse().expect("unexpected error parsing expression");
time_range_to_df_expr(cond.expr().unwrap(), Some(chrono_tz::Australia::Hobart))
}
macro_rules! assert_expr {
($S: expr, $EXPECTED: expr) => {
let expr = process_expr($S).unwrap();
assert_eq!(expr.to_string(), $EXPECTED);
};
}
assert_expr!(
"'2004-04-09 10:05:00.123456789'",
"TimestampNanosecond(1081469100123456789, None)" // 2004-04-09T00:05:00.123456789Z
);
assert_expr!(
"'2004-04-09'",
"TimestampNanosecond(1081432800000000000, None)" // 2004-04-08T14:00:00Z
);
assert_expr!(
"'2004-04-09T10:05:00.123456789Z'",
"TimestampNanosecond(1081505100123456789, None)" // 2004-04-09T10:05:00.123456789Z
);
}
#[test]
fn test_expr_to_df_interval_dt() {
fn parse(s: &str) -> ExprResult {
let expr = s
.parse::<ConditionalExpression>()
.unwrap()
.expr()
.unwrap()
.clone();
expr_to_df_interval_dt(&expr)
}
let cases = vec![
("10s", ScalarValue::new_interval_mdn(0, 0, 10_000_000_000)),
(
"10s + 1d",
ScalarValue::new_interval_mdn(0, 0, 86_410_000_000_000),
),
(
"5d10ms",
ScalarValue::new_interval_mdn(0, 0, 432_000_010_000_000),
),
(
"-2d10ms",
ScalarValue::new_interval_mdn(0, 0, -172800010000000),
),
(
"-2d10ns",
ScalarValue::new_interval_mdn(0, 0, -172800000000010),
),
];
for (interval_str, expected_scalar) in cases {
let parsed_interval = parse(interval_str).unwrap();
let DFExpr::Literal(actual_scalar) = parsed_interval else {
panic!("Expected literal Expr, got {parsed_interval:?}");
};
assert_eq!(actual_scalar, expected_scalar, "Actual: {actual_scalar:?}");
}
}
}

View File

@ -7,7 +7,6 @@ mod ir;
mod planner;
mod rewriter;
mod test_utils;
mod timestamp;
mod util;
mod util_copy;
mod var_ref;

View File

@ -4,9 +4,7 @@ mod test_utils;
mod time_range;
use crate::plan::error;
use crate::plan::influxql_time_range_expression::{
duration_expr_to_nanoseconds, expr_to_df_interval_dt, time_range_to_df_expr,
};
use crate::plan::influxql_time_range_expression::expr_to_df_interval_dt;
use crate::plan::ir::{DataSource, Field, Select, SelectQuery};
use crate::plan::planner::select::{
fields_to_exprs_no_nulls, make_tag_key_column_meta, plan_with_sort, ProjectionInfo,
@ -55,6 +53,10 @@ use influxdb_influxql_parser::show_measurements::{
use influxdb_influxql_parser::show_tag_keys::ShowTagKeysStatement;
use influxdb_influxql_parser::show_tag_values::{ShowTagValuesStatement, WithKeyClause};
use influxdb_influxql_parser::simple_from_clause::ShowFromClause;
use influxdb_influxql_parser::time_range::{
duration_expr_to_nanoseconds, reduce_time_expr, ReduceContext,
};
use influxdb_influxql_parser::timestamp::Timestamp;
use influxdb_influxql_parser::{
common::{MeasurementName, WhereClause},
expression::Expr as IQLExpr,
@ -633,7 +635,7 @@ impl<'a> InfluxQLToLogicalPlan<'a> {
select_exprs[time_column_index] = if let Some(dim) = ctx.group_by.and_then(|gb| gb.time_dimension()) {
let stride = expr_to_df_interval_dt(&dim.interval)?;
let offset = if let Some(offset) = &dim.offset {
duration_expr_to_nanoseconds(offset)?
duration_expr_to_nanoseconds(offset).map_err(error::map::expr_error)?
} else {
0
};
@ -981,14 +983,31 @@ impl<'a> InfluxQLToLogicalPlan<'a> {
return error::query("invalid time comparison operator: !=");
}
let rc = ReduceContext {
now: Some(Timestamp::from(
self.s.execution_props().query_execution_start_time,
)),
tz: ctx.tz,
};
if lhs_time {
(
self.conditional_to_df_expr(ctx, lhs, schemas)?,
time_range_to_df_expr(find_expr(rhs)?, ctx.tz)?,
self.expr_to_df_expr(
ctx,
ExprScope::Where,
&reduce_time_expr(&rc, find_expr(rhs)?).map_err(error::map::expr_error)?,
schemas,
)?,
)
} else {
(
time_range_to_df_expr(find_expr(lhs)?, ctx.tz)?,
self.expr_to_df_expr(
ctx,
ExprScope::Where,
&reduce_time_expr(&rc, find_expr(lhs)?).map_err(error::map::expr_error)?,
schemas,
)?,
self.conditional_to_df_expr(ctx, rhs, schemas)?,
)
}
@ -1075,7 +1094,7 @@ impl<'a> InfluxQLToLogicalPlan<'a> {
Literal::String(v) => Ok(lit(v)),
Literal::Boolean(v) => Ok(lit(*v)),
Literal::Timestamp(v) => Ok(lit(ScalarValue::TimestampNanosecond(
Some(v.timestamp()),
Some(v.timestamp_nanos()),
None,
))),
Literal::Duration(_) => error::not_implemented("duration literal"),

View File

@ -21,7 +21,7 @@ use std::collections::Bound;
///
/// Combining relational operators like `time > now() - 5s` and equality
/// operators like `time = <timestamp>` with a disjunction (`OR`)
/// will evaluate to false, like InfluxQL.
/// will evaluate to `false`, like InfluxQL.
///
/// # Background
///
@ -214,7 +214,11 @@ pub fn rewrite_time_range_exprs(
let lhs = if let Some(expr) = rw.time_range.as_expr() {
Some(expr)
} else if !rw.equality.is_empty() {
disjunction(rw.equality)
disjunction(rw.equality.iter().map(|t| {
"time"
.as_expr()
.eq(lit(ScalarValue::TimestampNanosecond(Some(*t), None)))
}))
} else {
None
};
@ -263,7 +267,8 @@ fn is_time_range(expr: &Expr) -> bool {
}
/// Represents the lower bound, in nanoseconds, of a [`TimeRange`].
pub struct LowerBound(Bound<i64>);
#[derive(Clone, Debug)]
struct LowerBound(Bound<i64>);
impl LowerBound {
/// Create a new, time bound that is unbounded
@ -349,7 +354,8 @@ impl Ord for LowerBound {
}
/// Represents the upper bound, in nanoseconds, of a [`TimeRange`].
pub struct UpperBound(Bound<i64>);
#[derive(Clone, Debug)]
struct UpperBound(Bound<i64>);
impl UpperBound {
/// Create a new, unbounded upper bound.
@ -435,8 +441,8 @@ impl Ord for UpperBound {
}
/// Represents a time range, with a single lower and upper bound.
#[derive(Default, PartialEq, Eq)]
struct TimeRange {
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(super) struct TimeRange {
lower: LowerBound,
upper: UpperBound,
}
@ -466,7 +472,7 @@ struct SeparateTimeRange<'a> {
///
/// ```sql
/// time = '2004-04-09T12:00:00Z'
equality: Vec<Expr>,
equality: Vec<i64>,
}
impl<'a> SeparateTimeRange<'a> {
@ -507,17 +513,17 @@ impl<'a> TreeNodeVisitor for SeparateTimeRange<'a> {
op: op @ (Eq | NotEq | Gt | Lt | GtEq | LtEq),
right,
}) if is_time_column(left) | is_time_column(right) => {
self.stack.push(None);
if matches!(op, Eq | NotEq) {
let node = self.simplifier.simplify(node.clone())?;
self.equality.push(node);
return Ok(VisitRecursion::Continue);
if matches!(op, NotEq) {
// Stop recursing, as != is an invalid operator for time expressions
return Ok(VisitRecursion::Stop);
}
self.stack.push(None);
/// Op is the limited set of operators expected from here on,
/// to avoid repeated wildcard match arms with unreachable!().
enum Op {
Eq,
Gt,
GtEq,
Lt,
@ -526,20 +532,22 @@ impl<'a> TreeNodeVisitor for SeparateTimeRange<'a> {
// Map the DataFusion Operator to Op
let op = match op {
Eq => Op::Eq,
Gt => Op::Gt,
GtEq => Op::GtEq,
Lt => Op::Lt,
LtEq => Op::LtEq,
_ => unreachable!("expected: Gt | Lt | GtEq | LtEq"),
_ => unreachable!("expected: Eq | Gt | GtEq | Lt | LtEq"),
};
let (expr, op) = if is_time_column(left) {
(right, op)
} else {
// swap the operators when the conditional is `expression OP "time"`
(
left,
match op {
Op::Eq => Op::Eq,
// swap the relational operators when the conditional is `expression OP "time"`
Op::Gt => Op::Lt,
Op::GtEq => Op::LtEq,
Op::Lt => Op::Gt,
@ -562,6 +570,20 @@ impl<'a> TreeNodeVisitor for SeparateTimeRange<'a> {
};
match op {
Op::Eq => {
if self.time_range.is_unbounded() {
self.equality.push(ts);
} else {
// Stop recursing, as we have observed incompatible
// time conditions using equality and relational operators
return Ok(VisitRecursion::Stop);
};
}
Op::Gt | Op::GtEq | Op::Lt | Op::LtEq if !self.equality.is_empty() => {
// Stop recursing, as we have observed incompatible
// time conditions using equality and relational operators
return Ok(VisitRecursion::Stop);
}
Op::Gt => {
let ts = LowerBound::excluded(ts);
if ts > self.time_range.lower {
@ -627,171 +649,202 @@ impl<'a> TreeNodeVisitor for SeparateTimeRange<'a> {
#[cfg(test)]
mod test {
use crate::plan::planner::test_utils::{execution_props, new_schemas};
use crate::plan::planner::time_range::{LowerBound, UpperBound};
use datafusion::logical_expr::{binary_expr, lit, lit_timestamp_nano, now, Operator};
use datafusion::optimizer::simplify_expressions::{ExprSimplifier, SimplifyContext};
use datafusion_util::AsExpr;
use std::sync::Arc;
#[test]
fn test_rewrite_time_range_exprs() {
use crate::plan::planner::time_range::rewrite_time_range_exprs;
use datafusion::common::ScalarValue as V;
// #[test]
// fn test_split_exprs() {
// use super::{LowerBound as L, TimeCondition as TC, UpperBound as U};
// use datafusion::common::ScalarValue as V;
//
// let props = execution_props();
// let schema = Arc::new(ArrowSchema::new(vec![ArrowField::new(
// "time",
// (&InfluxColumnType::Timestamp).into(),
// false,
// )]));
// let df_schema = schema.to_dfschema_ref().unwrap();
// let simplify_context = SimplifyContext::new(&props).with_schema(Arc::clone(&df_schema));
// let simplifier = ExprSimplifier::new(simplify_context);
//
// use arrow::datatypes::{Field as ArrowField, Schema as ArrowSchema};
//
// let split_exprs = |expr| super::split(expr, &simplifier).unwrap();
//
// macro_rules! range {
// (lower=$LOWER:literal) => {
// TC::Range(TimeRange{lower: L::included($LOWER), upper: U::unbounded()})
// };
// (lower=$LOWER:literal, upper ex=$UPPER:literal) => {
// TC::Range(TimeRange{lower: L::included($LOWER), upper: U::excluded($UPPER)})
// };
// (lower ex=$LOWER:literal) => {
// TC::Range(TimeRange{lower: L::excluded($LOWER), upper: U::unbounded()})
// };
// (upper=$UPPER:literal) => {
// TC::Range(TimeRange{lower: L::unbounded(), upper: U::included($UPPER)})
// };
// (upper ex=$UPPER:literal) => {
// TC::Range(TimeRange{lower: L::unbounded(), upper: U::excluded($UPPER)})
// };
// (list=$($TS:literal),*) => {
// TC::List(vec![$($TS),*])
// }
// }
//
// let expr = "time"
// .as_expr()
// .gt_eq(now() - lit(V::new_interval_dt(0, 1000)));
// let (cond, tr) = split_exprs(expr);
// assert!(cond.is_none());
// assert_eq!(tr.unwrap(), range!(lower = 1672531199000000000));
//
// // reduces the lower bound to a single expression
// let expr = "time"
// .as_expr()
// .gt_eq(now() - lit(V::new_interval_dt(0, 1000)))
// .and(
// "time"
// .as_expr()
// .gt_eq(now() - lit(V::new_interval_dt(0, 500))),
// );
// let (cond, tr) = split_exprs(expr);
// assert!(cond.is_none());
// assert_eq!(tr.unwrap(), range!(lower = 1672531199500000000));
//
// let expr = "time"
// .as_expr()
// .lt_eq(now() - lit(V::new_interval_dt(0, 1000)));
// let (cond, tr) = split_exprs(expr);
// assert!(cond.is_none());
// assert_eq!(tr.unwrap(), range!(upper = 1672531199000000000));
//
// // reduces the upper bound to a single expression
// let expr = "time"
// .as_expr()
// .lt_eq(now() + lit(V::new_interval_dt(0, 1000)))
// .and(
// "time"
// .as_expr()
// .lt_eq(now() + lit(V::new_interval_dt(0, 500))),
// );
// let (cond, tr) = split_exprs(expr);
// assert!(cond.is_none());
// assert_eq!(tr.unwrap(), range!(upper = 1672531200500000000));
//
// let expr = "time"
// .as_expr()
// .gt_eq(now() - lit(V::new_interval_dt(0, 1000)))
// .and("time".as_expr().lt(now()));
// let (cond, tr) = split_exprs(expr);
// assert!(cond.is_none());
// assert_eq!(
// tr.unwrap(),
// range!(lower=1672531199000000000, upper ex=1672531200000000000)
// );
//
// let expr = "time"
// .as_expr()
// .gt_eq(now() - lit(V::new_interval_dt(0, 1000)))
// .and("cpu".as_expr().eq(lit("cpu0")));
// let (cond, tr) = split_exprs(expr);
// assert_eq!(cond.unwrap().to_string(), r#"cpu = Utf8("cpu0")"#);
// assert_eq!(tr.unwrap(), range!(lower = 1672531199000000000));
//
// let expr = "time".as_expr().eq(lit_timestamp_nano(0));
// let (cond, tr) = split_exprs(expr);
// assert!(cond.is_none());
// assert_eq!(tr.unwrap(), range!(list = 0));
//
// let expr = "instance"
// .as_expr()
// .eq(lit("instance-01"))
// .or("instance".as_expr().eq(lit("instance-02")))
// .and(
// "time"
// .as_expr()
// .gt_eq(now() - lit(V::new_interval_dt(0, 1000))),
// );
// let (cond, tr) = split_exprs(expr);
// assert_eq!(
// cond.unwrap().to_string(),
// r#"instance = Utf8("instance-01") OR instance = Utf8("instance-02")"#
// );
// assert_eq!(tr.unwrap(), range!(lower = 1672531199000000000));
//
// let expr = "time"
// .as_expr()
// .gt_eq(now() - lit(V::new_interval_dt(0, 1000)))
// .and("time".as_expr().lt(now()))
// .and(
// "cpu"
// .as_expr()
// .eq(lit("cpu0"))
// .or("cpu".as_expr().eq(lit("cpu1"))),
// );
// let (cond, tr) = split_exprs(expr);
// assert_eq!(
// cond.unwrap().to_string(),
// r#"cpu = Utf8("cpu0") OR cpu = Utf8("cpu1")"#
// );
// assert_eq!(
// tr.unwrap(),
// range!(lower=1672531199000000000, upper ex=1672531200000000000)
// );
//
// // time >= now - 60s AND time < now() OR cpu = 'cpu0' OR cpu = 'cpu1'
// //
// // Split the time range, despite using the disjunction (OR) operator
// let expr = "time"
// .as_expr()
// .gt_eq(now() - lit(V::new_interval_dt(0, 1000)))
// .and("time".as_expr().lt(now()))
// .or("cpu"
// .as_expr()
// .eq(lit("cpu0"))
// .or("cpu".as_expr().eq(lit("cpu1"))));
// let (cond, tr) = split_exprs(expr);
// assert_eq!(
// cond.unwrap().to_string(),
// r#"cpu = Utf8("cpu0") OR cpu = Utf8("cpu1")"#
// );
// assert_eq!(
// tr.unwrap(),
// range!(lower=1672531199000000000, upper ex=1672531200000000000)
// );
//
// // time = 0 OR time = 10 AND cpu = 'cpu0'
// let expr = "time".as_expr().eq(lit_timestamp_nano(0)).or("time"
// .as_expr()
// .eq(lit_timestamp_nano(10))
// .and("cpu".as_expr().eq(lit("cpu0"))));
// let (cond, tr) = split_exprs(expr);
// assert_eq!(cond.unwrap().to_string(), r#"cpu = Utf8("cpu0")"#);
// assert_eq!(tr.unwrap(), range!(list = 0, 10));
//
// // no time
// let expr = "f64".as_expr().gt_eq(lit(19.5_f64)).or(binary_expr(
// "f64".as_expr(),
// Operator::RegexMatch,
// lit("foo"),
// ));
// let (cond, tr) = split_exprs(expr);
// assert_eq!(
// cond.unwrap().to_string(),
// r#"f64 >= Float64(19.5) OR (f64 ~ Utf8("foo"))"#
// );
// assert!(tr.is_none());
//
// // fallible
//
// let expr = "time"
// .as_expr()
// .eq(lit_timestamp_nano(0))
// .or("time".as_expr().gt(now()));
// let (cond, tr) = split_exprs(expr);
// assert_eq!(cond.unwrap().to_string(), r#"Boolean(false)"#);
// assert!(tr.is_none());
// }
test_helpers::maybe_start_logging();
let props = execution_props();
let (schemas, _) = new_schemas();
let simplify_context =
SimplifyContext::new(&props).with_schema(Arc::clone(&schemas.df_schema));
let simplifier = ExprSimplifier::new(simplify_context);
let rewrite = |expr| {
rewrite_time_range_exprs(expr, &simplifier)
.unwrap()
.to_string()
};
let expr = "time"
.as_expr()
.gt_eq(now() - lit(V::new_interval_dt(0, 1000)));
assert_eq!(
rewrite(expr),
r#"time >= TimestampNanosecond(1672531199000000000, None)"#
);
// reduces the lower bound to a single expression
let expr = "time"
.as_expr()
.gt_eq(now() - lit(V::new_interval_dt(0, 1000)))
.and(
"time"
.as_expr()
.gt_eq(now() - lit(V::new_interval_dt(0, 500))),
);
assert_eq!(
rewrite(expr),
r#"time >= TimestampNanosecond(1672531199500000000, None)"#
);
let expr = "time"
.as_expr()
.lt_eq(now() - lit(V::new_interval_dt(0, 1000)));
assert_eq!(
rewrite(expr),
r#"time <= TimestampNanosecond(1672531199000000000, None)"#
);
// reduces the upper bound to a single expression
let expr = "time"
.as_expr()
.lt_eq(now() + lit(V::new_interval_dt(0, 1000)))
.and(
"time"
.as_expr()
.lt_eq(now() + lit(V::new_interval_dt(0, 500))),
);
assert_eq!(
rewrite(expr),
r#"time <= TimestampNanosecond(1672531200500000000, None)"#
);
let expr = "time"
.as_expr()
.gt_eq(now() - lit(V::new_interval_dt(0, 1000)))
.and("time".as_expr().lt(now()));
assert_eq!(
rewrite(expr),
r#"time >= TimestampNanosecond(1672531199000000000, None) AND time < TimestampNanosecond(1672531200000000000, None)"#
);
let expr = "time"
.as_expr()
.gt_eq(now() - lit(V::new_interval_dt(0, 1000)))
.and("cpu".as_expr().eq(lit("cpu0")));
assert_eq!(
rewrite(expr),
r#"time >= TimestampNanosecond(1672531199000000000, None) AND cpu = Utf8("cpu0")"#
);
let expr = "time".as_expr().eq(lit_timestamp_nano(0));
assert_eq!(rewrite(expr), r#"time = TimestampNanosecond(0, None)"#);
let expr = "instance"
.as_expr()
.eq(lit("instance-01"))
.or("instance".as_expr().eq(lit("instance-02")))
.and(
"time"
.as_expr()
.gt_eq(now() - lit(V::new_interval_dt(0, 60_000))),
);
assert_eq!(
rewrite(expr),
r#"time >= TimestampNanosecond(1672531140000000000, None) AND (instance = Utf8("instance-01") OR instance = Utf8("instance-02"))"#
);
let expr = "time"
.as_expr()
.gt_eq(now() - lit(V::new_interval_dt(0, 60_000)))
.and("time".as_expr().lt(now()))
.and(
"cpu"
.as_expr()
.eq(lit("cpu0"))
.or("cpu".as_expr().eq(lit("cpu1"))),
);
assert_eq!(
rewrite(expr),
r#"time >= TimestampNanosecond(1672531140000000000, None) AND time < TimestampNanosecond(1672531200000000000, None) AND (cpu = Utf8("cpu0") OR cpu = Utf8("cpu1"))"#
);
// time >= now - 60s AND time < now() OR cpu = 'cpu0' OR cpu = 'cpu1'
//
// Expects the time range to be combined with a conjunction (AND)
let expr = "time"
.as_expr()
.gt_eq(now() - lit(V::new_interval_dt(0, 60_000)))
.and("time".as_expr().lt(now()))
.or("cpu"
.as_expr()
.eq(lit("cpu0"))
.or("cpu".as_expr().eq(lit("cpu1"))));
assert_eq!(
rewrite(expr),
r#"time >= TimestampNanosecond(1672531140000000000, None) AND time < TimestampNanosecond(1672531200000000000, None) AND (cpu = Utf8("cpu0") OR cpu = Utf8("cpu1"))"#
);
// time = 0 OR time = 10 AND cpu = 'cpu0'
let expr = "time".as_expr().eq(lit_timestamp_nano(0)).or("time"
.as_expr()
.eq(lit_timestamp_nano(10))
.and("cpu".as_expr().eq(lit("cpu0"))));
assert_eq!(
rewrite(expr),
r#"(time = TimestampNanosecond(0, None) OR time = TimestampNanosecond(10, None)) AND cpu = Utf8("cpu0")"#
);
// no time
let expr = "f64".as_expr().gt_eq(lit(19.5_f64)).or(binary_expr(
"f64".as_expr(),
Operator::RegexMatch,
lit("foo"),
));
assert_eq!(
rewrite(expr),
"f64 >= Float64(19.5) OR (f64 ~ Utf8(\"foo\"))"
);
// fallible
let expr = "time"
.as_expr()
.eq(lit_timestamp_nano(0))
.or("time".as_expr().gt(now()));
assert_eq!(rewrite(expr), "Boolean(false)");
}
#[test]
fn test_lower_bound_cmp() {
let (a, b) = (LowerBound::unbounded(), LowerBound::unbounded());