summaryrefslogtreecommitdiff
path: root/tests/test_graph.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_graph.py')
-rw-r--r--tests/test_graph.py477
1 files changed, 477 insertions, 0 deletions
diff --git a/tests/test_graph.py b/tests/test_graph.py
new file mode 100644
index 0000000..c6dc9a4
--- /dev/null
+++ b/tests/test_graph.py
@@ -0,0 +1,477 @@
+import pytest
+
+from redis.commands.graph import Edge, Node, Path
+from redis.exceptions import ResponseError
+
+
+@pytest.fixture
+def client(modclient):
+ modclient.flushdb()
+ return modclient
+
+
+@pytest.mark.redismod
+def test_bulk(client):
+ with pytest.raises(NotImplementedError):
+ client.graph().bulk()
+ client.graph().bulk(foo="bar!")
+
+
+@pytest.mark.redismod
+def test_graph_creation(client):
+ graph = client.graph()
+
+ john = Node(
+ label="person",
+ properties={
+ "name": "John Doe",
+ "age": 33,
+ "gender": "male",
+ "status": "single",
+ },
+ )
+ graph.add_node(john)
+ japan = Node(label="country", properties={"name": "Japan"})
+
+ graph.add_node(japan)
+ edge = Edge(john, "visited", japan, properties={"purpose": "pleasure"})
+ graph.add_edge(edge)
+
+ graph.commit()
+
+ query = (
+ 'MATCH (p:person)-[v:visited {purpose:"pleasure"}]->(c:country) '
+ "RETURN p, v, c"
+ )
+
+ result = graph.query(query)
+
+ person = result.result_set[0][0]
+ visit = result.result_set[0][1]
+ country = result.result_set[0][2]
+
+ assert person == john
+ assert visit.properties == edge.properties
+ assert country == japan
+
+ query = """RETURN [1, 2.3, "4", true, false, null]"""
+ result = graph.query(query)
+ assert [1, 2.3, "4", True, False, None] == result.result_set[0][0]
+
+ # All done, remove graph.
+ graph.delete()
+
+
+@pytest.mark.redismod
+def test_array_functions(client):
+ query = """CREATE (p:person{name:'a',age:32, array:[0,1,2]})"""
+ client.graph().query(query)
+
+ query = """WITH [0,1,2] as x return x"""
+ result = client.graph().query(query)
+ assert [0, 1, 2] == result.result_set[0][0]
+
+ query = """MATCH(n) return collect(n)"""
+ result = client.graph().query(query)
+
+ a = Node(
+ node_id=0,
+ label="person",
+ properties={"name": "a", "age": 32, "array": [0, 1, 2]},
+ )
+
+ assert [a] == result.result_set[0][0]
+
+
+@pytest.mark.redismod
+def test_path(client):
+ node0 = Node(node_id=0, label="L1")
+ node1 = Node(node_id=1, label="L1")
+ edge01 = Edge(node0, "R1", node1, edge_id=0, properties={"value": 1})
+
+ graph = client.graph()
+ graph.add_node(node0)
+ graph.add_node(node1)
+ graph.add_edge(edge01)
+ graph.flush()
+
+ path01 = Path.new_empty_path().add_node(node0).add_edge(edge01).add_node(node1)
+ expected_results = [[path01]]
+
+ query = "MATCH p=(:L1)-[:R1]->(:L1) RETURN p ORDER BY p"
+ result = graph.query(query)
+ assert expected_results == result.result_set
+
+
+@pytest.mark.redismod
+def test_param(client):
+ params = [1, 2.3, "str", True, False, None, [0, 1, 2]]
+ query = "RETURN $param"
+ for param in params:
+ result = client.graph().query(query, {"param": param})
+ expected_results = [[param]]
+ assert expected_results == result.result_set
+
+
+@pytest.mark.redismod
+def test_map(client):
+ query = "RETURN {a:1, b:'str', c:NULL, d:[1,2,3], e:True, f:{x:1, y:2}}"
+
+ actual = client.graph().query(query).result_set[0][0]
+ expected = {
+ "a": 1,
+ "b": "str",
+ "c": None,
+ "d": [1, 2, 3],
+ "e": True,
+ "f": {"x": 1, "y": 2},
+ }
+
+ assert actual == expected
+
+
+@pytest.mark.redismod
+def test_point(client):
+ query = "RETURN point({latitude: 32.070794860, longitude: 34.820751118})"
+ expected_lat = 32.070794860
+ expected_lon = 34.820751118
+ actual = client.graph().query(query).result_set[0][0]
+ assert abs(actual["latitude"] - expected_lat) < 0.001
+ assert abs(actual["longitude"] - expected_lon) < 0.001
+
+ query = "RETURN point({latitude: 32, longitude: 34.0})"
+ expected_lat = 32
+ expected_lon = 34
+ actual = client.graph().query(query).result_set[0][0]
+ assert abs(actual["latitude"] - expected_lat) < 0.001
+ assert abs(actual["longitude"] - expected_lon) < 0.001
+
+
+@pytest.mark.redismod
+def test_index_response(client):
+ result_set = client.graph().query("CREATE INDEX ON :person(age)")
+ assert 1 == result_set.indices_created
+
+ result_set = client.graph().query("CREATE INDEX ON :person(age)")
+ assert 0 == result_set.indices_created
+
+ result_set = client.graph().query("DROP INDEX ON :person(age)")
+ assert 1 == result_set.indices_deleted
+
+ with pytest.raises(ResponseError):
+ client.graph().query("DROP INDEX ON :person(age)")
+
+
+@pytest.mark.redismod
+def test_stringify_query_result(client):
+ graph = client.graph()
+
+ john = Node(
+ alias="a",
+ label="person",
+ properties={
+ "name": "John Doe",
+ "age": 33,
+ "gender": "male",
+ "status": "single",
+ },
+ )
+ graph.add_node(john)
+
+ japan = Node(alias="b", label="country", properties={"name": "Japan"})
+ graph.add_node(japan)
+
+ edge = Edge(john, "visited", japan, properties={"purpose": "pleasure"})
+ graph.add_edge(edge)
+
+ assert (
+ str(john)
+ == """(a:person{age:33,gender:"male",name:"John Doe",status:"single"})""" # noqa
+ )
+ assert (
+ str(edge)
+ == """(a:person{age:33,gender:"male",name:"John Doe",status:"single"})""" # noqa
+ + """-[:visited{purpose:"pleasure"}]->"""
+ + """(b:country{name:"Japan"})"""
+ )
+ assert str(japan) == """(b:country{name:"Japan"})"""
+
+ graph.commit()
+
+ query = """MATCH (p:person)-[v:visited {purpose:"pleasure"}]->(c:country)
+ RETURN p, v, c"""
+
+ result = client.graph().query(query)
+ person = result.result_set[0][0]
+ visit = result.result_set[0][1]
+ country = result.result_set[0][2]
+
+ assert (
+ str(person)
+ == """(:person{age:33,gender:"male",name:"John Doe",status:"single"})""" # noqa
+ )
+ assert str(visit) == """()-[:visited{purpose:"pleasure"}]->()"""
+ assert str(country) == """(:country{name:"Japan"})"""
+
+ graph.delete()
+
+
+@pytest.mark.redismod
+def test_optional_match(client):
+ # Build a graph of form (a)-[R]->(b)
+ node0 = Node(node_id=0, label="L1", properties={"value": "a"})
+ node1 = Node(node_id=1, label="L1", properties={"value": "b"})
+
+ edge01 = Edge(node0, "R", node1, edge_id=0)
+
+ graph = client.graph()
+ graph.add_node(node0)
+ graph.add_node(node1)
+ graph.add_edge(edge01)
+ graph.flush()
+
+ # Issue a query that collects all outgoing edges from both nodes
+ # (the second has none)
+ query = """MATCH (a) OPTIONAL MATCH (a)-[e]->(b) RETURN a, e, b ORDER BY a.value""" # noqa
+ expected_results = [[node0, edge01, node1], [node1, None, None]]
+
+ result = client.graph().query(query)
+ assert expected_results == result.result_set
+
+ graph.delete()
+
+
+@pytest.mark.redismod
+def test_cached_execution(client):
+ client.graph().query("CREATE ()")
+
+ uncached_result = client.graph().query("MATCH (n) RETURN n, $param", {"param": [0]})
+ assert uncached_result.cached_execution is False
+
+ # loop to make sure the query is cached on each thread on server
+ for x in range(0, 64):
+ cached_result = client.graph().query(
+ "MATCH (n) RETURN n, $param", {"param": [0]}
+ )
+ assert uncached_result.result_set == cached_result.result_set
+
+ # should be cached on all threads by now
+ assert cached_result.cached_execution
+
+
+@pytest.mark.redismod
+def test_explain(client):
+ create_query = """CREATE (:Rider {name:'Valentino Rossi'})-[:rides]->(:Team {name:'Yamaha'}),
+ (:Rider {name:'Dani Pedrosa'})-[:rides]->(:Team {name:'Honda'}),
+ (:Rider {name:'Andrea Dovizioso'})-[:rides]->(:Team {name:'Ducati'})"""
+ client.graph().query(create_query)
+
+ result = client.graph().explain(
+ "MATCH (r:Rider)-[:rides]->(t:Team) WHERE t.name = $name RETURN r.name, t.name, $params", # noqa
+ {"name": "Yehuda"},
+ )
+ expected = "Results\n Project\n Conditional Traverse | (t:Team)->(r:Rider)\n Filter\n Node By Label Scan | (t:Team)" # noqa
+ assert result == expected
+
+
+@pytest.mark.redismod
+def test_slowlog(client):
+ create_query = """CREATE (:Rider {name:'Valentino Rossi'})-[:rides]->(:Team {name:'Yamaha'}),
+ (:Rider {name:'Dani Pedrosa'})-[:rides]->(:Team {name:'Honda'}),
+ (:Rider {name:'Andrea Dovizioso'})-[:rides]->(:Team {name:'Ducati'})"""
+ client.graph().query(create_query)
+
+ results = client.graph().slowlog()
+ assert results[0][1] == "GRAPH.QUERY"
+ assert results[0][2] == create_query
+
+
+@pytest.mark.redismod
+def test_query_timeout(client):
+ # Build a sample graph with 1000 nodes.
+ client.graph().query("UNWIND range(0,1000) as val CREATE ({v: val})")
+ # Issue a long-running query with a 1-millisecond timeout.
+ with pytest.raises(ResponseError):
+ client.graph().query("MATCH (a), (b), (c), (d) RETURN *", timeout=1)
+ assert False is False
+
+ with pytest.raises(Exception):
+ client.graph().query("RETURN 1", timeout="str")
+ assert False is False
+
+
+@pytest.mark.redismod
+def test_read_only_query(client):
+ with pytest.raises(Exception):
+ # Issue a write query, specifying read-only true,
+ # this call should fail.
+ client.graph().query("CREATE (p:person {name:'a'})", read_only=True)
+ assert False is False
+
+
+@pytest.mark.redismod
+def test_profile(client):
+ q = """UNWIND range(1, 3) AS x CREATE (p:Person {v:x})"""
+ profile = client.graph().profile(q).result_set
+ assert "Create | Records produced: 3" in profile
+ assert "Unwind | Records produced: 3" in profile
+
+ q = "MATCH (p:Person) WHERE p.v > 1 RETURN p"
+ profile = client.graph().profile(q).result_set
+ assert "Results | Records produced: 2" in profile
+ assert "Project | Records produced: 2" in profile
+ assert "Filter | Records produced: 2" in profile
+ assert "Node By Label Scan | (p:Person) | Records produced: 3" in profile
+
+
+@pytest.mark.redismod
+def test_config(client):
+ config_name = "RESULTSET_SIZE"
+ config_value = 3
+
+ # Set configuration
+ response = client.graph().config(config_name, config_value, set=True)
+ assert response == "OK"
+
+ # Make sure config been updated.
+ response = client.graph().config(config_name, set=False)
+ expected_response = [config_name, config_value]
+ assert response == expected_response
+
+ config_name = "QUERY_MEM_CAPACITY"
+ config_value = 1 << 20 # 1MB
+
+ # Set configuration
+ response = client.graph().config(config_name, config_value, set=True)
+ assert response == "OK"
+
+ # Make sure config been updated.
+ response = client.graph().config(config_name, set=False)
+ expected_response = [config_name, config_value]
+ assert response == expected_response
+
+ # reset to default
+ client.graph().config("QUERY_MEM_CAPACITY", 0, set=True)
+ client.graph().config("RESULTSET_SIZE", -100, set=True)
+
+
+@pytest.mark.redismod
+def test_list_keys(client):
+ result = client.graph().list_keys()
+ assert result == []
+
+ client.execute_command("GRAPH.EXPLAIN", "G", "RETURN 1")
+ result = client.graph().list_keys()
+ assert result == ["G"]
+
+ client.execute_command("GRAPH.EXPLAIN", "X", "RETURN 1")
+ result = client.graph().list_keys()
+ assert result == ["G", "X"]
+
+ client.delete("G")
+ client.rename("X", "Z")
+ result = client.graph().list_keys()
+ assert result == ["Z"]
+
+ client.delete("Z")
+ result = client.graph().list_keys()
+ assert result == []
+
+
+@pytest.mark.redismod
+def test_multi_label(client):
+ redis_graph = client.graph("g")
+
+ node = Node(label=["l", "ll"])
+ redis_graph.add_node(node)
+ redis_graph.commit()
+
+ query = "MATCH (n) RETURN n"
+ result = redis_graph.query(query)
+ result_node = result.result_set[0][0]
+ assert result_node == node
+
+ try:
+ Node(label=1)
+ assert False
+ except AssertionError:
+ assert True
+
+ try:
+ Node(label=["l", 1])
+ assert False
+ except AssertionError:
+ assert True
+
+
+@pytest.mark.redismod
+def test_cache_sync(client):
+ pass
+ return
+ # This test verifies that client internal graph schema cache stays
+ # in sync with the graph schema
+ #
+ # Client B will try to get Client A out of sync by:
+ # 1. deleting the graph
+ # 2. reconstructing the graph in a different order, this will casuse
+ # a differance in the current mapping between string IDs and the
+ # mapping Client A is aware of
+ #
+ # Client A should pick up on the changes by comparing graph versions
+ # and resyncing its cache.
+
+ A = client.graph("cache-sync")
+ B = client.graph("cache-sync")
+
+ # Build order:
+ # 1. introduce label 'L' and 'K'
+ # 2. introduce attribute 'x' and 'q'
+ # 3. introduce relationship-type 'R' and 'S'
+
+ A.query("CREATE (:L)")
+ B.query("CREATE (:K)")
+ A.query("MATCH (n) SET n.x = 1")
+ B.query("MATCH (n) SET n.q = 1")
+ A.query("MATCH (n) CREATE (n)-[:R]->()")
+ B.query("MATCH (n) CREATE (n)-[:S]->()")
+
+ # Cause client A to populate its cache
+ A.query("MATCH (n)-[e]->() RETURN n, e")
+
+ assert len(A._labels) == 2
+ assert len(A._properties) == 2
+ assert len(A._relationshipTypes) == 2
+ assert A._labels[0] == "L"
+ assert A._labels[1] == "K"
+ assert A._properties[0] == "x"
+ assert A._properties[1] == "q"
+ assert A._relationshipTypes[0] == "R"
+ assert A._relationshipTypes[1] == "S"
+
+ # Have client B reconstruct the graph in a different order.
+ B.delete()
+
+ # Build order:
+ # 1. introduce relationship-type 'R'
+ # 2. introduce label 'L'
+ # 3. introduce attribute 'x'
+ B.query("CREATE ()-[:S]->()")
+ B.query("CREATE ()-[:R]->()")
+ B.query("CREATE (:K)")
+ B.query("CREATE (:L)")
+ B.query("MATCH (n) SET n.q = 1")
+ B.query("MATCH (n) SET n.x = 1")
+
+ # A's internal cached mapping is now out of sync
+ # issue a query and make sure A's cache is synced.
+ A.query("MATCH (n)-[e]->() RETURN n, e")
+
+ assert len(A._labels) == 2
+ assert len(A._properties) == 2
+ assert len(A._relationshipTypes) == 2
+ assert A._labels[0] == "K"
+ assert A._labels[1] == "L"
+ assert A._properties[0] == "q"
+ assert A._properties[1] == "x"
+ assert A._relationshipTypes[0] == "S"
+ assert A._relationshipTypes[1] == "R"