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}