tellaro-query-language 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tellaro_query_language-0.1.0.dist-info/LICENSE +21 -0
- tellaro_query_language-0.1.0.dist-info/METADATA +401 -0
- tellaro_query_language-0.1.0.dist-info/RECORD +56 -0
- tellaro_query_language-0.1.0.dist-info/WHEEL +4 -0
- tellaro_query_language-0.1.0.dist-info/entry_points.txt +7 -0
- tql/__init__.py +47 -0
- tql/analyzer.py +385 -0
- tql/cache/__init__.py +7 -0
- tql/cache/base.py +25 -0
- tql/cache/memory.py +63 -0
- tql/cache/redis.py +68 -0
- tql/core.py +929 -0
- tql/core_components/README.md +92 -0
- tql/core_components/__init__.py +20 -0
- tql/core_components/file_operations.py +113 -0
- tql/core_components/opensearch_operations.py +869 -0
- tql/core_components/stats_operations.py +200 -0
- tql/core_components/validation_operations.py +599 -0
- tql/evaluator.py +379 -0
- tql/evaluator_components/README.md +131 -0
- tql/evaluator_components/__init__.py +17 -0
- tql/evaluator_components/field_access.py +176 -0
- tql/evaluator_components/special_expressions.py +296 -0
- tql/evaluator_components/value_comparison.py +315 -0
- tql/exceptions.py +160 -0
- tql/geoip_normalizer.py +233 -0
- tql/mutator_analyzer.py +830 -0
- tql/mutators/__init__.py +222 -0
- tql/mutators/base.py +78 -0
- tql/mutators/dns.py +316 -0
- tql/mutators/encoding.py +218 -0
- tql/mutators/geo.py +363 -0
- tql/mutators/list.py +212 -0
- tql/mutators/network.py +163 -0
- tql/mutators/security.py +225 -0
- tql/mutators/string.py +165 -0
- tql/opensearch.py +78 -0
- tql/opensearch_components/README.md +130 -0
- tql/opensearch_components/__init__.py +17 -0
- tql/opensearch_components/field_mapping.py +399 -0
- tql/opensearch_components/lucene_converter.py +305 -0
- tql/opensearch_components/query_converter.py +775 -0
- tql/opensearch_mappings.py +309 -0
- tql/opensearch_stats.py +451 -0
- tql/parser.py +1363 -0
- tql/parser_components/README.md +72 -0
- tql/parser_components/__init__.py +20 -0
- tql/parser_components/ast_builder.py +162 -0
- tql/parser_components/error_analyzer.py +101 -0
- tql/parser_components/field_extractor.py +112 -0
- tql/parser_components/grammar.py +473 -0
- tql/post_processor.py +737 -0
- tql/scripts.py +124 -0
- tql/stats_evaluator.py +444 -0
- tql/stats_transformer.py +184 -0
- tql/validators.py +110 -0
tql/evaluator.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""TQL query evaluator.
|
|
2
|
+
|
|
3
|
+
This module provides the TQLEvaluator class for executing TQL queries against
|
|
4
|
+
data records in memory.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from .evaluator_components import FieldAccessor, SpecialExpressionEvaluator, ValueComparator
|
|
10
|
+
from .mutators import apply_mutators
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TQLEvaluator:
|
|
14
|
+
"""Evaluates TQL queries against data records.
|
|
15
|
+
|
|
16
|
+
This class takes parsed TQL ASTs and evaluates them against Python
|
|
17
|
+
dictionaries representing data records.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Sentinel value to distinguish missing fields from None values
|
|
21
|
+
_MISSING_FIELD = object()
|
|
22
|
+
|
|
23
|
+
def __init__(self):
|
|
24
|
+
"""Initialize the evaluator."""
|
|
25
|
+
# Initialize component evaluators
|
|
26
|
+
self.field_accessor = FieldAccessor()
|
|
27
|
+
self.value_comparator = ValueComparator()
|
|
28
|
+
# Pass sentinel value to components
|
|
29
|
+
self.field_accessor._MISSING_FIELD = self._MISSING_FIELD
|
|
30
|
+
self.value_comparator._MISSING_FIELD = self._MISSING_FIELD
|
|
31
|
+
# Initialize special expression evaluator with callbacks
|
|
32
|
+
self.special_evaluator = SpecialExpressionEvaluator(self._get_field_value, self._evaluate_node)
|
|
33
|
+
self.special_evaluator._MISSING_FIELD = self._MISSING_FIELD
|
|
34
|
+
|
|
35
|
+
def evaluate(
|
|
36
|
+
self, ast: Dict[str, Any], records: List[Dict[str, Any]], field_mappings: Optional[Dict[str, str]] = None
|
|
37
|
+
) -> List[Dict[str, Any]]:
|
|
38
|
+
"""Evaluate a TQL query against a list of records.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
ast: The parsed TQL query AST
|
|
42
|
+
records: List of dictionaries to evaluate against
|
|
43
|
+
field_mappings: Optional field name mappings
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
List of records that match the query
|
|
47
|
+
"""
|
|
48
|
+
results = []
|
|
49
|
+
for record in records:
|
|
50
|
+
if self.evaluate_single(ast, record, field_mappings):
|
|
51
|
+
results.append(record)
|
|
52
|
+
return results
|
|
53
|
+
|
|
54
|
+
def evaluate_single(
|
|
55
|
+
self, ast: Dict[str, Any], record: Dict[str, Any], field_mappings: Optional[Dict[str, str]] = None
|
|
56
|
+
) -> bool:
|
|
57
|
+
"""Evaluate a TQL query against a single record.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
ast: The parsed TQL query AST
|
|
61
|
+
record: Dictionary to evaluate against
|
|
62
|
+
field_mappings: Optional field name mappings
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
True if the record matches the query
|
|
66
|
+
"""
|
|
67
|
+
field_mappings = field_mappings or {}
|
|
68
|
+
return self._evaluate_node(ast, record, field_mappings)
|
|
69
|
+
|
|
70
|
+
def _evaluate_node(self, node: Any, record: Dict[str, Any], field_mappings: Dict[str, str]) -> bool:
|
|
71
|
+
"""Evaluate a single AST node against a record.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
node: AST node to evaluate
|
|
75
|
+
record: Record to evaluate against
|
|
76
|
+
field_mappings: Field name mappings
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Boolean result of evaluation
|
|
80
|
+
"""
|
|
81
|
+
if isinstance(node, dict):
|
|
82
|
+
node_type = node.get("type")
|
|
83
|
+
|
|
84
|
+
if node_type == "comparison":
|
|
85
|
+
return self._evaluate_comparison(node, record, field_mappings)
|
|
86
|
+
elif node_type == "logical_op":
|
|
87
|
+
return self._evaluate_logical_op(node, record, field_mappings)
|
|
88
|
+
elif node_type == "unary_op":
|
|
89
|
+
return self._evaluate_unary_op(node, record, field_mappings)
|
|
90
|
+
elif node_type == "collection_op":
|
|
91
|
+
return self._evaluate_collection_op(node, record, field_mappings)
|
|
92
|
+
elif node_type == "geo_expr":
|
|
93
|
+
return self.special_evaluator.evaluate_geo_expr(node, record, field_mappings)
|
|
94
|
+
elif node_type == "nslookup_expr":
|
|
95
|
+
return self.special_evaluator.evaluate_nslookup_expr(node, record, field_mappings)
|
|
96
|
+
|
|
97
|
+
# Unknown node type
|
|
98
|
+
return False
|
|
99
|
+
|
|
100
|
+
def _evaluate_comparison(
|
|
101
|
+
self, node: Dict[str, Any], record: Dict[str, Any], field_mappings: Dict[str, str]
|
|
102
|
+
) -> bool:
|
|
103
|
+
"""Evaluate a comparison operation.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
node: Comparison node with field, operator, and value
|
|
107
|
+
record: Record to evaluate against
|
|
108
|
+
field_mappings: Field name mappings
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Boolean result of comparison
|
|
112
|
+
"""
|
|
113
|
+
field_name = node["field"]
|
|
114
|
+
operator = node["operator"]
|
|
115
|
+
expected_value = node["value"]
|
|
116
|
+
field_mutators = node.get("field_mutators", [])
|
|
117
|
+
value_mutators = node.get("value_mutators", [])
|
|
118
|
+
type_hint = node.get("type_hint")
|
|
119
|
+
|
|
120
|
+
# Apply field mapping
|
|
121
|
+
actual_field = self.field_accessor.apply_field_mapping(field_name, field_mappings)
|
|
122
|
+
|
|
123
|
+
# Get the field value
|
|
124
|
+
field_value = self._get_field_value(record, actual_field)
|
|
125
|
+
|
|
126
|
+
# Apply field mutators if any
|
|
127
|
+
if field_mutators and field_value is not self._MISSING_FIELD:
|
|
128
|
+
field_value = apply_mutators(field_value, field_mutators, field_name, record)
|
|
129
|
+
|
|
130
|
+
# Apply value mutators if any
|
|
131
|
+
if value_mutators:
|
|
132
|
+
expected_value = apply_mutators(expected_value, value_mutators, field_name, record)
|
|
133
|
+
|
|
134
|
+
# Apply type hint if specified
|
|
135
|
+
if type_hint and field_value is not self._MISSING_FIELD:
|
|
136
|
+
field_value = self.field_accessor.apply_type_hint(
|
|
137
|
+
field_value, type_hint, field_name, operator, field_mappings
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Perform the comparison
|
|
141
|
+
return self.value_comparator.compare_values(field_value, operator, expected_value)
|
|
142
|
+
|
|
143
|
+
def _evaluate_logical_op(
|
|
144
|
+
self, node: Dict[str, Any], record: Dict[str, Any], field_mappings: Dict[str, str]
|
|
145
|
+
) -> bool:
|
|
146
|
+
"""Evaluate a logical operation (AND/OR).
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
node: Logical operation node
|
|
150
|
+
record: Record to evaluate against
|
|
151
|
+
field_mappings: Field name mappings
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
Boolean result of logical operation
|
|
155
|
+
"""
|
|
156
|
+
operator = node["operator"]
|
|
157
|
+
left = node["left"]
|
|
158
|
+
right = node["right"]
|
|
159
|
+
|
|
160
|
+
if operator == "and":
|
|
161
|
+
# Short-circuit evaluation for AND
|
|
162
|
+
left_result = self._evaluate_node(left, record, field_mappings)
|
|
163
|
+
if not left_result:
|
|
164
|
+
return False
|
|
165
|
+
return self._evaluate_node(right, record, field_mappings)
|
|
166
|
+
elif operator == "or":
|
|
167
|
+
# Short-circuit evaluation for OR
|
|
168
|
+
left_result = self._evaluate_node(left, record, field_mappings)
|
|
169
|
+
if left_result:
|
|
170
|
+
return True
|
|
171
|
+
return self._evaluate_node(right, record, field_mappings)
|
|
172
|
+
else:
|
|
173
|
+
raise ValueError(f"Unknown logical operator: {operator}")
|
|
174
|
+
|
|
175
|
+
def _evaluate_unary_op(self, node: Dict[str, Any], record: Dict[str, Any], field_mappings: Dict[str, str]) -> bool:
|
|
176
|
+
"""Evaluate a unary operation (NOT).
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
node: Unary operation node
|
|
180
|
+
record: Record to evaluate against
|
|
181
|
+
field_mappings: Field name mappings
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Boolean result of unary operation
|
|
185
|
+
"""
|
|
186
|
+
operator = node["operator"]
|
|
187
|
+
operand = node["operand"]
|
|
188
|
+
|
|
189
|
+
if operator == "not":
|
|
190
|
+
# Handle special optimizations for NOT operations
|
|
191
|
+
# NOT (field exists) and NOT (field is null) need special handling
|
|
192
|
+
|
|
193
|
+
# First check if the operand would fail due to missing fields
|
|
194
|
+
# But NOT of an exists check on a missing field should still be True
|
|
195
|
+
if self._is_exists_operation(operand):
|
|
196
|
+
# NOT EXISTS check - return opposite of exists
|
|
197
|
+
return not self._evaluate_node(operand, record, field_mappings)
|
|
198
|
+
elif self._is_null_operation(operand):
|
|
199
|
+
# NOT (field IS NULL) check
|
|
200
|
+
return not self._evaluate_node(operand, record, field_mappings)
|
|
201
|
+
elif self._is_not_null_operation(operand):
|
|
202
|
+
# NOT (field IS NOT NULL) - double negative
|
|
203
|
+
# This should only be True if the field exists with a null value
|
|
204
|
+
# For missing fields, it should remain False
|
|
205
|
+
# field = operand.get("field") # Not needed - handled by _operand_has_missing_fields
|
|
206
|
+
if self._operand_has_missing_fields(operand, record, field_mappings):
|
|
207
|
+
# Missing field - NOT (IS NOT NULL) is False
|
|
208
|
+
return False
|
|
209
|
+
else:
|
|
210
|
+
# Field exists - evaluate normally
|
|
211
|
+
return not self._evaluate_node(operand, record, field_mappings)
|
|
212
|
+
elif self._operand_has_missing_fields(operand, record, field_mappings):
|
|
213
|
+
# For operations on missing fields (except exists/null checks), NOT returns True
|
|
214
|
+
# This matches OpenSearch behavior where must_not includes docs with missing fields
|
|
215
|
+
return True
|
|
216
|
+
else:
|
|
217
|
+
# Standard NOT operation
|
|
218
|
+
return not self._evaluate_node(operand, record, field_mappings)
|
|
219
|
+
else:
|
|
220
|
+
raise ValueError(f"Unknown unary operator: {operator}")
|
|
221
|
+
|
|
222
|
+
def _evaluate_collection_op( # noqa: C901
|
|
223
|
+
self, node: Dict[str, Any], record: Dict[str, Any], field_mappings: Dict[str, str]
|
|
224
|
+
) -> bool:
|
|
225
|
+
"""Evaluate a collection operation (ANY/ALL).
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
node: Collection operation node
|
|
229
|
+
record: Record to evaluate against
|
|
230
|
+
field_mappings: Field name mappings
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Boolean result of collection operation
|
|
234
|
+
"""
|
|
235
|
+
operator = node["operator"]
|
|
236
|
+
field_name = node["field"]
|
|
237
|
+
comparison_operator = node["comparison_operator"]
|
|
238
|
+
expected_value = node["value"]
|
|
239
|
+
field_mutators = node.get("field_mutators", [])
|
|
240
|
+
|
|
241
|
+
# Apply field mapping
|
|
242
|
+
actual_field = self.field_accessor.apply_field_mapping(field_name, field_mappings)
|
|
243
|
+
|
|
244
|
+
# Get the field value
|
|
245
|
+
field_value = self._get_field_value(record, actual_field)
|
|
246
|
+
|
|
247
|
+
# If field is missing, return False
|
|
248
|
+
if field_value is self._MISSING_FIELD:
|
|
249
|
+
return False
|
|
250
|
+
|
|
251
|
+
# Apply mutators if any
|
|
252
|
+
if field_mutators:
|
|
253
|
+
field_value = self._apply_collection_mutators(field_value, field_mutators, field_name, record)
|
|
254
|
+
|
|
255
|
+
# For non-list values, convert to single-element list
|
|
256
|
+
if not isinstance(field_value, (list, tuple, set)):
|
|
257
|
+
field_value = [field_value]
|
|
258
|
+
|
|
259
|
+
# Evaluate the collection operation
|
|
260
|
+
if operator == "any":
|
|
261
|
+
# ANY: at least one element must match
|
|
262
|
+
for element in field_value:
|
|
263
|
+
if self.value_comparator.compare_values(element, comparison_operator, expected_value):
|
|
264
|
+
return True
|
|
265
|
+
return False
|
|
266
|
+
elif operator == "all":
|
|
267
|
+
# ALL: all elements must match
|
|
268
|
+
if not field_value: # Empty collection
|
|
269
|
+
return False
|
|
270
|
+
for element in field_value:
|
|
271
|
+
if not self.value_comparator.compare_values(element, comparison_operator, expected_value):
|
|
272
|
+
return False
|
|
273
|
+
return True
|
|
274
|
+
else:
|
|
275
|
+
raise ValueError(f"Unknown collection operator: {operator}")
|
|
276
|
+
|
|
277
|
+
def _get_field_value(self, record: Dict[str, Any], field_path: str) -> Any:
|
|
278
|
+
"""Get a field value from a record, supporting nested field access.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
record: The record dictionary
|
|
282
|
+
field_path: Dot-separated field path (e.g., "user.name")
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
The field value or _MISSING_FIELD if not found
|
|
286
|
+
"""
|
|
287
|
+
return self.field_accessor.get_field_value(record, field_path)
|
|
288
|
+
|
|
289
|
+
def _operand_has_missing_fields(self, node: Any, record: Dict[str, Any], field_mappings: Dict[str, str]) -> bool:
|
|
290
|
+
"""Check if an operand references missing fields.
|
|
291
|
+
|
|
292
|
+
This is used for NOT operations to handle missing field cases properly.
|
|
293
|
+
|
|
294
|
+
Args:
|
|
295
|
+
node: AST node to check
|
|
296
|
+
record: Record to check against
|
|
297
|
+
field_mappings: Field name mappings
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
True if the operand references any missing fields
|
|
301
|
+
"""
|
|
302
|
+
if isinstance(node, dict):
|
|
303
|
+
node_type = node.get("type")
|
|
304
|
+
|
|
305
|
+
if node_type == "comparison":
|
|
306
|
+
field_name = node["field"]
|
|
307
|
+
# Apply field mapping
|
|
308
|
+
actual_field = self.field_accessor.apply_field_mapping(field_name, field_mappings)
|
|
309
|
+
# Check if the field exists
|
|
310
|
+
field_value = self._get_field_value(record, actual_field)
|
|
311
|
+
return field_value is self._MISSING_FIELD
|
|
312
|
+
elif node_type == "logical_op":
|
|
313
|
+
# For logical operations, check both sides
|
|
314
|
+
left_missing = self._operand_has_missing_fields(node["left"], record, field_mappings)
|
|
315
|
+
right_missing = self._operand_has_missing_fields(node["right"], record, field_mappings)
|
|
316
|
+
return left_missing or right_missing
|
|
317
|
+
elif node_type == "unary_op":
|
|
318
|
+
# Don't recurse through NOT operators - they handle missing fields themselves
|
|
319
|
+
return False
|
|
320
|
+
elif node_type == "collection_op":
|
|
321
|
+
field_name = node["field"]
|
|
322
|
+
# Apply field mapping
|
|
323
|
+
actual_field = self.field_accessor.apply_field_mapping(field_name, field_mappings)
|
|
324
|
+
# Check if the field exists
|
|
325
|
+
field_value = self._get_field_value(record, actual_field)
|
|
326
|
+
return field_value is self._MISSING_FIELD
|
|
327
|
+
|
|
328
|
+
return False
|
|
329
|
+
|
|
330
|
+
def _is_exists_operation(self, node: Any) -> bool:
|
|
331
|
+
"""Check if a node is an exists operation."""
|
|
332
|
+
if isinstance(node, dict) and node.get("type") == "comparison":
|
|
333
|
+
return node.get("operator") in ["exists", "not_exists"]
|
|
334
|
+
return False
|
|
335
|
+
|
|
336
|
+
def _is_null_operation(self, node: Any) -> bool:
|
|
337
|
+
"""Check if a node is checking for null (field IS NULL)."""
|
|
338
|
+
if isinstance(node, dict) and node.get("type") == "comparison":
|
|
339
|
+
if node.get("operator") == "is":
|
|
340
|
+
value = node.get("value")
|
|
341
|
+
return value is None or (isinstance(value, str) and value.lower() == "null")
|
|
342
|
+
return False
|
|
343
|
+
|
|
344
|
+
def _is_not_null_operation(self, node: Any) -> bool:
|
|
345
|
+
"""Check if a node is checking for not null (field IS NOT NULL)."""
|
|
346
|
+
if isinstance(node, dict) and node.get("type") == "comparison":
|
|
347
|
+
if node.get("operator") == "is_not":
|
|
348
|
+
value = node.get("value")
|
|
349
|
+
return value is None or (isinstance(value, str) and value.lower() == "null")
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
def _apply_collection_mutators(
|
|
353
|
+
self, field_value: Any, mutators: List[Dict[str, Any]], field_name: str, record: Dict[str, Any]
|
|
354
|
+
) -> Any:
|
|
355
|
+
"""Apply mutators that work on collections.
|
|
356
|
+
|
|
357
|
+
Some mutators like split() can convert single values to arrays.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
field_value: Original field value
|
|
361
|
+
mutators: List of mutators to apply
|
|
362
|
+
field_name: The field name
|
|
363
|
+
record: The record being processed
|
|
364
|
+
|
|
365
|
+
Returns:
|
|
366
|
+
Mutated value
|
|
367
|
+
"""
|
|
368
|
+
# Apply mutators
|
|
369
|
+
result = apply_mutators(field_value, mutators, field_name, record)
|
|
370
|
+
|
|
371
|
+
# Check if any mutator converted to array
|
|
372
|
+
for mutator in mutators:
|
|
373
|
+
if mutator.get("name") == "split":
|
|
374
|
+
# Split always returns a list
|
|
375
|
+
if not isinstance(result, (list, tuple)):
|
|
376
|
+
result = [result]
|
|
377
|
+
break
|
|
378
|
+
|
|
379
|
+
return result
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Evaluator Components
|
|
2
|
+
|
|
3
|
+
This package contains the modular components for in-memory query evaluation.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
The evaluator components package provides the building blocks for evaluating TQL queries against Python dictionaries (records).
|
|
8
|
+
|
|
9
|
+
### Components
|
|
10
|
+
|
|
11
|
+
#### `field_access.py` - Field Access Utilities
|
|
12
|
+
Handles accessing values from nested dictionary structures:
|
|
13
|
+
- Dot-notation field paths (e.g., `user.profile.name`)
|
|
14
|
+
- Array indexing support (e.g., `items.0.price`)
|
|
15
|
+
- Field mapping resolution
|
|
16
|
+
- Type hint application
|
|
17
|
+
- Missing field handling
|
|
18
|
+
|
|
19
|
+
**Key Classes:**
|
|
20
|
+
- `FieldAccessor` - Field value extraction
|
|
21
|
+
|
|
22
|
+
**Key Methods:**
|
|
23
|
+
- `get_field_value()` - Extract value from nested path
|
|
24
|
+
- `apply_field_mapping()` - Resolve field name mappings
|
|
25
|
+
- `apply_type_hint()` - Convert values based on type hints
|
|
26
|
+
|
|
27
|
+
#### `value_comparison.py` - Value Comparison Operations
|
|
28
|
+
Implements all TQL comparison operators:
|
|
29
|
+
- Equality and inequality operators
|
|
30
|
+
- Range comparisons (>, <, >=, <=, between)
|
|
31
|
+
- String operations (contains, startswith, endswith)
|
|
32
|
+
- Pattern matching (regexp)
|
|
33
|
+
- List operations (in, not_in)
|
|
34
|
+
- Array operators (any, all, not_any, not_all)
|
|
35
|
+
- Network operations (cidr)
|
|
36
|
+
- Null handling (is, is_not)
|
|
37
|
+
|
|
38
|
+
**Key Classes:**
|
|
39
|
+
- `ValueComparator` - Comparison operations
|
|
40
|
+
|
|
41
|
+
**Key Methods:**
|
|
42
|
+
- `compare_values()` - Main comparison entry point
|
|
43
|
+
- `_convert_numeric()` - Smart type conversion
|
|
44
|
+
- `_check_cidr()` - CIDR range matching
|
|
45
|
+
|
|
46
|
+
#### `special_expressions.py` - Special Expression Evaluators
|
|
47
|
+
Handles geo() and nslookup() expressions:
|
|
48
|
+
- GeoIP lookups with enrichment
|
|
49
|
+
- DNS lookups and resolution
|
|
50
|
+
- Conditional evaluation on enriched data
|
|
51
|
+
- Caching of enrichment results
|
|
52
|
+
|
|
53
|
+
**Key Classes:**
|
|
54
|
+
- `SpecialExpressionEvaluator` - Special expression handler
|
|
55
|
+
|
|
56
|
+
**Key Methods:**
|
|
57
|
+
- `evaluate_geo_expr()` - Evaluate geo() expressions
|
|
58
|
+
- `evaluate_nslookup_expr()` - Evaluate nslookup() expressions
|
|
59
|
+
|
|
60
|
+
## Value Comparison Behavior
|
|
61
|
+
|
|
62
|
+
### Missing Fields
|
|
63
|
+
- Most operators return `False` for missing fields
|
|
64
|
+
- `not_exists` returns `True` for missing fields
|
|
65
|
+
- Negated string operators return `True` for missing fields
|
|
66
|
+
|
|
67
|
+
### Null Values
|
|
68
|
+
- `exists` returns `True` (field exists even if null)
|
|
69
|
+
- `is null` returns `True`
|
|
70
|
+
- Other operators return `False`
|
|
71
|
+
|
|
72
|
+
### Type Conversion
|
|
73
|
+
- Numeric strings are converted for comparison
|
|
74
|
+
- Boolean strings ("true"/"false") are converted
|
|
75
|
+
- CIDR operations validate IP addresses
|
|
76
|
+
|
|
77
|
+
### Array Handling
|
|
78
|
+
- `contains` checks if value is in array
|
|
79
|
+
- `any` checks if any element matches
|
|
80
|
+
- `all` checks if all elements match
|
|
81
|
+
- Single values are treated as one-element arrays for collection operators
|
|
82
|
+
|
|
83
|
+
## Architecture
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
TQLEvaluator (main class)
|
|
87
|
+
├── FieldAccessor (field extraction)
|
|
88
|
+
├── ValueComparator (comparisons)
|
|
89
|
+
└── SpecialExpressionEvaluator (geo/nslookup)
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Special Features
|
|
93
|
+
|
|
94
|
+
### Sentinel Value
|
|
95
|
+
The evaluator uses a sentinel value `_MISSING_FIELD` to distinguish between:
|
|
96
|
+
- Fields that don't exist in the record
|
|
97
|
+
- Fields that exist but have `None` value
|
|
98
|
+
|
|
99
|
+
This distinction is important for operators like `exists` and `is null`.
|
|
100
|
+
|
|
101
|
+
### Type Hints
|
|
102
|
+
Support for explicit type conversion:
|
|
103
|
+
```
|
|
104
|
+
ip_field:ip cidr "10.0.0.0/8"
|
|
105
|
+
count:integer > 100
|
|
106
|
+
active:boolean = true
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Mutator Support
|
|
110
|
+
Field and value mutators are applied during evaluation:
|
|
111
|
+
- Field mutators transform the field value before comparison
|
|
112
|
+
- Value mutators transform the expected value
|
|
113
|
+
- Special mutators (geo, nslookup) enrich data
|
|
114
|
+
|
|
115
|
+
## Usage
|
|
116
|
+
|
|
117
|
+
Used internally by `TQLEvaluator`:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from tql import TQL
|
|
121
|
+
|
|
122
|
+
tql = TQL()
|
|
123
|
+
data = [
|
|
124
|
+
{"name": "Alice", "age": 30, "city": "NYC"},
|
|
125
|
+
{"name": "Bob", "age": 25, "city": "LA"}
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
# Evaluation happens internally
|
|
129
|
+
results = tql.query(data, "age > 27 AND city = 'NYC'")
|
|
130
|
+
# Returns: [{"name": "Alice", "age": 30, "city": "NYC"}]
|
|
131
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""Evaluator components for TQL.
|
|
2
|
+
|
|
3
|
+
This package contains modular components for TQL evaluation:
|
|
4
|
+
- field_access: Field value extraction and type handling
|
|
5
|
+
- value_comparison: Value comparison operations and operator implementations
|
|
6
|
+
- special_expressions: Geo and NSLookup expression evaluators
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .field_access import FieldAccessor
|
|
10
|
+
from .special_expressions import SpecialExpressionEvaluator
|
|
11
|
+
from .value_comparison import ValueComparator
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"FieldAccessor",
|
|
15
|
+
"ValueComparator",
|
|
16
|
+
"SpecialExpressionEvaluator",
|
|
17
|
+
]
|