pub mod comparator;
pub mod error;
pub mod evaluator;
pub mod field_accessor;
pub mod file_ops;
pub mod mutators;
pub mod parser;
pub mod regex_compat;
pub mod stats_evaluator;
#[cfg(feature = "opensearch")]
pub mod opensearch;
pub use error::{Result, TqlError};
pub use evaluator::TqlEvaluator;
pub use file_ops::{CsvConfig, FileFormat, FileOps};
pub use parser::{AstNode, TqlParser};
pub use regex_compat::{is_lucene_safe, to_lucene_regex, LuceneRegex};
pub use stats_evaluator::{AggregationSpec, GroupBySpec, StatsEvaluator, StatsQuery};
use serde_json::Value as JsonValue;
use std::path::Path;
pub struct Tql {
parser: TqlParser,
evaluator: TqlEvaluator,
stats_evaluator: StatsEvaluator,
}
impl Default for Tql {
fn default() -> Self {
Self::new()
}
}
impl Tql {
pub fn new() -> Self {
Self {
parser: TqlParser::new(),
evaluator: TqlEvaluator::new(),
stats_evaluator: StatsEvaluator::new(),
}
}
pub fn with_max_depth(max_depth: usize) -> Self {
Self {
parser: TqlParser::with_max_depth(max_depth),
evaluator: TqlEvaluator::with_max_depth(max_depth),
stats_evaluator: StatsEvaluator::new(),
}
}
pub fn query<'a>(&self, records: &'a [JsonValue], query: &str) -> Result<Vec<&'a JsonValue>> {
let ast = self.parser.parse(query)?;
self.evaluator.filter(&ast, records)
}
pub fn query_enriched(&self, records: &[JsonValue], query: &str) -> Result<Vec<JsonValue>> {
let ast = self.parser.parse(query)?;
self.evaluator.filter_and_enrich(&ast, records)
}
pub fn count(&self, records: &[JsonValue], query: &str) -> Result<usize> {
let ast = self.parser.parse(query)?;
self.evaluator.count(&ast, records)
}
pub fn matches(&self, record: &JsonValue, query: &str) -> Result<bool> {
let ast = self.parser.parse(query)?;
self.evaluator.evaluate(&ast, record)
}
pub fn parse(&self, query: &str) -> Result<AstNode> {
self.parser.parse(query)
}
pub fn is_stats_query(&self, query: &str) -> Result<bool> {
let ast = self.parser.parse(query)?;
Ok(matches!(
ast,
AstNode::StatsExpr(_) | AstNode::QueryWithStats(_)
))
}
pub fn evaluate_stats(&self, records: &[JsonValue], query: &str) -> Result<JsonValue> {
use crate::parser::QueryWithStatsNode;
let ast = self.parser.parse(query)?;
match ast {
AstNode::StatsExpr(stats_node) => {
self.evaluate_stats_node(records, &stats_node)
}
AstNode::QueryWithStats(QueryWithStatsNode { filter, stats }) => {
let filtered = self.evaluator.filter(&filter, records)?;
let owned_records: Vec<JsonValue> = filtered.iter().map(|&r| r.clone()).collect();
self.evaluate_stats_node(&owned_records, &stats)
}
_ => Err(TqlError::SyntaxError {
message: "Query does not contain stats expressions".to_string(),
position: None,
query: Some(query.to_string()),
suggestions: vec!["Use '| stats' to add aggregations".to_string()],
}),
}
}
fn evaluate_stats_node(
&self,
records: &[JsonValue],
stats_node: &parser::StatsNode,
) -> Result<JsonValue> {
let aggregations: Vec<AggregationSpec> = stats_node
.aggregations
.iter()
.map(AggregationSpec::from)
.collect();
let group_by: Vec<GroupBySpec> =
stats_node.group_by.iter().map(GroupBySpec::from).collect();
let stats_query = StatsQuery {
aggregations,
group_by,
};
let mut result = self.stats_evaluator.evaluate_stats(records, &stats_query)?;
if let Some(obj) = result.as_object_mut() {
if let Some(hint) = &stats_node.viz_hint {
obj.insert("viz_hint".to_string(), serde_json::json!(hint));
}
if let Some(params) = &stats_node.viz_params {
obj.insert(
"viz_params".to_string(),
serde_json::to_value(params).unwrap_or_default(),
);
}
}
Ok(result)
}
pub fn query_file(
&self,
path: &Path,
query: &str,
format: FileFormat,
csv_config: &CsvConfig,
) -> Result<Vec<JsonValue>> {
let ops = FileOps::new();
ops.query_file(path, query, format, csv_config)
}
pub fn query_folder(
&self,
folder_path: &Path,
query: &str,
pattern: &str,
format: FileFormat,
csv_config: &CsvConfig,
parallel: bool,
) -> Result<Vec<JsonValue>> {
let ops = FileOps::new();
ops.query_folder(folder_path, query, pattern, format, csv_config, parallel)
}
pub fn has_post_processing_mutators(&self, query: &str) -> Result<bool> {
let ast = self.parser.parse(query)?;
Ok(Self::ast_has_mutators(&ast))
}
pub fn ast_has_post_processing_mutators(&self, ast: &AstNode) -> bool {
Self::ast_has_mutators(ast)
}
pub fn extract_fields(&self, query: &str) -> Result<Vec<String>> {
self.parser.extract_fields(query)
}
pub fn extract_mutators_from_ast(ast: &AstNode) -> Vec<(String, Vec<parser::Mutator>)> {
let mut mutators = Vec::new();
Self::collect_mutators_recursive(ast, &mut mutators);
mutators
}
fn ast_has_mutators(node: &AstNode) -> bool {
match node {
AstNode::MatchAll => false,
AstNode::Comparison(comp) => {
if let Some(mutators) = &comp.field_mutators {
if !mutators.is_empty() {
return true;
}
}
if let Some(mutators) = &comp.value_mutators {
if !mutators.is_empty() {
return true;
}
}
false
}
AstNode::LogicalOp(logical) => {
Self::ast_has_mutators(&logical.left) || Self::ast_has_mutators(&logical.right)
}
AstNode::UnaryOp(unary) => Self::ast_has_mutators(&unary.operand),
AstNode::CollectionOp(coll) => {
if let Some(mutators) = &coll.field_mutators {
if !mutators.is_empty() {
return true;
}
}
false
}
AstNode::GeoExpr(_) => {
true
}
AstNode::NslookupExpr(_) => {
true
}
AstNode::StatsExpr(stats) => {
for agg in &stats.aggregations {
if let Some(mutators) = &agg.field_mutators {
if !mutators.is_empty() {
return true;
}
}
}
false
}
AstNode::QueryWithStats(qws) => {
if Self::ast_has_mutators(&qws.filter) {
return true;
}
for agg in &qws.stats.aggregations {
if let Some(mutators) = &agg.field_mutators {
if !mutators.is_empty() {
return true;
}
}
}
false
}
}
}
fn collect_mutators_recursive(
node: &AstNode,
mutators: &mut Vec<(String, Vec<parser::Mutator>)>,
) {
match node {
AstNode::MatchAll => {}
AstNode::Comparison(comp) => {
if let Some(field_mutators) = &comp.field_mutators {
if !field_mutators.is_empty() {
mutators.push((comp.field.clone(), field_mutators.clone()));
}
}
}
AstNode::LogicalOp(logical) => {
Self::collect_mutators_recursive(&logical.left, mutators);
Self::collect_mutators_recursive(&logical.right, mutators);
}
AstNode::UnaryOp(unary) => {
Self::collect_mutators_recursive(&unary.operand, mutators);
}
AstNode::CollectionOp(coll) => {
if let Some(field_mutators) = &coll.field_mutators {
if !field_mutators.is_empty() {
mutators.push((coll.field.clone(), field_mutators.clone()));
}
}
}
AstNode::GeoExpr(geo) => {
mutators.push((
geo.field.clone(),
vec![parser::Mutator {
name: "geoip".to_string(),
args: vec![],
named_args: std::collections::HashMap::new(),
}],
));
}
AstNode::NslookupExpr(nslookup) => {
mutators.push((
nslookup.field.clone(),
vec![parser::Mutator {
name: "nslookup".to_string(),
args: vec![],
named_args: std::collections::HashMap::new(),
}],
));
}
AstNode::StatsExpr(stats) => {
for agg in &stats.aggregations {
if let Some(field_mutators) = &agg.field_mutators {
if !field_mutators.is_empty() {
if let Some(field) = &agg.field {
mutators.push((field.clone(), field_mutators.clone()));
}
}
}
}
}
AstNode::QueryWithStats(qws) => {
Self::collect_mutators_recursive(&qws.filter, mutators);
for agg in &qws.stats.aggregations {
if let Some(field_mutators) = &agg.field_mutators {
if !field_mutators.is_empty() {
if let Some(field) = &agg.field {
mutators.push((field.clone(), field_mutators.clone()));
}
}
}
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_tql_creation() {
let _tql = Tql::new();
}
#[test]
fn test_tql_query() {
let tql = Tql::new();
let records = vec![
json!({"name": "Alice", "age": 30}),
json!({"name": "Bob", "age": 20}),
json!({"name": "Charlie", "age": 35}),
];
let results = tql.query(&records, "age > 25").unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn test_tql_count() {
let tql = Tql::new();
let records = vec![
json!({"status": "active"}),
json!({"status": "inactive"}),
json!({"status": "active"}),
];
let count = tql.count(&records, "status eq 'active'").unwrap();
assert_eq!(count, 2);
}
#[test]
fn test_tql_matches() {
let tql = Tql::new();
let record = json!({"name": "John", "age": 30});
assert!(tql.matches(&record, "age >= 25").unwrap());
assert!(!tql.matches(&record, "age < 25").unwrap());
}
#[test]
fn test_tql_parse() {
let tql = Tql::new();
let ast = tql.parse("age > 25 AND name eq 'John'").unwrap();
assert!(matches!(ast, AstNode::LogicalOp(_)));
}
#[test]
fn test_tql_with_mutators() {
let tql = Tql::new();
let records = vec![
json!({"email": "USER@EXAMPLE.COM"}),
json!({"email": "user@test.org"}),
];
let results = tql
.query(&records, "email | lowercase contains '@example.com'")
.unwrap();
assert_eq!(results.len(), 1);
}
#[test]
fn test_has_post_processing_mutators_no_mutators() {
let tql = Tql::new();
assert!(!tql.has_post_processing_mutators("age > 25").unwrap());
assert!(!tql.has_post_processing_mutators("name eq 'John'").unwrap());
assert!(!tql
.has_post_processing_mutators("age > 25 AND status eq 'active'")
.unwrap());
assert!(!tql.has_post_processing_mutators("NOT (age < 18)").unwrap());
assert!(!tql.has_post_processing_mutators("name exists").unwrap());
}
#[test]
fn test_has_post_processing_mutators_with_mutators() {
let tql = Tql::new();
assert!(tql
.has_post_processing_mutators("name | lowercase eq 'john'")
.unwrap());
assert!(tql
.has_post_processing_mutators("name | uppercase eq 'JOHN'")
.unwrap());
assert!(tql
.has_post_processing_mutators("message | trim eq 'hello'")
.unwrap());
assert!(tql
.has_post_processing_mutators("source.ip | is_private eq true")
.unwrap());
assert!(tql
.has_post_processing_mutators("dest.ip | is_global eq true")
.unwrap());
assert!(tql
.has_post_processing_mutators("age > 25 AND name | lowercase eq 'john'")
.unwrap());
assert!(tql
.has_post_processing_mutators("NOT (name | lowercase eq 'admin')")
.unwrap());
assert!(tql
.has_post_processing_mutators("message | trim | lowercase eq 'hello'")
.unwrap());
}
#[test]
fn test_extract_mutators_from_ast() {
let tql = Tql::new();
let ast = tql.parse("name | lowercase eq 'john'").unwrap();
let mutators = Tql::extract_mutators_from_ast(&ast);
assert_eq!(mutators.len(), 1);
assert_eq!(mutators[0].0, "name");
assert_eq!(mutators[0].1.len(), 1);
assert_eq!(mutators[0].1[0].name, "lowercase");
}
#[test]
fn test_extract_mutators_from_ast_chained() {
let tql = Tql::new();
let ast = tql.parse("message | trim | lowercase eq 'hello'").unwrap();
let mutators = Tql::extract_mutators_from_ast(&ast);
assert_eq!(mutators.len(), 1);
assert_eq!(mutators[0].0, "message");
assert_eq!(mutators[0].1.len(), 2);
assert_eq!(mutators[0].1[0].name, "trim");
assert_eq!(mutators[0].1[1].name, "lowercase");
}
#[test]
fn test_extract_mutators_from_ast_multiple_fields() {
let tql = Tql::new();
let ast = tql
.parse("name | lowercase eq 'john' AND email | uppercase contains 'TEST'")
.unwrap();
let mutators = Tql::extract_mutators_from_ast(&ast);
assert_eq!(mutators.len(), 2);
let field_names: Vec<&str> = mutators.iter().map(|(f, _)| f.as_str()).collect();
assert!(field_names.contains(&"name"));
assert!(field_names.contains(&"email"));
}
}