Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions docs/how-to-guides/query-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -743,6 +743,44 @@ MATCH (p:Person) WHERE p.name IN ["Alice", "Bob"] RETURN *;
</TabItem>
</Tabs>

### Filter data with the IS NOT NULL operator

You can use the `IS NOT NULL` operator to **check that a property is set**. It is a unary
operator: pass neither `literal` nor `expression`.

<Tabs
defaultValue="gqlalchemy"
values={[
{label: 'GQLAlchemy', value: 'gqlalchemy'},
{label: 'Cypher', value: 'cypher'}
]}>
<TabItem value="gqlalchemy">

```python
from gqlalchemy import match
from gqlalchemy.query_builders.memgraph_query_builder import Operator

results = list(
match()
.node(labels="Person", variable="p")
.where(item="p.name", operator=Operator.IS_NOT_NULL)
.return_()
.execute()
)

print(results)
```

</TabItem>
<TabItem value="cypher">

```cypher
MATCH (p:Person) WHERE p.name IS NOT NULL RETURN *;
```

</TabItem>
</Tabs>

The `literal` keyword argument is used because you want list values to be properly quoted in the Cypher query. You can also combine `IN` with other boolean operators like `AND`, `OR`, etc.

### Filter data by label
Expand Down
18 changes: 16 additions & 2 deletions gqlalchemy/query_builders/declarative_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class Operator(Enum):
GREATER_THAN = ">"
IN = "IN"
INEQUAL = "<>"
IS_NOT_NULL = "IS NOT NULL"
LABEL_FILTER = ":"
LESS_THAN = "<"
LEQ_THAN = "<="
Expand Down Expand Up @@ -167,6 +168,8 @@ def construct_query(self) -> str:
class WhereConditionPartialQuery(PartialQuery):
_LITERAL = "literal"
_EXPRESSION = "expression"
# Postfix operators that take no right-hand side (no literal or expression).
_UNARY_OPERATORS = (Operator.IS_NOT_NULL.value,)

def __init__(self, item: str, operator: Operator, keyword: Where = Where.WHERE, is_negated: bool = False, **kwargs):
super().__init__(type=keyword.name if not is_negated else f"{keyword.name} {Where.NOT.name}")
Expand All @@ -186,6 +189,12 @@ def _build_where_query(self, item: str, operator: Operator, **kwargs) -> "Declar
if operator_str not in Operator._value2member_map_:
raise GQLAlchemyOperatorTypeError(clause=self.type)

if operator_str in WhereConditionPartialQuery._UNARY_OPERATORS:
if literal is not None or value is not None:
raise GQLAlchemyExtraKeywordArguments(clause=self.type)

return " ".join([item, operator_str])

if value is None:
if literal is None:
raise GQLAlchemyLiteralAndExpressionMissing(clause=self.type)
Expand Down Expand Up @@ -871,8 +880,8 @@ def where(self, item: str, operator: Operator, **kwargs) -> "DeclarativeBase":
expression: A node label or property that won't be converted to Cypher value (no additional quotes will be added).

Raises:
GQLAlchemyLiteralAndExpressionMissingInWhere: Raises an error when neither literal nor expression keyword arguments were provided.
GQLAlchemyExtraKeywordArgumentsInWhere: Raises an error when both literal and expression keyword arguments were provided.
GQLAlchemyLiteralAndExpressionMissingInWhere: Raises an error when neither literal nor expression keyword arguments were provided, except for unary operators such as `Operator.IS_NOT_NULL`, which take neither.
GQLAlchemyExtraKeywordArgumentsInWhere: Raises an error when both literal and expression keyword arguments were provided, or when either is provided with a unary operator.

Returns:
self: A partial Cypher query built from the given parameters.
Expand All @@ -892,6 +901,11 @@ def where(self, item: str, operator: Operator, **kwargs) -> "DeclarativeBase":

Python: `match().node(variable='n').where(item='n.age', operator=Operator.GREATER_THAN, literal=18).return_()`
Cypher: `MATCH (n) WHERE n.age > 18 RETURN *;`

Filtering query results by property existence, using a unary operator (no literal or expression).

Python: `match().node(variable='n').to().node(variable='m').where(item='n.name', operator=Operator.IS_NOT_NULL).return_()`
Cypher: `MATCH (n)-[]->(m) WHERE n.name IS NOT NULL RETURN *;`
"""
# WHERE item operator (literal | expression)
# item: variable | property
Expand Down
59 changes: 59 additions & 0 deletions tests/query_builders/test_query_builders.py
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,65 @@ def test_where_in(self, vendor):

mock.assert_called_with(expected_query)

def test_where_is_not_null(self, vendor):
query_builder = (
vendor[1]
.match()
.node(labels="L1", variable="n")
.to(relationship_type="TO")
.node(labels="L2", variable="m")
.where(item="n.name", operator=Operator.IS_NOT_NULL)
.return_()
)
expected_query = " MATCH (n:L1)-[:TO]->(m:L2) WHERE n.name IS NOT NULL RETURN * "

with patch.object(vendor[0], "execute_and_fetch", return_value=None) as mock:
query_builder.execute()

mock.assert_called_with(expected_query)

def test_and_where_is_not_null(self, vendor):
query_builder = (
vendor[1]
.match()
.node(labels="L1", variable="n")
.to(relationship_type="TO")
.node(labels="L2", variable="m")
.where(item="n.name", operator=Operator.IS_NOT_NULL)
.and_where(item="m.name", operator=Operator.IS_NOT_NULL)
.return_()
)
expected_query = " MATCH (n:L1)-[:TO]->(m:L2) WHERE n.name IS NOT NULL AND m.name IS NOT NULL RETURN * "

with patch.object(vendor[0], "execute_and_fetch", return_value=None) as mock:
query_builder.execute()

mock.assert_called_with(expected_query)

def test_where_is_not_null_with_literal_throws(self, vendor):
with pytest.raises(GQLAlchemyExtraKeywordArguments):
(
vendor[1]
.match()
.node(labels="L1", variable="n")
.to(relationship_type="TO")
.node(labels="L2", variable="m")
.where(item="n.name", operator=Operator.IS_NOT_NULL, literal="best_name")
.return_()
)

def test_where_is_not_null_with_expression_throws(self, vendor):
with pytest.raises(GQLAlchemyExtraKeywordArguments):
(
vendor[1]
.match()
.node(labels="L1", variable="n")
.to(relationship_type="TO")
.node(labels="L2", variable="m")
.where(item="n.name", operator=Operator.IS_NOT_NULL, expression="m.name")
.return_()
)

def test_where_not_label(self, vendor):
query_builder = (
vendor[1]
Expand Down