diff --git a/src/coprocessor/dag/mod.rs b/src/coprocessor/dag/mod.rs index 224f7335751..f3efbf07298 100644 --- a/src/coprocessor/dag/mod.rs +++ b/src/coprocessor/dag/mod.rs @@ -39,6 +39,7 @@ mod builder; pub mod executor; pub mod expr; pub mod handler; +pub mod rpn_expr; pub use self::executor::{ScanOn, Scanner}; pub use self::handler::DAGRequestHandler; diff --git a/src/coprocessor/dag/rpn_expr/function.rs b/src/coprocessor/dag/rpn_expr/function.rs new file mode 100644 index 00000000000..05750d82ee4 --- /dev/null +++ b/src/coprocessor/dag/rpn_expr/function.rs @@ -0,0 +1,68 @@ +// Copyright 2019 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +/// A trait for all RPN functions. +pub trait RpnFunction: std::fmt::Debug + Send + Sync + 'static { + /// The display name of the function. + fn name(&self) -> &'static str; + + /// The accepted argument length of this RPN function. + /// + /// Currently we do not support variable arguments. + fn args_len(&self) -> usize; +} + +impl RpnFunction for Box { + #[inline] + fn name(&self) -> &'static str { + (**self).name() + } + + #[inline] + fn args_len(&self) -> usize { + (**self).args_len() + } +} + +/// Implements `RpnFunction` automatically for structure that accepts 0, 1, 2 or 3 arguments. +/// +/// The structure must have a `call` member function accepting corresponding number of scalar +/// arguments. +#[macro_export] +macro_rules! impl_template_fn { + (0 arg @ $name:ident) => { + impl_template_fn! { @inner $name, 0 } + }; + (1 arg @ $name:ident) => { + impl_template_fn! { @inner $name, 1 } + }; + (2 arg @ $name:ident) => { + impl_template_fn! { @inner $name, 2 } + }; + (3 arg @ $name:ident) => { + impl_template_fn! { @inner $name, 3 } + }; + (@inner $name:ident, $args:expr) => { + impl $crate::coprocessor::dag::rpn_expr::RpnFunction for $name { + #[inline] + fn name(&self) -> &'static str { + stringify!($name) + } + + #[inline] + fn args_len(&self) -> usize { + $args + } + } + }; +} diff --git a/src/coprocessor/dag/rpn_expr/mod.rs b/src/coprocessor/dag/rpn_expr/mod.rs new file mode 100644 index 00000000000..834be0f486e --- /dev/null +++ b/src/coprocessor/dag/rpn_expr/mod.rs @@ -0,0 +1,34 @@ +// Copyright 2019 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +#[macro_use] +mod function; +mod types; + +pub use self::function::RpnFunction; +pub use self::types::RpnExpression; + +use tipb::expression::ScalarFuncSig; + +use crate::coprocessor::Error; + +// TODO: We should not expose this function as `pub` in future once all executors are batch +// executors. +pub fn map_pb_sig_to_rpn_func(value: ScalarFuncSig) -> Result, Error> { + match value { + v => Err(box_err!( + "ScalarFunction {:?} is not supported in batch mode", + v + )), + } +} diff --git a/src/coprocessor/dag/rpn_expr/types/expr.rs b/src/coprocessor/dag/rpn_expr/types/expr.rs new file mode 100644 index 00000000000..02570a5b88a --- /dev/null +++ b/src/coprocessor/dag/rpn_expr/types/expr.rs @@ -0,0 +1,135 @@ +// Copyright 2019 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +use tipb::expression::FieldType; + +use super::super::function::RpnFunction; +use crate::coprocessor::codec::data_type::ScalarValue; + +/// A type for each node in the RPN expression list. +#[derive(Debug)] +pub enum RpnExpressionNode { + /// Represents a function call. + FnCall { + func: Box, + field_type: FieldType, + }, + + /// Represents a scalar constant value. + Constant { + value: ScalarValue, + field_type: FieldType, + }, + + /// Represents a reference to a column in the columns specified in evaluation. + ColumnRef { + offset: usize, + + // Although we can know `ColumnInfo` according to `offset` and columns info in scan + // executors, its type is `ColumnInfo` instead of `FieldType`. + // Maybe we can remove this field in future. + field_type: FieldType, + }, +} + +impl RpnExpressionNode { + /// Gets the field type. + #[inline] + pub fn field_type(&self) -> &FieldType { + match self { + RpnExpressionNode::FnCall { ref field_type, .. } => field_type, + RpnExpressionNode::Constant { ref field_type, .. } => field_type, + RpnExpressionNode::ColumnRef { ref field_type, .. } => field_type, + } + } + + /// Borrows the function instance for `FnCall` variant. + #[inline] + pub fn fn_call_func(&self) -> Option<&dyn RpnFunction> { + match self { + RpnExpressionNode::FnCall { ref func, .. } => Some(&*func), + _ => None, + } + } + + /// Borrows the constant value for `Constant` variant. + #[inline] + pub fn constant_value(&self) -> Option<&ScalarValue> { + match self { + RpnExpressionNode::Constant { ref value, .. } => Some(value), + _ => None, + } + } + + /// Gets the column offset for `ColumnRef` variant. + #[inline] + pub fn column_ref_offset(&self) -> Option { + match self { + RpnExpressionNode::ColumnRef { ref offset, .. } => Some(*offset), + _ => None, + } + } +} + +/// An expression in Reverse Polish notation, which is simply a list of RPN expression nodes. +/// +/// You may want to build it using `RpnExpressionBuilder`. +#[derive(Debug)] +pub struct RpnExpression(Vec); + +impl std::ops::Deref for RpnExpression { + type Target = Vec; + + fn deref(&self) -> &Vec { + &self.0 + } +} + +impl std::ops::DerefMut for RpnExpression { + fn deref_mut(&mut self) -> &mut Vec { + &mut self.0 + } +} + +impl From> for RpnExpression { + fn from(v: Vec) -> Self { + Self(v) + } +} + +#[cfg(test)] +pub mod tests { + /// An RPN function for test. It accepts 1 int argument, returns the value in float. + #[derive(Debug, Clone, Copy)] + pub struct FnA; + + impl_template_fn! { 1 arg @ FnA } + + /// An RPN function for test. It accepts 2 float arguments, returns their sum in int. + #[derive(Debug, Clone, Copy)] + pub struct FnB; + + impl_template_fn! { 2 arg @ FnB } + + /// An RPN function for test. It accepts 3 int arguments, returns their sum in int. + #[derive(Debug, Clone, Copy)] + pub struct FnC; + + impl_template_fn! { 3 arg @ FnC } + + /// An RPN function for test. It accepts 3 float arguments, returns their sum in float. + #[derive(Debug, Clone, Copy)] + pub struct FnD; + + impl_template_fn! { 3 arg @ FnD } +} diff --git a/src/coprocessor/dag/rpn_expr/types/expr_builder.rs b/src/coprocessor/dag/rpn_expr/types/expr_builder.rs new file mode 100644 index 00000000000..70a69ac5818 --- /dev/null +++ b/src/coprocessor/dag/rpn_expr/types/expr_builder.rs @@ -0,0 +1,530 @@ +// Copyright 2019 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +#![allow(dead_code)] + +use std::convert::{TryFrom, TryInto}; + +use cop_datatype::{EvalType, FieldTypeAccessor}; +use tipb::expression::{Expr, ExprType, FieldType}; + +use super::super::function::RpnFunction; +use super::expr::{RpnExpression, RpnExpressionNode}; +use crate::coprocessor::codec::data_type::ScalarValue; +use crate::coprocessor::codec::mysql::Tz; +use crate::coprocessor::codec::mysql::{Decimal, Duration, Json, Time, MAX_FSP}; +use crate::coprocessor::{Error, Result}; +use crate::util::codec::number; + +/// Helper to build an `RpnExpression`. +/// +/// TODO: Deprecate it in Coprocessor V2 DAG interface. +pub struct RpnExpressionBuilder; + +impl RpnExpressionBuilder { + /// Builds the RPN expression node list from an expression definition tree. + pub fn build_from_expr_tree(tree_node: Expr, time_zone: &Tz) -> Result { + let mut expr_nodes = Vec::new(); + append_rpn_nodes_recursively( + tree_node, + &mut expr_nodes, + time_zone, + super::super::map_pb_sig_to_rpn_func, + )?; + Ok(RpnExpression::from(expr_nodes)) + } +} + +/// Transforms eval tree nodes into RPN nodes. +/// +/// Suppose that we have a function call: +/// +/// ```ignore +/// A(B, C(E, F, G), D) +/// ``` +/// +/// The eval tree looks like: +/// +/// ```ignore +/// +---+ +/// | A | +/// +---+ +/// | +/// +-------------------+ +/// | | | +/// +---+ +---+ +---+ +/// | B | | C | | D | +/// +---+ +---+ +---+ +/// | +/// +-------------+ +/// | | | +/// +---+ +---+ +---+ +/// | E | | F | | G | +/// +---+ +---+ +---+ +/// ``` +/// +/// We need to transform the tree into RPN nodes: +/// +/// ```ignore +/// B E F G C D A +/// ``` +/// +/// The transform process is very much like a post-order traversal. This function does it +/// recursively. +fn append_rpn_nodes_recursively( + tree_node: Expr, + rpn_nodes: &mut Vec, + time_zone: &Tz, + fn_mapper: F, +) -> Result<()> +where + F: Fn(tipb::expression::ScalarFuncSig) -> Result> + Copy, +{ + // TODO: We should check whether node types match the function signature. Otherwise there + // will be panics when the expression is evaluated. + + match tree_node.get_tp() { + ExprType::ScalarFunc => handle_node_fn_call(tree_node, rpn_nodes, time_zone, fn_mapper), + ExprType::ColumnRef => handle_node_column_ref(tree_node, rpn_nodes), + _ => handle_node_constant(tree_node, rpn_nodes, time_zone), + } +} + +/// TODO: Remove this helper function when we use Failure which can simplify the code. +#[inline] +fn get_eval_type(tree_node: &Expr) -> Result { + EvalType::try_from(tree_node.get_field_type().tp()).map_err(|e| Error::Other(box_err!(e))) +} + +#[inline] +fn handle_node_column_ref( + mut tree_node: Expr, + rpn_nodes: &mut Vec, +) -> Result<()> { + let offset = number::decode_i64(&mut tree_node.get_val()).map_err(|_| { + Error::Other(box_err!( + "Unable to decode column reference offset from the request" + )) + })? as usize; + rpn_nodes.push(RpnExpressionNode::ColumnRef { + offset, + field_type: tree_node.take_field_type(), + }); + Ok(()) +} + +#[inline] +fn handle_node_fn_call( + mut tree_node: Expr, + rpn_nodes: &mut Vec, + time_zone: &Tz, + fn_mapper: F, +) -> Result<()> +where + F: Fn(tipb::expression::ScalarFuncSig) -> Result> + Copy, +{ + // Map pb func to `RpnFunction`. + let func = fn_mapper(tree_node.get_sig())?; + let args = tree_node.take_children().into_vec(); + if func.args_len() != args.len() { + return Err(box_err!( + "Unexpected arguments, expect {}, received {}", + func.args_len(), + args.len() + )); + } + // Visit children first, then push current node, so that it is a post-order traversal. + for arg in args { + append_rpn_nodes_recursively(arg, rpn_nodes, time_zone, fn_mapper)?; + } + rpn_nodes.push(RpnExpressionNode::FnCall { + func, + field_type: tree_node.take_field_type(), + }); + Ok(()) +} + +#[inline] +fn handle_node_constant( + mut tree_node: Expr, + rpn_nodes: &mut Vec, + time_zone: &Tz, +) -> Result<()> { + let eval_type = get_eval_type(&tree_node)?; + + let scalar_value = match tree_node.get_tp() { + ExprType::Null => get_scalar_value_null(eval_type), + ExprType::Int64 if eval_type == EvalType::Int => { + extract_scalar_value_int64(tree_node.take_val())? + } + ExprType::Uint64 if eval_type == EvalType::Int => { + extract_scalar_value_uint64(tree_node.take_val())? + } + ExprType::String | ExprType::Bytes if eval_type == EvalType::Bytes => { + extract_scalar_value_bytes(tree_node.take_val())? + } + ExprType::Float32 | ExprType::Float64 if eval_type == EvalType::Real => { + extract_scalar_value_float(tree_node.take_val())? + } + ExprType::MysqlTime if eval_type == EvalType::DateTime => extract_scalar_value_date_time( + tree_node.take_val(), + tree_node.get_field_type(), + time_zone, + )?, + ExprType::MysqlDuration if eval_type == EvalType::Duration => { + extract_scalar_value_duration(tree_node.take_val())? + } + ExprType::MysqlDecimal if eval_type == EvalType::Decimal => { + extract_scalar_value_decimal(tree_node.take_val())? + } + ExprType::MysqlJson if eval_type == EvalType::Json => { + extract_scalar_value_json(tree_node.take_val())? + } + expr_type => { + return Err(box_err!( + "Unexpected ExprType {:?} and EvalType {:?}", + expr_type, + eval_type + )) + } + }; + rpn_nodes.push(RpnExpressionNode::Constant { + value: scalar_value, + field_type: tree_node.take_field_type(), + }); + Ok(()) +} + +#[inline] +fn get_scalar_value_null(eval_type: EvalType) -> ScalarValue { + match eval_type { + EvalType::Int => ScalarValue::Int(None), + EvalType::Real => ScalarValue::Real(None), + EvalType::Decimal => ScalarValue::Decimal(None), + EvalType::Bytes => ScalarValue::Bytes(None), + EvalType::DateTime => ScalarValue::DateTime(None), + EvalType::Duration => ScalarValue::Duration(None), + EvalType::Json => ScalarValue::Json(None), + } +} + +#[inline] +fn extract_scalar_value_int64(val: Vec) -> Result { + let value = number::decode_i64(&mut val.as_slice()) + .map_err(|_| Error::Other(box_err!("Unable to decode int64 from the request")))?; + Ok(ScalarValue::Int(Some(value))) +} + +#[inline] +fn extract_scalar_value_uint64(val: Vec) -> Result { + let value = number::decode_u64(&mut val.as_slice()) + .map_err(|_| Error::Other(box_err!("Unable to decode uint64 from the request")))?; + Ok(ScalarValue::Int(Some(value as i64))) +} + +#[inline] +fn extract_scalar_value_bytes(val: Vec) -> Result { + Ok(ScalarValue::Bytes(Some(val))) +} + +#[inline] +fn extract_scalar_value_float(val: Vec) -> Result { + let value = number::decode_f64(&mut val.as_slice()) + .map_err(|_| Error::Other(box_err!("Unable to decode float from the request")))?; + Ok(ScalarValue::Real(Some(value))) +} + +#[inline] +fn extract_scalar_value_date_time( + val: Vec, + field_type: &FieldType, + time_zone: &Tz, +) -> Result { + let v = number::decode_u64(&mut val.as_slice()) + .map_err(|_| Error::Other(box_err!("Unable to decode date time from the request")))?; + let fsp = field_type.decimal() as i8; + let value = Time::from_packed_u64(v, field_type.tp().try_into()?, fsp, time_zone) + .map_err(|_| Error::Other(box_err!("Unable to decode date time from the request")))?; + Ok(ScalarValue::DateTime(Some(value))) +} + +#[inline] +fn extract_scalar_value_duration(val: Vec) -> Result { + let n = number::decode_i64(&mut val.as_slice()) + .map_err(|_| Error::Other(box_err!("Unable to decode duration from the request")))?; + let value = Duration::from_nanos(n, MAX_FSP) + .map_err(|_| Error::Other(box_err!("Unable to decode duration from the request")))?; + Ok(ScalarValue::Duration(Some(value))) +} + +#[inline] +fn extract_scalar_value_decimal(val: Vec) -> Result { + let value = Decimal::decode(&mut val.as_slice()) + .map_err(|_| Error::Other(box_err!("Unable to decode decimal from the request")))?; + Ok(ScalarValue::Decimal(Some(value))) +} + +#[inline] +fn extract_scalar_value_json(val: Vec) -> Result { + let value = Json::decode(&mut val.as_slice()) + .map_err(|_| Error::Other(box_err!("Unable to decode json from the request")))?; + Ok(ScalarValue::Json(Some(value))) +} + +#[cfg(test)] +mod tests { + use super::super::expr::tests::*; + use super::*; + + use cop_datatype::FieldTypeTp; + use tipb::expression::ScalarFuncSig; + + /// For testing `append_rpn_nodes_recursively`. It accepts protobuf function sig enum, which + /// cannot be modified by us in tests to support FnA ~ FnD. So let's just hard code some + /// substitute. + fn fn_mapper(value: ScalarFuncSig) -> Result> { + // FnA: CastIntAsInt + // FnB: CastIntAsReal + // FnC: CastIntAsString + // FnD: CastIntAsDecimal + match value { + ScalarFuncSig::CastIntAsInt => Ok(Box::new(FnA)), + ScalarFuncSig::CastIntAsReal => Ok(Box::new(FnB)), + ScalarFuncSig::CastIntAsString => Ok(Box::new(FnC)), + ScalarFuncSig::CastIntAsDecimal => Ok(Box::new(FnD)), + _ => unreachable!(), + } + } + + #[test] + #[allow(clippy::float_cmp)] + fn test_append_rpn_nodes_recursively() { + use crate::util::codec::number::NumberEncoder; + + // Input: + // FnD(a, FnA(FnC(b, c, d)), FnA(FnB(e, f)) + // + // Tree: + // FnD + // +----------+----------+ + // a FnA FnA + // | | + // FnC FnB + // +---+---+ +---+ + // b c d e f + // + // RPN: + // a b c d FnC FnA e f FnB FnA FnD + + let node_fn_a_1 = { + // node b + let mut node_b = Expr::new(); + node_b.set_tp(ExprType::Int64); + node_b + .mut_field_type() + .as_mut_accessor() + .set_tp(FieldTypeTp::LongLong); + node_b.mut_val().encode_i64(7).unwrap(); + + // node c + let mut node_c = Expr::new(); + node_c.set_tp(ExprType::Int64); + node_c + .mut_field_type() + .as_mut_accessor() + .set_tp(FieldTypeTp::LongLong); + node_c.mut_val().encode_i64(3).unwrap(); + + // node d + let mut node_d = Expr::new(); + node_d.set_tp(ExprType::Int64); + node_d + .mut_field_type() + .as_mut_accessor() + .set_tp(FieldTypeTp::LongLong); + node_d.mut_val().encode_i64(11).unwrap(); + + // FnC + let mut node_fn_c = Expr::new(); + node_fn_c.set_tp(ExprType::ScalarFunc); + node_fn_c.set_sig(ScalarFuncSig::CastIntAsString); + node_fn_c + .mut_field_type() + .as_mut_accessor() + .set_tp(FieldTypeTp::LongLong); + node_fn_c.mut_children().push(node_b); + node_fn_c.mut_children().push(node_c); + node_fn_c.mut_children().push(node_d); + + // FnA + let mut node_fn_a = Expr::new(); + node_fn_a.set_tp(ExprType::ScalarFunc); + node_fn_a.set_sig(ScalarFuncSig::CastIntAsInt); + node_fn_a + .mut_field_type() + .as_mut_accessor() + .set_tp(FieldTypeTp::Double); + node_fn_a.mut_children().push(node_fn_c); + node_fn_a + }; + + let node_fn_a_2 = { + // node e + let mut node_e = Expr::new(); + node_e.set_tp(ExprType::Float64); + node_e + .mut_field_type() + .as_mut_accessor() + .set_tp(FieldTypeTp::Double); + node_e.mut_val().encode_f64(-1.5).unwrap(); + + // node f + let mut node_f = Expr::new(); + node_f.set_tp(ExprType::Float64); + node_f + .mut_field_type() + .as_mut_accessor() + .set_tp(FieldTypeTp::Double); + node_f.mut_val().encode_f64(100.12).unwrap(); + + // FnB + let mut node_fn_b = Expr::new(); + node_fn_b.set_tp(ExprType::ScalarFunc); + node_fn_b.set_sig(ScalarFuncSig::CastIntAsReal); + node_fn_b + .mut_field_type() + .as_mut_accessor() + .set_tp(FieldTypeTp::LongLong); + node_fn_b.mut_children().push(node_e); + node_fn_b.mut_children().push(node_f); + + // FnA + let mut node_fn_a = Expr::new(); + node_fn_a.set_tp(ExprType::ScalarFunc); + node_fn_a.set_sig(ScalarFuncSig::CastIntAsInt); + node_fn_a + .mut_field_type() + .as_mut_accessor() + .set_tp(FieldTypeTp::Double); + node_fn_a.mut_children().push(node_fn_b); + node_fn_a + }; + + // node a (NULL) + let mut node_a = Expr::new(); + node_a.set_tp(ExprType::Null); + node_a + .mut_field_type() + .as_mut_accessor() + .set_tp(FieldTypeTp::Double); + + // FnD + let mut node_fn_d = Expr::new(); + node_fn_d.set_tp(ExprType::ScalarFunc); + node_fn_d.set_sig(ScalarFuncSig::CastIntAsDecimal); + node_fn_d + .mut_field_type() + .as_mut_accessor() + .set_tp(FieldTypeTp::Double); + node_fn_d.mut_children().push(node_a); + node_fn_d.mut_children().push(node_fn_a_1); + node_fn_d.mut_children().push(node_fn_a_2); + + let mut vec = vec![]; + append_rpn_nodes_recursively(node_fn_d, &mut vec, &Tz::utc(), fn_mapper).unwrap(); + + let mut it = vec.into_iter(); + + // node a + assert!(it + .next() + .unwrap() + .constant_value() + .unwrap() + .as_real() + .is_none()); + + // node b + assert_eq!( + it.next() + .unwrap() + .constant_value() + .unwrap() + .as_int() + .unwrap(), + 7 + ); + + // node c + assert_eq!( + it.next() + .unwrap() + .constant_value() + .unwrap() + .as_int() + .unwrap(), + 3 + ); + + // node d + assert_eq!( + it.next() + .unwrap() + .constant_value() + .unwrap() + .as_int() + .unwrap(), + 11 + ); + + // FnC + assert_eq!(it.next().unwrap().fn_call_func().unwrap().name(), "FnC"); + + // FnA + assert_eq!(it.next().unwrap().fn_call_func().unwrap().name(), "FnA"); + + // node e + assert_eq!( + it.next() + .unwrap() + .constant_value() + .unwrap() + .as_real() + .unwrap(), + -1.5 + ); + + // node f + assert_eq!( + it.next() + .unwrap() + .constant_value() + .unwrap() + .as_real() + .unwrap(), + 100.12 + ); + + // FnB + assert_eq!(it.next().unwrap().fn_call_func().unwrap().name(), "FnB"); + + // FnA + assert_eq!(it.next().unwrap().fn_call_func().unwrap().name(), "FnA"); + + // FnD + assert_eq!(it.next().unwrap().fn_call_func().unwrap().name(), "FnD"); + + // Finish + assert!(it.next().is_none()) + } +} diff --git a/src/coprocessor/dag/rpn_expr/types/mod.rs b/src/coprocessor/dag/rpn_expr/types/mod.rs new file mode 100644 index 00000000000..e13e19f4d40 --- /dev/null +++ b/src/coprocessor/dag/rpn_expr/types/mod.rs @@ -0,0 +1,18 @@ +// Copyright 2019 PingCAP, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// See the License for the specific language governing permissions and +// limitations under the License. + +mod expr; +mod expr_builder; + +pub use self::expr::RpnExpression; +pub use self::expr_builder::RpnExpressionBuilder;