Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement #820: Add support for INCLUDES/INCLUDES_ALL/INCLUDES_ANY operators in cypher filters #821

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Next Next commit
feat: Add support for INCLUDES/INCLUDES_ALL/INCLUDES_ANY operator in …
…cypher filters
wbenbihi committed Jul 24, 2024
commit c25568ff11423633b0d7fef2ea085dce89fe823f
98 changes: 98 additions & 0 deletions neomodel/async_/match.py
Original file line number Diff line number Diff line change
@@ -154,6 +154,9 @@ def _rel_merge_helper(
_SPECIAL_OPERATOR_ISNULL = "IS NULL"
_SPECIAL_OPERATOR_ISNOTNULL = "IS NOT NULL"
_SPECIAL_OPERATOR_REGEX = "=~"
_SPECIAL_OPERATOR_INCLUDES = "{val} IN {ident}.{prop}"
_SPECIAL_OPERATOR_INCLUDES_ALL = "all(x IN {val} WHERE x IN {ident}.{prop})"
_SPECIAL_OPERATOR_INCLUDES_ANY = "any(x IN {val} WHERE x IN {ident}.{prop})"

_UNARY_OPERATORS = (_SPECIAL_OPERATOR_ISNULL, _SPECIAL_OPERATOR_ISNOTNULL)

@@ -190,6 +193,9 @@ def _rel_merge_helper(
"isnull": _SPECIAL_OPERATOR_ISNULL,
"regex": _SPECIAL_OPERATOR_REGEX,
"exact": "=",
"includes": _SPECIAL_OPERATOR_INCLUDES,
"includes_all": _SPECIAL_OPERATOR_INCLUDES_ALL,
"includes_any": _SPECIAL_OPERATOR_INCLUDES_ANY,
}
# add all regex operators
OPERATOR_TABLE.update(_REGEX_OPERATOR_TABLE)
@@ -256,6 +262,62 @@ def process_filter_args(cls, kwargs):
return output


def transform_includes_operator_to_filter(
operator, filter_key, filter_value, property_obj
):
"""
Transform includes operator to a cypher filter
Args:
operator (str): operator to transform
filter_key (str): filter key
filter_value (str): filter value
property_obj (object): property object
Returns:
tuple: operator, deflated_value
"""
if not isinstance(filter_value, str):
raise ValueError(
f"Value must be a string for INCLUDES operation {filter_key}={filter_value}"
)
if not isinstance(property_obj, ArrayProperty):
raise ValueError(
f"Property {filter_key} must be an ArrayProperty to use INCLUDES operation"
)
deflated_value = filter_value
operator = _SPECIAL_OPERATOR_INCLUDES
return operator, deflated_value


def transform_includes_all_any_operator_to_filter(
operator, filter_key, filter_value, property_obj
):
"""
Transform includes operator to a cypher filter
Args:
operator (str): operator to transform
filter_key (str): filter key
filter_value (str): filter value
property_obj (object): property object
Returns:
tuple: operator, deflated_value
"""
if not isinstance(filter_value, (tuple, list)):
raise ValueError(
f"Value must be an iterable for INCLUDES operation {filter_key}={filter_value}"
)
if not isinstance(property_obj, ArrayProperty):
raise ValueError(
f"Property {filter_key} must be an ArrayProperty to use INCLUDES operation"
)
deflated_value = property_obj.deflate(filter_value)
operator = (
_SPECIAL_OPERATOR_INCLUDES_ANY
if operator == _SPECIAL_OPERATOR_INCLUDES_ANY
else _SPECIAL_OPERATOR_INCLUDES_ALL
)
return operator, deflated_value


def transform_in_operator_to_filter(operator, filter_key, filter_value, property_obj):
"""
Transform in operator to a cypher filter
@@ -339,6 +401,20 @@ def transform_operator_to_filter(operator, filter_key, filter_value, property_ob
filter_value=filter_value,
property_obj=property_obj,
)
elif operator == _SPECIAL_OPERATOR_INCLUDES:
operator, deflated_value = transform_includes_operator_to_filter(
operator=operator,
filter_key=filter_key,
filter_value=filter_value,
property_obj=property_obj,
)
elif operator in [_SPECIAL_OPERATOR_INCLUDES_ALL, _SPECIAL_OPERATOR_INCLUDES_ANY]:
operator, deflated_value = transform_includes_all_any_operator_to_filter(
operator=operator,
filter_key=filter_key,
filter_value=filter_value,
property_obj=property_obj,
)
else:
deflated_value = property_obj.deflate(filter_value)

@@ -640,6 +716,16 @@ def _parse_q_filters(self, ident, q, source_class):
prop=prop,
val=f"${place_holder}",
)
elif operator in [
_SPECIAL_OPERATOR_INCLUDES,
_SPECIAL_OPERATOR_INCLUDES_ALL,
_SPECIAL_OPERATOR_INCLUDES_ANY,
]:
statement = operator.format(
ident=ident,
prop=prop,
val=f"${place_holder}",
)
else:
statement = f"{ident}.{prop} {operator} ${place_holder}"
self._query_params[place_holder] = val
@@ -674,6 +760,18 @@ def build_where_stmt(self, ident, filters, q_filters=None, source_class=None):
statement = (
f"{'NOT' if negate else ''} {ident}.{prop} {operator}"
)
elif operator in [
_SPECIAL_OPERATOR_INCLUDES,
_SPECIAL_OPERATOR_INCLUDES_ALL,
_SPECIAL_OPERATOR_INCLUDES_ANY,
]:
place_holder = self._register_place_holder(ident + "_" + prop)
self._query_params[place_holder] = val
statement = operator.format(
ident=ident,
prop=prop,
val=f"${place_holder}",
)
else:
place_holder = self._register_place_holder(ident + "_" + prop)
statement = f"{'NOT' if negate else ''} {ident}.{prop} {operator} ${place_holder}"
98 changes: 98 additions & 0 deletions neomodel/sync_/match.py
Original file line number Diff line number Diff line change
@@ -154,6 +154,9 @@ def _rel_merge_helper(
_SPECIAL_OPERATOR_ISNULL = "IS NULL"
_SPECIAL_OPERATOR_ISNOTNULL = "IS NOT NULL"
_SPECIAL_OPERATOR_REGEX = "=~"
_SPECIAL_OPERATOR_INCLUDES = "{val} IN {ident}.{prop}"
_SPECIAL_OPERATOR_INCLUDES_ALL = "all(x IN {val} WHERE x IN {ident}.{prop})"
_SPECIAL_OPERATOR_INCLUDES_ANY = "any(x IN {val} WHERE x IN {ident}.{prop})"

_UNARY_OPERATORS = (_SPECIAL_OPERATOR_ISNULL, _SPECIAL_OPERATOR_ISNOTNULL)

@@ -190,6 +193,9 @@ def _rel_merge_helper(
"isnull": _SPECIAL_OPERATOR_ISNULL,
"regex": _SPECIAL_OPERATOR_REGEX,
"exact": "=",
"includes": _SPECIAL_OPERATOR_INCLUDES,
"includes_all": _SPECIAL_OPERATOR_INCLUDES_ALL,
"includes_any": _SPECIAL_OPERATOR_INCLUDES_ANY,
}
# add all regex operators
OPERATOR_TABLE.update(_REGEX_OPERATOR_TABLE)
@@ -256,6 +262,62 @@ def process_filter_args(cls, kwargs):
return output


def transform_includes_operator_to_filter(
operator, filter_key, filter_value, property_obj
):
"""
Transform includes operator to a cypher filter
Args:
operator (str): operator to transform
filter_key (str): filter key
filter_value (str): filter value
property_obj (object): property object
Returns:
tuple: operator, deflated_value
"""
if not isinstance(filter_value, str):
raise ValueError(
f"Value must be a string for INCLUDES operation {filter_key}={filter_value}"
)
if not isinstance(property_obj, ArrayProperty):
raise ValueError(
f"Property {filter_key} must be an ArrayProperty to use INCLUDES operation"
)
deflated_value = filter_value
operator = _SPECIAL_OPERATOR_INCLUDES
return operator, deflated_value


def transform_includes_all_any_operator_to_filter(
operator, filter_key, filter_value, property_obj
):
"""
Transform includes operator to a cypher filter
Args:
operator (str): operator to transform
filter_key (str): filter key
filter_value (str): filter value
property_obj (object): property object
Returns:
tuple: operator, deflated_value
"""
if not isinstance(filter_value, (tuple, list)):
raise ValueError(
f"Value must be an iterable for INCLUDES operation {filter_key}={filter_value}"
)
if not isinstance(property_obj, ArrayProperty):
raise ValueError(
f"Property {filter_key} must be an ArrayProperty to use INCLUDES operation"
)
deflated_value = property_obj.deflate(filter_value)
operator = (
_SPECIAL_OPERATOR_INCLUDES_ANY
if operator == _SPECIAL_OPERATOR_INCLUDES_ANY
else _SPECIAL_OPERATOR_INCLUDES_ALL
)
return operator, deflated_value


def transform_in_operator_to_filter(operator, filter_key, filter_value, property_obj):
"""
Transform in operator to a cypher filter
@@ -339,6 +401,20 @@ def transform_operator_to_filter(operator, filter_key, filter_value, property_ob
filter_value=filter_value,
property_obj=property_obj,
)
elif operator == _SPECIAL_OPERATOR_INCLUDES:
operator, deflated_value = transform_includes_operator_to_filter(
operator=operator,
filter_key=filter_key,
filter_value=filter_value,
property_obj=property_obj,
)
elif operator in [_SPECIAL_OPERATOR_INCLUDES_ALL, _SPECIAL_OPERATOR_INCLUDES_ANY]:
operator, deflated_value = transform_includes_all_any_operator_to_filter(
operator=operator,
filter_key=filter_key,
filter_value=filter_value,
property_obj=property_obj,
)
else:
deflated_value = property_obj.deflate(filter_value)

@@ -640,6 +716,16 @@ def _parse_q_filters(self, ident, q, source_class):
prop=prop,
val=f"${place_holder}",
)
elif operator in [
_SPECIAL_OPERATOR_INCLUDES,
_SPECIAL_OPERATOR_INCLUDES_ALL,
_SPECIAL_OPERATOR_INCLUDES_ANY,
]:
statement = operator.format(
ident=ident,
prop=prop,
val=f"${place_holder}",
)
else:
statement = f"{ident}.{prop} {operator} ${place_holder}"
self._query_params[place_holder] = val
@@ -674,6 +760,18 @@ def build_where_stmt(self, ident, filters, q_filters=None, source_class=None):
statement = (
f"{'NOT' if negate else ''} {ident}.{prop} {operator}"
)
elif operator in [
_SPECIAL_OPERATOR_INCLUDES,
_SPECIAL_OPERATOR_INCLUDES_ALL,
_SPECIAL_OPERATOR_INCLUDES_ANY,
]:
place_holder = self._register_place_holder(ident + "_" + prop)
self._query_params[place_holder] = val
statement = operator.format(
ident=ident,
prop=prop,
val=f"${place_holder}",
)
else:
place_holder = self._register_place_holder(ident + "_" + prop)
statement = f"{'NOT' if negate else ''} {ident}.{prop} {operator} ${place_holder}"