diff --git a/src/main/java/com/redislabs/redisgraph/RedisGraph.java b/src/main/java/com/redislabs/redisgraph/RedisGraph.java index 3a2ae19..6cc57ce 100644 --- a/src/main/java/com/redislabs/redisgraph/RedisGraph.java +++ b/src/main/java/com/redislabs/redisgraph/RedisGraph.java @@ -14,6 +14,14 @@ public interface RedisGraph extends Closeable { */ ResultSet query(String graphId, String query); + /** + * Execute a Cypher read-only query. + * @param graphId a graph to perform the query on + * @param query Cypher query + * @return a result set + */ + ResultSet readOnlyQuery(String graphId, String query); + /** * Execute a Cypher query with timeout. * @param graphId a graph to perform the query on @@ -23,6 +31,16 @@ public interface RedisGraph extends Closeable { */ ResultSet query(String graphId, String query, long timeout); + /** + * Execute a Cypher read-only query with timeout. + * @param graphId a graph to perform the query on + * @param query Cypher query + * @param timeout + * @return a result set + */ + ResultSet readOnlyQuery(String graphId, String query, long timeout); + + /** * Execute a Cypher query with arguments * @param graphId a graph to perform the query on @@ -34,6 +52,7 @@ public interface RedisGraph extends Closeable { @Deprecated ResultSet query(String graphId, String query, Object ...args); + /** * Executes a cypher query with parameters. * @param graphId a graph to perform the query on. @@ -43,6 +62,15 @@ public interface RedisGraph extends Closeable { */ ResultSet query(String graphId, String query, Map params); + /** + * Executes a cypher read-only query with parameters. + * @param graphId a graph to perform the query on. + * @param query Cypher query. + * @param params parameters map. + * @return a result set. + */ + ResultSet readOnlyQuery(String graphId, String query, Map params); + /** * Executes a cypher query with parameters and timeout. * @param graphId a graph to perform the query on. @@ -53,6 +81,16 @@ public interface RedisGraph extends Closeable { */ ResultSet query(String graphId, String query, Map params, long timeout); + /** + * Executes a cypher read-only query with parameters and timeout. + * @param graphId a graph to perform the query on. + * @param query Cypher query. + * @param params parameters map. + * @param timeout + * @return a result set. + */ + ResultSet readOnlyQuery(String graphId, String query, Map params, long timeout); + /** * Invokes stored procedures without arguments * @param graphId a graph to perform the query on diff --git a/src/main/java/com/redislabs/redisgraph/RedisGraphTransaction.java b/src/main/java/com/redislabs/redisgraph/RedisGraphTransaction.java index 08259fe..cbcd127 100644 --- a/src/main/java/com/redislabs/redisgraph/RedisGraphTransaction.java +++ b/src/main/java/com/redislabs/redisgraph/RedisGraphTransaction.java @@ -31,6 +31,14 @@ public interface RedisGraphTransaction extends */ Response query(String graphId, String query); + /** + * Execute a Cypher read-only query. + * @param graphId a graph to perform the query on + * @param query Cypher query + * @return a response which builds the result set with the query answer. + */ + Response readOnlyQuery(String graphId, String query); + /** * Execute a Cypher query with timeout. * @param graphId a graph to perform the query on @@ -40,6 +48,15 @@ public interface RedisGraphTransaction extends */ Response query(String graphId, String query, long timeout); + /** + * Execute a Cypher read-only query with timeout. + * @param graphId a graph to perform the query on + * @param query Cypher query + * @param timeout + * @return a response which builds the result set with the query answer. + */ + Response readOnlyQuery(String graphId, String query, long timeout); + /** * Execute a Cypher query with arguments * @param graphId a graph to perform the query on @@ -60,6 +77,15 @@ public interface RedisGraphTransaction extends */ Response query(String graphId, String query, Map params); + /** + * Executes a cypher read-only query with parameters. + * @param graphId a graph to perform the query on. + * @param query Cypher query. + * @param params parameters map. + * @return a response which builds the result set with the query answer. + */ + Response readOnlyQuery(String graphId, String query, Map params); + /** * Executes a cypher query with parameters and timeout. * @param graphId a graph to perform the query on. @@ -70,6 +96,16 @@ public interface RedisGraphTransaction extends */ Response query(String graphId, String query, Map params, long timeout); + /** + * Executes a cypher read-only query with parameters and timeout. + * @param graphId a graph to perform the query on. + * @param query Cypher query. + * @param params parameters map. + * @param timeout + * @return a response which builds the result set with the query answer. + */ + Response readOnlyQuery(String graphId, String query, Map params, long timeout); + /** * Invokes stored procedures without arguments * @param graphId a graph to perform the query on diff --git a/src/main/java/com/redislabs/redisgraph/impl/api/AbstractRedisGraph.java b/src/main/java/com/redislabs/redisgraph/impl/api/AbstractRedisGraph.java index e1149e0..57fce53 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/api/AbstractRedisGraph.java +++ b/src/main/java/com/redislabs/redisgraph/impl/api/AbstractRedisGraph.java @@ -27,6 +27,14 @@ public abstract class AbstractRedisGraph implements RedisGraph { */ protected abstract ResultSet sendQuery(String graphId, String preparedQuery); + /** + * Sends a read-only query to the redis graph. Implementation and context dependent + * @param graphId graph to be queried + * @param preparedQuery prepared query + * @return Result set + */ + protected abstract ResultSet sendReadOnlyQuery(String graphId, String preparedQuery); + /** * Sends a query to the redis graph.Implementation and context dependent * @param graphId graph to be queried @@ -36,6 +44,15 @@ public abstract class AbstractRedisGraph implements RedisGraph { */ protected abstract ResultSet sendQuery(String graphId, String preparedQuery, long timeout); + /** + * Sends a read-query to the redis graph.Implementation and context dependent + * @param graphId graph to be queried + * @param preparedQuery prepared query + * @param timeout + * @return Result set + */ + protected abstract ResultSet sendReadOnlyQuery(String graphId, String preparedQuery, long timeout); + /** * Execute a Cypher query. * @param graphId a graph to perform the query on @@ -46,6 +63,16 @@ public ResultSet query(String graphId, String query) { return sendQuery(graphId, query); } + /** + * Execute a Cypher read-only query. + * @param graphId a graph to perform the query on + * @param query Cypher query + * @return a result set + */ + public ResultSet readOnlyQuery(String graphId, String query) { + return sendReadOnlyQuery(graphId, query); + } + /** * Execute a Cypher query with timeout. * @param graphId a graph to perform the query on @@ -58,6 +85,18 @@ public ResultSet query(String graphId, String query, long timeout) { return sendQuery(graphId, query, timeout); } + /** + * Execute a Cypher read-only query with timeout. + * @param graphId a graph to perform the query on + * @param timeout + * @param query Cypher query + * @return a result set + */ + @Override + public ResultSet readOnlyQuery(String graphId, String query, long timeout) { + return sendReadOnlyQuery(graphId, query, timeout); + } + /** * Execute a Cypher query with arguments * @param graphId a graph to perform the query on @@ -84,6 +123,18 @@ public ResultSet query(String graphId, String query, Map params) return sendQuery(graphId, preparedQuery); } + /** + * Executes a cypher read-only query with parameters. + * @param graphId a graph to perform the query on. + * @param query Cypher query. + * @param params parameters map. + * @return a result set. + */ + public ResultSet readOnlyQuery(String graphId, String query, Map params) { + String preparedQuery = Utils.prepareQuery(query, params); + return sendReadOnlyQuery(graphId, preparedQuery); + } + /** * Executes a cypher query with parameters and timeout. * @param graphId a graph to perform the query on. @@ -98,6 +149,20 @@ public ResultSet query(String graphId, String query, Map params, return sendQuery(graphId, preparedQuery, timeout); } + /** + * Executes a cypher read-only query with parameters and timeout. + * @param graphId a graph to perform the query on. + * @param timeout + * @param query Cypher query. + * @param params parameters map. + * @return a result set. + */ + @Override + public ResultSet readOnlyQuery(String graphId, String query, Map params, long timeout) { + String preparedQuery = Utils.prepareQuery(query, params); + return sendReadOnlyQuery(graphId, preparedQuery, timeout); + } + public ResultSet callProcedure(String graphId, String procedure){ return callProcedure(graphId, procedure, Utils.DUMMY_LIST, Utils.DUMMY_MAP); } diff --git a/src/main/java/com/redislabs/redisgraph/impl/api/ContextedRedisGraph.java b/src/main/java/com/redislabs/redisgraph/impl/api/ContextedRedisGraph.java index 7918c37..92896d0 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/api/ContextedRedisGraph.java +++ b/src/main/java/com/redislabs/redisgraph/impl/api/ContextedRedisGraph.java @@ -59,6 +59,27 @@ protected ResultSet sendQuery(String graphId, String preparedQuery) { } } + /** + * Sends the read-only query over the instance only connection + * @param graphId graph to be queried + * @param preparedQuery prepared query + * @return Result set with the query answer + */ + @Override + protected ResultSet sendReadOnlyQuery(String graphId, String preparedQuery) { + Jedis conn = getConnection(); + try { + List rawResponse = (List) conn.sendCommand(RedisGraphCommand.RO_QUERY, graphId, preparedQuery, Utils.COMPACT_STRING); + return new ResultSetImpl(rawResponse, this, caches.getGraphCache(graphId)); + } + catch (JRedisGraphException ge) { + throw ge; + } + catch (JedisDataException de) { + throw new JRedisGraphException(de); + } + } + /** * Sends the query over the instance only connection * @param graphId graph to be queried @@ -82,6 +103,29 @@ protected ResultSet sendQuery(String graphId, String preparedQuery, long timeout } } + /** + * Sends the read-only query over the instance only connection + * @param graphId graph to be queried + * @param timeout + * @param preparedQuery prepared query + * @return Result set with the query answer + */ + @Override + protected ResultSet sendReadOnlyQuery(String graphId, String preparedQuery, long timeout) { + Jedis conn = getConnection(); + try { + List rawResponse = (List) conn.sendBlockingCommand(RedisGraphCommand.RO_QUERY, + graphId, preparedQuery, Utils.COMPACT_STRING, Utils.TIMEOUT_STRING, Long.toString(timeout)); + return new ResultSetImpl(rawResponse, this, caches.getGraphCache(graphId)); + } + catch (JRedisGraphException ge) { + throw ge; + } + catch (JedisDataException de) { + throw new JRedisGraphException(de); + } + } + /** * @return Returns the instance Jedis connection. */ diff --git a/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraph.java b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraph.java index c2f4cc2..1bb0f50 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraph.java +++ b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraph.java @@ -69,6 +69,21 @@ protected ResultSet sendQuery(String graphId, String preparedQuery){ } } + /** + * Overrides the abstract function. + * Sends the read-only query from any Jedis connection received from the Jedis pool and closes it once done + * @param graphId graph to be queried + * @param preparedQuery prepared query + * @return Result set with the query answer + */ + @Override + protected ResultSet sendReadOnlyQuery(String graphId, String preparedQuery){ + try (ContextedRedisGraph contextedRedisGraph = new ContextedRedisGraph(getConnection())) { + contextedRedisGraph.setRedisGraphCaches(caches); + return contextedRedisGraph.sendReadOnlyQuery(graphId, preparedQuery); + } + } + /** * Overrides the abstract function. * Sends the query from any Jedis connection received from the Jedis pool and closes it once done @@ -85,6 +100,22 @@ protected ResultSet sendQuery(String graphId, String preparedQuery, long timeout } } + /** + * Overrides the abstract function. + * Sends the read-only query from any Jedis connection received from the Jedis pool and closes it once done + * @param graphId graph to be queried + * @param preparedQuery prepared query + * @param timeout + * @return Result set with the query answer + */ + @Override + protected ResultSet sendReadOnlyQuery(String graphId, String preparedQuery, long timeout){ + try (ContextedRedisGraph contextedRedisGraph = new ContextedRedisGraph(getConnection())) { + contextedRedisGraph.setRedisGraphCaches(caches); + return contextedRedisGraph.sendReadOnlyQuery(graphId, preparedQuery, timeout); + } + } + /** * Closes the Jedis pool */ diff --git a/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphCommand.java b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphCommand.java index 3db45d0..0af0f61 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphCommand.java +++ b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphCommand.java @@ -9,6 +9,7 @@ */ public enum RedisGraphCommand implements ProtocolCommand { QUERY("graph.QUERY"), + RO_QUERY("graph.RO_QUERY"), DELETE("graph.DELETE"); private final byte[] raw; diff --git a/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphTransaction.java b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphTransaction.java index c96c749..2f2fd73 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphTransaction.java +++ b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphTransaction.java @@ -47,6 +47,23 @@ public ResultSet build(Object o) { }); } + /** + * Execute a Cypher read-oly query. + * @param graphId a graph to perform the query on + * @param query Cypher query + * @return a response which builds the result set with the query answer. + */ + @Override + public Response readOnlyQuery(String graphId, String query) { + client.sendCommand(RedisGraphCommand.RO_QUERY, graphId, query, "--COMPACT"); + return getResponse(new Builder() { + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List)o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + /** * Execute a Cypher query with timeout. * @@ -67,6 +84,26 @@ public ResultSet build(Object o) { }); } + /** + * Execute a Cypher read-only query with timeout. + * + * NOTE: timeout is simply sent to DB. Socket timeout will not be changed. + * @param graphId a graph to perform the query on + * @param query Cypher query + * @param timeout + * @return a response which builds the result set with the query answer. + */ + @Override + public Response readOnlyQuery(String graphId, String query, long timeout) { + client.sendCommand(RedisGraphCommand.RO_QUERY, graphId, query, "--COMPACT", "TIMEOUT", Long.toString(timeout)); + return getResponse(new Builder() { + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List)o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + /** * Execute a Cypher query with arguments * @@ -108,6 +145,25 @@ public ResultSet build(Object o) { }); } + /** + * Executes a cypher read-only query with parameters. + * @param graphId a graph to perform the query on. + * @param query Cypher query. + * @param params parameters map. + * @return a response which builds the result set with the query answer. + */ + @Override + public Response readOnlyQuery(String graphId, String query, Map params) { + String preparedQuery = Utils.prepareQuery(query, params); + client.sendCommand(RedisGraphCommand.RO_QUERY, graphId, preparedQuery, "--COMPACT"); + return getResponse(new Builder() { + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List)o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + /** * Executes a cypher query with parameters and timeout. * @@ -131,6 +187,29 @@ public ResultSet build(Object o) { }); } + /** + * Executes a cypher read-only query with parameters and timeout. + * + * NOTE: timeout is simply sent to DB. Socket timeout will not be changed. + * timeout. + * @param graphId a graph to perform the query on. + * @param query Cypher query. + * @param params parameters map. + * @param timeout + * @return a response which builds the result set with the query answer. + */ + @Override + public Response readOnlyQuery(String graphId, String query, Map params, long timeout) { + String preparedQuery = Utils.prepareQuery(query, params); + client.sendCommand(RedisGraphCommand.RO_QUERY, graphId, preparedQuery, "--COMPACT", "TIMEOUT", Long.toString(timeout)); + return getResponse(new Builder() { + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List)o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + /** * Invokes stored procedures without arguments, in multi/exec context * @param graphId a graph to perform the query on diff --git a/src/test/java/com/redislabs/redisgraph/RedisGraphAPITest.java b/src/test/java/com/redislabs/redisgraph/RedisGraphAPITest.java index e0c4f09..8a137bc 100644 --- a/src/test/java/com/redislabs/redisgraph/RedisGraphAPITest.java +++ b/src/test/java/com/redislabs/redisgraph/RedisGraphAPITest.java @@ -9,7 +9,6 @@ import com.redislabs.redisgraph.impl.resultset.RecordImpl; import com.redislabs.redisgraph.impl.resultset.ResultSetImpl; import com.redislabs.redisgraph.test.utils.PathBuilder; -import redis.clients.jedis.exceptions.JedisDataException; import java.util.*; import java.util.stream.Collectors; @@ -479,6 +478,7 @@ public void testEscapedQuery() { Assert.assertNotNull(api.query("social", "CREATE (:escaped{s1:%s,s2:%s})", "S\"'", "S'\"")); Assert.assertNotNull(api.query("social", "MATCH (n) where n.s1=%s and n.s2=%s RETURN n", "S\"'", "S'\"")); Assert.assertNotNull(api.query("social", "MATCH (n) where n.s1='S\"' RETURN n")); + } @Test @@ -877,6 +877,28 @@ public void testParameters(){ } } + + @Test + public void testParametersReadOnly(){ + Object[] parameters = {1, 2.3, true, false, null, "str", 'a', "b" ,Arrays.asList(1,2,3), new Integer[]{1,2,3}}; + Object[] expected_anwsers = {1L, 2.3, true, false, null, "str", "a", "b", Arrays.asList(1L, 2L, 3L), new Long[]{1L, 2L, 3L}}; + Map params = new HashMap<>(); + for (int i=0; i < parameters.length; i++) { + Object param = parameters[i]; + params.put("param", param); + ResultSet resultSetRo = api.readOnlyQuery("social", "RETURN $param", params); + Assert.assertEquals(1, resultSetRo.size()); + Record rRo = resultSetRo.next(); + Object oRo = rRo.getValue(0); + Object expected = expected_anwsers[i]; + if(i == parameters.length-1) { + expected = Arrays.asList((Object[])expected); + } + Assert.assertEquals(expected, oRo); + } + } + + @Test public void testNullGraphEntities() { // Create two nodes connected by a single outgoing edge. @@ -977,6 +999,30 @@ public void testMapDataType() { Assert.assertEquals(expected, actual); } + @Test + public void testCachedExecutionReadOnly() { + api.query("social", "CREATE (:N {val:1}), (:N {val:2})"); + + // First time should not be loaded from execution cache + Map params = new HashMap<>(); + params.put("val", 1L); + ResultSet resultSet = api.readOnlyQuery("social","MATCH (n:N {val:$val}) RETURN n.val", params); + Assert.assertEquals(1, resultSet.size()); + Record r = resultSet.next(); + Assert.assertEquals(params.get("val"), r.getValue(0)); + Assert.assertFalse(resultSet.getStatistics().cachedExecution()); + + // Run in loop many times to make sure the query will be loaded + // from cache at least once + for (int i = 0 ; i < 64; i++){ + resultSet = api.readOnlyQuery("social","MATCH (n:N {val:$val}) RETURN n.val", params); + } + Assert.assertEquals(1, resultSet.size()); + r = resultSet.next(); + Assert.assertEquals(params.get("val"), r.getValue(0)); + Assert.assertTrue(resultSet.getStatistics().cachedExecution()); + } + @Test public void timeoutArgument() { ResultSet rs = api.query("social", "UNWIND range(0,100) AS x WITH x AS x WHERE x = 100 RETURN x", 1L); @@ -984,4 +1030,88 @@ public void timeoutArgument() { Record r = rs.next(); Assert.assertEquals(Long.valueOf(100), r.getValue(0)); } + + @Test + public void testSimpleReadOnly() { + api.query("social","CREATE (:person{name:'filipe',age:30})"); + ResultSet rsRo = api.readOnlyQuery("social", "MATCH (a:person) WHERE (a.name = 'filipe') RETURN a.age"); + Assert.assertEquals(1, rsRo.size()); + Record r = rsRo.next(); + Assert.assertEquals(Long.valueOf(30), r.getValue(0)); + } + + @Test + public void testMultiExecWithReadOnlyQueries(){ + try (RedisGraphContext c = api.getContext()) { + RedisGraphTransaction transaction = api.getContext().multi(); + + transaction.set("x", "1"); + transaction.query("social", "CREATE (:Person {name:'a'})"); + transaction.query("g", "CREATE (:Person {name:'a'})"); + transaction.readOnlyQuery("social", "MATCH (n:Person) RETURN n"); + transaction.deleteGraph("g"); + transaction.callProcedure("social", "db.labels"); + List results = transaction.exec(); + + // Redis set command + Assert.assertEquals(String.class, results.get(0).getClass()); + Assert.assertEquals("OK", results.get(0)); + + // Redis graph command + Assert.assertEquals(ResultSetImpl.class, results.get(1).getClass()); + ResultSet resultSet = (ResultSet) results.get(1); + Assert.assertEquals(1, resultSet.getStatistics().nodesCreated()); + Assert.assertEquals(1, resultSet.getStatistics().propertiesSet()); + + + Assert.assertEquals(ResultSetImpl.class, results.get(2).getClass()); + resultSet = (ResultSet) results.get(2); + Assert.assertEquals(1, resultSet.getStatistics().nodesCreated()); + Assert.assertEquals(1, resultSet.getStatistics().propertiesSet()); + + // Graph read-only query result + Assert.assertEquals(ResultSetImpl.class, results.get(5).getClass()); + resultSet = (ResultSet) results.get(3); + + Assert.assertNotNull(resultSet.getHeader()); + Header header = resultSet.getHeader(); + + List schemaNames = header.getSchemaNames(); + Assert.assertNotNull(schemaNames); + Assert.assertEquals(1, schemaNames.size()); + Assert.assertEquals("n", schemaNames.get(0)); + + Property nameProperty = new Property<>("name", "a"); + + Node expectedNode = new Node(); + expectedNode.setId(0); + expectedNode.addLabel("Person"); + expectedNode.addProperty(nameProperty); + // see that the result were pulled from the right graph + Assert.assertEquals(1, resultSet.size()); + Assert.assertTrue(resultSet.hasNext()); + Record record = resultSet.next(); + Assert.assertFalse(resultSet.hasNext()); + Assert.assertEquals(Arrays.asList("n"), record.keys()); + Assert.assertEquals(expectedNode, record.getValue("n")); + + Assert.assertEquals(ResultSetImpl.class, results.get(5).getClass()); + resultSet = (ResultSet) results.get(5); + + Assert.assertNotNull(resultSet.getHeader()); + header = resultSet.getHeader(); + + schemaNames = header.getSchemaNames(); + Assert.assertNotNull(schemaNames); + Assert.assertEquals(1, schemaNames.size()); + Assert.assertEquals("label", schemaNames.get(0)); + + Assert.assertEquals(1, resultSet.size()); + Assert.assertTrue(resultSet.hasNext()); + record = resultSet.next(); + Assert.assertFalse(resultSet.hasNext()); + Assert.assertEquals(Arrays.asList("label"), record.keys()); + Assert.assertEquals("Person", record.getValue("label")); + } + } }