From a9ec0720b2ce9157ae76c81d12de6fdfdc83bed8 Mon Sep 17 00:00:00 2001 From: Marco Neumann Date: Tue, 23 Nov 2021 10:14:39 +0100 Subject: [PATCH] feat: allow to format delete predicates as SQL strings This is required to feed them back into the gRPC delete API. --- data_types/src/delete_predicate.rs | 238 ++++++++++++++++++++++++++++- predicate/src/delete_expr.rs | 8 +- 2 files changed, 239 insertions(+), 7 deletions(-) diff --git a/data_types/src/delete_predicate.rs b/data_types/src/delete_predicate.rs index a0dc94c2ec..db6c639bf6 100644 --- a/data_types/src/delete_predicate.rs +++ b/data_types/src/delete_predicate.rs @@ -1,4 +1,5 @@ use crate::timestamp::TimestampRange; +use std::{fmt::Write, num::FpCategory}; /// Represents a parsed delete predicate for evaluation by the InfluxDB IOx /// query engine. @@ -16,6 +17,20 @@ pub struct DeletePredicate { pub exprs: Vec, } +impl DeletePredicate { + /// Format expr to SQL string. + pub fn expr_sql_string(&self) -> String { + let mut out = String::new(); + for expr in &self.exprs { + if !out.is_empty() { + write!(&mut out, " AND ").expect("writing to a string shouldn't fail"); + } + write!(&mut out, "{}", expr).expect("writing to a string shouldn't fail"); + } + out + } +} + /// Single expression to be used as parts of a predicate. /// /// Only very simple expression of the type ` ` are supported. @@ -55,7 +70,15 @@ impl DeleteExpr { impl std::fmt::Display for DeleteExpr { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}{}{}", self.column(), self.op(), self.scalar()) + write!( + f, + r#""{}"{}{}"#, + self.column() + .replace(r#"\"#, r#"\\"#) + .replace(r#"""#, r#"\""#), + self.op(), + self.scalar(), + ) } } @@ -93,8 +116,217 @@ impl std::fmt::Display for Scalar { match self { Scalar::Bool(value) => value.fmt(f), Scalar::I64(value) => value.fmt(f), - Scalar::F64(value) => value.fmt(f), - Scalar::String(value) => write!(f, "'{}'", value), + Scalar::F64(value) => match value.classify() { + FpCategory::Nan => write!(f, "'NaN'"), + FpCategory::Infinite if *value.as_ref() < 0.0 => write!(f, "'-Infinity'"), + FpCategory::Infinite => write!(f, "'Infinity'"), + _ => write!(f, "{:?}", value.as_ref()), + }, + Scalar::String(value) => { + write!( + f, + "'{}'", + value.replace(r#"\"#, r#"\\"#).replace(r#"'"#, r#"\'"#), + ) + } } } } + +#[cfg(test)] +mod tests { + use ordered_float::OrderedFloat; + + use super::*; + + #[test] + fn test_expr_to_sql_no_expressions() { + let pred = DeletePredicate { + range: TimestampRange { start: 1, end: 2 }, + exprs: vec![], + }; + assert_eq!(&pred.expr_sql_string(), ""); + } + + #[test] + fn test_expr_to_sql_operators() { + let pred = DeletePredicate { + range: TimestampRange { start: 1, end: 2 }, + exprs: vec![ + DeleteExpr { + column: String::from("col1"), + op: Op::Eq, + scalar: Scalar::I64(1), + }, + DeleteExpr { + column: String::from("col2"), + op: Op::Ne, + scalar: Scalar::I64(2), + }, + ], + }; + assert_eq!(&pred.expr_sql_string(), r#""col1"=1 AND "col2"!=2"#); + } + + #[test] + fn test_expr_to_sql_column_escape() { + let pred = DeletePredicate { + range: TimestampRange { start: 1, end: 2 }, + exprs: vec![ + DeleteExpr { + column: String::from("col 1"), + op: Op::Eq, + scalar: Scalar::I64(1), + }, + DeleteExpr { + column: String::from(r#"col\2"#), + op: Op::Eq, + scalar: Scalar::I64(2), + }, + DeleteExpr { + column: String::from(r#"col"3"#), + op: Op::Eq, + scalar: Scalar::I64(3), + }, + ], + }; + assert_eq!( + &pred.expr_sql_string(), + r#""col 1"=1 AND "col\\2"=2 AND "col\"3"=3"# + ); + } + + #[test] + fn test_expr_to_sql_bool() { + let pred = DeletePredicate { + range: TimestampRange { start: 1, end: 2 }, + exprs: vec![ + DeleteExpr { + column: String::from("col1"), + op: Op::Eq, + scalar: Scalar::Bool(false), + }, + DeleteExpr { + column: String::from("col2"), + op: Op::Eq, + scalar: Scalar::Bool(true), + }, + ], + }; + assert_eq!(&pred.expr_sql_string(), r#""col1"=false AND "col2"=true"#); + } + + #[test] + fn test_expr_to_sql_i64() { + let pred = DeletePredicate { + range: TimestampRange { start: 1, end: 2 }, + exprs: vec![ + DeleteExpr { + column: String::from("col1"), + op: Op::Eq, + scalar: Scalar::I64(0), + }, + DeleteExpr { + column: String::from("col2"), + op: Op::Eq, + scalar: Scalar::I64(-1), + }, + DeleteExpr { + column: String::from("col3"), + op: Op::Eq, + scalar: Scalar::I64(1), + }, + DeleteExpr { + column: String::from("col4"), + op: Op::Eq, + scalar: Scalar::I64(i64::MIN), + }, + DeleteExpr { + column: String::from("col5"), + op: Op::Eq, + scalar: Scalar::I64(i64::MAX), + }, + ], + }; + assert_eq!( + &pred.expr_sql_string(), + r#""col1"=0 AND "col2"=-1 AND "col3"=1 AND "col4"=-9223372036854775808 AND "col5"=9223372036854775807"# + ); + } + + #[test] + fn test_expr_to_sql_f64() { + let pred = DeletePredicate { + range: TimestampRange { start: 1, end: 2 }, + exprs: vec![ + DeleteExpr { + column: String::from("col1"), + op: Op::Eq, + scalar: Scalar::F64(OrderedFloat::from(0.0)), + }, + DeleteExpr { + column: String::from("col2"), + op: Op::Eq, + scalar: Scalar::F64(OrderedFloat::from(-0.0)), + }, + DeleteExpr { + column: String::from("col3"), + op: Op::Eq, + scalar: Scalar::F64(OrderedFloat::from(1.0)), + }, + DeleteExpr { + column: String::from("col4"), + op: Op::Eq, + scalar: Scalar::F64(OrderedFloat::from(f64::INFINITY)), + }, + DeleteExpr { + column: String::from("col5"), + op: Op::Eq, + scalar: Scalar::F64(OrderedFloat::from(f64::NEG_INFINITY)), + }, + DeleteExpr { + column: String::from("col6"), + op: Op::Eq, + scalar: Scalar::F64(OrderedFloat::from(f64::NAN)), + }, + ], + }; + assert_eq!( + &pred.expr_sql_string(), + r#""col1"=0.0 AND "col2"=-0.0 AND "col3"=1.0 AND "col4"='Infinity' AND "col5"='-Infinity' AND "col6"='NaN'"# + ); + } + + #[test] + fn test_expr_to_sql_string() { + let pred = DeletePredicate { + range: TimestampRange { start: 1, end: 2 }, + exprs: vec![ + DeleteExpr { + column: String::from("col1"), + op: Op::Eq, + scalar: Scalar::String(String::from("")), + }, + DeleteExpr { + column: String::from("col2"), + op: Op::Eq, + scalar: Scalar::String(String::from("foo")), + }, + DeleteExpr { + column: String::from("col3"), + op: Op::Eq, + scalar: Scalar::String(String::from(r#"fo\o"#)), + }, + DeleteExpr { + column: String::from("col4"), + op: Op::Eq, + scalar: Scalar::String(String::from(r#"fo'o"#)), + }, + ], + }; + assert_eq!( + &pred.expr_sql_string(), + r#""col1"='' AND "col2"='foo' AND "col3"='fo\\o' AND "col4"='fo\'o'"# + ); + } +} diff --git a/predicate/src/delete_expr.rs b/predicate/src/delete_expr.rs index d357a4ae76..ab1d3483db 100644 --- a/predicate/src/delete_expr.rs +++ b/predicate/src/delete_expr.rs @@ -146,7 +146,7 @@ mod tests { op: Op::Eq, scalar: Scalar::Bool(true), }, - "foo=true", + r#""foo"=true"#, ); assert_expr_works( DeleteExpr { @@ -154,7 +154,7 @@ mod tests { op: Op::Ne, scalar: Scalar::I64(-1), }, - "bar!=-1", + r#""bar"!=-1"#, ); assert_expr_works( DeleteExpr { @@ -162,7 +162,7 @@ mod tests { op: Op::Eq, scalar: Scalar::F64((-1.1).into()), }, - "baz=-1.1", + r#""baz"=-1.1"#, ); assert_expr_works( DeleteExpr { @@ -170,7 +170,7 @@ mod tests { op: Op::Eq, scalar: Scalar::String("foo".to_string()), }, - "col='foo'", + r#""col"='foo'"#, ); }