From ee9593186896e4205a1a9d2a33090044abb03d43 Mon Sep 17 00:00:00 2001 From: Ihor Panasiuk Date: Wed, 15 Jan 2025 13:28:01 +0200 Subject: [PATCH] feat: enhance PostgreSQL provider with json_extract and coalesce methods; update method call handling for improved SQL generation --- .../api/core/cel_to_sql/sql_providers/base.py | 68 +++++++++++++++++-- .../get_cel_to_sql_provider_for_dialect.py | 3 + .../cel_to_sql/sql_providers/postgresql.py | 42 +++--------- 3 files changed, 75 insertions(+), 38 deletions(-) diff --git a/keep/api/core/cel_to_sql/sql_providers/base.py b/keep/api/core/cel_to_sql/sql_providers/base.py index e24166a0b..ce683ce28 100644 --- a/keep/api/core/cel_to_sql/sql_providers/base.py +++ b/keep/api/core/cel_to_sql/sql_providers/base.py @@ -1,4 +1,4 @@ -from typing import Any, List +from typing import List from keep.api.core.cel_to_sql.ast_nodes import ( ConstantNode, MemberAccessNode, @@ -23,6 +23,65 @@ def __init__(self, where: str, select_fields: str = None, select_json: str = Non self.select_json = select_json class BaseCelToSqlProvider: + """ + Base class for converting CEL (Common Expression Language) expressions to SQL strings. + Methods: + convert_to_sql_str(cel: str) -> BuiltQueryMetadata: + Converts a CEL expression to an SQL string. + json_extract(column: str, path: str) -> str: + Abstract method to extract JSON data from a column. Must be implemented in the child class. + coalesce(args: List[str]) -> str: + Abstract method to perform COALESCE operation. Must be implemented in the child class. + _visit_parentheses(node: str) -> str: + Wraps a given SQL string in parentheses. + _visit_logical_node(logical_node: LogicalNode) -> str: + Visits a logical node and converts it to an SQL string. + _visit_logical_and(left: str, right: str) -> str: + Converts a logical AND operation to an SQL string. + _visit_logical_or(left: str, right: str) -> str: + Converts a logical OR operation to an SQL string. + _visit_comparison_node(comparison_node: ComparisonNode) -> str: + Visits a comparison node and converts it to an SQL string. + _cast_property(exp: str, to_type: type) -> str: + Casts a property to a specified type in SQL. + _visit_equal(first_operand: str, second_operand: str) -> str: + Converts an equality comparison to an SQL string. + _visit_not_equal(first_operand: str, second_operand: str) -> str: + Converts a not-equal comparison to an SQL string. + _visit_greater_than(first_operand: str, second_operand: str) -> str: + Converts a greater-than comparison to an SQL string. + _visit_greater_than_or_equal(first_operand: str, second_operand: str) -> str: + Converts a greater-than-or-equal comparison to an SQL string. + _visit_less_than(first_operand: str, second_operand: str) -> str: + Converts a less-than comparison to an SQL string. + _visit_less_than_or_equal(first_operand: str, second_operand: str) -> str: + Converts a less-than-or-equal comparison to an SQL string. + _visit_in(first_operand: Node, array: list[ConstantNode]) -> str: + Converts an IN operation to an SQL string. + _visit_constant_node(value: str) -> str: + Converts a constant value to an SQL string. + _visit_multiple_fields_node(multiple_fields_node: MultipleFieldsNode) -> str: + Visits a multiple fields node and converts it to an SQL string. + _visit_member_access_node(member_access_node: MemberAccessNode) -> str: + Visits a member access node and converts it to an SQL string. + _visit_property_access_node(property_access_node: PropertyAccessNode) -> str: + Visits a property access node and converts it to an SQL string. + _visit_index_property(property_path: str) -> str: + Abstract method to handle index properties. Must be implemented in the child class. + _visit_method_calling(property_path: str, method_name: str, method_args: List[str]) -> str: + Visits a method calling node and converts it to an SQL string. + _visit_contains_method_calling(property_path: str, method_args: List[str]) -> str: + Abstract method to handle 'contains' method calls. Must be implemented in the child class. + _visit_startwith_method_calling(property_path: str, method_args: List[str]) -> str: + Abstract method to handle 'startsWith' method calls. Must be implemented in the child class. + _visit_endswith_method_calling(property_path: str, method_args: List[str]) -> str: + Abstract method to handle 'endsWith' method calls. Must be implemented in the child class. + _visit_unary_node(unary_node: UnaryNode) -> str: + Visits a unary node and converts it to an SQL string. + _visit_unary_not(operand: str) -> str: + Converts a NOT operation to an SQL string. + """ + __null_replacement = "'__@NULL@__'" def __init__(self, properties_metadata: PropertiesMetadata): @@ -149,7 +208,6 @@ def _cast_property(self, exp: str, to_type: type) -> str: return exp if to_type == bool: return exp - # return "TRUE" if exp == "true" else "FALSE" raise NotImplementedError(f"{to_type.__name__} type casting is not supported yet") @@ -247,17 +305,17 @@ def _visit_method_calling( def _visit_contains_method_calling( self, property_path: str, method_args: List[str] ) -> str: - raise NotImplementedError("'contains' method call is not supported") + raise NotImplementedError("'contains' method must be implemented in the child class") def _visit_startwith_method_calling( self, property_path: str, method_args: List[str] ) -> str: - raise NotImplementedError("'startswith' method call is not supported") + raise NotImplementedError("'startswith' method call must be implemented in the child class") def _visit_endswith_method_calling( self, property_path: str, method_args: List[str] ) -> str: - raise NotImplementedError("'endswith' method call is not supported") + raise NotImplementedError("'endswith' method call must be implemented in the child class") # endregion diff --git a/keep/api/core/cel_to_sql/sql_providers/get_cel_to_sql_provider_for_dialect.py b/keep/api/core/cel_to_sql/sql_providers/get_cel_to_sql_provider_for_dialect.py index 27acf6cc0..dbb557855 100644 --- a/keep/api/core/cel_to_sql/sql_providers/get_cel_to_sql_provider_for_dialect.py +++ b/keep/api/core/cel_to_sql/sql_providers/get_cel_to_sql_provider_for_dialect.py @@ -1,4 +1,5 @@ from keep.api.core.cel_to_sql.sql_providers.base import BaseCelToSqlProvider +from keep.api.core.cel_to_sql.sql_providers.postgresql import CelToPostgreSqlProvider from keep.api.core.cel_to_sql.sql_providers.sqlite import CelToSqliteProvider from keep.api.core.cel_to_sql.sql_providers.mysql import CelToMySqlProvider @@ -7,6 +8,8 @@ def get_cel_to_sql_provider_for_dialect(dialect: str) -> type[BaseCelToSqlProvid return CelToSqliteProvider elif dialect == "mysql": return CelToMySqlProvider + elif dialect == "postgresql": + return CelToPostgreSqlProvider else: raise ValueError(f"Unsupported dialect: {dialect}") \ No newline at end of file diff --git a/keep/api/core/cel_to_sql/sql_providers/postgresql.py b/keep/api/core/cel_to_sql/sql_providers/postgresql.py index edecb6ccb..ae8501809 100644 --- a/keep/api/core/cel_to_sql/sql_providers/postgresql.py +++ b/keep/api/core/cel_to_sql/sql_providers/postgresql.py @@ -2,42 +2,18 @@ from keep.api.core.cel_to_sql.sql_providers.base import BaseCelToSqlProvider class CelToPostgreSqlProvider(BaseCelToSqlProvider): - def _visit_property(self, property_path: str): - if property_path.startswith('event'): - property_access = property_path.replace('event.', '') - return f"event ->> '{property_access}'" - - return super()._visit_property(property_path) + def json_extract(self, column: str, path: str) -> str: + ' -> '.join([column] + path.split('.')) + return ' -> '.join([column] + path.split('.')) # example: 'json_column' -> 'key1' -> 'key2' + + def coalesce(self, args): + return f"COALESCE({', '.join(args)})" def _visit_contains_method_calling(self, property_path: str, method_args: List[str]) -> str: - if property_path and property_path.startswith('event'): - prop = property_path.replace('event.', '') - return f"event ->> '{prop}' LIKE '%{method_args[0]}%'" - - if property_path and property_path.startswith('enrichments'): - prop = property_path.replace('enrichments.', '') - return f"enrichments ->> '{prop}' LIKE '%{method_args[0]}%'" - - return f"{property_path} LIKE '%{method_args[0]}%'" + return f"{property_path} LIKE \"%{method_args[0]}%\"" def _visit_starts_with_method_calling(self, property_path: str, method_args: List[str]) -> str: - if property_path and property_path.startswith('event'): - prop = property_path.replace('event.', '') - return f"event ->> '{prop}' LIKE '{method_args[0]}%'" - - if property_path and property_path.startswith('enrichments'): - prop = property_path.replace('enrichments.', '') - return f"enrichments ->> '{prop}' LIKE '{method_args[0]}%'" - - return f"{property_path} LIKE '{method_args[0]}%'" + return f"{property_path} LIKE \"{method_args[0]}%\"" def _visit_ends_with_method_calling(self, property_path: str, method_args: List[str]) -> str: - if property_path and property_path.startswith('event'): - prop = property_path.replace('event.', '') - return f"event ->> '{prop}' LIKE '%{method_args[0]}'" - - if property_path and property_path.startswith('enrichments'): - prop = property_path.replace('enrichments.', '') - return f"enrichments ->> '{prop}' LIKE '%{method_args[0]}'" - - return f"{property_path} LIKE '%{method_args[0]}'" \ No newline at end of file + return f"{property_path} LIKE \"%{method_args[0]}\"" \ No newline at end of file