tellaro_query_language/lib.rs
1//! # TQL - Tellaro Query Language
2//!
3//! A flexible, human-friendly query language for searching and filtering structured data.
4//!
5//! TQL provides an intuitive syntax similar to SQL WHERE clauses, with support for:
6//! - Nested field access (`user.profile.name`)
7//! - Rich set of operators (comparison, logical, collection)
8//! - Field transformations via mutators (`field | lowercase`)
9//! - Statistical aggregations (`| stats count() by field`)
10//! - OpenSearch backend integration
11//! - GeoIP lookups (MaxMind and DB-IP)
12//!
13//! ## Quick Start
14//!
15//! ```ignore
16//! use tql::{Tql, TqlConfig};
17//! use serde_json::json;
18//!
19//! let tql = Tql::new(TqlConfig::default());
20//! let records = vec![
21//! json!({"name": "John", "age": 30}),
22//! json!({"name": "Jane", "age": 25}),
23//! ];
24//!
25//! let results = tql.query(&records, "age > 25").unwrap();
26//! assert_eq!(results.len(), 1);
27//! ```
28//!
29//! ## Feature Parity
30//!
31//! This Rust implementation maintains 100% feature parity with the Python version,
32//! providing identical syntax, behavior, and capabilities.
33
34pub mod error;
35pub mod parser;
36pub mod field_accessor;
37pub mod comparator;
38pub mod evaluator;
39pub mod mutators;
40pub mod stats_evaluator;
41
42// OpenSearch backend support (optional feature)
43#[cfg(feature = "opensearch")]
44pub mod opensearch;
45
46// Re-export main types
47pub use error::{Result, TqlError};
48pub use parser::{AstNode, TqlParser};
49pub use evaluator::TqlEvaluator;
50pub use stats_evaluator::{StatsEvaluator, StatsQuery, AggregationSpec};
51
52use serde_json::Value as JsonValue;
53
54/// Main TQL query interface
55///
56/// Provides a high-level API for parsing and executing TQL queries against JSON data.
57///
58/// # Examples
59///
60/// ```
61/// use tql::Tql;
62/// use serde_json::json;
63///
64/// let tql = Tql::new();
65///
66/// let records = vec![
67/// json!({"name": "Alice", "age": 30}),
68/// json!({"name": "Bob", "age": 25}),
69/// ];
70///
71/// let results = tql.query(&records, "age > 25").unwrap();
72/// assert_eq!(results.len(), 1);
73/// ```
74pub struct Tql {
75 parser: TqlParser,
76 evaluator: TqlEvaluator,
77 stats_evaluator: StatsEvaluator,
78}
79
80impl Default for Tql {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86impl Tql {
87 /// Create a new TQL instance with default settings
88 pub fn new() -> Self {
89 Self {
90 parser: TqlParser::new(),
91 evaluator: TqlEvaluator::new(),
92 stats_evaluator: StatsEvaluator::new(),
93 }
94 }
95
96 /// Create a new TQL instance with custom parser depth limit
97 pub fn with_max_depth(max_depth: usize) -> Self {
98 Self {
99 parser: TqlParser::with_max_depth(max_depth),
100 evaluator: TqlEvaluator::with_max_depth(max_depth),
101 stats_evaluator: StatsEvaluator::new(),
102 }
103 }
104
105 /// Execute a TQL query against a list of records
106 ///
107 /// # Arguments
108 ///
109 /// * `records` - A slice of JSON values to query against
110 /// * `query` - The TQL query string
111 ///
112 /// # Returns
113 ///
114 /// A vector of references to matching records
115 ///
116 /// # Examples
117 ///
118 /// ```
119 /// use tql::Tql;
120 /// use serde_json::json;
121 ///
122 /// let tql = Tql::new();
123 /// let records = vec![
124 /// json!({"name": "Alice", "age": 30}),
125 /// json!({"name": "Bob", "age": 20}),
126 /// ];
127 ///
128 /// let results = tql.query(&records, "age >= 25").unwrap();
129 /// assert_eq!(results.len(), 1);
130 /// ```
131 pub fn query<'a>(&self, records: &'a [JsonValue], query: &str) -> Result<Vec<&'a JsonValue>> {
132 let ast = self.parser.parse(query)?;
133 self.evaluator.filter(&ast, records)
134 }
135
136 /// Execute a TQL query with enrichment (applies field mutators to results)
137 ///
138 /// # Arguments
139 ///
140 /// * `records` - A slice of JSON values to query against
141 /// * `query` - The TQL query string
142 ///
143 /// # Returns
144 ///
145 /// A vector of owned records with mutators applied
146 pub fn query_enriched(&self, records: &[JsonValue], query: &str) -> Result<Vec<JsonValue>> {
147 let ast = self.parser.parse(query)?;
148 self.evaluator.filter_and_enrich(&ast, records)
149 }
150
151 /// Count the number of records matching a query
152 ///
153 /// # Arguments
154 ///
155 /// * `records` - A slice of JSON values to query against
156 /// * `query` - The TQL query string
157 ///
158 /// # Returns
159 ///
160 /// The number of matching records
161 pub fn count(&self, records: &[JsonValue], query: &str) -> Result<usize> {
162 let ast = self.parser.parse(query)?;
163 self.evaluator.count(&ast, records)
164 }
165
166 /// Evaluate a query against a single record
167 ///
168 /// # Arguments
169 ///
170 /// * `record` - A JSON value to evaluate against
171 /// * `query` - The TQL query string
172 ///
173 /// # Returns
174 ///
175 /// true if the record matches the query, false otherwise
176 pub fn matches(&self, record: &JsonValue, query: &str) -> Result<bool> {
177 let ast = self.parser.parse(query)?;
178 self.evaluator.evaluate(&ast, record)
179 }
180
181 /// Parse a TQL query into an AST without executing it
182 ///
183 /// Useful for pre-compiling queries or validating syntax
184 pub fn parse(&self, query: &str) -> Result<AstNode> {
185 self.parser.parse(query)
186 }
187
188 /// Check if a query contains stats expressions
189 ///
190 /// # Arguments
191 ///
192 /// * `query` - The TQL query string
193 ///
194 /// # Returns
195 ///
196 /// true if the query contains stats aggregations
197 pub fn is_stats_query(&self, query: &str) -> Result<bool> {
198 let ast = self.parser.parse(query)?;
199 Ok(matches!(ast, AstNode::StatsExpr(_) | AstNode::QueryWithStats(_)))
200 }
201
202 /// Execute a stats query against a list of records
203 ///
204 /// # Arguments
205 ///
206 /// * `records` - A slice of JSON values to aggregate
207 /// * `query` - The TQL query string containing stats expression
208 ///
209 /// # Returns
210 ///
211 /// Aggregated results as a JSON value
212 ///
213 /// # Examples
214 ///
215 /// ```ignore
216 /// use tql::Tql;
217 /// use serde_json::json;
218 ///
219 /// let tql = Tql::new();
220 /// let records = vec![
221 /// json!({"name": "Alice", "status": "active"}),
222 /// json!({"name": "Bob", "status": "active"}),
223 /// json!({"name": "Charlie", "status": "inactive"}),
224 /// ];
225 ///
226 /// let results = tql.evaluate_stats(&records, "| stats count() by status").unwrap();
227 /// ```
228 pub fn evaluate_stats(&self, records: &[JsonValue], query: &str) -> Result<JsonValue> {
229 use crate::parser::QueryWithStatsNode;
230
231 let ast = self.parser.parse(query)?;
232
233 match ast {
234 AstNode::StatsExpr(stats_node) => {
235 // Pure stats query (no filter)
236 self.evaluate_stats_node(records, &stats_node)
237 }
238 AstNode::QueryWithStats(QueryWithStatsNode { filter, stats }) => {
239 // Filter + stats query
240 let filtered = self.evaluator.filter(&filter, records)?;
241 let owned_records: Vec<JsonValue> = filtered.iter().map(|&r| r.clone()).collect();
242 self.evaluate_stats_node(&owned_records, &stats)
243 }
244 _ => Err(TqlError::SyntaxError {
245 message: "Query does not contain stats expressions".to_string(),
246 position: None,
247 query: Some(query.to_string()),
248 suggestions: vec!["Use '| stats' to add aggregations".to_string()],
249 }),
250 }
251 }
252
253 /// Helper to evaluate a stats node
254 fn evaluate_stats_node(&self, records: &[JsonValue], stats_node: &parser::StatsNode) -> Result<JsonValue> {
255 use crate::parser::{Aggregation, GroupBy};
256
257 // Convert AST stats node to StatsQuery
258 let aggregations: Vec<AggregationSpec> = stats_node
259 .aggregations
260 .iter()
261 .map(|agg: &Aggregation| AggregationSpec {
262 function: agg.function.clone(),
263 field: agg.field.clone().unwrap_or_else(|| "*".to_string()),
264 alias: agg.alias.clone(),
265 params: std::collections::HashMap::new(),
266 })
267 .collect();
268
269 let group_by: Vec<String> = stats_node
270 .group_by
271 .iter()
272 .map(|gb: &GroupBy| gb.field.clone())
273 .collect();
274
275 let stats_query = StatsQuery {
276 aggregations,
277 group_by,
278 };
279
280 self.stats_evaluator.evaluate_stats(records, &stats_query)
281 }
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use serde_json::json;
288
289 #[test]
290 fn test_tql_creation() {
291 let _tql = Tql::new();
292 }
293
294 #[test]
295 fn test_tql_query() {
296 let tql = Tql::new();
297 let records = vec![
298 json!({"name": "Alice", "age": 30}),
299 json!({"name": "Bob", "age": 20}),
300 json!({"name": "Charlie", "age": 35}),
301 ];
302
303 let results = tql.query(&records, "age > 25").unwrap();
304 assert_eq!(results.len(), 2);
305 }
306
307 #[test]
308 fn test_tql_count() {
309 let tql = Tql::new();
310 let records = vec![
311 json!({"status": "active"}),
312 json!({"status": "inactive"}),
313 json!({"status": "active"}),
314 ];
315
316 let count = tql.count(&records, "status eq 'active'").unwrap();
317 assert_eq!(count, 2);
318 }
319
320 #[test]
321 fn test_tql_matches() {
322 let tql = Tql::new();
323 let record = json!({"name": "John", "age": 30});
324
325 assert!(tql.matches(&record, "age >= 25").unwrap());
326 assert!(!tql.matches(&record, "age < 25").unwrap());
327 }
328
329 #[test]
330 fn test_tql_parse() {
331 let tql = Tql::new();
332 let ast = tql.parse("age > 25 AND name eq 'John'").unwrap();
333
334 // Verify AST was created (just check it doesn't error)
335 assert!(matches!(ast, AstNode::LogicalOp(_)));
336 }
337
338 #[test]
339 fn test_tql_with_mutators() {
340 let tql = Tql::new();
341 let records = vec![
342 json!({"email": "USER@EXAMPLE.COM"}),
343 json!({"email": "user@test.org"}),
344 ];
345
346 let results = tql.query(&records, "email | lowercase contains '@example.com'").unwrap();
347 assert_eq!(results.len(), 1);
348 }
349}