class testEdgeByIndexScanFlow(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) def setUp(self): global redis_graph redis_con = self.env.getConnection() redis_graph = Graph("social", redis_con) self.populate_graph(redis_graph) self.build_indices() def tearDown(self): self.env.cmd('flushall') def populate_graph(self, redis_graph): nodes = {} # Create entities node_id = 0 for p in people: node = Node(label="person", properties={ "name": p, "created_at": node_id }) redis_graph.add_node(node) nodes[p] = node node_id = node_id + 1 # Fully connected graph edge_id = 0 for src in nodes: for dest in nodes: if src != dest: edge = Edge(nodes[src], "knows", nodes[dest], properties={"created_at": edge_id * 2}) redis_graph.add_edge(edge) edge = Edge(nodes[src], "friend", nodes[dest], properties={ "created_at": edge_id * 2 + 1, "updated_at": edge_id * 3 }) redis_graph.add_edge(edge) edge_id = edge_id + 1 redis_graph.commit() def build_indices(self): global redis_graph redis_graph.query("CREATE INDEX ON :person(age)") redis_graph.query( "CREATE INDEX FOR ()-[f:friend]-() ON (f.created_at)") redis_graph.query("CREATE INDEX FOR ()-[f:knows]-() ON (f.created_at)") # Validate that Cartesian products using index and label scans succeed def test01_cartesian_product_mixed_scans(self): query = "MATCH ()-[f:friend]->(), ()-[k:knows]->() WHERE f.created_at >= 0 RETURN f.created_at, k.created_at ORDER BY f.created_at, k.created_at" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Conditional Traverse', plan) indexed_result = redis_graph.query(query) query = "MATCH ()-[f:friend]->(), ()-[k:knows]->() RETURN f.created_at, k.created_at ORDER BY f.created_at, k.created_at" plan = redis_graph.execution_plan(query) self.env.assertNotIn('Edge By Index Scan', plan) self.env.assertIn('Conditional Traverse', plan) unindexed_result = redis_graph.query(query) self.env.assertEquals(indexed_result.result_set, unindexed_result.result_set) # Validate that Cartesian products using just index scans succeed def test02_cartesian_product_index_scans_only(self): query = "MATCH ()-[f:friend]->(), ()-[k:knows]->() WHERE f.created_at >= 0 AND k.created_at >= 0 RETURN f.created_at, k.created_at ORDER BY f.created_at, k.created_at" plan = redis_graph.execution_plan(query) # The two streams should both use index scans self.env.assertEquals(plan.count('Edge By Index Scan'), 2) self.env.assertNotIn('Conditional Traverse', plan) indexed_result = redis_graph.query(query) query = "MATCH ()-[f:friend]->(), ()-[k:knows]->() RETURN f.created_at, k.created_at ORDER BY f.created_at, k.created_at" plan = redis_graph.execution_plan(query) self.env.assertNotIn('Edge By Index Scan', plan) self.env.assertIn('Conditional Traverse', plan) unindexed_result = redis_graph.query(query) self.env.assertEquals(indexed_result.result_set, unindexed_result.result_set) # Validate that the appropriate bounds are respected when a Cartesian product uses the same index in two streams def test03_cartesian_product_reused_index(self): redis_graph.query( "CREATE INDEX FOR ()-[f:friend]-() ON (f.updated_at)") query = "MATCH ()-[a:friend]->(), ()-[b:friend]->() WHERE a.created_at >= 80 AND b.updated_at >= 120 RETURN a.created_at, b.updated_at" plan = redis_graph.execution_plan(query) # The two streams should both use index scans self.env.assertEquals(plan.count('Edge By Index Scan'), 2) self.env.assertNotIn('Conditional Traverse', plan) expected_result = [[81, 120], [83, 120], [81, 123], [83, 123]] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate index utilization when filtering on a numeric field with the `IN` keyword. def test04_test_in_operator_numerics(self): # Validate the transformation of IN to multiple OR expressions. query = "MATCH ()-[f:friend]-() WHERE f.created_at IN [1,2,3] RETURN f" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) # Validate that nested arrays are not scanned in index. query = "MATCH ()-[f:friend]-() WHERE f.created_at IN [[1,2],3] RETURN f" plan = redis_graph.execution_plan(query) self.env.assertNotIn('Edge By Index Scan', plan) self.env.assertIn('Conditional Traverse', plan) # Validate the transformation of IN to multiple OR, over a range. query = "MATCH (n)-[f:friend]->() WHERE f.created_at IN range(0,30) RETURN DISTINCT n.name ORDER BY n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) expected_result = [['Ailon'], ['Alon'], ['Roi']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate the transformation of IN to empty index iterator. query = "MATCH ()-[f:friend]-() WHERE f.created_at IN [] RETURN f.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) expected_result = [] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate the transformation of IN OR IN to empty index iterators. query = "MATCH ()-[f:friend]->() WHERE f.created_at IN [] OR f.created_at IN [] RETURN f.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) expected_result = [] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate the transformation of multiple IN filters. query = "MATCH (n)-[f:friend]->() WHERE f.created_at IN [0, 1, 2] OR f.created_at IN [14, 15, 16] RETURN n.name ORDER BY n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) expected_result = [['Alon'], ['Roi']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate the transformation of multiple IN filters. query = "MATCH (n)-[f:friend]->() WHERE f.created_at IN [0, 1, 2] OR f.created_at IN [14, 15, 16] OR f.created_at IN [] RETURN n.name ORDER BY n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) def test07_index_scan_and_id(self): query = """MATCH (n)-[f:friend]->() WHERE id(f)>=10 AND f.created_at<15 RETURN n.name ORDER BY n.name""" plan = redis_graph.execution_plan(query) query_result = redis_graph.query(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) self.env.assertEqual(2, len(query_result.result_set)) expected_result = [['Alon'], ['Roi']] self.env.assertEquals(expected_result, query_result.result_set) # Validate placement of index scans and filter ops when not all filters can be replaced. def test08_index_scan_multiple_filters(self): query = "MATCH (n)-[f:friend]->() WHERE f.created_at = 31 AND NOT EXISTS(f.fakeprop) RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertNotIn('Conditional Traverse', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Ailon"] self.env.assertEquals(query_result.result_set[0], expected_result) def test09_index_scan_with_params(self): query = "MATCH (n)-[f:friend]->() WHERE f.created_at = $time RETURN n.name" params = {'time': 31} plan = redis_graph.execution_plan(query, params=params) self.env.assertIn('Edge By Index Scan', plan) query_result = redis_graph.query(query, params=params) expected_result = ["Ailon"] self.env.assertEquals(query_result.result_set[0], expected_result) def test10_index_scan_with_param_array(self): query = "MATCH (n)-[f:friend]->() WHERE f.created_at in $times RETURN n.name" params = {'times': [31]} plan = redis_graph.execution_plan(query, params=params) self.env.assertIn('Edge By Index Scan', plan) query_result = redis_graph.query(query, params=params) expected_result = ["Ailon"] self.env.assertEquals(query_result.result_set[0], expected_result) def test11_single_index_multiple_scans(self): query = "MATCH (p1:person {name: 'Roi'}), (p2:person {name: 'Alon'}) MERGE (p1)-[:friend {created_at: 100}]->(p2) MERGE (p1)-[:friend {created_at: 101}]->(p2)" plan = redis_graph.execution_plan(query) # Two index scans should be performed. self.env.assertEqual(plan.count("Edge By Index Scan"), 2) query_result = redis_graph.query(query) # Two new nodes should be created. self.env.assertEquals(query_result.relationships_created, 2) def test16_runtime_index_utilization(self): # find all person nodes with age in the range 33-37 # current age (x) should be resolved at runtime # index query should be constructed for each age value q = """UNWIND range(33, 37) AS x MATCH (n)-[f:friend {created_at: x}]->() RETURN n.name ORDER BY n.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Edge By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [['Ailon'], ['Ailon'], ['Boaz']] self.env.assertEquals(query_result.result_set, expected_result) # similar to the query above, only this time the filter is specified # by an OR condition q = """WITH 33 AS min, 37 AS max MATCH (n)-[f:friend]->() WHERE f.created_at = min OR f.created_at = max RETURN n.name ORDER BY n.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Edge By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [['Ailon'], ['Boaz']] self.env.assertEquals(query_result.result_set, expected_result) # find all person nodes with age equals 33 'x' # 'x' value is known only at runtime q = """WITH 33 AS x MATCH (n)-[f:friend {created_at: x}]->() RETURN n.name ORDER BY n.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Edge By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["Ailon"]] self.env.assertEquals(query_result.result_set, expected_result) # find all person nodes with age equals x + 1 # the expression x+1 is evaluated to the constant 33 only at runtime # expecting index query to be constructed at runtime q = """WITH 32 AS x MATCH (n)-[f:friend]->() WHERE f.created_at = (x + 1) RETURN n.name ORDER BY n.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Edge By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["Ailon"]] self.env.assertEquals(query_result.result_set, expected_result) # same idea as previous query only we've switched the position of the # operands, queried entity (p.age) is now on the right hand side of the # filter, expecting the same behavior q = """WITH 32 AS x MATCH (n)-[f:friend]->() WHERE (x + 1) = f.created_at RETURN n.name ORDER BY n.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Edge By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["Ailon"]] self.env.assertEquals(query_result.result_set, expected_result) # make sure all node scan not removed because we need to filter q = """MATCH (a)-[e:friend]->() WHERE a.created_at > 5 AND e.created_at > a.created_at RETURN DISTINCT a.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Filter', plan) self.env.assertIn('All Node Scan', plan) query_result = redis_graph.query(q) expected_result = [["Ori"]] self.env.assertEquals(query_result.result_set, expected_result) def test_18_index_scan_and_label_filter(self): query = "MATCH (n)-[f:friend]->(m) WHERE f.created_at = 1 RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertNotIn('All Node Scan', plan) self.env.assertNotIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Roi"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person)-[f:friend]->(m) WHERE f.created_at = 1 RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertNotIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Roi"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person)-[f:friend]->(m:person) WHERE f.created_at = 1 RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Roi"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person {name: 'Roi'})-[f:friend]->(m:person) WHERE f.created_at = 1 RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Roi"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person {name: 'Alon'})-[f:friend]->(m:person) WHERE f.created_at = 1 RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) self.env.assertEquals(query_result.result_set, []) query = "MATCH (n)<-[f:friend]-(m) WHERE f.created_at = 1 RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertNotIn('All Node Scan', plan) self.env.assertNotIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Alon"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person)<-[f:friend]-(m) WHERE f.created_at = 1 RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertNotIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Alon"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person)<-[f:friend]-(m:person) WHERE f.created_at = 1 RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Alon"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person {name: 'Roi'})<-[f:friend]-(m:person) WHERE f.created_at = 1 RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) self.env.assertEquals(query_result.result_set, []) query = "MATCH (n:person {name: 'Alon'})<-[f:friend]-(m:person) WHERE f.created_at = 1 RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Alon"] self.env.assertEquals(query_result.result_set[0], expected_result) def test19_index_scan_and_with(self): query = "MATCH (n)-[f:friend]->(m) WHERE f.created_at = 1 WITH n RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertNotIn('All Node Scan', plan) self.env.assertNotIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Roi"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person)-[f:friend]->(m) WHERE f.created_at = 1 WITH n RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertNotIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Roi"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person)-[f:friend]->(m:person) WHERE f.created_at = 1 WITH n RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Roi"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person {name: 'Roi'})-[f:friend]->(m:person) WHERE f.created_at = 1 WITH n RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Roi"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person {name: 'Alon'})-[f:friend]->(m:person) WHERE f.created_at = 1 WITH n RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) self.env.assertEquals(query_result.result_set, []) query = "MATCH (n)<-[f:friend]-(m) WHERE f.created_at = 1 WITH n RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertNotIn('All Node Scan', plan) self.env.assertNotIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Alon"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person)<-[f:friend]-(m) WHERE f.created_at = 1 WITH n RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertNotIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Alon"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person)<-[f:friend]-(m:person) WHERE f.created_at = 1 WITH n RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Alon"] self.env.assertEquals(query_result.result_set[0], expected_result) query = "MATCH (n:person {name: 'Roi'})<-[f:friend]-(m:person) WHERE f.created_at = 1 WITH n RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) self.env.assertEquals(query_result.result_set, []) query = "MATCH (n:person {name: 'Alon'})<-[f:friend]-(m:person) WHERE f.created_at = 1 WITH n RETURN n.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Edge By Index Scan', plan) self.env.assertIn('Node By Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Alon"] self.env.assertEquals(query_result.result_set[0], expected_result)
class testComprehensionFunctions(FlowTestsBase): def __init__(self): self.env = Env() global redis_graph graph_id = "list_comprehension" redis_con = self.env.getConnection() redis_graph = Graph(graph_id, redis_con) self.populate_graph() def populate_graph(self): global redis_graph # Construct a graph with the form: # (v1)-[e1]->(v2)-[e2]->(v3) node_props = ['v1', 'v2', 'v3'] nodes = [] for idx, v in enumerate(node_props): node = Node(label="L", properties={"val": v}) nodes.append(node) redis_graph.add_node(node) edge = Edge(nodes[0], "E", nodes[1], properties={"edge_val": ['v1', 'v2']}) redis_graph.add_edge(edge) edge = Edge(nodes[1], "E", nodes[2], properties={"edge_val": ['v2', 'v3']}) redis_graph.add_edge(edge) redis_graph.commit() # Test list comprehension queries with scalar inputs and a single result row def test01_list_comprehension_single_return(self): expected_result = [[[2, 6]]] # Test logically identical queries that generate the same input array with different methods. query = """WITH [1,2,3] AS arr RETURN [elem IN arr WHERE elem % 2 = 1 | elem * 2]""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN [elem IN [1,2,3] WHERE elem % 2 = 1 | elem * 2]""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN [elem IN range(1,3) WHERE elem % 2 = 1 | elem * 2]""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, expected_result) def test02_list_comprehension_no_filter_no_map(self): expected_result = [[[1, 2, 3]]] query = """WITH [1,2,3] AS arr RETURN [elem IN arr]""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN [elem IN [1,2,3]]""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, expected_result) def test03_list_comprehension_map_no_filter(self): query = """WITH [1,2,3] AS arr RETURN [elem IN arr | elem * 2]""" actual_result = redis_graph.query(query) expected_result = [[[2, 4, 6]]] self.env.assertEquals(actual_result.result_set, expected_result) def test04_list_comprehension_filter_no_map(self): query = """WITH [1,2,3] AS arr RETURN [elem IN arr WHERE elem % 2 = 1]""" actual_result = redis_graph.query(query) expected_result = [[[1, 3]]] self.env.assertEquals(actual_result.result_set, expected_result) def test05_list_comprehension_on_allocated_values(self): query = """WITH [toUpper('str1'), toUpper('str2'), toUpper('str3')] AS arr RETURN [elem IN arr]""" actual_result = redis_graph.query(query) expected_result = [[['STR1', 'STR2', 'STR3']]] self.env.assertEquals(actual_result.result_set, expected_result) query = """WITH [toUpper('str1'), toUpper('str2'), toUpper('str3')] AS arr RETURN [elem IN arr WHERE toLower(elem) = 'str2']""" actual_result = redis_graph.query(query) expected_result = [[['STR2']]] self.env.assertEquals(actual_result.result_set, expected_result) query = """WITH [toUpper('str1'), toUpper('str2'), toUpper('str3')] AS arr RETURN [elem IN arr WHERE toLower(elem) = 'str2' | elem + 'low']""" actual_result = redis_graph.query(query) expected_result = [[['STR2low']]] self.env.assertEquals(actual_result.result_set, expected_result) def test06_list_comprehension_on_graph_entities(self): query = """MATCH p=()-[*]->() WITH nodes(p) AS nodes RETURN [elem IN nodes]""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), 3) query = """MATCH p=()-[*]->() WITH nodes(p) AS nodes WITH [elem IN nodes | elem.val] AS vals RETURN vals ORDER BY vals""" actual_result = redis_graph.query(query) expected_result = [[['v1', 'v2']], [['v1', 'v2', 'v3']], [['v2', 'v3']]] self.env.assertEquals(actual_result.result_set, expected_result) query = """MATCH p=()-[*]->() WITH nodes(p) AS nodes RETURN [elem IN nodes WHERE elem.val = 'v2' | elem.val]""" actual_result = redis_graph.query(query) expected_result = [[['v2']], [['v2']], [['v2']]] self.env.assertEquals(actual_result.result_set, expected_result) query = """MATCH p=()-[*]->() WITH nodes(p) AS nodes RETURN [elem IN nodes WHERE elem.val = 'v2' | elem.val + 'a']""" actual_result = redis_graph.query(query) expected_result = [[['v2a']], [['v2a']], [['v2a']]] self.env.assertEquals(actual_result.result_set, expected_result) def test07_list_comprehension_in_where_predicate(self): # List comprehension with predicate in WHERE predicate on MATCH clause - evaluates to true query = """MATCH (n) WHERE n.val IN [x in ['v1', 'v3']] RETURN n.val ORDER BY n.val""" actual_result = redis_graph.query(query) expected_result = [['v1'], ['v3']] self.env.assertEquals(actual_result.result_set, expected_result) # List comprehension with predicate in WHERE predicate - evaluates to true query = """WITH 1 AS a WHERE a IN [x in [1, 2]] RETURN a""" actual_result = redis_graph.query(query) expected_result = [[1]] self.env.assertEquals(actual_result.result_set, expected_result) # List comprehension with predicate in WHERE predicate - evaluates to false query = """WITH 1 AS a WHERE a IN [x in [2,3]] RETURN a""" actual_result = redis_graph.query(query) expected_result = [] self.env.assertEquals(actual_result.result_set, expected_result) # List comprehension with predicate and eval in WHERE predicate - evaluates to false query = """WITH 1 AS a WHERE [i in [2,3] WHERE i > 5] RETURN a""" actual_result = redis_graph.query(query) expected_result = [] self.env.assertEquals(actual_result.result_set, expected_result) # List comprehension without predicate or eval in WHERE predicate - evaluates to true query = """WITH 1 AS a WHERE [i in [2,3]] RETURN a""" actual_result = redis_graph.query(query) expected_result = [[1]] self.env.assertEquals(actual_result.result_set, expected_result) def test08_list_comprehension_on_property_array(self): query = """MATCH (n)-[e]->() WITH n, e ORDER BY n.val RETURN [elem IN e.edge_val WHERE elem = n.val]""" actual_result = redis_graph.query(query) expected_result = [[['v1']], [['v2']]] self.env.assertEquals(actual_result.result_set, expected_result) def test09_nested_list_comprehension(self): query = """RETURN [elem IN [nested_val IN range(0, 6) WHERE nested_val % 2 = 0] WHERE elem * 2 >= 4 | elem * 2]""" actual_result = redis_graph.query(query) expected_result = [[[4, 8, 12]]] self.env.assertEquals(actual_result.result_set, expected_result) def test10_any_all_comprehension_acceptance(self): # Reject ANY and ALL comprehensions that don't include a WHERE predicate. try: redis_graph.query("RETURN any(x IN [1,2])") assert (False) except redis.exceptions.ResponseError as e: # Expecting a type error. self.env.assertIn("requires a WHERE predicate", e.message) try: redis_graph.query("RETURN all(x IN [1,2])") assert (False) except redis.exceptions.ResponseError as e: # Expecting a type error. self.env.assertIn("requires a WHERE predicate", e.message) def test11_any_all_truth_table(self): # Test inputs and predicates where ANY and ALL are both false. query = """RETURN any(x IN [0,1] WHERE x = 2)""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, [[False]]) query = """RETURN all(x IN [0,1] WHERE x = 2)""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, [[False]]) # Test inputs and predicates where ANY is true and ALL is false. query = """RETURN any(x IN [0,1] WHERE x = 1)""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, [[True]]) query = """RETURN all(x IN [0,1] WHERE x = 1)""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, [[False]]) # Test inputs and predicates where ANY and ALL are both true. query = """RETURN any(x IN [0,1] WHERE x = 0 OR x = 1)""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, [[True]]) query = """RETURN all(x IN [0,1] WHERE x = 0 OR x = 1)""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, [[True]]) # Test inputs and predicates where ANY and ALL are both NULL. query = """RETURN any(x IN NULL WHERE x = 1)""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, [[None]]) query = """RETURN all(x IN NULL WHERE x = 1)""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, [[None]]) def test12_any_all_on_property_arrays(self): # The first array evaluates to ['v1', 'v2'] and the second evaluates to ['v2', 'v3'] query = """MATCH ()-[e]->() WITH e ORDER BY e.edge_val RETURN ANY(elem IN e.edge_val WHERE elem = 'v2' OR elem = 'v3')""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, [[True], [True]]) query = """MATCH ()-[e]->() WITH e ORDER BY e.edge_val RETURN ALL(elem IN e.edge_val WHERE elem = 'v2' OR elem = 'v3')""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, [[False], [True]]) def test13_any_all_path_filtering(self): # Use ANY and ALL to introspect on named variable-length paths. # All paths should be returned using both ANY and ALL filters. expected_result = [['v1'], ['v1'], ['v2']] query = """MATCH p=()-[*]->() WHERE any(node IN nodes(p) WHERE node.val STARTS WITH 'v') WITH head(nodes(p)) AS n RETURN n.val ORDER BY n.val""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, expected_result) query = """MATCH p=()-[*]->() WHERE all(node IN nodes(p) WHERE node.val STARTS WITH 'v') WITH head(nodes(p)) AS n RETURN n.val ORDER BY n.val""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set, expected_result) # Run a query in which 2 paths pass an ANY filter and 1 path passes an ALL filter. query = """MATCH p=()-[*0..1]->() WHERE any(node IN nodes(p) WHERE node.val = 'v1') RETURN length(p) ORDER BY length(p)""" actual_result = redis_graph.query(query) expected_result = [[0], [1]] self.env.assertEquals(actual_result.result_set, expected_result) query = """MATCH p=()-[*0..1]->() WHERE all(node IN nodes(p) WHERE node.val = 'v1') RETURN length(p) ORDER BY length(p)""" actual_result = redis_graph.query(query) expected_result = [[0]] self.env.assertEquals(actual_result.result_set, expected_result)
class testGraphMergeFlow(FlowTestsBase): def __init__(self): self.env = Env() global redis_graph global graph_2 redis_con = self.env.getConnection() redis_graph = Graph("G", redis_con) graph_2 = Graph("H", redis_con) # Create a single node without any labels or properties. def test01_single_node_with_label(self): global redis_graph query = """MERGE (robert:Critic)""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 1) self.env.assertEquals(result.nodes_created, 1) self.env.assertEquals(result.properties_set, 0) # Retry to create an existing entity. def test02_existing_single_node_with_label(self): global redis_graph query = """MERGE (robert:Critic)""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.properties_set, 0) # Create a single node with two properties and no labels. def test03_single_node_with_properties(self): global redis_graph query = """MERGE (charlie { name: 'Charlie Sheen', age: 10 })""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 1) self.env.assertEquals(result.properties_set, 2) # Retry to create an existing entity. def test04_existing_single_node_with_properties(self): global redis_graph query = """MERGE (charlie { name: 'Charlie Sheen', age: 10 })""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.properties_set, 0) # Create a single node with both label and property. def test05_single_node_both_label_and_property(self): global redis_graph query = """MERGE (michael:Person { name: 'Michael Douglas' })""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 1) self.env.assertEquals(result.nodes_created, 1) self.env.assertEquals(result.properties_set, 1) # Retry to create an existing entity. def test06_existing_single_node_both_label_and_property(self): global redis_graph query = """MERGE (michael:Person { name: 'Michael Douglas' })""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.properties_set, 0) # Create a single edge and additional two nodes. def test07_merge_on_relationship(self): global redis_graph query = """MERGE (charlie:ACTOR)-[r:ACTED_IN]->(wallStreet:MOVIE)""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 2) self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.properties_set, 0) self.env.assertEquals(result.relationships_created, 1) # Retry to create a single edge and additional two nodes. def test08_existing_merge_on_relationship(self): global redis_graph query = """MERGE (charlie:ACTOR)-[r:ACTED_IN]->(wallStreet:MOVIE)""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.properties_set, 0) self.env.assertEquals(result.relationships_created, 0) # Update existing entity def test09_update_existing_node(self): global redis_graph query = """MERGE (charlie { name: 'Charlie Sheen' }) SET charlie.age = 11, charlie.lastname='Sheen' """ result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.properties_set, 2) self.env.assertEquals(result.relationships_created, 0) query = """MATCH (charlie { name: 'Charlie Sheen' }) RETURN charlie.age, charlie.name, charlie.lastname""" actual_result = redis_graph.query(query) expected_result = [[11, 'Charlie Sheen', 'Sheen']] self.env.assertEquals(actual_result.result_set, expected_result) # Update new entity def test10_update_new_node(self): global redis_graph query = """MERGE (tamara:ACTOR { name: 'tamara tunie' }) SET tamara.age = 59, tamara.name = 'Tamara Tunie' """ result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 1) self.env.assertEquals(result.properties_set, 3) self.env.assertEquals(result.relationships_created, 0) query = """MATCH (tamara:ACTOR { name: 'Tamara Tunie' }) RETURN tamara.name, tamara.age""" actual_result = redis_graph.query(query) expected_result = [['Tamara Tunie', 59]] self.env.assertEquals(actual_result.result_set, expected_result) # Create a single edge and additional two nodes. def test11_update_new_relationship(self): global redis_graph query = """MERGE (franklin:ACTOR { name: 'Franklin Cover' })-[r:ACTED_IN {rate:5.7}]->(almostHeroes:MOVIE) SET r.date=1998, r.rate=5.8""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.properties_set, 4) self.env.assertEquals(result.relationships_created, 1) # Update existing relation def test12_update_existing_edge(self): global redis_graph query = """MERGE (franklin:ACTOR { name: 'Franklin Cover' })-[r:ACTED_IN {rate:5.8, date:1998}]->(almostHeroes:MOVIE) SET r.date=1998, r.rate=5.9""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.properties_set, 2) self.env.assertEquals(result.relationships_created, 0) query = """MATCH (franklin:ACTOR { name: 'Franklin Cover' })-[r:ACTED_IN {rate:5.9, date:1998}]->(almostHeroes:MOVIE) RETURN franklin.name, franklin.age, r.rate, r.date""" actual_result = redis_graph.query(query) expected_result = [['Franklin Cover', None, 5.9, 1998]] self.env.assertEquals(actual_result.result_set, expected_result) # Update multiple nodes def test13_update_multiple_nodes(self): global redis_graph query = """CREATE (:person {age:31}),(:person {age:31}),(:person {age:31}),(:person {age:31})""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 1) self.env.assertEquals(result.nodes_created, 4) self.env.assertEquals(result.properties_set, 4) query = """MERGE (p:person {age:31}) SET p.newprop=100""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.properties_set, 4) query = """MATCH (p:person) RETURN p.age, p.newprop""" actual_result = redis_graph.query(query) expected_result = [[31, 100], [31, 100], [31, 100], [31, 100]] self.env.assertEquals(actual_result.result_set, expected_result) # Update multiple nodes def test14_merge_unbounded_pattern(self): global redis_graph query = """MERGE (p:person {age:31})-[:owns]->(d:dog {name:'max'})""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 1) self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.properties_set, 2) self.env.assertEquals(result.relationships_created, 1) # Although person with age 31 and dog with the name max exists, # specified pattern doesn't exists, as a result the entire pattern # will be created, if we were to support MATCH MERGE 'p' and 'd' # would probably be defined in the MATCH clause, as a result they're # bounded and won't be duplicated. query = """MERGE (p:person {age:31})-[:owns]->(d:dog {name:'max'})-[:eats]->(f:food {name:'Royal Canin'})""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 1) self.env.assertEquals(result.nodes_created, 3) self.env.assertEquals(result.properties_set, 3) self.env.assertEquals(result.relationships_created, 2) # Add node that matches pre-existing index def test15_merge_indexed_entity(self): global redis_graph # Create index query = """CREATE INDEX ON :person(age)""" redis_graph.query(query) count_query = """MATCH (p:person) WHERE p.age > 0 RETURN COUNT(p)""" result = redis_graph.query(count_query) original_count = result.result_set[0][0] # Add one new person merge_query = """MERGE (p:person {age:40})""" result = redis_graph.query(merge_query) self.env.assertEquals(result.nodes_created, 1) self.env.assertEquals(result.properties_set, 1) # Verify that one indexed node has been added result = redis_graph.query(count_query) updated_count = result.result_set[0][0] self.env.assertEquals(updated_count, original_count + 1) # Perform another merge that does not create an entity result = redis_graph.query(merge_query) self.env.assertEquals(result.nodes_created, 0) # Verify that indexed node count is unchanged result = redis_graph.query(count_query) updated_count = result.result_set[0][0] self.env.assertEquals(updated_count, original_count + 1) # Update nodes based on non-constant inlined properties def test16_merge_dynamic_properties(self): global redis_graph # Create and verify a new node query = """MERGE (q:dyn {name: toUpper('abcde')}) RETURN q.name""" expected = [['ABCDE']] result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 1) self.env.assertEquals(result.nodes_created, 1) self.env.assertEquals(result.properties_set, 1) self.env.assertEquals(result.result_set, expected) # Repeat the query and verify that no changes were introduced result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.properties_set, 0) # Verify that MATCH...MERGE on the same entity does not introduce changes query = """MATCH (q {name: 'ABCDE'}) MERGE (r {name: q.name}) RETURN r.name""" result = redis_graph.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.properties_set, 0) self.env.assertEquals(result.result_set, expected) def test17_complex_merge_queries(self): # Beginning with an empty graph global graph_2 # Create a new pattern query = """MERGE (a:Person {name: 'a'}) MERGE (b:Person {name: 'b'}) MERGE (a)-[e:FRIEND {val: 1}]->(b) RETURN a.name, e.val, b.name""" result = graph_2.query(query) expected = [['a', 1, 'b']] # Verify the results self.env.assertEquals(result.labels_added, 1) self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.relationships_created, 1) self.env.assertEquals(result.properties_set, 3) self.env.assertEquals(result.result_set, expected) # Repeat the query and verify that no changes were introduced result = graph_2.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.relationships_created, 0) self.env.assertEquals(result.properties_set, 0) self.env.assertEquals(result.result_set, expected) # Verify that these entities are accessed properly with MATCH...MERGE queries query = """MATCH (a:Person {name: 'a'}), (b:Person {name: 'b'}) MERGE (a)-[e:FRIEND {val: 1}]->(b) RETURN a.name, e.val, b.name""" result = graph_2.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.relationships_created, 0) self.env.assertEquals(result.properties_set, 0) self.env.assertEquals(result.result_set, expected) # Verify that we can bind entities properly in variable-length traversals query = """MATCH (a)-[*]->(b) MERGE (a)-[e:FRIEND {val: 1}]->(b) RETURN a.name, e.val, b.name""" result = graph_2.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.relationships_created, 0) self.env.assertEquals(result.properties_set, 0) self.env.assertEquals(result.result_set, expected) # Verify UNWIND...MERGE does not recreate existing entities query = """UNWIND ['a', 'b'] AS names MERGE (a:Person {name: names}) RETURN a.name""" expected = [['a'], ['b']] result = graph_2.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.relationships_created, 0) self.env.assertEquals(result.properties_set, 0) self.env.assertEquals(result.result_set, expected) # Merging entities from an UNWIND list query = """UNWIND ['a', 'b', 'c'] AS names MERGE (a:Person {name: names}) ON MATCH SET a.set_by = 'match' ON CREATE SET a.set_by = 'create' RETURN a.name, a.set_by ORDER BY a.name""" expected = [['a', 'match'], ['b', 'match'], ['c', 'create']] result = graph_2.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 1) self.env.assertEquals(result.properties_set, 4) self.env.assertEquals(result.result_set, expected) # Connect 'c' to both 'a' and 'b' via a Friend relation # One thing to note here is that both `c` and `x` are bounded, which means # our current merge distinct validation inspect the created edge only using its relationship, properties and bounded # nodes! as such the first created edge is different from the second one (due to changes in the destination node). query = """MATCH (c:Person {name: 'c'}) MATCH (x:Person) WHERE x.name in ['a', 'b'] WITH c, x MERGE(c)-[:FRIEND]->(x)""" result = graph_2.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.properties_set, 0) self.env.assertEquals(result.relationships_created, 2) # Verify function calls in MERGE do not recreate existing entities query = """UNWIND ['A', 'B'] AS names MERGE (a:Person {name: toLower(names)}) RETURN a.name""" expected = [['a'], ['b']] result = graph_2.query(query) self.env.assertEquals(result.labels_added, 0) self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.relationships_created, 0) self.env.assertEquals(result.properties_set, 0) self.env.assertEquals(result.result_set, expected) query = """MERGE (a:Person {name: 'a'}) ON MATCH SET a.set_by = 'match' ON CREATE SET a.set_by = 'create' MERGE (b:Clone {name: a.name + '_clone'}) ON MATCH SET b.set_by = 'match' ON CREATE SET b.set_by = 'create' RETURN a.name, a.set_by, b.name, b.set_by""" result = graph_2.query(query) expected = [['a', 'match', 'a_clone', 'create']] # Verify the results self.env.assertEquals(result.labels_added, 1) self.env.assertEquals(result.nodes_created, 1) self.env.assertEquals(result.properties_set, 3) self.env.assertEquals(result.result_set, expected) def test18_merge_unique_creations(self): global graph_2 # Create a new pattern with non-unique entities. query = """UNWIND ['newprop1', 'newprop2'] AS x MERGE ({v:x})-[:e]->(n {v:'newprop1'})""" result = graph_2.query(query) # Verify that every entity was created in both executions. self.env.assertEquals(result.nodes_created, 4) self.env.assertEquals(result.relationships_created, 2) self.env.assertEquals(result.properties_set, 4) # Repeat the query. result = graph_2.query(query) # Verify that no data was modified. self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.relationships_created, 0) self.env.assertEquals(result.properties_set, 0) def test19_merge_dependency(self): redis_con = self.env.getConnection() graph = Graph("M", redis_con) # Starting with an empty graph. # Create 2 nodes and connect them to one another. self.env.flush() query = """MERGE (a:Person {name: 'a'}) MERGE (b:Person {name: 'b'}) MERGE (a)-[:FRIEND]->(b) MERGE (b)-[:FRIEND]->(a)""" result = graph.query(query) # Verify that every entity was created. self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.relationships_created, 2) self.env.assertEquals(result.properties_set, 2) # Repeat the query. result = graph.query(query) # Verify that no data was modified. self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.relationships_created, 0) self.env.assertEquals(result.properties_set, 0) def test20_merge_edge_dependency(self): redis_con = self.env.getConnection() graph = Graph("M", redis_con) # Starting with an empty graph. # Make sure the pattern ()-[]->()-[]->()-[]->() exists. self.env.flush() query = """MERGE (a {v:1}) MERGE (b {v:2}) MERGE (a)-[:KNOWS]->(b) MERGE ()-[:KNOWS]->()-[:KNOWS]->()""" result = graph.query(query) # Verify that every entity was created. self.env.assertEquals(result.nodes_created, 5) self.env.assertEquals(result.relationships_created, 3) self.env.assertEquals(result.properties_set, 2) # Repeat the query. result = graph.query(query) # Verify that no data was modified. self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.relationships_created, 0) self.env.assertEquals(result.properties_set, 0) def test21_merge_scan(self): redis_con = self.env.getConnection() graph = Graph("M", redis_con) # Starting with an empty graph. # All node scan should see created nodes. self.env.flush() query = """MERGE (a {v:1}) WITH a MATCH (n) MERGE (n)-[:KNOWS]->(m)""" result = graph.query(query) # Verify that every entity was created. self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.relationships_created, 1) self.env.assertEquals(result.properties_set, 1) # Starting with an empty graph. # Label scan should see created nodes. self.env.flush() query = """MERGE (a:L {v:1}) WITH a MATCH (n:L) MERGE (n)-[:KNOWS]->(m)""" result = graph.query(query) # Verify that every entity was created. self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.relationships_created, 1) self.env.assertEquals(result.properties_set, 1) def test22_merge_label_scan(self): redis_con = self.env.getConnection() graph = Graph("M", redis_con) # Starting with an empty graph. # Make sure the pattern ()-[]->()-[]->()-[]->() exists. self.env.flush() query = """MERGE (a {v:1}) MERGE (b {v:2}) MERGE (a)-[:KNOWS]->(b) WITH a AS c, b AS d MATCH (c)-[:KNOWS]->(d) MERGE (c)-[:LIKES]->(d)""" result = graph.query(query) # Verify that every entity was created. self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.relationships_created, 2) self.env.assertEquals(result.properties_set, 2) # Repeat the query. result = graph.query(query) # Verify that no data was modified. self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.relationships_created, 0) self.env.assertEquals(result.properties_set, 0) def test23_merge_var_traverse(self): redis_con = self.env.getConnection() graph = Graph("M", redis_con) # Starting with an empty graph. # Make sure the pattern ()-[]->()-[]->()-[]->() exists. self.env.flush() query = """MERGE (a {v:1}) MERGE (b {v:2}) MERGE (a)-[:KNOWS]->(b) WITH a AS c, b AS d MATCH (c)-[:KNOWS*]->(d) MERGE (c)-[:LIKES]->(d)""" result = graph.query(query) # Verify that every entity was created. self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.relationships_created, 2) self.env.assertEquals(result.properties_set, 2) # Repeat the query. result = graph.query(query) # Verify that no data was modified. self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.relationships_created, 0) self.env.assertEquals(result.properties_set, 0) def test24_merge_merge_delete(self): redis_con = self.env.getConnection() graph = Graph("M", redis_con) # Merge followed by an additional merge and ending with a deletion # which doesn't have any data to operate on, # this used to trigger force lock release, as the delete didn't tried to acquire/release the lock self.env.flush() query = """MERGE (user:User {name:'Sceat'}) WITH user UNWIND [1,2,3] AS sessionHash MERGE (user)-[:HAS_SESSION]->(newSession:Session {hash:sessionHash}) WITH DISTINCT user, collect(newSession.hash) as newSessionHash MATCH (user)-->(s:Session) WHERE NOT s.hash IN newSessionHash DELETE s""" result = graph.query(query) # Verify that every entity was created. self.env.assertEquals(result.nodes_created, 4) self.env.assertEquals(result.properties_set, 4) self.env.assertEquals(result.relationships_created, 3) # Repeat the query. result = graph.query(query) # Verify that no data was modified. self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.properties_set, 0) self.env.assertEquals(result.relationships_created, 0) def test25_merge_with_where(self): redis_con = self.env.getConnection() graph = Graph("M", redis_con) # Index the "L:prop) combination so that the MERGE tree will not have a filter op. query = """CREATE INDEX ON :L(prop)""" graph.query(query) query = """MERGE (n:L {prop:1}) WITH n WHERE n.prop < 1 RETURN n.prop""" result = graph.query(query) plan = graph.execution_plan(query) # Verify that the Filter op follows a Project op. self.env.assertTrue(re.search('Project\s+Filter', plan)) # Verify that there is no Filter op after the Merge op. self.env.assertFalse(re.search('Merge\s+Filter', plan)) # Verify that the entity was created and no results were returned. self.env.assertEquals(result.nodes_created, 1) self.env.assertEquals(result.properties_set, 1) # Repeat the query. result = graph.query(query) # Verify that no data was modified and no results were returned. self.env.assertEquals(result.nodes_created, 0) self.env.assertEquals(result.properties_set, 0) def test26_merge_set_invalid_property(self): redis_con = self.env.getConnection() graph = Graph("M", redis_con) query = """MATCH p=() MERGE () ON MATCH SET p.prop4 = 5""" result = graph.query(query) self.env.assertEquals(result.properties_set, 0) def test27_merge_create_invalid_entity(self): # Skip this test if running under Valgrind, as it causes a memory leak. if Env().envRunner.debugger is not None: Env().skip() redis_con = self.env.getConnection() graph = Graph("N", redis_con) # Instantiate a new graph. try: # Try to create a node with an invalid NULL property. query = """MERGE (n {v: NULL})""" graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("Cannot merge node using null property value" in e.message) pass # Verify that no entities were created. query = """MATCH (a) RETURN a""" result = graph.query(query) self.env.assertEquals(result.result_set, []) try: # Try to merge a node with a self-referential property. query = """MERGE (a:L {v: a.v})""" graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. self.env.assertIn("undefined property", e.message)
class testFunctionCallsFlow(FlowTestsBase): def __init__(self): self.env = Env() global graph global redis_con redis_con = self.env.getConnection() graph = Graph("G", redis_con) self.populate_graph() def populate_graph(self): global graph nodes = {} # Create entities for idx, p in enumerate(people): node = Node(label="person", properties={"name": p, "val": idx}) graph.add_node(node) nodes[p] = node # Fully connected graph for src in nodes: for dest in nodes: if src != dest: edge = Edge(nodes[src], "know", nodes[dest]) graph.add_edge(edge) for src in nodes: for dest in nodes: if src != dest: edge = Edge(nodes[src], "works_with", nodes[dest]) graph.add_edge(edge) graph.commit() query = """MATCH (a)-[:know]->(b) CREATE (a)-[:know]->(b)""" graph.query(query) def expect_type_error(self, query): try: graph.query(query) assert(False) except redis.exceptions.ResponseError as e: # Expecting a type error. self.env.assertIn("Type mismatch", e.message) def expect_error(self, query, expected_err_msg): try: graph.query(query) assert(False) except redis.exceptions.ResponseError as e: # Expecting a type error. self.env.assertIn(expected_err_msg, e.message) # Validate capturing of errors prior to query execution. def test01_compile_time_errors(self): query = """RETURN toUpper(5)""" self.expect_type_error(query) query = """RETURN 'a' * 2""" self.expect_type_error(query) query = """RETURN max(1 + min(2))""" self.expect_error(query, "Can't use aggregate functions inside of aggregate functions") def test02_boolean_comparisons(self): query = """RETURN true = 5""" actual_result = graph.query(query) expected_result = [[False]] self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN true <> 'str'""" actual_result = graph.query(query) expected_result = [[True]] self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN 'anything' <> NULL""" actual_result = graph.query(query) expected_result = [[None]] self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN 'anything' = NULL""" actual_result = graph.query(query) expected_result = [[None]] self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN 10 >= 1.5""" actual_result = graph.query(query) expected_result = [[True]] self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN -1 < 1""" actual_result = graph.query(query) expected_result = [[True]] self.env.assertEquals(actual_result.result_set, expected_result) def test03_boolean_errors(self): query = """RETURN 'str' < 5.5""" self.expect_type_error(query) query = """RETURN true > 5""" self.expect_type_error(query) query = """MATCH (a) RETURN a < 'anything' LIMIT 1""" self.expect_type_error(query) def test04_entity_functions(self): query = "RETURN ID(5)" self.expect_type_error(query) query = "MATCH (a) RETURN ID(a) ORDER BY ID(a) LIMIT 3" actual_result = graph.query(query) expected_result = [[0], [1], [2]] self.env.assertEquals(actual_result.result_set, expected_result) query = "MATCH (a)-[e]->() RETURN ID(e) ORDER BY ID(e) LIMIT 3" actual_result = graph.query(query) expected_result = [[0], [1], [2]] self.env.assertEquals(actual_result.result_set, expected_result) query = "RETURN EXISTS(null)" actual_result = graph.query(query) expected_result = [[False]] self.env.assertEquals(actual_result.result_set, expected_result) query = "RETURN EXISTS('anything')" actual_result = graph.query(query) expected_result = [[True]] self.env.assertEquals(actual_result.result_set, expected_result) def test07_nonmap_errors(self): query = """MATCH (a) WITH a.name AS scalar RETURN scalar.name""" self.expect_type_error(query) def test08_apply_all_function(self): query = "MATCH () RETURN COUNT(*)" actual_result = graph.query(query) expected_result = [[4]] self.env.assertEquals(actual_result.result_set, expected_result) query = "UNWIND [1, 2] AS a RETURN COUNT(*)" actual_result = graph.query(query) expected_result = [[2]] self.env.assertEquals(actual_result.result_set, expected_result) def test09_static_aggregation(self): query = "RETURN count(*)" actual_result = graph.query(query) expected_result = [[1]] self.env.assertEquals(actual_result.result_set, expected_result) query = "RETURN max(2)" actual_result = graph.query(query) expected_result = [[2]] self.env.assertEquals(actual_result.result_set, expected_result) query = "RETURN min(3)" actual_result = graph.query(query) expected_result = [[3]] self.env.assertEquals(actual_result.result_set, expected_result) def test10_modulo_inputs(self): # Validate modulo with integer inputs. query = "RETURN 5 % 2" actual_result = graph.query(query) expected_result = [[1]] self.env.assertEquals(actual_result.result_set, expected_result) # Validate modulo with a floating-point dividend. query = "RETURN 5.5 % 2" actual_result = graph.query(query) expected_result = [[1.5]] self.env.assertEquals(actual_result.result_set, expected_result) # Validate modulo with a floating-point divisor. query = "RETURN 5 % 2.5" actual_result = graph.query(query) expected_result = [[0]] self.env.assertEquals(actual_result.result_set, expected_result) # Validate modulo with both a floating-point dividen and a floating-point divisor. query = "RETURN 5.5 % 2.5" actual_result = graph.query(query) expected_result = [[0.5]] self.env.assertEquals(actual_result.result_set, expected_result) # Validate modulo with negative integer inputs. query = "RETURN -5 % -2" actual_result = graph.query(query) expected_result = [[-1]] self.env.assertEquals(actual_result.result_set, expected_result) # Validate modulo with negative floating-point inputs. query = "RETURN -5.5 % -2.5" actual_result = graph.query(query) expected_result = [[-0.5]] self.env.assertEquals(actual_result.result_set, expected_result) # Aggregate functions should handle null inputs appropriately. def test11_null_aggregate_function_inputs(self): # SUM should sum all non-null inputs. query = """UNWIND [1, NULL, 3] AS a RETURN sum(a)""" actual_result = graph.query(query) expected_result = [[4]] self.env.assertEquals(actual_result.result_set, expected_result) # SUM should return 0 given a fully NULL input. query = """WITH NULL AS a RETURN sum(a)""" actual_result = graph.query(query) expected_result = [[0]] self.env.assertEquals(actual_result.result_set, expected_result) # COUNT should count all non-null inputs. query = """UNWIND [1, NULL, 3] AS a RETURN count(a)""" actual_result = graph.query(query) expected_result = [[2]] self.env.assertEquals(actual_result.result_set, expected_result) # COUNT should return 0 given a fully NULL input. query = """WITH NULL AS a RETURN count(a)""" actual_result = graph.query(query) expected_result = [[0]] self.env.assertEquals(actual_result.result_set, expected_result) # COLLECT should ignore null inputs. query = """UNWIND [1, NULL, 3] AS a RETURN collect(a)""" actual_result = graph.query(query) expected_result = [[[1, 3]]] self.env.assertEquals(actual_result.result_set, expected_result) # COLLECT should return an empty array on all null inputs. query = """WITH NULL AS a RETURN collect(a)""" actual_result = graph.query(query) expected_result = [[[]]] self.env.assertEquals(actual_result.result_set, expected_result)
class testNodeByIDFlow(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) global redis_graph redis_con = self.env.getConnection() redis_graph = Graph(GRAPH_ID, redis_con) self.populate_graph() def populate_graph(self): global redis_graph # Create entities for i in range(10): node = Node(label="person", properties={"id": i}) redis_graph.add_node(node) redis_graph.commit() # Make sure node id attribute matches node's internal ID. query = """MATCH (n) SET n.id = ID(n)""" redis_graph.query(query) # Expect an error when trying to use a function which does not exists. def test_get_nodes(self): # All nodes, not including first node. query = """MATCH (n) WHERE ID(n) > 0 RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE n.id > 0 RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) query = """MATCH (n) WHERE 0 < ID(n) RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE 0 < n.id RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) # All nodes. query = """MATCH (n) WHERE ID(n) >= 0 RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE n.id >= 0 RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) query = """MATCH (n) WHERE 0 <= ID(n) RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE 0 <= n.id RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) # A single node. query = """MATCH (n) WHERE ID(n) = 0 RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE n.id = 0 RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) # 4 nodes (6,7,8,9) query = """MATCH (n) WHERE ID(n) > 5 RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE n.id > 5 RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) query = """MATCH (n) WHERE 5 < ID(n) RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE 5 < n.id RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) # 5 nodes (5, 6,7,8,9) query = """MATCH (n) WHERE ID(n) >= 5 RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE n.id >= 5 RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) query = """MATCH (n) WHERE 5 <= ID(n) RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE 5 <= n.id RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) # 5 nodes (0,1,2,3,4) query = """MATCH (n) WHERE ID(n) < 5 RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE n.id < 5 RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) query = """MATCH (n) WHERE 5 < ID(n) RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE 5 < n.id RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) # 6 nodes (0,1,2,3,4,5) query = """MATCH (n) WHERE ID(n) <= 5 RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE n.id <= 5 RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) query = """MATCH (n) WHERE 5 >= ID(n) RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE 5 >= n.id RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) # All nodes except last one. query = """MATCH (n) WHERE ID(n) < 9 RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE n.id < 9 RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) query = """MATCH (n) WHERE 9 > ID(n) RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE 9 > n.id RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) # All nodes. query = """MATCH (n) WHERE ID(n) <= 9 RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE n.id <= 9 RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) query = """MATCH (n) WHERE 9 >= ID(n) RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE 9 >= n.id RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) # All nodes. query = """MATCH (n) WHERE ID(n) < 100 RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE n.id < 100 RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) query = """MATCH (n) WHERE 100 > ID(n) RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE 100 > n.id RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) # All nodes. query = """MATCH (n) WHERE ID(n) <= 100 RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE n.id <= 100 RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) query = """MATCH (n) WHERE 100 >= ID(n) RETURN n ORDER BY n.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (n) WHERE 100 >= n.id RETURN n ORDER BY n.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) # cartesian product, tests reset works as expected. query = """MATCH (a), (b) WHERE ID(a) > 5 AND ID(b) <= 5 RETURN a,b ORDER BY a.id, b.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (a), (b) WHERE a.id > 5 AND b.id <= 5 RETURN a,b ORDER BY a.id, b.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) query = """MATCH (a), (b) WHERE 5 < ID(a) AND 5 >= ID(b) RETURN a,b ORDER BY a.id, b.id""" resultsetA = redis_graph.query(query).result_set self.env.assertIn("NodeByIdSeek", redis_graph.execution_plan(query)) query = """MATCH (a), (b) WHERE 5 < a.id AND 5 >= b.id RETURN a,b ORDER BY a.id, b.id""" self.env.assertNotIn("NodeByIdSeek", redis_graph.execution_plan(query)) resultsetB = redis_graph.query(query).result_set self.env.assertEqual(resultsetA, resultsetB) # Try to fetch none existing entities by ID(s). def test_for_none_existing_entity_ids(self): # Try to fetch an entity with a none existing ID. queries = [ """MATCH (a:person) WHERE ID(a) = 999 RETURN a""", """MATCH (a:person) WHERE ID(a) > 999 RETURN a""", """MATCH (a:person) WHERE ID(a) > 800 AND ID(a) < 900 RETURN a""" ] for query in queries: resultset = redis_graph.query(query).result_set self.env.assertEquals(len(resultset), 0) # Expecting no results. self.env.assertIn("Node By Label and ID Scan", redis_graph.execution_plan(query))
class testIndexUpdatesFlow(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) global redis_graph redis_con = self.env.getConnection() redis_graph = Graph(GRAPH_ID, redis_con) self.populate_graph() self.build_indices() def new_node(self): return Node(label=labels[node_ctr % 2], properties={ 'unique': node_ctr, 'group': random.choice(groups), 'doubleval': round(random.uniform(-1, 1), 2), 'intval': random.randint(1, 10000), 'stringval': ''.join( random.choice(string.ascii_lowercase) for x in range(6)) }) def populate_graph(self): global node_ctr for i in range(1000): node = self.new_node() redis_graph.add_node(node) node_ctr += 1 redis_graph.commit() def build_indices(self): for field in fields: redis_graph.redis_con.execute_command( "GRAPH.QUERY", GRAPH_ID, "CREATE INDEX ON :label_a(%s)" % (field)) redis_graph.redis_con.execute_command( "GRAPH.QUERY", GRAPH_ID, "CREATE INDEX ON :label_b(%s)" % (field)) # Validate that all properties are indexed def validate_indexed(self): for field in fields: resp = redis_graph.execution_plan( """MATCH (a:label_a) WHERE a.%s > 0 RETURN a""" % (field)) self.env.assertIn('Index Scan', resp) # So long as 'unique' is not modified, label_a.unique will always be even and label_b.unique will always be odd def validate_unique(self): result = redis_graph.query("MATCH (a:label_a) RETURN a.unique") # Remove the header result.result_set.pop(0) for val in result.result_set: self.env.assertEquals(int(float(val[0])) % 2, 0) result = redis_graph.query("MATCH (b:label_b) RETURN b.unique") # Remove the header result.result_set.pop(0) for val in result.result_set: self.env.assertEquals(int(float(val[0])) % 2, 1) # The index scan ought to return identical results to a label scan over the same range of values. def validate_doubleval(self): for label in labels: resp = redis_graph.execution_plan( """MATCH (a:%s) WHERE a.doubleval < 100 RETURN a.doubleval ORDER BY a.doubleval""" % (label)) self.env.assertIn('Index Scan', resp) indexed_result = redis_graph.query( """MATCH (a:%s) WHERE a.doubleval < 100 RETURN a.doubleval ORDER BY a.doubleval""" % (label)) scan_result = redis_graph.query( """MATCH (a:%s) RETURN a.doubleval ORDER BY a.doubleval""" % (label)) self.env.assertEqual(len(indexed_result.result_set), len(scan_result.result_set)) # Collect any elements between the two result sets that fail a string comparison # so that we may compare them as doubles (specifically, -0 and 0 should be considered equal) differences = [[i[0], j[0]] for i, j in zip( indexed_result.result_set, scan_result.result_set) if i != j] for pair in differences: self.env.assertEqual(float(pair[0]), float(pair[1])) # The intval property can be assessed similar to doubleval, but the result sets should be identical def validate_intval(self): for label in labels: resp = redis_graph.execution_plan( """MATCH (a:%s) WHERE a.intval > 0 RETURN a.intval ORDER BY a.intval""" % (label)) self.env.assertIn('Index Scan', resp) indexed_result = redis_graph.query( """MATCH (a:%s) WHERE a.intval > 0 RETURN a.intval ORDER BY a.intval""" % (label)) scan_result = redis_graph.query( """MATCH (a:%s) RETURN a.intval ORDER BY a.intval""" % (label)) self.env.assertEqual(indexed_result.result_set, scan_result.result_set) # Validate a series of premises to ensure that the graph has not been modified unexpectedly def validate_state(self): self.validate_unique() self.validate_indexed() self.validate_doubleval() self.validate_intval() # Modify a property, triggering updates to all nodes in two indices def test01_full_property_update(self): result = redis_graph.query( "MATCH (a) SET a.doubleval = a.doubleval + %f" % (round(random.uniform(-1, 1), 2))) self.env.assertEquals(result.properties_set, 1000) # Verify that index scans still function and return correctly self.validate_state() # Modify a property, triggering updates to a subset of nodes in two indices def test02_partial_property_update(self): redis_graph.query( "MATCH (a) WHERE a.doubleval > 0 SET a.doubleval = a.doubleval + %f" % (round(random.uniform(-1, 1), 2))) # Verify that index scans still function and return correctly self.validate_state() # Add 100 randomized nodes and validate indices def test03_node_creation(self): # Reset nodes in the Graph object so that we won't double-commit the originals redis_graph.nodes = {} global node_ctr for i in range(100): node = self.new_node() redis_graph.add_node(node) node_ctr += 1 redis_graph.commit() self.validate_state() # Delete every other node in first 100 and validate indices def test04_node_deletion(self): # Reset nodes in the Graph object so that we won't double-commit the originals redis_graph.nodes = {} global node_ctr # Delete nodes one at a time for i in range(0, 100, 2): result = redis_graph.query("MATCH (a) WHERE ID(a) = %d DELETE a" % (i)) self.env.assertEquals(result.nodes_deleted, 1) node_ctr -= 1 self.validate_state() # Delete all nodes matching a filter result = redis_graph.query( "MATCH (a:label_a) WHERE a.group = 'Group A' DELETE a") self.env.assertGreater(result.nodes_deleted, 0) self.validate_state() def test05_unindexed_property_update(self): # Add an unindexed property to all nodes. redis_graph.query("MATCH (a) SET a.unindexed = 'unindexed'") # Retrieve a single node result = redis_graph.query("MATCH (a) RETURN a.unique LIMIT 1") unique_prop = result.result_set[0][0] query = """MATCH (a {unique: %s }) SET a.unindexed = 5, a.unique = %s RETURN a.unindexed, a.unique""" % ( unique_prop, unique_prop) result = redis_graph.query(query) expected_result = [[5, unique_prop]] self.env.assertEquals(result.result_set, expected_result) self.env.assertEquals(result.properties_set, 1) # Validate that after deleting an indexed property, that property can no longer be found in the index. def test06_remove_indexed_prop(self): # Create a new node with a single indexed property query = """CREATE (:NEW {v: 5})""" result = redis_graph.query(query) self.env.assertEquals(result.properties_set, 1) self.env.assertEquals(result.labels_added, 1) redis_graph.query("CREATE INDEX ON :NEW(v)") # Delete the entity's property query = """MATCH (a:NEW {v: 5}) SET a.v = NULL""" result = redis_graph.query(query) self.env.assertEquals(result.properties_set, 1) # Query the index for the entity query = """MATCH (a:NEW {v: 5}) RETURN a""" plan = redis_graph.execution_plan(query) self.env.assertIn("Index Scan", plan) result = redis_graph.query(query) # No entities should be returned expected_result = [] self.env.assertEquals(result.result_set, expected_result) # Validate that when a label has both exact-match and full-text indexes # on different properties, an update operation checks all indexes to # determine whether they must be updated. # This is necessary because either one of the indexes may not track the # property being updated, but that does not guarantee that the other # index does not track the property. def test07_update_property_only_on_fulltext_index(self): # Remove the exact-match index on a property redis_graph.redis_con.execute_command("GRAPH.QUERY", GRAPH_ID, "DROP INDEX ON :label_a(group)") # Add a full-text index on the property redis_graph.query( "CALL db.idx.fulltext.createNodeIndex('label_a', 'group')") # Modify the values of the property result = redis_graph.query( "MATCH (a:label_a) WHERE a.group = 'Group C' SET a.group = 'Group NEW'" ) modified_count = result.properties_set self.env.assertGreater(modified_count, 0) # Validate that the full-text index reflects the update result = redis_graph.query( "CALL db.idx.fulltext.queryNodes('label_a', 'Group NEW')") self.env.assertEquals(len(result.result_set), modified_count) # Validate that the previous value has been removed result = redis_graph.query( "CALL db.idx.fulltext.queryNodes('label_a', 'Group C')") self.env.assertEquals(len(result.result_set), 0)
class testGraphCreationFlow(FlowTestsBase): def __init__(self): self.env = Env() global redis_graph redis_con = self.env.getConnection() redis_graph = Graph(GRAPH_ID, redis_con) def test01_create_return(self): query = """CREATE (a:person {name:'A'}), (b:person {name:'B'})""" result = redis_graph.query(query) self.env.assertEquals(result.nodes_created, 2) query = """MATCH (src:person) CREATE (src)-[e:knows]->(dest {name:'C'}) RETURN src,e,dest ORDER BY ID(src) DESC LIMIT 1""" result = redis_graph.query(query) self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.relationships_created, 2) self.env.assertEquals(len(result.result_set), 1) self.env.assertEquals(result.result_set[0][0].properties['name'], 'B') def test02_create_from_prop(self): query = """MATCH (p:person)-[e:knows]->() CREATE (c:clone {doublename: p.name + toLower(p.name), source_of: TYPE(e)}) RETURN c.doublename, c.source_of ORDER BY c.doublename""" result = redis_graph.query(query) expected_result = [['Aa', 'knows'], ['Bb', 'knows']] self.env.assertEquals(result.labels_added, 1) self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.properties_set, 4) self.env.assertEquals(result.result_set, expected_result) def test03_create_from_projection(self): query = """UNWIND [10,20,30] AS x CREATE (p:person {age:x}) RETURN p.age ORDER BY p.age""" result = redis_graph.query(query) expected_result = [[10], [20], [30]] self.env.assertEquals(result.nodes_created, 3) self.env.assertEquals(result.properties_set, 3) self.env.assertEquals(result.result_set, expected_result)
class testGraphCreationFlow(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) global redis_graph redis_con = self.env.getConnection() redis_graph = Graph(GRAPH_ID, redis_con) def test01_create_return(self): query = """CREATE (a:person {name:'A'}), (b:person {name:'B'})""" result = redis_graph.query(query) self.env.assertEquals(result.nodes_created, 2) query = """MATCH (src:person) CREATE (src)-[e:knows]->(dest {name:'C'}) RETURN src,e,dest ORDER BY ID(src) DESC LIMIT 1""" result = redis_graph.query(query) self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.relationships_created, 2) self.env.assertEquals(len(result.result_set), 1) self.env.assertEquals(result.result_set[0][0].properties['name'], 'B') def test02_create_from_prop(self): query = """MATCH (p:person)-[e:knows]->() CREATE (c:clone {doublename: p.name + toLower(p.name), source_of: TYPE(e)}) RETURN c.doublename, c.source_of ORDER BY c.doublename""" result = redis_graph.query(query) expected_result = [['Aa', 'knows'], ['Bb', 'knows']] self.env.assertEquals(result.labels_added, 1) self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.properties_set, 4) self.env.assertEquals(result.result_set, expected_result) def test03_create_from_projection(self): query = """UNWIND [10,20,30] AS x CREATE (p:person {age:x}) RETURN p.age ORDER BY p.age""" result = redis_graph.query(query) expected_result = [[10], [20], [30]] self.env.assertEquals(result.nodes_created, 3) self.env.assertEquals(result.properties_set, 3) self.env.assertEquals(result.result_set, expected_result) query = """UNWIND ['Vancouver', 'Portland', 'Calgary'] AS city CREATE (p:person {birthplace: city}) RETURN p.birthplace ORDER BY p.birthplace""" result = redis_graph.query(query) expected_result = [['Calgary'], ['Portland'], ['Vancouver']] self.env.assertEquals(result.nodes_created, 3) self.env.assertEquals(result.properties_set, 3) self.env.assertEquals(result.result_set, expected_result) def test04_create_with_null_properties(self): query = """CREATE (a:L {v1: NULL, v2: 'prop'}) RETURN a""" result = redis_graph.query(query) node = Node(label="L", properties={"v2": "prop"}) expected_result = [[node]] self.env.assertEquals(result.labels_added, 1) self.env.assertEquals(result.nodes_created, 1) self.env.assertEquals(result.properties_set, 1) self.env.assertEquals(result.result_set, expected_result) # Create 2 new nodes, one with no properties and one with a property 'v' query = """CREATE (:M), (:M {v: 1})""" redis_graph.query(query) # Verify that a MATCH...CREATE accesses the property correctly. query = """MATCH (m:M) WITH m ORDER BY m.v DESC CREATE ({v: m.v})""" result = redis_graph.query(query) self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.properties_set, 1) def test05_create_with_property_reference(self): # Skip this test if running under Valgrind, as it causes a memory leak. if Env().envRunner.debugger is not None: Env().skip() # Queries that reference properties before they have been created should emit an error. try: query = """CREATE (a {val: 2}), (b {val: a.val})""" redis_graph.query(query) self.env.assertTrue(False) except redis.exceptions.ResponseError as e: self.env.assertIn("undefined property", str(e)) def test06_create_project_volatile_value(self): # The path e is volatile; verify that it can be projected after entity creation. query = """MATCH ()-[e*]->() CREATE (:L) WITH e RETURN 5""" result = redis_graph.query(query) expected_result = [[5], [5]] self.env.assertEquals(result.nodes_created, 2) self.env.assertEquals(result.result_set, expected_result) query = """UNWIND [1, 2] AS val WITH collect(val) AS arr CREATE (:L) RETURN arr""" result = redis_graph.query(query) expected_result = [[[1, 2]]] self.env.assertEquals(result.nodes_created, 1) self.env.assertEquals(result.result_set, expected_result)
class testMultiExecFlow(FlowTestsBase): def __init__(self): self.env = Env() global redis_con redis_con = self.env.getConnection() def test_graph_entities(self): # Delete previous graph if exists. redis_con.execute_command("DEL", GRAPH_ID) # Start a multi exec transaction. redis_con.execute_command("MULTI") # Create graph. redis_con.execute_command("GRAPH.QUERY", GRAPH_ID, CREATE_QUERY) # Count outgoing connections from Al, expecting 2 edges. # (Al)-[e]->() count (e) redis_con.execute_command("GRAPH.QUERY", GRAPH_ID, MATCH_QUERY) # Disconnect edge connecting Al to Betty. # (Al)-[e]->(Betty) delete (e) redis_con.execute_command("GRAPH.QUERY", GRAPH_ID, DEL_QUERY) # Count outgoing connections from Al, expecting 1 edges. # (Al)-[e]->() count (e) redis_con.execute_command("GRAPH.QUERY", GRAPH_ID, MATCH_QUERY) # Change Al name from Al to Steve. # (Al) set Al.name = Steve redis_con.execute_command("GRAPH.QUERY", GRAPH_ID, UPDATE_QUERY) # Count outgoing connections from Al, expecting 0 edges. # (Al)-[e]->() count (e) redis_con.execute_command("GRAPH.QUERY", GRAPH_ID, MATCH_QUERY) # Commit transaction. results = redis_con.execute_command("EXEC") # [ # [ # ['al.name', 'count(b)'], # ['Al', '2'] # ], # ['Query internal execution time: 0.143000 milliseconds'] # ] two_edges = results[1] two_edges = two_edges[1][0][1] self.env.assertEquals(two_edges, 2) one_edge = results[3] one_edge = one_edge[1][0][1] self.env.assertEquals(one_edge, 1) no_edges = results[5] no_edges = no_edges[1] self.env.assertEquals(len(no_edges), 0) def test_transaction_failure(self): redis_con_a = self.env.getConnection() redis_con_b = self.env.getConnection() results = redis_con_b.execute_command("INFO", "CLIENTS") self.env.assertGreaterEqual(results['connected_clients'], 2) # Delete previous graph if exists. redis_con_a.execute_command("DEL", GRAPH_ID) redis_con_a.execute_command("GRAPH.QUERY", GRAPH_ID, CREATE_QUERY) redis_con_a.execute_command("WATCH", GRAPH_ID) redis_con_a.execute_command("MULTI") redis_con_a.execute_command("GRAPH.QUERY", GRAPH_ID, MATCH_QUERY) results = redis_con_a.execute_command("EXEC") self.env.assertNotEqual(results, None) # read only query from client B - transaction OK redis_con_a.execute_command("WATCH", GRAPH_ID) redis_con_a.execute_command("MULTI") redis_con_b.execute_command("GRAPH.QUERY", GRAPH_ID, MATCH_QUERY) redis_con_a.execute_command("GRAPH.QUERY", GRAPH_ID, MATCH_QUERY) results = redis_con_a.execute_command("EXEC") self.env.assertNotEqual(results, None) # write query from client B - transaction fails redis_con_a.execute_command("WATCH", GRAPH_ID) redis_con_a.execute_command("MULTI") redis_con_b.execute_command("GRAPH.QUERY", GRAPH_ID, UPDATE_QUERY) redis_con_a.execute_command("GRAPH.QUERY", GRAPH_ID, MATCH_QUERY) results = redis_con_a.execute_command("EXEC") self.env.assertEqual(results, None) # GRAPH.EXPLAIN is read only - no data change - transaction OK redis_con_a.execute_command("WATCH", GRAPH_ID) redis_con_a.execute_command("MULTI") redis_con_b.execute_command("GRAPH.EXPLAIN", GRAPH_ID, UPDATE_QUERY) redis_con_a.execute_command("GRAPH.QUERY", GRAPH_ID, MATCH_QUERY) results = redis_con_a.execute_command("EXEC") self.env.assertNotEqual(results, None)
class testIndexUpdatesFlow(FlowTestsBase): def __init__(self): self.env = Env() global redis_graph redis_con = self.env.getConnection() redis_graph = Graph(GRAPH_ID, redis_con) self.populate_graph() self.build_indices() def new_node(self): return Node(label=labels[node_ctr % 2], properties={ 'unique': node_ctr, 'group': random.choice(groups), 'doubleval': round(random.uniform(-1, 1), 2), 'intval': random.randint(1, 10000), 'stringval': ''.join( random.choice(string.lowercase) for x in range(6)) }) def populate_graph(self): global node_ctr for i in range(1000): node = self.new_node() redis_graph.add_node(node) node_ctr += 1 redis_graph.commit() def build_indices(self): for field in fields: redis_graph.redis_con.execute_command( "GRAPH.QUERY", GRAPH_ID, "CREATE INDEX ON :label_a(%s)" % (field)) redis_graph.redis_con.execute_command( "GRAPH.QUERY", GRAPH_ID, "CREATE INDEX ON :label_b(%s)" % (field)) # Validate that all properties are indexed def validate_indexed(self): for field in fields: resp = redis_graph.execution_plan( """MATCH (a:label_a) WHERE a.%s > 0 RETURN a""" % (field)) self.env.assertIn('Index Scan', resp) # So long as 'unique' is not modified, label_a.unique will always be even and label_b.unique will always be odd def validate_unique(self): result = redis_graph.query("MATCH (a:label_a) RETURN a.unique") # Remove the header result.result_set.pop(0) for val in result.result_set: self.env.assertEquals(int(float(val[0])) % 2, 0) result = redis_graph.query("MATCH (b:label_b) RETURN b.unique") # Remove the header result.result_set.pop(0) for val in result.result_set: self.env.assertEquals(int(float(val[0])) % 2, 1) # The index scan ought to return identical results to a label scan over the same range of values. def validate_doubleval(self): for label in labels: resp = redis_graph.execution_plan( """MATCH (a:%s) WHERE a.doubleval < 100 RETURN a.doubleval ORDER BY a.doubleval""" % (label)) self.env.assertIn('Index Scan', resp) indexed_result = redis_graph.query( """MATCH (a:%s) WHERE a.doubleval < 100 RETURN a.doubleval ORDER BY a.doubleval""" % (label)) scan_result = redis_graph.query( """MATCH (a:%s) RETURN a.doubleval ORDER BY a.doubleval""" % (label)) self.env.assertEqual(len(indexed_result.result_set), len(scan_result.result_set)) # Collect any elements between the two result sets that fail a string comparison # so that we may compare them as doubles (specifically, -0 and 0 should be considered equal) differences = [[i[0], j[0]] for i, j in zip( indexed_result.result_set, scan_result.result_set) if i != j] for pair in differences: self.env.assertEqual(float(pair[0]), float(pair[1])) # The intval property can be assessed similar to doubleval, but the result sets should be identical def validate_intval(self): for label in labels: resp = redis_graph.execution_plan( """MATCH (a:%s) WHERE a.intval > 0 RETURN a.intval ORDER BY a.intval""" % (label)) self.env.assertIn('Index Scan', resp) indexed_result = redis_graph.query( """MATCH (a:%s) WHERE a.intval > 0 RETURN a.intval ORDER BY a.intval""" % (label)) scan_result = redis_graph.query( """MATCH (a:%s) RETURN a.intval ORDER BY a.intval""" % (label)) self.env.assertEqual(indexed_result.result_set, scan_result.result_set) # Validate a series of premises to ensure that the graph has not been modified unexpectedly def validate_state(self): self.validate_unique() self.validate_indexed() self.validate_doubleval() self.validate_intval() # Modify a property, triggering updates to all nodes in two indices def test01_full_property_update(self): result = redis_graph.query( "MATCH (a) SET a.doubleval = a.doubleval + %f" % (round(random.uniform(-1, 1), 2))) self.env.assertEquals(result.properties_set, 1000) # Verify that index scans still function and return correctly self.validate_state() # Modify a property, triggering updates to a subset of nodes in two indices def test02_partial_property_update(self): redis_graph.query( "MATCH (a) WHERE a.doubleval > 0 SET a.doubleval = a.doubleval + %f" % (round(random.uniform(-1, 1), 2))) # Verify that index scans still function and return correctly self.validate_state() # Add 100 randomized nodes and validate indices def test03_node_creation(self): # Reset nodes in the Graph object so that we won't double-commit the originals redis_graph.nodes = {} global node_ctr for i in range(100): node = self.new_node() redis_graph.add_node(node) node_ctr += 1 redis_graph.commit() self.validate_state() # Delete every other node in first 100 and validate indices def test04_node_deletion(self): # Reset nodes in the Graph object so that we won't double-commit the originals redis_graph.nodes = {} global node_ctr # Delete nodes one at a time for i in range(0, 100, 2): result = redis_graph.query("MATCH (a) WHERE ID(a) = %d DELETE a" % (i)) self.env.assertEquals(result.nodes_deleted, 1) node_ctr -= 1 self.validate_state() # Delete all nodes matching a filter result = redis_graph.query( "MATCH (a:label_a) WHERE a.group = 'Group A' DELETE a") self.env.assertGreater(result.nodes_deleted, 0) self.validate_state() def test05_unindexed_property_update(self): # Add an unindexed property to all nodes. redis_graph.query("MATCH (a) SET a.unindexed = 'unindexed'") # Retrieve a single node result = redis_graph.query("MATCH (a) RETURN a.unique LIMIT 1") unique_prop = result.result_set[0][0] query = """MATCH (a {unique: %s }) SET a.unindexed = 5, a.unique = %s RETURN a.unindexed, a.unique""" % ( unique_prop, unique_prop) result = redis_graph.query(query) expected_result = [[5, unique_prop]] self.env.assertEquals(result.result_set, expected_result) self.env.assertEquals(result.properties_set, 2)
class testOptionalFlow(FlowTestsBase): def __init__(self): self.env = Env() global redis_graph redis_con = self.env.getConnection() redis_graph = Graph("optional_match", redis_con) self.populate_graph() def populate_graph(self): global nodes # Construct a graph with the form: # (v1)-[:E1]->(v2)-[:E2]->(v3), (v4) node_props = ['v1', 'v2', 'v3', 'v4'] for idx, v in enumerate(node_props): node = Node(label="L", properties={"v": v}) nodes[v] = node redis_graph.add_node(node) edge = Edge(nodes['v1'], "E1", nodes['v2']) redis_graph.add_edge(edge) edge = Edge(nodes['v2'], "E2", nodes['v3']) redis_graph.add_edge(edge) redis_graph.flush() # Optional MATCH clause that does not interact with the mandatory MATCH. def test01_disjoint_optional(self): global redis_graph query = """MATCH (a {v: 'v1'}) OPTIONAL MATCH (b) RETURN a.v, b.v ORDER BY a.v, b.v""" actual_result = redis_graph.query(query) expected_result = [['v1', 'v1'], ['v1', 'v2'], ['v1', 'v3'], ['v1', 'v4']] self.env.assertEquals(actual_result.result_set, expected_result) # Optional MATCH clause that extends the mandatory MATCH pattern and has matches for all results. def test02_optional_traverse(self): global redis_graph query = """MATCH (a) WHERE a.v IN ['v1', 'v2'] OPTIONAL MATCH (a)-[]->(b) RETURN a.v, b.v ORDER BY a.v, b.v""" actual_result = redis_graph.query(query) expected_result = [['v1', 'v2'], ['v2', 'v3']] self.env.assertEquals(actual_result.result_set, expected_result) # Optional MATCH clause that extends the mandatory MATCH pattern and has null results. def test03_optional_traverse_with_nulls(self): global redis_graph query = """MATCH (a) OPTIONAL MATCH (a)-[]->(b) RETURN a.v, b.v ORDER BY a.v, b.v""" actual_result = redis_graph.query(query) # (v3) and (v4) have no outgoing edges. expected_result = [['v1', 'v2'], ['v2', 'v3'], ['v3', None], ['v4', None]] self.env.assertEquals(actual_result.result_set, expected_result) # Optional MATCH clause that extends the mandatory MATCH pattern and has a WHERE clause. def test04_optional_traverse_with_predicate(self): global redis_graph query = """MATCH (a) OPTIONAL MATCH (a)-[]->(b) WHERE b.v = 'v2' RETURN a.v, b.v ORDER BY a.v, b.v""" actual_result = redis_graph.query(query) # only (v1) has an outgoing edge to (v2). expected_result = [['v1', 'v2'], ['v2', None], ['v3', None], ['v4', None]] self.env.assertEquals(actual_result.result_set, expected_result) # Optional MATCH clause with endpoints resolved by the mandatory MATCH pattern. def test05_optional_expand_into(self): global redis_graph query = """MATCH (a)-[]->(b) OPTIONAL MATCH (a)-[e]->(b) RETURN a.v, b.v, TYPE(e) ORDER BY a.v, b.v""" actual_result = redis_graph.query(query) expected_result = [['v1', 'v2', 'E1'], ['v2', 'v3', 'E2']] self.env.assertEquals(actual_result.result_set, expected_result) # The OPTIONAL MATCH exactly repeats the MATCH, producing identical results. query_without_optional = """MATCH (a)-[e]->(b) RETURN a.v, b.v, TYPE(e) ORDER BY a.v, b.v""" result_without_optional = redis_graph.query(query_without_optional) self.env.assertEquals(actual_result.result_set, result_without_optional.result_set) # Optional MATCH clause with endpoints resolved by the mandatory MATCH pattern and new filters introduced. def test06_optional_expand_into_with_reltype(self): global redis_graph query = """MATCH (a)-[]->(b) OPTIONAL MATCH (a)-[e:E2]->(b) RETURN a.v, b.v, TYPE(e) ORDER BY a.v, b.v""" actual_result = redis_graph.query(query) # Only (v2)-[E2]->(v3) fulfills the constraint of the OPTIONAL MATCH clause. expected_result = [['v1', 'v2', None], ['v2', 'v3', 'E2']] self.env.assertEquals(actual_result.result_set, expected_result) # Optional MATCH clause with endpoints resolved by the mandatory MATCH pattern, but no mandatory traversal. def test07_optional_expand_into_cartesian_product(self): global redis_graph query = """MATCH (a {v: 'v1'}), (b) OPTIONAL MATCH (a)-[e]->(b) RETURN a.v, b.v, TYPE(e) ORDER BY a.v, b.v""" actual_result = redis_graph.query(query) # All nodes are represented, but (v1)-[E1]->(v2) is the only matching connection. expected_result = [['v1', 'v1', None], ['v1', 'v2', 'E1'], ['v1', 'v3', None], ['v1', 'v4', None]] self.env.assertEquals(actual_result.result_set, expected_result) # TODO ExpandInto doesn't evaluate bidirectionally properly # Optional MATCH clause with endpoints resolved by the mandatory MATCH pattern and a bidirectional optional pattern. # def test08_optional_expand_into_bidirectional(self): # global redis_graph # query = """MATCH (a), (b {v: 'v2'}) OPTIONAL MATCH (a)-[e]-(b) RETURN a.v, b.v, TYPE(e) ORDER BY a.v, b.v""" # actual_result = redis_graph.query(query) # # All nodes are represented, but only edges with (v2) as an endpoint match. # expected_result = [['v1', 'v2', 'E1'], # ['v2', 'v2', None], # ['v3', 'v2', 'E2'], # ['v3', 'v2', None]] # self.env.assertEquals(actual_result.result_set, expected_result) # Optional MATCH clause with variable-length traversal and some results match. def test09_optional_variable_length(self): global redis_graph query = """MATCH (a) OPTIONAL MATCH (a)-[*]->(b) RETURN a.v, b.v ORDER BY a.v, b.v""" actual_result = redis_graph.query(query) expected_result = [['v1', 'v2'], ['v1', 'v3'], ['v2', 'v3'], ['v3', None], ['v4', None]] self.env.assertEquals(actual_result.result_set, expected_result) # Optional MATCH clause with variable-length traversal and all results match. def test10_optional_variable_length_all_matches(self): global redis_graph query = """MATCH (a {v: 'v1'}) OPTIONAL MATCH (a)-[*]->(b) RETURN a.v, b.v ORDER BY a.v, b.v""" actual_result = redis_graph.query(query) expected_result = [['v1', 'v2'], ['v1', 'v3']] self.env.assertEquals(actual_result.result_set, expected_result) # Optional MATCH clause with a variable-length traversal that has no matches. def test11_optional_variable_length_no_matches(self): global redis_graph query = """MATCH (a {v: 'v3'}) OPTIONAL MATCH (a)-[*]->(b) RETURN a.v, b.v ORDER BY a.v, b.v""" actual_result = redis_graph.query(query) expected_result = [['v3', None]] self.env.assertEquals(actual_result.result_set, expected_result) # Multiple interdependent optional MATCH clauses. def test12_multiple_optional_traversals(self): global redis_graph query = """MATCH (a) OPTIONAL MATCH (a)-[]->(b) OPTIONAL MATCH (b)-[]->(c) RETURN a.v, b.v, c.v ORDER BY a.v, b.v, c.v""" actual_result = redis_graph.query(query) expected_result = [['v1', 'v2', 'v3'], ['v2', 'v3', None], ['v3', None, None], ['v4', None, None]] self.env.assertEquals(actual_result.result_set, expected_result) # Multiple interdependent optional MATCH clauses with both directed and bidirectional traversals. def test13_multiple_optional_multi_directional_traversals(self): global redis_graph query = """MATCH (a) OPTIONAL MATCH (a)-[]-(b) OPTIONAL MATCH (b)-[]->(c) RETURN a.v, b.v, c.v ORDER BY a.v, b.v, c.v""" actual_result = redis_graph.query(query) expected_result = [['v1', 'v2', 'v3'], ['v2', 'v1', 'v2'], ['v2', 'v3', None], ['v3', 'v2', 'v3'], ['v4', None, None]] self.env.assertEquals(actual_result.result_set, expected_result) # Multiple interdependent optional MATCH clauses with exclusively bidirectional traversals. def test14_multiple_optional_bidirectional_traversals(self): global redis_graph query = """MATCH (a) OPTIONAL MATCH (a)-[]-(b) OPTIONAL MATCH (b)-[]-(c) RETURN a.v, b.v, c.v ORDER BY a.v, b.v, c.v""" actual_result = redis_graph.query(query) expected_result = [['v1', 'v2', 'v1'], ['v1', 'v2', 'v3'], ['v2', 'v1', 'v2'], ['v2', 'v3', 'v2'], ['v3', 'v2', 'v1'], ['v3', 'v2', 'v3'], ['v4', None, None]] self.env.assertEquals(actual_result.result_set, expected_result) # Build a named path in an optional clause. def test15_optional_named_path(self): global redis_graph query = """MATCH (a) OPTIONAL MATCH p = (a)-[]->(b) RETURN length(p) ORDER BY length(p)""" actual_result = redis_graph.query(query) # 2 nodes have outgoing edges and 2 do not, so expected 2 paths of length 1 and 2 null results. expected_result = [[1], [1], [None], [None]] self.env.assertEquals(actual_result.result_set, expected_result) # Return a result set with null values in the first record and non-null values in subsequent records. def test16_optional_null_first_result(self): global redis_graph query = """MATCH (a) OPTIONAL MATCH (a)-[e]->(b) RETURN a, b, TYPE(e) ORDER BY EXISTS(b), a.v, b.v""" actual_result = redis_graph.query(query) expected_result = [[nodes['v3'], None, None], [nodes['v4'], None, None], [nodes['v1'], nodes['v2'], 'E1'], [nodes['v2'], nodes['v3'], 'E2']] self.env.assertEquals(actual_result.result_set, expected_result) def test17_optional_label_introductions(self): global redis_graph query = """MATCH (a) OPTIONAL MATCH (a:L)-[]->(b:L) RETURN a.v, b.v ORDER BY a.v, b.v""" actual_result = redis_graph.query(query) expected_result = [['v1', 'v2'], ['v2', 'v3'], ['v3', None], ['v4', None]] self.env.assertEquals(actual_result.result_set, expected_result) # Make sure highly connected nodes aren't lost def test18_optional_over_intermidate(self): global redis_graph query = """MATCH (a)-[]->(b)-[]->(c) OPTIONAL MATCH (b)-[]->(c) RETURN a""" plan = redis_graph.execution_plan(query) # Expecting to find "Expand Into" operation as both 'b' and 'c' # are bounded, which means 'b' is treated as an intermidate node # that needs to be tracked. self.env.assertIn("Expand Into", plan)
class testGraphPersistency(FlowTestsBase): def __init__(self): self.env = Env() global redis_con redis_con = self.env.getConnection() def populate_graph(self, graph_name): people = ["Roi", "Alon", "Ailon", "Boaz", "Tal", "Omri", "Ori"] countries = ["Israel", "USA", "Japan", "United Kingdom"] visits = [("Roi", "USA"), ("Alon", "Israel"), ("Ailon", "Japan"), ("Boaz", "United Kingdom")] redis_graph = Graph(graph_name, redis_con) if not redis_con.exists(graph_name): personNodes = {} countryNodes = {} # Create entities for p in people: person = Node(label="person", properties={"name": p}) redis_graph.add_node(person) personNodes[p] = person for p in countries: country = Node(label="country", properties={"name": p}) redis_graph.add_node(country) countryNodes[p] = country for v in visits: person = v[0] country = v[1] edge = Edge(personNodes[person], 'visit', countryNodes[country], properties={'purpose': 'pleasure'}) redis_graph.add_edge(edge) redis_graph.commit() # Delete nodes, to introduce deleted item within our datablock query = """MATCH (n:person) WHERE n.name = 'Roi' or n.name = 'Ailon' DELETE n""" redis_graph.query(query) query = """MATCH (n:country) WHERE n.name = 'USA' DELETE n""" redis_graph.query(query) # Create index. actual_result = redis_con.execute_command( "GRAPH.QUERY", graph_name, "CREATE INDEX ON :person(name)") actual_result = redis_con.execute_command( "GRAPH.QUERY", graph_name, "CREATE INDEX ON :country(name)") return redis_graph def populate_dense_graph(self, dense_graph_name): dense_graph = Graph(dense_graph_name, redis_con) if not redis_con.exists(dense_graph_name): nodes = [] for i in range(10): node = Node(label="n", properties={"val": i}) dense_graph.add_node(node) nodes.append(node) for n_idx, n in enumerate(nodes): for m_idx, m in enumerate(nodes[:n_idx]): dense_graph.add_edge(Edge(n, "connected", m)) dense_graph.flush() return dense_graph # Connect a single node to all other nodes. def test01_save_load_rdb(self): graph_names = ["G", "{tag}_G"] for graph_name in graph_names: redis_graph = self.populate_graph(graph_name) for i in range(2): if i == 1: # Save RDB & Load from RDB self.env.dumpAndReload() # Verify # Expecting 5 person entities. query = """MATCH (p:person) RETURN COUNT(p)""" actual_result = redis_graph.query(query) nodeCount = actual_result.result_set[0][0] self.env.assertEquals(nodeCount, 5) query = """MATCH (p:person) WHERE p.name='Alon' RETURN COUNT(p)""" actual_result = redis_graph.query(query) nodeCount = actual_result.result_set[0][0] self.env.assertEquals(nodeCount, 1) # Expecting 3 country entities. query = """MATCH (c:country) RETURN COUNT(c)""" actual_result = redis_graph.query(query) nodeCount = actual_result.result_set[0][0] self.env.assertEquals(nodeCount, 3) query = """MATCH (c:country) WHERE c.name = 'Israel' RETURN COUNT(c)""" actual_result = redis_graph.query(query) nodeCount = actual_result.result_set[0][0] self.env.assertEquals(nodeCount, 1) # Expecting 2 visit edges. query = """MATCH (n:person)-[e:visit]->(c:country) WHERE e.purpose='pleasure' RETURN COUNT(e)""" actual_result = redis_graph.query(query) edgeCount = actual_result.result_set[0][0] self.env.assertEquals(edgeCount, 2) # Verify indices exists. plan = redis_graph.execution_plan( "MATCH (n:person) WHERE n.name = 'Roi' RETURN n") self.env.assertIn("Index Scan", plan) plan = redis_graph.execution_plan( "MATCH (n:country) WHERE n.name = 'Israel' RETURN n") self.env.assertIn("Index Scan", plan) # Verify that edges are not modified after entity deletion def test02_deleted_entity_migration(self): graph_names = ("H", "{tag}_H") for graph_name in graph_names: dense_graph = self.populate_dense_graph(graph_name) query = """MATCH (p) WHERE ID(p) = 0 OR ID(p) = 3 OR ID(p) = 7 OR ID(p) = 9 DELETE p""" actual_result = dense_graph.query(query) self.env.assertEquals(actual_result.nodes_deleted, 4) query = """MATCH (p)-[]->(q) RETURN p.val, q.val ORDER BY p.val, q.val""" first_result = dense_graph.query(query) # Save RDB & Load from RDB redis_con.execute_command("DEBUG", "RELOAD") second_result = dense_graph.query(query) self.env.assertEquals(first_result.result_set, second_result.result_set) # Strings, numerics, booleans, and array properties should be properly serialized and reloaded def test03_restore_properties(self): graph_names = ("simple_props", "{tag}_simple_props") for graph_name in graph_names: graph = Graph(graph_name, redis_con) query = """CREATE (:p {strval: 'str', numval: 5.5, boolval: true, array: [1,2,3]})""" actual_result = graph.query(query) # Verify that node was created correctly self.env.assertEquals(actual_result.nodes_created, 1) self.env.assertEquals(actual_result.properties_set, 4) # Save RDB & Load from RDB redis_con.execute_command("DEBUG", "RELOAD") query = """MATCH (p) RETURN p.boolval, p.numval, p.strval, p.array""" actual_result = graph.query(query) # Verify that the properties are loaded correctly. expected_result = [[True, 5.5, 'str', [1, 2, 3]]] self.env.assertEquals(actual_result.result_set, expected_result) # Verify multiple edges of the same relation between nodes A and B # are saved and restored correctly. def test04_repeated_edges(self): graph_names = ["repeated_edges", "{tag}_repeated_edges"] for graph_name in graph_names: g = Graph(graph_name, redis_con) src = Node(label='p', properties={'name': 'src'}) dest = Node(label='p', properties={'name': 'dest'}) edge1 = Edge(src, 'e', dest, properties={'val': 1}) edge2 = Edge(src, 'e', dest, properties={'val': 2}) g.add_node(src) g.add_node(dest) g.add_edge(edge1) g.add_edge(edge2) g.flush() # Verify the new edge q = """MATCH (a)-[e]->(b) RETURN e.val, a.name, b.name ORDER BY e.val""" actual_result = g.query(q) expected_result = [[ edge1.properties['val'], src.properties['name'], dest.properties['name'] ], [ edge2.properties['val'], src.properties['name'], dest.properties['name'] ]] self.env.assertEquals(actual_result.result_set, expected_result) # Save RDB & Load from RDB redis_con.execute_command("DEBUG", "RELOAD") # Verify that the latest edge was properly saved and loaded actual_result = g.query(q) self.env.assertEquals(actual_result.result_set, expected_result)
class testQueryValidationFlow(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) global redis_con global redis_graph redis_con = self.env.getConnection() redis_graph = Graph("G", redis_con) self.populate_graph() def populate_graph(self): # Create a single graph. global redis_graph node = Node(properties={"age": 34}) redis_graph.add_node(node) redis_graph.commit() # Expect an error when trying to use a function which does not exists. def test01_none_existing_function(self): query = """MATCH (n) RETURN noneExistingFunc(n.age) AS cast""" try: redis_graph.query(query) self.env.assertTrue(False) except redis.exceptions.ResponseError: # Expecting an error. pass # Make sure function validation is type case insensitive. def test02_case_insensitive_function_name(self): try: query = """MATCH (n) RETURN mAx(n.age)""" redis_graph.query(query) except redis.exceptions.ResponseError: # function validation should be case insensitive. self.env.assertTrue(False) def test03_edge_missing_relation_type(self): try: query = """CREATE (n:Person {age:32})-[]->(:person {age:30})""" redis_graph.query(query) self.env.assertTrue(False) except redis.exceptions.ResponseError: # Expecting an error. pass def test04_escaped_quotes(self): query = r"CREATE (:escaped{prop1:'single \' char', prop2: 'double \" char', prop3: 'mixed \' and \" chars'})" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.nodes_created, 1) self.env.assertEquals(actual_result.properties_set, 3) query = r"MATCH (a:escaped) RETURN a.prop1, a.prop2, a.prop3" actual_result = redis_graph.query(query) expected_result = [[ "single ' char", 'double " char', 'mixed \' and " chars' ]] self.env.assertEquals(actual_result.result_set, expected_result) def test05_invalid_entity_references(self): try: query = """MATCH (a) RETURN e""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass try: query = """MATCH (a) RETURN a ORDER BY e""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass def test06_where_references(self): try: query = """MATCH (a) WHERE fake = true RETURN a""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass def test07_with_references(self): try: query = """MATCH (a) WITH e RETURN e""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass def test08_count_distinct_star(self): try: query = """MATCH (a) RETURN COUNT(DISTINCT *)""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass def test09_invalid_apply_all(self): try: query = """MATCH (a) RETURN SUM(*)""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass def test10_missing_params(self): try: query = """MATCH (a {name:$name}) RETURN a""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass def test11_param_error(self): try: query = """CYPHER name=({name:'a'}) MATCH (a {name:$name}) RETURN a""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass def test12_invalid_query_order(self): try: query = """MERGE (a) MATCH (a)-[]->(b) RETURN b""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass def test13_create_bound_variables(self): try: query = """MATCH (a)-[e]->(b) CREATE (a)-[e]->(b)""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass def test14_treat_path_as_entity(self): redis_graph.query("CREATE ()-[:R]->()") try: query = """MATCH x=()-[]->() RETURN x.name""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass def test15_dont_crash_on_multiple_errors(self): try: query = """MATCH (a) where id(a) IN range(0) OR id(a) in range(1)""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass # Run a query in which a parsed parameter introduces a type in an unsupported context. def test16_param_introduces_unhandled_type(self): try: query = """CYPHER props={a:1,b:2} CREATE (a:A $props)""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("Encountered unhandled type" in str(e)) pass # Validate that the module fails properly with incorrect argument counts. def test17_query_arity(self): # Call GRAPH.QUERY with a missing query argument. try: res = redis_con.execute_command("GRAPH.QUERY", "G") assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("wrong number of arguments" in str(e)) pass # Run queries in which compile-time variables are accessed but not defined. def test18_undefined_variable_access(self): try: query = """CREATE (:person{name:bar[1]})""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("not defined" in str(e)) pass try: query = """MATCH (a {val: undeclared}) RETURN a""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("not defined" in str(e)) pass try: query = """UNWIND [fake] AS ref RETURN ref""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("not defined" in str(e)) pass def test19_invalid_cypher_options(self): query = "EXPLAIN MATCH (p:president)-[:born]->(:state {name:'Hawaii'}) RETURN p" try: redis_graph.query(query) assert (False) except: # Expecting an error. pass query = "PROFILE MATCH (p:president)-[:born]->(:state {name:'Hawaii'}) RETURN p" try: redis_graph.query(query) assert (False) except: # Expecting an error. pass query = "CYPHER val=1 EXPLAIN MATCH (p:president)-[:born]->(:state {name:'Hawaii'}) RETURN p" try: redis_graph.query(query) assert (False) except: # Expecting an error. pass query = "CYPHER val=1 PROFILE MATCH (p:president)-[:born]->(:state {name:'Hawaii'}) RETURN p" try: redis_graph.query(query) assert (False) except: # Expecting an error. pass # Undirected edges are not allowed in CREATE clauses. def test20_undirected_edge_creation(self): try: query = """CREATE (:Endpoint)-[:R]-(:Endpoint)""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("Only directed relationships" in str(e)) pass # Applying a filter for non existing entity. def test20_non_existing_graph_entity(self): try: query = """MATCH p=() WHERE p.name='value' RETURN p""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("Type mismatch: expected Node but was Path" in str(e)) pass # Comments should not affect query functionality. def test21_ignore_query_comments(self): query = """MATCH (n) // This is a comment /* This is a block comment */ WHERE EXISTS(n.age) RETURN n.age /* Also a block comment*/""" actual_result = redis_graph.query(query) expected_result = [[34]] self.env.assertEquals(actual_result.result_set, expected_result) query = """/* A block comment*/ MATCH (n) // This is a comment /* This is a block comment */ WHERE EXISTS(n.age) RETURN n.age /* Also a block comment*/""" actual_result = redis_graph.query(query) expected_result = [[34]] self.env.assertEquals(actual_result.result_set, expected_result) query = """// This is a comment MATCH (n) // This is a comment /* This is a block comment */ WHERE EXISTS(n.age) RETURN n.age /* Also a block comment*/""" actual_result = redis_graph.query(query) expected_result = [[34]] self.env.assertEquals(actual_result.result_set, expected_result) query = """MATCH (n) /* This is a block comment */ WHERE EXISTS(n.age) RETURN n.age /* Also a block comment*/""" actual_result = redis_graph.query(query) expected_result = [[34]] self.env.assertEquals(actual_result.result_set, expected_result) # Validate procedure call refrences and definitions def test22_procedure_validations(self): try: # procedure call refering to a none existing alias 'n' query = """CALL db.idx.fulltext.queryNodes(n, 'B') YIELD node RETURN node""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("not defined" in str(e)) pass # refer to procedure call original output when output is aliased. try: query = """CALL db.idx.fulltext.queryNodes('A', 'B') YIELD node AS n RETURN node""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("not defined" in str(e)) pass # valid procedure call, no output aliasing query = """CALL db.idx.fulltext.queryNodes('A', 'B') YIELD node RETURN node""" redis_graph.query(query) # valid procedure call, output aliasing query = """CALL db.idx.fulltext.queryNodes('A', 'B') YIELD node AS n RETURN n""" redis_graph.query(query) # Applying a filter for a non-boolean constant should raise a compile-time error. def test23_invalid_constant_filter(self): try: query = """MATCH (a) WHERE 1 RETURN a""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: assert ("Expected boolean predicate" in str(e)) pass # Referencing a variable before defining it should raise a compile-time error. def test24_reference_before_definition(self): try: query = """MATCH ({prop: reference}) MATCH (reference) RETURN *""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("not defined" in str(e)) pass # Invalid filters in cartesian products should raise errors. def test25_cartesian_product_invalid_filter(self): try: query = """MATCH p1=(), (n), ({prop: p1.path_val}) RETURN *""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("Type mismatch: expected Node but was Path" in str(e)) pass # Scalar predicates in filters should raise errors. def test26_invalid_filter_predicate(self): try: query = """WITH 1 AS a WHERE '' RETURN a""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("Expected boolean predicate" in str(e)) pass # Conditional filters with non-boolean scalar predicate children should raise errors. def test27_invalid_filter_predicate_child(self): try: # 'Amor' is an invalid construct for the RHS of 'OR'. query = """MATCH (a:Author) WHERE a.name CONTAINS 'Ernest' OR 'Amor' RETURN a""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("Expected boolean predicate" in str(e)) pass # The NOT operator does not compare left and right side expressions. def test28_invalid_filter_binary_not(self): try: # Query should have been: # MATCH (u) where u.v IS NOT NULL RETURN u query = """MATCH (u) where u.v NOT NULL RETURN u""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("Invalid usage of 'NOT' filter" in str(e)) pass def test29_invalid_filter_non_boolean_constant(self): try: query = """MATCH (a) WHERE a RETURN a""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: assert ("expected Boolean but was Node" in str(e)) pass try: query = """MATCH (a) WHERE 1+rand() RETURN a""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: assert ("expected Boolean but was Float" in str(e)) pass try: query = """CYPHER p=3 WITH 1 AS a WHERE $p RETURN a""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: assert ("expected Boolean but was Integer" in str(e)) pass # 'val' is a boolean, so this query is valid. query = """WITH true AS val WHERE val return val""" redis_graph.query(query) # Non-existent properties are treated as NULLs, which are boolean in Cypher's 3-valued logic. query = """MATCH (a) WHERE a.fakeprop RETURN a""" redis_graph.query(query) # Encountering traversals as property values or ORDER BY expressions should raise compile-time errors. def test30_unexpected_traversals(self): queries = [ """MATCH (a {prop: ()-[]->()}) RETURN a""", """MATCH (a) RETURN a ORDER BY (a)-[]->()""", """MATCH (a) RETURN (a)-[]->()""" ] for query in queries: try: redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("Encountered path traversal" in str(e)) def test31_set_invalid_property_type(self): # Skip this test if running under Valgrind, as it causes a memory leak. if self.env.envRunner.debugger is not None: self.env.skip() queries = [ """MATCH (a) CREATE (:L {v: a})""", """MATCH (a), (b) WHERE b.age IS NOT NULL SET b.age = a""", """MERGE (a) ON MATCH SET a.age = a""" ] for q in queries: try: redis_graph.query(q) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("Property values can only be of primitive types" in str(e)) pass def test32_return_following_clauses(self): # After a RETURN clause we're expecting only the following clauses: # SKIP, LIMIT, ORDER-BY and UNION, given that SKIP and LIMIT are # actually attributes of the RETURN clause this leaves us with # ORDER-BY and UNION. invalid_queries = [ """RETURN 1 CREATE ()""", """RETURN 1 RETURN 2""", """MATCH(n) RETURN n DELETE n""", """MATCH(n) RETURN n SET n.v = 1""", """RETURN 1 MERGE ()""", """RETURN 1 MATCH (n) RETURN n""", """RETURN 1 WITH 1 as one RETURN one""" ] # Invalid queries, expecting errors. for q in invalid_queries: try: redis_graph.query(q) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("Unexpected clause following RETURN" in str(e)) pass # Parameters cannot reference aliases. def test33_alias_reference_in_param(self): try: query = """CYPHER A=[a] RETURN 5""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. assert ("Attempted to access variable" in str(e)) pass def test34_self_referential_properties(self): # Skip this test if running under Valgrind, as it causes a memory leak. if self.env.envRunner.debugger is not None: self.env.skip() try: # The server should emit an error on trying to create a node with a self-referential property. query = """CREATE (a:L {v: a.v})""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting an error. self.env.assertIn("undefined property", str(e)) # MATCH clauses should be able to use self-referential properties as existential filters. query = """MATCH (a {age: a.age}) RETURN a.age""" actual_result = redis_graph.query(query) expected_result = [[34]] self.env.assertEquals(actual_result.result_set, expected_result) # Test a query that allocates a large buffer. def test35_large_query(self): retval = "abcdef" * 1_000 query = "RETURN " + "\"" + retval + "\"" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.result_set[0][0], retval) def test36_multiple_proc_calls(self): query = """MATCH (a) CALL algo.BFS(a, 3, NULL) YIELD nodes as ns1 MATCH (b) CALL algo.BFS(b, 3, NULL) YIELD nodes as ns2 RETURN ns1""" plan = redis_graph.execution_plan(query) self.env.assertTrue(plan.count("ProcedureCall") == 2) def test37_list_comprehension_missuse(self): # all expect list comprehension, # unfortunately this isn't enforced by the parser # as such it is possible for a user miss-use this function # and our current arithmetic expression construction logic will # construct a malformed function call # make sure we're reciving an exception for each miss-use query queries = [ "WITH 1 AS x RETURN all(x > 2)", "WITH 1 AS x RETURN all([1],2,3)" ] for q in queries: try: redis_graph.query(q) assert (False) except redis.exceptions.ResponseError as e: pass
class testIndexScanFlow(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) def setUp(self): global redis_graph redis_con = self.env.getConnection() redis_graph = Graph(social_utils.graph_name, redis_con) social_utils.populate_graph(redis_con, redis_graph) self.build_indices() def tearDown(self): self.env.cmd('flushall') def build_indices(self): global redis_graph redis_graph.query("CREATE INDEX ON :person(age)") redis_graph.query("CREATE INDEX ON :country(name)") # Validate that Cartesian products using index and label scans succeed def test01_cartesian_product_mixed_scans(self): query = "MATCH (p:person), (c:country) WHERE p.age > 0 RETURN p.age, c.name ORDER BY p.age, c.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) self.env.assertIn('Label Scan', plan) indexed_result = redis_graph.query(query) query = "MATCH (p:person), (c:country) RETURN p.age, c.name ORDER BY p.age, c.name" plan = redis_graph.execution_plan(query) self.env.assertNotIn('Node By Index Scan', plan) self.env.assertIn('Label Scan', plan) unindexed_result = redis_graph.query(query) self.env.assertEquals(indexed_result.result_set, unindexed_result.result_set) # Validate that Cartesian products using just index scans succeed def test02_cartesian_product_index_scans_only(self): query = "MATCH (p:person), (c:country) WHERE p.age > 0 AND c.name > '' RETURN p.age, c.name ORDER BY p.age, c.name" plan = redis_graph.execution_plan(query) # The two streams should both use index scans self.env.assertEquals(plan.count('Node By Index Scan'), 2) self.env.assertNotIn('Label Scan', plan) indexed_result = redis_graph.query(query) query = "MATCH (p:person), (c:country) RETURN p.age, c.name ORDER BY p.age, c.name" plan = redis_graph.execution_plan(query) self.env.assertNotIn('Node By Index Scan', plan) self.env.assertIn('Label Scan', plan) unindexed_result = redis_graph.query(query) self.env.assertEquals(indexed_result.result_set, unindexed_result.result_set) # Validate that the appropriate bounds are respected when a Cartesian product uses the same index in two streams def test03_cartesian_product_reused_index(self): redis_graph.query("CREATE INDEX ON :person(name)") query = "MATCH (a:person {name: 'Omri Traub'}), (b:person) WHERE b.age <= 30 RETURN a.name, b.name ORDER BY a.name, b.name" plan = redis_graph.execution_plan(query) # The two streams should both use index scans self.env.assertEquals(plan.count('Node By Index Scan'), 2) self.env.assertNotIn('Label Scan', plan) expected_result = [['Omri Traub', 'Gal Derriere'], ['Omri Traub', 'Lucy Yanfital']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate index utilization when filtering on a numeric field with the `IN` keyword. def test04_test_in_operator_numerics(self): # Validate the transformation of IN to multiple OR expressions. query = "MATCH (p:person) WHERE p.age IN [1,2,3] RETURN p" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) # Validate that nested arrays are not scanned in index. query = "MATCH (p:person) WHERE p.age IN [[1,2],3] RETURN p" plan = redis_graph.execution_plan(query) self.env.assertNotIn('Node By Index Scan', plan) self.env.assertIn('Label Scan', plan) # Validate the transformation of IN to multiple OR, over a range. query = "MATCH (p:person) WHERE p.age IN range(0,30) RETURN p.name ORDER BY p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) expected_result = [['Gal Derriere'], ['Lucy Yanfital']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate the transformation of IN to empty index iterator. query = "MATCH (p:person) WHERE p.age IN [] RETURN p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) expected_result = [] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate the transformation of IN OR IN to empty index iterators. query = "MATCH (p:person) WHERE p.age IN [] OR p.age IN [] RETURN p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) expected_result = [] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate the transformation of multiple IN filters. query = "MATCH (p:person) WHERE p.age IN [26, 27, 30] OR p.age IN [33, 34, 35] RETURN p.name ORDER BY p.age" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) expected_result = [['Gal Derriere'], ['Lucy Yanfital'], ['Omri Traub'], ['Noam Nativ']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate the transformation of multiple IN filters. query = "MATCH (p:person) WHERE p.age IN [26, 27, 30] OR p.age IN [33, 34, 35] OR p.age IN [] RETURN p.name ORDER BY p.age" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) expected_result = [['Gal Derriere'], ['Lucy Yanfital'], ['Omri Traub'], ['Noam Nativ']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate index utilization when filtering on string fields with the `IN` keyword. def test05_test_in_operator_string_props(self): # Build an index on the name property. redis_graph.query("CREATE INDEX ON :person(name)") # Validate the transformation of IN to multiple OR expressions over string properties. query = "MATCH (p:person) WHERE p.name IN ['Gal Derriere', 'Lucy Yanfital'] RETURN p.name ORDER BY p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) self.env.assertNotIn('Label Scan', plan) expected_result = [['Gal Derriere'], ['Lucy Yanfital']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Combine numeric and string filters specified by IN. query = "MATCH (p:person) WHERE p.name IN ['Gal Derriere', 'Lucy Yanfital'] AND p.age in [30] RETURN p.name ORDER BY p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) self.env.assertNotIn('Label Scan', plan) expected_result = [['Lucy Yanfital']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate an empty index on IN with multiple indexes query = "MATCH (p:person) WHERE p.name IN [] OR p.age IN [] RETURN p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) expected_result = [] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Combine IN filters with other relational filters. query = "MATCH (p:person) WHERE p.name IN ['Gal Derriere', 'Lucy Yanfital'] AND p.name < 'H' RETURN p.name ORDER BY p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) self.env.assertNotIn('Label Scan', plan) expected_result = [['Gal Derriere']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) query = "MATCH (p:person) WHERE p.name IN ['Gal Derriere', 'Lucy Yanfital'] OR p.age = 33 RETURN p.name ORDER BY p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) self.env.assertNotIn('Label Scan', plan) expected_result = [['Gal Derriere'], ['Lucy Yanfital'], ['Omri Traub']] result = redis_graph.query(query) result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # ',' is the default separator for tag indices # we've updated our separator to '\0' this test verifies issue 696: # https://github.com/RedisGraph/RedisGraph/issues/696 def test06_tag_separator(self): redis_con = self.env.getConnection() redis_graph = Graph("G", redis_con) # Create a single node with a long string property, introduce a comma as part of the string. query = """CREATE (:Node{value:"A ValuePartition is a pattern that describes a restricted set of classes from which a property can be associated. The parent class is used in restrictions, and the covering axiom means that only members of the subclasses may be used as values."})""" redis_graph.query(query) # Index property. query = """CREATE INDEX ON :Node(value)""" redis_graph.query(query) # Make sure node is returned by index scan. query = """MATCH (a:Node{value:"A ValuePartition is a pattern that describes a restricted set of classes from which a property can be associated. The parent class is used in restrictions, and the covering axiom means that only members of the subclasses may be used as values."}) RETURN a""" plan = redis_graph.execution_plan(query) result_set = redis_graph.query(query).result_set self.env.assertIn('Node By Index Scan', plan) self.env.assertEqual(len(result_set), 1) def test07_index_scan_and_id(self): redis_con = self.env.getConnection() redis_graph = Graph("G", redis_con) nodes=[] for i in range(10): node = Node(node_id=i, label='person', properties={'age':i}) nodes.append(node) redis_graph.add_node(node) redis_graph.flush() query = """CREATE INDEX ON :person(age)""" query_result = redis_graph.query(query) self.env.assertEqual(1, query_result.indices_created) query = """MATCH (n:person) WHERE id(n)>=7 AND n.age<9 RETURN n ORDER BY n.age""" plan = redis_graph.execution_plan(query) query_result = redis_graph.query(query) self.env.assertIn('Node By Index Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) self.env.assertEqual(2, len(query_result.result_set)) expected_result = [[nodes[7]], [nodes[8]]] self.env.assertEquals(expected_result, query_result.result_set) # Validate placement of index scans and filter ops when not all filters can be replaced. def test08_index_scan_multiple_filters(self): query = "MATCH (p:person) WHERE p.age = 30 AND NOT EXISTS(p.fakeprop) RETURN p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Node By Index Scan', plan) self.env.assertNotIn('Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Lucy Yanfital"] self.env.assertEquals(query_result.result_set[0], expected_result) def test09_index_scan_with_params(self): query = "MATCH (p:person) WHERE p.age = $age RETURN p.name" params = {'age': 30} plan = redis_graph.execution_plan(query, params=params) self.env.assertIn('Node By Index Scan', plan) query_result = redis_graph.query(query, params=params) expected_result = ["Lucy Yanfital"] self.env.assertEquals(query_result.result_set[0], expected_result) def test10_index_scan_with_param_array(self): query = "MATCH (p:person) WHERE p.age in $ages RETURN p.name" params = {'ages': [30]} plan = redis_graph.execution_plan(query, params=params) self.env.assertIn('Node By Index Scan', plan) query_result = redis_graph.query(query, params=params) expected_result = ["Lucy Yanfital"] self.env.assertEquals(query_result.result_set[0], expected_result) def test11_single_index_multiple_scans(self): query = "MERGE (p1:person {age: 40}) MERGE (p2:person {age: 41})" plan = redis_graph.execution_plan(query) # Two index scans should be performed. self.env.assertEqual(plan.count("Node By Index Scan"), 2) query_result = redis_graph.query(query) # Two new nodes should be created. self.env.assertEquals(query_result.nodes_created, 2) def test12_remove_scans_before_index(self): query = "MATCH (a:person {age: 32})-[]->(b) WHERE (b:person)-[]->(a) RETURN a" plan = redis_graph.execution_plan(query) # One index scan should be performed. self.env.assertEqual(plan.count("Node By Index Scan"), 1) def test13_point_index_scan(self): # create index q = "CREATE INDEX ON :restaurant(location)" redis_graph.query(q) # create restaurant q = "CREATE (:restaurant {location: point({latitude:30.27822306, longitude:-97.75134723})})" redis_graph.query(q) # locate other restaurants within a 1000m radius q = """MATCH (r:restaurant) WHERE distance(r.location, point({latitude:30.27822306, longitude:-97.75134723})) < 1000 RETURN r""" # make sure index is used plan = redis_graph.execution_plan(q) self.env.assertIn("Node By Index Scan", plan) # refine query from '<' to '<=' q = """MATCH (r:restaurant) WHERE distance(r.location, point({latitude:30.27822306, longitude:-97.75134723})) <= 1000 RETURN r""" # make sure index is used plan = redis_graph.execution_plan(q) self.env.assertIn("Node By Index Scan", plan) # index should NOT be used when searching for points outside of a circle # testing operand: '>', '>=' and '=' q = """MATCH (r:restaurant) WHERE distance(r.location, point({latitude:30.27822306, longitude:-97.75134723})) > 1000 RETURN r""" # make sure index is NOT used plan = redis_graph.execution_plan(q) self.env.assertNotIn("Node By Index Scan", plan) q = """MATCH (r:restaurant) WHERE distance(r.location, point({latitude:30.27822306, longitude:-97.75134723})) >= 1000 RETURN r""" # make sure index is NOT used plan = redis_graph.execution_plan(q) self.env.assertNotIn("Node By Index Scan", plan) q = """MATCH (r:restaurant) WHERE distance(r.location, point({latitude:30.27822306, longitude:-97.75134723})) = 1000 RETURN r""" # make sure index is NOT used plan = redis_graph.execution_plan(q) self.env.assertNotIn("Node By Index Scan", plan) def test14_index_scan_utilize_array(self): # Querying indexed properties using IN a constant array should utilize indexes. query = "MATCH (a:person) WHERE a.age IN [34, 33] RETURN a.name ORDER BY a.name" plan = redis_graph.execution_plan(query) # One index scan should be performed. self.env.assertEqual(plan.count("Node By Index Scan"), 1) query_result = redis_graph.query(query) expected_result = [["Noam Nativ"], ["Omri Traub"]] self.env.assertEquals(query_result.result_set, expected_result) # Querying indexed properties using IN a generated array should utilize indexes. query = "MATCH (a:person) WHERE a.age IN range(33, 34) RETURN a.name ORDER BY a.name" plan = redis_graph.execution_plan(query) # One index scan should be performed. self.env.assertEqual(plan.count("Node By Index Scan"), 1) query_result = redis_graph.query(query) expected_result = [["Noam Nativ"], ["Omri Traub"]] self.env.assertEquals(query_result.result_set, expected_result) # Querying indexed properties using IN a non-constant array should not utilize indexes. query = "MATCH (a:person)-[]->(b) WHERE a.age IN b.arr RETURN a" plan = redis_graph.execution_plan(query) # No index scans should be performed. self.env.assertEqual(plan.count("Label Scan"), 1) self.env.assertEqual(plan.count("Node By Index Scan"), 0) # Test fulltext result scoring def test15_fulltext_result_scoring(self): g = Graph('fulltext_scoring', self.env.getConnection()) # create full-text index over label 'L', attribute 'v' g.call_procedure('db.idx.fulltext.createNodeIndex', 'L', 'v') # introduce 2 nodes g.query("create (:L {v:'hello world hello'})") g.query("create (:L {v:'hello world hello world'})") # query nodes using fulltext search q = """CALL db.idx.fulltext.queryNodes('L', 'hello world') YIELD node, score RETURN node.v, score ORDER BY score""" res = g.query(q) actual = res.result_set expected = [['hello world hello', 1.5], ['hello world hello world', 2]] self.env.assertEqual(expected, actual) def test16_runtime_index_utilization(self): # find all person nodes with age in the range 33-37 # current age (x) should be resolved at runtime # index query should be constructed for each age value q = """UNWIND range(33, 37) AS x MATCH (p:person {age:x}) RETURN p.name ORDER BY p.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Node By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["Noam Nativ"], ["Omri Traub"]] self.env.assertEquals(query_result.result_set, expected_result) # similar to the query above, only this time the filter is specified # by an OR condition q = """WITH 33 AS min, 34 AS max MATCH (p:person) WHERE p.age = min OR p.age = max RETURN p.name ORDER BY p.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Node By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["Noam Nativ"], ["Omri Traub"]] self.env.assertEquals(query_result.result_set, expected_result) # find all person nodes with age equals 33 'x' # 'x' value is known only at runtime q = """WITH 33 AS x MATCH (p:person {age:x}) RETURN p.name ORDER BY p.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Node By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["Omri Traub"]] self.env.assertEquals(query_result.result_set, expected_result) # find all person nodes with age equals x + 1 # the expression x+1 is evaluated to the constant 33 only at runtime # expecting index query to be constructed at runtime q = """WITH 32 AS x MATCH (p:person) WHERE p.age = (x + 1) RETURN p.name ORDER BY p.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Node By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["Omri Traub"]] self.env.assertEquals(query_result.result_set, expected_result) # same idea as previous query only we've switched the position of the # operands, queried entity (p.age) is now on the right hand side of the # filter, expecting the same behavior q = """WITH 32 AS x MATCH (p:person) WHERE (x + 1) = p.age RETURN p.name ORDER BY p.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Node By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["Omri Traub"]] self.env.assertEquals(query_result.result_set, expected_result) # find all person nodes 'b' with age greater than node 'a' # a's age value is determined only at runtime # expecting index to be used to resolve 'b' nodes, index query should be # constructed at runtime q = """MATCH (a:person {name:'Omri Traub'}) WITH a AS a MATCH (b:person) WHERE b.age > a.age RETURN b.name ORDER BY b.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Node By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["Noam Nativ"]] self.env.assertEquals(query_result.result_set, expected_result) # same idea as previous query, only this time we've switched filter # operands position, queries entity is on the right hand side q = """MATCH (a:person {name: 'Omri Traub'}) WITH a AS a MATCH (b:person) WHERE a.age < b.age RETURN b.name ORDER BY b.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Node By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["Noam Nativ"]] self.env.assertEquals(query_result.result_set, expected_result) # TODO: The following query uses the "Value Hash Join" where it would be # better to use "Index Scan" q = """UNWIND range(33, 37) AS x MATCH (a:person {age:x}), (b:person {age:x}) RETURN a.name, b.name ORDER BY a.name, b.name""" def test17_runtime_index_utilization_array_values(self): # when constructing an index query at runtime it is possible to encounter # none indexable values e.g. Array, in which case the index will still be # utilize, producing every entity which was indexed with a none indexable value # to which the index scan operation will have to apply the original filter # create person nodes with array value for their 'age' attribute q = """CREATE (:person {age:[36], name:'leonard'}), (:person {age:[34], name:['maynard']})""" redis_graph.query(q) # find all person nodes with age value of [36] q = """WITH [36] AS age MATCH (a:person {age:age}) RETURN a.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Node By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["leonard"]] self.env.assertEquals(query_result.result_set, expected_result) # find all person nodes with age > [33] q = """WITH [33] AS age MATCH (a:person) WHERE a.age > age RETURN a.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Node By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["leonard"], [["maynard"]]] self.env.assertEquals(query_result.result_set, expected_result) # combine indexable value with none-indexable value index query q = """WITH [33] AS age, 'leonard' AS name MATCH (a:person) WHERE a.age >= age AND a.name = name RETURN a.name""" plan = redis_graph.execution_plan(q) self.env.assertIn('Node By Index Scan', plan) query_result = redis_graph.query(q) expected_result = [["leonard"]] self.env.assertEquals(query_result.result_set, expected_result) # test for https://github.com/RedisGraph/RedisGraph/issues/1980 def test18_index_scan_inside_apply(self): redis_graph = Graph('g', self.env.getConnection()) redis_graph.query("CREATE INDEX ON :L1(id)") redis_graph.query("UNWIND range(1, 5) AS v CREATE (:L1 {id: v})") result = redis_graph.query("UNWIND range(1, 5) AS id OPTIONAL MATCH (u:L1{id: 5}) RETURN u.id") expected_result = [[5], [5], [5], [5], [5]] self.env.assertEquals(result.result_set, expected_result)
class testGraphMultiPatternQueryFlow(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) global redis_graph redis_con = self.env.getConnection() redis_graph = Graph("G", redis_con) self.populate_graph() def populate_graph(self): global redis_graph nodes = {} # Create entities for p in people: node = Node(label="person", properties={"name": p}) redis_graph.add_node(node) nodes[p] = node redis_graph.commit() # Connect a single node to all other nodes. def test01_connect_node_to_rest(self): query = """MATCH(r:person {name:"Roi"}), (f:person) WHERE f.name <> r.name CREATE (r)-[:friend]->(f) RETURN count(f)""" actual_result = redis_graph.query(query) friend_count = actual_result.result_set[0][0] self.env.assertEquals(friend_count, 6) self.env.assertEquals(actual_result.relationships_created, 6) def test02_verify_cartesian_product_streams_reset(self): # See https://github.com/RedisGraph/RedisGraph/issues/249 # Forevery outgoing edge, we expect len(people) to be matched. expected_resultset_size = 6 * len(people) queries = [ """MATCH (r:person {name:"Roi"})-[]->(f), (x) RETURN f, x""", """MATCH (x), (r:person {name:"Roi"})-[]->(f) RETURN f, x""", """MATCH (r:person {name:"Roi"})-[]->(f) MATCH (x) RETURN f, x""", """MATCH (x) MATCH (r:person {name:"Roi"})-[]->(f) RETURN f, x""" ] for q in queries: actual_result = redis_graph.query(q) records_count = len(actual_result.result_set) self.env.assertEquals(records_count, expected_resultset_size) # Connect every node to every node. def test03_create_fully_connected_graph(self): query = """MATCH(a:person), (b:person) WHERE a.name <> b.name CREATE (a)-[f:friend]->(b) RETURN count(f)""" actual_result = redis_graph.query(query) friend_count = actual_result.result_set[0][0] self.env.assertEquals(friend_count, 42) self.env.assertEquals(actual_result.relationships_created, 42) # Perform a cartesian product of 3 sets. def test04_cartesian_product(self): queries = [ """MATCH (a), (b), (c) RETURN count(a)""", """MATCH (a) MATCH (b), (c) RETURN count(a)""", """MATCH (a), (b) MATCH (c) RETURN count(a)""", """MATCH (a) MATCH (b) MATCH (c) RETURN count(a)""" ] for q in queries: actual_result = redis_graph.query(q) friend_count = actual_result.result_set[0][0] self.env.assertEquals(friend_count, 343) def test06_multiple_create_clauses(self): queries = [ """CREATE (:a {v:1}), (:b {v:2, z:3}), (:c), (:a)-[:r0 {k:9}]->(:b), (:c)-[:r1]->(:d)""", """CREATE (:a {v:1}) CREATE (:b {v:2, z:3}) CREATE (:c) CREATE (:a)-[:r0 {k:9}]->(:b) CREATE (:c)-[:r1]->(:d)""", """CREATE (:a {v:1}), (:b {v:2, z:3}) CREATE (:c), (:a)-[:r0 {k:9}]->(:b) CREATE (:c)-[:r1]->(:d)""" ] for q in queries: actual_result = redis_graph.query(q) self.env.assertEquals(actual_result.relationships_created, 2) self.env.assertEquals(actual_result.properties_set, 4) self.env.assertEquals(actual_result.nodes_created, 7)
class testBidirectionalTraversals(FlowTestsBase): def __init__(self): self.env = Env() global redis_con redis_con = self.env.getConnection() self.populate_acyclic_graph() self.populate_cyclic_graph() def populate_acyclic_graph(self): global acyclic_graph acyclic_graph = Graph("G", redis_con) # Construct a graph with the form: # (v1)-[:E]->(v2)-[:E]->(v3) node_props = ['v1', 'v2', 'v3'] nodes = [] for idx, v in enumerate(node_props): node = Node(label="L", properties={"val": v}) nodes.append(node) acyclic_graph.add_node(node) edge = Edge(nodes[0], "E", nodes[1]) acyclic_graph.add_edge(edge) edge = Edge(nodes[1], "E", nodes[2]) acyclic_graph.add_edge(edge) acyclic_graph.commit() def populate_cyclic_graph(self): global graph_with_cycle graph_with_cycle = Graph("H", redis_con) # Construct a graph with the form: # (v1)-[:E]->(v2)-[:E]->(v3), (v2)-[:E]->(v1) node_props = ['v1', 'v2', 'v3'] nodes = [] for idx, v in enumerate(node_props): node = Node(label="L", properties={"val": v}) nodes.append(node) graph_with_cycle.add_node(node) edge = Edge(nodes[0], "E", nodes[1]) graph_with_cycle.add_edge(edge) edge = Edge(nodes[1], "E", nodes[2]) graph_with_cycle.add_edge(edge) # Introduce a cycle between v2 and v1. edge = Edge(nodes[1], "E", nodes[0]) graph_with_cycle.add_edge(edge) graph_with_cycle.commit() # Test traversals that don't specify an edge direction. def test01_bidirectional_traversals(self): query = """MATCH (a)-[:E]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" actual_result = acyclic_graph.query(query) # Each relation should appear twice with the source and destination swapped in the second result. expected_result = [['v1', 'v2'], ['v2', 'v1'], ['v2', 'v3'], ['v3', 'v2']] self.env.assertEquals(actual_result.result_set, expected_result) # Test undirected traversals with a referenced edge. query = """MATCH (a)-[e:E]-(b) RETURN ID(e), a.val, b.val ORDER BY a.val, b.val""" actual_result = acyclic_graph.query(query) expected_result = [[0, 'v1', 'v2'], [0, 'v2', 'v1'], [1, 'v2', 'v3'], [1, 'v3', 'v2']] self.env.assertEquals(actual_result.result_set, expected_result) # Test 0-hop undirected traversals. def test02_bidirectional_zero_hop_traversals(self): query = """MATCH (a)-[*0]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" actual_result = acyclic_graph.query(query) expected_result = [['v1', 'v1'], ['v2', 'v2'], ['v3', 'v3']] self.env.assertEquals(actual_result.result_set, expected_result) # TODO doesn't work - returns each node with itself as source and destination in adition to expected results. # Test combinations of directed and undirected traversals. # query = """MATCH (a)-[:E]->()-[]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" # actual_result = acyclic_graph.query(query) # expected_result = [['v1', 'v3']] # self.env.assertEquals(actual_result.result_set, expected_result) # TODO doesn't work for the same reason. # Test fixed-length multi-hop undirected traversals. # query = """MATCH (a)-[:E*2]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" # actual_result = acyclic_graph.query(query) # expected_result = [[0, 'v1', 'v3'], # [0, 'v3', 'v1']] # self.env.assertEquals(actual_result.result_set, expected_result) # Test variable-length traversals that don't specify an edge direction. def test03_bidirectional_variable_length_traversals(self): query = """MATCH (a)-[*]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" actual_result = acyclic_graph.query(query) # Each combination of distinct node source and destination should appear once. expected_result = [['v1', 'v2'], ['v1', 'v3'], ['v2', 'v1'], ['v2', 'v3'], ['v3', 'v1'], ['v3', 'v2']] self.env.assertEquals(actual_result.result_set, expected_result) # Should generate the same results as the previous query. query = """MATCH (a)-[*1..2]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" actual_result = acyclic_graph.query(query) self.env.assertEquals(actual_result.result_set, expected_result) # Test collecting self and all direct neighbors. def test04_bidirectional_variable_bounded_length_traversals(self): query = """MATCH (a)-[*0..1]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" actual_result = acyclic_graph.query(query) # Each combination of distinct node source and destination should appear once. expected_result = [['v1', 'v1'], ['v1', 'v2'], ['v2', 'v1'], ['v2', 'v2'], ['v2', 'v3'], ['v3', 'v2'], ['v3', 'v3']] self.env.assertEquals(actual_result.result_set, expected_result) # Test bidirectional query on nonexistent edge. def test05_bidirectional_variable_length_traversals_over_nonexistent_type( self): query = """MATCH (a)-[:NONEXISTENT*]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" actual_result = acyclic_graph.query(query) expected_result = [] self.env.assertEquals(actual_result.result_set, expected_result) # Test bidirectional query on real edge or nonexistent edge. def test06_bidirectional_variable_length_traversals_over_partial_existing_types( self): query = """MATCH (a)-[:NONEXISTENT|:E*]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" actual_result = acyclic_graph.query(query) # Each combination of distinct node source and destination should appear once. expected_result = [['v1', 'v2'], ['v1', 'v3'], ['v2', 'v1'], ['v2', 'v3'], ['v3', 'v1'], ['v3', 'v2']] self.env.assertEquals(actual_result.result_set, expected_result) # TODO returns 16 rows; 18 rows expected. # The missing two rows are both `['v2', 'v3'] # Test bidirectional query on two real edge types. # def test07_bidirectional_variable_length_traversals_over_multiple_existing_types(self): # # Generate new dest->src edges between every current src->dest pair. # query = """MATCH (a {val: 'v1'})-[e]->(b {val: 'v2'}) CREATE (a)-[:CLONE]->(b)""" # actual_result = acyclic_graph.query(query) # self.env.assertEquals(actual_result.relationships_created, 1) # query = """MATCH (a)-[:E|:CLONE*]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" # actual_result = acyclic_graph.query(query) # expected_result = [['v1', 'v1'], # ['v1', 'v1'], # ['v1', 'v2'], # ['v1', 'v2'], # ['v1', 'v3'], # ['v1', 'v3'], # ['v2', 'v1'], # ['v2', 'v1'], # ['v2', 'v2'], # ['v2', 'v2'], # ['v2', 'v3'], # ['v2', 'v3'], # ['v2', 'v3'], # ['v3', 'v1'], # ['v3', 'v1'], # ['v3', 'v2'], # ['v3', 'v2'], # ['v3', 'v2']] # self.env.assertEquals(actual_result.result_set, expected_result) # Test bidirectional query on two real edge types. def test08_bidirectional_variable_bounded_length_traversals_over_multiple_existing_types( self): # Generate one new edge between v1 and v2. query = """MATCH (a {val: 'v1'})-[e]->(b {val: 'v2'}) CREATE (a)-[:CLONE]->(b)""" actual_result = acyclic_graph.query(query) self.env.assertEquals(actual_result.relationships_created, 1) query = """MATCH (a)-[:E|:CLONE*1..2]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" actual_result = acyclic_graph.query(query) expected_result = [['v1', 'v1'], ['v1', 'v1'], ['v1', 'v2'], ['v1', 'v2'], ['v1', 'v3'], ['v1', 'v3'], ['v2', 'v1'], ['v2', 'v1'], ['v2', 'v2'], ['v2', 'v2'], ['v2', 'v3'], ['v3', 'v1'], ['v3', 'v1'], ['v3', 'v2']] self.env.assertEquals(actual_result.result_set, expected_result) # Delete cloned edge. query = """MATCH ()-[e:CLONE]->() DELETE e""" actual_result = acyclic_graph.query(query) self.env.assertEquals(actual_result.relationships_deleted, 1) # Test traversals that don't specify an edge direction in a graph with a cycle. def test09_bidirectional_traversals_with_cycle(self): # Test undirected traversals with a referenced edge. # TODO The variant query in which the edge is not referenced does not work: # query = """MATCH (a)-[:E]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" query = """MATCH (a)-[e:E]-(b) RETURN ID(e) AS id, a.val, b.val ORDER BY id, a.val, b.val""" actual_result = graph_with_cycle.query(query) # Each relation should appear twice with the source and destination swapped in the second result. expected_result = [[0, 'v1', 'v2'], [0, 'v2', 'v1'], [1, 'v2', 'v3'], [1, 'v3', 'v2'], [2, 'v1', 'v2'], [2, 'v2', 'v1']] self.env.assertEquals(actual_result.result_set, expected_result) # Test variable-length traversals that don't specify an edge direction. def test10_bidirectional_variable_length_traversals_with_cycle(self): # TODO returns 16 rows; 18 rows expected. # The missing two rows are both `['v2', 'v3'] # query = """MATCH (a)-[*]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" query = """MATCH (a)-[*1..2]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" actual_result = graph_with_cycle.query(query) # Each src/dest pair (including when the source and dest are the same) is returned twice # except for (v2)-[]->(v3), which correctly only occurs once as the missing traversal pattern takes 3 hops. expected_result = [['v1', 'v1'], ['v1', 'v1'], ['v1', 'v2'], ['v1', 'v2'], ['v1', 'v3'], ['v1', 'v3'], ['v2', 'v1'], ['v2', 'v1'], ['v2', 'v2'], ['v2', 'v2'], ['v2', 'v3'], ['v3', 'v1'], ['v3', 'v1'], ['v3', 'v2']] self.env.assertEquals(actual_result.result_set, expected_result) # Collect self and all direct neighbors with the pattern (v1)-[]-(v2) repeated. query = """MATCH (a)-[*0..1]-(b) RETURN a.val, b.val ORDER BY a.val, b.val""" actual_result = graph_with_cycle.query(query) expected_result = [['v1', 'v1'], ['v1', 'v2'], ['v1', 'v2'], ['v2', 'v1'], ['v2', 'v1'], ['v2', 'v2'], ['v2', 'v3'], ['v3', 'v2'], ['v3', 'v3']] self.env.assertEquals(actual_result.result_set, expected_result) def test11_bidirectional_multiple_edge_type(self): # Construct a simple graph: # (a)-[E1]->(b), (c)-[E2]->(d) g = Graph("multi_edge_type", redis_con) a = Node(properties={'val': 'a'}) b = Node(properties={'val': 'b'}) c = Node(properties={'val': 'c'}) d = Node(properties={'val': 'd'}) g.add_node(a) g.add_node(b) g.add_node(c) g.add_node(d) ab = Edge(a, "E1", b) cd = Edge(c, "E2", d) g.add_edge(ab) g.add_edge(cd) g.flush() query = """MATCH (a)-[:E1|:E2]-(z) RETURN a.val, z.val ORDER BY a.val, z.val""" actual_result = g.query(query) expected_result = [['a', 'b'], ['b', 'a'], ['c', 'd'], ['d', 'c']] self.env.assertEquals(actual_result.result_set, expected_result)
class testIndexCreationFlow(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) global redis_graph redis_con = self.env.getConnection() redis_graph = Graph(GRAPH_ID, redis_con) # full-text index creation def test01_fulltext_index_creation(self): # create an index over L:v0 result = redis_graph.query( "CALL db.idx.fulltext.createNodeIndex('L', 'v0')") self.env.assertEquals(result.indices_created, 1) # create an index over L:v1 result = redis_graph.query( "CALL db.idx.fulltext.createNodeIndex('L', 'v1')") self.env.assertEquals(result.indices_created, 1) # create an index over L:v1 and L:v2 result = redis_graph.query( "CALL db.idx.fulltext.createNodeIndex('L', 'v1', 'v2')") self.env.assertEquals(result.indices_created, 1) # create an index over L:v0, L:v1 and L:v2 result = redis_graph.query( "CALL db.idx.fulltext.createNodeIndex('L', 'v0', 'v1', 'v2')") self.env.assertEquals(result.indices_created, 0) # create an index over L:v2, L:v1 and L:v0 result = redis_graph.query( "CALL db.idx.fulltext.createNodeIndex('L', 'v2', 'v1', 'v0')") self.env.assertEquals(result.indices_created, 0) # create an index over L:v3 and L:v4 result = redis_graph.query( "CALL db.idx.fulltext.createNodeIndex('L', 'v3', 'v4')") self.env.assertEquals(result.indices_created, 2)
class testGraphMixLabelsFlow(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) global redis_graph redis_con = self.env.getConnection() redis_graph = Graph("G", redis_con) self.populate_graph() def populate_graph(self): redis_graph nodes = {} # Create entities for m in male: node = Node(label="male", properties={"name": m}) redis_graph.add_node(node) nodes[m] = node for f in female: node = Node(label="female", properties={"name": f}) redis_graph.add_node(node) nodes[f] = node for n in nodes: for m in nodes: if n == m: continue edge = Edge(nodes[n], "knows", nodes[m]) redis_graph.add_edge(edge) redis_graph.commit() # Connect a single node to all other nodes. def test_male_to_all(self): query = """MATCH (m:male)-[:knows]->(t) RETURN m,t ORDER BY m.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), (len(male) * (len(male + female) - 1))) def test_male_to_male(self): query = """MATCH (m:male)-[:knows]->(t:male) RETURN m,t ORDER BY m.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), (len(male) * (len(male) - 1))) def test_male_to_female(self): query = """MATCH (m:male)-[:knows]->(t:female) RETURN m,t ORDER BY m.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), (len(male) * len(female))) def test_female_to_all(self): query = """MATCH (f:female)-[:knows]->(t) RETURN f,t ORDER BY f.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), (len(female) * (len(male + female) - 1))) def test_female_to_male(self): query = """MATCH (f:female)-[:knows]->(t:male) RETURN f,t ORDER BY f.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), (len(female) * len(male))) def test_female_to_female(self): query = """MATCH (f:female)-[:knows]->(t:female) RETURN f,t ORDER BY f.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), (len(female) * (len(female) - 1))) def test_all_to_female(self): query = """MATCH (f)-[:knows]->(t:female) RETURN f,t ORDER BY f.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), (len(male) * len(female)) + (len(female) * (len(female) - 1))) def test_all_to_male(self): query = """MATCH (f)-[:knows]->(t:male) RETURN f,t ORDER BY f.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), (len(male) * (len(male) - 1)) + len(female) * len(male)) def test_all_to_all(self): query = """MATCH (f)-[:knows]->(t) RETURN f,t ORDER BY f.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), (len(male + female) * (len(male + female) - 1)))
class testVariableLengthTraversals(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) global redis_con global redis_graph redis_con = self.env.getConnection() redis_graph = Graph("G", redis_con) self.populate_graph() def populate_graph(self): global redis_graph nodes = [] # Create nodes for n in node_names: node = Node(label="node", properties={"name": n}) redis_graph.add_node(node) nodes.append(node) # Create edges for i in range(len(nodes) - 1): edge = Edge( nodes[i], "knows", nodes[i + 1], properties={"connects": node_names[i] + node_names[i + 1]}) redis_graph.add_edge(edge) redis_graph.commit() # Sanity check against single-hop traversal def test01_conditional_traverse(self): query = """MATCH (a)-[e]->(b) RETURN a.name, e.connects, b.name ORDER BY a.name, b.name""" actual_result = redis_graph.query(query) expected_result = [['A', 'AB', 'B'], ['B', 'BC', 'C'], ['C', 'CD', 'D']] self.env.assertEquals(actual_result.result_set, expected_result) # Traversal with no labels def test02_unlabeled_traverse(self): query = """MATCH (a)-[*]->(b) RETURN a.name, b.name ORDER BY a.name, b.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), max_results) query = """MATCH (a)<-[*]-(b) RETURN a, b ORDER BY a.name, b.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), max_results) # Traversal with labeled source def test03_source_labeled(self): query = """MATCH (a:node)-[*]->(b) RETURN a.name, b.name ORDER BY a.name, b.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), max_results) query = """MATCH (a:node)<-[*]-(b) RETURN a.name, b.name ORDER BY a.name, b.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), max_results) # Traversal with labeled dest def test04_dest_labeled(self): query = """MATCH (a)-[*]->(b:node) RETURN a.name, b.name ORDER BY a.name, b.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), max_results) query = """MATCH (a)<-[*]-(b:node) RETURN a.name, b.name ORDER BY a.name, b.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), max_results) # Attempt to traverse non-existent relationship type. def test05_invalid_traversal(self): query = """MATCH (a)-[:no_edge*]->(b) RETURN a.name""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), 0) # Test bidirectional traversal def test06_bidirectional_traversal(self): query = """MATCH (a)-[*]-(b) RETURN a.name, b.name ORDER BY a.name, b.name""" actual_result = redis_graph.query(query) # The undirected traversal should represent every combination twice. self.env.assertEquals(len(actual_result.result_set), max_results * 2) def test07_non_existing_edge_traversal_with_zero_length(self): # Verify that zero length traversals always return source, even for non existing edges. query = """MATCH (a)-[:not_knows*0..1]->(b) RETURN a""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), 4) # Test traversal with a possibly-null source. def test08_optional_source(self): query = """OPTIONAL MATCH (a:fake) OPTIONAL MATCH (a)-[*]->(b) RETURN a.name, b.name ORDER BY a.name, b.name""" actual_result = redis_graph.query(query) expected_result = [[None, None]] self.env.assertEquals(actual_result.result_set, expected_result) query = """OPTIONAL MATCH (a:node {name: 'A'}) OPTIONAL MATCH (a)-[*]->(b {name: 'B'}) RETURN a.name, b.name ORDER BY a.name, b.name""" actual_result = redis_graph.query(query) expected_result = [['A', 'B']] self.env.assertEquals(actual_result.result_set, expected_result) # Test traversals with filters on variable-length edges def test09_filtered_edges(self): # Test an inline equality predicate query = """MATCH (a)-[* {connects: 'BC'}]->(b) RETURN a.name, b.name ORDER BY a.name, b.name""" # The filter op should have been optimized out plan = redis_graph.execution_plan(query) self.env.assertNotIn("Filter", plan) actual_result = redis_graph.query(query) expected_result = [['B', 'C']] self.env.assertEquals(actual_result.result_set, expected_result) # Test a WHERE clause predicate query = """MATCH (a)-[e*]->(b) WHERE e.connects IN ['BC', 'CD'] RETURN a.name, b.name ORDER BY a.name, b.name""" # The filter op should have been optimized out plan = redis_graph.execution_plan(query) self.env.assertNotIn("Filter", plan) actual_result = redis_graph.query(query) expected_result = [['B', 'C'], ['B', 'D'], ['C', 'D']] self.env.assertEquals(actual_result.result_set, expected_result) # Test a WHERE clause predicate with an OR condition query = """MATCH (a)-[e*]->(b) WHERE e.connects = 'BC' OR e.connects = 'CD' RETURN a.name, b.name ORDER BY a.name, b.name""" # The filter op should have been optimized out plan = redis_graph.execution_plan(query) self.env.assertNotIn("Filter", plan) actual_result = redis_graph.query(query) # Expecting the same result self.env.assertEquals(actual_result.result_set, expected_result) # Test the concatenation of multiple predicates query = """MATCH (a)-[e*]->(b) WHERE e.connects IN ['AB', 'BC', 'CD'] AND e.connects <> 'CD' RETURN a.name, b.name ORDER BY a.name, b.name""" # The filter op should have been optimized out plan = redis_graph.execution_plan(query) self.env.assertNotIn("Filter", plan) actual_result = redis_graph.query(query) expected_result = [['A', 'B'], ['A', 'C'], ['B', 'C']] self.env.assertEquals(actual_result.result_set, expected_result) # Test the concatenation of AND and OR conditions query = """MATCH (a)-[e*]->(b) WHERE e.connects IN ['AB', 'BC', 'CD'] AND (e.connects = 'AB' OR e.connects = 'BC') AND e.connects <> 'CD' RETURN a.name, b.name ORDER BY a.name, b.name""" # The filter op should have been optimized out plan = redis_graph.execution_plan(query) self.env.assertNotIn("Filter", plan) actual_result = redis_graph.query(query) expected_result = [['A', 'B'], ['A', 'C'], ['B', 'C']] self.env.assertEquals(actual_result.result_set, expected_result) # Validate that WHERE clause predicates are applied to edges lower than the minHops value query = """MATCH (a)-[e*2..]->(b) WHERE e.connects <> 'AB' RETURN a.name, b.name ORDER BY a.name, b.name""" actual_result = redis_graph.query(query) expected_result = [['B', 'D']] self.env.assertEquals(actual_result.result_set, expected_result)
class testPathFilter(FlowTestsBase): def __init__(self): self.env = Env() global redis_con redis_con = self.env.getConnection() def setUp(self): global redis_graph redis_graph = Graph(GRAPH_ID, redis_con) self.env.flush() def test00_simple_path_filter(self): node0 = Node(node_id=0, label="L") node1 = Node(node_id=1, label="L", properties={'x': 1}) edge01 = Edge(src_node=node0, dest_node=node1, relation="R") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_edge(edge01) redis_graph.flush() query = "MATCH (n:L) WHERE (n)-[:R]->(:L) RETURN n" result_set = redis_graph.query(query) expected_results = [[node0]] query_info = QueryInfo(query=query, description="Tests simple path filter", expected_result=expected_results) self._assert_resultset_equals_expected(result_set, query_info) def test01_negated_simple_path_filter(self): node0 = Node(node_id=0, label="L") node1 = Node(node_id=1, label="L", properties={'x': 1}) edge01 = Edge(src_node=node0, dest_node=node1, relation="R") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_edge(edge01) redis_graph.flush() query = "MATCH (n:L) WHERE NOT (n)-[:R]->(:L) RETURN n" result_set = redis_graph.query(query) expected_results = [[node1]] query_info = QueryInfo(query=query, description="Tests simple negated path filter", expected_result=expected_results) self._assert_resultset_equals_expected(result_set, query_info) def test02_test_path_filter_or_property_filter(self): node0 = Node(node_id=0, label="L") node1 = Node(node_id=1, label="L", properties={'x': 1}) edge01 = Edge(src_node=node0, dest_node=node1, relation="R") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_edge(edge01) redis_graph.flush() query = "MATCH (n:L) WHERE (n)-[:R]->(:L) OR n.x=1 RETURN n" result_set = redis_graph.query(query) expected_results = [[node0], [node1]] query_info = QueryInfo( query=query, description="Tests OR condition with simple filter and path filter", expected_result=expected_results) self._assert_resultset_and_expected_mutually_included( result_set, query_info) def test03_path_filter_or_negated_path_filter(self): node0 = Node(node_id=0, label="L") node1 = Node(node_id=1, label="L", properties={'x': 1}) edge01 = Edge(src_node=node0, dest_node=node1, relation="R") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_edge(edge01) redis_graph.flush() query = "MATCH (n:L) WHERE (n)-[:R]->(:L) OR NOT (n)-[:R]->(:L) RETURN n" result_set = redis_graph.query(query) expected_results = [[node0], [node1]] query_info = QueryInfo( query=query, description="Tests OR condition with path and negated path filters", expected_result=expected_results) self._assert_resultset_and_expected_mutually_included( result_set, query_info) def test04_test_level_1_nesting_logical_operators_over_path_and_property_filters( self): node0 = Node(node_id=0, label="L") node1 = Node(node_id=1, label="L", properties={'x': 1}) edge01 = Edge(src_node=node0, dest_node=node1, relation="R") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_edge(edge01) redis_graph.flush() query = "MATCH (n:L) WHERE (n)-[:R]->(:L) OR (n.x=1 AND NOT (n)-[:R]->(:L)) RETURN n" result_set = redis_graph.query(query) expected_results = [[node0], [node1]] query_info = QueryInfo( query=query, description= "Tests AND condition with simple filter and negated path filter", expected_result=expected_results) self._assert_resultset_and_expected_mutually_included( result_set, query_info) def test05_test_level_2_nesting_logical_operators_over_path_and_property_filters( self): node0 = Node(node_id=0, label="L") node1 = Node(node_id=1, label="L", properties={'x': 1}) edge01 = Edge(src_node=node0, dest_node=node1, relation="R") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_edge(edge01) redis_graph.flush() query = "MATCH (n:L) WHERE (n)-[:R]->(:L) OR (n.x=1 AND (n.x = 2 OR NOT (n)-[:R]->(:L))) RETURN n" result_set = redis_graph.query(query) expected_results = [[node0], [node1]] query_info = QueryInfo( query=query, description="Tests AND condition with simple filter and nested OR", expected_result=expected_results) self._assert_resultset_and_expected_mutually_included( result_set, query_info) def test06_test_level_2_nesting_logical_operators_over_path_filters(self): node0 = Node(node_id=0, label="L") node1 = Node(node_id=1, label="L", properties={'x': 1}) node2 = Node(node_id=2, label="L2") edge01 = Edge(src_node=node0, dest_node=node1, relation="R") edge12 = Edge(src_node=node1, dest_node=node2, relation="R2") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_node(node2) redis_graph.add_edge(edge01) redis_graph.add_edge(edge12) redis_graph.flush() query = "MATCH (n:L) WHERE (n)-[:R]->(:L) OR (n.x=1 AND ((n)-[:R2]->(:L2) OR (n)-[:R]->(:L))) RETURN n" result_set = redis_graph.query(query) expected_results = [[node0], [node1]] query_info = QueryInfo( query=query, description="Tests AND condition with simple filter and nested OR", expected_result=expected_results) self._assert_resultset_and_expected_mutually_included( result_set, query_info) def test07_test_edge_filters(self): node0 = Node(node_id=0, label="L", properties={'x': 'a'}) node1 = Node(node_id=1, label="L", properties={'x': 'b'}) node2 = Node(node_id=2, label="L", properties={'x': 'c'}) edge01 = Edge(src_node=node0, dest_node=node1, relation="R", properties={'x': 1}) edge12 = Edge(src_node=node1, dest_node=node2, relation="R") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_node(node2) redis_graph.add_edge(edge01) redis_graph.add_edge(edge12) redis_graph.flush() query = "MATCH (n:L) WHERE (n)-[:R {x:1}]->() RETURN n.x" result_set = redis_graph.query(query) expected_results = [['a']] query_info = QueryInfo( query=query, description="Tests pattern filter edge conditions", expected_result=expected_results) self._assert_resultset_and_expected_mutually_included( result_set, query_info) def test08_indexed_child_stream_resolution(self): node0 = Node(node_id=0, label="L", properties={'x': 'a'}) node1 = Node(node_id=1, label="L", properties={'x': 'b'}) node2 = Node(node_id=2, label="L", properties={'x': 'c'}) edge01 = Edge(src_node=node0, dest_node=node1, relation="R") edge12 = Edge(src_node=node1, dest_node=node2, relation="R") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_node(node2) redis_graph.add_edge(edge01) redis_graph.add_edge(edge12) redis_graph.flush() # Create index. query = "CREATE INDEX ON :L(x)" result_set = redis_graph.query(query) self.env.assertEquals(result_set.indices_created, 1) # Issue a query in which the bound variable stream of the SemiApply op is an Index Scan. query = "MATCH (n:L) WHERE (:L)<-[]-(n)<-[]-(:L {x: 'a'}) AND n.x = 'b' RETURN n.x" result_set = redis_graph.query(query) expected_results = [['b']] self.env.assertEquals(result_set.result_set, expected_results) def test09_no_invalid_expand_into(self): node0 = Node(node_id=0, label="L", properties={'x': 'a'}) node1 = Node(node_id=1, label="L", properties={'x': 'b'}) node2 = Node(node_id=2, label="L", properties={'x': 'c'}) edge01 = Edge(src_node=node0, dest_node=node1, relation="R") edge12 = Edge(src_node=node1, dest_node=node2, relation="R") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_node(node2) redis_graph.add_edge(edge01) redis_graph.add_edge(edge12) redis_graph.flush() # Issue a query in which the match stream and the bound stream must both perform traversal. query = "MATCH (n:L)-[]->(:L) WHERE ({x: 'a'})-[]->(n) RETURN n.x" plan = redis_graph.execution_plan(query) # Verify that the execution plan has no Expand Into and two traversals. self.env.assertNotIn("Expand Into", plan) self.env.assertEquals(2, plan.count("Conditional Traverse")) result_set = redis_graph.query(query) expected_results = [['b']] self.env.assertEquals(result_set.result_set, expected_results) def test10_verify_apply_results(self): # Build a graph with 3 nodes and 3 edges, 2 of which have the same source. node0 = Node(node_id=0, label="L", properties={'x': 'a'}) node1 = Node(node_id=1, label="L", properties={'x': 'b'}) node2 = Node(node_id=2, label="L", properties={'x': 'c'}) edge01 = Edge(src_node=node0, dest_node=node1, relation="R") edge02 = Edge(src_node=node0, dest_node=node2, relation="R") edge12 = Edge(src_node=node1, dest_node=node2, relation="R") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_node(node2) redis_graph.add_edge(edge01) redis_graph.add_edge(edge02) redis_graph.add_edge(edge12) redis_graph.flush() query = "MATCH (n:L) WHERE (n)-[]->() RETURN n.x ORDER BY n.x" result_set = redis_graph.query(query) # Each source node should be returned exactly once. expected_results = [['a'], ['b']] self.env.assertEquals(result_set.result_set, expected_results) def test11_unbound_path_filters(self): # Build a graph with 2 nodes connected by 1 edge. node0 = Node(node_id=0, label="L", properties={'x': 'a'}) node1 = Node(node_id=1, label="L", properties={'x': 'b'}) edge01 = Edge(src_node=node0, dest_node=node1, relation="R") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_edge(edge01) redis_graph.flush() # Emit a query that uses an AntiSemiApply op to return values. query = "MATCH (n:L) WHERE NOT (:L)-[]->() RETURN n.x ORDER BY n.x" result_set = redis_graph.query(query) # The WHERE filter evaluates to false, no results should be returned. expected_result = [] self.env.assertEquals(result_set.result_set, expected_result) # Emit a query that uses a SemiApply op to return values. query = "MATCH (n:L) WHERE (:L)-[]->() RETURN n.x ORDER BY n.x" result_set = redis_graph.query(query) # The WHERE filter evaluates to true, all results should be returned. expected_result = [['a'], ['b']] self.env.assertEquals(result_set.result_set, expected_result) def test12_label_introduced_in_path_filter(self): # Build a graph with 2 nodes connected by 1 edge. node0 = Node(node_id=0, label="L", properties={'x': 'a'}) node1 = Node(node_id=1, label="L", properties={'x': 'b'}) edge01 = Edge(src_node=node0, dest_node=node1, relation="R") redis_graph.add_node(node0) redis_graph.add_node(node1) redis_graph.add_edge(edge01) redis_graph.flush() # Write a WHERE filter that introduces label data. query = "MATCH (a1)-[]->(a2) WHERE (a1:L)-[]->(a2:L) return a1.x, a2.x" result_set = redis_graph.query(query) expected_result = [['a', 'b']] self.env.assertEquals(result_set.result_set, expected_result)
class testGraphBulkInsertFlow(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) global redis_graph global redis_con redis_con = self.env.getConnection() port = self.env.envRunner.port redis_graph = Graph("graph", redis_con) # Run bulk loader script and validate terminal output def test01_run_script(self): graphname = "graph" runner = CliRunner() csv_path = os.path.dirname( os.path.abspath(__file__)) + '/../../demo/bulk_insert/resources/' res = runner.invoke(bulk_insert, [ '--port', port, '--nodes', csv_path + 'Person.csv', '--nodes', csv_path + 'Country.csv', '--relations', csv_path + 'KNOWS.csv', '--relations', csv_path + 'VISITED.csv', graphname ]) # The script should report 27 node creations and 48 edge creations self.env.assertEquals(res.exit_code, 0) self.env.assertIn('27 nodes created', res.output) self.env.assertIn('56 relations created', res.output) # Validate that the expected nodes and properties have been constructed def test02_validate_nodes(self): global redis_graph # Query the newly-created graph query_result = redis_graph.query( 'MATCH (p:Person) RETURN p.name, p.age, p.gender, p.status, ID(p) ORDER BY p.name' ) # Verify that the Person label exists, has the correct attributes, and is properly populated expected_result = [['Ailon Velger', 32, 'male', 'married', 2], ['Alon Fital', 32, 'male', 'married', 1], ['Boaz Arad', 31, 'male', 'married', 4], ['Gal Derriere', 26, 'male', 'single', 11], ['Jane Chernomorin', 31, 'female', 'married', 8], ['Lucy Yanfital', 30, 'female', 'married', 7], ['Mor Yesharim', 31, 'female', 'married', 12], ['Noam Nativ', 34, 'male', 'single', 13], ['Omri Traub', 33, 'male', 'single', 5], ['Ori Laslo', 32, 'male', 'married', 3], ['Roi Lipman', 32, 'male', 'married', 0], ['Shelly Laslo Rooz', 31, 'female', 'married', 9], ['Tal Doron', 32, 'male', 'single', 6], [ 'Valerie Abigail Arad', 31, 'female', 'married', 10 ]] self.env.assertEquals(query_result.result_set, expected_result) # Verify that the Country label exists, has the correct attributes, and is properly populated query_result = redis_graph.query( 'MATCH (c:Country) RETURN c.name, ID(c) ORDER BY c.name') expected_result = [['Andora', 21], ['Canada', 18], ['China', 19], ['Germany', 24], ['Greece', 17], ['Italy', 25], ['Japan', 16], ['Kazakhstan', 22], ['Netherlands', 20], ['Prague', 15], ['Russia', 23], ['Thailand', 26], ['USA', 14]] self.env.assertEquals(query_result.result_set, expected_result) # Validate that the expected relations and properties have been constructed def test03_validate_relations(self): # Query the newly-created graph query_result = redis_graph.query( 'MATCH (a)-[e:KNOWS]->(b) RETURN a.name, e.relation, b.name ORDER BY e.relation, a.name, b.name' ) expected_result = [['Ailon Velger', 'friend', 'Noam Nativ'], ['Alon Fital', 'friend', 'Gal Derriere'], ['Alon Fital', 'friend', 'Mor Yesharim'], ['Boaz Arad', 'friend', 'Valerie Abigail Arad'], ['Roi Lipman', 'friend', 'Ailon Velger'], ['Roi Lipman', 'friend', 'Alon Fital'], ['Roi Lipman', 'friend', 'Boaz Arad'], ['Roi Lipman', 'friend', 'Omri Traub'], ['Roi Lipman', 'friend', 'Ori Laslo'], ['Roi Lipman', 'friend', 'Tal Doron'], ['Ailon Velger', 'married', 'Jane Chernomorin'], ['Alon Fital', 'married', 'Lucy Yanfital'], ['Ori Laslo', 'married', 'Shelly Laslo Rooz']] self.env.assertEquals(query_result.result_set, expected_result) query_result = redis_graph.query( 'MATCH (a)-[e:VISITED]->(b) RETURN a.name, e.purpose, b.name ORDER BY e.purpose, a.name, b.name' ) expected_result = [['Alon Fital', 'business', 'Prague'], ['Alon Fital', 'business', 'USA'], ['Boaz Arad', 'business', 'Netherlands'], ['Boaz Arad', 'business', 'USA'], ['Gal Derriere', 'business', 'Netherlands'], ['Jane Chernomorin', 'business', 'USA'], ['Lucy Yanfital', 'business', 'USA'], ['Mor Yesharim', 'business', 'Germany'], ['Ori Laslo', 'business', 'China'], ['Ori Laslo', 'business', 'USA'], ['Roi Lipman', 'business', 'Prague'], ['Roi Lipman', 'business', 'USA'], ['Tal Doron', 'business', 'Japan'], ['Tal Doron', 'business', 'USA'], ['Alon Fital', 'pleasure', 'Greece'], ['Alon Fital', 'pleasure', 'Prague'], ['Alon Fital', 'pleasure', 'USA'], ['Boaz Arad', 'pleasure', 'Netherlands'], ['Boaz Arad', 'pleasure', 'USA'], ['Jane Chernomorin', 'pleasure', 'Greece'], ['Jane Chernomorin', 'pleasure', 'Netherlands'], ['Jane Chernomorin', 'pleasure', 'USA'], ['Lucy Yanfital', 'pleasure', 'Kazakhstan'], ['Lucy Yanfital', 'pleasure', 'Prague'], ['Lucy Yanfital', 'pleasure', 'USA'], ['Mor Yesharim', 'pleasure', 'Greece'], ['Mor Yesharim', 'pleasure', 'Italy'], ['Noam Nativ', 'pleasure', 'Germany'], ['Noam Nativ', 'pleasure', 'Netherlands'], ['Noam Nativ', 'pleasure', 'Thailand'], ['Omri Traub', 'pleasure', 'Andora'], ['Omri Traub', 'pleasure', 'Greece'], ['Omri Traub', 'pleasure', 'USA'], ['Ori Laslo', 'pleasure', 'Canada'], ['Roi Lipman', 'pleasure', 'Japan'], ['Roi Lipman', 'pleasure', 'Prague'], ['Shelly Laslo Rooz', 'pleasure', 'Canada'], ['Shelly Laslo Rooz', 'pleasure', 'China'], ['Shelly Laslo Rooz', 'pleasure', 'USA'], ['Tal Doron', 'pleasure', 'Andora'], ['Tal Doron', 'pleasure', 'USA'], ['Valerie Abigail Arad', 'pleasure', 'Netherlands'], ['Valerie Abigail Arad', 'pleasure', 'Russia']] self.env.assertEquals(query_result.result_set, expected_result) def test04_private_identifiers(self): graphname = "tmpgraph1" # Write temporary files with open('/tmp/nodes.tmp', mode='w') as csv_file: out = csv.writer(csv_file) out.writerow(["_identifier", "nodename"]) out.writerow([0, "a"]) out.writerow([5, "b"]) out.writerow([3, "c"]) with open('/tmp/relations.tmp', mode='w') as csv_file: out = csv.writer(csv_file) out.writerow(["src", "dest"]) out.writerow([0, 3]) out.writerow([5, 3]) runner = CliRunner() res = runner.invoke(bulk_insert, [ '--port', port, '--nodes', '/tmp/nodes.tmp', '--relations', '/tmp/relations.tmp', graphname ]) # The script should report 3 node creations and 2 edge creations self.env.assertEquals(res.exit_code, 0) self.env.assertIn('3 nodes created', res.output) self.env.assertIn('2 relations created', res.output) # Delete temporary files os.remove('/tmp/nodes.tmp') os.remove('/tmp/relations.tmp') tmp_graph = Graph(graphname, redis_con) # The field "_identifier" should not be a property in the graph query_result = tmp_graph.query('MATCH (a) RETURN a') for propname in query_result.header: self.env.assertNotIn('_identifier', propname) def test05_reused_identifier(self): graphname = "tmpgraph2" # Write temporary files with open('/tmp/nodes.tmp', mode='w') as csv_file: out = csv.writer(csv_file) out.writerow(["_identifier", "nodename"]) out.writerow([0, "a"]) out.writerow([5, "b"]) out.writerow([0, "c"]) # reused identifier with open('/tmp/relations.tmp', mode='w') as csv_file: out = csv.writer(csv_file) out.writerow(["src", "dest"]) out.writerow([0, 3]) runner = CliRunner() res = runner.invoke(bulk_insert, [ '--port', port, '--nodes', '/tmp/nodes.tmp', '--relations', '/tmp/relations.tmp', graphname ]) # The script should fail because a node identifier is reused self.env.assertNotEqual(res.exit_code, 0) self.env.assertIn('used multiple times', res.output) # Run the script again without creating relations runner = CliRunner() res = runner.invoke( bulk_insert, ['--port', port, '--nodes', '/tmp/nodes.tmp', graphname]) # The script should succeed and create 3 nodes self.env.assertEquals(res.exit_code, 0) self.env.assertIn('3 nodes created', res.output) # Delete temporary files os.remove('/tmp/nodes.tmp') os.remove('/tmp/relations.tmp') def test06_batched_build(self): # Create demo graph wth one query per input file graphname = "batched_graph" runner = CliRunner() csv_path = os.path.dirname( os.path.abspath(__file__)) + '/../../demo/bulk_insert/resources/' res = runner.invoke(bulk_insert, [ '--port', port, '--nodes', csv_path + 'Person.csv', '--nodes', csv_path + 'Country.csv', '--relations', csv_path + 'KNOWS.csv', '--relations', csv_path + 'VISITED.csv', '--max-token-count', 1, graphname ]) self.env.assertEquals(res.exit_code, 0) # The script should report statistics multiple times self.env.assertGreater(res.output.count('nodes created'), 1) new_graph = Graph(graphname, redis_con) # Newly-created graph should be identical to graph created in single query original_result = redis_graph.query( 'MATCH (p:Person) RETURN p, ID(p) ORDER BY p.name') new_result = new_graph.query( 'MATCH (p:Person) RETURN p, ID(p) ORDER BY p.name') self.env.assertEquals(original_result.result_set, new_result.result_set) original_result = redis_graph.query( 'MATCH (a)-[e:KNOWS]->(b) RETURN a.name, e, b.name ORDER BY e.relation, a.name' ) new_result = new_graph.query( 'MATCH (a)-[e:KNOWS]->(b) RETURN a.name, e, b.name ORDER BY e.relation, a.name' ) self.env.assertEquals(original_result.result_set, new_result.result_set) def test07_script_failures(self): graphname = "tmpgraph3" # Write temporary files with open('/tmp/nodes.tmp', mode='w') as csv_file: out = csv.writer(csv_file) out.writerow(["id", "nodename"]) out.writerow([0]) # Wrong number of properites runner = CliRunner() res = runner.invoke( bulk_insert, ['--port', port, '--nodes', '/tmp/nodes.tmp', graphname]) # The script should fail because a row has the wrong number of fields self.env.assertNotEqual(res.exit_code, 0) self.env.assertIn('Expected 2 columns', str(res.exception)) # Write temporary files with open('/tmp/nodes.tmp', mode='w') as csv_file: out = csv.writer(csv_file) out.writerow(["id", "nodename"]) out.writerow([0, "a"]) with open('/tmp/relations.tmp', mode='w') as csv_file: out = csv.writer(csv_file) out.writerow(["src"]) # Incomplete relation description out.writerow([0]) runner = CliRunner() res = runner.invoke(bulk_insert, [ '--port', port, '--nodes', '/tmp/nodes.tmp', '--relations', '/tmp/relations.tmp', graphname ]) # The script should fail because a row has the wrong number of fields self.env.assertNotEqual(res.exit_code, 0) self.env.assertIn('should have at least 2 elements', str(res.exception)) with open('/tmp/relations.tmp', mode='w') as csv_file: out = csv.writer(csv_file) out.writerow(["src", "dest"]) out.writerow([0, "fakeidentifier"]) runner = CliRunner() res = runner.invoke(bulk_insert, [ '--port', port, '--nodes', '/tmp/nodes.tmp', '--relations', '/tmp/relations.tmp', graphname ]) # The script should fail because an invalid node identifier was used self.env.assertNotEqual(res.exit_code, 0) self.env.assertIn('fakeidentifier', str(res.exception)) os.remove('/tmp/nodes.tmp') os.remove('/tmp/relations.tmp') # Verify that numeric, boolean, and null types are properly handled def test08_property_types(self): graphname = "tmpgraph4" # Write temporary files with open('/tmp/nodes.tmp', mode='w') as csv_file: out = csv.writer(csv_file) out.writerow(["numeric", "mixed", "bool"]) out.writerow([0, '', True]) out.writerow([5, "notnull", False]) out.writerow([7, '', False]) # reused identifier with open('/tmp/relations.tmp', mode='w') as csv_file: out = csv.writer(csv_file) out.writerow(["src", "dest", "prop"]) out.writerow([0, 5, True]) out.writerow([5, 7, 3.5]) out.writerow([7, 0, '']) runner = CliRunner() res = runner.invoke(bulk_insert, [ '--port', port, '--nodes', '/tmp/nodes.tmp', '--relations', '/tmp/relations.tmp', graphname ]) self.env.assertEquals(res.exit_code, 0) self.env.assertIn('3 nodes created', res.output) self.env.assertIn('3 relations created', res.output) graph = Graph(graphname, redis_con) query_result = graph.query( 'MATCH (a)-[e]->() RETURN a.numeric, a.mixed, a.bool, e.prop ORDER BY a.numeric, e.prop' ) expected_result = [[0, None, True, True], [5, 'notnull', False, 3.5], [7, None, False, None]] # The graph should have the correct types for all properties self.env.assertEquals(query_result.result_set, expected_result)
class testBoundVariables(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) global redis_graph redis_con = self.env.getConnection() redis_graph = Graph("G", redis_con) self.populate_graph() def populate_graph(self): global redis_graph # Construct a graph with the form: # (v1)-[:E]->(v2)-[:E]->(v3) node_props = ['v1', 'v2', 'v3'] nodes = [] for idx, v in enumerate(node_props): node = Node(label="L", properties={"val": v}) nodes.append(node) redis_graph.add_node(node) edge = Edge(nodes[0], "E", nodes[1]) redis_graph.add_edge(edge) edge = Edge(nodes[1], "E", nodes[2]) redis_graph.add_edge(edge) redis_graph.commit() def test01_with_projected_entity(self): query = """MATCH (a:L {val: 'v1'}) WITH a MATCH (a)-[e]->(b) RETURN b.val""" actual_result = redis_graph.query(query) # Verify that this query does not generate a Cartesian product. execution_plan = redis_graph.execution_plan(query) self.env.assertNotIn('Cartesian Product', execution_plan) # Verify results. expected_result = [['v2']] self.env.assertEquals(actual_result.result_set, expected_result) def test02_match_create_bound_variable(self): # Extend the graph such that the new form is: # (v1)-[:E]->(v2)-[:E]->(v3)-[:e]->(v4) query = """MATCH (a:L {val: 'v3'}) CREATE (a)-[:E]->(b:L {val: 'v4'}) RETURN b.val""" actual_result = redis_graph.query(query) expected_result = [['v4']] self.env.assertEquals(actual_result.result_set, expected_result) self.env.assertEquals(actual_result.relationships_created, 1) self.env.assertEquals(actual_result.nodes_created, 1) def test03_procedure_match_bound_variable(self): # Create a full-text index. redis_graph.call_procedure("db.idx.fulltext.createNodeIndex", 'L', 'val') # Project the result of scanning this index into a MATCH pattern. query = """CALL db.idx.fulltext.queryNodes('L', 'v1') YIELD node MATCH (node)-[]->(b) RETURN b.val""" # Verify that execution begins at the procedure call and proceeds into the traversals. execution_plan = redis_graph.execution_plan(query) # For the moment, we'll just verify that ProcedureCall appears later in the plan than # its parent, Conditional Traverse. traverse_idx = execution_plan.index("Conditional Traverse") call_idx = execution_plan.index("ProcedureCall") self.env.assertTrue(call_idx > traverse_idx) # Verify the results actual_result = redis_graph.query(query) expected_result = [['v2']] self.env.assertEquals(actual_result.result_set, expected_result) def test04_projected_scanned_entity(self): query = """MATCH (a:L {val: 'v1'}) WITH a MATCH (a), (b {val: 'v2'}) RETURN a.val, b.val""" actual_result = redis_graph.query(query) # Verify that this query generates exactly 2 scan ops. execution_plan = redis_graph.execution_plan(query) self.env.assertEquals(2, execution_plan.count('Scan')) # Verify results. expected_result = [['v1', 'v2']] self.env.assertEquals(actual_result.result_set, expected_result)
class testFunctionCallsFlow(FlowTestsBase): def __init__(self): self.env = Env() global graph global redis_con redis_con = self.env.getConnection() graph = Graph("G", redis_con) self.populate_graph() def populate_graph(self): global graph nodes = {} # Create entities for idx, p in enumerate(people): node = Node(label="person", properties={"name": p, "val": idx}) graph.add_node(node) nodes[p] = node # Fully connected graph for src in nodes: for dest in nodes: if src != dest: edge = Edge(nodes[src], "know", nodes[dest]) graph.add_edge(edge) for src in nodes: for dest in nodes: if src != dest: edge = Edge(nodes[src], "works_with", nodes[dest]) graph.add_edge(edge) graph.commit() query = """MATCH (a)-[:know]->(b) CREATE (a)-[:know]->(b)""" graph.query(query) def expect_type_error(self, query): try: graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting a type error. self.env.assertIn("Type mismatch", e.message) def expect_error(self, query, expected_err_msg): try: graph.query(query) assert (False) except redis.exceptions.ResponseError as e: # Expecting a type error. self.env.assertIn(expected_err_msg, e.message) # Validate capturing of errors prior to query execution. def test01_compile_time_errors(self): query = """RETURN toUpper(5)""" self.expect_type_error(query) query = """RETURN 'a' * 2""" self.expect_type_error(query) query = """RETURN max(1 + min(2))""" self.expect_error( query, "Can't use aggregate functions inside of aggregate functions") def test02_boolean_comparisons(self): query = """RETURN true = 5""" actual_result = graph.query(query) expected_result = [[False]] self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN true <> 'str'""" actual_result = graph.query(query) expected_result = [[True]] self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN 'anything' <> NULL""" actual_result = graph.query(query) expected_result = [[None]] self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN 'anything' = NULL""" actual_result = graph.query(query) expected_result = [[None]] self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN 10 >= 1.5""" actual_result = graph.query(query) expected_result = [[True]] self.env.assertEquals(actual_result.result_set, expected_result) query = """RETURN -1 < 1""" actual_result = graph.query(query) expected_result = [[True]] self.env.assertEquals(actual_result.result_set, expected_result) def test03_boolean_errors(self): query = """RETURN 'str' < 5.5""" self.expect_type_error(query) query = """RETURN true > 5""" self.expect_type_error(query) query = """MATCH (a) RETURN a < 'anything' LIMIT 1""" self.expect_type_error(query) def test04_entity_functions(self): query = "RETURN ID(5)" self.expect_type_error(query) query = "MATCH (a) RETURN ID(a) ORDER BY ID(a) LIMIT 3" actual_result = graph.query(query) expected_result = [[0], [1], [2]] self.env.assertEquals(actual_result.result_set, expected_result) query = "MATCH (a)-[e]->() RETURN ID(e) ORDER BY ID(e) LIMIT 3" actual_result = graph.query(query) expected_result = [[0], [1], [2]] self.env.assertEquals(actual_result.result_set, expected_result) query = "RETURN EXISTS(null)" actual_result = graph.query(query) expected_result = [[False]] self.env.assertEquals(actual_result.result_set, expected_result) query = "RETURN EXISTS('anything')" actual_result = graph.query(query) expected_result = [[True]] self.env.assertEquals(actual_result.result_set, expected_result) def test07_nonmap_errors(self): query = """MATCH (a) WITH a.name AS scalar RETURN scalar.name""" self.expect_type_error(query) def test08_apply_all_function(self): query = "MATCH () RETURN COUNT(*)" actual_result = graph.query(query) expected_result = [[4]] self.env.assertEquals(actual_result.result_set, expected_result) query = "UNWIND [1, 2] AS a RETURN COUNT(*)" actual_result = graph.query(query) expected_result = [[2]] self.env.assertEquals(actual_result.result_set, expected_result) def test09_static_aggregation(self): query = "RETURN count(*)" actual_result = graph.query(query) expected_result = [[1]] self.env.assertEquals(actual_result.result_set, expected_result) query = "RETURN max(2)" actual_result = graph.query(query) expected_result = [[2]] self.env.assertEquals(actual_result.result_set, expected_result) query = "RETURN min(3)" actual_result = graph.query(query) expected_result = [[3]] self.env.assertEquals(actual_result.result_set, expected_result) def test10_modulo_inputs(self): # Validate modulo with integer inputs. query = "RETURN 5 % 2" actual_result = graph.query(query) expected_result = [[1]] self.env.assertEquals(actual_result.result_set, expected_result) # Validate modulo with a floating-point dividend. query = "RETURN 5.5 % 2" actual_result = graph.query(query) expected_result = [[1.5]] self.env.assertEquals(actual_result.result_set, expected_result) # Validate modulo with a floating-point divisor. query = "RETURN 5 % 2.5" actual_result = graph.query(query) expected_result = [[0]] self.env.assertEquals(actual_result.result_set, expected_result) # Validate modulo with both a floating-point dividen and a floating-point divisor. query = "RETURN 5.5 % 2.5" actual_result = graph.query(query) expected_result = [[0.5]] self.env.assertEquals(actual_result.result_set, expected_result) # Validate modulo with negative integer inputs. query = "RETURN -5 % -2" actual_result = graph.query(query) expected_result = [[-1]] self.env.assertEquals(actual_result.result_set, expected_result) # Validate modulo with negative floating-point inputs. query = "RETURN -5.5 % -2.5" actual_result = graph.query(query) expected_result = [[-0.5]] self.env.assertEquals(actual_result.result_set, expected_result) # Aggregate functions should handle null inputs appropriately. def test11_null_aggregate_function_inputs(self): # SUM should sum all non-null inputs. query = """UNWIND [1, NULL, 3] AS a RETURN sum(a)""" actual_result = graph.query(query) expected_result = [[4]] self.env.assertEquals(actual_result.result_set, expected_result) # SUM should return 0 given a fully NULL input. query = """WITH NULL AS a RETURN sum(a)""" actual_result = graph.query(query) expected_result = [[0]] self.env.assertEquals(actual_result.result_set, expected_result) # COUNT should count all non-null inputs. query = """UNWIND [1, NULL, 3] AS a RETURN count(a)""" actual_result = graph.query(query) expected_result = [[2]] self.env.assertEquals(actual_result.result_set, expected_result) # COUNT should return 0 given a fully NULL input. query = """WITH NULL AS a RETURN count(a)""" actual_result = graph.query(query) expected_result = [[0]] self.env.assertEquals(actual_result.result_set, expected_result) # COLLECT should ignore null inputs. query = """UNWIND [1, NULL, 3] AS a RETURN collect(a)""" actual_result = graph.query(query) expected_result = [[[1, 3]]] self.env.assertEquals(actual_result.result_set, expected_result) # COLLECT should return an empty array on all null inputs. query = """WITH NULL AS a RETURN collect(a)""" actual_result = graph.query(query) expected_result = [[[]]] self.env.assertEquals(actual_result.result_set, expected_result) # Verify that nested functions that perform heap allocations return properly. def test12_nested_heap_functions(self): query = """MATCH p = (n) WITH head(nodes(p)) AS node RETURN node.name ORDER BY node.name""" actual_result = graph.query(query) expected_result = [['Ailon'], ['Alon'], ['Boaz'], ['Roi']] self.env.assertEquals(actual_result.result_set, expected_result) # CASE...WHEN statements should properly handle NULL, false, and true evaluations. def test13_case_when_inputs(self): # Simple case form: single value evaluation. query = """UNWIND [NULL, true, false] AS v RETURN v, CASE v WHEN true THEN v END""" actual_result = graph.query(query) expected_result = [[None, None], [True, True], [False, None]] self.env.assertEquals(actual_result.result_set, expected_result) query = """UNWIND [NULL, true, false] AS v RETURN v, CASE v WHEN true THEN v WHEN false THEN v END""" actual_result = graph.query(query) expected_result = [[None, None], [True, True], [False, False]] self.env.assertEquals(actual_result.result_set, expected_result) # Generic case form: evaluation for each case. query = """UNWIND [NULL, true, false] AS v RETURN v, CASE WHEN v THEN v END""" actual_result = graph.query(query) # Only the true value should return non-NULL. expected_result = [[None, None], [True, True], [False, None]] self.env.assertEquals(actual_result.result_set, expected_result) query = """UNWIND [NULL, true, false] AS v RETURN v, CASE WHEN v IS NOT NULL THEN v END""" actual_result = graph.query(query) # The true and false values should both return non-NULL. expected_result = [[None, None], [True, True], [False, False]] self.env.assertEquals(actual_result.result_set, expected_result) # CASE...WHEN statements should manage allocated values properly. def test14_case_when_memory_management(self): # Simple case form: single value evaluation. query = """WITH 'A' AS a WITH CASE a WHEN 'A' THEN toString(a) END AS key RETURN toLower(key)""" actual_result = graph.query(query) expected_result = [['a']] self.env.assertEquals(actual_result.result_set, expected_result) # Generic case form: evaluation for each case. query = """WITH 'A' AS a WITH CASE WHEN true THEN toString(a) END AS key RETURN toLower(key)""" actual_result = graph.query(query) expected_result = [['a']] self.env.assertEquals(actual_result.result_set, expected_result) def test15_aggregate_error_handling(self): functions = [ "avg", "collect", "count", "max", "min", "sum", "percentileDisc", "percentileCont", "stDev" ] # Test all functions for invalid argument counts. for function in functions: query = """UNWIND range(0, 10) AS val RETURN %s(val, val, val)""" % ( function) self.expect_error(query, "Received 3 arguments") # Test numeric functions for invalid input types. numeric_functions = ["avg", "sum", "stDev"] for function in numeric_functions: query = """UNWIND ['a', 'b', 'c'] AS val RETURN %s(val)""" % ( function) self.expect_type_error(query) # Test invalid numeric input for percentile function. query = """UNWIND range(0, 10) AS val RETURN percentileDisc(val, -1)""" self.expect_error(query, "must be a number in the range 0.0 to 1.0") # startNode and endNode calls should return the appropriate nodes. def test16_edge_endpoints(self): query = """MATCH (a)-[e]->(b) RETURN a.name, startNode(e).name, b.name, endNode(e).name""" actual_result = graph.query(query) for row in actual_result.result_set: self.env.assertEquals(row[0], row[1]) self.env.assertEquals(row[2], row[3])
class testGraphPersistency(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) global redis_con redis_con = self.env.getConnection() def populate_graph(self, graph_name): # quick return if graph already exists if redis_con.exists(graph_name): return redis_graph people = ["Roi", "Alon", "Ailon", "Boaz", "Tal", "Omri", "Ori"] visits = [("Roi", "USA"), ("Alon", "Israel"), ("Ailon", "Japan"), ("Boaz", "United Kingdom")] countries = ["Israel", "USA", "Japan", "United Kingdom"] redis_graph = Graph(graph_name, redis_con) personNodes = {} countryNodes = {} # create nodes for p in people: person = Node(label="person", properties={ "name": p, "height": random.randint(160, 200) }) redis_graph.add_node(person) personNodes[p] = person for p in countries: country = Node(label="country", properties={ "name": p, "population": random.randint(100, 400) }) redis_graph.add_node(country) countryNodes[p] = country # create edges for v in visits: person = v[0] country = v[1] edge = Edge(personNodes[person], 'visit', countryNodes[country], properties={'purpose': 'pleasure'}) redis_graph.add_edge(edge) redis_graph.commit() # delete nodes, to introduce deleted item within our datablock query = """MATCH (n:person) WHERE n.name = 'Roi' or n.name = 'Ailon' DELETE n""" redis_graph.query(query) query = """MATCH (n:country) WHERE n.name = 'USA' DELETE n""" redis_graph.query(query) # create indices actual_result = redis_con.execute_command( "GRAPH.QUERY", graph_name, "CREATE INDEX ON :person(name, height)") actual_result = redis_con.execute_command( "GRAPH.QUERY", graph_name, "CREATE INDEX ON :country(name, population)") return redis_graph def populate_dense_graph(self, graph_name): dense_graph = Graph(graph_name, redis_con) # return early if graph exists if redis_con.exists(graph_name): return dense_graph nodes = [] for i in range(10): node = Node(label="n", properties={"val": i}) dense_graph.add_node(node) nodes.append(node) for n_idx, n in enumerate(nodes): for m_idx, m in enumerate(nodes[:n_idx]): dense_graph.add_edge(Edge(n, "connected", m)) dense_graph.flush() return dense_graph def test01_save_load_rdb(self): graph_names = ["G", "{tag}_G"] for graph_name in graph_names: graph = self.populate_graph(graph_name) for i in range(2): if i == 1: # Save RDB & Load from RDB self.env.dumpAndReload() # Verify # Expecting 5 person entities. query = """MATCH (p:person) RETURN COUNT(p)""" actual_result = graph.query(query) nodeCount = actual_result.result_set[0][0] self.env.assertEquals(nodeCount, 5) query = """MATCH (p:person) WHERE p.name='Alon' RETURN COUNT(p)""" actual_result = graph.query(query) nodeCount = actual_result.result_set[0][0] self.env.assertEquals(nodeCount, 1) # Expecting 3 country entities. query = """MATCH (c:country) RETURN COUNT(c)""" actual_result = graph.query(query) nodeCount = actual_result.result_set[0][0] self.env.assertEquals(nodeCount, 3) query = """MATCH (c:country) WHERE c.name = 'Israel' RETURN COUNT(c)""" actual_result = graph.query(query) nodeCount = actual_result.result_set[0][0] self.env.assertEquals(nodeCount, 1) # Expecting 2 visit edges. query = """MATCH (n:person)-[e:visit]->(c:country) WHERE e.purpose='pleasure' RETURN COUNT(e)""" actual_result = graph.query(query) edgeCount = actual_result.result_set[0][0] self.env.assertEquals(edgeCount, 2) # Verify indices exists expected_indices = [[ "exact-match", "country", ["name", "population"] ], ["exact-match", "person", ["name", "height"]]] indices = graph.query("""CALL db.indexes()""").result_set self.env.assertEquals(indices, expected_indices) # Verify that edges are not modified after entity deletion def test02_deleted_entity_migration(self): graph_names = ("H", "{tag}_H") for graph_name in graph_names: graph = self.populate_dense_graph(graph_name) query = """MATCH (p) WHERE ID(p) = 0 OR ID(p) = 3 OR ID(p) = 7 OR ID(p) = 9 DELETE p""" actual_result = graph.query(query) self.env.assertEquals(actual_result.nodes_deleted, 4) query = """MATCH (p)-[]->(q) RETURN p.val, q.val ORDER BY p.val, q.val""" first_result = graph.query(query) # Save RDB & Load from RDB redis_con.execute_command("DEBUG", "RELOAD") second_result = graph.query(query) self.env.assertEquals(first_result.result_set, second_result.result_set) # Strings, numerics, booleans, array, and point properties should be properly serialized and reloaded def test03_restore_properties(self): graph_names = ("simple_props", "{tag}_simple_props") for graph_name in graph_names: graph = Graph(graph_name, redis_con) query = """CREATE (:p {strval: 'str', numval: 5.5, boolval: true, array: [1,2,3], pointval: point({latitude: 5.5, longitude: 6})})""" result = graph.query(query) # Verify that node was created correctly self.env.assertEquals(result.nodes_created, 1) self.env.assertEquals(result.properties_set, 5) # Save RDB & Load from RDB redis_con.execute_command("DEBUG", "RELOAD") query = """MATCH (p) RETURN p.boolval, p.numval, p.strval, p.array, p.pointval""" actual_result = graph.query(query) # Verify that the properties are loaded correctly. expected_result = [[ True, 5.5, 'str', [1, 2, 3], { "latitude": 5.5, "longitude": 6.0 } ]] self.env.assertEquals(actual_result.result_set, expected_result) # Verify multiple edges of the same relation between nodes A and B # are saved and restored correctly. def test04_repeated_edges(self): graph_names = ["repeated_edges", "{tag}_repeated_edges"] for graph_name in graph_names: graph = Graph(graph_name, redis_con) src = Node(label='p', properties={'name': 'src'}) dest = Node(label='p', properties={'name': 'dest'}) edge1 = Edge(src, 'e', dest, properties={'val': 1}) edge2 = Edge(src, 'e', dest, properties={'val': 2}) graph.add_node(src) graph.add_node(dest) graph.add_edge(edge1) graph.add_edge(edge2) graph.flush() # Verify the new edge q = """MATCH (a)-[e]->(b) RETURN e.val, a.name, b.name ORDER BY e.val""" actual_result = graph.query(q) expected_result = [[ edge1.properties['val'], src.properties['name'], dest.properties['name'] ], [ edge2.properties['val'], src.properties['name'], dest.properties['name'] ]] self.env.assertEquals(actual_result.result_set, expected_result) # Save RDB & Load from RDB redis_con.execute_command("DEBUG", "RELOAD") # Verify that the latest edge was properly saved and loaded actual_result = graph.query(q) self.env.assertEquals(actual_result.result_set, expected_result) # Verify that graphs larger than the # default capacity are persisted correctly. def test05_load_large_graph(self): graph_name = "LARGE_GRAPH" graph = Graph(graph_name, redis_con) q = """UNWIND range(1, 50000) AS v CREATE (:L)-[:R {v: v}]->(:L)""" actual_result = graph.query(q) self.env.assertEquals(actual_result.nodes_created, 100_000) self.env.assertEquals(actual_result.relationships_created, 50_000) redis_con.execute_command("DEBUG", "RELOAD") expected_result = [[50000]] queries = [ """MATCH (:L)-[r {v: 50000}]->(:L) RETURN r.v""", """MATCH (:L)-[r:R {v: 50000}]->(:L) RETURN r.v""", """MATCH ()-[r:R {v: 50000}]->() RETURN r.v""" ] for q in queries: actual_result = graph.query(q) self.env.assertEquals(actual_result.result_set, expected_result)
class testIndexScanFlow(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) def setUp(self): global redis_graph redis_con = self.env.getConnection() redis_graph = Graph(social_utils.graph_name, redis_con) social_utils.populate_graph(redis_con, redis_graph) self.build_indices() def tearDown(self): self.env.cmd('flushall') def build_indices(self): global redis_graph redis_graph.redis_con.execute_command("GRAPH.QUERY", "social", "CREATE INDEX ON :person(age)") redis_graph.redis_con.execute_command( "GRAPH.QUERY", "social", "CREATE INDEX ON :country(name)") # Validate that Cartesian products using index and label scans succeed def test01_cartesian_product_mixed_scans(self): query = "MATCH (p:person), (c:country) WHERE p.age > 0 RETURN p.age, c.name ORDER BY p.age, c.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) self.env.assertIn('Label Scan', plan) indexed_result = redis_graph.query(query) query = "MATCH (p:person), (c:country) RETURN p.age, c.name ORDER BY p.age, c.name" plan = redis_graph.execution_plan(query) self.env.assertNotIn('Index Scan', plan) self.env.assertIn('Label Scan', plan) unindexed_result = redis_graph.query(query) self.env.assertEquals(indexed_result.result_set, unindexed_result.result_set) # Validate that Cartesian products using just index scans succeed def test02_cartesian_product_index_scans_only(self): query = "MATCH (p:person), (c:country) WHERE p.age > 0 AND c.name > '' RETURN p.age, c.name ORDER BY p.age, c.name" plan = redis_graph.execution_plan(query) # The two streams should both use index scans self.env.assertEquals(plan.count('Index Scan'), 2) self.env.assertNotIn('Label Scan', plan) indexed_result = redis_graph.query(query) query = "MATCH (p:person), (c:country) RETURN p.age, c.name ORDER BY p.age, c.name" plan = redis_graph.execution_plan(query) self.env.assertNotIn('Index Scan', plan) self.env.assertIn('Label Scan', plan) unindexed_result = redis_graph.query(query) self.env.assertEquals(indexed_result.result_set, unindexed_result.result_set) # Validate that the appropriate bounds are respected when a Cartesian product uses the same index in two streams def test03_cartesian_product_reused_index(self): redis_graph.redis_con.execute_command("GRAPH.QUERY", "social", "CREATE INDEX ON :person(name)") query = "MATCH (a:person {name: 'Omri Traub'}), (b:person) WHERE b.age <= 30 RETURN a.name, b.name ORDER BY a.name, b.name" plan = redis_graph.execution_plan(query) # The two streams should both use index scans self.env.assertEquals(plan.count('Index Scan'), 2) self.env.assertNotIn('Label Scan', plan) expected_result = [['Omri Traub', 'Gal Derriere'], ['Omri Traub', 'Lucy Yanfital']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate index utilization when filtering on a numeric field with the `IN` keyword. def test04_test_in_operator_numerics(self): # Validate the transformation of IN to multiple OR expressions. query = "MATCH (p:person) WHERE p.age IN [1,2,3] RETURN p" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) # Validate that nested arrays are not scanned in index. query = "MATCH (p:person) WHERE p.age IN [[1,2],3] RETURN p" plan = redis_graph.execution_plan(query) self.env.assertNotIn('Index Scan', plan) self.env.assertIn('Label Scan', plan) # Validate the transformation of IN to multiple OR, over a range. query = "MATCH (p:person) WHERE p.age IN range(0,30) RETURN p.name ORDER BY p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) expected_result = [['Gal Derriere'], ['Lucy Yanfital']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate the transformation of IN to empty index iterator. query = "MATCH (p:person) WHERE p.age IN [] RETURN p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) expected_result = [] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate the transformation of IN OR IN to empty index iterators. query = "MATCH (p:person) WHERE p.age IN [] OR p.age IN [] RETURN p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) expected_result = [] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate the transformation of multiple IN filters. query = "MATCH (p:person) WHERE p.age IN [26, 27, 30] OR p.age IN [33, 34, 35] RETURN p.name ORDER BY p.age" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) expected_result = [['Gal Derriere'], ['Lucy Yanfital'], ['Omri Traub'], ['Noam Nativ']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate the transformation of multiple IN filters. query = "MATCH (p:person) WHERE p.age IN [26, 27, 30] OR p.age IN [33, 34, 35] OR p.age IN [] RETURN p.name ORDER BY p.age" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) expected_result = [['Gal Derriere'], ['Lucy Yanfital'], ['Omri Traub'], ['Noam Nativ']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate index utilization when filtering on string fields with the `IN` keyword. def test05_test_in_operator_string_props(self): # Build an index on the name property. redis_graph.redis_con.execute_command("GRAPH.QUERY", "social", "CREATE INDEX ON :person(name)") # Validate the transformation of IN to multiple OR expressions over string properties. query = "MATCH (p:person) WHERE p.name IN ['Gal Derriere', 'Lucy Yanfital'] RETURN p.name ORDER BY p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) self.env.assertNotIn('Label Scan', plan) expected_result = [['Gal Derriere'], ['Lucy Yanfital']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Combine numeric and string filters specified by IN. query = "MATCH (p:person) WHERE p.name IN ['Gal Derriere', 'Lucy Yanfital'] AND p.age in [30] RETURN p.name ORDER BY p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) self.env.assertNotIn('Label Scan', plan) expected_result = [['Lucy Yanfital']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Validate an empty index on IN with multiple indexes query = "MATCH (p:person) WHERE p.name IN [] OR p.age IN [] RETURN p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) expected_result = [] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # Combine IN filters with other relational filters. query = "MATCH (p:person) WHERE p.name IN ['Gal Derriere', 'Lucy Yanfital'] AND p.name < 'H' RETURN p.name ORDER BY p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) self.env.assertNotIn('Label Scan', plan) expected_result = [['Gal Derriere']] result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) query = "MATCH (p:person) WHERE p.name IN ['Gal Derriere', 'Lucy Yanfital'] OR p.age = 33 RETURN p.name ORDER BY p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) self.env.assertNotIn('Label Scan', plan) expected_result = [['Gal Derriere'], ['Lucy Yanfital'], ['Omri Traub']] result = redis_graph.query(query) result = redis_graph.query(query) self.env.assertEquals(result.result_set, expected_result) # ',' is the default separator for tag indices # we've updated our separator to '\0' this test verifies issue 696: # https://github.com/RedisGraph/RedisGraph/issues/696 def test06_tag_separator(self): redis_con = self.env.getConnection() redis_graph = Graph("G", redis_con) # Create a single node with a long string property, introduce a comma as part of the string. query = """CREATE (:Node{value:"A ValuePartition is a pattern that describes a restricted set of classes from which a property can be associated. The parent class is used in restrictions, and the covering axiom means that only members of the subclasses may be used as values."})""" redis_graph.query(query) # Index property. query = """CREATE INDEX ON :Node(value)""" redis_graph.query(query) # Make sure node is returned by index scan. query = """MATCH (a:Node{value:"A ValuePartition is a pattern that describes a restricted set of classes from which a property can be associated. The parent class is used in restrictions, and the covering axiom means that only members of the subclasses may be used as values."}) RETURN a""" plan = redis_graph.execution_plan(query) result_set = redis_graph.query(query).result_set self.env.assertIn('Index Scan', plan) self.env.assertEqual(len(result_set), 1) def test07_index_scan_and_id(self): redis_con = self.env.getConnection() redis_graph = Graph("G", redis_con) nodes = [] for i in range(10): node = Node(node_id=i, label='person', properties={'age': i}) nodes.append(node) redis_graph.add_node(node) redis_graph.flush() query = """CREATE INDEX ON :person(age)""" query_result = redis_graph.query(query) self.env.assertEqual(1, query_result.indices_created) query = """MATCH (n:person) WHERE id(n)>=7 AND n.age<9 RETURN n ORDER BY n.age""" plan = redis_graph.execution_plan(query) query_result = redis_graph.query(query) self.env.assertIn('Index Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) self.env.assertEqual(2, len(query_result.result_set)) expected_result = [[nodes[7]], [nodes[8]]] self.env.assertEquals(expected_result, query_result.result_set) # Validate placement of index scans and filter ops when not all filters can be replaced. def test08_index_scan_multiple_filters(self): query = "MATCH (p:person) WHERE p.age = 30 AND NOT EXISTS(p.fakeprop) RETURN p.name" plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) self.env.assertNotIn('Label Scan', plan) self.env.assertIn('Filter', plan) query_result = redis_graph.query(query) expected_result = ["Lucy Yanfital"] self.env.assertEquals(query_result.result_set[0], expected_result) def test09_index_scan_with_params(self): query = "MATCH (p:person) WHERE p.age = $age RETURN p.name" params = {'age': 30} query = redis_graph.build_params_header(params) + query plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) query_result = redis_graph.query(query) expected_result = ["Lucy Yanfital"] self.env.assertEquals(query_result.result_set[0], expected_result) def test10_index_scan_with_param_array(self): query = "MATCH (p:person) WHERE p.age in $ages RETURN p.name" params = {'ages': [30]} query = redis_graph.build_params_header(params) + query plan = redis_graph.execution_plan(query) self.env.assertIn('Index Scan', plan) query_result = redis_graph.query(query) expected_result = ["Lucy Yanfital"] self.env.assertEquals(query_result.result_set[0], expected_result) def test11_single_index_multiple_scans(self): query = "MERGE (p1:person {age: 40}) MERGE (p2:person {age: 41})" plan = redis_graph.execution_plan(query) # Two index scans should be performed. self.env.assertEqual(plan.count("Index Scan"), 2) query_result = redis_graph.query(query) # Two new nodes should be created. self.env.assertEquals(query_result.nodes_created, 2) def test12_remove_scans_before_index(self): query = "MATCH (a:person {age: 32})-[]->(b) WHERE (b:person)-[]->(a) RETURN a" plan = redis_graph.execution_plan(query) # One index scan should be performed. self.env.assertEqual(plan.count("Index Scan"), 1) def test13_point_index_scan(self): # create index q = "CREATE INDEX ON :restaurant(location)" redis_graph.query(q) # create restaurant q = "CREATE (:restaurant {location: point({latitude:30.27822306, longitude:-97.75134723})})" redis_graph.query(q) # locate other restaurants within a 1000m radius q = """MATCH (r:restaurant) WHERE distance(r.location, point({latitude:30.27822306, longitude:-97.75134723})) < 1000 RETURN r""" # make sure index is used plan = redis_graph.execution_plan(q) self.env.assertIn("Index Scan", plan) # refine query from '<' to '<=' q = """MATCH (r:restaurant) WHERE distance(r.location, point({latitude:30.27822306, longitude:-97.75134723})) <= 1000 RETURN r""" # make sure index is used plan = redis_graph.execution_plan(q) self.env.assertIn("Index Scan", plan) # index should NOT be used when searching for points outside of a circle # testing operand: '>', '>=' and '=' q = """MATCH (r:restaurant) WHERE distance(r.location, point({latitude:30.27822306, longitude:-97.75134723})) > 1000 RETURN r""" # make sure index is NOT used plan = redis_graph.execution_plan(q) self.env.assertNotIn("Index Scan", plan) q = """MATCH (r:restaurant) WHERE distance(r.location, point({latitude:30.27822306, longitude:-97.75134723})) >= 1000 RETURN r""" # make sure index is NOT used plan = redis_graph.execution_plan(q) self.env.assertNotIn("Index Scan", plan) q = """MATCH (r:restaurant) WHERE distance(r.location, point({latitude:30.27822306, longitude:-97.75134723})) = 1000 RETURN r""" # make sure index is NOT used plan = redis_graph.execution_plan(q) self.env.assertNotIn("Index Scan", plan) def test14_index_scan_utilize_array(self): # Querying indexed properties using IN a constant array should utilize indexes. query = "MATCH (a:person) WHERE a.age IN [34, 33] RETURN a.name ORDER BY a.name" plan = redis_graph.execution_plan(query) # One index scan should be performed. self.env.assertEqual(plan.count("Index Scan"), 1) query_result = redis_graph.query(query) expected_result = [["Noam Nativ"], ["Omri Traub"]] self.env.assertEquals(query_result.result_set, expected_result) # Querying indexed properties using IN a generated array should utilize indexes. query = "MATCH (a:person) WHERE a.age IN range(33, 34) RETURN a.name ORDER BY a.name" plan = redis_graph.execution_plan(query) # One index scan should be performed. self.env.assertEqual(plan.count("Index Scan"), 1) query_result = redis_graph.query(query) expected_result = [["Noam Nativ"], ["Omri Traub"]] self.env.assertEquals(query_result.result_set, expected_result) # Querying indexed properties using IN a non-constant array should not utilize indexes. query = "MATCH (a:person)-[]->(b) WHERE a.age IN b.arr RETURN a" plan = redis_graph.execution_plan(query) # No index scans should be performed. self.env.assertEqual(plan.count("Label Scan"), 1) self.env.assertEqual(plan.count("Index Scan"), 0)
class testGraphDeletionFlow(FlowTestsBase): def __init__(self): self.env = Env() global redis_graph redis_con = self.env.getConnection() redis_graph = Graph(GRAPH_ID, redis_con) self.populate_graph() def populate_graph(self): nodes = {} # Create entities people = ["Roi", "Alon", "Ailon", "Boaz", "Tal", "Omri", "Ori"] for p in people: node = Node(label="person", properties={"name": p}) redis_graph.add_node(node) nodes[p] = node # Fully connected graph for src in nodes: for dest in nodes: if src != dest: edge = Edge(nodes[src], "know", nodes[dest]) redis_graph.add_edge(edge) # Connect Roi to Alon via another edge type. edge = Edge(nodes["Roi"], "SameBirthday", nodes["Alon"]) redis_graph.add_edge(edge) redis_graph.commit() # Count how many nodes contains the `name` attribute # remove the `name` attribute from some nodes # make sure the count updates accordingly, # restore `name` attribute from, verify that count returns to its original value. def test01_delete_attribute(self): # How many nodes contains the 'name' attribute query = """MATCH (n) WHERE EXISTS(n.name)=true RETURN count(n)""" actual_result = redis_graph.query(query) nodeCount = actual_result.result_set[0][0] self.env.assertEquals(nodeCount, 7) # Remove Tal's name attribute. query = """MATCH (n) WHERE n.name = 'Tal' SET n.name = NULL""" redis_graph.query(query) # How many nodes contains the 'name' attribute, # should reduce by 1 from previous count. query = """MATCH (n) WHERE EXISTS(n.name)=true RETURN count(n)""" actual_result = redis_graph.query(query) nodeCount = actual_result.result_set[0][0] self.env.assertEquals(nodeCount, 6) # Reintroduce Tal's name attribute. query = """MATCH (n) WHERE EXISTS(n.name)=false SET n.name = 'Tal'""" actual_result = redis_graph.query(query) # How many nodes contains the 'name' attribute query = """MATCH (n) WHERE EXISTS(n.name)=true RETURN count(n)""" actual_result = redis_graph.query(query) nodeCount = actual_result.result_set[0][0] self.env.assertEquals(nodeCount, 7) # Delete edges pointing into either Boaz or Ori. def test02_delete_edges(self): query = """MATCH (s:person)-[e:know]->(d:person) WHERE d.name = "Boaz" OR d.name = "Ori" RETURN count(e)""" actual_result = redis_graph.query(query) edge_count = actual_result.result_set[0][0] query = """MATCH (s:person)-[e:know]->(d:person) WHERE d.name = "Boaz" OR d.name = "Ori" DELETE e""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.relationships_deleted, edge_count) self.env.assertEquals(actual_result.nodes_deleted, 0) # Make sure there are no edges going into either Boaz or Ori. def test03_verify_edge_deletion(self): query = """MATCH (s:person)-[e:know]->(d:person) WHERE d.name = "Boaz" AND d.name = "Ori" RETURN COUNT(s)""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), 0) # Remove 'know' edge connecting Roi to Alon # Leaving a single edge of type SameBirthday # connecting the two. def test04_delete_typed_edge(self): query = """MATCH (s:person {name: "Roi"})-[e:know]->(d:person {name: "Alon"}) RETURN count(e)""" actual_result = redis_graph.query(query) edge_count = actual_result.result_set[0][0] query = """MATCH (s:person {name: "Roi"})-[e:know]->(d:person {name: "Alon"}) DELETE e""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.relationships_deleted, edge_count) self.env.assertEquals(actual_result.nodes_deleted, 0) # Make sure Roi is still connected to Alon # via the "SameBirthday" type edge. def test05_verify_delete_typed_edge(self): query = """MATCH (s:person {name: "Roi"})-[e:SameBirthday]->(d:person {name: "Alon"}) RETURN COUNT(s)""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), 1) query = """MATCH (s:person {name: "Roi"})-[e:know]->(d:person {name: "Alon"}) RETURN COUNT(s)""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), 0) # Remove both Alon and Boaz from the graph. def test06_delete_nodes(self): rel_count_query = """MATCH (a:person)-[e]->(b:person) WHERE a.name = 'Boaz' OR a.name = 'Alon' OR b.name = 'Boaz' OR b.name = 'Alon' RETURN COUNT(e)""" rel_count_result = redis_graph.query(rel_count_query) # Get the total number of unique edges (incoming and outgoing) # connected to Alon and Boaz. rel_count = rel_count_result.result_set[0][0] query = """MATCH (s:person) WHERE s.name = "Boaz" OR s.name = "Alon" DELETE s""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.relationships_deleted, rel_count) self.env.assertEquals(actual_result.nodes_deleted, 2) # Make sure Alon and Boaz are not in the graph. def test07_get_deleted_nodes(self): query = """MATCH (s:person) WHERE s.name = "Boaz" OR s.name = "Alon" RETURN s""" actual_result = redis_graph.query(query) self.env.assertEquals(len(actual_result.result_set), 0) # Make sure Alon and Boaz are the only removed nodes. def test08_verify_node_deletion(self): query = """MATCH (s:person) RETURN COUNT(s)""" actual_result = redis_graph.query(query) nodeCount = actual_result.result_set[0][0] self.env.assertEquals(nodeCount, 5) def test09_delete_entire_graph(self): # Make sure graph exists. query = """MATCH (n) RETURN COUNT(n)""" result = redis_graph.query(query) nodeCount = result.result_set[0][0] self.env.assertGreater(nodeCount, 0) # Delete graph. redis_graph.delete() # Try to query a deleted graph. redis_graph.query(query) result = redis_graph.query(query) nodeCount = result.result_set[0][0] self.env.assertEquals(nodeCount, 0) def test10_bulk_edge_deletion_timing(self): # Create large amount of relationships (50000). redis_graph.query( """UNWIND(range(1, 50000)) as x CREATE ()-[:R]->()""") # Delete and benchmark for 300ms. query = """MATCH (a)-[e:R]->(b) DELETE e""" result = redis_graph.query(query) query_info = QueryInfo( query=query, description= "Test the execution time for deleting large number of edges", max_run_time_ms=300) # Test will not fail for execution time > 300ms but a warning will be shown at the test output. self.env.assertEquals(result.relationships_deleted, 50000) self._assert_run_time(result, query_info) def test11_delete_entity_type_validation(self): # Currently we only support deletion of either nodes or edges # we've yet to introduce deletion of Path. # Try to delete an integer. query = """UNWIND [1] AS x DELETE x""" try: redis_graph.query(query) self.env.assertTrue(False) except Exception as error: self.env.assertTrue("Delete type mismatch" in error.message) query = """MATCH p=(n) DELETE p""" try: redis_graph.query(query) self.env.assertTrue(False) except Exception as error: self.env.assertTrue("Delete type mismatch" in error.message) def test12_delete_unwind_entity(self): redis_con = self.env.getConnection() redis_graph = Graph("delete_test", redis_con) # Create 10 nodes. for i in range(10): redis_graph.add_node(Node()) redis_graph.flush() # Unwind path nodes. query = """MATCH p = () UNWIND nodes(p) AS node DELETE node""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.nodes_deleted, 10) self.env.assertEquals(actual_result.relationships_deleted, 0) for i in range(10): redis_graph.add_node(Node()) redis_graph.flush() # Unwind collected nodes. query = """MATCH (n) WITH collect(n) AS nodes UNWIND nodes AS node DELETE node""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.nodes_deleted, 10) self.env.assertEquals(actual_result.relationships_deleted, 0) def test13_delete_path_elements(self): self.env.flush() redis_con = self.env.getConnection() redis_graph = Graph("delete_test", redis_con) src = Node() dest = Node() edge = Edge(src, "R", dest) redis_graph.add_node(src) redis_graph.add_node(dest) redis_graph.add_edge(edge) redis_graph.flush() # Delete projected # Unwind path nodes. query = """MATCH p = (src)-[e]->(dest) WITH nodes(p)[0] AS node, relationships(p)[0] as edge DELETE node, edge""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.nodes_deleted, 1) self.env.assertEquals(actual_result.relationships_deleted, 1) # Verify that variable-length traversals in each direction produce the correct results after deletion. def test14_post_deletion_traversal_directions(self): self.env.flush() redis_con = self.env.getConnection() redis_graph = Graph("G", redis_con) nodes = {} # Create entities. labels = ["Dest", "Src", "Src2"] for idx, l in enumerate(labels): node = Node(label=l, properties={"val": idx}) redis_graph.add_node(node) nodes[l] = node edge = Edge(nodes["Src"], "R", nodes["Dest"]) redis_graph.add_edge(edge) edge = Edge(nodes["Src2"], "R", nodes["Dest"]) redis_graph.add_edge(edge) redis_graph.commit() # Delete a node. query = """MATCH (n:Src2) DELETE n""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.nodes_deleted, 1) self.env.assertEquals(actual_result.relationships_deleted, 1) query = """MATCH (n1:Src)-[*]->(n2:Dest) RETURN COUNT(*)""" actual_result = redis_graph.query(query) expected_result = [[1]] self.env.assertEquals(actual_result.result_set, expected_result) # Perform the same traversal, this time traveling from destination to source. query = """MATCH (n1:Src)-[*]->(n2:Dest {val: 0}) RETURN COUNT(*)""" actual_result = redis_graph.query(query) expected_result = [[1]] self.env.assertEquals(actual_result.result_set, expected_result) def test15_update_deleted_entities(self): self.env.flush() redis_con = self.env.getConnection() redis_graph = Graph("delete_test", redis_con) src = Node() dest = Node() edge = Edge(src, "R", dest) redis_graph.add_node(src) redis_graph.add_node(dest) redis_graph.add_edge(edge) redis_graph.flush() # Attempt to update entities after deleting them. query = """MATCH (a)-[e]->(b) DELETE a, b SET a.v = 1, e.v = 2, b.v = 3""" actual_result = redis_graph.query(query) self.env.assertEquals(actual_result.nodes_deleted, 2) self.env.assertEquals(actual_result.relationships_deleted, 1) # No properties should be set. # (Note that this behavior is left unspecified by Cypher.) self.env.assertEquals(actual_result.properties_set, 0) # Validate that the graph is empty. query = """MATCH (a) RETURN a""" actual_result = redis_graph.query(query) expected_result = [] self.env.assertEquals(actual_result.result_set, expected_result)
class testProcedures(FlowTestsBase): def __init__(self): self.env = Env() global redis_con global redis_graph redis_con = self.env.getConnection() redis_graph = Graph(GRAPH_ID, redis_con) self.populate_graph() def populate_graph(self): if redis_con.exists(GRAPH_ID): return edge = Edge(node1, 'goWellWith', node5) redis_graph.add_node(node1) redis_graph.add_node(node2) redis_graph.add_node(node3) redis_graph.add_node(node4) redis_graph.add_node(node5) redis_graph.add_edge(edge) redis_graph.commit() # Create full-text index. redis_graph.call_procedure("db.idx.fulltext.createNodeIndex", 'fruit', 'name') # Compares two nodes based on their properties. def _compareNodes(self, a, b): return a.properties == b.properties # Make sure given item is found within resultset. def _inResultSet(self, item, resultset): for i in range(len(resultset)): result = resultset[i][0] if self._compareNodes(item, result): return True return False # Issue query and validates resultset. def queryAndValidate(self, query, expected_results, query_params={}): actual_resultset = redis_graph.query(query, query_params).result_set self.env.assertEquals(len(actual_resultset), len(expected_results)) for i in range(len(actual_resultset)): self.env.assertTrue(self._inResultSet(expected_results[i], actual_resultset)) # Call procedure, omit yield, expecting all procedure outputs to # be included in result-set. def test_no_yield(self): actual_result = redis_graph.call_procedure("db.idx.fulltext.queryNodes", "fruit", "Orange1") assert(len(actual_result.result_set) is 1) header = actual_result.header data = actual_result.result_set[0] assert(header[0][1] == 'node') assert(data[0] is not None) # Call procedure specify different outputs. def test_yield(self): actual_result = redis_graph.call_procedure("db.idx.fulltext.queryNodes", "fruit", "Orange1", y=["node"]) assert(len(actual_result.result_set) is 1) header = actual_result.header data = actual_result.result_set[0] assert(header[0][1] == 'node') assert(data[0] is not None) # Yield an unknown output. # Expect an error when trying to use an unknown procedure output. try: redis_graph.call_procedure("db.idx.fulltext.queryNodes", "fruit", "Orange1", y=["unknown"]) self.env.assertFalse(1) except redis.exceptions.ResponseError: # Expecting an error. pass # Yield the same output multiple times. # Expect an error when trying to use the same output multiple times. try: redis_graph.call_procedure("db.idx.fulltext.queryNodes", "fruit", "Orange1", y=["node", "node"]) self.env.assertFalse(1) except redis.exceptions.ResponseError: # Expecting an error. pass def test_arguments(self): # Omit arguments. # Expect an error when trying to omit arguments. try: redis_graph.call_procedure("db.idx.fulltext.queryNodes") self.env.assertFalse(1) except redis.exceptions.ResponseError: # Expecting an error. pass # Omit arguments, queryNodes expecting 2 argument, provide 1. # Expect an error when trying to omit arguments. try: redis_graph.call_procedure("db.idx.fulltext.queryNodes", "arg1") self.env.assertFalse(1) except redis.exceptions.ResponseError: # Expecting an error. pass # Overload arguments. # Expect an error when trying to send too many arguments. try: redis_graph.call_procedure("db.idx.fulltext.queryNodes", "fruit", "query", "fruit", "query", y=["node"]) self.env.assertFalse(1) except redis.exceptions.ResponseError: # Expecting an error. pass # Test procedure call while mixing a number of addition clauses. def test_mix_clauses(self): query_params = {'prefix': 'Orange*'} # CALL + RETURN. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node RETURN node""" expected_results = [node4, node2, node3, node1] self.queryAndValidate(query, expected_results, query_params=query_params) # The combination of CALL and WHERE currently creates a syntax error in libcypher-parser. # CALL + WHERE + RETURN + ORDER. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node WHERE node.value > 2 RETURN node """ expected_results = [node3, node4] self.queryAndValidate(query, expected_results, query_params=query_params) # CALL + WHERE + RETURN + ORDER + SKIP. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node WHERE node.value > 2 RETURN node ORDER BY node.value SKIP 1""" expected_results = [node4] self.queryAndValidate(query, expected_results, query_params=query_params) # CALL + WHERE + RETURN + LIMIT. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node WHERE node.value > 2 RETURN node LIMIT 2""" expected_results = [node3, node4] self.queryAndValidate(query, expected_results, query_params=query_params) # CALL + WHERE + RETURN + ORDER + SKIP + LIMIT. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node WHERE node.value > 2 RETURN node ORDER BY node.value SKIP 1 LIMIT 1""" expected_results = [node4] self.queryAndValidate(query, expected_results, query_params=query_params) # CALL + RETURN + ORDER. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node RETURN node ORDER BY node.value """ expected_results = [node1, node2, node3, node4] self.queryAndValidate(query, expected_results, query_params=query_params) # CALL + RETURN + ORDER + SKIP. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node RETURN node ORDER BY node.value SKIP 1 """ expected_results = [node2, node3, node4] self.queryAndValidate(query, expected_results, query_params=query_params) # CALL + RETURN + ORDER + LIMIT. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node RETURN node ORDER BY node.value LIMIT 2 """ expected_results = [node1, node2] self.queryAndValidate(query, expected_results, query_params=query_params) # CALL + RETURN + ORDER + SKIP + LIMIT. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node RETURN node ORDER BY node.value SKIP 1 LIMIT 1 """ expected_results = [node2] self.queryAndValidate(query, expected_results, query_params=query_params) # CALL + WHERE + RETURN + ORDER. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node WHERE node.value > 2 RETURN node ORDER BY node.value""" expected_results = [node3, node4] self.queryAndValidate(query, expected_results, query_params=query_params) # CALL + WHERE + RETURN + ORDER + SKIP. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node WHERE node.value > 2 RETURN node ORDER BY node.value SKIP 1""" expected_results = [node4] self.queryAndValidate(query, expected_results, query_params=query_params) # CALL + WHERE + RETURN + ORDER + LIMIT. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node WHERE node.value > 2 RETURN node ORDER BY node.value LIMIT 1""" expected_results = [node3] self.queryAndValidate(query, expected_results, query_params=query_params) # CALL + WHERE + RETURN + ORDER + SKIP + LIMIT. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node WHERE node.value > 2 RETURN node ORDER BY node.value SKIP 1 LIMIT 1""" expected_results = [node4] self.queryAndValidate(query, expected_results, query_params=query_params) # CALL + MATCH + RETURN. query = """CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node MATCH (node)-[]->(z) RETURN z""" expected_results = [node5] self.queryAndValidate(query, expected_results, query_params=query_params) # UNWIND + CALL + RETURN. query = """UNWIND([1,2]) AS x CALL db.idx.fulltext.queryNodes('fruit', $prefix) YIELD node RETURN node""" expected_results = [node4, node2, node3, node1, node4, node2, node3, node1] self.queryAndValidate(query, expected_results, query_params=query_params) def test_procedure_labels(self): actual_resultset = redis_graph.call_procedure("db.labels").result_set expected_results = [["fruit"]] self.env.assertEquals(actual_resultset, expected_results) def test_procedure_relationshipTypes(self): actual_resultset = redis_graph.call_procedure("db.relationshipTypes").result_set expected_results = [["goWellWith"]] self.env.assertEquals(actual_resultset, expected_results) def test_procedure_propertyKeys(self): actual_resultset = redis_graph.call_procedure("db.propertyKeys").result_set expected_results = [["name"], ["value"]] self.env.assertEquals(actual_resultset, expected_results) def test_procedure_fulltext_syntax_error(self): try: query = """CALL db.idx.fulltext.queryNodes('fruit', 'Orange || Apple') YIELD node RETURN node""" redis_graph.query(query) self.env.assertFalse(1) except redis.exceptions.ResponseError: # Expecting an error. pass def test_procedure_lookup(self): try: redis_graph.call_procedure("dB.LaBeLS") except redis.exceptions.ResponseError: # This should not cause an error self.env.assertFalse(1) pass try: # looking for a non existing procedure redis_graph.call_procedure("db.nonExistingProc") self.env.assertFalse(1) except redis.exceptions.ResponseError: # Expecting an error. pass try: redis_graph.call_procedure("db.IDX.FulLText.QueRyNoDes", "fruit", "or") except redis.exceptions.ResponseError: # This should not cause an error self.env.assertFalse(1) pass def test_procedure_get_all_procedures(self): actual_resultset = redis_graph.call_procedure("dbms.procedures").result_set # The following two procedure are a part of the expected results expected_result = [["db.labels", "READ"], ["db.idx.fulltext.createNodeIndex", "WRITE"], ["db.propertyKeys", "READ"], ["dbms.procedures", "READ"], ["db.relationshipTypes", "READ"], ["algo.BFS", "READ"], ["algo.pageRank", "READ"], ["db.idx.fulltext.queryNodes", "READ"], ["db.idx.fulltext.drop", "WRITE"]] for res in expected_result: self.env.assertContains(res, actual_resultset)
class testPendingQueryLimit(): def __init__(self): self.env = Env(decodeResponses=True) self.conn = self.env.getConnection() def test_01_query_limit_config(self): # read max queued queries config result = self.conn.execute_command("GRAPH.CONFIG", "GET", "MAX_QUEUED_QUERIES") max_queued_queries = result[1] self.env.assertEquals(max_queued_queries, 4294967295) # update configuration, set max queued queries self.conn.execute_command("GRAPH.CONFIG", "SET", "MAX_QUEUED_QUERIES", 10) # re-read configuration result = self.conn.execute_command("GRAPH.CONFIG", "GET", "MAX_QUEUED_QUERIES") max_queued_queries = result[1] self.env.assertEquals(max_queued_queries, 10) def stress_server(self): threads = [] connections = [] threadpool_size = self.conn.execute_command("GRAPH.CONFIG", "GET", "THREAD_COUNT")[1] thread_count = threadpool_size * 5 # init connections for i in range(thread_count): connections.append(self.env.getConnection()) # invoke queries for i in range(thread_count): con = connections.pop() t = threading.Thread(target=issue_query, args=(con, SLOW_QUERY)) t.setDaemon(True) threads.append(t) t.start() # wait for threads to return for i in range(thread_count): t = threads[i] t.join() def test_02_overflow_no_limit(self): global error_encountered error_encountered = False # no limit on number of pending queries limit = 4294967295 self.conn.execute_command("GRAPH.CONFIG", "SET", "MAX_QUEUED_QUERIES", limit) self.stress_server() self.env.assertFalse(error_encountered) def test_03_overflow_with_limit(self): global error_encountered error_encountered = False # limit number of pending queries limit = 1 self.conn.execute_command("GRAPH.CONFIG", "SET", "MAX_QUEUED_QUERIES", limit) self.stress_server() self.env.assertTrue(error_encountered)
class testUnion(FlowTestsBase): def __init__(self): self.env = Env() global redis_graph redis_con = self.env.getConnection() redis_graph = Graph(GRAPH_ID, redis_con) self.populate_graph() def populate_graph(self): global redis_graph # Construct a graph with the form: # (v1)-[:E1]->(v2)-[:E2]->(v3) node_props = ['v1', 'v2', 'v3'] nodes = {} for idx, v in enumerate(node_props): node = Node(label="L", properties={"v": v}) nodes[v] = node redis_graph.add_node(node) edge = Edge(nodes['v1'], "E1", nodes['v2'], properties={"v": "v1_v2"}) redis_graph.add_edge(edge) edge = Edge(nodes['v2'], "E2", nodes['v3'], properties={"v": "v2_v3"}) redis_graph.add_edge(edge) redis_graph.flush() def test01_union(self): q = """RETURN 1 as one UNION ALL RETURN 1 as one""" result = redis_graph.query(q) # Expecting 2 identical records. self.env.assertEquals(len(result.result_set), 2) expected_result = [[1], [1]] self.env.assertEquals(result.result_set, expected_result) q = """RETURN 1 as one UNION RETURN 1 as one""" result = redis_graph.query(q) # Expecting a single record, duplicate removed. self.env.assertEquals(len(result.result_set), 1) expected_result = [[1]] self.env.assertEquals(result.result_set, expected_result) q = """MATCH a = () return length(a) AS len UNION ALL MATCH b = () RETURN length(b) AS len""" result = redis_graph.query(q) # 3 records from each sub-query, coresponding to each path matched. self.env.assertEquals(len(result.result_set), 6) def test02_invalid_union(self): try: # projection must be exactly the same. q = """RETURN 1 as one UNION RETURN 1 as two""" redis_graph.query(q) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass # Performing UNION with the same left and right side should # produce the same result as evaluating just one side. def test03_union_deduplication(self): non_union_query = """MATCH (a)-[]->(b) RETURN a.v, b.v ORDER BY a.v, b.v""" non_union_result = redis_graph.query(non_union_query) union_query = """MATCH (a)-[]->(b) RETURN a.v, b.v ORDER BY a.v, b.v UNION MATCH (a)-[]->(b) RETURN a.v, b.v ORDER BY a.v, b.v""" union_result = redis_graph.query(union_query) self.env.assertEquals(union_result.result_set, non_union_result.result_set) # A syntax error should be raised on edge alias reuse in one side of a union. def test04_union_invalid_reused_edge(self): try: query = """MATCH ()-[e]->()-[e]->() RETURN e UNION MATCH ()-[e]->() RETURN e""" redis_graph.query(query) assert (False) except redis.exceptions.ResponseError: # Expecting an error. pass # An edge alias appearing on both sides of a UNION is expected. def test05_union_valid_reused_edge(self): query = """MATCH ()-[e]->() RETURN e.v ORDER BY e.v UNION MATCH ()-[e]->() RETURN e.v ORDER BY e.v UNION MATCH ()-[e]->() RETURN e.v ORDER BY e.v""" result = redis_graph.query(query) expected_result = [["v1_v2"], ["v2_v3"]] self.env.assertEquals(result.result_set, expected_result) # Union should be capable of collating nodes and edges in a single column. def test06_union_nodes_with_edges(self): query = """MATCH ()-[e]->() RETURN e UNION MATCH (e) RETURN e""" union_result = redis_graph.query(query) # All 3 nodes and 2 edges should be returned. self.env.assertEquals(len(union_result.result_set), 5) query = """MATCH ()-[e]->() RETURN e UNION ALL MATCH (e) RETURN e""" union_all_result = redis_graph.query(query) # The same results should be produced regardless of whether ALL is specified. self.env.assertEquals(union_result.result_set, union_all_result.result_set)
class testResultSetFlow(FlowTestsBase): def __init__(self): self.env = Env() global graph global redis_con redis_con = self.env.getConnection() graph = Graph("G", redis_con) self.populate_graph() def populate_graph(self): global graph nodes = {} # Create entities for idx, p in enumerate(people): node = Node(label="person", properties={"name": p, "val": idx}) graph.add_node(node) nodes[p] = node # Fully connected graph for src in nodes: for dest in nodes: if src != dest: edge = Edge(nodes[src], "know", nodes[dest]) graph.add_edge(edge) graph.commit() # Verify that scalar returns function properly def test01_return_scalars(self): query = """MATCH (a) RETURN a.name, a.val ORDER BY a.val""" result = graph.query(query) expected_result = [['Roi', 0], ['Alon', 1], ['Ailon', 2], ['Boaz', 3]] self.env.assertEquals(len(result.result_set), 4) self.env.assertEquals(len(result.header), 2) # 2 columns in result set self.env.assertEquals(result.result_set, expected_result) # Verify that full node returns function properly def test02_return_nodes(self): query = """MATCH (a) RETURN a""" result = graph.query(query) # TODO add more assertions after updated client format is defined self.env.assertEquals(len(result.result_set), 4) self.env.assertEquals(len(result.header), 1) # 1 column in result set # Verify that full edge returns function properly def test03_return_edges(self): query = """MATCH ()-[e]->() RETURN e""" result = graph.query(query) # TODO add more assertions after updated client format is defined self.env.assertEquals(len(result.result_set), 12) # 12 relations (fully connected graph) self.env.assertEquals(len(result.header), 1) # 1 column in result set def test04_mixed_returns(self): query = """MATCH (a)-[e]->() RETURN a.name, a, e ORDER BY a.val""" result = graph.query(query) # TODO add more assertions after updated client format is defined self.env.assertEquals(len(result.result_set), 12) # 12 relations (fully connected graph) self.env.assertEquals(len(result.header), 3) # 3 columns in result set # Verify that the DISTINCT operator works with full entity returns def test05_distinct_full_entities(self): graph2 = Graph("H", redis_con) query = """CREATE (a)-[:e]->(), (a)-[:e]->()""" result = graph2.query(query) self.env.assertEquals(result.nodes_created, 3) self.env.assertEquals(result.relationships_created, 2) query = """MATCH (a)-[]->() RETURN a""" non_distinct = graph2.query(query) query = """MATCH (a)-[]->() RETURN DISTINCT a""" distinct = graph2.query(query) self.env.assertEquals(len(non_distinct.result_set), 2) self.env.assertEquals(len(distinct.result_set), 1) # Verify that RETURN * projections include all user-defined aliases. def test06_return_all(self): query = """MATCH (a)-[e]->(b) RETURN *""" result = graph.query(query) # Validate the header strings of the 3 columns. # NOTE - currently, RETURN * populates values in alphabetical order, but that is subject to later change. self.env.assertEqual(result.header[0][1], 'a') self.env.assertEqual(result.header[1][1], 'b') self.env.assertEqual(result.header[2][1], 'e') # Verify that 3 columns are returned self.env.assertEqual(len(result.result_set[0]), 3) # Tests for aggregation functions default values. Fix for issue 767. def test07_agg_func_default_values(self): # Test for aggregation over non existing node properties. # Max default value is null. query = """MATCH (a) return max(a.missing_field)""" result = graph.query(query) self.env.assertEqual(None, result.result_set[0][0]) # Min default value is null. query = """MATCH (a) return min(a.missing_field)""" result = graph.query(query) self.env.assertEqual(None, result.result_set[0][0]) # Count default value is 0. query = """MATCH (a) return count(a.missing_field)""" result = graph.query(query) self.env.assertEqual(0, result.result_set[0][0]) # Avarage default value is 0. query = """MATCH (a) return avg(a.missing_field)""" result = graph.query(query) self.env.assertEqual(0, result.result_set[0][0]) # Collect default value is an empty array. query = """MATCH (a) return collect(a.missing_field)""" result = graph.query(query) self.env.assertEqual([], result.result_set[0][0]) # StdDev default value is 0. query = """MATCH (a) return stdev(a.missing_field)""" result = graph.query(query) self.env.assertEqual(0, result.result_set[0][0]) # percentileCont default value is null. query = """MATCH (a) return percentileCont(a.missing_field, 0.1)""" result = graph.query(query) self.env.assertEqual(None, result.result_set[0][0]) # percentileDisc default value is null. query = """MATCH (a) return percentileDisc(a.missing_field, 0.1)""" result = graph.query(query) self.env.assertEqual(None, result.result_set[0][0]) # Test returning multiple occurrence of an expression. def test08_return_duplicate_expression(self): query = """MATCH (a) RETURN max(a.val), max(a.val)""" result = graph.query(query) self.env.assertEqual(result.result_set[0][0], result.result_set[0][1]) query = """MATCH (a) return max(a.val) as x, max(a.val) as x""" result = graph.query(query) self.env.assertEqual(result.result_set[0][0], result.result_set[0][1]) query = """MATCH (a) RETURN a.val, a.val LIMIT 1""" result = graph.query(query) self.env.assertEqual(result.result_set[0][0], result.result_set[0][1]) query = """MATCH (a) return a.val as x, a.val as x LIMIT 1""" result = graph.query(query) self.env.assertEqual(result.result_set[0][0], result.result_set[0][1])