pub mod ast;
use pest::Parser as PestParser;
use pest_derive::Parser;
pub use ast::{
Aggregation, AstNode, CollectionOpNode, ComparisonNode, GeoExprNode, GroupBy, LogicalOpNode,
Mutator, NslookupExprNode, QueryWithStatsNode, StatsNode, UnaryOpNode, Value, VizParamValue,
};
use crate::error::{Result, TqlError};
#[derive(Parser)]
#[grammar = "parser/grammar.pest"]
pub struct TqlPestParser;
pub struct TqlParser {
max_depth: usize,
}
impl Default for TqlParser {
fn default() -> Self {
Self::new()
}
}
impl TqlParser {
pub const MAX_QUERY_DEPTH: usize = 50;
pub fn new() -> Self {
Self {
max_depth: Self::MAX_QUERY_DEPTH,
}
}
pub fn with_max_depth(max_depth: usize) -> Self {
Self { max_depth }
}
pub fn parse(&self, query: &str) -> Result<AstNode> {
if query.trim().is_empty() {
return Ok(AstNode::MatchAll);
}
let pairs = TqlPestParser::parse(Rule::query, query).map_err(|e| {
let location = match &e.location {
pest::error::InputLocation::Pos(pos) => *pos,
pest::error::InputLocation::Span((start, _)) => *start,
};
TqlError::ParseError {
message: format!("Parse error: {}", e),
position: location,
query: Some(query.to_string()),
}
})?;
self.build_ast_from_pairs(pairs, 0)
}
fn build_ast_from_pairs(
&self,
mut pairs: pest::iterators::Pairs<Rule>,
depth: usize,
) -> Result<AstNode> {
if depth > self.max_depth {
return Err(TqlError::SyntaxError {
message: format!(
"Query depth exceeds maximum allowed depth of {}",
self.max_depth
),
position: Some(0),
query: None,
suggestions: vec![
"Reduce query nesting depth".to_string(),
"Split into multiple simpler queries".to_string(),
],
});
}
if let Some(pair) = pairs.next() {
match pair.as_rule() {
Rule::query => {
let inner = pair.into_inner();
return self.build_ast_from_pairs(inner, depth);
}
Rule::query_with_stats => {
return self.parse_query_with_stats(pair, depth + 1);
}
Rule::stats_expr => {
return self.parse_stats_expr(pair, depth + 1);
}
Rule::logical_expr => {
return self.parse_logical_expr(pair, depth + 1);
}
_ => {
return Err(TqlError::ParseError {
message: format!("Unexpected rule: {:?}", pair.as_rule()),
position: 0,
query: None,
});
}
}
}
Ok(AstNode::MatchAll)
}
fn parse_query_with_stats(
&self,
pair: pest::iterators::Pair<Rule>,
depth: usize,
) -> Result<AstNode> {
let mut inner = pair.into_inner();
let filter_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing filter expression in query_with_stats".to_string(),
position: 0,
query: None,
})?;
let filter = Box::new(self.parse_logical_expr(filter_pair, depth + 1)?);
let stats_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing stats expression in query_with_stats".to_string(),
position: 0,
query: None,
})?;
match self.parse_stats_expr(stats_pair, depth + 1)? {
AstNode::StatsExpr(stats) => Ok(AstNode::QueryWithStats(QueryWithStatsNode {
filter,
stats,
})),
_ => Err(TqlError::ParseError {
message: "Expected stats expression".to_string(),
position: 0,
query: None,
}),
}
}
fn parse_stats_expr(
&self,
pair: pest::iterators::Pair<Rule>,
_depth: usize,
) -> Result<AstNode> {
let mut aggregations = Vec::new();
let mut group_by = Vec::new();
let mut viz_hint = None;
let mut viz_params = None;
for inner_pair in pair.into_inner() {
match inner_pair.as_rule() {
Rule::aggregation => {
aggregations.push(self.parse_aggregation(inner_pair)?);
}
Rule::group_by_list => {
group_by = self.parse_group_by_list(inner_pair)?;
}
Rule::viz_hint => {
let mut viz_inner = inner_pair.into_inner();
viz_hint = Some(
viz_inner
.next()
.ok_or_else(|| TqlError::ParseError {
message: "Missing viz hint identifier".to_string(),
position: 0,
query: None,
})?
.as_str()
.to_string(),
);
if let Some(params_pair) = viz_inner.next() {
if params_pair.as_rule() == Rule::viz_params {
let mut params = std::collections::HashMap::new();
for param_pair in params_pair.into_inner() {
if param_pair.as_rule() == Rule::viz_param {
let mut param_inner = param_pair.into_inner();
let key = param_inner
.next()
.ok_or_else(|| TqlError::ParseError {
message: "Missing viz param key".to_string(),
position: 0,
query: None,
})?
.as_str()
.to_string();
let value_pair = param_inner
.next()
.ok_or_else(|| TqlError::ParseError {
message: format!(
"Missing viz param value for key '{}'",
key
),
position: 0,
query: None,
})?;
let value = Self::parse_viz_value(value_pair)?;
params.insert(key, value);
}
}
if !params.is_empty() {
viz_params = Some(params);
}
}
}
}
_ => {}
}
}
Ok(AstNode::StatsExpr(StatsNode {
aggregations,
group_by,
viz_hint,
viz_params,
}))
}
fn parse_aggregation(&self, pair: pest::iterators::Pair<Rule>) -> Result<Aggregation> {
let mut function = String::new();
let mut field = None;
let mut alias = None;
let mut modifier = None;
let mut limit = None;
let mut percentile_values = None;
let rank_values = None;
let mut field_mutators = None;
for inner_pair in pair.into_inner() {
match inner_pair.as_rule() {
Rule::agg_func_name => {
function = inner_pair.as_str().to_lowercase();
}
Rule::agg_field => {
let field_str = inner_pair.as_str();
if field_str != "*" {
let mut field_name = String::new();
let mut mutators = Vec::new();
for field_inner in inner_pair.into_inner() {
match field_inner.as_rule() {
Rule::field_name => {
field_name = field_inner.as_str().to_string();
}
Rule::mutator => {
mutators.push(self.parse_mutator(field_inner)?);
}
_ => {}
}
}
field = Some(field_name);
if !mutators.is_empty() {
field_mutators = Some(mutators);
}
} else {
field = Some("*".to_string());
}
}
Rule::agg_modifier => {
let mod_text = inner_pair.as_str();
if mod_text.starts_with("top") {
modifier = Some("top".to_string());
} else if mod_text.starts_with("bottom") {
modifier = Some("bottom".to_string());
}
for mod_inner in inner_pair.into_inner() {
if mod_inner.as_rule() == Rule::integer {
limit = Some(mod_inner.as_str().parse().unwrap_or(10));
}
}
}
Rule::percentile_values => {
let values: Vec<f64> = inner_pair
.into_inner()
.filter_map(|p| {
if p.as_rule() == Rule::number {
p.as_str().parse().ok()
} else {
None
}
})
.collect();
percentile_values = Some(values);
}
Rule::identifier => {
alias = Some(inner_pair.as_str().to_string());
}
_ => {}
}
}
Ok(Aggregation {
function,
field,
alias,
modifier,
limit,
percentile_values,
rank_values,
field_mutators,
})
}
fn parse_group_by_list(&self, pair: pest::iterators::Pair<Rule>) -> Result<Vec<GroupBy>> {
let mut group_by = Vec::new();
for inner_pair in pair.into_inner() {
if inner_pair.as_rule() == Rule::group_by_field {
let mut field = String::new();
let mut bucket_size = None;
for field_inner in inner_pair.into_inner() {
match field_inner.as_rule() {
Rule::field_with_mutators => {
for fwm_inner in field_inner.into_inner() {
if fwm_inner.as_rule() == Rule::field_name {
field = fwm_inner.as_str().to_string();
break;
}
}
}
Rule::integer => {
bucket_size = Some(field_inner.as_str().parse().unwrap_or(10));
}
_ => {}
}
}
group_by.push(GroupBy { field, bucket_size });
}
}
Ok(group_by)
}
fn parse_logical_expr(
&self,
pair: pest::iterators::Pair<Rule>,
depth: usize,
) -> Result<AstNode> {
let mut inner = pair.into_inner();
let first_term_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing term in logical expression".to_string(),
position: 0,
query: None,
})?;
let mut left = self.parse_term(first_term_pair, depth + 1)?;
while let Some(op_pair) = inner.next() {
if op_pair.as_rule() != Rule::logical_op {
return Err(TqlError::ParseError {
message: "Expected logical operator".to_string(),
position: 0,
query: None,
});
}
let operator = op_pair.as_str().to_lowercase();
let right_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing right operand after logical operator".to_string(),
position: 0,
query: None,
})?;
let right = self.parse_term(right_pair, depth + 1)?;
left = AstNode::LogicalOp(LogicalOpNode {
operator,
left: Box::new(left),
right: Box::new(right),
});
}
Ok(left)
}
fn parse_term(&self, pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<AstNode> {
match pair.as_rule() {
Rule::term => {
let inner_pair = pair
.into_inner()
.next()
.ok_or_else(|| TqlError::ParseError {
message: "Empty term".to_string(),
position: 0,
query: None,
})?;
self.parse_term(inner_pair, depth + 1)
}
Rule::not_expr => {
let mut inner = pair.into_inner();
let _op = inner.next(); let operand_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing operand after NOT".to_string(),
position: 0,
query: None,
})?;
let operand = self.parse_term(operand_pair, depth + 1)?;
Ok(AstNode::UnaryOp(UnaryOpNode {
operator: "not".to_string(),
operand: Box::new(operand),
}))
}
Rule::primary => self.parse_primary(pair, depth + 1),
_ => Err(TqlError::ParseError {
message: format!("Unexpected rule in term: {:?}", pair.as_rule()),
position: 0,
query: None,
}),
}
}
fn parse_primary(&self, pair: pest::iterators::Pair<Rule>, depth: usize) -> Result<AstNode> {
match pair.as_rule() {
Rule::primary => {
let inner_pair = pair
.into_inner()
.next()
.ok_or_else(|| TqlError::ParseError {
message: "Empty primary".to_string(),
position: 0,
query: None,
})?;
self.parse_primary(inner_pair, depth + 1)
}
Rule::paren_expr => {
let inner_pair = pair
.into_inner()
.next()
.ok_or_else(|| TqlError::ParseError {
message: "Empty parenthesized expression".to_string(),
position: 0,
query: None,
})?;
self.parse_logical_expr(inner_pair, depth + 1)
}
Rule::comparison => self.parse_comparison(pair, depth + 1),
_ => Err(TqlError::ParseError {
message: format!("Unexpected rule in primary: {:?}", pair.as_rule()),
position: 0,
query: None,
}),
}
}
fn parse_comparison(
&self,
pair: pest::iterators::Pair<Rule>,
_depth: usize,
) -> Result<AstNode> {
let inner_pair = pair
.into_inner()
.next()
.ok_or_else(|| TqlError::ParseError {
message: "Empty comparison".to_string(),
position: 0,
query: None,
})?;
match inner_pair.as_rule() {
Rule::collection_comparison => self.parse_collection_comparison(inner_pair),
Rule::between_comparison => self.parse_between_comparison(inner_pair),
Rule::in_fields_comparison => self.parse_in_fields_comparison(inner_pair),
Rule::is_null_comparison => self.parse_is_null_comparison(inner_pair),
Rule::unary_comparison => self.parse_unary_comparison(inner_pair),
Rule::binary_comparison => self.parse_binary_comparison(inner_pair),
Rule::field_only_expression => self.parse_field_only_expression(inner_pair),
_ => Err(TqlError::ParseError {
message: format!("Unknown comparison type: {:?}", inner_pair.as_rule()),
position: 0,
query: None,
}),
}
}
fn parse_collection_comparison(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
let mut inner = pair.into_inner();
let first = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing collection comparison element".to_string(),
position: 0,
query: None,
})?;
let (operator, field, field_mutators, type_hint) = match first.as_rule() {
Rule::collection_op => {
let op = self.normalize_operator(first.as_str());
let field_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing field in collection comparison".to_string(),
position: 0,
query: None,
})?;
let (f, fm, th) = self.parse_field_with_mutators(field_pair)?;
(op, f, fm, th)
}
Rule::field_with_mutators => {
let (f, fm, th) = self.parse_field_with_mutators(first)?;
let op_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing collection operator".to_string(),
position: 0,
query: None,
})?;
let op = self.normalize_operator(op_pair.as_str());
(op, f, fm, th)
}
_ => {
return Err(TqlError::ParseError {
message: format!(
"Unexpected rule in collection comparison: {:?}",
first.as_rule()
),
position: 0,
query: None,
});
}
};
let next_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing comparison operator or value in collection comparison".to_string(),
position: 0,
query: None,
})?;
let (comparison_operator, value) = match next_pair.as_rule() {
Rule::comparison_op => {
let comp_op = self.normalize_operator(next_pair.as_str());
let value_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing value in collection comparison".to_string(),
position: 0,
query: None,
})?;
let (val, _value_mutators) = self.parse_value_with_mutators(value_pair)?;
(comp_op, val)
}
Rule::value_with_mutators => {
let (val, _value_mutators) = self.parse_value_with_mutators(next_pair)?;
("eq".to_string(), val)
}
_ => {
return Err(TqlError::ParseError {
message: format!(
"Unexpected rule in collection comparison: {:?}",
next_pair.as_rule()
),
position: 0,
query: None,
});
}
};
Ok(AstNode::CollectionOp(CollectionOpNode {
operator,
field,
comparison_operator,
value,
field_mutators,
type_hint,
}))
}
fn parse_between_comparison(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
let mut inner = pair.into_inner();
let field_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing field in between comparison".to_string(),
position: 0,
query: None,
})?;
let (field, field_mutators, type_hint) = self.parse_field_with_mutators(field_pair)?;
let op_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing operator in between comparison".to_string(),
position: 0,
query: None,
})?;
let operator = self.normalize_operator(op_pair.as_str());
let next_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing value in between comparison".to_string(),
position: 0,
query: None,
})?;
let value = match next_pair.as_rule() {
Rule::list_value => self.parse_list(next_pair)?,
Rule::value => {
let first = self.parse_value(next_pair)?;
let second_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing second value in between X and Y".to_string(),
position: 0,
query: None,
})?;
let second = self.parse_value(second_pair)?;
Value::List(vec![first, second])
}
_ => {
return Err(TqlError::ParseError {
message: format!(
"Unexpected rule in between comparison: {:?}",
next_pair.as_rule()
),
position: 0,
query: None,
});
}
};
Ok(AstNode::Comparison(ComparisonNode {
field,
operator,
value: Some(value),
field_mutators,
value_mutators: None,
type_hint,
}))
}
fn parse_in_fields_comparison(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
let mut inner = pair.into_inner();
let value_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing value in 'value in field' comparison".to_string(),
position: 0,
query: None,
})?;
let (value, value_mutators) = self.parse_value_with_mutators(value_pair)?;
let _op_pair = inner.next();
let next_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing field/list in 'value in ...' comparison".to_string(),
position: 0,
query: None,
})?;
match next_pair.as_rule() {
Rule::in_fields_list => {
let fields: Vec<String> = next_pair
.into_inner()
.filter(|p| p.as_rule() == Rule::field_name)
.map(|p| p.as_str().to_string())
.collect();
if fields.is_empty() {
return Ok(AstNode::MatchAll);
}
let mut result = AstNode::Comparison(ComparisonNode {
field: fields[0].clone(),
operator: "eq".to_string(),
value: Some(value.clone()),
field_mutators: None,
value_mutators: value_mutators.clone(),
type_hint: None,
});
for field in &fields[1..] {
let right = AstNode::Comparison(ComparisonNode {
field: field.clone(),
operator: "eq".to_string(),
value: Some(value.clone()),
field_mutators: None,
value_mutators: value_mutators.clone(),
type_hint: None,
});
result = AstNode::LogicalOp(LogicalOpNode {
operator: "or".to_string(),
left: Box::new(result),
right: Box::new(right),
});
}
Ok(result)
}
Rule::field_with_mutators => {
let (field, field_mutators, type_hint) =
self.parse_field_with_mutators(next_pair)?;
Ok(AstNode::Comparison(ComparisonNode {
field,
operator: "contains".to_string(),
value: Some(value),
field_mutators,
value_mutators,
type_hint,
}))
}
_ => Err(TqlError::ParseError {
message: format!(
"Unexpected rule in in_fields_comparison: {:?}",
next_pair.as_rule()
),
position: 0,
query: None,
}),
}
}
fn parse_is_null_comparison(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
let mut inner = pair.into_inner();
let field_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing field in is null comparison".to_string(),
position: 0,
query: None,
})?;
let (field, field_mutators, type_hint) = self.parse_field_with_mutators(field_pair)?;
let op_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing operator in is null comparison".to_string(),
position: 0,
query: None,
})?;
let operator = self.normalize_operator(op_pair.as_str());
Ok(AstNode::Comparison(ComparisonNode {
field,
operator,
value: Some(Value::Null),
field_mutators,
value_mutators: None,
type_hint,
}))
}
fn parse_unary_comparison(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
let mut inner = pair.into_inner();
let field_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing field in unary comparison".to_string(),
position: 0,
query: None,
})?;
let (field, field_mutators, type_hint) = self.parse_field_with_mutators(field_pair)?;
let op_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing operator in unary comparison".to_string(),
position: 0,
query: None,
})?;
let operator = self.normalize_operator(op_pair.as_str());
Ok(AstNode::Comparison(ComparisonNode {
field,
operator,
value: None,
field_mutators,
value_mutators: None,
type_hint,
}))
}
fn parse_binary_comparison(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
let mut inner = pair.into_inner();
let field_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing field in binary comparison".to_string(),
position: 0,
query: None,
})?;
let (field, field_mutators, type_hint) = self.parse_field_with_mutators(field_pair)?;
let op_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing operator in binary comparison".to_string(),
position: 0,
query: None,
})?;
let operator = self.normalize_operator(op_pair.as_str());
let value_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing value in binary comparison".to_string(),
position: 0,
query: None,
})?;
let (value, value_mutators) = self.parse_value_with_mutators(value_pair)?;
Ok(AstNode::Comparison(ComparisonNode {
field,
operator,
value: Some(value),
field_mutators,
value_mutators,
type_hint,
}))
}
fn parse_field_only_expression(&self, pair: pest::iterators::Pair<Rule>) -> Result<AstNode> {
let field_pair = pair
.into_inner()
.next()
.ok_or_else(|| TqlError::ParseError {
message: "Missing field in field-only expression".to_string(),
position: 0,
query: None,
})?;
let (field, field_mutators, type_hint) = self.parse_field_with_mutators(field_pair)?;
Ok(AstNode::Comparison(ComparisonNode {
field,
operator: "exists".to_string(),
value: None,
field_mutators,
value_mutators: None,
type_hint,
}))
}
fn parse_field_with_mutators(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<(String, Option<Vec<Mutator>>, Option<String>)> {
let mut field = String::new();
let mut mutators = Vec::new();
let mut type_hint = None;
for inner_pair in pair.into_inner() {
match inner_pair.as_rule() {
Rule::field_name => {
field = inner_pair.as_str().to_string();
}
Rule::mutator => {
mutators.push(self.parse_mutator(inner_pair)?);
}
Rule::type_hint => {
for type_inner in inner_pair.into_inner() {
if type_inner.as_rule() == Rule::type_name {
type_hint = Some(type_inner.as_str().to_lowercase());
}
}
}
_ => {}
}
}
let field_mutators = if mutators.is_empty() {
None
} else {
Some(mutators)
};
Ok((field, field_mutators, type_hint))
}
fn parse_value_with_mutators(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<(Value, Option<Vec<Mutator>>)> {
let mut value = Value::Null;
let mut mutators = Vec::new();
for inner_pair in pair.into_inner() {
match inner_pair.as_rule() {
Rule::value => {
value = self.parse_value(inner_pair)?;
}
Rule::mutator => {
mutators.push(self.parse_mutator(inner_pair)?);
}
_ => {}
}
}
let value_mutators = if mutators.is_empty() {
None
} else {
Some(mutators)
};
Ok((value, value_mutators))
}
fn parse_value(&self, pair: pest::iterators::Pair<Rule>) -> Result<Value> {
let inner_pair = pair
.into_inner()
.next()
.ok_or_else(|| TqlError::ParseError {
message: "Empty value".to_string(),
position: 0,
query: None,
})?;
match inner_pair.as_rule() {
Rule::string => self.parse_string(inner_pair),
Rule::cidr_value | Rule::ip_value => {
Ok(Value::String(inner_pair.as_str().to_string()))
}
Rule::number => self.parse_number(inner_pair),
Rule::boolean => Ok(Value::Boolean(inner_pair.as_str().to_lowercase() == "true")),
Rule::null => Ok(Value::Null),
Rule::list_value => self.parse_list(inner_pair),
Rule::identifier => {
Ok(Value::String(inner_pair.as_str().to_string()))
}
_ => Err(TqlError::ParseError {
message: format!("Unknown value type: {:?}", inner_pair.as_rule()),
position: 0,
query: None,
}),
}
}
fn parse_string(&self, pair: pest::iterators::Pair<Rule>) -> Result<Value> {
let string_pair = pair
.into_inner()
.next()
.ok_or_else(|| TqlError::ParseError {
message: "Empty string rule".to_string(),
position: 0,
query: None,
})?;
let inner_pair = string_pair
.into_inner()
.next()
.ok_or_else(|| TqlError::ParseError {
message: "No inner string content".to_string(),
position: 0,
query: None,
})?;
let content = inner_pair.as_str();
let unescaped = content
.replace("\\n", "\n")
.replace("\\r", "\r")
.replace("\\t", "\t")
.replace("\\\\", "\\")
.replace("\\\"", "\"")
.replace("\\'", "'");
Ok(Value::String(unescaped))
}
fn parse_number(&self, pair: pest::iterators::Pair<Rule>) -> Result<Value> {
let inner_pair = pair
.into_inner()
.next()
.ok_or_else(|| TqlError::ParseError {
message: "Empty number".to_string(),
position: 0,
query: None,
})?;
match inner_pair.as_rule() {
Rule::float => {
let f = inner_pair
.as_str()
.parse::<f64>()
.map_err(|_| TqlError::ParseError {
message: format!("Invalid float: {}", inner_pair.as_str()),
position: 0,
query: None,
})?;
Ok(Value::Float(f))
}
Rule::integer => {
let i = inner_pair
.as_str()
.parse::<i64>()
.map_err(|_| TqlError::ParseError {
message: format!("Invalid integer: {}", inner_pair.as_str()),
position: 0,
query: None,
})?;
Ok(Value::Integer(i))
}
_ => Err(TqlError::ParseError {
message: format!("Unknown number type: {:?}", inner_pair.as_rule()),
position: 0,
query: None,
}),
}
}
fn parse_list(&self, pair: pest::iterators::Pair<Rule>) -> Result<Value> {
let mut values = Vec::new();
for inner_pair in pair.into_inner() {
if inner_pair.as_rule() == Rule::value {
values.push(self.parse_value(inner_pair)?);
}
}
Ok(Value::List(values))
}
fn parse_mutator(&self, pair: pest::iterators::Pair<Rule>) -> Result<Mutator> {
let mut name = String::new();
let mut args = Vec::new();
let mut named_args = std::collections::HashMap::new();
for inner_pair in pair.into_inner() {
match inner_pair.as_rule() {
Rule::mutator_name => {
name = inner_pair.as_str().to_string();
}
Rule::mutator_args => {
for arg_pair in inner_pair.into_inner() {
if arg_pair.as_rule() == Rule::mutator_arg {
let mut inner = arg_pair.into_inner();
let first = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Empty mutator argument".to_string(),
position: 0,
query: None,
})?;
if first.as_rule() == Rule::mutator_named_arg {
let (key, val) = self.parse_mutator_named_arg(first)?;
named_args.insert(key, val);
} else {
args.push(self.parse_value_from_rule(first)?);
}
}
}
}
_ => {}
}
}
Ok(Mutator {
name,
args,
named_args,
})
}
fn parse_mutator_named_arg(
&self,
pair: pest::iterators::Pair<Rule>,
) -> Result<(String, Value)> {
let mut inner = pair.into_inner();
let key = inner
.next()
.ok_or_else(|| TqlError::ParseError {
message: "Missing named arg key".to_string(),
position: 0,
query: None,
})?
.as_str()
.to_string();
let value_pair = inner.next().ok_or_else(|| TqlError::ParseError {
message: "Missing named arg value".to_string(),
position: 0,
query: None,
})?;
let value = self.parse_value_from_rule(value_pair)?;
Ok((key, value))
}
fn parse_value_from_rule(&self, pair: pest::iterators::Pair<Rule>) -> Result<Value> {
match pair.as_rule() {
Rule::string => self.parse_string(pair),
Rule::number => self.parse_number(pair),
Rule::boolean => Ok(Value::Boolean(pair.as_str().to_lowercase() == "true")),
Rule::null => Ok(Value::Null),
Rule::identifier => Ok(Value::String(pair.as_str().to_string())),
_ => Err(TqlError::ParseError {
message: format!("Invalid mutator argument type: {:?}", pair.as_rule()),
position: 0,
query: None,
}),
}
}
fn normalize_operator(&self, op: &str) -> String {
let normalized = op.to_lowercase().replace(' ', "_");
match normalized.as_str() {
"=" => return "eq".to_string(),
"!=" => return "ne".to_string(),
">" => return "gt".to_string(),
">=" => return "gte".to_string(),
"<" => return "lt".to_string(),
"<=" => return "lte".to_string(),
"&&" => return "and".to_string(),
"||" => return "or".to_string(),
"!" => return "not".to_string(),
_ => {}
}
let (negated, base) = if let Some(rest) = normalized.strip_prefix('!') {
(true, rest.to_string())
} else if let Some(rest) = normalized.strip_prefix("not_") {
(true, rest.to_string())
} else {
(false, normalized)
};
let base = match base.as_str() {
"regex" | "regexp" => "matches".to_string(),
_ => base,
};
if negated {
format!("not_{}", base)
} else {
base
}
}
pub fn extract_fields(&self, query: &str) -> Result<Vec<String>> {
let ast = self.parse(query)?;
let mut fields = Vec::new();
self.collect_fields(&ast, &mut fields);
fields.sort();
fields.dedup();
Ok(fields)
}
#[allow(clippy::only_used_in_recursion)]
fn collect_fields(&self, node: &AstNode, fields: &mut Vec<String>) {
match node {
AstNode::Comparison(comp) => {
fields.push(comp.field.clone());
}
AstNode::LogicalOp(logical) => {
self.collect_fields(&logical.left, fields);
self.collect_fields(&logical.right, fields);
}
AstNode::UnaryOp(unary) => {
self.collect_fields(&unary.operand, fields);
}
AstNode::CollectionOp(coll) => {
fields.push(coll.field.clone());
}
AstNode::GeoExpr(geo) => {
fields.push(geo.field.clone());
if let Some(ref cond) = geo.conditions {
self.collect_fields(cond, fields);
}
}
AstNode::NslookupExpr(nslookup) => {
fields.push(nslookup.field.clone());
if let Some(ref cond) = nslookup.conditions {
self.collect_fields(cond, fields);
}
}
AstNode::QueryWithStats(qws) => {
self.collect_fields(&qws.filter, fields);
for agg in &qws.stats.aggregations {
if let Some(ref field) = agg.field {
if field != "*" {
fields.push(field.clone());
}
}
}
for group_by in &qws.stats.group_by {
fields.push(group_by.field.clone());
}
}
AstNode::StatsExpr(stats) => {
for agg in &stats.aggregations {
if let Some(ref field) = agg.field {
if field != "*" {
fields.push(field.clone());
}
}
}
for group_by in &stats.group_by {
fields.push(group_by.field.clone());
}
}
AstNode::MatchAll => {}
}
}
fn parse_viz_value(pair: pest::iterators::Pair<Rule>) -> Result<VizParamValue> {
let inner = pair.into_inner().next().ok_or_else(|| TqlError::ParseError {
message: "Empty viz value".to_string(),
position: 0,
query: None,
})?;
match inner.as_rule() {
Rule::string => {
let string_inner = inner.into_inner().next().unwrap();
let content = string_inner
.into_inner()
.next()
.map(|p| p.as_str().to_string())
.unwrap_or_default();
Ok(VizParamValue::String(content))
}
Rule::number => {
let num_inner = inner.into_inner().next().unwrap();
match num_inner.as_rule() {
Rule::float => {
let f: f64 =
num_inner.as_str().parse().map_err(|_| TqlError::ParseError {
message: format!("Invalid float: {}", num_inner.as_str()),
position: 0,
query: None,
})?;
Ok(VizParamValue::Float(f))
}
Rule::integer => {
let i: i64 =
num_inner.as_str().parse().map_err(|_| TqlError::ParseError {
message: format!("Invalid integer: {}", num_inner.as_str()),
position: 0,
query: None,
})?;
Ok(VizParamValue::Integer(i))
}
_ => Err(TqlError::ParseError {
message: format!("Unexpected number type: {:?}", num_inner.as_rule()),
position: 0,
query: None,
}),
}
}
Rule::boolean => {
let b = inner.as_str().eq_ignore_ascii_case("true");
Ok(VizParamValue::Boolean(b))
}
Rule::identifier => {
Ok(VizParamValue::String(inner.as_str().to_string()))
}
_ => Err(TqlError::ParseError {
message: format!("Unexpected viz value type: {:?}", inner.as_rule()),
position: 0,
query: None,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parser_creation() {
let parser = TqlParser::new();
assert_eq!(parser.max_depth, TqlParser::MAX_QUERY_DEPTH);
}
#[test]
fn test_empty_query() {
let parser = TqlParser::new();
let result = parser.parse("").unwrap();
assert!(matches!(result, AstNode::MatchAll));
}
#[test]
fn test_whitespace_only_query() {
let parser = TqlParser::new();
let result = parser.parse(" \t\n ").unwrap();
assert!(matches!(result, AstNode::MatchAll));
}
#[test]
fn test_custom_max_depth() {
let parser = TqlParser::with_max_depth(100);
assert_eq!(parser.max_depth, 100);
}
#[test]
fn test_hyphenated_field_name_eq() {
let parser = TqlParser::new();
let ast = parser.parse("event-code eq 5").unwrap();
match ast {
AstNode::Comparison(comp) => {
assert_eq!(comp.field, "event-code");
assert_eq!(comp.operator, "eq");
}
other => panic!("Expected Comparison, got {:?}", other),
}
}
#[test]
fn test_hyphenated_field_name_contains() {
let parser = TqlParser::new();
let ast = parser.parse("user-agent contains 'Mozilla'").unwrap();
match ast {
AstNode::Comparison(comp) => {
assert_eq!(comp.field, "user-agent");
assert_eq!(comp.operator, "contains");
}
other => panic!("Expected Comparison, got {:?}", other),
}
}
#[test]
fn test_hyphenated_nested_field_name() {
let parser = TqlParser::new();
let ast = parser.parse("http.x-forwarded-for eq '10.0.0.1'").unwrap();
match ast {
AstNode::Comparison(comp) => {
assert_eq!(comp.field, "http.x-forwarded-for");
}
other => panic!("Expected Comparison, got {:?}", other),
}
}
}