-
-
Notifications
You must be signed in to change notification settings - Fork 63
feat(eap): translate OP_REGEXP to ClickHouse match() #8437
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
4ffa24d
e745cbf
e6e0935
fdfc2bc
4217985
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -567,7 +567,9 @@ def _attribute_value_to_expression(v: AttributeValue) -> Expression: | |
| AnyAttributeFilter.OP_NOT_IN, | ||
| } | ||
|
|
||
| _POSITIVE_OP_FOR_NEGATIVE: dict[int, int] = { | ||
| _POSITIVE_OP_FOR_NEGATIVE: dict[ | ||
| AnyAttributeFilter.Op.ValueType, AnyAttributeFilter.Op.ValueType | ||
| ] = { | ||
| AnyAttributeFilter.OP_NOT_EQUALS: AnyAttributeFilter.OP_EQUALS, | ||
| AnyAttributeFilter.OP_NOT_LIKE: AnyAttributeFilter.OP_LIKE, | ||
| AnyAttributeFilter.OP_NOT_IN: AnyAttributeFilter.OP_IN, | ||
|
|
@@ -607,15 +609,15 @@ def _array_value_length(v: AttributeValue, value_type: str) -> int: | |
| def _validate_comparison_filter_type_array( | ||
| op: ComparisonFilter.Op.ValueType, v: AttributeValue, key: AttributeKey | ||
| ) -> None: | ||
| if op in (ComparisonFilter.OP_LIKE, ComparisonFilter.OP_NOT_LIKE): | ||
| if op in (ComparisonFilter.OP_LIKE, ComparisonFilter.OP_NOT_LIKE, ComparisonFilter.OP_REGEXP): | ||
| if v.WhichOneof("value") != "val_str": | ||
| raise BadSnubaRPCRequestException( | ||
| "LIKE/NOT_LIKE on array keys requires a string pattern" | ||
| ) | ||
| # LIKE only matches string elements, so it makes sense only for string arrays. | ||
| label = "REGEXP" if op == ComparisonFilter.OP_REGEXP else "LIKE/NOT_LIKE" | ||
| raise BadSnubaRPCRequestException(f"{label} on array keys requires a string pattern") | ||
| # LIKE/REGEXP only match string elements, so they make sense only for string arrays. | ||
| if key.type not in (AttributeKey.Type.TYPE_ARRAY, AttributeKey.Type.TYPE_ARRAY_STRING): | ||
| label = "REGEXP" if op == ComparisonFilter.OP_REGEXP else "LIKE/NOT_LIKE" | ||
| raise BadSnubaRPCRequestException( | ||
| "LIKE/NOT_LIKE on array keys is only supported on string arrays " | ||
| f"{label} on array keys is only supported on string arrays " | ||
| f"(TYPE_ARRAY_STRING), got {AttributeKey.Type.Name(key.type)}" | ||
| ) | ||
| return | ||
|
|
@@ -854,6 +856,57 @@ def _typed_array_like_expression( | |
| ) | ||
|
|
||
|
|
||
| def _regexp_match(value: Expression, pattern: Expression, ignore_case: bool) -> FunctionCall: | ||
| # Needs ClickHouse > 26.8 for matchCaseInsensitive; lower() is a stand-in until then. | ||
| if ignore_case: | ||
| return f.match(f.lower(value), f.lower(pattern)) | ||
| return f.match(value, pattern) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ignore-case regex mangles escape sequencesMedium Severity
Reviewed by Cursor Bugbot for commit 96b1da7. Configure here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can fix it when we upgrade our clickhouse to 26.8+ imo. This is fine as a stopgap solution. |
||
|
|
||
|
|
||
| def _is_valid_regexp_pattern(v: AttributeValue) -> None: | ||
| if v.WhichOneof("value") != "val_str" or v.val_str == "": | ||
| raise BadSnubaRPCRequestException("REGEXP pattern must be a non-empty string") | ||
|
pbhandari marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def _any_attribute_op_expression( | ||
|
pbhandari marked this conversation as resolved.
|
||
| *, | ||
| filter_: AnyAttributeFilter, | ||
| op: AnyAttributeFilter.Op.ValueType, | ||
| element: Argument, | ||
| value_expr: Expression, | ||
| membership_as_has: bool, | ||
| ) -> Expression: | ||
| match op: | ||
| case AnyAttributeFilter.OP_EQUALS: | ||
| if filter_.ignore_case: | ||
| return f.equals(f.lower(element), f.lower(value_expr)) | ||
| return f.equals(element, value_expr) | ||
| case AnyAttributeFilter.OP_LIKE: | ||
| if filter_.ignore_case: | ||
| return f.ilike(element, value_expr) | ||
| return f.like(element, value_expr) | ||
| case AnyAttributeFilter.OP_REGEXP: | ||
| return _regexp_match(element, value_expr, filter_.ignore_case) | ||
| case AnyAttributeFilter.OP_IN: | ||
| if filter_.ignore_case: | ||
| if filter_.value.WhichOneof("value") == "val_str_array": | ||
| lowered = [literal(s.lower()) for s in filter_.value.val_str_array.values] | ||
| else: | ||
| lowered = [ | ||
| literal(elem.val_str.lower()) for elem in filter_.value.val_array.values | ||
| ] | ||
| return _in_or_has( | ||
| f.lower(element), | ||
| literals_array(None, lowered), | ||
| as_has=membership_as_has, | ||
| ) | ||
| return _in_or_has(element, value_expr, as_has=membership_as_has) | ||
| case _: | ||
| raise BadSnubaRPCRequestException( | ||
| f"Unsupported any_attribute_filter op: {AnyAttributeFilter.Op.Name(filter_.op)}" | ||
| ) | ||
|
|
||
|
|
||
| def _any_attribute_filter_to_expression( | ||
|
Comment on lines
+905
to
910
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The error message for unsupported operators in Suggested FixUpdate the exception logging in the default case of the Prompt for AI Agent |
||
| filt: AnyAttributeFilter, | ||
| *, | ||
|
|
@@ -918,11 +971,16 @@ def _any_attribute_filter_to_expression( | |
| ) | ||
| col_name = _VALUE_TYPE_TO_COLUMN[value_type] | ||
|
|
||
| # LIKE/NOT_LIKE only makes sense on string columns | ||
| if effective_op == AnyAttributeFilter.OP_LIKE and col_name not in _STRING_COLUMNS: | ||
| raise BadSnubaRPCRequestException( | ||
| "LIKE/NOT_LIKE operations are only supported on string values" | ||
| ) | ||
| # LIKE/NOT_LIKE/REGEXP only makes sense on string columns | ||
| if ( | ||
| effective_op in (AnyAttributeFilter.OP_LIKE, AnyAttributeFilter.OP_REGEXP) | ||
| and col_name not in _STRING_COLUMNS | ||
| ): | ||
| label = "REGEXP" if effective_op == AnyAttributeFilter.OP_REGEXP else "LIKE/NOT_LIKE" | ||
| raise BadSnubaRPCRequestException(f"{label} operations are only supported on string values") | ||
|
|
||
| if effective_op == AnyAttributeFilter.OP_REGEXP: | ||
| _is_valid_regexp_pattern(v) | ||
|
|
||
| # ignore_case uses lower() which only works on string columns | ||
| if filt.ignore_case and col_name not in _STRING_COLUMNS: | ||
|
|
@@ -932,35 +990,13 @@ def _any_attribute_filter_to_expression( | |
|
|
||
| # 3. Build the lambda comparison | ||
| x = Argument(None, "x") | ||
|
|
||
| if effective_op == AnyAttributeFilter.OP_EQUALS: | ||
| if filt.ignore_case: | ||
| comparison = f.equals(f.lower(x), f.lower(v_expression)) | ||
| else: | ||
| comparison = f.equals(x, v_expression) | ||
| elif effective_op == AnyAttributeFilter.OP_LIKE: | ||
| if filt.ignore_case: | ||
| comparison = f.ilike(x, v_expression) | ||
| else: | ||
| comparison = f.like(x, v_expression) | ||
| elif effective_op == AnyAttributeFilter.OP_IN: | ||
| if filt.ignore_case: | ||
| if value_type == "val_str_array": | ||
| lowered = [literal(s.lower()) for s in v.val_str_array.values] | ||
| else: | ||
| lowered = [literal(elem.val_str.lower()) for elem in v.val_array.values] | ||
| comparison = _in_or_has( | ||
| f.lower(x), | ||
| literals_array(None, lowered), | ||
| as_has=membership_as_has, | ||
| ) | ||
| else: | ||
| comparison = _in_or_has(x, v_expression, as_has=membership_as_has) | ||
| else: | ||
| raise BadSnubaRPCRequestException( | ||
| f"Unsupported any_attribute_filter op: {AnyAttributeFilter.Op.Name(filt.op)}" | ||
| ) | ||
|
|
||
| comparison = _any_attribute_op_expression( | ||
| filter_=filt, | ||
| op=effective_op, | ||
| element=x, | ||
| value_expr=v_expression, | ||
| membership_as_has=membership_as_has, | ||
| ) | ||
| lam = Lambda(None, ("x",), comparison) | ||
|
|
||
| # 4. Build the arrayExists expression for the single matching column. | ||
|
|
@@ -1261,6 +1297,26 @@ def trace_item_filters_to_expression( | |
| value, exists = _map_backed_operands(k) | ||
| return and_cond(exists, comparison_function(value, v_expression)) | ||
| return comparison_function(k_expression, v_expression) | ||
| if op == ComparisonFilter.OP_REGEXP: | ||
| _is_valid_regexp_pattern(v) | ||
| ignore_case = item_filter.comparison_filter.ignore_case | ||
| if k.type in ARRAY_TYPES: | ||
| return f.arrayExists( | ||
| Lambda( | ||
| None, | ||
| ("x",), | ||
| _regexp_match(Argument(None, "x"), v_expression, ignore_case), | ||
| ), | ||
| type_array_typed_column_native_array(k, "attributes_array_string"), | ||
| ) | ||
| if k.type != AttributeKey.Type.TYPE_STRING: | ||
| raise BadSnubaRPCRequestException( | ||
| "the REGEXP comparison is only supported on string and array keys" | ||
| ) | ||
| if _is_map_backed_key(k): | ||
| value, exists = _map_backed_operands(k) | ||
| return and_cond(exists, _regexp_match(value, v_expression, ignore_case)) | ||
| return _regexp_match(k_expression, v_expression, ignore_case) | ||
| if op == ComparisonFilter.OP_NOT_LIKE: | ||
| if k.type in ARRAY_TYPES: | ||
| return not_cond( | ||
|
|
||


Uh oh!
There was an error while loading. Please reload this page.