class testQueryTimeout(FlowTestsBase): def __init__(self): # skip test if we're running under Valgrind if Env().envRunner.debugger is not None: Env().skip() # queries will be much slower under Valgrind self.env = Env(decodeResponses=True) global redis_con global redis_graph redis_con = self.env.getConnection() redis_graph = Graph("timeout", redis_con) def test01_read_query_timeout(self): query = "UNWIND range(0,100000) AS x WITH x AS x WHERE x = 10000 RETURN x" try: # The query is expected to time out redis_graph.query(query, timeout=1) assert (False) except ResponseError as error: self.env.assertContains("Query timed out", str(error)) try: # The query is expected to succeed redis_graph.query(query, timeout=100) except: assert (False) def test02_configured_timeout(self): # Verify that the module-level timeout is set to the default of 0 response = redis_con.execute_command("GRAPH.CONFIG GET timeout") self.env.assertEquals(response[1], 0) # Set a default timeout of 1 millisecond redis_con.execute_command("GRAPH.CONFIG SET timeout 1") response = redis_con.execute_command("GRAPH.CONFIG GET timeout") self.env.assertEquals(response[1], 1) # Validate that a read query times out query = "UNWIND range(0,100000) AS x WITH x AS x WHERE x = 10000 RETURN x" try: redis_graph.query(query) assert (False) except ResponseError as error: self.env.assertContains("Query timed out", str(error)) def test03_write_query_ignore_timeout(self): # Verify that the timeout argument is ignored by write queries query = "CREATE (a:M) WITH a UNWIND range(1,10000) AS ctr SET a.v = ctr" try: # The query should complete successfully actual_result = redis_graph.query(query, timeout=1) # The query should have taken longer than the timeout value self.env.assertGreater(actual_result.run_time_ms, 1) # The query should have updated properties 10,000 times self.env.assertEquals(actual_result.properties_set, 10000) except ResponseError: assert (False)
class TestAggregate(): def __init__(self): self.env = Env() add_values(self.env) def testGroupBy(self): cmd = [ 'ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'AS', 'count', 'SORTBY', 2, '@count', 'desc', 'LIMIT', '0', '5' ] res = self.env.cmd(*cmd) self.env.assertIsNotNone(res) self.env.assertEqual([ 292L, ['brand', '', 'count', '1518'], ['brand', 'mad catz', 'count', '43'], ['brand', 'generic', 'count', '40'], ['brand', 'steelseries', 'count', '37'], ['brand', 'logitech', 'count', '35'] ], res) def testMinMax(self): cmd = [ 'ft.aggregate', 'games', 'sony', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'REDUCE', 'min', '1', '@price', 'as', 'minPrice', 'SORTBY', '2', '@minPrice', 'DESC' ] res = self.env.cmd(*cmd) self.env.assertIsNotNone(res) row = to_dict(res[1]) self.env.assertEqual(88, int(float(row['minPrice']))) cmd = [ 'ft.aggregate', 'games', 'sony', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'REDUCE', 'max', '1', '@price', 'as', 'maxPrice', 'SORTBY', '2', '@maxPrice', 'DESC' ] res = self.env.cmd(*cmd) row = to_dict(res[1]) self.env.assertEqual(695, int(float(row['maxPrice']))) def testAvg(self): cmd = [ 'ft.aggregate', 'games', 'sony', 'GROUPBY', '1', '@brand', 'REDUCE', 'avg', '1', '@price', 'AS', 'avg_price', 'REDUCE', 'count', '0', 'SORTBY', '2', '@avg_price', 'DESC' ] res = self.env.cmd(*cmd) self.env.assertIsNotNone(res) self.env.assertEqual(26, res[0]) # Ensure the formatting actually exists first_row = to_dict(res[1]) self.env.assertEqual(109, int(float(first_row['avg_price']))) for row in res[1:]: row = to_dict(row) self.env.assertIn('avg_price', row) # Test aliasing cmd = [ 'FT.AGGREGATE', 'games', 'sony', 'GROUPBY', '1', '@brand', 'REDUCE', 'avg', '1', '@price', 'AS', 'avgPrice' ] res = self.env.cmd(*cmd) first_row = to_dict(res[1]) self.env.assertEqual(17, int(float(first_row['avgPrice']))) def testCountDistinct(self): cmd = [ 'FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'COUNT_DISTINCT', '1', '@title', 'AS', 'count_distinct(title)', 'REDUCE', 'COUNT', '0' ] res = self.env.cmd(*cmd)[1:] # print res row = to_dict(res[0]) self.env.assertEqual(1484, int(row['count_distinct(title)'])) cmd = [ 'FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'COUNT_DISTINCTISH', '1', '@title', 'AS', 'count_distinctish(title)', 'REDUCE', 'COUNT', '0' ] res = self.env.cmd(*cmd)[1:] # print res row = to_dict(res[0]) self.env.assertEqual(1461, int(row['count_distinctish(title)'])) def testQuantile(self): cmd = [ 'FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'QUANTILE', '2', '@price', '0.50', 'AS', 'q50', 'REDUCE', 'QUANTILE', '2', '@price', '0.90', 'AS', 'q90', 'REDUCE', 'QUANTILE', '2', '@price', '0.95', 'AS', 'q95', 'REDUCE', 'AVG', '1', '@price', 'REDUCE', 'COUNT', '0', 'AS', 'rowcount', 'SORTBY', '2', '@rowcount', 'DESC', 'MAX', '1' ] res = self.env.cmd(*cmd) row = to_dict(res[1]) # TODO: Better samples self.env.assertAlmostEqual(14.99, float(row['q50']), delta=3) self.env.assertAlmostEqual(70, float(row['q90']), delta=50) # This tests the 95th percentile, which is error prone because # so few samples actually exist. I'm disabling it for now so that # there is no breakage in CI # self.env.assertAlmostEqual(110, (float(row['q95'])), delta=50) def testStdDev(self): cmd = [ 'FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'STDDEV', '1', '@price', 'AS', 'stddev(price)', 'REDUCE', 'AVG', '1', '@price', 'AS', 'avgPrice', 'REDUCE', 'QUANTILE', '2', '@price', '0.50', 'AS', 'q50Price', 'REDUCE', 'COUNT', '0', 'AS', 'rowcount', 'SORTBY', '2', '@rowcount', 'DESC', 'LIMIT', '0', '10' ] res = self.env.cmd(*cmd) row = to_dict(res[1]) self.env.assertTrue(10 <= int(float(row['q50Price'])) <= 20) self.env.assertAlmostEqual(53, int(float(row['stddev(price)'])), delta=50) self.env.assertEqual(29, int(float(row['avgPrice']))) def testParseTime(self): cmd = [ 'FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'COUNT', '0', 'AS', 'count', 'APPLY', 'timefmt(1517417144)', 'AS', 'dt', 'APPLY', 'parse_time("%FT%TZ", @dt)', 'as', 'parsed_dt', 'LIMIT', '0', '1' ] res = self.env.cmd(*cmd) self.env.assertEqual([ 'brand', '', 'count', '1518', 'dt', '2018-01-31T16:45:44Z', 'parsed_dt', '1517417144' ], res[1]) def testRandomSample(self): cmd = [ 'FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'COUNT', '0', 'AS', 'num', 'REDUCE', 'RANDOM_SAMPLE', '2', '@price', '10', 'SORTBY', '2', '@num', 'DESC', 'MAX', '10' ] for row in self.env.cmd(*cmd)[1:]: self.env.assertIsInstance(row[5], list) self.env.assertGreater(len(row[5]), 0) self.env.assertGreaterEqual(row[3], len(row[5])) self.env.assertLessEqual(len(row[5]), 10) def testTimeFunctions(self): cmd = [ 'FT.AGGREGATE', 'games', '*', 'APPLY', '1517417144', 'AS', 'dt', 'APPLY', 'timefmt(@dt)', 'AS', 'timefmt', 'APPLY', 'day(@dt)', 'AS', 'day', 'APPLY', 'hour(@dt)', 'AS', 'hour', 'APPLY', 'minute(@dt)', 'AS', 'minute', 'APPLY', 'month(@dt)', 'AS', 'month', 'APPLY', 'dayofweek(@dt)', 'AS', 'dayofweek', 'APPLY', 'dayofmonth(@dt)', 'AS', 'dayofmonth', 'APPLY', 'dayofyear(@dt)', 'AS', 'dayofyear', 'APPLY', 'year(@dt)', 'AS', 'year', 'LIMIT', '0', '1' ] res = self.env.cmd(*cmd) self.env.assertListEqual([ 1L, [ 'dt', '1517417144', 'timefmt', '2018-01-31T16:45:44Z', 'day', '1517356800', 'hour', '1517414400', 'minute', '1517417100', 'month', '1514764800', 'dayofweek', '3', 'dayofmonth', '31', 'dayofyear', '30', 'year', '2018' ] ], res) def testStringFormat(self): cmd = [ 'FT.AGGREGATE', 'games', '@brand:sony', 'GROUPBY', '2', '@title', '@brand', 'REDUCE', 'COUNT', '0', 'REDUCE', 'MAX', '1', '@price', 'AS', 'price', 'APPLY', 'format("%s|%s|%s|%s", @title, @brand, "Mark", @price)', 'as', 'titleBrand', 'LIMIT', '0', '10' ] res = self.env.cmd(*cmd) for row in res[1:]: row = to_dict(row) expected = '%s|%s|%s|%g' % (row['title'], row['brand'], 'Mark', float(row['price'])) self.env.assertEqual(expected, row['titleBrand']) def testSum(self): cmd = [ 'ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'AS', 'count', 'REDUCE', 'sum', 1, '@price', 'AS', 'sum(price)', 'SORTBY', 2, '@sum(price)', 'desc', 'LIMIT', '0', '5' ] res = self.env.cmd(*cmd) self.env.assertEqual([ 292L, ['brand', '', 'count', '1518', 'sum(price)', '44780.69'], ['brand', 'mad catz', 'count', '43', 'sum(price)', '3973.48'], ['brand', 'razer', 'count', '26', 'sum(price)', '2558.58'], ['brand', 'logitech', 'count', '35', 'sum(price)', '2329.21'], ['brand', 'steelseries', 'count', '37', 'sum(price)', '1851.12'] ], res) def testFilter(self): cmd = [ 'ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'AS', 'count', 'FILTER', '@count > 5' ] res = self.env.cmd(*cmd) for row in res[1:]: row = to_dict(row) self.env.assertGreater(int(row['count']), 5) cmd = [ 'ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'AS', 'count', 'FILTER', '@count < 5', 'FILTER', '@count > 2 && @brand != ""' ] res = self.env.cmd(*cmd) for row in res[1:]: row = to_dict(row) self.env.assertLess(int(row['count']), 5) self.env.assertGreater(int(row['count']), 2) def testToList(self): cmd = [ 'ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count_distinct', '1', '@price', 'as', 'count', 'REDUCE', 'tolist', 1, '@price', 'as', 'prices', 'SORTBY', 2, '@count', 'desc', 'LIMIT', '0', '5' ] res = self.env.cmd(*cmd) for row in res[1:]: row = to_dict(row) self.env.assertEqual(int(row['count']), len(row['prices'])) def testSortBy(self): res = self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'sum', 1, '@price', 'as', 'price', 'SORTBY', 2, '@price', 'desc', 'LIMIT', '0', '2') self.env.assertListEqual([ 292L, ['brand', '', 'price', '44780.69'], ['brand', 'mad catz', 'price', '3973.48'] ], res) res = self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'sum', 1, '@price', 'as', 'price', 'SORTBY', 2, '@price', 'asc', 'LIMIT', '0', '2') self.env.assertListEqual([ 292L, ['brand', 'myiico', 'price', '0.23'], ['brand', 'crystal dynamics', 'price', '0.25'] ], res) # Test MAX with limit higher than it res = self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'sum', 1, '@price', 'as', 'price', 'SORTBY', 2, '@price', 'asc', 'MAX', 2) self.env.assertListEqual([ 292L, ['brand', 'myiico', 'price', '0.23'], ['brand', 'crystal dynamics', 'price', '0.25'] ], res) # Test Sorting by multiple properties res = self.env.cmd( 'ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'sum', 1, '@price', 'as', 'price', 'APPLY', '(@price % 10)', 'AS', 'price', 'SORTBY', 4, '@price', 'asc', '@brand', 'desc', 'MAX', 10, ) self.env.assertListEqual([ 292L, ['brand', 'zps', 'price', '0'], ['brand', 'zalman', 'price', '0'], [ 'brand', 'yoozoo', 'price', '0' ], ['brand', 'white label', 'price', '0'], ['brand', 'stinky', 'price', '0'], ['brand', 'polaroid', 'price', '0'], ['brand', 'plantronics', 'price', '0'], ['brand', 'ozone', 'price', '0'], ['brand', 'oooo', 'price', '0'], ['brand', 'neon', 'price', '0'] ], res) def testExpressions(self): pass def testNoGroup(self): res = self.env.cmd( 'ft.aggregate', 'games', '*', 'LOAD', '2', '@brand', '@price', 'APPLY', 'floor(sqrt(@price)) % 10', 'AS', 'price', 'SORTBY', 4, '@price', 'desc', '@brand', 'desc', 'MAX', 5, ) exp = [ 2265L, ['brand', 'xbox', 'price', '9'], ['brand', 'turtle beach', 'price', '9'], ['brand', 'trust', 'price', '9'], ['brand', 'steelseries', 'price', '9'], ['brand', 'speedlink', 'price', '9'] ] # exp = [2265L, ['brand', 'Xbox', 'price', '9'], ['brand', 'Turtle Beach', 'price', '9'], [ # 'brand', 'Trust', 'price', '9'], ['brand', 'SteelSeries', 'price', '9'], ['brand', 'Speedlink', 'price', '9']] self.env.assertListEqual(exp[1], res[1]) def testLoad(self): res = self.env.cmd('ft.aggregate', 'games', '*', 'LOAD', '3', '@brand', '@price', '@nonexist', 'SORTBY', 2, '@price', 'DESC', 'MAX', 2) exp = [ 3L, ['brand', '', 'price', '759.12'], ['brand', 'Sony', 'price', '695.8'] ] self.env.assertEqual(exp[1], res[1]) def testSplit(self): res = self.env.cmd( 'ft.aggregate', 'games', '*', 'APPLY', 'split("hello world, foo,,,bar,", ",", " ")', 'AS', 'strs', 'APPLY', 'split("hello world, foo,,,bar,", " ", ",")', 'AS', 'strs2', 'APPLY', 'split("hello world, foo,,,bar,", "", "")', 'AS', 'strs3', 'APPLY', 'split("hello world, foo,,,bar,")', 'AS', 'strs4', 'APPLY', 'split("hello world, foo,,,bar,",",")', 'AS', 'strs5', 'APPLY', 'split("")', 'AS', 'empty', 'LIMIT', '0', '1') # print "Got {} results".format(len(res)) # return # pprint.pprint(res) self.env.assertListEqual([ 1L, [ 'strs', ['hello world', 'foo', 'bar'], 'strs2', ['hello', 'world', 'foo,,,bar'], 'strs3', ['hello world, foo,,,bar,'], 'strs4', ['hello world', 'foo', 'bar'], 'strs5', ['hello world', 'foo', 'bar'], 'empty', [] ] ], res) def testFirstValue(self): res = self.env.cmd( 'ft.aggregate', 'games', '@brand:(sony|matias|beyerdynamic|(mad catz))', 'GROUPBY', 1, '@brand', 'REDUCE', 'FIRST_VALUE', 4, '@title', 'BY', '@price', 'DESC', 'AS', 'top_item', 'REDUCE', 'FIRST_VALUE', 4, '@price', 'BY', '@price', 'DESC', 'AS', 'top_price', 'REDUCE', 'FIRST_VALUE', 4, '@title', 'BY', '@price', 'ASC', 'AS', 'bottom_item', 'REDUCE', 'FIRST_VALUE', 4, '@price', 'BY', '@price', 'ASC', 'AS', 'bottom_price', 'SORTBY', 2, '@top_price', 'DESC', 'MAX', 5) expected = [ 4L, [ 'brand', 'sony', 'top_item', 'sony psp slim & lite 2000 console', 'top_price', '695.8', 'bottom_item', 'sony dlchd20p high speed hdmi cable for playstation 3', 'bottom_price', '5.88' ], [ 'brand', 'matias', 'top_item', 'matias halfkeyboard usb', 'top_price', '559.99', 'bottom_item', 'matias halfkeyboard usb', 'bottom_price', '559.99' ], [ 'brand', 'beyerdynamic', 'top_item', 'beyerdynamic mmx300 pc gaming premium digital headset with microphone', 'top_price', '359.74', 'bottom_item', 'beyerdynamic headzone pc gaming digital surround sound system with mmx300 digital headset with microphone', 'bottom_price', '0' ], [ 'brand', 'mad catz', 'top_item', 'mad catz s.t.r.i.k.e.7 gaming keyboard', 'top_price', '295.95', 'bottom_item', 'madcatz mov4545 xbox replacement breakaway cable', 'bottom_price', '3.49' ] ] self.env.assertListEqual(expected, res) def testLoadAfterGroupBy(self): with self.env.assertResponseError(): self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', 1, '@brand', 'LOAD', 1, '@brand')
class TestAggregate(): def __init__(self): self.env = Env() add_values(self.env) def testGroupBy(self): cmd = [ 'ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'AS', 'count', 'SORTBY', 2, '@count', 'desc', 'LIMIT', '0', '5' ] res = self.env.cmd(*cmd) self.env.assertIsNotNone(res) self.env.assertEqual([ 292L, ['brand', '', 'count', '1518'], ['brand', 'mad catz', 'count', '43'], ['brand', 'generic', 'count', '40'], ['brand', 'steelseries', 'count', '37'], ['brand', 'logitech', 'count', '35'] ], res) def testMinMax(self): cmd = [ 'ft.aggregate', 'games', 'sony', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'REDUCE', 'min', '1', '@price', 'as', 'minPrice', 'SORTBY', '2', '@minPrice', 'DESC' ] res = self.env.cmd(*cmd) self.env.assertIsNotNone(res) row = to_dict(res[1]) self.env.assertEqual(88, int(float(row['minPrice']))) cmd = [ 'ft.aggregate', 'games', 'sony', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'REDUCE', 'max', '1', '@price', 'as', 'maxPrice', 'SORTBY', '2', '@maxPrice', 'DESC' ] res = self.env.cmd(*cmd) row = to_dict(res[1]) self.env.assertEqual(695, int(float(row['maxPrice']))) def testAvg(self): cmd = [ 'ft.aggregate', 'games', 'sony', 'GROUPBY', '1', '@brand', 'REDUCE', 'avg', '1', '@price', 'AS', 'avg_price', 'REDUCE', 'count', '0', 'SORTBY', '2', '@avg_price', 'DESC' ] res = self.env.cmd(*cmd) self.env.assertIsNotNone(res) self.env.assertEqual(26, res[0]) # Ensure the formatting actually exists first_row = to_dict(res[1]) self.env.assertEqual(109, int(float(first_row['avg_price']))) for row in res[1:]: row = to_dict(row) self.env.assertIn('avg_price', row) # Test aliasing cmd = [ 'FT.AGGREGATE', 'games', 'sony', 'GROUPBY', '1', '@brand', 'REDUCE', 'avg', '1', '@price', 'AS', 'avgPrice' ] res = self.env.cmd(*cmd) first_row = to_dict(res[1]) self.env.assertEqual(17, int(float(first_row['avgPrice']))) def testCountDistinct(self): cmd = [ 'FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'COUNT_DISTINCT', '1', '@title', 'AS', 'count_distinct(title)', 'REDUCE', 'COUNT', '0' ] res = self.env.cmd(*cmd)[1:] # print res row = to_dict(res[0]) self.env.assertEqual(1484, int(row['count_distinct(title)'])) cmd = [ 'FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'COUNT_DISTINCTISH', '1', '@title', 'AS', 'count_distinctish(title)', 'REDUCE', 'COUNT', '0' ] res = self.env.cmd(*cmd)[1:] # print res row = to_dict(res[0]) self.env.assertEqual(1461, int(row['count_distinctish(title)'])) def testQuantile(self): cmd = [ 'FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'QUANTILE', '2', '@price', '0.50', 'AS', 'q50', 'REDUCE', 'QUANTILE', '2', '@price', '0.90', 'AS', 'q90', 'REDUCE', 'QUANTILE', '2', '@price', '0.95', 'AS', 'q95', 'REDUCE', 'AVG', '1', '@price', 'REDUCE', 'COUNT', '0', 'AS', 'rowcount', 'SORTBY', '2', '@rowcount', 'DESC', 'MAX', '1' ] res = self.env.cmd(*cmd) row = to_dict(res[1]) # TODO: Better samples self.env.assertAlmostEqual(14.99, float(row['q50']), delta=3) self.env.assertAlmostEqual(70, float(row['q90']), delta=50) self.env.assertAlmostEqual(110, (float(row['q95'])), delta=50) def testStdDev(self): cmd = [ 'FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'STDDEV', '1', '@price', 'AS', 'stddev(price)', 'REDUCE', 'AVG', '1', '@price', 'AS', 'avgPrice', 'REDUCE', 'QUANTILE', '2', '@price', '0.50', 'AS', 'q50Price', 'REDUCE', 'COUNT', '0', 'AS', 'rowcount', 'SORTBY', '2', '@rowcount', 'DESC', 'LIMIT', '0', '10' ] res = self.env.cmd(*cmd) row = to_dict(res[1]) self.env.assertTrue(10 <= int(float(row['q50Price'])) <= 20) self.env.assertAlmostEqual(53, int(float(row['stddev(price)'])), delta=50) self.env.assertEqual(29, int(float(row['avgPrice']))) def testParseTime(self): cmd = [ 'FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'COUNT', '0', 'AS', 'count', 'APPLY', 'timefmt(1517417144)', 'AS', 'dt', 'APPLY', 'parse_time("%FT%TZ", @dt)', 'as', 'parsed_dt', 'LIMIT', '0', '1' ] res = self.env.cmd(*cmd) self.env.assertEqual([ 'brand', '', 'count', '1518', 'dt', '2018-01-31T16:45:44Z', 'parsed_dt', '1517417144' ], res[1]) def testRandomSample(self): cmd = [ 'FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'COUNT', '0', 'AS', 'num', 'REDUCE', 'RANDOM_SAMPLE', '2', '@price', '10', 'SORTBY', '2', '@num', 'DESC', 'MAX', '10' ] for row in self.env.cmd(*cmd)[1:]: self.env.assertIsInstance(row[5], list) self.env.assertGreater(len(row[5]), 0) self.env.assertGreaterEqual(row[3], len(row[5])) self.env.assertLessEqual(len(row[5]), 10) def testTimeFunctions(self): cmd = [ 'FT.AGGREGATE', 'games', '*', 'APPLY', '1517417144', 'AS', 'dt', 'APPLY', 'timefmt(@dt)', 'AS', 'timefmt', 'APPLY', 'day(@dt)', 'AS', 'day', 'APPLY', 'hour(@dt)', 'AS', 'hour', 'APPLY', 'minute(@dt)', 'AS', 'minute', 'APPLY', 'month(@dt)', 'AS', 'month', 'APPLY', 'dayofweek(@dt)', 'AS', 'dayofweek', 'APPLY', 'dayofmonth(@dt)', 'AS', 'dayofmonth', 'APPLY', 'dayofyear(@dt)', 'AS', 'dayofyear', 'APPLY', 'year(@dt)', 'AS', 'year', 'LIMIT', '0', '1' ] res = self.env.cmd(*cmd) self.env.assertListEqual([ 1L, [ 'dt', '1517417144', 'timefmt', '2018-01-31T16:45:44Z', 'day', '1517356800', 'hour', '1517414400', 'minute', '1517417100', 'month', '1514764800', 'dayofweek', '3', 'dayofmonth', '31', 'dayofyear', '30', 'year', '2018' ] ], res) def testStringFormat(self): cmd = [ 'FT.AGGREGATE', 'games', '@brand:sony', 'GROUPBY', '2', '@title', '@brand', 'REDUCE', 'COUNT', '0', 'REDUCE', 'MAX', '1', '@price', 'AS', 'price', 'APPLY', 'format("%s|%s|%s|%s", @title, @brand, "Mark", @price)', 'as', 'titleBrand', 'LIMIT', '0', '10' ] res = self.env.cmd(*cmd) for row in res[1:]: row = to_dict(row) expected = '%s|%s|%s|%g' % (row['title'], row['brand'], 'Mark', float(row['price'])) self.env.assertEqual(expected, row['titleBrand']) def testSum(self): cmd = [ 'ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'AS', 'count', 'REDUCE', 'sum', 1, '@price', 'AS', 'sum(price)', 'SORTBY', 2, '@sum(price)', 'desc', 'LIMIT', '0', '5' ] res = self.env.cmd(*cmd) self.env.assertEqual([ 292L, ['brand', '', 'count', '1518', 'sum(price)', '44780.69'], ['brand', 'mad catz', 'count', '43', 'sum(price)', '3973.48'], ['brand', 'razer', 'count', '26', 'sum(price)', '2558.58'], ['brand', 'logitech', 'count', '35', 'sum(price)', '2329.21'], ['brand', 'steelseries', 'count', '37', 'sum(price)', '1851.12'] ], res) def testFilter(self): cmd = [ 'ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'AS', 'count', 'FILTER', '@count > 5' ] res = self.env.cmd(*cmd) for row in res[1:]: row = to_dict(row) self.env.assertGreater(int(row['count']), 5) cmd = [ 'ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'AS', 'count', 'FILTER', '@count < 5', 'FILTER', '@count > 2 && @brand != ""' ] res = self.env.cmd(*cmd) for row in res[1:]: row = to_dict(row) self.env.assertLess(int(row['count']), 5) self.env.assertGreater(int(row['count']), 2) def testToList(self): cmd = [ 'ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count_distinct', '1', '@price', 'as', 'count', 'REDUCE', 'tolist', 1, '@price', 'as', 'prices', 'SORTBY', 2, '@count', 'desc', 'LIMIT', '0', '5' ] res = self.env.cmd(*cmd) for row in res[1:]: row = to_dict(row) self.env.assertEqual(int(row['count']), len(row['prices'])) def testSortBy(self): res = self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'sum', 1, '@price', 'as', 'price', 'SORTBY', 2, '@price', 'desc', 'LIMIT', '0', '2') self.env.assertListEqual([ 292L, ['brand', '', 'price', '44780.69'], ['brand', 'mad catz', 'price', '3973.48'] ], res) res = self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'sum', 1, '@price', 'as', 'price', 'SORTBY', 2, '@price', 'asc', 'LIMIT', '0', '2') self.env.assertListEqual([ 292L, ['brand', 'myiico', 'price', '0.23'], ['brand', 'crystal dynamics', 'price', '0.25'] ], res) # Test MAX with limit higher than it res = self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'sum', 1, '@price', 'as', 'price', 'SORTBY', 2, '@price', 'asc', 'MAX', 2) self.env.assertListEqual([ 292L, ['brand', 'myiico', 'price', '0.23'], ['brand', 'crystal dynamics', 'price', '0.25'] ], res) # Test Sorting by multiple properties res = self.env.cmd( 'ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'sum', 1, '@price', 'as', 'price', 'APPLY', '(@price % 10)', 'AS', 'price', 'SORTBY', 4, '@price', 'asc', '@brand', 'desc', 'MAX', 10, ) self.env.assertListEqual([ 292L, ['brand', 'zps', 'price', '0'], ['brand', 'zalman', 'price', '0'], [ 'brand', 'yoozoo', 'price', '0' ], ['brand', 'white label', 'price', '0'], ['brand', 'stinky', 'price', '0'], ['brand', 'polaroid', 'price', '0'], ['brand', 'plantronics', 'price', '0'], ['brand', 'ozone', 'price', '0'], ['brand', 'oooo', 'price', '0'], ['brand', 'neon', 'price', '0'] ], res) def testExpressions(self): pass def testNoGroup(self): res = self.env.cmd( 'ft.aggregate', 'games', '*', 'LOAD', '2', '@brand', '@price', 'APPLY', 'floor(sqrt(@price)) % 10', 'AS', 'price', 'SORTBY', 4, '@price', 'desc', '@brand', 'desc', 'MAX', 5, ) exp = [ 2265L, ['brand', 'Xbox', 'price', '9'], ['brand', 'turtle beach', 'price', '9'], ['brand', 'trust', 'price', '9'], ['brand', 'steelseries', 'price', '9'], ['brand', 'speedlink', 'price', '9'] ] # exp = [2265L, ['brand', 'Xbox', 'price', '9'], ['brand', 'Turtle Beach', 'price', '9'], [ # 'brand', 'Trust', 'price', '9'], ['brand', 'SteelSeries', 'price', '9'], ['brand', 'Speedlink', 'price', '9']] self.env.assertListEqual(exp[1], res[1]) def testLoad(self): res = self.env.cmd('ft.aggregate', 'games', '*', 'LOAD', '3', '@brand', '@price', '@nonexist', 'SORTBY', 2, '@price', 'DESC', 'MAX', 2) exp = [ 3L, ['brand', '', 'price', '759.12'], ['brand', 'Sony', 'price', '695.8'] ] self.env.assertEqual(exp[1], res[1]) self.env.assertEqual(exp[2], res[2]) def testLoadWithDocId(self): res = self.env.cmd('ft.aggregate', 'games', '*', 'LOAD', '3', '@brand', '@price', '@__key', 'SORTBY', 2, '@price', 'DESC', 'MAX', 4) exp = [ 3L, ['brand', '', 'price', '759.12', '__key', 'B00006JJIC'], ['brand', 'Sony', 'price', '695.8', '__key', 'B000F6W1AG'] ] self.env.assertEqual(exp[1], res[1]) self.env.assertEqual(exp[2], res[2]) res = self.env.cmd('ft.aggregate', 'games', '*', 'LOAD', '3', '@brand', '@price', '@__key', 'FILTER', '@__key == "B000F6W1AG"') self.env.assertEqual( res[1], ['brand', 'Sony', 'price', '695.8', '__key', 'B000F6W1AG']) def testLoadImplicit(self): # same as previous res = self.env.cmd('ft.aggregate', 'games', '*', 'LOAD', '1', '@brand', 'SORTBY', 2, '@price', 'DESC') exp = [ 3L, ['brand', '', 'price', '759.12'], ['brand', 'Sony', 'price', '695.8'] ] self.env.assertEqual(exp[1], res[1]) def testSplit(self): res = self.env.cmd( 'ft.aggregate', 'games', '*', 'APPLY', 'split("hello world, foo,,,bar,", ",", " ")', 'AS', 'strs', 'APPLY', 'split("hello world, foo,,,bar,", " ", ",")', 'AS', 'strs2', 'APPLY', 'split("hello world, foo,,,bar,", "", "")', 'AS', 'strs3', 'APPLY', 'split("hello world, foo,,,bar,")', 'AS', 'strs4', 'APPLY', 'split("hello world, foo,,,bar,",",")', 'AS', 'strs5', 'APPLY', 'split("")', 'AS', 'empty', 'LIMIT', '0', '1') # print "Got {} results".format(len(res)) # return # pprint.pprint(res) self.env.assertListEqual([ 1L, [ 'strs', ['hello world', 'foo', 'bar'], 'strs2', ['hello', 'world', 'foo,,,bar'], 'strs3', ['hello world, foo,,,bar,'], 'strs4', ['hello world', 'foo', 'bar'], 'strs5', ['hello world', 'foo', 'bar'], 'empty', [] ] ], res) def testFirstValue(self): res = self.env.cmd( 'ft.aggregate', 'games', '@brand:(sony|matias|beyerdynamic|(mad catz))', 'GROUPBY', 1, '@brand', 'REDUCE', 'FIRST_VALUE', 4, '@title', 'BY', '@price', 'DESC', 'AS', 'top_item', 'REDUCE', 'FIRST_VALUE', 4, '@price', 'BY', '@price', 'DESC', 'AS', 'top_price', 'REDUCE', 'FIRST_VALUE', 4, '@title', 'BY', '@price', 'ASC', 'AS', 'bottom_item', 'REDUCE', 'FIRST_VALUE', 4, '@price', 'BY', '@price', 'ASC', 'AS', 'bottom_price', 'SORTBY', 2, '@top_price', 'DESC', 'MAX', 5) expected = [ 4L, [ 'brand', 'sony', 'top_item', 'sony psp slim & lite 2000 console', 'top_price', '695.8', 'bottom_item', 'sony dlchd20p high speed hdmi cable for playstation 3', 'bottom_price', '5.88' ], [ 'brand', 'matias', 'top_item', 'matias halfkeyboard usb', 'top_price', '559.99', 'bottom_item', 'matias halfkeyboard usb', 'bottom_price', '559.99' ], [ 'brand', 'beyerdynamic', 'top_item', 'beyerdynamic mmx300 pc gaming premium digital headset with microphone', 'top_price', '359.74', 'bottom_item', 'beyerdynamic headzone pc gaming digital surround sound system with mmx300 digital headset with microphone', 'bottom_price', '0' ], [ 'brand', 'mad catz', 'top_item', 'mad catz s.t.r.i.k.e.7 gaming keyboard', 'top_price', '295.95', 'bottom_item', 'madcatz mov4545 xbox replacement breakaway cable', 'bottom_price', '3.49' ] ] # hack :( def mklower(result): for arr in result[1:]: for x in range(len(arr)): arr[x] = arr[x].lower() mklower(expected) mklower(res) self.env.assertListEqual(expected, res) def testLoadAfterGroupBy(self): with self.env.assertResponseError(): self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', 1, '@brand', 'LOAD', 1, '@brand') def testReducerGeneratedAliasing(self): rv = self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', 1, '@brand', 'REDUCE', 'MIN', 1, '@price', 'LIMIT', 0, 1) self.env.assertEqual( [292L, ['brand', '', '__generated_aliasminprice', '0']], rv) rv = self.env.cmd('ft.aggregate', 'games', '@brand:(sony|matias|beyerdynamic|(mad catz))', 'GROUPBY', 1, '@brand', 'REDUCE', 'FIRST_VALUE', 4, '@title', 'BY', '@price', 'DESC', 'SORTBY', 2, '@brand', 'ASC') self.env.assertEqual('__generated_aliasfirst_valuetitle,by,price,desc', rv[1][2]) def testIssue1125(self): self.env.skipOnCluster() # SEARCH should fail self.env.expect('ft.search', 'games', '*', 'limit', 0, 2000000).error() \ .contains('LIMIT exceeds maximum of 1000000') # SEARCH should succeed self.env.expect('ft.config', 'set', 'MAXSEARCHRESULTS', -1).ok() rv = self.env.cmd('ft.search', 'games', '*', 'LIMIT', 0, 12345678) self.env.assertEqual(4531, len(rv)) # AGGREGATE should succeed rv = self.env.cmd('ft.aggregate', 'games', '*', 'LIMIT', 0, 12345678) self.env.assertEqual(2266, len(rv)) # AGGREGATE should fail self.env.expect('ft.config', 'set', 'MAXAGGREGATERESULTS', 1000000).ok() self.env.expect('ft.aggregate', 'games', '*', 'limit', 0, 2000000).error() \ .contains('LIMIT exceeds maximum of 1000000') # force global limit on aggregate num = 10 self.env.expect('ft.config', 'set', 'MAXAGGREGATERESULTS', num).ok() rv = self.env.cmd('ft.aggregate', 'games', '*') self.env.assertEqual(num + 1, len(rv)) def testMultiSortBy(self): self.env.expect('ft.aggregate', 'games', '*', 'LOAD', '2', '@brand', '@price', 'SORTBY', 2, '@brand', 'DESC', 'SORTBY', 2, '@price', 'DESC').error()\ .contains('Multiple SORTBY steps are not allowed. Sort multiple fields in a single step')
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()
class testGraphBulkInsertFlow(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) global redis_graph global redis_con redis_con = self.env.getConnection() 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/social/resources/bulk_formatted/' 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/social/resources/bulk_formatted/' 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') # Test passing invalid arguments directly to the GRAPH.BULK endpoint try: redis_con.execute_command("GRAPH.BULK", "a", "a", "a") self.env.assertTrue(False) except redis.exceptions.ResponseError as e: self.env.assertIn("Invalid graph operation on empty key", str(e)) # 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) # Verify that the bulk loader does not block the server def test09_large_bulk_insert(self): graphname = "tmpgraph5" prop_str = "Property value to be repeated 1 million generating a multi-megabyte CSV" # Write temporary files filename = '/tmp/nodes.tmp' with open(filename, mode='w') as csv_file: out = csv.writer(csv_file) out.writerow(["long_property_string"]) for i in range(100_000): out.writerow([prop_str]) # Instantiate a thread to run the bulk loader thread = threading.Thread(target=run_bulk_loader, args=(graphname, filename)) thread.start() # Ping server while bulk-loader is running ping_count = 0 while thread.is_alive(): t0 = time.time() redis_con.ping() t1 = time.time() - t0 # Verify that pinging the server takes less than 1 second during bulk insertion self.env.assertLess(t1, 2) ping_count += 1 thread.join() # Verify that at least one ping was issued self.env.assertGreater(ping_count, 1)
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)
class testQueryTimeout(FlowTestsBase): def __init__(self): self.env = Env(decodeResponses=True) # skip test if we're running under Valgrind if self.env.envRunner.debugger is not None or os.getenv('COV') == '1': self.env.skip() # queries will be much slower under Valgrind global redis_con global redis_graph redis_con = self.env.getConnection() redis_graph = Graph("timeout", redis_con) def test01_read_query_timeout(self): query = "UNWIND range(0,100000) AS x WITH x AS x WHERE x = 10000 RETURN x" try: # The query is expected to timeout redis_graph.query(query, timeout=1) assert (False) except ResponseError as error: self.env.assertContains("Query timed out", str(error)) try: # The query is expected to succeed redis_graph.query(query, timeout=200) except: assert (False) def test02_configured_timeout(self): # Verify that the module-level timeout is set to the default of 0 response = redis_con.execute_command("GRAPH.CONFIG GET timeout") self.env.assertEquals(response[1], 0) # Set a default timeout of 1 millisecond redis_con.execute_command("GRAPH.CONFIG SET timeout 1") response = redis_con.execute_command("GRAPH.CONFIG GET timeout") self.env.assertEquals(response[1], 1) # Validate that a read query times out query = "UNWIND range(0,100000) AS x WITH x AS x WHERE x = 10000 RETURN x" try: redis_graph.query(query) assert (False) except ResponseError as error: self.env.assertContains("Query timed out", str(error)) def test03_write_query_ignore_timeout(self): #---------------------------------------------------------------------- # verify that the timeout argument is ignored by write queries #---------------------------------------------------------------------- write_queries = [ # create query "UNWIND range(0, 10000) AS x CREATE (a:M)", # update query "MATCH (a:M) SET a.v = 2", # delete query "MATCH (a:M) DELETE a" ] # queries should complete successfully for q in write_queries: try: result = redis_graph.query(q, timeout=1) # the query should have taken longer than the timeout value self.env.assertGreater(result.run_time_ms, 2) except ResponseError: assert (False) #---------------------------------------------------------------------- # index creation should ignore timeouts #---------------------------------------------------------------------- query = "UNWIND range (0, 100000) AS x CREATE (:M {v:x})" redis_graph.query(query) # create index query = "CREATE INDEX ON :M(v)" try: # the query should complete successfully result = redis_graph.query(query, timeout=1) self.env.assertEquals(result.indices_created, 1) except ResponseError: assert (False) #---------------------------------------------------------------------- # index deletion should ignore timeouts #---------------------------------------------------------------------- query = "DROP INDEX ON :M(v)" try: # the query should complete successfully result = redis_graph.query(query, timeout=1) self.env.assertEquals(result.indices_deleted, 1) except ResponseError: assert (False) def test04_timeout_index_scan(self): # construct a graph and create multiple indices query = """UNWIND range(0, 100000) AS x CREATE (p:Person {age: x%90, height: x%200, weight: x%80})""" redis_graph.query(query) query = """CREATE INDEX ON :Person(age, height, weight)""" redis_graph.query(query) queries = [ # full scan "MATCH (a) RETURN a", # ID scan "MATCH (a) WHERE ID(a) > 20 RETURN a", # label scan "MATCH (a:Person) RETURN a", # single index scan "MATCH (a:Person) WHERE a.age > 40 RETURN a", # index scan + full scan "MATCH (a:Person), (b) WHERE a.age > 40 RETURN a, b", # index scan + ID scan "MATCH (a:Person), (b) WHERE a.age > 40 AND ID(b) > 20 RETURN a, b", # index scan + label scan "MATCH (a:Person), (b:Person) WHERE a.age > 40 RETURN a, b", # multi full and index scans "MATCH (a:Person), (b:Person), (c), (d) WHERE a.age > 40 AND b.height < 150 RETURN a,b,c,d", # multi ID and index scans "MATCH (a:Person), (b:Person), (c:Person), (d) WHERE a.age > 40 AND b.height < 150 AND ID(c) > 20 AND ID(d) > 30 RETURN a,b,c,d", # multi label and index scans "MATCH (a:Person), (b:Person), (c:Person), (d:Person) WHERE a.age > 40 AND b.height < 150 RETURN a,b,c,d", # multi index scans "MATCH (a:Person), (b:Person), (c:Person) WHERE a.age > 40 AND b.height < 150 AND c.weight = 50 RETURN a,b,c" ] for q in queries: try: # query is expected to timeout redis_graph.query(q, timeout=2) assert (False) except ResponseError as error: self.env.assertContains("Query timed out", str(error)) # validate that server didn't crash redis_con.ping() # rerun each query with timeout and limit # expecting queries to run to completion for q in queries: q += " LIMIT 2" redis_graph.query(q, timeout=10) # validate that server didn't crash redis_con.ping()
class testEdgeIndexUpdatesFlow(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 new_edge(self, from_node, to_node): return Edge(from_node, types[edge_ctr % 2], to_node, properties={'unique': edge_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 global edge_ctr for i in range(1000): from_node = self.new_node() node_ctr += 1 redis_graph.add_node(from_node) to_node = self.new_node() node_ctr += 1 redis_graph.add_node(to_node) edge = self.new_edge(from_node, to_node) edge_ctr += 1 redis_graph.add_edge(edge) redis_graph.commit() def build_indices(self): for field in fields: redis_graph.query("CREATE INDEX FOR ()-[r:type_a]-() ON (r.%s)" % (field)) redis_graph.query("CREATE INDEX FOR ()-[r:type_b]-() ON (r.%s)" % (field)) # Validate that all properties are indexed def validate_indexed(self): for field in fields: resp = redis_graph.execution_plan("""MATCH ()-[a:type_a]->() WHERE a.%s > 0 RETURN a""" % (field)) self.env.assertIn('Edge By 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 ()-[r:type_a]->() RETURN r.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 ()-[r:type_b]->() RETURN r.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 type in types: resp = redis_graph.execution_plan("""MATCH ()-[a:%s]->() WHERE a.doubleval < 100 RETURN a.doubleval ORDER BY a.doubleval""" % (type)) self.env.assertIn('Edge By Index Scan', resp) indexed_result = redis_graph.query("""MATCH ()-[a:%s]->() WHERE a.doubleval < 100 RETURN a.doubleval ORDER BY a.doubleval""" % (type)) scan_result = redis_graph.query("""MATCH ()-[a:%s]->() RETURN a.doubleval ORDER BY a.doubleval""" % (type)) 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 type in types: resp = redis_graph.execution_plan("""MATCH ()-[a:%s]->() WHERE a.intval > 0 RETURN a.intval ORDER BY a.intval""" % (type)) self.env.assertIn('Edge By Index Scan', resp) indexed_result = redis_graph.query("""MATCH ()-[a:%s]->() WHERE a.intval > 0 RETURN a.intval ORDER BY a.intval""" % (type)) scan_result = redis_graph.query("""MATCH ()-[a:%s]->() RETURN a.intval ORDER BY a.intval""" % (type)) 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 edges 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 edges 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 edges and validate indices def test03_edge_creation(self): # Reset nodes in the Graph object so that we won't double-commit the originals redis_graph.nodes = {} redis_graph.edges = [] global node_ctr global edge_ctr for i in range(100): from_node = self.new_node() redis_graph.add_node(from_node) node_ctr += 1 to_node = self.new_node() redis_graph.add_node(to_node) node_ctr += 1 edge = self.new_edge(from_node, to_node) redis_graph.add_edge(edge) edge_ctr += 1 redis_graph.commit() self.validate_state() # Delete every other edge in first 100 and validate indices def test04_edge_deletion(self): # Reset nodes in the Graph object so that we won't double-commit the originals redis_graph.nodes = {} redis_graph.edges = [] global node_ctr global edge_ctr # Delete edges 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.relationships_deleted, 1) edge_ctr -= 1 self.validate_state() # Delete all edges matching a filter result = redis_graph.query("MATCH ()-[a:type_a]->() WHERE a.group = 'Group A' DELETE a") self.env.assertGreater(result.relationships_deleted, 0) self.validate_state() def test05_unindexed_property_update(self): # Add an unindexed property to all edges. redis_graph.query("MATCH ()-[a]->() SET a.unindexed = 'unindexed'") # Retrieve a single edge 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 edge with a single indexed property query = """CREATE ()-[:NEW {v: 5}]->()""" result = redis_graph.query(query) self.env.assertEquals(result.properties_set, 1) redis_graph.query("CREATE INDEX FOR ()-[r:NEW]-() ON (r.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("Edge By Index Scan", plan) result = redis_graph.query(query) # No entities should be returned expected_result = [] self.env.assertEquals(result.result_set, expected_result)
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.query("CREATE INDEX ON :label_a(%s)" % (field)) redis_graph.query("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('Node By 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('Node By 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('Node By 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("Node By 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 TestAggregate(): def __init__(self): self.env = Env() add_values(self.env) def testGroupBy(self): cmd = ['ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'AS', 'count', 'SORTBY', 2, '@count', 'desc', 'LIMIT', '0', '5' ] res = self.env.cmd(*cmd) self.env.assertIsNotNone(res) self.env.assertEqual([292L, ['brand', '', 'count', '1518'], ['brand', 'mad catz', 'count', '43'], ['brand', 'generic', 'count', '40'], ['brand', 'steelseries', 'count', '37'], ['brand', 'logitech', 'count', '35']], res) def testMinMax(self): cmd = ['ft.aggregate', 'games', 'sony', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'REDUCE', 'min', '1', '@price', 'as', 'minPrice', 'SORTBY', '2', '@minPrice', 'DESC'] res = self.env.cmd(*cmd) self.env.assertIsNotNone(res) row = to_dict(res[1]) self.env.assertEqual(88, int(float(row['minPrice']))) cmd = ['ft.aggregate', 'games', 'sony', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'REDUCE', 'max', '1', '@price', 'as', 'maxPrice', 'SORTBY', '2', '@maxPrice', 'DESC'] res = self.env.cmd(*cmd) row = to_dict(res[1]) self.env.assertEqual(695, int(float(row['maxPrice']))) def testAvg(self): cmd = ['ft.aggregate', 'games', 'sony', 'GROUPBY', '1', '@brand', 'REDUCE', 'avg', '1', '@price', 'AS', 'avg_price', 'REDUCE', 'count', '0', 'SORTBY', '2', '@avg_price', 'DESC'] res = self.env.cmd(*cmd) self.env.assertIsNotNone(res) self.env.assertEqual(26, res[0]) # Ensure the formatting actually exists first_row = to_dict(res[1]) self.env.assertEqual(109, int(float(first_row['avg_price']))) for row in res[1:]: row = to_dict(row) self.env.assertIn('avg_price', row) # Test aliasing cmd = ['FT.AGGREGATE', 'games', 'sony', 'GROUPBY', '1', '@brand', 'REDUCE', 'avg', '1', '@price', 'AS', 'avgPrice'] res = self.env.cmd(*cmd) first_row = to_dict(res[1]) self.env.assertEqual(17, int(float(first_row['avgPrice']))) def testCountDistinct(self): cmd = ['FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'COUNT_DISTINCT', '1', '@title', 'AS', 'count_distinct(title)', 'REDUCE', 'COUNT', '0' ] res = self.env.cmd(*cmd)[1:] # print res row = to_dict(res[0]) self.env.assertEqual(1484, int(row['count_distinct(title)'])) cmd = ['FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'COUNT_DISTINCTISH', '1', '@title', 'AS', 'count_distinctish(title)', 'REDUCE', 'COUNT', '0' ] res = self.env.cmd(*cmd)[1:] # print res row = to_dict(res[0]) self.env.assertEqual(1461, int(row['count_distinctish(title)'])) def testQuantile(self): cmd = ['FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'QUANTILE', '2', '@price', '0.50', 'AS', 'q50', 'REDUCE', 'QUANTILE', '2', '@price', '0.90', 'AS', 'q90', 'REDUCE', 'QUANTILE', '2', '@price', '0.95', 'AS', 'q95', 'REDUCE', 'AVG', '1', '@price', 'REDUCE', 'COUNT', '0', 'AS', 'rowcount', 'SORTBY', '2', '@rowcount', 'DESC', 'MAX', '1'] res = self.env.cmd(*cmd) row = to_dict(res[1]) # TODO: Better samples self.env.assertAlmostEqual(14.99, float(row['q50']), delta=3) self.env.assertAlmostEqual(70, float(row['q90']), delta=50) # This tests the 95th percentile, which is error prone because # so few samples actually exist. I'm disabling it for now so that # there is no breakage in CI # self.env.assertAlmostEqual(110, (float(row['q95'])), delta=50) def testStdDev(self): cmd = ['FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'STDDEV', '1', '@price', 'AS', 'stddev(price)', 'REDUCE', 'AVG', '1', '@price', 'AS', 'avgPrice', 'REDUCE', 'QUANTILE', '2', '@price', '0.50', 'AS', 'q50Price', 'REDUCE', 'COUNT', '0', 'AS', 'rowcount', 'SORTBY', '2', '@rowcount', 'DESC', 'LIMIT', '0', '10'] res = self.env.cmd(*cmd) row = to_dict(res[1]) self.env.assertTrue(10 <= int( float(row['q50Price'])) <= 20) self.env.assertAlmostEqual(53, int(float(row['stddev(price)'])), delta=50) self.env.assertEqual(29, int(float(row['avgPrice']))) def testParseTime(self): cmd = ['FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'COUNT', '0', 'AS', 'count', 'APPLY', 'timefmt(1517417144)', 'AS', 'dt', 'APPLY', 'parse_time("%FT%TZ", @dt)', 'as', 'parsed_dt', 'LIMIT', '0', '1'] res = self.env.cmd(*cmd) self.env.assertEqual(['brand', '', 'count', '1518', 'dt', '2018-01-31T16:45:44Z', 'parsed_dt', '1517417144'], res[1]) def testRandomSample(self): cmd = ['FT.AGGREGATE', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'COUNT', '0', 'AS', 'num', 'REDUCE', 'RANDOM_SAMPLE', '2', '@price', '10', 'SORTBY', '2', '@num', 'DESC', 'MAX', '10'] for row in self.env.cmd(*cmd)[1:]: self.env.assertIsInstance(row[5], list) self.env.assertGreater(len(row[5]), 0) self.env.assertGreaterEqual(row[3], len(row[5])) self.env.assertLessEqual(len(row[5]), 10) def testTimeFunctions(self): cmd = ['FT.AGGREGATE', 'games', '*', 'APPLY', '1517417144', 'AS', 'dt', 'APPLY', 'timefmt(@dt)', 'AS', 'timefmt', 'APPLY', 'day(@dt)', 'AS', 'day', 'APPLY', 'hour(@dt)', 'AS', 'hour', 'APPLY', 'minute(@dt)', 'AS', 'minute', 'APPLY', 'month(@dt)', 'AS', 'month', 'APPLY', 'dayofweek(@dt)', 'AS', 'dayofweek', 'APPLY', 'dayofmonth(@dt)', 'AS', 'dayofmonth', 'APPLY', 'dayofyear(@dt)', 'AS', 'dayofyear', 'APPLY', 'year(@dt)', 'AS', 'year', 'LIMIT', '0', '1'] res = self.env.cmd(*cmd) self.env.assertListEqual([1L, ['dt', '1517417144', 'timefmt', '2018-01-31T16:45:44Z', 'day', '1517356800', 'hour', '1517414400', 'minute', '1517417100', 'month', '1514764800', 'dayofweek', '3', 'dayofmonth', '31', 'dayofyear', '30', 'year', '2018']], res) def testStringFormat(self): cmd = ['FT.AGGREGATE', 'games', '@brand:sony', 'GROUPBY', '2', '@title', '@brand', 'REDUCE', 'COUNT', '0', 'REDUCE', 'MAX', '1', '@price', 'AS', 'price', 'APPLY', 'format("%s|%s|%s|%s", @title, @brand, "Mark", @price)', 'as', 'titleBrand', 'LIMIT', '0', '10'] res = self.env.cmd(*cmd) for row in res[1:]: row = to_dict(row) expected = '%s|%s|%s|%g' % ( row['title'], row['brand'], 'Mark', float(row['price'])) self.env.assertEqual(expected, row['titleBrand']) def testSum(self): cmd = ['ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'AS', 'count', 'REDUCE', 'sum', 1, '@price', 'AS', 'sum(price)', 'SORTBY', 2, '@sum(price)', 'desc', 'LIMIT', '0', '5' ] res = self.env.cmd(*cmd) self.env.assertEqual([292L, ['brand', '', 'count', '1518', 'sum(price)', '44780.69'], ['brand', 'mad catz', 'count', '43', 'sum(price)', '3973.48'], ['brand', 'razer', 'count', '26', 'sum(price)', '2558.58'], ['brand', 'logitech', 'count', '35', 'sum(price)', '2329.21'], ['brand', 'steelseries', 'count', '37', 'sum(price)', '1851.12']], res) def testFilter(self): cmd = ['ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'AS', 'count', 'FILTER', '@count > 5' ] res = self.env.cmd(*cmd) for row in res[1:]: row = to_dict(row) self.env.assertGreater(int(row['count']), 5) cmd = ['ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count', '0', 'AS', 'count', 'FILTER', '@count < 5', 'FILTER', '@count > 2 && @brand != ""' ] res = self.env.cmd(*cmd) for row in res[1:]: row = to_dict(row) self.env.assertLess(int(row['count']), 5) self.env.assertGreater(int(row['count']), 2) def testToList(self): cmd = ['ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'count_distinct', '1', '@price', 'as', 'count', 'REDUCE', 'tolist', 1, '@price', 'as', 'prices', 'SORTBY', 2, '@count', 'desc', 'LIMIT', '0', '5' ] res = self.env.cmd(*cmd) for row in res[1:]: row = to_dict(row) self.env.assertEqual(int(row['count']), len(row['prices'])) def testSortBy(self): res = self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'sum', 1, '@price', 'as', 'price', 'SORTBY', 2, '@price', 'desc', 'LIMIT', '0', '2') self.env.assertListEqual([292L, ['brand', '', 'price', '44780.69'], [ 'brand', 'mad catz', 'price', '3973.48']], res) res = self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'sum', 1, '@price', 'as', 'price', 'SORTBY', 2, '@price', 'asc', 'LIMIT', '0', '2') self.env.assertListEqual([292L, ['brand', 'myiico', 'price', '0.23'], [ 'brand', 'crystal dynamics', 'price', '0.25']], res) # Test MAX with limit higher than it res = self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'sum', 1, '@price', 'as', 'price', 'SORTBY', 2, '@price', 'asc', 'MAX', 2) self.env.assertListEqual([292L, ['brand', 'myiico', 'price', '0.23'], [ 'brand', 'crystal dynamics', 'price', '0.25']], res) # Test Sorting by multiple properties res = self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', '1', '@brand', 'REDUCE', 'sum', 1, '@price', 'as', 'price', 'APPLY', '(@price % 10)', 'AS', 'price', 'SORTBY', 4, '@price', 'asc', '@brand', 'desc', 'MAX', 10, ) self.env.assertListEqual([292L, ['brand', 'zps', 'price', '0'], ['brand', 'zalman', 'price', '0'], ['brand', 'yoozoo', 'price', '0'], ['brand', 'white label', 'price', '0'], ['brand', 'stinky', 'price', '0'], [ 'brand', 'polaroid', 'price', '0'], ['brand', 'plantronics', 'price', '0'], ['brand', 'ozone', 'price', '0'], ['brand', 'oooo', 'price', '0'], ['brand', 'neon', 'price', '0']], res) def testExpressions(self): pass def testNoGroup(self): res = self.env.cmd('ft.aggregate', 'games', '*', 'LOAD', '2', '@brand', '@price', 'APPLY', 'floor(sqrt(@price)) % 10', 'AS', 'price', 'SORTBY', 4, '@price', 'desc', '@brand', 'desc', 'MAX', 5, ) exp = [2265L, ['brand', 'xbox', 'price', '9'], ['brand', 'turtle beach', 'price', '9'], ['brand', 'trust', 'price', '9'], ['brand', 'steelseries', 'price', '9'], ['brand', 'speedlink', 'price', '9']] # exp = [2265L, ['brand', 'Xbox', 'price', '9'], ['brand', 'Turtle Beach', 'price', '9'], [ # 'brand', 'Trust', 'price', '9'], ['brand', 'SteelSeries', 'price', '9'], ['brand', 'Speedlink', 'price', '9']] self.env.assertListEqual(exp[1], res[1]) def testLoad(self): res = self.env.cmd('ft.aggregate', 'games', '*', 'LOAD', '3', '@brand', '@price', '@nonexist', 'SORTBY', 2, '@price', 'DESC', 'MAX', 2) exp = [3L, ['brand', '', 'price', '759.12'], ['brand', 'Sony', 'price', '695.8']] self.env.assertEqual(exp[1], res[1]) def testSplit(self): res = self.env.cmd('ft.aggregate', 'games', '*', 'APPLY', 'split("hello world, foo,,,bar,", ",", " ")', 'AS', 'strs', 'APPLY', 'split("hello world, foo,,,bar,", " ", ",")', 'AS', 'strs2', 'APPLY', 'split("hello world, foo,,,bar,", "", "")', 'AS', 'strs3', 'APPLY', 'split("hello world, foo,,,bar,")', 'AS', 'strs4', 'APPLY', 'split("hello world, foo,,,bar,",",")', 'AS', 'strs5', 'APPLY', 'split("")', 'AS', 'empty', 'LIMIT', '0', '1' ) # print "Got {} results".format(len(res)) # return # pprint.pprint(res) self.env.assertListEqual([1L, ['strs', ['hello world', 'foo', 'bar'], 'strs2', ['hello', 'world', 'foo,,,bar'], 'strs3', ['hello world, foo,,,bar,'], 'strs4', ['hello world', 'foo', 'bar'], 'strs5', ['hello world', 'foo', 'bar'], 'empty', []]], res) def testFirstValue(self): res = self.env.cmd('ft.aggregate', 'games', '@brand:(sony|matias|beyerdynamic|(mad catz))', 'GROUPBY', 1, '@brand', 'REDUCE', 'FIRST_VALUE', 4, '@title', 'BY', '@price', 'DESC', 'AS', 'top_item', 'REDUCE', 'FIRST_VALUE', 4, '@price', 'BY', '@price', 'DESC', 'AS', 'top_price', 'REDUCE', 'FIRST_VALUE', 4, '@title', 'BY', '@price', 'ASC', 'AS', 'bottom_item', 'REDUCE', 'FIRST_VALUE', 4, '@price', 'BY', '@price', 'ASC', 'AS', 'bottom_price', 'SORTBY', 2, '@top_price', 'DESC', 'MAX', 5 ) expected = [4L, ['brand', 'sony', 'top_item', 'sony psp slim & lite 2000 console', 'top_price', '695.8', 'bottom_item', 'sony dlchd20p high speed hdmi cable for playstation 3', 'bottom_price', '5.88'], ['brand', 'matias', 'top_item', 'matias halfkeyboard usb', 'top_price', '559.99', 'bottom_item', 'matias halfkeyboard usb', 'bottom_price', '559.99'], ['brand', 'beyerdynamic', 'top_item', 'beyerdynamic mmx300 pc gaming premium digital headset with microphone', 'top_price', '359.74', 'bottom_item', 'beyerdynamic headzone pc gaming digital surround sound system with mmx300 digital headset with microphone', 'bottom_price', '0'], ['brand', 'mad catz', 'top_item', 'mad catz s.t.r.i.k.e.7 gaming keyboard', 'top_price', '295.95', 'bottom_item', 'madcatz mov4545 xbox replacement breakaway cable', 'bottom_price', '3.49']] self.env.assertListEqual(expected, res) def testLoadAfterGroupBy(self): with self.env.assertResponseError(): self.env.cmd('ft.aggregate', 'games', '*', 'GROUPBY', 1, '@brand', 'LOAD', 1, '@brand')