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
129 changes: 107 additions & 22 deletions neomodel/async_/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -256,6 +262,61 @@ 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
return operator, deflated_value


def transform_includes_all_any_operator_to_filter(
operator, filter_key, filter_value, property_obj
):
"""
Transform includes__all/any 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)
selected_operator = (
_SPECIAL_OPERATOR_INCLUDES_ANY
if operator == _SPECIAL_OPERATOR_INCLUDES_ANY
else _SPECIAL_OPERATOR_INCLUDES_ALL
)
return selected_operator, deflated_value


def transform_in_operator_to_filter(operator, filter_key, filter_value, property_obj):
"""
Transform in operator to a cypher filter
Expand All @@ -280,7 +341,7 @@ def transform_in_operator_to_filter(operator, filter_key, filter_value, property
return operator, deflated_value


def transform_null_operator_to_filter(filter_key, filter_value):
def transform_null_operator_to_filter(filter_key, filter_value, **kwargs):
"""
Transform null operator to a cypher filter
Args:
Expand Down Expand Up @@ -320,28 +381,29 @@ def transform_regex_operator_to_filter(
return operator, deflated_value


TRANSFORM_TABLE = {
(_SPECIAL_OPERATOR_IN,): transform_in_operator_to_filter,
(_SPECIAL_OPERATOR_ISNULL,): transform_null_operator_to_filter,
tuple(_REGEX_OPERATOR_TABLE.values()): transform_regex_operator_to_filter,
(_SPECIAL_OPERATOR_INCLUDES,): transform_includes_operator_to_filter,
(
_SPECIAL_OPERATOR_INCLUDES_ALL,
_SPECIAL_OPERATOR_INCLUDES_ANY,
): transform_includes_all_any_operator_to_filter,
}


def transform_operator_to_filter(operator, filter_key, filter_value, property_obj):
if operator == _SPECIAL_OPERATOR_IN:
operator, deflated_value = transform_in_operator_to_filter(
operator=operator,
filter_key=filter_key,
filter_value=filter_value,
property_obj=property_obj,
)
elif operator == _SPECIAL_OPERATOR_ISNULL:
operator, deflated_value = transform_null_operator_to_filter(
filter_key=filter_key, filter_value=filter_value
)
elif operator in _REGEX_OPERATOR_TABLE.values():
operator, deflated_value = transform_regex_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)
for ops_it, transform in TRANSFORM_TABLE.items():
if operator in ops_it:
return transform(
operator=operator,
filter_key=filter_key,
filter_value=filter_value,
property_obj=property_obj,
)

deflated_value = property_obj.deflate(filter_value)
return operator, deflated_value


Expand Down Expand Up @@ -634,7 +696,12 @@ def _parse_q_filters(self, ident, q, source_class):
statement = f"{ident}.{prop} {operator}"
else:
place_holder = self._register_place_holder(ident + "_" + prop)
if operator == _SPECIAL_OPERATOR_ARRAY_IN:
if operator in [
_SPECIAL_OPERATOR_ARRAY_IN,
_SPECIAL_OPERATOR_INCLUDES,
_SPECIAL_OPERATOR_INCLUDES_ALL,
_SPECIAL_OPERATOR_INCLUDES_ANY,
]:
statement = operator.format(
ident=ident,
prop=prop,
Expand Down Expand Up @@ -674,6 +741,21 @@ def build_where_stmt(self, ident, filters, q_filters=None, source_class=None):
statement = (
f"{'NOT' if negate else ''} {ident}.{prop} {operator}"
)
# Fix IN operator for Traversal Sets
# Potential bug: Must be investigated if it is really an issue
elif operator in [
_SPECIAL_OPERATOR_ARRAY_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}"
Expand Down Expand Up @@ -1004,6 +1086,9 @@ def filter(self, *args, **kwargs):
* 'istartswith': case insensitive string starts with
* 'endswith': string ends with
* 'iendswith': case insensitive string ends with
* 'includes': array contains value
* 'includes_all': array contains all values
* 'includes_any': array contains any of the values

:return: self
"""
Expand Down
129 changes: 107 additions & 22 deletions neomodel/sync_/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -256,6 +262,61 @@ 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
return operator, deflated_value


def transform_includes_all_any_operator_to_filter(
operator, filter_key, filter_value, property_obj
):
"""
Transform includes__all/any 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)
selected_operator = (
_SPECIAL_OPERATOR_INCLUDES_ANY
if operator == _SPECIAL_OPERATOR_INCLUDES_ANY
else _SPECIAL_OPERATOR_INCLUDES_ALL
)
return selected_operator, deflated_value


def transform_in_operator_to_filter(operator, filter_key, filter_value, property_obj):
"""
Transform in operator to a cypher filter
Expand All @@ -280,7 +341,7 @@ def transform_in_operator_to_filter(operator, filter_key, filter_value, property
return operator, deflated_value


def transform_null_operator_to_filter(filter_key, filter_value):
def transform_null_operator_to_filter(filter_key, filter_value, **kwargs):
"""
Transform null operator to a cypher filter
Args:
Expand Down Expand Up @@ -320,28 +381,29 @@ def transform_regex_operator_to_filter(
return operator, deflated_value


TRANSFORM_TABLE = {
(_SPECIAL_OPERATOR_IN,): transform_in_operator_to_filter,
(_SPECIAL_OPERATOR_ISNULL,): transform_null_operator_to_filter,
tuple(_REGEX_OPERATOR_TABLE.values()): transform_regex_operator_to_filter,
(_SPECIAL_OPERATOR_INCLUDES,): transform_includes_operator_to_filter,
(
_SPECIAL_OPERATOR_INCLUDES_ALL,
_SPECIAL_OPERATOR_INCLUDES_ANY,
): transform_includes_all_any_operator_to_filter,
}


def transform_operator_to_filter(operator, filter_key, filter_value, property_obj):
if operator == _SPECIAL_OPERATOR_IN:
operator, deflated_value = transform_in_operator_to_filter(
operator=operator,
filter_key=filter_key,
filter_value=filter_value,
property_obj=property_obj,
)
elif operator == _SPECIAL_OPERATOR_ISNULL:
operator, deflated_value = transform_null_operator_to_filter(
filter_key=filter_key, filter_value=filter_value
)
elif operator in _REGEX_OPERATOR_TABLE.values():
operator, deflated_value = transform_regex_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)
for ops_it, transform in TRANSFORM_TABLE.items():
if operator in ops_it:
return transform(
operator=operator,
filter_key=filter_key,
filter_value=filter_value,
property_obj=property_obj,
)

deflated_value = property_obj.deflate(filter_value)
return operator, deflated_value


Expand Down Expand Up @@ -634,7 +696,12 @@ def _parse_q_filters(self, ident, q, source_class):
statement = f"{ident}.{prop} {operator}"
else:
place_holder = self._register_place_holder(ident + "_" + prop)
if operator == _SPECIAL_OPERATOR_ARRAY_IN:
if operator in [
_SPECIAL_OPERATOR_ARRAY_IN,
_SPECIAL_OPERATOR_INCLUDES,
_SPECIAL_OPERATOR_INCLUDES_ALL,
_SPECIAL_OPERATOR_INCLUDES_ANY,
]:
statement = operator.format(
ident=ident,
prop=prop,
Expand Down Expand Up @@ -674,6 +741,21 @@ def build_where_stmt(self, ident, filters, q_filters=None, source_class=None):
statement = (
f"{'NOT' if negate else ''} {ident}.{prop} {operator}"
)
# Fix IN operator for Traversal Sets
# Potential bug: Must be investigated if it is really an issue
elif operator in [
_SPECIAL_OPERATOR_ARRAY_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}"
Expand Down Expand Up @@ -1000,6 +1082,9 @@ def filter(self, *args, **kwargs):
* 'istartswith': case insensitive string starts with
* 'endswith': string ends with
* 'iendswith': case insensitive string ends with
* 'includes': array contains value
* 'includes_all': array contains all values
* 'includes_any': array contains any of the values

:return: self
"""
Expand Down
Loading
Loading