From c63854b163c6f76fd2d2f279ca1bb7bd7c68ac29 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Wed, 19 Nov 2025 17:04:34 +0100 Subject: [PATCH 01/50] [EQL] match automatically determines which comparator type to use, whether to flatter, and whether to filter by type. --- src/krrood/entity_query_language/entity.py | 148 +++++++++++++++++++-- src/krrood/entity_query_language/utils.py | 13 +- 2 files changed, 152 insertions(+), 9 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index 3ed8ed3..e55f187 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -1,13 +1,14 @@ from __future__ import annotations from dataclasses import dataclass, field -from functools import cached_property +from functools import cached_property, lru_cache from black.strings import Match from .hashed_data import T from .symbol_graph import SymbolGraph -from .utils import is_iterable +from .utils import is_iterable, is_iterable_type +from ..class_diagrams.wrapped_field import WrappedField """ User interface (grammar & vocabulary) for entity query language. @@ -45,6 +46,7 @@ Exists, Literal, ResultQuantifier, + Attribute, ) from .result_quantification_constraint import ResultQuantificationConstraint @@ -265,7 +267,9 @@ def not_(operand: SymbolicExpression): return operand.__invert__() -def contains(container: Union[Iterable, CanBehaveLikeAVariable[T]], item: Any): +def contains( + container: Union[Iterable, CanBehaveLikeAVariable[T]], item: Any +) -> Comparator: """ Check whether a container contains an item. @@ -367,6 +371,10 @@ class Match(Generic[T]): """ The conditions that define the match. """ + _resolved: bool = field(init=False, default=False) + """ + Whether the match has been resolved. + """ def _resolve(self, variable: Optional[CanBehaveLikeAVariable] = None): """ @@ -377,13 +385,137 @@ def _resolve(self, variable: Optional[CanBehaveLikeAVariable] = None): """ self.variable = variable if variable else self._create_variable() for k, v in self.kwargs.items(): - attr = getattr(self.variable, k) - if isinstance(v, Match): + attr: Attribute = getattr(self.variable, k) + attr_wrapped_field = attr._wrapped_field_ + if isinstance(v, Match) and not v._resolved: + attr = self._flatten_the_attribute_if_is_iterable_while_value_is_not( + attr, v, attr_wrapped_field + ) v._resolve(attr) - self.conditions.append(HasType(attr, v.type_)) + self._add_type_filter_if_needed(attr, v, attr_wrapped_field) self.conditions.extend(v.conditions) else: - self.conditions.append(attr == v) + if isinstance(v, Match): + v = v.variable + condition = self._get_either_a_containment_or_an_equal_condition( + attr, v, attr_wrapped_field + ) + self.conditions.append(condition) + self._resolved = True + + def _get_either_a_containment_or_an_equal_condition( + self, + attr: Attribute, + assigned_value: Any, + wrapped_field: Optional[WrappedField] = None, + ) -> Comparator: + """ + Find and return the appropriate condition for the attribute and its assigned value. This can be one of contains, + in_, or == depending on the type of the assigned value and the type of the attribute. + + :param attr: The attribute to check. + :param assigned_value: The value assigned to the attribute. + :param wrapped_field: The WrappedField representing the attribute. + :return: A comparator expression representing the condition. + """ + if self._attribute_is_iterable_while_the_value_is_not( + assigned_value, wrapped_field + ): + return contains(attr, assigned_value) + elif self._value_is_iterable_while_the_attribute_is_not( + assigned_value, wrapped_field + ): + return in_(attr, assigned_value) + else: + return attr == assigned_value + + def _attribute_is_iterable_while_the_value_is_not( + self, + assigned_value: Any, + wrapped_field: Optional[WrappedField] = None, + ) -> bool: + """ + Return True if the attribute is iterable while the assigned value is not an iterable. + + :param assigned_value: The value assigned to the attribute. + :param wrapped_field: The WrappedField representing the attribute. + """ + return ( + wrapped_field + and wrapped_field.is_iterable + and not self._is_iterable_value(assigned_value) + ) + + def _value_is_iterable_while_the_attribute_is_not( + self, assigned_value: Any, wrapped_field: Optional[WrappedField] = None + ) -> bool: + """ + Return True if the assigned value is iterable while the attribute is not an iterable. + + :param assigned_value: The value assigned to the attribute. + :param wrapped_field: The WrappedField representing the attribute. + """ + return ( + wrapped_field + and not wrapped_field.is_iterable + and self._is_iterable_value(assigned_value) + ) + + def _flatten_the_attribute_if_is_iterable_while_value_is_not( + self, + attr: Attribute, + attr_assigned_value: Any, + attr_wrapped_field: Optional[WrappedField] = None, + ) -> Union[Attribute, Flatten]: + """ + Apply a flatten operation to the attribute if it is an iterable while the assigned value is not an iterable. + + :param attr: The attribute to flatten. + :param attr_assigned_value: The value assigned to the attribute. + :param attr_wrapped_field: The WrappedField representing the attribute. + :return: The flattened attribute if it is an iterable, else the original attribute. + """ + if self._is_iterable_value(attr_assigned_value): + return attr + if attr_wrapped_field and attr_wrapped_field.is_iterable: + return flatten(attr) + return attr + + @staticmethod + def _is_iterable_value(value) -> bool: + """ + Whether the value is an iterable or a Match instance with an iterable type. + + :param value: The value to check. + :return: True if the value is an iterable or a Match instance with an iterable type, else False. + """ + if not isinstance(value, Match) and is_iterable(value): + return True + elif isinstance(value, Match) and is_iterable_type(value.type_): + return True + return False + + def _add_type_filter_if_needed( + self, + attr: Attribute, + attr_match: Match, + attr_wrapped_field: Optional[WrappedField] = None, + ): + """ + Adds a type filter to the match if needed. Basically when the type hint is not found or when it is + a superclass of the type provided in the match. + + :param attr: The attribute to filter. + :param attr_match:The Match instance of the attribute. + :param attr_wrapped_field: The WrappedField representing the attribute. + :return: + """ + attr_type = attr_wrapped_field.type_endpoint if attr_wrapped_field else None + if (not attr_type) or ( + (attr_match.type_ is not attr_type) + and issubclass(attr_match.type_, attr_type) + ): + self.conditions.append(HasType(attr, attr_match.type_)) def _create_variable(self) -> Variable[T]: """ @@ -420,7 +552,7 @@ def _create_variable(self) -> Variable[T]: return let(self.type_, self.domain) -def match(type_: Type[T]) -> Union[Type[T], Callable[..., Match[T]]]: +def match(type_: Type[T]) -> Union[Iterable[Type[T]], Callable[..., Match[T]]]: """ This returns a factory function that creates a Match instance that looks for the pattern provided by the type and the keyword arguments. diff --git a/src/krrood/entity_query_language/utils.py b/src/krrood/entity_query_language/utils.py index a5f72c3..e46072c 100644 --- a/src/krrood/entity_query_language/utils.py +++ b/src/krrood/entity_query_language/utils.py @@ -18,7 +18,7 @@ except ImportError: Source = None -from typing_extensions import Set, Any, List +from typing_extensions import Set, Any, List, Type class IDGenerator: @@ -142,6 +142,17 @@ def is_iterable(obj: Any) -> bool: ) +def is_iterable_type(obj: Type) -> bool: + """ + Check if an object type is iterable. + + :param obj: The object to check. + """ + return hasattr(obj, "__iter__") and not issubclass( + obj, (str, type, bytes, bytearray) + ) + + def make_tuple(value: Any) -> Any: """ Make a tuple from a value. From dc6103c7bab9b9bedb2dfefbfaf09584efcba340 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Thu, 20 Nov 2025 18:30:45 +0100 Subject: [PATCH 02/50] [EQL] set_of can use match instances as its selectables. --- src/krrood/entity_query_language/entity.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index e55f187..b2ea23a 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -2,8 +2,7 @@ from dataclasses import dataclass, field from functools import cached_property, lru_cache - -from black.strings import Match +from copy import copy from .hashed_data import T from .symbol_graph import SymbolGraph @@ -62,7 +61,7 @@ """ The possible types for conditions. """ -EntityType = Union[SetOf[T], Entity[T], T, Iterable[T], Type[T], Match[T]] +EntityType = Union[SetOf[T], Entity[T], T, Iterable[T], Type[T]] """ The possible types for entities. """ @@ -170,6 +169,11 @@ def _extract_variables_and_expression( """ expression_list = list(properties) selected_variables = list(selected_variables) + for i, sv in enumerate(copy(selected_variables)): + if isinstance(sv, Match): + if not sv._resolved: + expression_list.append(sv.expression._child_) + selected_variables[i] = sv.variable expression = None if len(expression_list) > 0: expression = ( From 9df3ea4b16e9c3a979cb6149ea0061879a40219b Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Thu, 20 Nov 2025 22:13:10 +0100 Subject: [PATCH 03/50] [EQLMatch] match does not require kwargs. --- src/krrood/entity_query_language/entity.py | 27 +++++++++++----------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index b2ea23a..cf93cb2 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -363,7 +363,7 @@ class Match(Generic[T]): """ The type of the variable. """ - kwargs: Dict[str, Any] + kwargs: Dict[str, Any] = field(init=False, default_factory=dict) """ The keyword arguments to match against. """ @@ -380,6 +380,15 @@ class Match(Generic[T]): Whether the match has been resolved. """ + def __call__(self, **kwargs) -> Self: + """ + Update the match with new keyword arguments to constrain the type we are matching with. + + :param kwargs: The keyword arguments to match against. + """ + self.kwargs = kwargs + return self + def _resolve(self, variable: Optional[CanBehaveLikeAVariable] = None): """ Resolve the match by creating the variable and conditions expressions. @@ -562,13 +571,9 @@ def match(type_: Type[T]) -> Union[Iterable[Type[T]], Callable[..., Match[T]]]: keyword arguments. :param type_: The type of the variable (i.e., The class you want to instantiate). - :return: The factory function for creating the match query. + :return: The Match instance. """ - - def match_factory(**kwargs) -> Match[T]: - return Match(type_, kwargs) - - return match_factory + return Match(type_) def entity_matching( @@ -580,10 +585,6 @@ def entity_matching( :param type_: The type of the variable (i.e., The class you want to instantiate). :param domain: The domain used for the variable created by the match. - :return: The factory function for creating the match query. + :return: The MatchEntity instance. """ - - def match_factory(**kwargs) -> MatchEntity[T]: - return MatchEntity(type_, kwargs, domain) - - return match_factory + return MatchEntity(type_, domain) From 191df0a26b7e8ddeb86fbccb51233bfb0a144164 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Thu, 20 Nov 2025 23:01:19 +0100 Subject: [PATCH 04/50] [EQLMatch] selectables. --- src/krrood/entity_query_language/entity.py | 44 ++++++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index cf93cb2..b7a4e47 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -25,6 +25,7 @@ Tuple, List, Callable, + Self, ) from .symbolic import ( @@ -174,6 +175,7 @@ def _extract_variables_and_expression( if not sv._resolved: expression_list.append(sv.expression._child_) selected_variables[i] = sv.variable + selected_variables.extend(sv.selected_attributes) expression = None if len(expression_list) > 0: expression = ( @@ -353,6 +355,12 @@ def inference( ) +SELECTED = object() +""" +A special object that can be used to indicate that the variable should be selected in the result. +""" + + @dataclass class Match(Generic[T]): """ @@ -379,6 +387,14 @@ class Match(Generic[T]): """ Whether the match has been resolved. """ + selected_attributes: List[Attribute] = field(init=False, default_factory=list) + """ + A list of selected attributes. + """ + parent: Optional[Match] = field(init=False, default=None) + """ + The parent match if this is a nested match. + """ def __call__(self, **kwargs) -> Self: """ @@ -389,14 +405,21 @@ def __call__(self, **kwargs) -> Self: self.kwargs = kwargs return self - def _resolve(self, variable: Optional[CanBehaveLikeAVariable] = None): + def _resolve( + self, + variable: Optional[CanBehaveLikeAVariable] = None, + parent: Optional[Match] = None, + ): """ Resolve the match by creating the variable and conditions expressions. - :param variable: An optional pre-existing variable to use for the match; if not provided, a new variable will be created. + :param variable: An optional pre-existing variable to use for the match; if not provided, a new variable will + be created. + :param parent: The parent match if this is a nested match. :return: """ self.variable = variable if variable else self._create_variable() + self.parent = parent for k, v in self.kwargs.items(): attr: Attribute = getattr(self.variable, k) attr_wrapped_field = attr._wrapped_field_ @@ -404,9 +427,11 @@ def _resolve(self, variable: Optional[CanBehaveLikeAVariable] = None): attr = self._flatten_the_attribute_if_is_iterable_while_value_is_not( attr, v, attr_wrapped_field ) - v._resolve(attr) + v._resolve(attr, self) self._add_type_filter_if_needed(attr, v, attr_wrapped_field) self.conditions.extend(v.conditions) + elif v is SELECTED: + self._update_selected_attributes(attr) else: if isinstance(v, Match): v = v.variable @@ -416,6 +441,15 @@ def _resolve(self, variable: Optional[CanBehaveLikeAVariable] = None): self.conditions.append(condition) self._resolved = True + def _update_selected_attributes(self, attr: Attribute): + """ + Update the selected attributes of the match by adding the given attribute to the root Match selected attributes. + """ + if self.parent: + self.parent._update_selected_attributes(attr) + else: + self.selected_attributes.append(attr) + def _get_either_a_containment_or_an_equal_condition( self, attr: Attribute, @@ -565,7 +599,9 @@ def _create_variable(self) -> Variable[T]: return let(self.type_, self.domain) -def match(type_: Type[T]) -> Union[Iterable[Type[T]], Callable[..., Match[T]]]: +def match( + type_: Type[T], +) -> Union[Iterable[Type[T]], Callable[..., Match[T]]]: """ This returns a factory function that creates a Match instance that looks for the pattern provided by the type and the keyword arguments. From 4de351f07ff2417f1976f6d0b4519138475b76f2 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Thu, 20 Nov 2025 23:40:20 +0100 Subject: [PATCH 05/50] [EQLMatch] match can return a set_of. --- src/krrood/entity_query_language/entity.py | 46 ++++++++++++++-------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index b7a4e47..a020351 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -47,6 +47,7 @@ Literal, ResultQuantifier, Attribute, + QueryObjectDescriptor, ) from .result_quantification_constraint import ResultQuantificationConstraint @@ -170,12 +171,6 @@ def _extract_variables_and_expression( """ expression_list = list(properties) selected_variables = list(selected_variables) - for i, sv in enumerate(copy(selected_variables)): - if isinstance(sv, Match): - if not sv._resolved: - expression_list.append(sv.expression._child_) - selected_variables[i] = sv.variable - selected_variables.extend(sv.selected_attributes) expression = None if len(expression_list) > 0: expression = ( @@ -387,7 +382,7 @@ class Match(Generic[T]): """ Whether the match has been resolved. """ - selected_attributes: List[Attribute] = field(init=False, default_factory=list) + selected_variables: List[Attribute] = field(init=False, default_factory=list) """ A list of selected attributes. """ @@ -395,6 +390,10 @@ class Match(Generic[T]): """ The parent match if this is a nested match. """ + is_selected: bool = field(default=False) + """ + Whether the variable should be selected in the result. + """ def __call__(self, **kwargs) -> Self: """ @@ -420,6 +419,8 @@ def _resolve( """ self.variable = variable if variable else self._create_variable() self.parent = parent + if self.is_selected or not parent: + self._update_selected_variables(self.variable) for k, v in self.kwargs.items(): attr: Attribute = getattr(self.variable, k) attr_wrapped_field = attr._wrapped_field_ @@ -431,7 +432,7 @@ def _resolve( self._add_type_filter_if_needed(attr, v, attr_wrapped_field) self.conditions.extend(v.conditions) elif v is SELECTED: - self._update_selected_attributes(attr) + self._update_selected_variables(attr) else: if isinstance(v, Match): v = v.variable @@ -441,14 +442,14 @@ def _resolve( self.conditions.append(condition) self._resolved = True - def _update_selected_attributes(self, attr: Attribute): + def _update_selected_variables(self, variable: CanBehaveLikeAVariable): """ - Update the selected attributes of the match by adding the given attribute to the root Match selected attributes. + Update the selected variables of the match by adding the given variable to the root Match selected variables. """ if self.parent: - self.parent._update_selected_attributes(attr) + self.parent._update_selected_variables(variable) else: - self.selected_attributes.append(attr) + self.selected_variables.append(variable) def _get_either_a_containment_or_an_equal_condition( self, @@ -571,12 +572,15 @@ def _create_variable(self) -> Variable[T]: return let(self.type_, None) @cached_property - def expression(self) -> Entity[T]: + def expression(self) -> QueryObjectDescriptor[T]: """ Return the entity expression corresponding to the match query. """ self._resolve() - return entity(self.variable, *self.conditions) + if len(self.selected_variables) > 1: + return set_of(self.selected_variables, *self.conditions) + else: + return entity(self.selected_variables[0], *self.conditions) @dataclass @@ -599,9 +603,10 @@ def _create_variable(self) -> Variable[T]: return let(self.type_, self.domain) -def match( - type_: Type[T], -) -> Union[Iterable[Type[T]], Callable[..., Match[T]]]: +MatchType = Union[Iterable[Type[T]], Callable[..., Match[T]]] + + +def match(type_: Type[T]) -> MatchType: """ This returns a factory function that creates a Match instance that looks for the pattern provided by the type and the keyword arguments. @@ -612,6 +617,13 @@ def match( return Match(type_) +def select(type_: Type[T]) -> MatchType: + """ + Equivalent to match(type_) and selecting the variable to be included in the result. + """ + return Match(type_, is_selected=True) + + def entity_matching( type_: Type[T], domain: DomainType ) -> Union[Type[T], Callable[..., MatchEntity[T]]]: From d0581d14a70f37d2555fc41c99a99fab84c32f37 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Thu, 20 Nov 2025 23:41:55 +0100 Subject: [PATCH 06/50] [EQLMatch] match can return a set_of. --- src/krrood/entity_query_language/entity.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index a020351..a8b9f52 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -1,8 +1,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from functools import cached_property, lru_cache -from copy import copy +from functools import cached_property from .hashed_data import T from .symbol_graph import SymbolGraph @@ -390,7 +389,7 @@ class Match(Generic[T]): """ The parent match if this is a nested match. """ - is_selected: bool = field(default=False) + is_selected: bool = field(default=False, kw_only=True) """ Whether the variable should be selected in the result. """ From cac9883fe1418f71c7a3a09d147ee67aa7366d8b Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Fri, 21 Nov 2025 00:59:51 +0100 Subject: [PATCH 07/50] [EQLMatch] in progress selecting literals. --- src/krrood/entity_query_language/entity.py | 46 ++++++++++++++++++---- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index a8b9f52..2cc6e52 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -355,6 +355,10 @@ def inference( """ +class Select: + value: Any + + @dataclass class Match(Generic[T]): """ @@ -369,7 +373,7 @@ class Match(Generic[T]): """ The keyword arguments to match against. """ - variable: CanBehaveLikeAVariable[T] = field(init=False) + variable: CanBehaveLikeAVariable[T] = field(kw_only=True, default=None) """ The created variable from the type and kwargs. """ @@ -393,6 +397,10 @@ class Match(Generic[T]): """ Whether the variable should be selected in the result. """ + existential: bool = field(default=False, kw_only=True) + """ + Whether the match is an existential match check or find all matches. + """ def __call__(self, **kwargs) -> Self: """ @@ -416,7 +424,7 @@ def _resolve( :param parent: The parent match if this is a nested match. :return: """ - self.variable = variable if variable else self._create_variable() + self.variable = variable if variable else self._get_or_create_variable() self.parent = parent if self.is_selected or not parent: self._update_selected_variables(self.variable) @@ -433,7 +441,10 @@ def _resolve( elif v is SELECTED: self._update_selected_variables(attr) else: - if isinstance(v, Match): + if isinstance(v, Select): + self._update_selected_variables(attr) + v = v.value + elif isinstance(v, Match): v = v.variable condition = self._get_either_a_containment_or_an_equal_condition( attr, v, attr_wrapped_field @@ -473,6 +484,8 @@ def _get_either_a_containment_or_an_equal_condition( assigned_value, wrapped_field ): return in_(attr, assigned_value) + elif self.existential: + return contains(assigned_value, flatten(attr)) else: return attr == assigned_value @@ -536,6 +549,8 @@ def _is_iterable_value(value) -> bool: :param value: The value to check. :return: True if the value is an iterable or a Match instance with an iterable type, else False. """ + if isinstance(value, Attribute): + return value._wrapped_field_.is_iterable if not isinstance(value, Match) and is_iterable(value): return True elif isinstance(value, Match) and is_iterable_type(value.type_): @@ -564,10 +579,12 @@ def _add_type_filter_if_needed( ): self.conditions.append(HasType(attr, attr_match.type_)) - def _create_variable(self) -> Variable[T]: + def _get_or_create_variable(self) -> CanBehaveLikeAVariable[T]: """ - Create a variable with the given type. + Create a variable with the given type if """ + if self.variable: + return self.variable return let(self.type_, None) @cached_property @@ -595,7 +612,7 @@ class MatchEntity(Match[T]): The domain to use for the variable created by the match. """ - def _create_variable(self) -> Variable[T]: + def _get_or_create_variable(self) -> Variable[T]: """ Create a variable with the given type and domain. """ @@ -620,7 +637,22 @@ def select(type_: Type[T]) -> MatchType: """ Equivalent to match(type_) and selecting the variable to be included in the result. """ - return Match(type_, is_selected=True) + match_ = match(type_) + match_.is_selected = True + return match_ + + +def select_literal(value: Any): + return Select(value) + + +def select_any(type_: Type[T]) -> MatchType: + """ + Equivalent to match(type_) and selecting the variable to be included in the result. + """ + select_ = select(type_) + select_.existential = True + return select_ def entity_matching( From d0ec23574b7ed7bd3c2c7f3d01008cf7302a9d21 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Fri, 21 Nov 2025 12:17:36 +0100 Subject: [PATCH 08/50] [EQL] select seems to work. --- src/krrood/entity_query_language/entity.py | 64 ++++++++++++++------ src/krrood/entity_query_language/symbolic.py | 19 +++--- 2 files changed, 58 insertions(+), 25 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index 2cc6e52..a9992ca 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -3,7 +3,7 @@ from dataclasses import dataclass, field from functools import cached_property -from .hashed_data import T +from .hashed_data import T, HashedValue from .symbol_graph import SymbolGraph from .utils import is_iterable, is_iterable_type from ..class_diagrams.wrapped_field import WrappedField @@ -47,6 +47,8 @@ ResultQuantifier, Attribute, QueryObjectDescriptor, + Selectable, + OperationResult, ) from .result_quantification_constraint import ResultQuantificationConstraint @@ -355,17 +357,13 @@ def inference( """ -class Select: - value: Any - - @dataclass class Match(Generic[T]): """ Construct a query that looks for the pattern provided by the type and the keyword arguments. """ - type_: Type[T] + type_: Optional[Type[T]] = None """ The type of the variable. """ @@ -373,7 +371,7 @@ class Match(Generic[T]): """ The keyword arguments to match against. """ - variable: CanBehaveLikeAVariable[T] = field(kw_only=True, default=None) + variable: Optional[CanBehaveLikeAVariable[T]] = field(kw_only=True, default=None) """ The created variable from the type and kwargs. """ @@ -428,6 +426,8 @@ def _resolve( self.parent = parent if self.is_selected or not parent: self._update_selected_variables(self.variable) + if not self.type_: + self.type_ = self.variable._type_ for k, v in self.kwargs.items(): attr: Attribute = getattr(self.variable, k) attr_wrapped_field = attr._wrapped_field_ @@ -443,8 +443,7 @@ def _resolve( else: if isinstance(v, Select): self._update_selected_variables(attr) - v = v.value - elif isinstance(v, Match): + if isinstance(v, Match): v = v.variable condition = self._get_either_a_containment_or_an_equal_condition( attr, v, attr_wrapped_field @@ -607,7 +606,7 @@ class MatchEntity(Match[T]): of the outer match variable. """ - domain: DomainType + domain: DomainType = None """ The domain to use for the variable created by the match. """ @@ -619,6 +618,38 @@ def _get_or_create_variable(self) -> Variable[T]: return let(self.type_, self.domain) +@dataclass +class Select(Match[T], Selectable[T]): + _var_: CanBehaveLikeAVariable[T] = field(init=False) + + def __post_init__(self): + self.is_selected = True + self._var_ = self.variable + + def _resolve( + self, + variable: Optional[CanBehaveLikeAVariable] = None, + parent: Optional[Match] = None, + ): + super()._resolve(variable, parent) + self._var_ = self.variable + + def _evaluate__( + self, + sources: Optional[Dict[int, HashedValue]] = None, + parent: Optional[SymbolicExpression] = None, + ) -> Iterable[OperationResult]: + yield from self.variable._evaluate__(sources, parent) + + @property + def _name_(self) -> str: + return self._var_._name_ + + @cached_property + def _all_variable_instances_(self) -> List[CanBehaveLikeAVariable[T]]: + return self._var_._all_variable_instances_ + + MatchType = Union[Iterable[Type[T]], Callable[..., Match[T]]] @@ -633,17 +664,14 @@ def match(type_: Type[T]) -> MatchType: return Match(type_) -def select(type_: Type[T]) -> MatchType: +def select(type_: Optional[Type[T]] = None) -> MatchType: """ Equivalent to match(type_) and selecting the variable to be included in the result. """ - match_ = match(type_) - match_.is_selected = True - return match_ - - -def select_literal(value: Any): - return Select(value) + variable = None + if isinstance(type_, CanBehaveLikeAVariable): + type_ = type_._type_ + return Select(type_, variable=variable) def select_any(type_: Type[T]) -> MatchType: diff --git a/src/krrood/entity_query_language/symbolic.py b/src/krrood/entity_query_language/symbolic.py index 1f25c9b..6495a75 100644 --- a/src/krrood/entity_query_language/symbolic.py +++ b/src/krrood/entity_query_language/symbolic.py @@ -374,13 +374,9 @@ def __repr__(self): @dataclass(eq=False, repr=False) -class CanBehaveLikeAVariable(SymbolicExpression[T], ABC): - """ - This class adds the monitoring/tracking behaviour on variables that tracks attribute access, calling, - and comparison operations. - """ +class Selectable(SymbolicExpression[T], ABC): - _var_: CanBehaveLikeAVariable[T] = field(init=False, default=None) + _var_: CanBehaveLikeAVariable = field(init=False, default=None) """ A variable that is used if the child class to this class want to provide a variable to be tracked other than itself, this is specially useful for child classes that holds a variable instead of being a variable and want @@ -388,12 +384,21 @@ class CanBehaveLikeAVariable(SymbolicExpression[T], ABC): For example, this is the case for the ResultQuantifiers & QueryDescriptors that operate on a single selected variable. """ + + +@dataclass(eq=False, repr=False) +class CanBehaveLikeAVariable(Selectable[T], ABC): + """ + This class adds the monitoring/tracking behaviour on variables that tracks attribute access, calling, + and comparison operations. + """ + _path_: List[ClassRelation] = field(init=False, default_factory=list) """ The path of the variable in the symbol graph as a sequence of relation instances. """ - _type_: Type = field(init=False, default=None) + _type_: Type[T] = field(init=False, default=None) """ The type of the variable. """ From 7e5af653bf740b56ab85a7019e4385d143d55b72 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Mon, 24 Nov 2025 09:28:28 +0100 Subject: [PATCH 09/50] [EQLMatch] cleaning. --- src/krrood/entity_query_language/entity.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index a9992ca..5587b32 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -620,11 +620,19 @@ def _get_or_create_variable(self) -> Variable[T]: @dataclass class Select(Match[T], Selectable[T]): + """ + This is a Match with the addition that the matched entity is selected in the result. + """ + _var_: CanBehaveLikeAVariable[T] = field(init=False) + is_selected: bool = field(init=False, default=True) def __post_init__(self): - self.is_selected = True - self._var_ = self.variable + """ + This is needed to prevent the SymbolicExpression __post_init__ from being called which will make a node out of + this instance, and that is not what we want. + """ + ... def _resolve( self, From 0a9a16c4e631e6a48668210350b0f7214560c74b Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Mon, 24 Nov 2025 10:06:32 +0100 Subject: [PATCH 10/50] [EQLMatch] only return entity() with the parent match variable selected if nothing is explicitly selected. --- src/krrood/entity_query_language/entity.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index 5587b32..cb79c97 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -351,12 +351,6 @@ def inference( ) -SELECTED = object() -""" -A special object that can be used to indicate that the variable should be selected in the result. -""" - - @dataclass class Match(Generic[T]): """ @@ -383,7 +377,7 @@ class Match(Generic[T]): """ Whether the match has been resolved. """ - selected_variables: List[Attribute] = field(init=False, default_factory=list) + selected_variables: List[CanBehaveLikeAVariable] = field(init=False, default_factory=list) """ A list of selected attributes. """ @@ -424,7 +418,7 @@ def _resolve( """ self.variable = variable if variable else self._get_or_create_variable() self.parent = parent - if self.is_selected or not parent: + if self.is_selected: self._update_selected_variables(self.variable) if not self.type_: self.type_ = self.variable._type_ @@ -438,8 +432,6 @@ def _resolve( v._resolve(attr, self) self._add_type_filter_if_needed(attr, v, attr_wrapped_field) self.conditions.extend(v.conditions) - elif v is SELECTED: - self._update_selected_variables(attr) else: if isinstance(v, Select): self._update_selected_variables(attr) @@ -595,6 +587,8 @@ def expression(self) -> QueryObjectDescriptor[T]: if len(self.selected_variables) > 1: return set_of(self.selected_variables, *self.conditions) else: + if not self.selected_variables: + self.selected_variables.append(self.variable) return entity(self.selected_variables[0], *self.conditions) @@ -661,7 +655,7 @@ def _all_variable_instances_(self) -> List[CanBehaveLikeAVariable[T]]: MatchType = Union[Iterable[Type[T]], Callable[..., Match[T]]] -def match(type_: Type[T]) -> MatchType: +def match(type_: Optional[Type[T]] = None) -> MatchType: """ This returns a factory function that creates a Match instance that looks for the pattern provided by the type and the keyword arguments. From d430882ecab4022f380add7a41d7221b92317829 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Mon, 24 Nov 2025 10:55:54 +0100 Subject: [PATCH 11/50] [EQLMatch] cleaning. --- src/krrood/entity_query_language/entity.py | 102 ++++++++++++++++----- 1 file changed, 81 insertions(+), 21 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index cb79c97..12f322d 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -377,7 +377,9 @@ class Match(Generic[T]): """ Whether the match has been resolved. """ - selected_variables: List[CanBehaveLikeAVariable] = field(init=False, default_factory=list) + selected_variables: List[CanBehaveLikeAVariable] = field( + init=False, default_factory=list + ) """ A list of selected attributes. """ @@ -416,33 +418,91 @@ def _resolve( :param parent: The parent match if this is a nested match. :return: """ - self.variable = variable if variable else self._get_or_create_variable() - self.parent = parent - if self.is_selected: - self._update_selected_variables(self.variable) - if not self.type_: - self.type_ = self.variable._type_ - for k, v in self.kwargs.items(): + self._update_the_match_fields(variable, parent) + for k, attr_assigned_value in self.kwargs.items(): attr: Attribute = getattr(self.variable, k) attr_wrapped_field = attr._wrapped_field_ - if isinstance(v, Match) and not v._resolved: - attr = self._flatten_the_attribute_if_is_iterable_while_value_is_not( - attr, v, attr_wrapped_field + if isinstance(attr_assigned_value, Select): + self._update_selected_variables(attr) + if self.is_an_unresolved_match(attr_assigned_value): + self._resolve_child_match_and_merge_conditions( + attr, attr_assigned_value, attr_wrapped_field ) - v._resolve(attr, self) - self._add_type_filter_if_needed(attr, v, attr_wrapped_field) - self.conditions.extend(v.conditions) else: - if isinstance(v, Select): - self._update_selected_variables(attr) - if isinstance(v, Match): - v = v.variable - condition = self._get_either_a_containment_or_an_equal_condition( - attr, v, attr_wrapped_field + self._add_proper_conditions_for_an_already_resolved_child_match( + attr, attr_assigned_value, attr_wrapped_field ) - self.conditions.append(condition) self._resolved = True + @staticmethod + def is_an_unresolved_match(value: Any) -> bool: + """ + Check whether the given value is an unresolved Match instance. + + :param value: The value to check. + :return: True if the value is an unresolved Match instance, else False. + """ + return isinstance(value, Match) and not value._resolved + + def _add_proper_conditions_for_an_already_resolved_child_match( + self, + attr: Attribute, + attr_assigned_value: Any, + attr_wrapped_field: WrappedField, + ): + """ + Add proper conditions for an already resolved child match. These could be an equal, or a containment condition. + + :param attr: A symbolic attribute of this match variable. + :param attr_assigned_value: The assigned value of the attribute, which can be a Match instance. + :param attr_wrapped_field: The WrappedField representing the attribute. + """ + if isinstance(attr_assigned_value, Match): + attr_assigned_value = attr_assigned_value.variable + condition = self._get_either_a_containment_or_an_equal_condition( + attr, attr_assigned_value, attr_wrapped_field + ) + self.conditions.append(condition) + + def _resolve_child_match_and_merge_conditions( + self, + attr: Attribute, + attr_assigned_value: Match, + attr_wrapped_field: WrappedField, + ): + """ + Resolve the child match and merge the conditions with the parent match. + + :param attr: A symbolic attribute of this match variable. + :param attr_assigned_value: The assigned value of the attribute, which is a Match instance. + :param attr_wrapped_field: The WrappedField representing the attribute. + """ + attr = self._flatten_the_attribute_if_is_iterable_while_value_is_not( + attr, attr_assigned_value, attr_wrapped_field + ) + attr_assigned_value._resolve(attr, self) + self._add_type_filter_if_needed(attr, attr_assigned_value, attr_wrapped_field) + self.conditions.extend(attr_assigned_value.conditions) + + def _update_the_match_fields( + self, + variable: Optional[CanBehaveLikeAVariable] = None, + parent: Optional[Match] = None, + ): + """ + Update the match variable, parent, is_selected, and type_ fields. + + :param variable: The variable to use for the match. + If None, a new variable will be created. + :param parent: The parent match if this is a nested match. + """ + self.variable = variable if variable else self._get_or_create_variable() + self.parent = parent + if self.is_selected: + self._update_selected_variables(self.variable) + if not self.type_: + self.type_ = self.variable._type_ + def _update_selected_variables(self, variable: CanBehaveLikeAVariable): """ Update the selected variables of the match by adding the given variable to the root Match selected variables. From 16503d55c6582f0df8e067c432611fb149b4bd87 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Mon, 24 Nov 2025 10:58:30 +0100 Subject: [PATCH 12/50] [EQLMatch] cleaning. --- src/krrood/entity_query_language/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index 12f322d..52049e8 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -419,8 +419,8 @@ def _resolve( :return: """ self._update_the_match_fields(variable, parent) - for k, attr_assigned_value in self.kwargs.items(): - attr: Attribute = getattr(self.variable, k) + for attr_name, attr_assigned_value in self.kwargs.items(): + attr: Attribute = getattr(self.variable, attr_name) attr_wrapped_field = attr._wrapped_field_ if isinstance(attr_assigned_value, Select): self._update_selected_variables(attr) From bb00cf5f62b3474a314a5ade473668345aefbe51 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Mon, 24 Nov 2025 19:30:28 +0100 Subject: [PATCH 13/50] [EQLMatch] doc. --- src/krrood/entity_query_language/entity.py | 63 +++++++++++++++------- 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index 52049e8..eb1dca1 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -395,6 +395,10 @@ class Match(Generic[T]): """ Whether the match is an existential match check or find all matches. """ + is_iterable: bool = field(default=False, kw_only=True) + """ + Whether the match variable is an iterable. + """ def __call__(self, **kwargs) -> Self: """ @@ -422,17 +426,16 @@ def _resolve( for attr_name, attr_assigned_value in self.kwargs.items(): attr: Attribute = getattr(self.variable, attr_name) attr_wrapped_field = attr._wrapped_field_ - if isinstance(attr_assigned_value, Select): - self._update_selected_variables(attr) if self.is_an_unresolved_match(attr_assigned_value): self._resolve_child_match_and_merge_conditions( attr, attr_assigned_value, attr_wrapped_field ) else: + if isinstance(attr_assigned_value, Select): + self._update_selected_variables(attr_assigned_value.variable) self._add_proper_conditions_for_an_already_resolved_child_match( attr, attr_assigned_value, attr_wrapped_field ) - self._resolved = True @staticmethod def is_an_unresolved_match(value: Any) -> bool: @@ -442,7 +445,7 @@ def is_an_unresolved_match(value: Any) -> bool: :param value: The value to check. :return: True if the value is an unresolved Match instance, else False. """ - return isinstance(value, Match) and not value._resolved + return isinstance(value, Match) and not value.variable def _add_proper_conditions_for_an_already_resolved_child_match( self, @@ -457,8 +460,6 @@ def _add_proper_conditions_for_an_already_resolved_child_match( :param attr_assigned_value: The assigned value of the attribute, which can be a Match instance. :param attr_wrapped_field: The WrappedField representing the attribute. """ - if isinstance(attr_assigned_value, Match): - attr_assigned_value = attr_assigned_value.variable condition = self._get_either_a_containment_or_an_equal_condition( attr, attr_assigned_value, attr_wrapped_field ) @@ -527,18 +528,23 @@ def _get_either_a_containment_or_an_equal_condition( :param wrapped_field: The WrappedField representing the attribute. :return: A comparator expression representing the condition. """ + assigned_variable = ( + assigned_value.variable + if isinstance(assigned_value, Match) + else assigned_value + ) if self._attribute_is_iterable_while_the_value_is_not( assigned_value, wrapped_field ): - return contains(attr, assigned_value) + return contains(attr, assigned_variable) elif self._value_is_iterable_while_the_attribute_is_not( assigned_value, wrapped_field ): - return in_(attr, assigned_value) - elif self.existential: - return contains(assigned_value, flatten(attr)) + return in_(attr, assigned_variable) + elif isinstance(assigned_value, Match) and assigned_value.existential: + return contains(assigned_variable, flatten(attr)) else: - return attr == assigned_value + return attr == assigned_variable def _attribute_is_iterable_while_the_value_is_not( self, @@ -604,7 +610,7 @@ def _is_iterable_value(value) -> bool: return value._wrapped_field_.is_iterable if not isinstance(value, Match) and is_iterable(value): return True - elif isinstance(value, Match) and is_iterable_type(value.type_): + elif isinstance(value, Match) and value._is_iterable_value(value.variable): return True return False @@ -713,32 +719,49 @@ def _all_variable_instances_(self) -> List[CanBehaveLikeAVariable[T]]: MatchType = Union[Iterable[Type[T]], Callable[..., Match[T]]] +""" +The types needed for the linter to hint the kwargs for the type construction. +""" +MatchInputType = Union[Type[T], CanBehaveLikeAVariable[T], None] +""" +The input type to the match function. +""" -def match(type_: Optional[Type[T]] = None) -> MatchType: +def match(type_: MatchInputType = None) -> MatchType: """ - This returns a factory function that creates a Match instance that looks for the pattern provided by the type and the + Create and return a Match instance that looks for the pattern provided by the type and the keyword arguments. :param type_: The type of the variable (i.e., The class you want to instantiate). :return: The Match instance. """ + if isinstance(type_, CanBehaveLikeAVariable): + return Match(type_._type_, variable=type_) return Match(type_) -def select(type_: Optional[Type[T]] = None) -> MatchType: +def match_any(type_: MatchInputType) -> MatchType: + """ + Equivalent to match(type_) but for existential matches. + """ + match_ = match(type_) + match_.existential = True + return match_ + + +def select(type_: MatchInputType = None) -> MatchType: """ Equivalent to match(type_) and selecting the variable to be included in the result. """ - variable = None if isinstance(type_, CanBehaveLikeAVariable): - type_ = type_._type_ - return Select(type_, variable=variable) + return Select(type_._type_, variable=type_) + return Select(type_) -def select_any(type_: Type[T]) -> MatchType: +def select_any(type_: MatchInputType) -> MatchType: """ - Equivalent to match(type_) and selecting the variable to be included in the result. + Equivalent to match_any(type_) and selecting the variable to be included in the result. """ select_ = select(type_) select_.existential = True From 3f59ad70506dbef0a017ae0a0cb2eb222be8cd9a Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Mon, 24 Nov 2025 19:36:09 +0100 Subject: [PATCH 14/50] [EQLMatch] fix type hints of match return --- src/krrood/entity_query_language/entity.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index eb1dca1..950bdf1 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -400,7 +400,7 @@ class Match(Generic[T]): Whether the match variable is an iterable. """ - def __call__(self, **kwargs) -> Self: + def __call__(self, **kwargs) -> Union[Self, T]: """ Update the match with new keyword arguments to constrain the type we are matching with. @@ -718,7 +718,7 @@ def _all_variable_instances_(self) -> List[CanBehaveLikeAVariable[T]]: return self._var_._all_variable_instances_ -MatchType = Union[Iterable[Type[T]], Callable[..., Match[T]]] +MatchType = Union[Iterable[Type[T]], Callable[..., Union[Match[T], T]]] """ The types needed for the linter to hint the kwargs for the type construction. """ From ad9fe808dc4ff78e46ee5167b75722770914931b Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Mon, 24 Nov 2025 20:02:07 +0100 Subject: [PATCH 15/50] [EQLMatch] test match. --- src/krrood/entity_query_language/entity.py | 18 ++++++++--- test/test_eql/test_core/test_queries.py | 36 +--------------------- test/test_eql/test_match.py | 34 ++++++++++++++++++++ 3 files changed, 48 insertions(+), 40 deletions(-) create mode 100644 test/test_eql/test_match.py diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index 950bdf1..817723a 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -718,7 +718,7 @@ def _all_variable_instances_(self) -> List[CanBehaveLikeAVariable[T]]: return self._var_._all_variable_instances_ -MatchType = Union[Iterable[Type[T]], Callable[..., Union[Match[T], T]]] +MatchType = Union[Iterable[Type[T]], Type[T], Callable[..., Union[Match[T], T]]] """ The types needed for the linter to hint the kwargs for the type construction. """ @@ -728,7 +728,9 @@ def _all_variable_instances_(self) -> List[CanBehaveLikeAVariable[T]]: """ -def match(type_: MatchInputType = None) -> MatchType: +def match( + type_: MatchInputType = None, +) -> MatchType: """ Create and return a Match instance that looks for the pattern provided by the type and the keyword arguments. @@ -741,7 +743,9 @@ def match(type_: MatchInputType = None) -> MatchType: return Match(type_) -def match_any(type_: MatchInputType) -> MatchType: +def match_any( + type_: MatchInputType, +) -> MatchType: """ Equivalent to match(type_) but for existential matches. """ @@ -750,7 +754,9 @@ def match_any(type_: MatchInputType) -> MatchType: return match_ -def select(type_: MatchInputType = None) -> MatchType: +def select( + type_: MatchInputType = None, +) -> MatchType: """ Equivalent to match(type_) and selecting the variable to be included in the result. """ @@ -759,7 +765,9 @@ def select(type_: MatchInputType = None) -> MatchType: return Select(type_) -def select_any(type_: MatchInputType) -> MatchType: +def select_any( + type_: MatchInputType, +) -> MatchType: """ Equivalent to match_any(type_) and selecting the variable to be included in the result. """ diff --git a/test/test_eql/test_core/test_queries.py b/test/test_eql/test_core/test_queries.py index b842d5f..4d2cde2 100644 --- a/test/test_eql/test_core/test_queries.py +++ b/test/test_eql/test_core/test_queries.py @@ -16,9 +16,7 @@ or_, a, exists, - flatten, - match, - entity_matching, + flatten ) from krrood.entity_query_language.failures import ( MultipleSolutionFound, @@ -31,7 +29,6 @@ symbolic_function, Predicate, ) -from krrood.entity_query_language.symbol_graph import SymbolGraph from krrood.entity_query_language.result_quantification_constraint import ( ResultQuantificationConstraint, Exactly, @@ -748,34 +745,3 @@ def get_quantified_query(quantification: ResultQuantificationConstraint): list(get_quantified_query(Exactly(2)).evaluate()) with pytest.raises(LessThanExpectedNumberOfSolutions): list(get_quantified_query(Exactly(4)).evaluate()) - - -def test_match(handles_and_containers_world): - world = handles_and_containers_world - - fixed_connection_query = the( - entity_matching(FixedConnection, world.connections)( - parent=match(Container)(name="Container1"), - child=match(Handle)(name="Handle1"), - ) - ) - - fixed_connection_query_manual = the( - entity( - fc := let(FixedConnection, domain=None), - HasType(fc.parent, Container), - HasType(fc.child, Handle), - fc.parent.name == "Container1", - fc.child.name == "Handle1", - ) - ) - - assert fixed_connection_query == fixed_connection_query_manual - - fixed_connection_query.visualize() - - fixed_connection = fixed_connection_query.evaluate() - assert isinstance(fixed_connection, FixedConnection) - assert fixed_connection.parent.name == "Container1" - assert isinstance(fixed_connection.child, Handle) - assert fixed_connection.child.name == "Handle1" diff --git a/test/test_eql/test_match.py b/test/test_eql/test_match.py new file mode 100644 index 0000000..33f61e6 --- /dev/null +++ b/test/test_eql/test_match.py @@ -0,0 +1,34 @@ +from ..dataset.semantic_world_like_classes import FixedConnection, Container, Handle +from krrood.entity_query_language.entity import the, entity_matching, match, entity, let +from krrood.entity_query_language.predicate import HasType + + +def test_match(handles_and_containers_world): + world = handles_and_containers_world + + fixed_connection_query = the( + entity_matching(FixedConnection, world.connections)( + parent=match(Container)(name="Container1"), + child=match(Handle)(name="Handle1"), + ) + ) + + fixed_connection_query_manual = the( + entity( + fc := let(FixedConnection, domain=None), + HasType(fc.parent, Container), + HasType(fc.child, Handle), + fc.parent.name == "Container1", + fc.child.name == "Handle1", + ) + ) + + assert fixed_connection_query == fixed_connection_query_manual + + fixed_connection_query.visualize() + + fixed_connection = fixed_connection_query.evaluate() + assert isinstance(fixed_connection, FixedConnection) + assert fixed_connection.parent.name == "Container1" + assert isinstance(fixed_connection.child, Handle) + assert fixed_connection.child.name == "Handle1" From bd59c08edc00b0ba17ef2749804f42540041e360 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Mon, 24 Nov 2025 22:56:13 +0100 Subject: [PATCH 16/50] [EQLMatch] test match any --- src/krrood/entity_query_language/entity.py | 28 +++++++----- test/test_eql/test_core/test_queries.py | 2 +- test/test_eql/test_match.py | 53 +++++++++++++++++++++- 3 files changed, 68 insertions(+), 15 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index 817723a..d57a73e 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -400,7 +400,7 @@ class Match(Generic[T]): Whether the match variable is an iterable. """ - def __call__(self, **kwargs) -> Union[Self, T]: + def __call__(self, **kwargs) -> Union[Self, T, CanBehaveLikeAVariable[T]]: """ Update the match with new keyword arguments to constrain the type we are matching with. @@ -718,7 +718,11 @@ def _all_variable_instances_(self) -> List[CanBehaveLikeAVariable[T]]: return self._var_._all_variable_instances_ -MatchType = Union[Iterable[Type[T]], Type[T], Callable[..., Union[Match[T], T]]] +MatchType = Union[ + Type[T], + CanBehaveLikeAVariable[T], + Callable[..., Union[Match[T], CanBehaveLikeAVariable[T], T]], +] """ The types needed for the linter to hint the kwargs for the type construction. """ @@ -729,8 +733,8 @@ def _all_variable_instances_(self) -> List[CanBehaveLikeAVariable[T]]: def match( - type_: MatchInputType = None, -) -> MatchType: + type_: Union[Type[T], CanBehaveLikeAVariable[T], None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: """ Create and return a Match instance that looks for the pattern provided by the type and the keyword arguments. @@ -744,8 +748,8 @@ def match( def match_any( - type_: MatchInputType, -) -> MatchType: + type_: Union[Type[T], CanBehaveLikeAVariable[T], None], +) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: """ Equivalent to match(type_) but for existential matches. """ @@ -755,8 +759,8 @@ def match_any( def select( - type_: MatchInputType = None, -) -> MatchType: + type_: Union[Type[T], CanBehaveLikeAVariable[T], None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: """ Equivalent to match(type_) and selecting the variable to be included in the result. """ @@ -766,8 +770,8 @@ def select( def select_any( - type_: MatchInputType, -) -> MatchType: + type_: Union[Type[T], CanBehaveLikeAVariable[T], None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: """ Equivalent to match_any(type_) and selecting the variable to be included in the result. """ @@ -777,8 +781,8 @@ def select_any( def entity_matching( - type_: Type[T], domain: DomainType -) -> Union[Type[T], Callable[..., MatchEntity[T]]]: + type_: Union[Type[T], CanBehaveLikeAVariable[T]], domain: DomainType +) -> Union[Type[T], CanBehaveLikeAVariable[T], MatchEntity[T]]: """ Same as :py:func:`krrood.entity_query_language.entity.match` but with a domain to use for the variable created by the match. diff --git a/test/test_eql/test_core/test_queries.py b/test/test_eql/test_core/test_queries.py index 4d2cde2..19c1883 100644 --- a/test/test_eql/test_core/test_queries.py +++ b/test/test_eql/test_core/test_queries.py @@ -16,7 +16,7 @@ or_, a, exists, - flatten + flatten, ) from krrood.entity_query_language.failures import ( MultipleSolutionFound, diff --git a/test/test_eql/test_match.py b/test/test_eql/test_match.py index 33f61e6..7ddce82 100644 --- a/test/test_eql/test_match.py +++ b/test/test_eql/test_match.py @@ -1,5 +1,24 @@ -from ..dataset.semantic_world_like_classes import FixedConnection, Container, Handle -from krrood.entity_query_language.entity import the, entity_matching, match, entity, let +import operator + +from krrood.entity_query_language.symbolic import UnificationDict, SetOf +from ..dataset.semantic_world_like_classes import ( + FixedConnection, + Container, + Handle, + Cabinet, + Drawer, +) +from krrood.entity_query_language.entity import ( + the, + entity_matching, + match, + entity, + let, + select, + match_any, + a, + an, +) from krrood.entity_query_language.predicate import HasType @@ -32,3 +51,33 @@ def test_match(handles_and_containers_world): assert fixed_connection.parent.name == "Container1" assert isinstance(fixed_connection.child, Handle) assert fixed_connection.child.name == "Handle1" + + +def test_select(handles_and_containers_world): + world = handles_and_containers_world + container, handle = select(Container), select(Handle) + fixed_connection_query = the( + entity_matching(FixedConnection, world.connections)( + parent=container(name="Container1"), + child=handle(name="Handle1"), + ) + ) + + assert isinstance(fixed_connection_query._child_, SetOf) + + answers = fixed_connection_query.evaluate() + assert isinstance(answers, UnificationDict) + assert answers[container].name == "Container1" + assert answers[handle].name == "Handle1" + + +def test_match_any(handles_and_containers_world): + world = handles_and_containers_world + drawer = the( + entity_matching(Drawer, world.views)(handle=match(Handle)(name="Handle1")) + ) + cabinet = the(entity_matching(Cabinet, world.views)(drawers=match_any(drawer))) + found_cabinet = cabinet.evaluate() + assert found_cabinet.drawers[0].handle.name == "Handle1" + assert cabinet._child_._child_.operation is operator.contains + assert cabinet._child_._child_.right is drawer From ae2ebec58018cfab0fd711190d570cd05a09896f Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Mon, 24 Nov 2025 23:06:56 +0100 Subject: [PATCH 17/50] [EQLMatch] cleaning and docs. --- examples/eql/match.md | 156 +++++++++++++++++++++ src/krrood/entity_query_language/entity.py | 22 --- 2 files changed, 156 insertions(+), 22 deletions(-) diff --git a/examples/eql/match.md b/examples/eql/match.md index ec9a60f..cb28758 100644 --- a/examples/eql/match.md +++ b/examples/eql/match.md @@ -123,3 +123,159 @@ Notes: - Use `entity_matching` for the outer pattern when a domain is involved; inner attributes use `match`. - Nested `match(...)` can be composed arbitrarily deep following your object graph. - `entity_matching` is a syntactic sugar over the explicit `entity` + predicates form, so both are interchangeable. + +## Selecting inner objects with `select()` + +Use `select(Type)` when you want the matched inner objects to appear in the result. The evaluation then +returns a mapping from the selected variables to the concrete objects (a unification dictionary). + +```{code-cell} ipython3 +from krrood.entity_query_language.entity import select + +container, handle = select(Container), select(Handle) +fixed_connection_query = the( + entity_matching(FixedConnection, world.connections)( + parent=container(name="Container1"), + child=handle(name="Handle1"), + ) +) + +answers = fixed_connection_query.evaluate() +print(answers[container].name, answers[handle].name) +``` + +## Existential matches in collections with `match_any()` + +When matching a container-like attribute (for example, a list), use `match_any(pattern)` to express that +at least one element of the collection should satisfy the given pattern. + +Below we add two simple view classes and build a small scene of drawers and a cabinet. + +```{code-cell} ipython3 +from dataclasses import dataclass +from typing_extensions import List +from krrood.entity_query_language.entity import match_any + + +@dataclass +class Drawer(Symbol): + handle: Handle + container: Container + + +@dataclass +class Cabinet(Symbol): + container: Container + drawers: List[Drawer] + + +# Build a simple set of views +drawer1 = Drawer(handle=h1, container=c1) +drawer2 = Drawer(handle=Handle("OtherHandle"), container=other_c) +cabinet1 = Cabinet(container=c1, drawers=[drawer1, drawer2]) +views = [drawer1, cabinet1] + +# Query: find the cabinet that has a drawer whose handle is named "Handle1" +drawer_pattern = the(entity_matching(Drawer, views)(handle=match(Handle)(name="Handle1"))) +cabinet_query = the(entity_matching(Cabinet, views)(drawers=match_any(drawer_pattern))) + +found_cabinet = cabinet_query.evaluate() +print(found_cabinet.container.name, found_cabinet.drawers[0].handle.name) +``` + +## Selecting elements from collections with `select_any()` + +If you want to retrieve a specific element from a collection attribute while matching, use `select_any(Type)`. +It behaves like `match_any(Type)` but also selects the matched element so you can access it in the result. + +```{code-cell} ipython3 +from krrood.entity_query_language.entity import select_any + +selected_drawer = select_any(Drawer) +cabinet_with_selected_drawer = the( + entity_matching(Cabinet, views)( + drawers=selected_drawer(handle=match(Handle)(name="Handle1")) + ) +) + +ans = cabinet_with_selected_drawer.evaluate() +print(ans[selected_drawer].handle.name) +``` + +## Selecting inner objects with `select()` + +Use `select(Type)` when you want the matched inner objects to appear in the result. The evaluation then +returns a mapping from the selected variables to the concrete objects (a unification dictionary). + +```{code-cell} ipython3 +from krrood.entity_query_language.entity import select + +container, handle = select(Container), select(Handle) +fixed_connection_query = the( + entity_matching(FixedConnection, world.connections)( + parent=container(name="Container1"), + child=handle(name="Handle1"), + ) +) + +answers = fixed_connection_query.evaluate() +print(answers[container].name, answers[handle].name) +``` + +## Existential matches in collections with `match_any()` + +When matching a container-like attribute (for example, a list), use `match_any(pattern)` to express that +at least one element of the collection should satisfy the given pattern. + +Below we add two simple view classes and build a small scene of drawers and a cabinet. + +```{code-cell} ipython3 +from dataclasses import dataclass +from typing_extensions import List +from krrood.entity_query_language.entity import match_any + + +@dataclass +class Drawer(Symbol): + handle: Handle + container: Container + + +@dataclass +class Cabinet(Symbol): + container: Container + drawers: List[Drawer] + + +# Build a simple set of views +drawer1 = Drawer(handle=h1, container=c1) +drawer2 = Drawer(handle=Handle("OtherHandle"), container=other_c) +cabinet1 = Cabinet(container=c1, drawers=[drawer1, drawer2]) +views = [drawer1, cabinet1] + +# Query: find the cabinet that has a drawer whose handle is named "Handle1" +drawer_pattern = the(entity_matching(Drawer, views)(handle=match(Handle)(name="Handle1"))) +cabinet_query = the(entity_matching(Cabinet, views)(drawers=match_any(drawer_pattern))) + +found_cabinet = cabinet_query.evaluate() +print(found_cabinet.container.name, found_cabinet.drawers[0].handle.name) +``` + +## Selecting elements from collections with `select_any()` + +If you want to retrieve a specific element from a collection attribute while matching, use `select_any(Type)`. +It behaves like `match_any(Type)` but also selects the matched element so you can access it in the result. + +```{code-cell} ipython3 +from krrood.entity_query_language.entity import select_any + +selected_drawer = select_any(Drawer) +cabinet_with_selected_drawer = the( + entity_matching(Cabinet, views)( + drawers=selected_drawer(handle=match(Handle)(name="Handle1")) + ) +) + +ans = cabinet_with_selected_drawer.evaluate() +print(ans[selected_drawer].handle.name) +``` diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index d57a73e..8c457e3 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -373,10 +373,6 @@ class Match(Generic[T]): """ The conditions that define the match. """ - _resolved: bool = field(init=False, default=False) - """ - Whether the match has been resolved. - """ selected_variables: List[CanBehaveLikeAVariable] = field( init=False, default_factory=list ) @@ -395,10 +391,6 @@ class Match(Generic[T]): """ Whether the match is an existential match check or find all matches. """ - is_iterable: bool = field(default=False, kw_only=True) - """ - Whether the match variable is an iterable. - """ def __call__(self, **kwargs) -> Union[Self, T, CanBehaveLikeAVariable[T]]: """ @@ -718,20 +710,6 @@ def _all_variable_instances_(self) -> List[CanBehaveLikeAVariable[T]]: return self._var_._all_variable_instances_ -MatchType = Union[ - Type[T], - CanBehaveLikeAVariable[T], - Callable[..., Union[Match[T], CanBehaveLikeAVariable[T], T]], -] -""" -The types needed for the linter to hint the kwargs for the type construction. -""" -MatchInputType = Union[Type[T], CanBehaveLikeAVariable[T], None] -""" -The input type to the match function. -""" - - def match( type_: Union[Type[T], CanBehaveLikeAVariable[T], None] = None, ) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: From 7bcd501bc458cc02730e58f5d2ada05790c91ec5 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Tue, 25 Nov 2025 15:50:35 +0100 Subject: [PATCH 18/50] [EQLMatch] more tests, cleaning. --- src/krrood/entity_query_language/entity.py | 60 +++++++++++--------- src/krrood/entity_query_language/failures.py | 5 +- src/krrood/entity_query_language/symbolic.py | 11 ++-- test/test_eql/test_match.py | 53 ++++++++++------- 4 files changed, 74 insertions(+), 55 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index 8c457e3..389d725 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -5,7 +5,7 @@ from .hashed_data import T, HashedValue from .symbol_graph import SymbolGraph -from .utils import is_iterable, is_iterable_type +from .utils import is_iterable from ..class_diagrams.wrapped_field import WrappedField """ @@ -389,7 +389,7 @@ class Match(Generic[T]): """ existential: bool = field(default=False, kw_only=True) """ - Whether the match is an existential match check or find all matches. + Whether the match is an existential match check or not. """ def __call__(self, **kwargs) -> Union[Self, T, CanBehaveLikeAVariable[T]]: @@ -397,10 +397,23 @@ def __call__(self, **kwargs) -> Union[Self, T, CanBehaveLikeAVariable[T]]: Update the match with new keyword arguments to constrain the type we are matching with. :param kwargs: The keyword arguments to match against. + :return: The current match instance after updating it with the new keyword arguments. """ self.kwargs = kwargs return self + @property + def any(self) -> Union[Self, T, CanBehaveLikeAVariable[T]]: + """ + This is useful only when the attribute and the assigned value (the current match instance) are both iterables. + Then this means that the intersection between the two iterables is not empty. i.e., at least one match between + the two iterables exists. + + :return: The current match instance after marking it as an existential match. + """ + self.existential = True + return self + def _resolve( self, variable: Optional[CanBehaveLikeAVariable] = None, @@ -600,6 +613,8 @@ def _is_iterable_value(value) -> bool: """ if isinstance(value, Attribute): return value._wrapped_field_.is_iterable + elif isinstance(value, CanBehaveLikeAVariable): + return is_iterable(next(iter(value._evaluate__())).operand_value.value) if not isinstance(value, Match) and is_iterable(value): return True elif isinstance(value, Match) and value._is_iterable_value(value.variable): @@ -711,7 +726,7 @@ def _all_variable_instances_(self) -> List[CanBehaveLikeAVariable[T]]: def match( - type_: Union[Type[T], CanBehaveLikeAVariable[T], None] = None, + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, ) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: """ Create and return a Match instance that looks for the pattern provided by the type and the @@ -720,42 +735,31 @@ def match( :param type_: The type of the variable (i.e., The class you want to instantiate). :return: The Match instance. """ - if isinstance(type_, CanBehaveLikeAVariable): - return Match(type_._type_, variable=type_) - return Match(type_) - - -def match_any( - type_: Union[Type[T], CanBehaveLikeAVariable[T], None], -) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: - """ - Equivalent to match(type_) but for existential matches. - """ - match_ = match(type_) - match_.existential = True - return match_ + return _match_or_select(Match, type_) def select( - type_: Union[Type[T], CanBehaveLikeAVariable[T], None] = None, + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, ) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: """ Equivalent to match(type_) and selecting the variable to be included in the result. """ - if isinstance(type_, CanBehaveLikeAVariable): - return Select(type_._type_, variable=type_) - return Select(type_) + return _match_or_select(Select, type_) -def select_any( - type_: Union[Type[T], CanBehaveLikeAVariable[T], None] = None, -) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: +def _match_or_select( + match_type: Type[Match], + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: """ - Equivalent to match_any(type_) and selecting the variable to be included in the result. + Create and return a Match/Select instance that looks for the pattern provided by the type and the + keyword arguments. """ - select_ = select(type_) - select_.existential = True - return select_ + if isinstance(type_, CanBehaveLikeAVariable): + return Select(type_._type_, variable=type_) + elif type_ and not isinstance(type_, type): + return match_type(type_=type_, variable=Literal(type_)) + return match_type(type_) def entity_matching( diff --git a/src/krrood/entity_query_language/failures.py b/src/krrood/entity_query_language/failures.py index 4cbb95b..acb0df6 100644 --- a/src/krrood/entity_query_language/failures.py +++ b/src/krrood/entity_query_language/failures.py @@ -13,6 +13,7 @@ if TYPE_CHECKING: from .symbolic import SymbolicExpression, ResultQuantifier + from .entity import Match @dataclass @@ -120,9 +121,7 @@ class UnSupportedOperand(UnsupportedOperation): """ def __post_init__(self): - self.message = ( - f"{self.unsupported_operand} cannot be used as an operand for {self.operation} operations." - ) + self.message = f"{self.unsupported_operand} cannot be used as an operand for {self.operation} operations." super().__post_init__() diff --git a/src/krrood/entity_query_language/symbolic.py b/src/krrood/entity_query_language/symbolic.py index 6495a75..ec62465 100644 --- a/src/krrood/entity_query_language/symbolic.py +++ b/src/krrood/entity_query_language/symbolic.py @@ -52,7 +52,7 @@ ) from .rxnode import RWXNode, ColorLegend from .symbol_graph import SymbolGraph -from .utils import IDGenerator, is_iterable, generate_combinations +from .utils import IDGenerator, is_iterable, generate_combinations, make_list from ..class_diagrams import ClassRelation from ..class_diagrams.class_diagram import Association, WrappedClass from ..class_diagrams.failures import ClassIsUnMappedInClassDiagram @@ -111,6 +111,10 @@ class OperationResult: def is_true(self): return not self.is_false + @property + def operand_value(self): + return self.bindings[self.operand._id_] + def __contains__(self, item): return item in self.bindings @@ -1050,10 +1054,9 @@ def __init__( ): original_data = data data = [data] - if not is_iterable(data): - data = HashedIterable([data]) if not type_: - first_value = next(iter(data), None) + original_data_lst = make_list(original_data) + first_value = original_data_lst[0] if len(original_data_lst) > 0 else None type_ = type(first_value) if first_value else None if name is None: if type_: diff --git a/test/test_eql/test_match.py b/test/test_eql/test_match.py index 7ddce82..83afefd 100644 --- a/test/test_eql/test_match.py +++ b/test/test_eql/test_match.py @@ -1,13 +1,5 @@ -import operator +import pytest -from krrood.entity_query_language.symbolic import UnificationDict, SetOf -from ..dataset.semantic_world_like_classes import ( - FixedConnection, - Container, - Handle, - Cabinet, - Drawer, -) from krrood.entity_query_language.entity import ( the, entity_matching, @@ -15,11 +7,17 @@ entity, let, select, - match_any, - a, an, ) from krrood.entity_query_language.predicate import HasType +from krrood.entity_query_language.symbolic import UnificationDict, SetOf +from ..dataset.semantic_world_like_classes import ( + FixedConnection, + Container, + Handle, + Cabinet, + Drawer, +) def test_match(handles_and_containers_world): @@ -44,8 +42,6 @@ def test_match(handles_and_containers_world): assert fixed_connection_query == fixed_connection_query_manual - fixed_connection_query.visualize() - fixed_connection = fixed_connection_query.evaluate() assert isinstance(fixed_connection, FixedConnection) assert fixed_connection.parent.name == "Container1" @@ -71,13 +67,30 @@ def test_select(handles_and_containers_world): assert answers[handle].name == "Handle1" -def test_match_any(handles_and_containers_world): +@pytest.fixture +def world_and_cabinets_and_specific_drawer(handles_and_containers_world): world = handles_and_containers_world - drawer = the( - entity_matching(Drawer, world.views)(handle=match(Handle)(name="Handle1")) + my_drawer = Drawer(handle=Handle("Handle2"), container=Container("Container1")) + drawers = list(filter(lambda v: isinstance(v, Drawer), world.views)) + my_cabinet_1 = Cabinet( + container=Container("container2"), drawers=[my_drawer] + drawers ) - cabinet = the(entity_matching(Cabinet, world.views)(drawers=match_any(drawer))) + my_cabinet_2 = Cabinet(container=Container("container2"), drawers=[my_drawer]) + my_cabinet_3 = Cabinet(container=Container("container2"), drawers=drawers) + return world, [my_cabinet_1, my_cabinet_2, my_cabinet_3], my_drawer + + +def test_match_any(world_and_cabinets_and_specific_drawer): + world, cabinets, my_drawer = world_and_cabinets_and_specific_drawer + cabinet = an(entity_matching(Cabinet, cabinets)(drawers=match([my_drawer]).any)) + found_cabinets = list(cabinet.evaluate()) + assert len(found_cabinets) == 2 + assert cabinets[0] in found_cabinets + assert cabinets[1] in found_cabinets + + +def test_match_all(world_and_cabinets_and_specific_drawer): + world, cabinets, my_drawer = world_and_cabinets_and_specific_drawer + cabinet = the(entity_matching(Cabinet, cabinets)(drawers=[my_drawer])) found_cabinet = cabinet.evaluate() - assert found_cabinet.drawers[0].handle.name == "Handle1" - assert cabinet._child_._child_.operation is operator.contains - assert cabinet._child_._child_.right is drawer + assert found_cabinet is cabinets[1] From 993b245328facab3a8438cc74caaf62b6911296f Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Tue, 25 Nov 2025 16:28:16 +0100 Subject: [PATCH 19/50] [EQLMatch] back to match_any, and select_any. --- src/krrood/entity_query_language/entity.py | 34 ++++++++++++++-------- test/test_eql/test_match.py | 3 +- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index 389d725..c5d44b2 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -402,18 +402,6 @@ def __call__(self, **kwargs) -> Union[Self, T, CanBehaveLikeAVariable[T]]: self.kwargs = kwargs return self - @property - def any(self) -> Union[Self, T, CanBehaveLikeAVariable[T]]: - """ - This is useful only when the attribute and the assigned value (the current match instance) are both iterables. - Then this means that the intersection between the two iterables is not empty. i.e., at least one match between - the two iterables exists. - - :return: The current match instance after marking it as an existential match. - """ - self.existential = True - return self - def _resolve( self, variable: Optional[CanBehaveLikeAVariable] = None, @@ -738,6 +726,17 @@ def match( return _match_or_select(Match, type_) +def match_any( + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: + """ + Equivalent to match(type_) but for existential checks. + """ + match_ = match(type_) + match_.existential = True + return match_ + + def select( type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, ) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: @@ -747,6 +746,17 @@ def select( return _match_or_select(Select, type_) +def select_any( + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: + """ + Equivalent to select(type_) but for existential checks. + """ + select_ = select(type_) + select_.existential = True + return select_ + + def _match_or_select( match_type: Type[Match], type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, diff --git a/test/test_eql/test_match.py b/test/test_eql/test_match.py index 83afefd..a0ae2c5 100644 --- a/test/test_eql/test_match.py +++ b/test/test_eql/test_match.py @@ -8,6 +8,7 @@ let, select, an, + match_any, ) from krrood.entity_query_language.predicate import HasType from krrood.entity_query_language.symbolic import UnificationDict, SetOf @@ -82,7 +83,7 @@ def world_and_cabinets_and_specific_drawer(handles_and_containers_world): def test_match_any(world_and_cabinets_and_specific_drawer): world, cabinets, my_drawer = world_and_cabinets_and_specific_drawer - cabinet = an(entity_matching(Cabinet, cabinets)(drawers=match([my_drawer]).any)) + cabinet = an(entity_matching(Cabinet, cabinets)(drawers=match_any([my_drawer]))) found_cabinets = list(cabinet.evaluate()) assert len(found_cabinets) == 2 assert cabinets[0] in found_cabinets From 67a752276171b908e17774a673929ab532badc84 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Tue, 25 Nov 2025 16:44:54 +0100 Subject: [PATCH 20/50] [EQLMatch] restructuring, created match.py, and quantify_entity.py. --- src/krrood/entity_query_language/entity.py | 511 +----------------- src/krrood/entity_query_language/failures.py | 1 - src/krrood/entity_query_language/match.py | 475 ++++++++++++++++ .../entity_query_language/quantify_entity.py | 60 ++ test/test_eql/test_aggregations.py | 3 +- test/test_eql/test_core/test_queries.py | 4 +- test/test_eql/test_core/test_rules.py | 3 +- test/test_eql/test_indexing.py | 3 +- test/test_eql/test_match.py | 8 +- test/test_eql/test_rendering.py | 2 +- test/test_eql/test_symbol_graph.py | 3 +- test/test_ormatic/test_eql.py | 3 +- 12 files changed, 552 insertions(+), 524 deletions(-) create mode 100644 src/krrood/entity_query_language/match.py create mode 100644 src/krrood/entity_query_language/quantify_entity.py diff --git a/src/krrood/entity_query_language/entity.py b/src/krrood/entity_query_language/entity.py index 4f0b917..18eaec4 100644 --- a/src/krrood/entity_query_language/entity.py +++ b/src/krrood/entity_query_language/entity.py @@ -1,12 +1,8 @@ from __future__ import annotations -from dataclasses import dataclass, field -from functools import cached_property - -from .hashed_data import T, HashedValue +from .hashed_data import T from .symbol_graph import SymbolGraph from .utils import is_iterable -from ..class_diagrams.wrapped_field import WrappedField """ User interface (grammar & vocabulary) for entity query language. @@ -18,21 +14,17 @@ Optional, Union, Iterable, - Dict, - Generic, Type, Tuple, List, Callable, - Self, + TYPE_CHECKING, ) from .symbolic import ( SymbolicExpression, Entity, SetOf, - The, - An, AND, Comparator, chained_logic, @@ -44,79 +36,21 @@ ForAll, Exists, Literal, - ResultQuantifier, - Attribute, - QueryObjectDescriptor, - Selectable, - OperationResult, ) -from .result_quantification_constraint import ResultQuantificationConstraint from .predicate import ( Predicate, # type: ignore Symbol, # type: ignore - HasType, ) +if TYPE_CHECKING: + pass + ConditionType = Union[SymbolicExpression, bool, Predicate] """ The possible types for conditions. """ -EntityType = Union[SetOf[T], Entity[T], T, Iterable[T], Type[T]] -""" -The possible types for entities. -""" - - -def an( - entity_: EntityType, - quantification: Optional[ResultQuantificationConstraint] = None, -) -> Union[An[T], T]: - """ - Select a single element satisfying the given entity description. - - :param entity_: An entity or a set expression to quantify over. - :param quantification: Optional quantification constraint. - :return: A quantifier representing "an" element. - :rtype: An[T] - """ - return _quantify_entity(An, entity_, _quantification_constraint_=quantification) - - -a = an -""" -This is an alias to accommodate for words not starting with vowels. -""" - - -def the( - entity_: EntityType, -) -> Union[The[T], T]: - """ - Select the unique element satisfying the given entity description. - - :param entity_: An entity or a set expression to quantify over. - :return: A quantifier representing "an" element. - :rtype: The[T] - """ - return _quantify_entity(The, entity_) - - -def _quantify_entity( - quantifier: Type[ResultQuantifier], entity_: EntityType, **quantifier_kwargs -) -> Union[ResultQuantifier[T], T]: - """ - Apply the given quantifier to the given entity. - - :param quantifier: The quantifier to apply. - :param entity_: The entity to quantify. - :param quantifier_kwargs: Keyword arguments to pass to the quantifier. - :return: The quantified entity. - """ - if isinstance(entity_, Match): - entity_ = entity_.expression - return quantifier(entity_, **quantifier_kwargs) def entity( @@ -348,438 +282,3 @@ def inference( return lambda **kwargs: Variable( _type_=type_, _name__=type_.__name__, _kwargs_=kwargs, _is_inferred_=True ) - - -@dataclass -class Match(Generic[T]): - """ - Construct a query that looks for the pattern provided by the type and the keyword arguments. - """ - - type_: Optional[Type[T]] = None - """ - The type of the variable. - """ - kwargs: Dict[str, Any] = field(init=False, default_factory=dict) - """ - The keyword arguments to match against. - """ - variable: Optional[CanBehaveLikeAVariable[T]] = field(kw_only=True, default=None) - """ - The created variable from the type and kwargs. - """ - conditions: List[ConditionType] = field(init=False, default_factory=list) - """ - The conditions that define the match. - """ - selected_variables: List[CanBehaveLikeAVariable] = field( - init=False, default_factory=list - ) - """ - A list of selected attributes. - """ - parent: Optional[Match] = field(init=False, default=None) - """ - The parent match if this is a nested match. - """ - is_selected: bool = field(default=False, kw_only=True) - """ - Whether the variable should be selected in the result. - """ - existential: bool = field(default=False, kw_only=True) - """ - Whether the match is an existential match check or not. - """ - - def __call__(self, **kwargs) -> Union[Self, T, CanBehaveLikeAVariable[T]]: - """ - Update the match with new keyword arguments to constrain the type we are matching with. - - :param kwargs: The keyword arguments to match against. - :return: The current match instance after updating it with the new keyword arguments. - """ - self.kwargs = kwargs - return self - - def _resolve( - self, - variable: Optional[CanBehaveLikeAVariable] = None, - parent: Optional[Match] = None, - ): - """ - Resolve the match by creating the variable and conditions expressions. - - :param variable: An optional pre-existing variable to use for the match; if not provided, a new variable will - be created. - :param parent: The parent match if this is a nested match. - :return: - """ - self._update_the_match_fields(variable, parent) - for attr_name, attr_assigned_value in self.kwargs.items(): - attr: Attribute = getattr(self.variable, attr_name) - attr_wrapped_field = attr._wrapped_field_ - if self.is_an_unresolved_match(attr_assigned_value): - self._resolve_child_match_and_merge_conditions( - attr, attr_assigned_value, attr_wrapped_field - ) - else: - if isinstance(attr_assigned_value, Select): - self._update_selected_variables(attr_assigned_value.variable) - self._add_proper_conditions_for_an_already_resolved_child_match( - attr, attr_assigned_value, attr_wrapped_field - ) - - @staticmethod - def is_an_unresolved_match(value: Any) -> bool: - """ - Check whether the given value is an unresolved Match instance. - - :param value: The value to check. - :return: True if the value is an unresolved Match instance, else False. - """ - return isinstance(value, Match) and not value.variable - - def _add_proper_conditions_for_an_already_resolved_child_match( - self, - attr: Attribute, - attr_assigned_value: Any, - attr_wrapped_field: WrappedField, - ): - """ - Add proper conditions for an already resolved child match. These could be an equal, or a containment condition. - - :param attr: A symbolic attribute of this match variable. - :param attr_assigned_value: The assigned value of the attribute, which can be a Match instance. - :param attr_wrapped_field: The WrappedField representing the attribute. - """ - condition = self._get_either_a_containment_or_an_equal_condition( - attr, attr_assigned_value, attr_wrapped_field - ) - self.conditions.append(condition) - - def _resolve_child_match_and_merge_conditions( - self, - attr: Attribute, - attr_assigned_value: Match, - attr_wrapped_field: WrappedField, - ): - """ - Resolve the child match and merge the conditions with the parent match. - - :param attr: A symbolic attribute of this match variable. - :param attr_assigned_value: The assigned value of the attribute, which is a Match instance. - :param attr_wrapped_field: The WrappedField representing the attribute. - """ - attr = self._flatten_the_attribute_if_is_iterable_while_value_is_not( - attr, attr_assigned_value, attr_wrapped_field - ) - attr_assigned_value._resolve(attr, self) - self._add_type_filter_if_needed(attr, attr_assigned_value, attr_wrapped_field) - self.conditions.extend(attr_assigned_value.conditions) - - def _update_the_match_fields( - self, - variable: Optional[CanBehaveLikeAVariable] = None, - parent: Optional[Match] = None, - ): - """ - Update the match variable, parent, is_selected, and type_ fields. - - :param variable: The variable to use for the match. - If None, a new variable will be created. - :param parent: The parent match if this is a nested match. - """ - self.variable = variable if variable else self._get_or_create_variable() - self.parent = parent - if self.is_selected: - self._update_selected_variables(self.variable) - if not self.type_: - self.type_ = self.variable._type_ - - def _update_selected_variables(self, variable: CanBehaveLikeAVariable): - """ - Update the selected variables of the match by adding the given variable to the root Match selected variables. - """ - if self.parent: - self.parent._update_selected_variables(variable) - else: - self.selected_variables.append(variable) - - def _get_either_a_containment_or_an_equal_condition( - self, - attr: Attribute, - assigned_value: Any, - wrapped_field: Optional[WrappedField] = None, - ) -> Comparator: - """ - Find and return the appropriate condition for the attribute and its assigned value. This can be one of contains, - in_, or == depending on the type of the assigned value and the type of the attribute. - - :param attr: The attribute to check. - :param assigned_value: The value assigned to the attribute. - :param wrapped_field: The WrappedField representing the attribute. - :return: A comparator expression representing the condition. - """ - assigned_variable = ( - assigned_value.variable - if isinstance(assigned_value, Match) - else assigned_value - ) - if self._attribute_is_iterable_while_the_value_is_not( - assigned_value, wrapped_field - ): - return contains(attr, assigned_variable) - elif self._value_is_iterable_while_the_attribute_is_not( - assigned_value, wrapped_field - ): - return in_(attr, assigned_variable) - elif isinstance(assigned_value, Match) and assigned_value.existential: - return contains(assigned_variable, flatten(attr)) - else: - return attr == assigned_variable - - def _attribute_is_iterable_while_the_value_is_not( - self, - assigned_value: Any, - wrapped_field: Optional[WrappedField] = None, - ) -> bool: - """ - Return True if the attribute is iterable while the assigned value is not an iterable. - - :param assigned_value: The value assigned to the attribute. - :param wrapped_field: The WrappedField representing the attribute. - """ - return ( - wrapped_field - and wrapped_field.is_iterable - and not self._is_iterable_value(assigned_value) - ) - - def _value_is_iterable_while_the_attribute_is_not( - self, assigned_value: Any, wrapped_field: Optional[WrappedField] = None - ) -> bool: - """ - Return True if the assigned value is iterable while the attribute is not an iterable. - - :param assigned_value: The value assigned to the attribute. - :param wrapped_field: The WrappedField representing the attribute. - """ - return ( - wrapped_field - and not wrapped_field.is_iterable - and self._is_iterable_value(assigned_value) - ) - - def _flatten_the_attribute_if_is_iterable_while_value_is_not( - self, - attr: Attribute, - attr_assigned_value: Any, - attr_wrapped_field: Optional[WrappedField] = None, - ) -> Union[Attribute, Flatten]: - """ - Apply a flatten operation to the attribute if it is an iterable while the assigned value is not an iterable. - - :param attr: The attribute to flatten. - :param attr_assigned_value: The value assigned to the attribute. - :param attr_wrapped_field: The WrappedField representing the attribute. - :return: The flattened attribute if it is an iterable, else the original attribute. - """ - if self._is_iterable_value(attr_assigned_value): - return attr - if attr_wrapped_field and attr_wrapped_field.is_iterable: - return flatten(attr) - return attr - - @staticmethod - def _is_iterable_value(value) -> bool: - """ - Whether the value is an iterable or a Match instance with an iterable type. - - :param value: The value to check. - :return: True if the value is an iterable or a Match instance with an iterable type, else False. - """ - if isinstance(value, Attribute): - return value._wrapped_field_.is_iterable - elif isinstance(value, CanBehaveLikeAVariable): - return is_iterable(next(iter(value._evaluate__())).operand_value.value) - if not isinstance(value, Match) and is_iterable(value): - return True - elif isinstance(value, Match) and value._is_iterable_value(value.variable): - return True - return False - - def _add_type_filter_if_needed( - self, - attr: Attribute, - attr_match: Match, - attr_wrapped_field: Optional[WrappedField] = None, - ): - """ - Adds a type filter to the match if needed. Basically when the type hint is not found or when it is - a superclass of the type provided in the match. - - :param attr: The attribute to filter. - :param attr_match:The Match instance of the attribute. - :param attr_wrapped_field: The WrappedField representing the attribute. - :return: - """ - attr_type = attr_wrapped_field.type_endpoint if attr_wrapped_field else None - if (not attr_type) or ( - (attr_match.type_ is not attr_type) - and issubclass(attr_match.type_, attr_type) - ): - self.conditions.append(HasType(attr, attr_match.type_)) - - def _get_or_create_variable(self) -> CanBehaveLikeAVariable[T]: - """ - Create a variable with the given type if - """ - if self.variable: - return self.variable - return let(self.type_, None) - - @cached_property - def expression(self) -> QueryObjectDescriptor[T]: - """ - Return the entity expression corresponding to the match query. - """ - self._resolve() - if len(self.selected_variables) > 1: - return set_of(self.selected_variables, *self.conditions) - else: - if not self.selected_variables: - self.selected_variables.append(self.variable) - return entity(self.selected_variables[0], *self.conditions) - - -@dataclass -class MatchEntity(Match[T]): - """ - A match that can also take a domain and should be used as the outermost match in a nested match statement. - This is because the inner match statements derive their domain from the outer match as they are basically attributes - of the outer match variable. - """ - - domain: DomainType = None - """ - The domain to use for the variable created by the match. - """ - - def _get_or_create_variable(self) -> Variable[T]: - """ - Create a variable with the given type and domain. - """ - return let(self.type_, self.domain) - - -@dataclass -class Select(Match[T], Selectable[T]): - """ - This is a Match with the addition that the matched entity is selected in the result. - """ - - _var_: CanBehaveLikeAVariable[T] = field(init=False) - is_selected: bool = field(init=False, default=True) - - def __post_init__(self): - """ - This is needed to prevent the SymbolicExpression __post_init__ from being called which will make a node out of - this instance, and that is not what we want. - """ - ... - - def _resolve( - self, - variable: Optional[CanBehaveLikeAVariable] = None, - parent: Optional[Match] = None, - ): - super()._resolve(variable, parent) - self._var_ = self.variable - - def _evaluate__( - self, - sources: Optional[Dict[int, HashedValue]] = None, - parent: Optional[SymbolicExpression] = None, - ) -> Iterable[OperationResult]: - yield from self.variable._evaluate__(sources, parent) - - @property - def _name_(self) -> str: - return self._var_._name_ - - @cached_property - def _all_variable_instances_(self) -> List[CanBehaveLikeAVariable[T]]: - return self._var_._all_variable_instances_ - - -def match( - type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, -) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: - """ - Create and return a Match instance that looks for the pattern provided by the type and the - keyword arguments. - - :param type_: The type of the variable (i.e., The class you want to instantiate). - :return: The Match instance. - """ - return _match_or_select(Match, type_) - - -def match_any( - type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, -) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: - """ - Equivalent to match(type_) but for existential checks. - """ - match_ = match(type_) - match_.existential = True - return match_ - - -def select( - type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, -) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: - """ - Equivalent to match(type_) and selecting the variable to be included in the result. - """ - return _match_or_select(Select, type_) - - -def select_any( - type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, -) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: - """ - Equivalent to select(type_) but for existential checks. - """ - select_ = select(type_) - select_.existential = True - return select_ - - -def _match_or_select( - match_type: Type[Match], - type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, -) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: - """ - Create and return a Match/Select instance that looks for the pattern provided by the type and the - keyword arguments. - """ - if isinstance(type_, CanBehaveLikeAVariable): - return Select(type_._type_, variable=type_) - elif type_ and not isinstance(type_, type): - return match_type(type_=type_, variable=Literal(type_)) - return match_type(type_) - - -def entity_matching( - type_: Union[Type[T], CanBehaveLikeAVariable[T]], domain: DomainType -) -> Union[Type[T], CanBehaveLikeAVariable[T], MatchEntity[T]]: - """ - Same as :py:func:`krrood.entity_query_language.entity.match` but with a domain to use for the variable created - by the match. - - :param type_: The type of the variable (i.e., The class you want to instantiate). - :param domain: The domain used for the variable created by the match. - :return: The MatchEntity instance. - """ - return MatchEntity(type_, domain) diff --git a/src/krrood/entity_query_language/failures.py b/src/krrood/entity_query_language/failures.py index acb0df6..e6e8559 100644 --- a/src/krrood/entity_query_language/failures.py +++ b/src/krrood/entity_query_language/failures.py @@ -13,7 +13,6 @@ if TYPE_CHECKING: from .symbolic import SymbolicExpression, ResultQuantifier - from .entity import Match @dataclass diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py new file mode 100644 index 0000000..6e85de8 --- /dev/null +++ b/src/krrood/entity_query_language/match.py @@ -0,0 +1,475 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from functools import cached_property +from typing import Generic, Optional, Type, Dict, Any, List, Union, Self, Iterable + +from ..class_diagrams.wrapped_field import WrappedField +from .entity import ( + ConditionType, + contains, + in_, + flatten, + let, + set_of, + entity, + DomainType, +) +from .hashed_data import T, HashedValue +from .predicate import HasType +from .symbolic import ( + CanBehaveLikeAVariable, + Attribute, + Comparator, + Flatten, + QueryObjectDescriptor, + Variable, + Selectable, + SymbolicExpression, + OperationResult, + Literal, + SetOf, + Entity, +) +from .utils import is_iterable + + +@dataclass +class Match(Generic[T]): + """ + Construct a query that looks for the pattern provided by the type and the keyword arguments. + """ + + type_: Optional[Type[T]] = None + """ + The type of the variable. + """ + kwargs: Dict[str, Any] = field(init=False, default_factory=dict) + """ + The keyword arguments to match against. + """ + variable: Optional[CanBehaveLikeAVariable[T]] = field(kw_only=True, default=None) + """ + The created variable from the type and kwargs. + """ + conditions: List[ConditionType] = field(init=False, default_factory=list) + """ + The conditions that define the match. + """ + selected_variables: List[CanBehaveLikeAVariable] = field( + init=False, default_factory=list + ) + """ + A list of selected attributes. + """ + parent: Optional[Match] = field(init=False, default=None) + """ + The parent match if this is a nested match. + """ + is_selected: bool = field(default=False, kw_only=True) + """ + Whether the variable should be selected in the result. + """ + existential: bool = field(default=False, kw_only=True) + """ + Whether the match is an existential match check or not. + """ + + def __call__(self, **kwargs) -> Union[Self, T, CanBehaveLikeAVariable[T]]: + """ + Update the match with new keyword arguments to constrain the type we are matching with. + + :param kwargs: The keyword arguments to match against. + :return: The current match instance after updating it with the new keyword arguments. + """ + self.kwargs = kwargs + return self + + def _resolve( + self, + variable: Optional[CanBehaveLikeAVariable] = None, + parent: Optional[Match] = None, + ): + """ + Resolve the match by creating the variable and conditions expressions. + + :param variable: An optional pre-existing variable to use for the match; if not provided, a new variable will + be created. + :param parent: The parent match if this is a nested match. + :return: + """ + self._update_the_match_fields(variable, parent) + for attr_name, attr_assigned_value in self.kwargs.items(): + attr: Attribute = getattr(self.variable, attr_name) + attr_wrapped_field = attr._wrapped_field_ + if self.is_an_unresolved_match(attr_assigned_value): + self._resolve_child_match_and_merge_conditions( + attr, attr_assigned_value, attr_wrapped_field + ) + else: + if isinstance(attr_assigned_value, Select): + self._update_selected_variables(attr_assigned_value.variable) + self._add_proper_conditions_for_an_already_resolved_child_match( + attr, attr_assigned_value, attr_wrapped_field + ) + + @staticmethod + def is_an_unresolved_match(value: Any) -> bool: + """ + Check whether the given value is an unresolved Match instance. + + :param value: The value to check. + :return: True if the value is an unresolved Match instance, else False. + """ + return isinstance(value, Match) and not value.variable + + def _add_proper_conditions_for_an_already_resolved_child_match( + self, + attr: Attribute, + attr_assigned_value: Any, + attr_wrapped_field: WrappedField, + ): + """ + Add proper conditions for an already resolved child match. These could be an equal, or a containment condition. + + :param attr: A symbolic attribute of this match variable. + :param attr_assigned_value: The assigned value of the attribute, which can be a Match instance. + :param attr_wrapped_field: The WrappedField representing the attribute. + """ + condition = self._get_either_a_containment_or_an_equal_condition( + attr, attr_assigned_value, attr_wrapped_field + ) + self.conditions.append(condition) + + def _resolve_child_match_and_merge_conditions( + self, + attr: Attribute, + attr_assigned_value: Match, + attr_wrapped_field: WrappedField, + ): + """ + Resolve the child match and merge the conditions with the parent match. + + :param attr: A symbolic attribute of this match variable. + :param attr_assigned_value: The assigned value of the attribute, which is a Match instance. + :param attr_wrapped_field: The WrappedField representing the attribute. + """ + attr = self._flatten_the_attribute_if_is_iterable_while_value_is_not( + attr, attr_assigned_value, attr_wrapped_field + ) + attr_assigned_value._resolve(attr, self) + self._add_type_filter_if_needed(attr, attr_assigned_value, attr_wrapped_field) + self.conditions.extend(attr_assigned_value.conditions) + + def _update_the_match_fields( + self, + variable: Optional[CanBehaveLikeAVariable] = None, + parent: Optional[Match] = None, + ): + """ + Update the match variable, parent, is_selected, and type_ fields. + + :param variable: The variable to use for the match. + If None, a new variable will be created. + :param parent: The parent match if this is a nested match. + """ + self.variable = variable if variable else self._get_or_create_variable() + self.parent = parent + if self.is_selected: + self._update_selected_variables(self.variable) + if not self.type_: + self.type_ = self.variable._type_ + + def _update_selected_variables(self, variable: CanBehaveLikeAVariable): + """ + Update the selected variables of the match by adding the given variable to the root Match selected variables. + """ + if self.parent: + self.parent._update_selected_variables(variable) + else: + self.selected_variables.append(variable) + + def _get_either_a_containment_or_an_equal_condition( + self, + attr: Attribute, + assigned_value: Any, + wrapped_field: Optional[WrappedField] = None, + ) -> Comparator: + """ + Find and return the appropriate condition for the attribute and its assigned value. This can be one of contains, + in_, or == depending on the type of the assigned value and the type of the attribute. + + :param attr: The attribute to check. + :param assigned_value: The value assigned to the attribute. + :param wrapped_field: The WrappedField representing the attribute. + :return: A comparator expression representing the condition. + """ + assigned_variable = ( + assigned_value.variable + if isinstance(assigned_value, Match) + else assigned_value + ) + if self._attribute_is_iterable_while_the_value_is_not( + assigned_value, wrapped_field + ): + return contains(attr, assigned_variable) + elif self._value_is_iterable_while_the_attribute_is_not( + assigned_value, wrapped_field + ): + return in_(attr, assigned_variable) + elif isinstance(assigned_value, Match) and assigned_value.existential: + return contains(assigned_variable, flatten(attr)) + else: + return attr == assigned_variable + + def _attribute_is_iterable_while_the_value_is_not( + self, + assigned_value: Any, + wrapped_field: Optional[WrappedField] = None, + ) -> bool: + """ + Return True if the attribute is iterable while the assigned value is not an iterable. + + :param assigned_value: The value assigned to the attribute. + :param wrapped_field: The WrappedField representing the attribute. + """ + return ( + wrapped_field + and wrapped_field.is_iterable + and not self._is_iterable_value(assigned_value) + ) + + def _value_is_iterable_while_the_attribute_is_not( + self, assigned_value: Any, wrapped_field: Optional[WrappedField] = None + ) -> bool: + """ + Return True if the assigned value is iterable while the attribute is not an iterable. + + :param assigned_value: The value assigned to the attribute. + :param wrapped_field: The WrappedField representing the attribute. + """ + return ( + wrapped_field + and not wrapped_field.is_iterable + and self._is_iterable_value(assigned_value) + ) + + def _flatten_the_attribute_if_is_iterable_while_value_is_not( + self, + attr: Attribute, + attr_assigned_value: Any, + attr_wrapped_field: Optional[WrappedField] = None, + ) -> Union[Attribute, Flatten]: + """ + Apply a flatten operation to the attribute if it is an iterable while the assigned value is not an iterable. + + :param attr: The attribute to flatten. + :param attr_assigned_value: The value assigned to the attribute. + :param attr_wrapped_field: The WrappedField representing the attribute. + :return: The flattened attribute if it is an iterable, else the original attribute. + """ + if self._is_iterable_value(attr_assigned_value): + return attr + if attr_wrapped_field and attr_wrapped_field.is_iterable: + return flatten(attr) + return attr + + @staticmethod + def _is_iterable_value(value) -> bool: + """ + Whether the value is an iterable or a Match instance with an iterable type. + + :param value: The value to check. + :return: True if the value is an iterable or a Match instance with an iterable type, else False. + """ + if isinstance(value, Attribute): + return value._wrapped_field_.is_iterable + elif isinstance(value, CanBehaveLikeAVariable): + return is_iterable(next(iter(value._evaluate__())).operand_value.value) + if not isinstance(value, Match) and is_iterable(value): + return True + elif isinstance(value, Match) and value._is_iterable_value(value.variable): + return True + return False + + def _add_type_filter_if_needed( + self, + attr: Attribute, + attr_match: Match, + attr_wrapped_field: Optional[WrappedField] = None, + ): + """ + Adds a type filter to the match if needed. Basically when the type hint is not found or when it is + a superclass of the type provided in the match. + + :param attr: The attribute to filter. + :param attr_match:The Match instance of the attribute. + :param attr_wrapped_field: The WrappedField representing the attribute. + :return: + """ + attr_type = attr_wrapped_field.type_endpoint if attr_wrapped_field else None + if (not attr_type) or ( + (attr_match.type_ is not attr_type) + and issubclass(attr_match.type_, attr_type) + ): + self.conditions.append(HasType(attr, attr_match.type_)) + + def _get_or_create_variable(self) -> CanBehaveLikeAVariable[T]: + """ + Create a variable with the given type if + """ + if self.variable: + return self.variable + return let(self.type_, None) + + @cached_property + def expression(self) -> QueryObjectDescriptor[T]: + """ + Return the entity expression corresponding to the match query. + """ + self._resolve() + if len(self.selected_variables) > 1: + return set_of(self.selected_variables, *self.conditions) + else: + if not self.selected_variables: + self.selected_variables.append(self.variable) + return entity(self.selected_variables[0], *self.conditions) + + +@dataclass +class MatchEntity(Match[T]): + """ + A match that can also take a domain and should be used as the outermost match in a nested match statement. + This is because the inner match statements derive their domain from the outer match as they are basically attributes + of the outer match variable. + """ + + domain: DomainType = None + """ + The domain to use for the variable created by the match. + """ + + def _get_or_create_variable(self) -> Variable[T]: + """ + Create a variable with the given type and domain. + """ + return let(self.type_, self.domain) + + +@dataclass +class Select(Match[T], Selectable[T]): + """ + This is a Match with the addition that the matched entity is selected in the result. + """ + + _var_: CanBehaveLikeAVariable[T] = field(init=False) + is_selected: bool = field(init=False, default=True) + + def __post_init__(self): + """ + This is needed to prevent the SymbolicExpression __post_init__ from being called which will make a node out of + this instance, and that is not what we want. + """ + ... + + def _resolve( + self, + variable: Optional[CanBehaveLikeAVariable] = None, + parent: Optional[Match] = None, + ): + super()._resolve(variable, parent) + self._var_ = self.variable + + def _evaluate__( + self, + sources: Optional[Dict[int, HashedValue]] = None, + parent: Optional[SymbolicExpression] = None, + ) -> Iterable[OperationResult]: + yield from self.variable._evaluate__(sources, parent) + + @property + def _name_(self) -> str: + return self._var_._name_ + + @cached_property + def _all_variable_instances_(self) -> List[CanBehaveLikeAVariable[T]]: + return self._var_._all_variable_instances_ + + +def match( + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: + """ + Create and return a Match instance that looks for the pattern provided by the type and the + keyword arguments. + + :param type_: The type of the variable (i.e., The class you want to instantiate). + :return: The Match instance. + """ + return _match_or_select(Match, type_) + + +def match_any( + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: + """ + Equivalent to match(type_) but for existential checks. + """ + match_ = match(type_) + match_.existential = True + return match_ + + +def select( + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: + """ + Equivalent to match(type_) and selecting the variable to be included in the result. + """ + return _match_or_select(Select, type_) + + +def select_any( + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: + """ + Equivalent to select(type_) but for existential checks. + """ + select_ = select(type_) + select_.existential = True + return select_ + + +def _match_or_select( + match_type: Type[Match], + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: + """ + Create and return a Match/Select instance that looks for the pattern provided by the type and the + keyword arguments. + """ + if isinstance(type_, CanBehaveLikeAVariable): + return Select(type_._type_, variable=type_) + elif type_ and not isinstance(type_, type): + return match_type(type_=type_, variable=Literal(type_)) + return match_type(type_) + + +def entity_matching( + type_: Union[Type[T], CanBehaveLikeAVariable[T]], domain: DomainType +) -> Union[Type[T], CanBehaveLikeAVariable[T], MatchEntity[T]]: + """ + Same as :py:func:`krrood.entity_query_language.entity.match` but with a domain to use for the variable created + by the match. + + :param type_: The type of the variable (i.e., The class you want to instantiate). + :param domain: The domain used for the variable created by the match. + :return: The MatchEntity instance. + """ + return MatchEntity(type_, domain) + + +EntityType = Union[SetOf[T], Entity[T], T, Iterable[T], Type[T], Match[T]] +""" +The possible types for entities. +""" diff --git a/src/krrood/entity_query_language/quantify_entity.py b/src/krrood/entity_query_language/quantify_entity.py new file mode 100644 index 0000000..feef243 --- /dev/null +++ b/src/krrood/entity_query_language/quantify_entity.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +from typing import Optional, Union, Type + +from .hashed_data import T +from .match import EntityType, Match +from .result_quantification_constraint import ( + ResultQuantificationConstraint, +) +from .symbolic import An, The, ResultQuantifier + + +def an( + entity_: EntityType, + quantification: Optional[ResultQuantificationConstraint] = None, +) -> Union[An[T], T]: + """ + Select a single element satisfying the given entity description. + + :param entity_: An entity or a set expression to quantify over. + :param quantification: Optional quantification constraint. + :return: A quantifier representing "an" element. + :rtype: An[T] + """ + return _quantify_entity(An, entity_, _quantification_constraint_=quantification) + + +a = an +""" +This is an alias to accommodate for words not starting with vowels. +""" + + +def the( + entity_: EntityType, +) -> Union[The[T], T]: + """ + Select the unique element satisfying the given entity description. + + :param entity_: An entity or a set expression to quantify over. + :return: A quantifier representing "an" element. + :rtype: The[T] + """ + return _quantify_entity(The, entity_) + + +def _quantify_entity( + quantifier: Type[ResultQuantifier], entity_: EntityType, **quantifier_kwargs +) -> Union[ResultQuantifier[T], T]: + """ + Apply the given quantifier to the given entity. + + :param quantifier: The quantifier to apply. + :param entity_: The entity to quantify. + :param quantifier_kwargs: Keyword arguments to pass to the quantifier. + :return: The quantified entity. + """ + if isinstance(entity_, Match): + entity_ = entity_.expression + return quantifier(entity_, **quantifier_kwargs) diff --git a/test/test_eql/test_aggregations.py b/test/test_eql/test_aggregations.py index a4ae0ff..2cc9bd6 100644 --- a/test/test_eql/test_aggregations.py +++ b/test/test_eql/test_aggregations.py @@ -1,14 +1,13 @@ from krrood.entity_query_language.entity import ( flatten, entity, - an, not_, in_, - the, for_all, let, exists, ) +from krrood.entity_query_language.quantify_entity import an, the from ..dataset.example_classes import VectorsWithProperty from ..dataset.semantic_world_like_classes import View, Drawer, Container, Cabinet diff --git a/test/test_eql/test_core/test_queries.py b/test/test_eql/test_core/test_queries.py index 19c1883..ff59553 100644 --- a/test/test_eql/test_core/test_queries.py +++ b/test/test_eql/test_core/test_queries.py @@ -8,16 +8,14 @@ not_, contains, in_, - an, entity, set_of, let, - the, or_, - a, exists, flatten, ) +from krrood.entity_query_language.quantify_entity import an, a, the from krrood.entity_query_language.failures import ( MultipleSolutionFound, UnsupportedNegation, diff --git a/test/test_eql/test_core/test_rules.py b/test/test_eql/test_core/test_rules.py index d1941da..142b6a7 100644 --- a/test/test_eql/test_core/test_rules.py +++ b/test/test_eql/test_core/test_rules.py @@ -1,5 +1,6 @@ from krrood.entity_query_language.conclusion import Add -from krrood.entity_query_language.entity import let, an, entity, and_, inference +from krrood.entity_query_language.entity import let, entity, and_, inference +from krrood.entity_query_language.quantify_entity import an from krrood.entity_query_language.predicate import HasType from krrood.entity_query_language.rule import refinement, alternative, next_rule from ...dataset.semantic_world_like_classes import ( diff --git a/test/test_eql/test_indexing.py b/test/test_eql/test_indexing.py index 1891f11..5714aa9 100644 --- a/test/test_eql/test_indexing.py +++ b/test/test_eql/test_indexing.py @@ -1,7 +1,8 @@ from dataclasses import dataclass from typing_extensions import Dict, List -from krrood.entity_query_language.entity import an, entity, let, From +from krrood.entity_query_language.entity import entity, let, From +from krrood.entity_query_language.quantify_entity import an from krrood.entity_query_language.predicate import Symbol from krrood.entity_query_language.symbol_graph import SymbolGraph diff --git a/test/test_eql/test_match.py b/test/test_eql/test_match.py index a0ae2c5..b43b083 100644 --- a/test/test_eql/test_match.py +++ b/test/test_eql/test_match.py @@ -1,15 +1,11 @@ import pytest from krrood.entity_query_language.entity import ( - the, - entity_matching, - match, entity, let, - select, - an, - match_any, ) +from krrood.entity_query_language.quantify_entity import an, the +from krrood.entity_query_language.match import match, match_any, select, entity_matching from krrood.entity_query_language.predicate import HasType from krrood.entity_query_language.symbolic import UnificationDict, SetOf from ..dataset.semantic_world_like_classes import ( diff --git a/test/test_eql/test_rendering.py b/test/test_eql/test_rendering.py index 878aea3..9795651 100644 --- a/test/test_eql/test_rendering.py +++ b/test/test_eql/test_rendering.py @@ -22,10 +22,10 @@ from krrood.entity_query_language.entity import ( entity, let, - an, inference, and_, ) +from krrood.entity_query_language.quantify_entity import an from krrood.entity_query_language.conclusion import Add from krrood.entity_query_language.predicate import HasType diff --git a/test/test_eql/test_symbol_graph.py b/test/test_eql/test_symbol_graph.py index 70b9f9e..e3e008e 100644 --- a/test/test_eql/test_symbol_graph.py +++ b/test/test_eql/test_symbol_graph.py @@ -2,7 +2,8 @@ import pytest -from krrood.entity_query_language.entity import an, entity, let +from krrood.entity_query_language.entity import entity, let +from krrood.entity_query_language.quantify_entity import an from krrood.entity_query_language.symbol_graph import SymbolGraph from ..dataset.example_classes import Position diff --git a/test/test_ormatic/test_eql.py b/test/test_ormatic/test_eql.py index c742327..7e8856b 100644 --- a/test/test_ormatic/test_eql.py +++ b/test/test_ormatic/test_eql.py @@ -22,14 +22,13 @@ ) from krrood.entity_query_language.entity import ( let, - an, entity, - the, contains, and_, or_, in_, ) +from krrood.entity_query_language.quantify_entity import an, the from krrood.ormatic.dao import to_dao from krrood.ormatic.eql_interface import eql_to_sql From c3219576c489a784d11fc709561f7b7beafdc323 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Tue, 25 Nov 2025 16:57:44 +0100 Subject: [PATCH 21/50] [EQLMatch] update docs. --- examples/eql/cache.md | 3 ++- examples/eql/comparators.md | 3 ++- examples/eql/domain_mapping.md | 3 +-- examples/eql/eql_for_sql_experts.md | 3 ++- examples/eql/intro.md | 3 ++- examples/eql/logical_operators.md | 3 ++- examples/eql/match.md | 20 +++++++++++-------- .../eql/predicate_and_symbolic_function.md | 3 ++- examples/eql/result_quantifiers.md | 3 ++- examples/eql/writing_queries.md | 3 ++- examples/eql/writing_rule_trees.md | 3 ++- .../result_quantification_constraint.py | 2 +- 12 files changed, 32 insertions(+), 20 deletions(-) diff --git a/examples/eql/cache.md b/examples/eql/cache.md index 78a3810..b05a70f 100644 --- a/examples/eql/cache.md +++ b/examples/eql/cache.md @@ -28,7 +28,8 @@ from dataclasses import dataclass from typing_extensions import List -from krrood.entity_query_language.entity import entity, an, let, contains, Symbol +from krrood.entity_query_language.entity import entity, let, contains, Symbol +from krrood.entity_query_language.quantify_entity import an @dataclass diff --git a/examples/eql/comparators.md b/examples/eql/comparators.md index dabe2fe..f85534f 100644 --- a/examples/eql/comparators.md +++ b/examples/eql/comparators.md @@ -21,9 +21,10 @@ from dataclasses import dataclass from typing_extensions import List from krrood.entity_query_language.entity import ( - entity, an, let, Symbol, + entity, let, Symbol, in_, contains, not_, and_, or_, ) +from krrood.entity_query_language.quantify_entity import an @dataclass diff --git a/examples/eql/domain_mapping.md b/examples/eql/domain_mapping.md index 57b3ca8..a57a217 100644 --- a/examples/eql/domain_mapping.md +++ b/examples/eql/domain_mapping.md @@ -29,12 +29,11 @@ from typing_extensions import List, Dict from krrood.entity_query_language.entity import ( entity, set_of, - an, let, flatten, Symbol, ) - +from krrood.entity_query_language.quantify_entity import an @dataclass class Body(Symbol): diff --git a/examples/eql/eql_for_sql_experts.md b/examples/eql/eql_for_sql_experts.md index 4d8f037..7d0ad62 100644 --- a/examples/eql/eql_for_sql_experts.md +++ b/examples/eql/eql_for_sql_experts.md @@ -55,7 +55,8 @@ from dataclasses import dataclass, field from typing_extensions import List -from krrood.entity_query_language.entity import let, Symbol, entity, an, and_, in_, contains, set_of +from krrood.entity_query_language.entity import let, Symbol, entity, and_, in_, contains, set_of +from krrood.entity_query_language.quantify_entity import an @dataclass diff --git a/examples/eql/intro.md b/examples/eql/intro.md index 83db5ce..708fb7f 100644 --- a/examples/eql/intro.md +++ b/examples/eql/intro.md @@ -28,7 +28,8 @@ from dataclasses import dataclass from typing_extensions import List -from krrood.entity_query_language.entity import entity, an, let, contains, Symbol +from krrood.entity_query_language.entity import entity, let, contains, Symbol +from krrood.entity_query_language.quantify_entity import an @dataclass diff --git a/examples/eql/logical_operators.md b/examples/eql/logical_operators.md index 5261853..f27aefa 100644 --- a/examples/eql/logical_operators.md +++ b/examples/eql/logical_operators.md @@ -21,7 +21,8 @@ from dataclasses import dataclass from typing_extensions import List -from krrood.entity_query_language.entity import entity, an, or_, Symbol, let, not_, and_ +from krrood.entity_query_language.entity import entity, or_, Symbol, let, not_, and_ +from krrood.entity_query_language.quantify_entity import an @dataclass diff --git a/examples/eql/match.md b/examples/eql/match.md index cb28758..fef8ed7 100644 --- a/examples/eql/match.md +++ b/examples/eql/match.md @@ -25,8 +25,12 @@ from dataclasses import dataclass from typing_extensions import List from krrood.entity_query_language.entity import ( - let, entity, the, - match, entity_matching, Symbol, + let, entity, Symbol, +) +from krrood.entity_query_language.quantify_entity import the +from krrood.entity_query_language.match import ( + match, + entity_matching, ) from krrood.entity_query_language.predicate import HasType @@ -130,7 +134,7 @@ Use `select(Type)` when you want the matched inner objects to appear in the resu returns a mapping from the selected variables to the concrete objects (a unification dictionary). ```{code-cell} ipython3 -from krrood.entity_query_language.entity import select +from krrood.entity_query_language.match import select container, handle = select(Container), select(Handle) fixed_connection_query = the( @@ -154,7 +158,7 @@ Below we add two simple view classes and build a small scene of drawers and a ca ```{code-cell} ipython3 from dataclasses import dataclass from typing_extensions import List -from krrood.entity_query_language.entity import match_any +from krrood.entity_query_language.match import match_any @dataclass @@ -189,7 +193,7 @@ If you want to retrieve a specific element from a collection attribute while mat It behaves like `match_any(Type)` but also selects the matched element so you can access it in the result. ```{code-cell} ipython3 -from krrood.entity_query_language.entity import select_any +from krrood.entity_query_language.match import select_any selected_drawer = select_any(Drawer) cabinet_with_selected_drawer = the( @@ -208,7 +212,7 @@ Use `select(Type)` when you want the matched inner objects to appear in the resu returns a mapping from the selected variables to the concrete objects (a unification dictionary). ```{code-cell} ipython3 -from krrood.entity_query_language.entity import select +from krrood.entity_query_language.match import select container, handle = select(Container), select(Handle) fixed_connection_query = the( @@ -232,7 +236,7 @@ Below we add two simple view classes and build a small scene of drawers and a ca ```{code-cell} ipython3 from dataclasses import dataclass from typing_extensions import List -from krrood.entity_query_language.entity import match_any +from krrood.entity_query_language.match import match_any @dataclass @@ -267,7 +271,7 @@ If you want to retrieve a specific element from a collection attribute while mat It behaves like `match_any(Type)` but also selects the matched element so you can access it in the result. ```{code-cell} ipython3 -from krrood.entity_query_language.entity import select_any +from krrood.entity_query_language.match import select_any selected_drawer = select_any(Drawer) cabinet_with_selected_drawer = the( diff --git a/examples/eql/predicate_and_symbolic_function.md b/examples/eql/predicate_and_symbolic_function.md index 98f697e..4030147 100644 --- a/examples/eql/predicate_and_symbolic_function.md +++ b/examples/eql/predicate_and_symbolic_function.md @@ -26,8 +26,9 @@ Lets first define our model and some sample data. from dataclasses import dataclass from typing_extensions import List -from krrood.entity_query_language.entity import entity, let, an, Symbol +from krrood.entity_query_language.entity import entity, let, Symbol from krrood.entity_query_language.predicate import Predicate, symbolic_function +from krrood.entity_query_language.quantify_entity import an @dataclass diff --git a/examples/eql/result_quantifiers.md b/examples/eql/result_quantifiers.md index b360b6a..8bc7894 100644 --- a/examples/eql/result_quantifiers.md +++ b/examples/eql/result_quantifiers.md @@ -27,7 +27,8 @@ from dataclasses import dataclass from typing_extensions import List -from krrood.entity_query_language.entity import entity, let, the, Symbol, an +from krrood.entity_query_language.entity import entity, let, Symbol +from krrood.entity_query_language.quantify_entity import an, the from krrood.entity_query_language.result_quantification_constraint import AtLeast, AtMost, Exactly, Range from krrood.entity_query_language.failures import MultipleSolutionFound, LessThanExpectedNumberOfSolutions, GreaterThanExpectedNumberOfSolutions diff --git a/examples/eql/writing_queries.md b/examples/eql/writing_queries.md index 14fc754..5af841f 100644 --- a/examples/eql/writing_queries.md +++ b/examples/eql/writing_queries.md @@ -42,7 +42,8 @@ from dataclasses import dataclass from typing_extensions import List -from krrood.entity_query_language.entity import entity, an, let, Symbol +from krrood.entity_query_language.entity import entity, let, Symbol +from krrood.entity_query_language.quantify_entity import an @dataclass diff --git a/examples/eql/writing_rule_trees.md b/examples/eql/writing_rule_trees.md index b5da65b..b258b33 100644 --- a/examples/eql/writing_rule_trees.md +++ b/examples/eql/writing_rule_trees.md @@ -24,7 +24,8 @@ Lets define our domain model and build a small world. We will then build a rule instances to the world. ```{code-cell} ipython3 -from krrood.entity_query_language.entity import entity, an, let, and_, Symbol, inference +from krrood.entity_query_language.entity import entity, let, and_, Symbol, inference +from krrood.entity_query_language.quantify_entity import an from krrood.entity_query_language.rule import refinement, alternative from krrood.entity_query_language.conclusion import Add diff --git a/src/krrood/entity_query_language/result_quantification_constraint.py b/src/krrood/entity_query_language/result_quantification_constraint.py index 3f4f865..59fc9f4 100644 --- a/src/krrood/entity_query_language/result_quantification_constraint.py +++ b/src/krrood/entity_query_language/result_quantification_constraint.py @@ -12,7 +12,7 @@ ) if TYPE_CHECKING: - from .symbolic import An, ResultQuantifier + from .symbolic import ResultQuantifier @dataclass From db9f2e53082afc0aa64286f20d18b7454ae99434 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Tue, 25 Nov 2025 17:54:02 +0100 Subject: [PATCH 22/50] [EQLMatch] better way of finding if variable values are iterable. --- src/krrood/entity_query_language/match.py | 8 +++--- src/krrood/entity_query_language/symbolic.py | 27 +++++++++++++++++--- 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 6e85de8..f00db92 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -282,11 +282,9 @@ def _is_iterable_value(value) -> bool: :param value: The value to check. :return: True if the value is an iterable or a Match instance with an iterable type, else False. """ - if isinstance(value, Attribute): - return value._wrapped_field_.is_iterable - elif isinstance(value, CanBehaveLikeAVariable): - return is_iterable(next(iter(value._evaluate__())).operand_value.value) - if not isinstance(value, Match) and is_iterable(value): + if isinstance(value, CanBehaveLikeAVariable): + return value._is_iterable_ + elif not isinstance(value, Match) and is_iterable(value): return True elif isinstance(value, Match) and value._is_iterable_value(value.variable): return True diff --git a/src/krrood/entity_query_language/symbolic.py b/src/krrood/entity_query_language/symbolic.py index ec62465..d70a5f6 100644 --- a/src/krrood/entity_query_language/symbolic.py +++ b/src/krrood/entity_query_language/symbolic.py @@ -111,10 +111,6 @@ class OperationResult: def is_true(self): return not self.is_false - @property - def operand_value(self): - return self.bindings[self.operand._id_] - def __contains__(self, item): return item in self.bindings @@ -389,6 +385,17 @@ class Selectable(SymbolicExpression[T], ABC): variable. """ + @property + def _is_iterable_(self): + """ + Whether the selectable is iterable. + + :return: True if the selectable is iterable, False otherwise. + """ + if self._var_ and self._var_ is not self: + return self._var_._is_iterable_ + return False + @dataclass(eq=False, repr=False) class CanBehaveLikeAVariable(Selectable[T], ABC): @@ -1030,6 +1037,12 @@ def _all_variable_instances_(self) -> List[Variable]: variables.extend(v._all_variable_instances_) return variables + @property + def _is_iterable_(self): + if self._domain_: + return next(iter(self._domain_), None) is not None + return False + @property def _plot_color_(self) -> ColorLegend: if self._plot_color__: @@ -1187,6 +1200,12 @@ def _relation_(self): ) return None + @property + def _is_iterable_(self): + if self._wrapped_field_: + return self._wrapped_field_.is_iterable + return False + @cached_property def _wrapped_type_(self): try: From 2a3ee590a7b11cbca8732614806deb9b79ffbe6a Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Tue, 25 Nov 2025 20:23:27 +0100 Subject: [PATCH 23/50] [EQLMatch] more efficient exists and comparator. --- scripts/test_documentation.sh | 0 src/krrood/class_diagrams/class_diagram.py | 17 +++++++++- src/krrood/entity_query_language/match.py | 3 +- .../entity_query_language/quantify_entity.py | 2 +- src/krrood/entity_query_language/symbolic.py | 32 ++++++++++--------- test/test_eql/test_match.py | 20 ++++++++++++ 6 files changed, 56 insertions(+), 18 deletions(-) mode change 100644 => 100755 scripts/test_documentation.sh diff --git a/scripts/test_documentation.sh b/scripts/test_documentation.sh old mode 100644 new mode 100755 diff --git a/src/krrood/class_diagrams/class_diagram.py b/src/krrood/class_diagrams/class_diagram.py index b425d7c..268e296 100644 --- a/src/krrood/class_diagrams/class_diagram.py +++ b/src/krrood/class_diagrams/class_diagram.py @@ -471,7 +471,7 @@ def get_wrapped_class(self, clazz: Type) -> Optional[WrappedClass]: except KeyError: raise ClassIsUnMappedInClassDiagram(clazz) - def add_node(self, clazz: WrappedClass): + def add_node(self, clazz: Union[Type, WrappedClass]): """ Adds a new node to the dependency graph for the specified wrapped class. @@ -481,10 +481,25 @@ class to the wrapped class. :param clazz: The wrapped class object to be added to the dependency graph. """ + clazz = self.ensure_wrapped_class(clazz) + if clazz.index is not None: + return clazz.index = self._dependency_graph.add_node(clazz) clazz._class_diagram = self self._cls_wrapped_cls_map[clazz.clazz] = clazz + @staticmethod + def ensure_wrapped_class(clazz: Union[Type, WrappedClass]) -> WrappedClass: + """ + Ensure that the clazz is a WrappedClass. + + :param clazz: The class to wrap. + :return: The wrapped class. + """ + if not isinstance(clazz, WrappedClass): + clazz = WrappedClass(clazz) + return clazz + def add_relation(self, relation: ClassRelation): """ Adds a relation to the internal dependency graph. diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index f00db92..11d5b80 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -14,6 +14,7 @@ set_of, entity, DomainType, + exists, ) from .hashed_data import T, HashedValue from .predicate import HasType @@ -218,7 +219,7 @@ def _get_either_a_containment_or_an_equal_condition( ): return in_(attr, assigned_variable) elif isinstance(assigned_value, Match) and assigned_value.existential: - return contains(assigned_variable, flatten(attr)) + return exists(attr, contains(assigned_variable, flatten(attr))) else: return attr == assigned_variable diff --git a/src/krrood/entity_query_language/quantify_entity.py b/src/krrood/entity_query_language/quantify_entity.py index feef243..a464848 100644 --- a/src/krrood/entity_query_language/quantify_entity.py +++ b/src/krrood/entity_query_language/quantify_entity.py @@ -55,6 +55,6 @@ def _quantify_entity( :param quantifier_kwargs: Keyword arguments to pass to the quantifier. :return: The quantified entity. """ - if isinstance(entity_, Match): + if isinstance(entity_, Match) and not entity_.variable: entity_ = entity_.expression return quantifier(entity_, **quantifier_kwargs) diff --git a/src/krrood/entity_query_language/symbolic.py b/src/krrood/entity_query_language/symbolic.py index d70a5f6..ddcbb8c 100644 --- a/src/krrood/entity_query_language/symbolic.py +++ b/src/krrood/entity_query_language/symbolic.py @@ -1244,9 +1244,11 @@ def _type_(self) -> Optional[Type]: @cached_property def _wrapped_field_(self) -> Optional[WrappedField]: - return self._wrapped_owner_class_._wrapped_field_name_map_.get( - self._attr_name_, None - ) + if self._wrapped_owner_class_ is not None: + return self._wrapped_owner_class_._wrapped_field_name_map_.get( + self._attr_name_, None + ) + return None @cached_property def _wrapped_owner_class_(self): @@ -1446,6 +1448,12 @@ def apply_operation(self, operand_values: OperationResult) -> bool: def get_first_second_operands( self, sources: Dict[int, HashedValue] ) -> Tuple[SymbolicExpression, SymbolicExpression]: + left_has_the = any(isinstance(desc, The) for desc in self.left._descendants_) + right_has_the = any(isinstance(desc, The) for desc in self.right._descendants_) + if left_has_the and not right_has_the: + return self.left, self.right + elif not left_has_the and right_has_the: + return self.right, self.left if sources and any( v.value._var_._id_ in sources for v in self.right._unique_variables_ ): @@ -1759,18 +1767,12 @@ def _evaluate__( ) -> Iterable[OperationResult]: sources = sources or {} self._eval_parent_ = parent - for var_val in self.variable._evaluate__(sources, parent=self): - yield from self.evaluate_condition(var_val.bindings) - - def evaluate_condition( - self, sources: Dict[int, HashedValue] - ) -> Iterable[OperationResult]: - # Evaluate the condition under this particular universal value - for condition_val in self.condition._evaluate__(sources, parent=self): - self._is_false_ = condition_val.is_false - if not self._is_false_: - yield OperationResult(condition_val.bindings, False, self) - break + seen_var_values = set() + for val in self.condition._evaluate__(sources, parent=self): + var_val = val[self.variable._id_] + if val.is_true and var_val not in seen_var_values: + seen_var_values.add(var_val) + yield OperationResult(val.bindings, False, self) def __invert__(self): return ForAll(self.variable, self.condition.__invert__()) diff --git a/test/test_eql/test_match.py b/test/test_eql/test_match.py index b43b083..c0d73a6 100644 --- a/test/test_eql/test_match.py +++ b/test/test_eql/test_match.py @@ -91,3 +91,23 @@ def test_match_all(world_and_cabinets_and_specific_drawer): cabinet = the(entity_matching(Cabinet, cabinets)(drawers=[my_drawer])) found_cabinet = cabinet.evaluate() assert found_cabinet is cabinets[1] + + +def test_match_any_on_collection_returns_unique_parent_entities(): + # setup from the notebook example + c1 = Container("Container1") + other_c = Container("ContainerX") + h1 = Handle("Handle1") + + drawer1 = Drawer(handle=h1, container=c1) + drawer2 = Drawer(handle=Handle("OtherHandle"), container=other_c) + cabinet1 = Cabinet(container=c1, drawers=[drawer1, drawer2]) + cabinet2 = Cabinet(container=other_c, drawers=[drawer2]) + views = [drawer1, drawer2, cabinet1, cabinet2] + + q = an(entity_matching(Cabinet, views)(drawers=match_any({drawer1, drawer2}))) + + results = list(q.evaluate()) + # Expect exactly the two cabinets, no duplicates + assert len(results) == 2 + assert {id(x) for x in results} == {id(cabinet1), id(cabinet2)} From c61339611a2c9bcd0d69eb188e6956fbf1ddc466 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Tue, 25 Nov 2025 21:06:38 +0100 Subject: [PATCH 24/50] [EQLMatch] doc update --- examples/eql/match.md | 30 +++++++++++------------ src/krrood/entity_query_language/match.py | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/eql/match.md b/examples/eql/match.md index fef8ed7..e4921f7 100644 --- a/examples/eql/match.md +++ b/examples/eql/match.md @@ -27,7 +27,7 @@ from typing_extensions import List from krrood.entity_query_language.entity import ( let, entity, Symbol, ) -from krrood.entity_query_language.quantify_entity import the +from krrood.entity_query_language.quantify_entity import the, an from krrood.entity_query_language.match import ( match, entity_matching, @@ -177,14 +177,16 @@ class Cabinet(Symbol): drawer1 = Drawer(handle=h1, container=c1) drawer2 = Drawer(handle=Handle("OtherHandle"), container=other_c) cabinet1 = Cabinet(container=c1, drawers=[drawer1, drawer2]) -views = [drawer1, cabinet1] +cabinet2 = Cabinet(container=other_c, drawers=[drawer2]) +views = [drawer1, drawer2, cabinet1, cabinet2] -# Query: find the cabinet that has a drawer whose handle is named "Handle1" -drawer_pattern = the(entity_matching(Drawer, views)(handle=match(Handle)(name="Handle1"))) -cabinet_query = the(entity_matching(Cabinet, views)(drawers=match_any(drawer_pattern))) +# Query: find the cabinet that has any drawer from the set {drawer1, drawer2} +cabinet_query = an(entity_matching(Cabinet, views)(drawers=match_any([drawer1, drawer2]))) -found_cabinet = cabinet_query.evaluate() -print(found_cabinet.container.name, found_cabinet.drawers[0].handle.name) +found_cabinets = list(cabinet_query.evaluate()) +assert len(found_cabinets) == 2 +print(found_cabinets[0].container.name, found_cabinets[0].drawers[0].handle.name) +print(found_cabinets[1].container.name, found_cabinets[1].drawers[0].handle.name) ``` ## Selecting elements from collections with `select_any()` @@ -195,15 +197,13 @@ It behaves like `match_any(Type)` but also selects the matched element so you ca ```{code-cell} ipython3 from krrood.entity_query_language.match import select_any -selected_drawer = select_any(Drawer) -cabinet_with_selected_drawer = the( - entity_matching(Cabinet, views)( - drawers=selected_drawer(handle=match(Handle)(name="Handle1")) - ) -) +selected_drawers = select_any([drawer1, drawer2]) +# Query: find the cabinet that has any drawer from the set {drawer1, drawer2} +cabinet_query = an(entity_matching(Cabinet, views)(drawers=selected_drawers)) -ans = cabinet_with_selected_drawer.evaluate() -print(ans[selected_drawer].handle.name) +ans = list(cabinet_query.evaluate()) +assert len(ans) == 2 +print(ans[0][selected_drawer][0].handle.name) ``` ## Selecting inner objects with `select()` diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 11d5b80..4f2cb8f 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -109,7 +109,7 @@ def _resolve( ) else: if isinstance(attr_assigned_value, Select): - self._update_selected_variables(attr_assigned_value.variable) + self._update_selected_variables(attr) self._add_proper_conditions_for_an_already_resolved_child_match( attr, attr_assigned_value, attr_wrapped_field ) From 95e195584e319e7613d65dea0fa6dd3d8cd91da9 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Tue, 25 Nov 2025 21:15:30 +0100 Subject: [PATCH 25/50] [EQLMatch] doc update --- examples/eql/match.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/examples/eql/match.md b/examples/eql/match.md index e4921f7..03e3de3 100644 --- a/examples/eql/match.md +++ b/examples/eql/match.md @@ -203,7 +203,7 @@ cabinet_query = an(entity_matching(Cabinet, views)(drawers=selected_drawers)) ans = list(cabinet_query.evaluate()) assert len(ans) == 2 -print(ans[0][selected_drawer][0].handle.name) +print(ans[0][0].handle.name) ``` ## Selecting inner objects with `select()` @@ -255,14 +255,16 @@ class Cabinet(Symbol): drawer1 = Drawer(handle=h1, container=c1) drawer2 = Drawer(handle=Handle("OtherHandle"), container=other_c) cabinet1 = Cabinet(container=c1, drawers=[drawer1, drawer2]) -views = [drawer1, cabinet1] +cabinet2 = Cabinet(container=other_c, drawers=[drawer2]) +views = [drawer1, drawer2, cabinet1, cabinet2] -# Query: find the cabinet that has a drawer whose handle is named "Handle1" -drawer_pattern = the(entity_matching(Drawer, views)(handle=match(Handle)(name="Handle1"))) -cabinet_query = the(entity_matching(Cabinet, views)(drawers=match_any(drawer_pattern))) +# Query: find the cabinet that has any drawer from the set {drawer1, drawer2} +cabinet_query = an(entity_matching(Cabinet, views)(drawers=match_any([drawer1, drawer2]))) -found_cabinet = cabinet_query.evaluate() -print(found_cabinet.container.name, found_cabinet.drawers[0].handle.name) +found_cabinets = list(cabinet_query.evaluate()) +assert len(found_cabinets) == 2 +print(found_cabinets[0].container.name, found_cabinets[0].drawers[0].handle.name) +print(found_cabinets[1].container.name, found_cabinets[1].drawers[0].handle.name) ``` ## Selecting elements from collections with `select_any()` @@ -273,13 +275,11 @@ It behaves like `match_any(Type)` but also selects the matched element so you ca ```{code-cell} ipython3 from krrood.entity_query_language.match import select_any -selected_drawer = select_any(Drawer) -cabinet_with_selected_drawer = the( - entity_matching(Cabinet, views)( - drawers=selected_drawer(handle=match(Handle)(name="Handle1")) - ) -) +selected_drawers = select_any([drawer1, drawer2]) +# Query: find the cabinet that has any drawer from the set {drawer1, drawer2} +cabinet_query = an(entity_matching(Cabinet, views)(drawers=selected_drawers)) -ans = cabinet_with_selected_drawer.evaluate() -print(ans[selected_drawer].handle.name) +ans = list(cabinet_query.evaluate()) +assert len(ans) == 2 +print(ans[0][0].handle.name) ``` From 7ea4804d468048bf00672989be17e8fc0fbd8212 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Wed, 26 Nov 2025 17:36:07 +0100 Subject: [PATCH 26/50] [EQL] updated match logic, need to fix existential and universal conditionals. --- src/krrood/entity_query_language/failures.py | 21 +++ src/krrood/entity_query_language/match.py | 149 ++++++++++--------- src/krrood/entity_query_language/symbolic.py | 9 ++ 3 files changed, 108 insertions(+), 71 deletions(-) diff --git a/src/krrood/entity_query_language/failures.py b/src/krrood/entity_query_language/failures.py index e6e8559..c7271c8 100644 --- a/src/krrood/entity_query_language/failures.py +++ b/src/krrood/entity_query_language/failures.py @@ -189,3 +189,24 @@ def __post_init__(self): f"e.g. Entity, or SetOf" ) super().__post_init__() + + +@dataclass +class ClassDiagramError(DataclassException): + """ + An error related to the class diagram. + """ + + +@dataclass +class NoneWrappedFieldError(ClassDiagramError): + """ + Raised when a field of a class is not wrapped by a WrappedField. + """ + + clazz: Type + attr_name: str + + def __post_init__(self): + self.message = f"Field '{self.attr_name}' of class '{self.clazz.__name__}' is not wrapped by a WrappedField." + super().__post_init__() diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 4f2cb8f..75f3145 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -4,6 +4,7 @@ from functools import cached_property from typing import Generic, Optional, Type, Dict, Any, List, Union, Self, Iterable +from .failures import NoneWrappedFieldError from ..class_diagrams.wrapped_field import WrappedField from .entity import ( ConditionType, @@ -101,19 +102,53 @@ def _resolve( """ self._update_the_match_fields(variable, parent) for attr_name, attr_assigned_value in self.kwargs.items(): - attr: Attribute = getattr(self.variable, attr_name) - attr_wrapped_field = attr._wrapped_field_ + attr = self._get_attribute_and_update_selected_variables( + attr_name, attr_assigned_value + ) if self.is_an_unresolved_match(attr_assigned_value): self._resolve_child_match_and_merge_conditions( - attr, attr_assigned_value, attr_wrapped_field + attr, attr_assigned_value ) else: - if isinstance(attr_assigned_value, Select): - self._update_selected_variables(attr) self._add_proper_conditions_for_an_already_resolved_child_match( - attr, attr_assigned_value, attr_wrapped_field + attr, attr_assigned_value ) + def _get_attribute_and_update_selected_variables( + self, attr_name: str, attr_assigned_value: Any + ) -> Union[Attribute, Flatten]: + """ + Get the attribute from the variable and update the selected variables with the attribute. + + :param attr_name: The name of the attribute to get. + :param attr_assigned_value: The assigned value of the attribute. + :return: The attribute. + """ + attr: Attribute = getattr(self.variable, attr_name) + if not attr._wrapped_field_: + raise NoneWrappedFieldError(self.variable._type_, attr_name) + if isinstance(attr_assigned_value, Select): + attr = self._update_attribute_and_selected_variables( + attr, attr_assigned_value + ) + return attr + + def _update_attribute_and_selected_variables( + self, attr: Attribute, attr_assigned_value: Select + ) -> Union[Attribute, Flatten]: + """ + Update the attribute by flattening it if it is iterable, and update the selected variables with the attribute. + + :param attr: The attribute to update. + :param attr_assigned_value: The assigned value of the attribute. + :return: The updated attribute.. + """ + if attr._is_iterable_: + attr = flatten(attr) + self._update_selected_variables(attr) + attr_assigned_value.update_selected_variable(attr) + return attr + @staticmethod def is_an_unresolved_match(value: Any) -> bool: """ @@ -128,17 +163,15 @@ def _add_proper_conditions_for_an_already_resolved_child_match( self, attr: Attribute, attr_assigned_value: Any, - attr_wrapped_field: WrappedField, ): """ Add proper conditions for an already resolved child match. These could be an equal, or a containment condition. :param attr: A symbolic attribute of this match variable. :param attr_assigned_value: The assigned value of the attribute, which can be a Match instance. - :param attr_wrapped_field: The WrappedField representing the attribute. """ condition = self._get_either_a_containment_or_an_equal_condition( - attr, attr_assigned_value, attr_wrapped_field + attr, attr_assigned_value ) self.conditions.append(condition) @@ -146,20 +179,19 @@ def _resolve_child_match_and_merge_conditions( self, attr: Attribute, attr_assigned_value: Match, - attr_wrapped_field: WrappedField, ): """ Resolve the child match and merge the conditions with the parent match. :param attr: A symbolic attribute of this match variable. :param attr_assigned_value: The assigned value of the attribute, which is a Match instance. - :param attr_wrapped_field: The WrappedField representing the attribute. """ - attr = self._flatten_the_attribute_if_is_iterable_while_value_is_not( - attr, attr_assigned_value, attr_wrapped_field - ) + type_filter_needed = self._is_type_filter_needed(attr, attr_assigned_value) + if attr._is_iterable_ and (attr_assigned_value.kwargs or type_filter_needed): + attr = flatten(attr) attr_assigned_value._resolve(attr, self) - self._add_type_filter_if_needed(attr, attr_assigned_value, attr_wrapped_field) + if type_filter_needed: + self._add_type_filter(attr, attr_assigned_value) self.conditions.extend(attr_assigned_value.conditions) def _update_the_match_fields( @@ -194,7 +226,6 @@ def _get_either_a_containment_or_an_equal_condition( self, attr: Attribute, assigned_value: Any, - wrapped_field: Optional[WrappedField] = None, ) -> Comparator: """ Find and return the appropriate condition for the attribute and its assigned value. This can be one of contains, @@ -202,7 +233,6 @@ def _get_either_a_containment_or_an_equal_condition( :param attr: The attribute to check. :param assigned_value: The value assigned to the attribute. - :param wrapped_field: The WrappedField representing the attribute. :return: A comparator expression representing the condition. """ assigned_variable = ( @@ -210,70 +240,41 @@ def _get_either_a_containment_or_an_equal_condition( if isinstance(assigned_value, Match) else assigned_value ) - if self._attribute_is_iterable_while_the_value_is_not( - assigned_value, wrapped_field - ): + if self._attribute_is_iterable_while_the_value_is_not(assigned_value, attr): return contains(attr, assigned_variable) - elif self._value_is_iterable_while_the_attribute_is_not( - assigned_value, wrapped_field - ): + elif self._value_is_iterable_while_the_attribute_is_not(assigned_value, attr): return in_(attr, assigned_variable) - elif isinstance(assigned_value, Match) and assigned_value.existential: - return exists(attr, contains(assigned_variable, flatten(attr))) + elif attr._is_iterable_ and self._is_iterable_value(assigned_value): + flat_attr = flatten(attr) if not isinstance(attr, Flatten) else attr + return contains(assigned_variable, flat_attr) else: return attr == assigned_variable def _attribute_is_iterable_while_the_value_is_not( self, assigned_value: Any, - wrapped_field: Optional[WrappedField] = None, + attr: Union[Flatten, Attribute], ) -> bool: """ Return True if the attribute is iterable while the assigned value is not an iterable. :param assigned_value: The value assigned to the attribute. - :param wrapped_field: The WrappedField representing the attribute. + :param attr: The attribute to check. """ - return ( - wrapped_field - and wrapped_field.is_iterable - and not self._is_iterable_value(assigned_value) - ) + return attr._is_iterable_ and not self._is_iterable_value(assigned_value) def _value_is_iterable_while_the_attribute_is_not( - self, assigned_value: Any, wrapped_field: Optional[WrappedField] = None + self, + assigned_value: Any, + attr: Union[Flatten, Attribute], ) -> bool: """ Return True if the assigned value is iterable while the attribute is not an iterable. :param assigned_value: The value assigned to the attribute. - :param wrapped_field: The WrappedField representing the attribute. - """ - return ( - wrapped_field - and not wrapped_field.is_iterable - and self._is_iterable_value(assigned_value) - ) - - def _flatten_the_attribute_if_is_iterable_while_value_is_not( - self, - attr: Attribute, - attr_assigned_value: Any, - attr_wrapped_field: Optional[WrappedField] = None, - ) -> Union[Attribute, Flatten]: + :param attr: The attribute to check. """ - Apply a flatten operation to the attribute if it is an iterable while the assigned value is not an iterable. - - :param attr: The attribute to flatten. - :param attr_assigned_value: The value assigned to the attribute. - :param attr_wrapped_field: The WrappedField representing the attribute. - :return: The flattened attribute if it is an iterable, else the original attribute. - """ - if self._is_iterable_value(attr_assigned_value): - return attr - if attr_wrapped_field and attr_wrapped_field.is_iterable: - return flatten(attr) - return attr + return not attr._is_iterable_ and self._is_iterable_value(assigned_value) @staticmethod def _is_iterable_value(value) -> bool: @@ -291,27 +292,26 @@ def _is_iterable_value(value) -> bool: return True return False - def _add_type_filter_if_needed( + def _add_type_filter( self, attr: Attribute, attr_match: Match, - attr_wrapped_field: Optional[WrappedField] = None, ): """ - Adds a type filter to the match if needed. Basically when the type hint is not found or when it is - a superclass of the type provided in the match. + Adds a type filter to the match. :param attr: The attribute to filter. :param attr_match:The Match instance of the attribute. - :param attr_wrapped_field: The WrappedField representing the attribute. - :return: """ - attr_type = attr_wrapped_field.type_endpoint if attr_wrapped_field else None - if (not attr_type) or ( - (attr_match.type_ is not attr_type) + self.conditions.append(HasType(attr, attr_match.type_)) + + @staticmethod + def _is_type_filter_needed(attr: Attribute, attr_match: Match): + attr_type = attr._type_ + return (not attr_type) or ( + (attr_match.type_ and attr_match.type_ is not attr_type) and issubclass(attr_match.type_, attr_type) - ): - self.conditions.append(HasType(attr, attr_match.type_)) + ) def _get_or_create_variable(self) -> CanBehaveLikeAVariable[T]: """ @@ -377,7 +377,14 @@ def _resolve( parent: Optional[Match] = None, ): super()._resolve(variable, parent) - self._var_ = self.variable + if not self._var_: + self.update_selected_variable(self.variable) + + def update_selected_variable(self, variable: CanBehaveLikeAVariable): + """ + Update the selected variable with the given one. + """ + self._var_ = variable def _evaluate__( self, diff --git a/src/krrood/entity_query_language/symbolic.py b/src/krrood/entity_query_language/symbolic.py index ddcbb8c..1c8fd77 100644 --- a/src/krrood/entity_query_language/symbolic.py +++ b/src/krrood/entity_query_language/symbolic.py @@ -1330,6 +1330,15 @@ def _apply_mapping_(self, value: HashedValue) -> Iterable[HashedValue]: def _name_(self): return f"Flatten({self._child_._name_})" + @property + def _is_iterable_(self): + """ + Whether the selectable is iterable. + + :return: True if the selectable is iterable, False otherwise. + """ + return False + @dataclass(eq=False, repr=False) class BinaryOperator(SymbolicExpression, ABC): From d627aaa0b299d64f402eba28c739f4a53338ecb0 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Thu, 27 Nov 2025 18:04:04 +0100 Subject: [PATCH 27/50] [EQL] universal match doesn't work. --- src/krrood/entity_query_language/match.py | 84 ++++++++++++++++++----- test/test_eql/test_match.py | 12 +++- 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 75f3145..eb852a2 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -4,6 +4,9 @@ from functools import cached_property from typing import Generic, Optional, Type, Dict, Any, List, Union, Self, Iterable +from krrood.entity_query_language.entity import for_all +from krrood.entity_query_language.symbolic import ForAll, Exists + from .failures import NoneWrappedFieldError from ..class_diagrams.wrapped_field import WrappedField from .entity import ( @@ -76,6 +79,10 @@ class Match(Generic[T]): """ Whether the match is an existential match check or not. """ + universal: bool = field(default=False, kw_only=True) + """ + Whether the match is a universal match (i.e., must match for all values of the variable/attribute) check or not. + """ def __call__(self, **kwargs) -> Union[Self, T, CanBehaveLikeAVariable[T]]: """ @@ -159,22 +166,6 @@ def is_an_unresolved_match(value: Any) -> bool: """ return isinstance(value, Match) and not value.variable - def _add_proper_conditions_for_an_already_resolved_child_match( - self, - attr: Attribute, - attr_assigned_value: Any, - ): - """ - Add proper conditions for an already resolved child match. These could be an equal, or a containment condition. - - :param attr: A symbolic attribute of this match variable. - :param attr_assigned_value: The assigned value of the attribute, which can be a Match instance. - """ - condition = self._get_either_a_containment_or_an_equal_condition( - attr, attr_assigned_value - ) - self.conditions.append(condition) - def _resolve_child_match_and_merge_conditions( self, attr: Attribute, @@ -222,6 +213,45 @@ def _update_selected_variables(self, variable: CanBehaveLikeAVariable): else: self.selected_variables.append(variable) + def _add_proper_conditions_for_an_already_resolved_child_match( + self, + attr: Attribute, + attr_assigned_value: Any, + ): + """ + Add proper conditions for an already resolved child match. These could be an equal, or a containment condition. + + :param attr: A symbolic attribute of this match variable. + :param attr_assigned_value: The assigned value of the attribute, which can be a Match instance. + """ + condition = self._get_either_a_containment_or_an_equal_condition( + attr, attr_assigned_value + ) + condition = self._update_condition_if_existential_or_universal( + attr, attr_assigned_value, condition + ) + self.conditions.append(condition) + + def _update_condition_if_existential_or_universal( + self, + attr: Union[Attribute, Flatten], + attr_assigned_value: Any, + condition: Comparator, + ) -> Union[Comparator, Exists, ForAll]: + """ + Update the condition depending on whether it is an existential or universal check. + + :param attr: The attribute on which the condition is applied. + :param condition: The condition to update. + :return: The updated condition. + """ + if isinstance(attr_assigned_value, Match) and attr_assigned_value.existential: + attr = attr if not isinstance(attr, Flatten) else attr._child_ + condition = exists(attr, condition) + elif isinstance(attr_assigned_value, Match) and attr_assigned_value.universal: + condition = for_all(attr, condition) + return condition + def _get_either_a_containment_or_an_equal_condition( self, attr: Attribute, @@ -426,6 +456,17 @@ def match_any( return match_ +def match_all( + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: + """ + Equivalent to match(type_) but for universal checks. + """ + match_ = match(type_) + match_.universal = True + return match_ + + def select( type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, ) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: @@ -446,6 +487,17 @@ def select_any( return select_ +def select_all( + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: + """ + Equivalent to select(type_) but for universal checks. + """ + select_ = select(type_) + select_.universal = True + return select_ + + def _match_or_select( match_type: Type[Match], type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, diff --git a/test/test_eql/test_match.py b/test/test_eql/test_match.py index c0d73a6..43d4da9 100644 --- a/test/test_eql/test_match.py +++ b/test/test_eql/test_match.py @@ -5,7 +5,13 @@ let, ) from krrood.entity_query_language.quantify_entity import an, the -from krrood.entity_query_language.match import match, match_any, select, entity_matching +from krrood.entity_query_language.match import ( + match, + match_any, + select, + entity_matching, + match_all, +) from krrood.entity_query_language.predicate import HasType from krrood.entity_query_language.symbolic import UnificationDict, SetOf from ..dataset.semantic_world_like_classes import ( @@ -88,7 +94,7 @@ def test_match_any(world_and_cabinets_and_specific_drawer): def test_match_all(world_and_cabinets_and_specific_drawer): world, cabinets, my_drawer = world_and_cabinets_and_specific_drawer - cabinet = the(entity_matching(Cabinet, cabinets)(drawers=[my_drawer])) + cabinet = the(entity_matching(Cabinet, cabinets)(drawers=match_all([my_drawer]))) found_cabinet = cabinet.evaluate() assert found_cabinet is cabinets[1] @@ -105,7 +111,7 @@ def test_match_any_on_collection_returns_unique_parent_entities(): cabinet2 = Cabinet(container=other_c, drawers=[drawer2]) views = [drawer1, drawer2, cabinet1, cabinet2] - q = an(entity_matching(Cabinet, views)(drawers=match_any({drawer1, drawer2}))) + q = an(entity_matching(Cabinet, views)(drawers=match_any([drawer1, drawer2]))) results = list(q.evaluate()) # Expect exactly the two cabinets, no duplicates From 6157830509ee232c6d1c3869aa0ef942a61c8322 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Thu, 27 Nov 2025 19:08:23 +0100 Subject: [PATCH 28/50] [EQL] Symbol doc update. --- src/krrood/entity_query_language/predicate.py | 47 +------------------ 1 file changed, 2 insertions(+), 45 deletions(-) diff --git a/src/krrood/entity_query_language/predicate.py b/src/krrood/entity_query_language/predicate.py index 169fbd0..70feafb 100644 --- a/src/krrood/entity_query_language/predicate.py +++ b/src/krrood/entity_query_language/predicate.py @@ -66,9 +66,9 @@ def wrapper(*args, **kwargs) -> Optional[Any]: return wrapper -@dataclass +@dataclass(eq=False) class Symbol: - """Base class for things that can be described by property descriptors.""" + """Base class for things that can be cached in the symbol graph.""" def __new__(cls, *args, **kwargs): instance = super().__new__(cls) @@ -145,49 +145,6 @@ class HasTypes(HasType): """ -def extract_selected_variable_and_expression( - symbolic_cls: Type, - domain: Optional[From] = None, - predicate_type: Optional[PredicateType] = None, - **kwargs, -): - """ - Extracts a variable and constructs its expression tree for the given symbolic class. - - This function generates a variable of the specified `symbolic_cls` and uses the - provided domain, predicate type, and additional arguments to create its expression - tree. The domain can optionally be filtered when iterating through its elements - if specified or retrieved from the cache keys associated with the symbolic class. - - :param symbolic_cls: The symbolic class type to be used for variable creation. - :param domain: Optional domain to provide constraints for the variable. - :param predicate_type: Optional predicate type associated with the variable. - :param kwargs: Additional properties to define and construct the variable. - :return: A tuple containing the generated variable and its corresponding expression tree. - """ - cache_keys = [symbolic_cls] + recursive_subclasses(symbolic_cls) - if not domain and cache_keys: - domain = From( - ( - instance - for instance in SymbolGraph()._class_to_wrapped_instances[symbolic_cls] - ) - ) - elif domain and is_iterable(domain.domain): - domain.domain = filter(lambda v: isinstance(v, symbolic_cls), domain.domain) - - var = Variable( - _name__=symbolic_cls.__name__, - _type_=symbolic_cls, - _domain_source_=domain, - _predicate_type_=predicate_type, - ) - - expression = properties_to_expression_tree(var, kwargs) - - return var, expression - - def update_cache(instance: Symbol): """ Updates the cache with the given instance of a symbolic type. From b261b8e67f0bfc66582882f8462654d5daf21e65 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Fri, 28 Nov 2025 10:47:32 +0100 Subject: [PATCH 29/50] [EQL] fixed match all. --- src/krrood/entity_query_language/match.py | 15 +++++++++------ src/krrood/entity_query_language/symbolic.py | 17 ++++++++++++++--- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index eb852a2..fba0a75 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -227,19 +227,19 @@ def _add_proper_conditions_for_an_already_resolved_child_match( condition = self._get_either_a_containment_or_an_equal_condition( attr, attr_assigned_value ) - condition = self._update_condition_if_existential_or_universal( + condition = self._update_condition_if_existential( attr, attr_assigned_value, condition ) self.conditions.append(condition) - def _update_condition_if_existential_or_universal( + def _update_condition_if_existential( self, attr: Union[Attribute, Flatten], attr_assigned_value: Any, condition: Comparator, ) -> Union[Comparator, Exists, ForAll]: """ - Update the condition depending on whether it is an existential or universal check. + Update the condition depending if it is an existential. :param attr: The attribute on which the condition is applied. :param condition: The condition to update. @@ -248,8 +248,6 @@ def _update_condition_if_existential_or_universal( if isinstance(attr_assigned_value, Match) and attr_assigned_value.existential: attr = attr if not isinstance(attr, Flatten) else attr._child_ condition = exists(attr, condition) - elif isinstance(attr_assigned_value, Match) and attr_assigned_value.universal: - condition = for_all(attr, condition) return condition def _get_either_a_containment_or_an_equal_condition( @@ -270,11 +268,16 @@ def _get_either_a_containment_or_an_equal_condition( if isinstance(assigned_value, Match) else assigned_value ) + universal = isinstance(assigned_value, Match) and assigned_value.universal if self._attribute_is_iterable_while_the_value_is_not(assigned_value, attr): return contains(attr, assigned_variable) elif self._value_is_iterable_while_the_attribute_is_not(assigned_value, attr): return in_(attr, assigned_variable) - elif attr._is_iterable_ and self._is_iterable_value(assigned_value): + elif ( + attr._is_iterable_ + and self._is_iterable_value(assigned_value) + and not universal + ): flat_attr = flatten(attr) if not isinstance(attr, Flatten) else attr return contains(assigned_variable, flat_attr) else: diff --git a/src/krrood/entity_query_language/symbolic.py b/src/krrood/entity_query_language/symbolic.py index 1c8fd77..1d0b239 100644 --- a/src/krrood/entity_query_language/symbolic.py +++ b/src/krrood/entity_query_language/symbolic.py @@ -52,7 +52,7 @@ ) from .rxnode import RWXNode, ColorLegend from .symbol_graph import SymbolGraph -from .utils import IDGenerator, is_iterable, generate_combinations, make_list +from .utils import IDGenerator, is_iterable, generate_combinations, make_list, make_set from ..class_diagrams import ClassRelation from ..class_diagrams.class_diagram import Association, WrappedClass from ..class_diagrams.failures import ClassIsUnMappedInClassDiagram @@ -111,6 +111,12 @@ class OperationResult: def is_true(self): return not self.is_false + @property + def value(self) -> Optional[HashedValue]: + if self.operand._id_ in self: + return self[self.operand._id_] + return None + def __contains__(self, item): return item in self.bindings @@ -1447,9 +1453,14 @@ def _evaluate__( ) def apply_operation(self, operand_values: OperationResult) -> bool: - res = self.operation( - operand_values[self.left._id_].value, operand_values[self.right._id_].value + left_value, right_value = ( + operand_values.bindings[self.left._id_], + operand_values.bindings[self.right._id_], ) + if is_iterable(left_value.value) and is_iterable(right_value.value): + left_value = HashedValue(make_set(left_value.value)) + right_value = HashedValue(make_set(right_value.value)) + res = self.operation(left_value.value, right_value.value) self._is_false_ = not res operand_values[self._id_] = HashedValue(res) return res From cead85eda95ef46944d344643fd219e2b30066cd Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Fri, 28 Nov 2025 11:15:38 +0100 Subject: [PATCH 30/50] [EQL] fixed match notebook. --- examples/eql/match.md | 45 ++++++-------------- src/krrood/entity_query_language/match.py | 2 +- src/krrood/entity_query_language/symbolic.py | 6 ++- 3 files changed, 20 insertions(+), 33 deletions(-) diff --git a/examples/eql/match.md b/examples/eql/match.md index 03e3de3..adaef05 100644 --- a/examples/eql/match.md +++ b/examples/eql/match.md @@ -21,6 +21,7 @@ The following example shows how nested patterns translate into an equivalent manual query built with `entity(...)` and predicates. ```{code-cell} ipython3 +from krrood.entity_query_language.symbol_graph import SymbolGraph from dataclasses import dataclass from typing_extensions import List @@ -65,8 +66,20 @@ class FixedConnection(Connection): @dataclass class World: connections: List[Connection] + +@dataclass +class Drawer(Symbol): + handle: Handle + container: Container +@dataclass +class Cabinet(Symbol): + container: Container + drawers: List[Drawer] + +SymbolGraph() + # Build a small world with a few connections c1 = Container("Container1") h1 = Handle("Handle1") @@ -156,23 +169,8 @@ at least one element of the collection should satisfy the given pattern. Below we add two simple view classes and build a small scene of drawers and a cabinet. ```{code-cell} ipython3 -from dataclasses import dataclass -from typing_extensions import List from krrood.entity_query_language.match import match_any - -@dataclass -class Drawer(Symbol): - handle: Handle - container: Container - - -@dataclass -class Cabinet(Symbol): - container: Container - drawers: List[Drawer] - - # Build a simple set of views drawer1 = Drawer(handle=h1, container=c1) drawer2 = Drawer(handle=Handle("OtherHandle"), container=other_c) @@ -203,7 +201,7 @@ cabinet_query = an(entity_matching(Cabinet, views)(drawers=selected_drawers)) ans = list(cabinet_query.evaluate()) assert len(ans) == 2 -print(ans[0][0].handle.name) +print(ans) ``` ## Selecting inner objects with `select()` @@ -234,23 +232,8 @@ at least one element of the collection should satisfy the given pattern. Below we add two simple view classes and build a small scene of drawers and a cabinet. ```{code-cell} ipython3 -from dataclasses import dataclass -from typing_extensions import List from krrood.entity_query_language.match import match_any - -@dataclass -class Drawer(Symbol): - handle: Handle - container: Container - - -@dataclass -class Cabinet(Symbol): - container: Container - drawers: List[Drawer] - - # Build a simple set of views drawer1 = Drawer(handle=h1, container=c1) drawer2 = Drawer(handle=Handle("OtherHandle"), container=other_c) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index fba0a75..ffc6c22 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -150,7 +150,7 @@ def _update_attribute_and_selected_variables( :param attr_assigned_value: The assigned value of the attribute. :return: The updated attribute.. """ - if attr._is_iterable_: + if attr._is_iterable_ and not attr_assigned_value.existential: attr = flatten(attr) self._update_selected_variables(attr) attr_assigned_value.update_selected_variable(attr) diff --git a/src/krrood/entity_query_language/symbolic.py b/src/krrood/entity_query_language/symbolic.py index 1d0b239..63024b6 100644 --- a/src/krrood/entity_query_language/symbolic.py +++ b/src/krrood/entity_query_language/symbolic.py @@ -1457,7 +1457,11 @@ def apply_operation(self, operand_values: OperationResult) -> bool: operand_values.bindings[self.left._id_], operand_values.bindings[self.right._id_], ) - if is_iterable(left_value.value) and is_iterable(right_value.value): + if ( + self.operation in [operator.eq, operator.ne] + and is_iterable(left_value.value) + and is_iterable(right_value.value) + ): left_value = HashedValue(make_set(left_value.value)) right_value = HashedValue(make_set(right_value.value)) res = self.operation(left_value.value, right_value.value) From e948639291b01eae2f0d5ef9549815200a4673c4 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Fri, 28 Nov 2025 12:54:27 +0100 Subject: [PATCH 31/50] [EQL] fixed match notebook. --- examples/eql/match.md | 7 +- src/krrood/entity_query_language/match.py | 73 ++++++++++---------- src/krrood/entity_query_language/symbolic.py | 13 ++-- 3 files changed, 49 insertions(+), 44 deletions(-) diff --git a/examples/eql/match.md b/examples/eql/match.md index adaef05..e9d39a3 100644 --- a/examples/eql/match.md +++ b/examples/eql/match.md @@ -256,13 +256,14 @@ If you want to retrieve a specific element from a collection attribute while mat It behaves like `match_any(Type)` but also selects the matched element so you can access it in the result. ```{code-cell} ipython3 -from krrood.entity_query_language.match import select_any +from krrood.entity_query_language.match import select_any, entity_selection selected_drawers = select_any([drawer1, drawer2]) # Query: find the cabinet that has any drawer from the set {drawer1, drawer2} -cabinet_query = an(entity_matching(Cabinet, views)(drawers=selected_drawers)) +cabinet = entity_selection(Cabinet, views) +cabinet_query = an(cabinet(drawers=selected_drawers)) ans = list(cabinet_query.evaluate()) assert len(ans) == 2 -print(ans[0][0].handle.name) +print(ans) ``` diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index ffc6c22..94829e0 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -49,6 +49,10 @@ class Match(Generic[T]): """ The type of the variable. """ + domain: DomainType = field(default=None, kw_only=True) + """ + The domain to use for the variable created by the match. + """ kwargs: Dict[str, Any] = field(init=False, default_factory=dict) """ The keyword arguments to match against. @@ -352,7 +356,7 @@ def _get_or_create_variable(self) -> CanBehaveLikeAVariable[T]: """ if self.variable: return self.variable - return let(self.type_, None) + return let(self.type_, self.domain) @cached_property def expression(self) -> QueryObjectDescriptor[T]: @@ -368,26 +372,6 @@ def expression(self) -> QueryObjectDescriptor[T]: return entity(self.selected_variables[0], *self.conditions) -@dataclass -class MatchEntity(Match[T]): - """ - A match that can also take a domain and should be used as the outermost match in a nested match statement. - This is because the inner match statements derive their domain from the outer match as they are basically attributes - of the outer match variable. - """ - - domain: DomainType = None - """ - The domain to use for the variable created by the match. - """ - - def _get_or_create_variable(self) -> Variable[T]: - """ - Create a variable with the given type and domain. - """ - return let(self.type_, self.domain) - - @dataclass class Select(Match[T], Selectable[T]): """ @@ -501,33 +485,48 @@ def select_all( return select_ -def _match_or_select( - match_type: Type[Match], - type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, -) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: +def entity_matching( + type_: Union[Type[T], CanBehaveLikeAVariable[T]], domain: DomainType +) -> Union[Type[T], CanBehaveLikeAVariable[T], MatchEntity[T]]: """ - Create and return a Match/Select instance that looks for the pattern provided by the type and the - keyword arguments. + Same as :py:func:`krrood.entity_query_language.match.match` but with a domain to use for the variable created + by the match. + + :param type_: The type of the variable (i.e., The class you want to instantiate). + :param domain: The domain used for the variable created by the match. + :return: The MatchEntity instance. """ - if isinstance(type_, CanBehaveLikeAVariable): - return Select(type_._type_, variable=type_) - elif type_ and not isinstance(type_, type): - return match_type(type_=type_, variable=Literal(type_)) - return match_type(type_) + return _match_or_select(Match, type_=type_, domain=domain) -def entity_matching( +def entity_selection( type_: Union[Type[T], CanBehaveLikeAVariable[T]], domain: DomainType ) -> Union[Type[T], CanBehaveLikeAVariable[T], MatchEntity[T]]: """ - Same as :py:func:`krrood.entity_query_language.entity.match` but with a domain to use for the variable created - by the match. + Same as :py:func:`krrood.entity_query_language.match.entity_matching` but also selecting the variable to be + included in the result. + """ + return _match_or_select(Select, type_=type_, domain=domain) + +def _match_or_select( + match_type: Type[Match], + type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, + domain: Optional[DomainType] = None, +) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: + """ + Create and return a Match/Select instance that looks for the pattern provided by the type and the + keyword arguments. + + :param match_type: The type of the match to create (Match or Select). :param type_: The type of the variable (i.e., The class you want to instantiate). :param domain: The domain used for the variable created by the match. - :return: The MatchEntity instance. """ - return MatchEntity(type_, domain) + if isinstance(type_, CanBehaveLikeAVariable): + return match_type(type_._type_, domain=domain, variable=type_) + elif type_ and not isinstance(type_, type): + return match_type(type_, domain=domain, variable=Literal(type_)) + return match_type(type_) EntityType = Union[SetOf[T], Entity[T], T, Iterable[T], Type[T], Match[T]] diff --git a/src/krrood/entity_query_language/symbolic.py b/src/krrood/entity_query_language/symbolic.py index 63024b6..27b370c 100644 --- a/src/krrood/entity_query_language/symbolic.py +++ b/src/krrood/entity_query_language/symbolic.py @@ -981,6 +981,11 @@ def _evaluate__( self._eval_parent_ = parent sources = sources or {} if self._id_ in sources: + if ( + isinstance(self._parent_, LogicalBinaryOperator) + or self is self._conditions_root_ + ): + self._is_false_ = not bool(sources[self._id_]) yield OperationResult(sources, not bool(sources[self._id_]), self) elif self._domain_: for v in self._domain_: @@ -1267,7 +1272,7 @@ def _wrapped_owner_class_(self): return None def _apply_mapping_(self, value: HashedValue) -> Iterable[HashedValue]: - yield HashedValue(id_=value.id_, value=getattr(value.value, self._attr_name_)) + yield HashedValue(getattr(value.value, self._attr_name_)) @property def _name_(self): @@ -1791,11 +1796,11 @@ def _evaluate__( ) -> Iterable[OperationResult]: sources = sources or {} self._eval_parent_ = parent - seen_var_values = set() + seen_var_values = [] for val in self.condition._evaluate__(sources, parent=self): var_val = val[self.variable._id_] - if val.is_true and var_val not in seen_var_values: - seen_var_values.add(var_val) + if val.is_true and var_val.value not in seen_var_values: + seen_var_values.append(var_val.value) yield OperationResult(val.bindings, False, self) def __invert__(self): From 84f8a877d2a3ece2c90e8d0d91194ce48749207e Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Fri, 28 Nov 2025 15:26:47 +0100 Subject: [PATCH 32/50] [EQL] fix method doc. --- src/krrood/entity_query_language/match.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 94829e0..8eddef4 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -4,11 +4,8 @@ from functools import cached_property from typing import Generic, Optional, Type, Dict, Any, List, Union, Self, Iterable -from krrood.entity_query_language.entity import for_all from krrood.entity_query_language.symbolic import ForAll, Exists -from .failures import NoneWrappedFieldError -from ..class_diagrams.wrapped_field import WrappedField from .entity import ( ConditionType, contains, @@ -20,6 +17,7 @@ DomainType, exists, ) +from .failures import NoneWrappedFieldError from .hashed_data import T, HashedValue from .predicate import HasType from .symbolic import ( @@ -28,7 +26,6 @@ Comparator, Flatten, QueryObjectDescriptor, - Variable, Selectable, SymbolicExpression, OperationResult, @@ -352,7 +349,8 @@ def _is_type_filter_needed(attr: Attribute, attr_match: Match): def _get_or_create_variable(self) -> CanBehaveLikeAVariable[T]: """ - Create a variable with the given type if + Return the existing variable if it exists; otherwise, create a new variable with the given type and domain, + then return it. """ if self.variable: return self.variable From 8c5365fc257606c3ca46b178c9909308cc879abf Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Fri, 28 Nov 2025 18:21:40 +0100 Subject: [PATCH 33/50] [EQL] review changes. --- examples/eql/match.md | 3 +- src/krrood/class_diagrams/class_diagram.py | 19 +-- src/krrood/entity_query_language/match.py | 143 ++++++++----------- src/krrood/entity_query_language/symbolic.py | 4 +- 4 files changed, 68 insertions(+), 101 deletions(-) diff --git a/examples/eql/match.md b/examples/eql/match.md index e9d39a3..927d56f 100644 --- a/examples/eql/match.md +++ b/examples/eql/match.md @@ -226,7 +226,8 @@ print(answers[container].name, answers[handle].name) ## Existential matches in collections with `match_any()` -When matching a container-like attribute (for example, a list), use `match_any(pattern)` to express that +When having multiple possible matches, and you care only if at least the attribute matches one possibility, use +`match_any(IterableOfPossibleValues)` to express that at least one element of the collection should satisfy the given pattern. Below we add two simple view classes and build a small scene of drawers and a cabinet. diff --git a/src/krrood/class_diagrams/class_diagram.py b/src/krrood/class_diagrams/class_diagram.py index 268e296..1111761 100644 --- a/src/krrood/class_diagrams/class_diagram.py +++ b/src/krrood/class_diagrams/class_diagram.py @@ -481,25 +481,16 @@ class to the wrapped class. :param clazz: The wrapped class object to be added to the dependency graph. """ - clazz = self.ensure_wrapped_class(clazz) + try: + clazz = self.get_wrapped_class(clazz) + except ClassIsUnMappedInClassDiagram: + clazz = WrappedClass(clazz) if clazz.index is not None: - return + raise clazz.index = self._dependency_graph.add_node(clazz) clazz._class_diagram = self self._cls_wrapped_cls_map[clazz.clazz] = clazz - @staticmethod - def ensure_wrapped_class(clazz: Union[Type, WrappedClass]) -> WrappedClass: - """ - Ensure that the clazz is a WrappedClass. - - :param clazz: The class to wrap. - :return: The wrapped class. - """ - if not isinstance(clazz, WrappedClass): - clazz = WrappedClass(clazz) - return clazz - def add_relation(self, relation: ClassRelation): """ Adds a relation to the internal dependency graph. diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 8eddef4..9c8892f 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -4,7 +4,7 @@ from functools import cached_property from typing import Generic, Optional, Type, Dict, Any, List, Union, Self, Iterable -from krrood.entity_query_language.symbolic import ForAll, Exists +from krrood.entity_query_language.symbolic import ForAll, Exists, DomainMapping from .entity import ( ConditionType, @@ -110,52 +110,67 @@ def _resolve( """ self._update_the_match_fields(variable, parent) for attr_name, attr_assigned_value in self.kwargs.items(): - attr = self._get_attribute_and_update_selected_variables( - attr_name, attr_assigned_value - ) + attr = self._get_attribute(attr_name, attr_assigned_value) + if isinstance(attr_assigned_value, Select): + self._update_selected_variables(attr) + attr_assigned_value.update_selected_variable(attr) if self.is_an_unresolved_match(attr_assigned_value): - self._resolve_child_match_and_merge_conditions( + attr = self._apply_needed_filtrations_and_mappings_to_the_attribute( attr, attr_assigned_value ) + attr_assigned_value._resolve(attr, self) + self.conditions.extend(attr_assigned_value.conditions) else: - self._add_proper_conditions_for_an_already_resolved_child_match( + condition = self._get_either_a_containment_or_an_equal_condition( attr, attr_assigned_value ) + if self.is_an_existential_match(attr_assigned_value): + condition = self._wrap_the_condition_in_an_exists_expression( + attr, condition + ) + self.conditions.append(condition) - def _get_attribute_and_update_selected_variables( - self, attr_name: str, attr_assigned_value: Any - ) -> Union[Attribute, Flatten]: + def _apply_needed_filtrations_and_mappings_to_the_attribute( + self, attr: Attribute, attr_assigned_value: Match + ) -> DomainMapping: """ - Get the attribute from the variable and update the selected variables with the attribute. + Apply needed filtrations and mappings to the attribute. This is can be flattening, and/or type filtering. + + :param attr: The attribute to apply the filtrations and mappings to. + :param attr_assigned_value: The assigned value of the attribute which is a Match instance. + :return: The attribute after applying the filtrations and mappings. + """ + type_filter_needed = self._is_type_filter_needed(attr, attr_assigned_value) + attr = self._flatten_attribute_if_needed( + attr, attr_assigned_value, type_filter_needed + ) + if type_filter_needed: + self.conditions.append(HasType(attr, attr_assigned_value.type_)) + return attr + + def _get_attribute(self, attr_name: str, attr_assigned_value: Any) -> Attribute: + """ + Get the attribute from the variable. :param attr_name: The name of the attribute to get. :param attr_assigned_value: The assigned value of the attribute. :return: The attribute. + :raises NoneWrappedFieldError: If the attribute does not have a WrappedField. """ attr: Attribute = getattr(self.variable, attr_name) if not attr._wrapped_field_: raise NoneWrappedFieldError(self.variable._type_, attr_name) - if isinstance(attr_assigned_value, Select): - attr = self._update_attribute_and_selected_variables( - attr, attr_assigned_value - ) return attr - def _update_attribute_and_selected_variables( - self, attr: Attribute, attr_assigned_value: Select - ) -> Union[Attribute, Flatten]: + @staticmethod + def is_an_existential_match(value: Any) -> bool: """ - Update the attribute by flattening it if it is iterable, and update the selected variables with the attribute. + Check whether the given value is an existential match. - :param attr: The attribute to update. - :param attr_assigned_value: The assigned value of the attribute. - :return: The updated attribute.. + :param value: The value to check. + :return: True if the value is an existential Match, else False. """ - if attr._is_iterable_ and not attr_assigned_value.existential: - attr = flatten(attr) - self._update_selected_variables(attr) - attr_assigned_value.update_selected_variable(attr) - return attr + return isinstance(value, Match) and value.existential @staticmethod def is_an_unresolved_match(value: Any) -> bool: @@ -167,25 +182,6 @@ def is_an_unresolved_match(value: Any) -> bool: """ return isinstance(value, Match) and not value.variable - def _resolve_child_match_and_merge_conditions( - self, - attr: Attribute, - attr_assigned_value: Match, - ): - """ - Resolve the child match and merge the conditions with the parent match. - - :param attr: A symbolic attribute of this match variable. - :param attr_assigned_value: The assigned value of the attribute, which is a Match instance. - """ - type_filter_needed = self._is_type_filter_needed(attr, attr_assigned_value) - if attr._is_iterable_ and (attr_assigned_value.kwargs or type_filter_needed): - attr = flatten(attr) - attr_assigned_value._resolve(attr, self) - if type_filter_needed: - self._add_type_filter(attr, attr_assigned_value) - self.conditions.extend(attr_assigned_value.conditions) - def _update_the_match_fields( self, variable: Optional[CanBehaveLikeAVariable] = None, @@ -214,42 +210,20 @@ def _update_selected_variables(self, variable: CanBehaveLikeAVariable): else: self.selected_variables.append(variable) - def _add_proper_conditions_for_an_already_resolved_child_match( - self, - attr: Attribute, - attr_assigned_value: Any, - ): - """ - Add proper conditions for an already resolved child match. These could be an equal, or a containment condition. - - :param attr: A symbolic attribute of this match variable. - :param attr_assigned_value: The assigned value of the attribute, which can be a Match instance. - """ - condition = self._get_either_a_containment_or_an_equal_condition( - attr, attr_assigned_value - ) - condition = self._update_condition_if_existential( - attr, attr_assigned_value, condition - ) - self.conditions.append(condition) - - def _update_condition_if_existential( - self, + @staticmethod + def _wrap_the_condition_in_an_exists_expression( attr: Union[Attribute, Flatten], - attr_assigned_value: Any, condition: Comparator, - ) -> Union[Comparator, Exists, ForAll]: + ) -> Exists: """ - Update the condition depending if it is an existential. + Return an Exists expression wrapping the given condition. :param attr: The attribute on which the condition is applied. :param condition: The condition to update. - :return: The updated condition. + :return: The exists expression. """ - if isinstance(attr_assigned_value, Match) and attr_assigned_value.existential: - attr = attr if not isinstance(attr, Flatten) else attr._child_ - condition = exists(attr, condition) - return condition + attr = attr if not isinstance(attr, Flatten) else attr._child_ + return exists(attr, condition) def _get_either_a_containment_or_an_equal_condition( self, @@ -326,18 +300,21 @@ def _is_iterable_value(value) -> bool: return True return False - def _add_type_filter( - self, - attr: Attribute, - attr_match: Match, - ): + @staticmethod + def _flatten_attribute_if_needed( + attr: Attribute, attr_assigned_value: Match, type_filter_needed: bool + ) -> Union[Attribute, Flatten]: """ - Adds a type filter to the match. + Flatten the attribute if needed. - :param attr: The attribute to filter. - :param attr_match:The Match instance of the attribute. + :param attr: The attribute to check. + :param attr_assigned_value: The assigned value of the attribute which is a Match instance. + :param type_filter_needed: Whether a type filter is needed for the attribute. + :return: True if flattening is needed, else False. """ - self.conditions.append(HasType(attr, attr_match.type_)) + if attr._is_iterable_ and (attr_assigned_value.kwargs or type_filter_needed): + return flatten(attr) + return attr @staticmethod def _is_type_filter_needed(attr: Attribute, attr_match: Match): diff --git a/src/krrood/entity_query_language/symbolic.py b/src/krrood/entity_query_language/symbolic.py index 27b370c..f33b8a3 100644 --- a/src/krrood/entity_query_language/symbolic.py +++ b/src/krrood/entity_query_language/symbolic.py @@ -113,9 +113,7 @@ def is_true(self): @property def value(self) -> Optional[HashedValue]: - if self.operand._id_ in self: - return self[self.operand._id_] - return None + return self.bindings.get(self.operand._id_, None) def __contains__(self, item): return item in self.bindings From 7686f7f4755e0d76a8aa08fa84d2a85a098508ef Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Fri, 28 Nov 2025 18:23:33 +0100 Subject: [PATCH 34/50] [EQL] review changes. --- src/krrood/entity_query_language/match.py | 30 ++--------------------- 1 file changed, 2 insertions(+), 28 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 9c8892f..5544b04 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -244,9 +244,9 @@ def _get_either_a_containment_or_an_equal_condition( else assigned_value ) universal = isinstance(assigned_value, Match) and assigned_value.universal - if self._attribute_is_iterable_while_the_value_is_not(assigned_value, attr): + if attr._is_iterable_ and not self._is_iterable_value(assigned_value): return contains(attr, assigned_variable) - elif self._value_is_iterable_while_the_attribute_is_not(assigned_value, attr): + elif not attr._is_iterable_ and self._is_iterable_value(assigned_value): return in_(attr, assigned_variable) elif ( attr._is_iterable_ @@ -258,32 +258,6 @@ def _get_either_a_containment_or_an_equal_condition( else: return attr == assigned_variable - def _attribute_is_iterable_while_the_value_is_not( - self, - assigned_value: Any, - attr: Union[Flatten, Attribute], - ) -> bool: - """ - Return True if the attribute is iterable while the assigned value is not an iterable. - - :param assigned_value: The value assigned to the attribute. - :param attr: The attribute to check. - """ - return attr._is_iterable_ and not self._is_iterable_value(assigned_value) - - def _value_is_iterable_while_the_attribute_is_not( - self, - assigned_value: Any, - attr: Union[Flatten, Attribute], - ) -> bool: - """ - Return True if the assigned value is iterable while the attribute is not an iterable. - - :param assigned_value: The value assigned to the attribute. - :param attr: The attribute to check. - """ - return not attr._is_iterable_ and self._is_iterable_value(assigned_value) - @staticmethod def _is_iterable_value(value) -> bool: """ From f0c159deeb8f54429facc3e4dbff83f6337362dc Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Fri, 28 Nov 2025 18:27:51 +0100 Subject: [PATCH 35/50] [EQL] review changes. --- src/krrood/entity_query_language/match.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 5544b04..f18e5c9 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -172,6 +172,16 @@ def is_an_existential_match(value: Any) -> bool: """ return isinstance(value, Match) and value.existential + @staticmethod + def is_a_universal_match(value: Any) -> bool: + """ + Check whether the given value is a universal match. + + :param value: The value to check. + :return: True if the value is a universal Match, else False. + """ + return isinstance(value, Match) and value.universal + @staticmethod def is_an_unresolved_match(value: Any) -> bool: """ @@ -243,7 +253,6 @@ def _get_either_a_containment_or_an_equal_condition( if isinstance(assigned_value, Match) else assigned_value ) - universal = isinstance(assigned_value, Match) and assigned_value.universal if attr._is_iterable_ and not self._is_iterable_value(assigned_value): return contains(attr, assigned_variable) elif not attr._is_iterable_ and self._is_iterable_value(assigned_value): @@ -251,7 +260,7 @@ def _get_either_a_containment_or_an_equal_condition( elif ( attr._is_iterable_ and self._is_iterable_value(assigned_value) - and not universal + and not self.is_a_universal_match(assigned_value) ): flat_attr = flatten(attr) if not isinstance(attr, Flatten) else attr return contains(assigned_variable, flat_attr) From e5f68385fe665a0475fe0aa317002c695d003110 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Fri, 28 Nov 2025 19:20:45 +0100 Subject: [PATCH 36/50] [EQL] Created AttributeAssignment class. --- src/krrood/entity_query_language/match.py | 312 +++++++++++----------- 1 file changed, 156 insertions(+), 156 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index f18e5c9..cc702c8 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -110,88 +110,21 @@ def _resolve( """ self._update_the_match_fields(variable, parent) for attr_name, attr_assigned_value in self.kwargs.items(): - attr = self._get_attribute(attr_name, attr_assigned_value) + attr_assignment = AttributeAssignment( + attr_name, self.variable, attr_assigned_value + ) if isinstance(attr_assigned_value, Select): - self._update_selected_variables(attr) - attr_assigned_value.update_selected_variable(attr) - if self.is_an_unresolved_match(attr_assigned_value): - attr = self._apply_needed_filtrations_and_mappings_to_the_attribute( - attr, attr_assigned_value - ) - attr_assigned_value._resolve(attr, self) - self.conditions.extend(attr_assigned_value.conditions) + self._update_selected_variables(attr_assignment.attr) + attr_assigned_value.update_selected_variable(attr_assignment.attr) + if attr_assignment.is_an_unresolved_match: + attr_assignment.resolve(self) + self.conditions.extend(attr_assignment.conditions) else: - condition = self._get_either_a_containment_or_an_equal_condition( - attr, attr_assigned_value + condition = ( + attr_assignment.infer_the_condition_between_the_attribute_and_its_assigned_value() ) - if self.is_an_existential_match(attr_assigned_value): - condition = self._wrap_the_condition_in_an_exists_expression( - attr, condition - ) self.conditions.append(condition) - def _apply_needed_filtrations_and_mappings_to_the_attribute( - self, attr: Attribute, attr_assigned_value: Match - ) -> DomainMapping: - """ - Apply needed filtrations and mappings to the attribute. This is can be flattening, and/or type filtering. - - :param attr: The attribute to apply the filtrations and mappings to. - :param attr_assigned_value: The assigned value of the attribute which is a Match instance. - :return: The attribute after applying the filtrations and mappings. - """ - type_filter_needed = self._is_type_filter_needed(attr, attr_assigned_value) - attr = self._flatten_attribute_if_needed( - attr, attr_assigned_value, type_filter_needed - ) - if type_filter_needed: - self.conditions.append(HasType(attr, attr_assigned_value.type_)) - return attr - - def _get_attribute(self, attr_name: str, attr_assigned_value: Any) -> Attribute: - """ - Get the attribute from the variable. - - :param attr_name: The name of the attribute to get. - :param attr_assigned_value: The assigned value of the attribute. - :return: The attribute. - :raises NoneWrappedFieldError: If the attribute does not have a WrappedField. - """ - attr: Attribute = getattr(self.variable, attr_name) - if not attr._wrapped_field_: - raise NoneWrappedFieldError(self.variable._type_, attr_name) - return attr - - @staticmethod - def is_an_existential_match(value: Any) -> bool: - """ - Check whether the given value is an existential match. - - :param value: The value to check. - :return: True if the value is an existential Match, else False. - """ - return isinstance(value, Match) and value.existential - - @staticmethod - def is_a_universal_match(value: Any) -> bool: - """ - Check whether the given value is a universal match. - - :param value: The value to check. - :return: True if the value is a universal Match, else False. - """ - return isinstance(value, Match) and value.universal - - @staticmethod - def is_an_unresolved_match(value: Any) -> bool: - """ - Check whether the given value is an unresolved Match instance. - - :param value: The value to check. - :return: True if the value is an unresolved Match instance, else False. - """ - return isinstance(value, Match) and not value.variable - def _update_the_match_fields( self, variable: Optional[CanBehaveLikeAVariable] = None, @@ -220,114 +153,181 @@ def _update_selected_variables(self, variable: CanBehaveLikeAVariable): else: self.selected_variables.append(variable) - @staticmethod - def _wrap_the_condition_in_an_exists_expression( - attr: Union[Attribute, Flatten], - condition: Comparator, - ) -> Exists: + def _get_or_create_variable(self) -> CanBehaveLikeAVariable[T]: """ - Return an Exists expression wrapping the given condition. + Return the existing variable if it exists; otherwise, create a new variable with the given type and domain, + then return it. + """ + if self.variable: + return self.variable + return let(self.type_, self.domain) - :param attr: The attribute on which the condition is applied. - :param condition: The condition to update. - :return: The exists expression. + @cached_property + def expression(self) -> QueryObjectDescriptor[T]: """ - attr = attr if not isinstance(attr, Flatten) else attr._child_ - return exists(attr, condition) + Return the entity expression corresponding to the match query. + """ + self._resolve() + if len(self.selected_variables) > 1: + return set_of(self.selected_variables, *self.conditions) + else: + if not self.selected_variables: + self.selected_variables.append(self.variable) + return entity(self.selected_variables[0], *self.conditions) + + +@dataclass +class AttributeAssignment: - def _get_either_a_containment_or_an_equal_condition( + attr_name: str + """ + The name of the attribute to assign the value to. + """ + variable: CanBehaveLikeAVariable + """ + The variable whose attribute is being assigned. + """ + assigned_value: Union[Literal, Match] + """ + The value to assign to the attribute, which can be a Match instance or a Literal. + """ + conditions: List[ConditionType] = field(init=False, default_factory=list) + """ + The conditions that define attribute assignment. + """ + + def resolve(self, parent_match: Match): + """ + Resolve the attribute assignment by creating the conditions and applying the necessary mappings + to the attribute. + + :param parent_match: The parent match of the attribute assignment. + """ + possibly_flattened_attr = self.attr + if self.attr._is_iterable_ and ( + self.assigned_value.kwargs or self.is_type_filter_needed + ): + possibly_flattened_attr = flatten(self.attr) + + self.assigned_value._resolve(possibly_flattened_attr, parent_match) + + if self.is_type_filter_needed: + self.conditions.append( + HasType(possibly_flattened_attr, self.assigned_value.type_) + ) + + self.conditions.extend(self.assigned_value.conditions) + + def infer_the_condition_between_the_attribute_and_its_assigned_value( self, - attr: Attribute, - assigned_value: Any, - ) -> Comparator: + ) -> Union[Comparator, Exists]: """ Find and return the appropriate condition for the attribute and its assigned value. This can be one of contains, - in_, or == depending on the type of the assigned value and the type of the attribute. + in_, or == depending on the type of the assigned value and the type of the attribute. In addition, if the + assigned value is a Match instance with an existential flag set, an Exists expression is created over the + comparator condition. - :param attr: The attribute to check. - :param assigned_value: The value assigned to the attribute. - :return: A comparator expression representing the condition. + :return: A Comparator or an Exists expression representing the condition. """ - assigned_variable = ( - assigned_value.variable - if isinstance(assigned_value, Match) - else assigned_value - ) - if attr._is_iterable_ and not self._is_iterable_value(assigned_value): - return contains(attr, assigned_variable) - elif not attr._is_iterable_ and self._is_iterable_value(assigned_value): - return in_(attr, assigned_variable) + if self.attr._is_iterable_ and not self.is_iterable_value: + condition = contains(self.attr, self.assigned_variable) + elif not self.attr._is_iterable_ and self.is_iterable_value: + condition = in_(self.attr, self.assigned_variable) elif ( - attr._is_iterable_ - and self._is_iterable_value(assigned_value) - and not self.is_a_universal_match(assigned_value) + self.attr._is_iterable_ + and self.is_iterable_value + and not self.is_a_universal_match ): - flat_attr = flatten(attr) if not isinstance(attr, Flatten) else attr - return contains(assigned_variable, flat_attr) + flat_attr = ( + flatten(self.attr) if not isinstance(self.attr, Flatten) else self.attr + ) + condition = contains(self.assigned_variable, flat_attr) else: - return attr == assigned_variable + condition = self.attr == self.assigned_variable - @staticmethod - def _is_iterable_value(value) -> bool: + if self.is_an_existential_match: + attr = ( + self.attr if not isinstance(self.attr, Flatten) else self.attr._child_ + ) + condition = exists(attr, condition) + + return condition + + @cached_property + def assigned_variable(self) -> CanBehaveLikeAVariable: + """ + :return: The symbolic variable representing the assigned value. """ - Whether the value is an iterable or a Match instance with an iterable type. + return ( + self.assigned_value.variable + if isinstance(self.assigned_value, Match) + else self.assigned_value + ) - :param value: The value to check. - :return: True if the value is an iterable or a Match instance with an iterable type, else False. + @cached_property + def attr(self) -> Attribute: """ - if isinstance(value, CanBehaveLikeAVariable): - return value._is_iterable_ - elif not isinstance(value, Match) and is_iterable(value): - return True - elif isinstance(value, Match) and value._is_iterable_value(value.variable): - return True - return False + :return: the attribute of the variable. + :raises NoneWrappedFieldError: If the attribute does not have a WrappedField. + """ + attr: Attribute = getattr(self.variable, self.attr_name) + if not attr._wrapped_field_: + raise NoneWrappedFieldError(self.variable._type_, self.attr_name) + return attr - @staticmethod - def _flatten_attribute_if_needed( - attr: Attribute, attr_assigned_value: Match, type_filter_needed: bool - ) -> Union[Attribute, Flatten]: + @cached_property + def is_an_existential_match(self) -> bool: """ - Flatten the attribute if needed. + :return: True if the value is an existential Match, else False. + """ + return ( + isinstance(self.assigned_value, Match) and self.assigned_value.existential + ) - :param attr: The attribute to check. - :param attr_assigned_value: The assigned value of the attribute which is a Match instance. - :param type_filter_needed: Whether a type filter is needed for the attribute. - :return: True if flattening is needed, else False. + @cached_property + def is_a_universal_match(self) -> bool: """ - if attr._is_iterable_ and (attr_assigned_value.kwargs or type_filter_needed): - return flatten(attr) - return attr + :return: True if the value is a universal Match, else False. + """ + return isinstance(self.assigned_value, Match) and self.assigned_value.universal - @staticmethod - def _is_type_filter_needed(attr: Attribute, attr_match: Match): - attr_type = attr._type_ - return (not attr_type) or ( - (attr_match.type_ and attr_match.type_ is not attr_type) - and issubclass(attr_match.type_, attr_type) + @cached_property + def is_an_unresolved_match(self) -> bool: + """ + :return: True if the value is an unresolved Match instance, else False. + """ + return ( + isinstance(self.assigned_value, Match) and not self.assigned_value.variable ) - def _get_or_create_variable(self) -> CanBehaveLikeAVariable[T]: + @cached_property + def is_iterable_value(self) -> bool: """ - Return the existing variable if it exists; otherwise, create a new variable with the given type and domain, - then return it. + :return: True if the value is an iterable or a Match instance with an iterable type, else False. """ - if self.variable: - return self.variable - return let(self.type_, self.domain) + if isinstance(self.assigned_value, CanBehaveLikeAVariable): + return self.assigned_value._is_iterable_ + elif not isinstance(self.assigned_value, Match) and is_iterable( + self.assigned_value + ): + return True + elif ( + isinstance(self.assigned_value, Match) + and self.assigned_value.variable._is_iterable_ + ): + return True + return False @cached_property - def expression(self) -> QueryObjectDescriptor[T]: + def is_type_filter_needed(self): """ - Return the entity expression corresponding to the match query. + :return: True if a type filter condition is needed for the attribute assignment, else False. """ - self._resolve() - if len(self.selected_variables) > 1: - return set_of(self.selected_variables, *self.conditions) - else: - if not self.selected_variables: - self.selected_variables.append(self.variable) - return entity(self.selected_variables[0], *self.conditions) + attr_type = self.attr._type_ + return (not attr_type) or ( + (self.assigned_value.type_ and self.assigned_value.type_ is not attr_type) + and issubclass(self.assigned_value.type_, attr_type) + ) @dataclass From 11358b3e0df98f21df0ff0f3f02f698b93270315 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Fri, 28 Nov 2025 19:22:07 +0100 Subject: [PATCH 37/50] [EQL] Class doc. --- src/krrood/entity_query_language/match.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index cc702c8..61d7488 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -178,6 +178,9 @@ def expression(self) -> QueryObjectDescriptor[T]: @dataclass class AttributeAssignment: + """ + A class representing an attribute assignment in a Match statement. + """ attr_name: str """ From 1fda11004c3b0bc03ae4d1173b66a82fdc0092c7 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Sat, 29 Nov 2025 11:34:15 +0100 Subject: [PATCH 38/50] [EQLMatch] fix type hints. --- src/krrood/entity_query_language/match.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 61d7488..6c6a25b 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -4,7 +4,7 @@ from functools import cached_property from typing import Generic, Optional, Type, Dict, Any, List, Union, Self, Iterable -from krrood.entity_query_language.symbolic import ForAll, Exists, DomainMapping +from krrood.entity_query_language.symbolic import Exists from .entity import ( ConditionType, @@ -448,7 +448,7 @@ def select_all( def entity_matching( type_: Union[Type[T], CanBehaveLikeAVariable[T]], domain: DomainType -) -> Union[Type[T], CanBehaveLikeAVariable[T], MatchEntity[T]]: +) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: """ Same as :py:func:`krrood.entity_query_language.match.match` but with a domain to use for the variable created by the match. @@ -462,7 +462,7 @@ def entity_matching( def entity_selection( type_: Union[Type[T], CanBehaveLikeAVariable[T]], domain: DomainType -) -> Union[Type[T], CanBehaveLikeAVariable[T], MatchEntity[T]]: +) -> Union[Type[T], CanBehaveLikeAVariable[T], Select[T]]: """ Same as :py:func:`krrood.entity_query_language.match.entity_matching` but also selecting the variable to be included in the result. From 6d9582e7baa4de87798ab7907daeeca334b871bb Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Sat, 29 Nov 2025 12:09:57 +0100 Subject: [PATCH 39/50] [EQLMatch] doc fix. --- examples/eql/result_quantifiers.md | 2 +- examples/eql/writing_queries.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/eql/result_quantifiers.md b/examples/eql/result_quantifiers.md index 8bc7894..3b74f4a 100644 --- a/examples/eql/result_quantifiers.md +++ b/examples/eql/result_quantifiers.md @@ -82,7 +82,7 @@ Below we reuse the same `World` and `Body` setup from above. The world contains exactly two bodies, so all the following examples will evaluate successfully. ```{code-cell} ipython3 -# Require at least two results +# Require at least one result query = an( entity(body := let(Body, domain=world.bodies)), quantification=AtLeast(1), diff --git a/examples/eql/writing_queries.md b/examples/eql/writing_queries.md index 5af841f..2613cd2 100644 --- a/examples/eql/writing_queries.md +++ b/examples/eql/writing_queries.md @@ -35,7 +35,7 @@ This approach ensures that your class definitions remain pure and decoupled from outside the explicit symbolic context. Consequently, your classes can focus exclusively on their domain logic, leading to better adherence to the [Single Responsibility Principle](https://realpython.com/solid-principles-python/#single-responsibility-principle-srp). -Here is a query that does work due to the missing `let` statement: +Here is a query example that finds all bodies in a world whose name starts with "B": ```{code-cell} ipython3 from dataclasses import dataclass From 144ff40a40b8ca58422420f6c8edb26b6f8d659e Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Sat, 29 Nov 2025 15:06:30 +0100 Subject: [PATCH 40/50] [EQLMatch] fix selection. --- src/krrood/entity_query_language/match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 6c6a25b..e792371 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -150,7 +150,7 @@ def _update_selected_variables(self, variable: CanBehaveLikeAVariable): """ if self.parent: self.parent._update_selected_variables(variable) - else: + elif hash(variable) not in [hash(sv) for sv in self.selected_variables]: self.selected_variables.append(variable) def _get_or_create_variable(self) -> CanBehaveLikeAVariable[T]: From 3a3e13889b0092a73dc60adc44f395301efde83d Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Sat, 29 Nov 2025 16:57:38 +0100 Subject: [PATCH 41/50] [EQLMatch] us normal in. --- src/krrood/entity_query_language/match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index e792371..030dfa1 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -150,7 +150,7 @@ def _update_selected_variables(self, variable: CanBehaveLikeAVariable): """ if self.parent: self.parent._update_selected_variables(variable) - elif hash(variable) not in [hash(sv) for sv in self.selected_variables]: + elif variable not in self.selected_variables: self.selected_variables.append(variable) def _get_or_create_variable(self) -> CanBehaveLikeAVariable[T]: From 2f72c1df631347e934af0689aec64432429349c2 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Sat, 29 Nov 2025 17:30:13 +0100 Subject: [PATCH 42/50] [EQLMatch] compare variables using hash to avoid symbolic comparison. --- src/krrood/entity_query_language/match.py | 2 +- test/conftest.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 030dfa1..98fdadf 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -150,7 +150,7 @@ def _update_selected_variables(self, variable: CanBehaveLikeAVariable): """ if self.parent: self.parent._update_selected_variables(variable) - elif variable not in self.selected_variables: + elif hash(variable) not in map(hash, self.selected_variables): self.selected_variables.append(variable) def _get_or_create_variable(self) -> CanBehaveLikeAVariable[T]: diff --git a/test/conftest.py b/test/conftest.py index 9814a75..fedeaaa 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -108,7 +108,8 @@ def pytest_configure(config): def pytest_sessionstart(session): try: - generate_sqlalchemy_interface() + # generate_sqlalchemy_interface() + pass except Exception as e: import warnings From 591faa08b75c381dd351dc117a22f4d54c4e6dd6 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Sat, 29 Nov 2025 17:30:51 +0100 Subject: [PATCH 43/50] [EQLMatch] compare variables using hash to avoid symbolic comparison. --- test/conftest.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/conftest.py b/test/conftest.py index fedeaaa..9814a75 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -108,8 +108,7 @@ def pytest_configure(config): def pytest_sessionstart(session): try: - # generate_sqlalchemy_interface() - pass + generate_sqlalchemy_interface() except Exception as e: import warnings From b5b941824c36ad9edd58c3c5d8403a6cc45e5a9f Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Sun, 30 Nov 2025 22:53:25 +0100 Subject: [PATCH 44/50] [EQLFeatures] review changes. --- src/krrood/entity_query_language/match.py | 56 ++++++++++---------- src/krrood/entity_query_language/symbolic.py | 24 ++++----- 2 files changed, 38 insertions(+), 42 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 98fdadf..d474cc3 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -108,24 +108,24 @@ def _resolve( :param parent: The parent match if this is a nested match. :return: """ - self._update_the_match_fields(variable, parent) + self._update_fields(variable, parent) for attr_name, attr_assigned_value in self.kwargs.items(): attr_assignment = AttributeAssignment( attr_name, self.variable, attr_assigned_value ) if isinstance(attr_assigned_value, Select): self._update_selected_variables(attr_assignment.attr) - attr_assigned_value.update_selected_variable(attr_assignment.attr) + attr_assigned_value._var_ = attr_assignment.attr if attr_assignment.is_an_unresolved_match: attr_assignment.resolve(self) self.conditions.extend(attr_assignment.conditions) else: condition = ( - attr_assignment.infer_the_condition_between_the_attribute_and_its_assigned_value() + attr_assignment.infer_condition_between_attribute_and_assigned_value() ) self.conditions.append(condition) - def _update_the_match_fields( + def _update_fields( self, variable: Optional[CanBehaveLikeAVariable] = None, parent: Optional[Match] = None, @@ -137,10 +137,17 @@ def _update_the_match_fields( If None, a new variable will be created. :param parent: The parent match if this is a nested match. """ - self.variable = variable if variable else self._get_or_create_variable() + + if variable is not None: + self.variable = variable + elif self.variable is None: + self.variable = let(self.type_, self.domain) + self.parent = parent + if self.is_selected: self._update_selected_variables(self.variable) + if not self.type_: self.type_ = self.variable._type_ @@ -153,15 +160,6 @@ def _update_selected_variables(self, variable: CanBehaveLikeAVariable): elif hash(variable) not in map(hash, self.selected_variables): self.selected_variables.append(variable) - def _get_or_create_variable(self) -> CanBehaveLikeAVariable[T]: - """ - Return the existing variable if it exists; otherwise, create a new variable with the given type and domain, - then return it. - """ - if self.variable: - return self.variable - return let(self.type_, self.domain) - @cached_property def expression(self) -> QueryObjectDescriptor[T]: """ @@ -221,7 +219,7 @@ def resolve(self, parent_match: Match): self.conditions.extend(self.assigned_value.conditions) - def infer_the_condition_between_the_attribute_and_its_assigned_value( + def infer_condition_between_attribute_and_assigned_value( self, ) -> Union[Comparator, Exists]: """ @@ -294,7 +292,7 @@ def is_a_universal_match(self) -> bool: """ return isinstance(self.assigned_value, Match) and self.assigned_value.universal - @cached_property + @property def is_an_unresolved_match(self) -> bool: """ :return: True if the value is an unresolved Match instance, else False. @@ -356,13 +354,7 @@ def _resolve( ): super()._resolve(variable, parent) if not self._var_: - self.update_selected_variable(self.variable) - - def update_selected_variable(self, variable: CanBehaveLikeAVariable): - """ - Update the selected variable with the given one. - """ - self._var_ = variable + self._var_ = variable def _evaluate__( self, @@ -390,7 +382,7 @@ def match( :param type_: The type of the variable (i.e., The class you want to instantiate). :return: The Match instance. """ - return _match_or_select(Match, type_) + return entity_matching(type_, None) def match_any( @@ -421,7 +413,7 @@ def select( """ Equivalent to match(type_) and selecting the variable to be included in the result. """ - return _match_or_select(Select, type_) + return entity_selection(type_, None) def select_any( @@ -457,7 +449,11 @@ def entity_matching( :param domain: The domain used for the variable created by the match. :return: The MatchEntity instance. """ - return _match_or_select(Match, type_=type_, domain=domain) + if isinstance(type_, CanBehaveLikeAVariable): + return Match(type_._type_, domain=domain, variable=type_) + elif type_ and not isinstance(type_, type): + return Match(type_, domain=domain, variable=Literal(type_)) + return Match(type_, domain=domain) def entity_selection( @@ -467,7 +463,11 @@ def entity_selection( Same as :py:func:`krrood.entity_query_language.match.entity_matching` but also selecting the variable to be included in the result. """ - return _match_or_select(Select, type_=type_, domain=domain) + if isinstance(type_, CanBehaveLikeAVariable): + return Select(type_._type_, domain=domain, variable=type_) + elif type_ and not isinstance(type_, type): + return Select(type_, domain=domain, variable=Literal(type_)) + return Select(type_, domain=domain) def _match_or_select( @@ -487,7 +487,7 @@ def _match_or_select( return match_type(type_._type_, domain=domain, variable=type_) elif type_ and not isinstance(type_, type): return match_type(type_, domain=domain, variable=Literal(type_)) - return match_type(type_) + return match_type(type_, domain=domain) EntityType = Union[SetOf[T], Entity[T], T, Iterable[T], Type[T], Match[T]] diff --git a/src/krrood/entity_query_language/symbolic.py b/src/krrood/entity_query_language/symbolic.py index f63a062..aac3e55 100644 --- a/src/krrood/entity_query_language/symbolic.py +++ b/src/krrood/entity_query_language/symbolic.py @@ -1045,9 +1045,7 @@ def _all_variable_instances_(self) -> List[Variable]: @property def _is_iterable_(self): - if self._domain_: - return next(iter(self._domain_), None) is not None - return False + return bool(self._domain_) @property def _plot_color_(self) -> ColorLegend: @@ -1209,9 +1207,9 @@ def _relation_(self): @property def _is_iterable_(self): - if self._wrapped_field_: - return self._wrapped_field_.is_iterable - return False + if not self._wrapped_field_: + return False + return self._wrapped_field_.is_iterable @cached_property def _wrapped_type_(self): @@ -1251,11 +1249,11 @@ def _type_(self) -> Optional[Type]: @cached_property def _wrapped_field_(self) -> Optional[WrappedField]: - if self._wrapped_owner_class_ is not None: - return self._wrapped_owner_class_._wrapped_field_name_map_.get( - self._attr_name_, None - ) - return None + if self._wrapped_owner_class_ is None: + return None + return self._wrapped_owner_class_._wrapped_field_name_map_.get( + self._attr_name_, None + ) @cached_property def _wrapped_owner_class_(self): @@ -1340,9 +1338,7 @@ def _name_(self): @property def _is_iterable_(self): """ - Whether the selectable is iterable. - - :return: True if the selectable is iterable, False otherwise. + :return: False as Flatten does not preserve the original iterable structure. """ return False From 32d029ac7fdf75b94b9170eee3550bde8557fcc4 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Mon, 1 Dec 2025 08:59:50 +0100 Subject: [PATCH 45/50] [EQLMatch] removed unused method. --- src/krrood/entity_query_language/match.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index d474cc3..b891a29 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -470,26 +470,6 @@ def entity_selection( return Select(type_, domain=domain) -def _match_or_select( - match_type: Type[Match], - type_: Union[Type[T], CanBehaveLikeAVariable[T], Any, None] = None, - domain: Optional[DomainType] = None, -) -> Union[Type[T], CanBehaveLikeAVariable[T], Match[T]]: - """ - Create and return a Match/Select instance that looks for the pattern provided by the type and the - keyword arguments. - - :param match_type: The type of the match to create (Match or Select). - :param type_: The type of the variable (i.e., The class you want to instantiate). - :param domain: The domain used for the variable created by the match. - """ - if isinstance(type_, CanBehaveLikeAVariable): - return match_type(type_._type_, domain=domain, variable=type_) - elif type_ and not isinstance(type_, type): - return match_type(type_, domain=domain, variable=Literal(type_)) - return match_type(type_, domain=domain) - - EntityType = Union[SetOf[T], Entity[T], T, Iterable[T], Type[T], Match[T]] """ The possible types for entities. From d620f5241f6d2a687fbb66dcf58a833b53497ee7 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Mon, 1 Dec 2025 23:03:21 +0100 Subject: [PATCH 46/50] [EQLMatch] review changes. --- src/krrood/class_diagrams/class_diagram.py | 2 +- src/krrood/entity_query_language/match.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/krrood/class_diagrams/class_diagram.py b/src/krrood/class_diagrams/class_diagram.py index 1111761..3fef988 100644 --- a/src/krrood/class_diagrams/class_diagram.py +++ b/src/krrood/class_diagrams/class_diagram.py @@ -486,7 +486,7 @@ class to the wrapped class. except ClassIsUnMappedInClassDiagram: clazz = WrappedClass(clazz) if clazz.index is not None: - raise + return clazz.index = self._dependency_graph.add_node(clazz) clazz._class_diagram = self self._cls_wrapped_cls_map[clazz.clazz] = clazz diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index b891a29..498d7a7 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -40,6 +40,14 @@ class Match(Generic[T]): """ Construct a query that looks for the pattern provided by the type and the keyword arguments. + Example usage where we look for an object of type Drawer with body of type Body that has the name"drawer_1": + >>> @dataclass + >>> class Body: + >>> name: str + >>> @dataclass + >>> class Drawer: + >>> body: Body + >>> drawer = match(Drawer)(body=match(Body)(name="drawer_1")) """ type_: Optional[Type[T]] = None From bf2e0c910c1dc650a044d83b66265b1341049dc5 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Mon, 1 Dec 2025 23:05:01 +0100 Subject: [PATCH 47/50] [EQLMatch] review changes. --- src/krrood/entity_query_language/utils.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/krrood/entity_query_language/utils.py b/src/krrood/entity_query_language/utils.py index e46072c..acd7e8b 100644 --- a/src/krrood/entity_query_language/utils.py +++ b/src/krrood/entity_query_language/utils.py @@ -142,17 +142,6 @@ def is_iterable(obj: Any) -> bool: ) -def is_iterable_type(obj: Type) -> bool: - """ - Check if an object type is iterable. - - :param obj: The object to check. - """ - return hasattr(obj, "__iter__") and not issubclass( - obj, (str, type, bytes, bytearray) - ) - - def make_tuple(value: Any) -> Any: """ Make a tuple from a value. From d497b4132f10428a9a711283285f6984a20aa220 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Tue, 2 Dec 2025 00:17:02 +0100 Subject: [PATCH 48/50] [EQLMatch] fix None instances problem. --- src/krrood/entity_query_language/symbol_graph.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/krrood/entity_query_language/symbol_graph.py b/src/krrood/entity_query_language/symbol_graph.py index 5c71867..5946883 100644 --- a/src/krrood/entity_query_language/symbol_graph.py +++ b/src/krrood/entity_query_language/symbol_graph.py @@ -1,5 +1,6 @@ from __future__ import annotations +from copy import copy import os import weakref from collections import defaultdict @@ -137,13 +138,14 @@ def color(self) -> str: return "red" if self.inferred else "black" def __eq__(self, other): - return self.instance == other.instance + return ( + self.instance == other.instance + if self.instance is not None and other.instance is not None + else False + ) def __hash__(self): - if self.instance: - return hash(self.instance) - else: - return id(self.instance) + return id(self.instance) @dataclass @@ -227,7 +229,7 @@ def remove_node(self, wrapped_instance: WrappedInstance): """ self._instance_index.pop(id(wrapped_instance.instance), None) self._class_to_wrapped_instances[wrapped_instance.instance_type].remove( - wrapped_instance, + wrapped_instance ) self._instance_graph.remove_node(wrapped_instance.index) @@ -246,7 +248,7 @@ def get_instances_of_type(self, type_: Type[Symbol]) -> Iterable[Symbol]: yield from ( instance.instance for cls in [type_] + recursive_subclasses(type_) - for instance in self._class_to_wrapped_instances[cls] + for instance in list(self._class_to_wrapped_instances[cls]) ) def get_wrapped_instance(self, instance: Any) -> Optional[WrappedInstance]: From c63d0379f25be60fd693beda4446c8fe6843e0c0 Mon Sep 17 00:00:00 2001 From: LucaKro Date: Tue, 2 Dec 2025 00:43:50 +0100 Subject: [PATCH 49/50] removed is_universal_match and is_existential_match --- src/krrood/entity_query_language/match.py | 22 ++++------------------ 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 498d7a7..46030b1 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -245,7 +245,9 @@ def infer_condition_between_attribute_and_assigned_value( elif ( self.attr._is_iterable_ and self.is_iterable_value - and not self.is_a_universal_match + and not ( + isinstance(self.assigned_value, Match) and self.assigned_value.universal + ) ): flat_attr = ( flatten(self.attr) if not isinstance(self.attr, Flatten) else self.attr @@ -254,7 +256,7 @@ def infer_condition_between_attribute_and_assigned_value( else: condition = self.attr == self.assigned_variable - if self.is_an_existential_match: + if isinstance(self.assigned_value, Match) and self.assigned_value.existential: attr = ( self.attr if not isinstance(self.attr, Flatten) else self.attr._child_ ) @@ -284,22 +286,6 @@ def attr(self) -> Attribute: raise NoneWrappedFieldError(self.variable._type_, self.attr_name) return attr - @cached_property - def is_an_existential_match(self) -> bool: - """ - :return: True if the value is an existential Match, else False. - """ - return ( - isinstance(self.assigned_value, Match) and self.assigned_value.existential - ) - - @cached_property - def is_a_universal_match(self) -> bool: - """ - :return: True if the value is a universal Match, else False. - """ - return isinstance(self.assigned_value, Match) and self.assigned_value.universal - @property def is_an_unresolved_match(self) -> bool: """ From a78c6d47c1adadb6a675048b57226203baefecf2 Mon Sep 17 00:00:00 2001 From: AbdelrhmanBassiouny Date: Tue, 2 Dec 2025 10:25:40 +0100 Subject: [PATCH 50/50] [EQLMatch] fix and cleaning --- src/krrood/entity_query_language/match.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/krrood/entity_query_language/match.py b/src/krrood/entity_query_language/match.py index 46030b1..67088eb 100644 --- a/src/krrood/entity_query_language/match.py +++ b/src/krrood/entity_query_language/match.py @@ -249,18 +249,12 @@ def infer_condition_between_attribute_and_assigned_value( isinstance(self.assigned_value, Match) and self.assigned_value.universal ) ): - flat_attr = ( - flatten(self.attr) if not isinstance(self.attr, Flatten) else self.attr - ) - condition = contains(self.assigned_variable, flat_attr) + condition = contains(self.assigned_variable, flatten(self.attr)) else: condition = self.attr == self.assigned_variable if isinstance(self.assigned_value, Match) and self.assigned_value.existential: - attr = ( - self.attr if not isinstance(self.attr, Flatten) else self.attr._child_ - ) - condition = exists(attr, condition) + condition = exists(self.attr, condition) return condition @@ -347,6 +341,7 @@ def _resolve( parent: Optional[Match] = None, ): super()._resolve(variable, parent) + variable = variable or self.variable if not self._var_: self._var_ = variable