diff --git a/.circleci/config.yml b/.circleci/config.yml index cfa036b..abebfaf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -8,7 +8,7 @@ jobs: docker: - image: circleci/openjdk:8u171-jdk - - image: redislabs/redisgraph:edge + - image: redislabs/redisgraph:2.0-edge port: 6379:6379 working_directory: ~/repo @@ -44,7 +44,7 @@ jobs: - run: bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN} - - run: mvn -s .circleci.settings.xml -DskipTests deploy + - run: mvn -s .circleci.settings.xml -DskipTests deploy workflows: version: 2 diff --git a/.gitignore b/.gitignore index 0740f5e..0dcfd34 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ hs_err_pid* .classpath .project /.settings/ + +#intelij +.idea \ No newline at end of file diff --git a/pom.xml b/pom.xml index 511fd50..1bd6487 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.redislabs jredisgraph - 1.0.7-SNAPSHOT + 2.0.0-SNAPSHOT JRedisGraph Official client for Redis-Graph diff --git a/src/main/java/com/redislabs/redisgraph/Header.java b/src/main/java/com/redislabs/redisgraph/Header.java new file mode 100644 index 0000000..43cf8b3 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/Header.java @@ -0,0 +1,23 @@ +package com.redislabs.redisgraph; + +import java.util.List; + +/** + * Query response header interface. Represents the response schame (column names and types) + */ +public interface Header { + + + public enum ResultSetColumnTypes { + COLUMN_UNKNOWN, + COLUMN_SCALAR, + COLUMN_NODE, + COLUMN_RELATION; + + } + + + List getSchemaNames(); + + List getSchemaTypes(); +} diff --git a/src/main/java/com/redislabs/redisgraph/RedisGraph.java b/src/main/java/com/redislabs/redisgraph/RedisGraph.java new file mode 100644 index 0000000..5542538 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/RedisGraph.java @@ -0,0 +1,190 @@ +package com.redislabs.redisgraph; + +import com.redislabs.redisgraph.impl.GraphCache; +import com.redislabs.redisgraph.impl.ResultSetImpl; +import org.apache.commons.text.translate.AggregateTranslator; +import org.apache.commons.text.translate.CharSequenceTranslator; +import org.apache.commons.text.translate.LookupTranslator; +import redis.clients.jedis.BinaryClient; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.commands.ProtocolCommand; +import redis.clients.jedis.util.Pool; + +import java.io.Closeable; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Collectors; + + +/** + * + */ +public class RedisGraph implements Closeable { + + + + private final Pool client; + private final Map graphCaches = new ConcurrentHashMap<>(); + + + + private static final CharSequenceTranslator ESCAPE_CHYPER; + static { + final Map escapeJavaMap = new HashMap<>(); + escapeJavaMap.put("\'", "\\'"); + escapeJavaMap.put("\"", "\\\""); + ESCAPE_CHYPER = new AggregateTranslator(new LookupTranslator(Collections.unmodifiableMap(escapeJavaMap))); + } + + /** + * Creates a client running on the local machine + + */ + public RedisGraph() { + this("localhost", 6379); + } + + /** + * Creates a client running on the specific host/post + * + * @param host Redis host + * @param port Redis port + */ + public RedisGraph(String host, int port) { + this( new JedisPool(host, port)); + } + + /** + * Creates a client using provided Jedis pool + * + * @param jedis bring your own Jedis pool + */ + public RedisGraph( Pool jedis) { + + this.client = jedis; + } + + @Override + public void close(){ + this.client.close(); + } + + + /** + * Execute a Cypher query with arguments + * + * @param graphId a graph to perform the query on + * @param query Cypher query + * @param args + * @return a result set + */ + public ResultSet query(String graphId, String query, Object ...args) { + if(args.length > 0) { + for(int i=0; i rawResponse = null; + try(Jedis conn = getConnection()){ + rawResponse= sendCompactCommand(conn, Command.QUERY, graphId, query).getObjectMultiBulkReply(); + } + return new ResultSetImpl(rawResponse, graphCaches.get(graphId)); + + } + + /** + * Invokes stored procedures without arguments + * @param graphId a graph to perform the query on + * @param procedure procedure name to invoke + * @return result set with the procedure data + */ + public ResultSet callProcedure(String graphId, String procedure ){ + return callProcedure(graphId, procedure, new ArrayList<>(), new HashMap<>()); + } + + + /** + * Invokes stored procedure with arguments + * @param graphId a graph to perform the query on + * @param procedure procedure name to invoke + * @param args procedure arguments + * @return result set with the procedure data + */ + public ResultSet callProcedure(String graphId, String procedure, List args ){ + return callProcedure(graphId, procedure, args, new HashMap<>()); + } + + + /** + * Deletes the entire graph + * + * @return delete running time statistics + */ + public String deleteGraph(String graphId) { + //clear local state + graphCaches.remove(graphId); + try (Jedis conn = getConnection()) { + return sendCommand(conn, Command.DELETE, graphId).getBulkReply(); + } + + } + + + /** + * Sends command - will be replaced with sendCompactCommand once graph.delete support --compact flag + * @param conn - connection + * @param provider - command type + * @param args - command arguments + * @return + */ + private BinaryClient sendCommand(Jedis conn, ProtocolCommand provider, String ...args) { + BinaryClient binaryClient = conn.getClient(); + binaryClient.sendCommand(provider, args); + return binaryClient; + } + + + /** + * Sends the command with --COMPACT flag + * @param conn - connection + * @param provider - command type + * @param args - command arguments + * @return + */ + private BinaryClient sendCompactCommand(Jedis conn, ProtocolCommand provider, String ...args) { + String[] t = new String[args.length +1]; + System.arraycopy(args, 0 , t, 0, args.length); + t[args.length]="--COMPACT"; + return sendCommand(conn, provider, t); + } + + private Jedis getConnection() { + return this.client.getResource(); + } + + + /** + * Invoke a stored procedure + * @param graphId a graph to perform the query on + * @param procedure - procedure to execute + * @param args - procedure arguments + * @param kwargs - procedure output arguments + * @return + */ + public ResultSet callProcedure(String graphId, String procedure, List args , Map> kwargs ){ + + args = args.stream().map( s -> Utils.quoteString(s)).collect(Collectors.toList()); + StringBuilder queryString = new StringBuilder(); + queryString.append(String.format("CALL %s(%s)", procedure, String.join(",", args))); + List kwargsList = kwargs.getOrDefault("y", null); + if(kwargsList != null){ + queryString.append(String.join(",", kwargsList)); + } + return query(graphId, queryString.toString()); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/RedisGraphAPI.java b/src/main/java/com/redislabs/redisgraph/RedisGraphAPI.java deleted file mode 100644 index 31f7b35..0000000 --- a/src/main/java/com/redislabs/redisgraph/RedisGraphAPI.java +++ /dev/null @@ -1,108 +0,0 @@ -package com.redislabs.redisgraph; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -import com.redislabs.redisgraph.impl.ResultSetImpl; -import org.apache.commons.text.translate.AggregateTranslator; -import org.apache.commons.text.translate.CharSequenceTranslator; -import org.apache.commons.text.translate.LookupTranslator; - -import redis.clients.jedis.BinaryClient; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.commands.ProtocolCommand; -import redis.clients.jedis.util.Pool; - -/** - * RedisGraph client - */ -public class RedisGraphAPI { - - private final Pool client; - private final String graphId; - - private static final CharSequenceTranslator ESCAPE_CHYPER; - static { - final Map escapeJavaMap = new HashMap<>(); - escapeJavaMap.put("\'", "\\'"); - escapeJavaMap.put("\"", "\\\""); - ESCAPE_CHYPER = new AggregateTranslator(new LookupTranslator(Collections.unmodifiableMap(escapeJavaMap))); - } - - /** - * Creates a client to a specific graph running on the local machine - * - * @param graphId the graph id - */ - public RedisGraphAPI(String graphId) { - this(graphId, "localhost", 6379); - } - - /** - * Creates a client to a specific graph running on the specific host/post - * - * @param graphId the graph id - * @param host Redis host - * @param port Redis port - */ - public RedisGraphAPI(String graphId, String host, int port) { - this(graphId, new JedisPool(host, port)); - } - - /** - * Creates a client to a specific graph using provided Jedis pool - * - * @param graphId the graph id - * @param jedis bring your own Jedis pool - */ - public RedisGraphAPI(String graphId, Pool jedis) { - this.graphId = graphId; - this.client = jedis; - } - - /** - * Execute a Cypher query with arguments - * - * @param query Cypher query - * @param args - * @return a result set - */ - public ResultSet query(String query, Object ...args) { - if(args.length > 0) { - for(int i=0; i{ - /** - * Return the query statistics - * @return statistics object - */ - Statistics getStatistics(); +public interface ResultSet extends Iterator { - List getHeader(); -} + public enum ResultSetScalarTypes { + PROPERTY_UNKNOWN, + PROPERTY_NULL, + PROPERTY_STRING, + PROPERTY_INTEGER, + PROPERTY_BOOLEAN, + PROPERTY_DOUBLE, + } + + public int size(); + + Statistics getStatistics(); + + Header getHeader(); + +} \ No newline at end of file diff --git a/src/main/java/com/redislabs/redisgraph/Utils.java b/src/main/java/com/redislabs/redisgraph/Utils.java new file mode 100644 index 0000000..180e8fb --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/Utils.java @@ -0,0 +1,28 @@ +package com.redislabs.redisgraph; + +/** + * Utilities class + */ +public class Utils { + + /** + * + * @param str - a string + * @return the input string surounded with quotation marks, if needed + */ + public static String quoteString(String str){ + if(str.startsWith("\"") && str.endsWith("\"")){ + return str; + } + + StringBuilder sb = new StringBuilder(str.length()+2); + if(str.charAt(0)!='"'){ + sb.append('"'); + } + sb.append(str); + if (str.charAt(str.length()-1)!= '"'){ + sb.append('"'); + } + return sb.toString(); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/Edge.java b/src/main/java/com/redislabs/redisgraph/impl/Edge.java new file mode 100644 index 0000000..4b113fc --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/Edge.java @@ -0,0 +1,92 @@ +package com.redislabs.redisgraph.impl; + +import java.util.Objects; + +/** + * A class represent an edge (graph entity). In addition to the base class id and properties, an edge shows its source, + * destination and relationship type + */ +public class Edge extends GraphEntity { + + //memebers + private String relationshipType; + private int source; + private int destination; + + + //getters & setters + + /** + * @return the edge relationship type + */ + public String getRelationshipType() { + return relationshipType; + } + + /** + * @param relationshipType - the relationship type to be set. + */ + public void setRelationshipType(String relationshipType) { + this.relationshipType = relationshipType; + } + + + /** + * @return The id of the source node + */ + public int getSource() { + return source; + } + + /** + * @param source - The id of the source node to be set + */ + public void setSource(int source) { + this.source = source; + } + + /** + * + * @return the id of the destination node + */ + public int getDestination() { + return destination; + } + + /** + * + * @param destination - The id of the destination node to be set + */ + public void setDestination(int destination) { + this.destination = destination; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Edge)) return false; + if (!super.equals(o)) return false; + Edge edge = (Edge) o; + return source == edge.source && + destination == edge.destination && + Objects.equals(relationshipType, edge.relationshipType); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), relationshipType, source, destination); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Edge{"); + sb.append("relationshipType='").append(relationshipType).append('\''); + sb.append(", source=").append(source); + sb.append(", destination=").append(destination); + sb.append(", id=").append(id); + sb.append(", propertyMap=").append(propertyMap); + sb.append('}'); + return sb.toString(); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/GraphCache.java b/src/main/java/com/redislabs/redisgraph/impl/GraphCache.java new file mode 100644 index 0000000..15239b3 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/GraphCache.java @@ -0,0 +1,50 @@ +package com.redislabs.redisgraph.impl; + +import com.redislabs.redisgraph.RedisGraph; + +/** + * A class to store a local cache in the client, for a specific graph. + * Holds the labels, property names and relationship types + */ +public class GraphCache { + + private final GraphCacheList labels; + private final GraphCacheList propertyNames; + private final GraphCacheList relationshipTypes; + + /** + * + * @param graphId - graph Id + * @param redisGraph - a client to use in the cache, for re-validate it by calling procedures + */ + public GraphCache(String graphId, RedisGraph redisGraph) { + this.labels = new GraphCacheList(graphId, "db.labels", redisGraph); + this.propertyNames = new GraphCacheList(graphId, "db.propertyKeys", redisGraph); + this.relationshipTypes = new GraphCacheList(graphId, "db.relationshipTypes", redisGraph); + } + + /** + * @param index - index of label + * @return requested label + */ + public String getLabel(int index) { + return labels.getCachedData(index); + } + + /** + * @param index index of the relationship type + * @return requested relationship type + */ + public String getRelationshipType(int index) { + return relationshipTypes.getCachedData(index); + } + + /** + * @param index index of property name + * @return requested property + */ + public String getPropertyName(int index) { + + return propertyNames.getCachedData(index); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/GraphCacheList.java b/src/main/java/com/redislabs/redisgraph/impl/GraphCacheList.java new file mode 100644 index 0000000..58b8b43 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/GraphCacheList.java @@ -0,0 +1,72 @@ +package com.redislabs.redisgraph.impl; + +import com.redislabs.redisgraph.Record; +import com.redislabs.redisgraph.RedisGraph; +import com.redislabs.redisgraph.ResultSet; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.locks.ReentrantReadWriteLock; + +/** + * Represents a local cache of list of strings. Holds data from a specific procedure, for a specific graph. + */ +public class GraphCacheList { + + private Object mutex = new Object(); + private final String graphId; + private final String procedure; + private final RedisGraph redisGraph; + private final List data = new CopyOnWriteArrayList<>(); + + + + /** + * + * @param graphId - graph id + * @param procedure - exact procedure command + * @param redisGraph - a client to use in the cache, for re-validate it by calling procedures + */ + public GraphCacheList(String graphId, String procedure, RedisGraph redisGraph) { + this.graphId = graphId; + this.procedure = procedure; + this.redisGraph = redisGraph; + } + + + /** + * A method to return a cached item if it is in the cache, or re-validate the cache if its invalidated + * @param index index of data item + * @return The string value of the specific procedure response, at the given index. + */ + public String getCachedData(int index) { + if (index >= data.size()) { + synchronized (mutex){ + if (index >= data.size()) { + getProcedureInfo(); + } + } + } + String s = data.get(index); + return s; + + } + + /** + * Auxiliary method to parse a procedure result set and refresh the cache + */ + private void getProcedureInfo() { + ResultSet resultSet = redisGraph.callProcedure(graphId, procedure); + List newData = new ArrayList<>(); + int i = 0; + while (resultSet.hasNext()) { + Record record = resultSet.next(); + if(i >= data.size()){ + newData.add(record.getString(0)); + } + i++; + } + data.addAll(newData); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/GraphEntity.java b/src/main/java/com/redislabs/redisgraph/impl/GraphEntity.java new file mode 100644 index 0000000..9357b97 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/GraphEntity.java @@ -0,0 +1,120 @@ +package com.redislabs.redisgraph.impl; + + +import com.redislabs.redisgraph.ResultSet.ResultSetScalarTypes; + +import java.util.*; + + +/** + * This is an abstract class for representing a graph entity. + * A graph entity has an id and a set of properties. The properties are mapped and accessed by their names. + */ +public abstract class GraphEntity { + + + + //members + + protected int id; + protected final Map propertyMap = new HashMap<>(); + + + //setters & getters + + /** + * + * @return entity id + */ + public int getId() { + return id; + } + + /** + * + * @param id - entity id to be set + */ + public void setId(int id) { + this.id = id; + } + + + /** + * Adds a property to the entity, by composing name, type and value to a property object + * @param name + * @param type + * @param value + */ + public void addProperty(String name, ResultSetScalarTypes type, Object value){ + + addProperty(new Property(name, type, value)); + + } + + /** + * Add a property to the entity + * @param property + */ + public void addProperty (Property property){ + + + propertyMap.put(property.getName(), property); + } + + /** + * + * @return number of properties + */ + public int getNumberOfProperties(){ + return propertyMap.size(); + } + + + /** + * + * @param propertyName - property name as lookup key (String) + * @return property object, or null if key is not found + */ + public Property getProperty(String propertyName){ + return propertyMap.get(propertyName); + } + + + /** + * + * @param name - the name of the property to be removed + */ + public void removeProperty(String name){ + + propertyMap.remove(name); + + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof GraphEntity)) return false; + GraphEntity that = (GraphEntity) o; + return id == that.id && + Objects.equals(propertyMap, that.propertyMap); + } + + @Override + public int hashCode() { + return Objects.hash(id, propertyMap); + } + + + /** + * Default toString implementation. + * @return + */ + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("GraphEntity{"); + sb.append("id=").append(id); + sb.append(", propertyMap=").append(propertyMap); + sb.append('}'); + return sb.toString(); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/HeaderImpl.java b/src/main/java/com/redislabs/redisgraph/impl/HeaderImpl.java new file mode 100644 index 0000000..2a7a40f --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/HeaderImpl.java @@ -0,0 +1,95 @@ +package com.redislabs.redisgraph.impl; + +import com.redislabs.redisgraph.Header; +import redis.clients.jedis.util.SafeEncoder; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Query result header interface implementation + */ +public class HeaderImpl implements Header { + + //members + private final List> raw; + private final List schemaTypes = new ArrayList<>(); + private final List schemaNames = new ArrayList<>(); + + + /** + * Parameterized constructor + * A raw representation of a header (query response schema) is a list. + * Each entry in the list is a tuple (list of size 2). + * tuple[0] represents the type of the column, and tuple[1] represents the name of the column. + * + * @param raw - raw representation of a header + */ + public HeaderImpl(List> raw) { + this.raw = raw; + } + + + /** + * @return a list of column names, ordered by they appearance in the query + */ + @Override + public List getSchemaNames() { + if (schemaNames.size() == 0) { + buildSchema(); + } + return schemaNames; + } + + /** + * @return a list of column types, ordered by they appearance in the query + */ + @Override + public List getSchemaTypes() { + if (schemaTypes.size() == 0) { + buildSchema(); + } + return schemaTypes; + } + + /** + * Extracts schema names and types from the raw representation + */ + private void buildSchema() { + for (List tuple : this.raw) { + + //get type + ResultSetColumnTypes type = ResultSetColumnTypes.values()[((Long) tuple.get(0)).intValue()]; + //get text + String text = SafeEncoder.encode((byte[]) tuple.get(1)); + if (type != null) { + schemaTypes.add(type); + schemaNames.add(text); + } + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof HeaderImpl)) return false; + HeaderImpl header = (HeaderImpl) o; + return Objects.equals(getSchemaTypes(), header.getSchemaTypes()) && + Objects.equals(getSchemaNames(), header.getSchemaNames()); + } + + @Override + public int hashCode() { + return Objects.hash(getSchemaTypes(), getSchemaNames()); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("HeaderImpl{"); + sb.append("schemaTypes=").append(schemaTypes); + sb.append(", schemaNames=").append(schemaNames); + sb.append('}'); + return sb.toString(); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/Node.java b/src/main/java/com/redislabs/redisgraph/impl/Node.java new file mode 100644 index 0000000..80b316f --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/Node.java @@ -0,0 +1,70 @@ +package com.redislabs.redisgraph.impl; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * * A class represent an node (graph entity). In addition to the base class id and properties, a node has labels. + */ +public class Node extends GraphEntity { + + //members + final private List labels = new ArrayList<>(); + + /** + * @param label - a label to be add + */ + public void addLabel(String label) { + labels.add(label); + } + + /** + * @param label - a label to be removed + */ + public void removeLabel(String label) { + labels.remove(label); + } + + /** + * @param index - label index + * @return the proprty label + * @throws IndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index >= getNumberOfLabels()}) + */ + public String getLabel(int index) throws IndexOutOfBoundsException{ + return labels.get(index); + } + + /** + * + * @return the number of labels + */ + public int getNumberOfLabels() { + return labels.size(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Node)) return false; + if (!super.equals(o)) return false; + Node node = (Node) o; + return Objects.equals(labels, node.labels); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), labels); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Node{"); + sb.append("labels=").append(labels); + sb.append(", id=").append(id); + sb.append(", propertyMap=").append(propertyMap); + sb.append('}'); + return sb.toString(); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/Property.java b/src/main/java/com/redislabs/redisgraph/impl/Property.java new file mode 100644 index 0000000..7eee7db --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/Property.java @@ -0,0 +1,113 @@ +package com.redislabs.redisgraph.impl; + +import com.redislabs.redisgraph.ResultSet; + +import java.util.Objects; + +/** + * A Graph entity property. Has a name, type, and value + */ +public class Property { + + //members + private String name; + private ResultSet.ResultSetScalarTypes type; + private Object value; + + + /** + * Default constructor + */ + public Property() { + + } + + /** + * Parameterized constructor + * + * @param name + * @param type + * @param value + */ + public Property(String name, ResultSet.ResultSetScalarTypes type, Object value) { + this.name = name; + this.type = type; + this.value = value; + } + + //getters & setters + + /** + * @return property name + */ + public String getName() { + return name; + } + + /** + * @param name - property name to be set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return property type + */ + public ResultSet.ResultSetScalarTypes getType() { + return type; + } + + /** + * @param type property type to be set + */ + public void setType(ResultSet.ResultSetScalarTypes type) { + this.type = type; + } + + + /** + * @return property value + */ + public Object getValue() { + return value; + } + + + /** + * @param value property value to be set + */ + public void setValue(Object value) { + this.value = value; + } + + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Property)) return false; + Property property = (Property) o; + return Objects.equals(name, property.name) && + type == property.type && + Objects.equals(value, property.value); + } + + @Override + public int hashCode() { + return Objects.hash(name, type, value); + } + + /** + * Default toString implementation + * @return + */ + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Property{"); + sb.append("name='").append(name).append('\''); + sb.append(", type=").append(type); + sb.append(", value=").append(value); + sb.append('}'); + return sb.toString(); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/RecordImpl.java b/src/main/java/com/redislabs/redisgraph/impl/RecordImpl.java index cee061a..8aca26c 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/RecordImpl.java +++ b/src/main/java/com/redislabs/redisgraph/impl/RecordImpl.java @@ -1,6 +1,7 @@ package com.redislabs.redisgraph.impl; import java.util.List; +import java.util.Objects; import com.redislabs.redisgraph.Record; @@ -55,9 +56,24 @@ public int size() { } @Override - public String toString() { - return this.values.toString(); + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof RecordImpl)) return false; + RecordImpl record = (RecordImpl) o; + return Objects.equals(header, record.header) && + Objects.equals(values, record.values); } + @Override + public int hashCode() { + return Objects.hash(header, values); + } + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Record{"); + sb.append("values=").append(values); + sb.append('}'); + return sb.toString(); + } } diff --git a/src/main/java/com/redislabs/redisgraph/impl/ResultSetImpl.java b/src/main/java/com/redislabs/redisgraph/impl/ResultSetImpl.java index 3e17805..252636a 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/ResultSetImpl.java +++ b/src/main/java/com/redislabs/redisgraph/impl/ResultSetImpl.java @@ -1,80 +1,276 @@ package com.redislabs.redisgraph.impl; +import com.redislabs.redisgraph.Header; +import com.redislabs.redisgraph.Record; +import com.redislabs.redisgraph.ResultSet; +import com.redislabs.redisgraph.Statistics; +import redis.clients.jedis.util.SafeEncoder; + import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; -import java.util.stream.Collectors; +import java.util.Objects; -import com.redislabs.redisgraph.Record; -import com.redislabs.redisgraph.ResultSet; -import com.redislabs.redisgraph.Statistics; +public class ResultSetImpl implements ResultSet { -import redis.clients.jedis.util.SafeEncoder; + private final Header header; + private final Statistics statistics; + private final List results ; + + private int position = 0; + private final GraphCache graphCache; + + /** + * @param rawResponse the raw representation of response is at most 3 lists of objects. + * The last list is the statistics list. + * @param graphCache, the graph local cache + */ + public ResultSetImpl(List rawResponse, GraphCache graphCache) { + this.graphCache = graphCache; + if (rawResponse.size() != 3) { + + header = parseHeader(new ArrayList<>()); + results = new ArrayList<>(); + statistics = rawResponse.size()> 0 ? parseStatistics(rawResponse.get(rawResponse.size() - 1)) : + parseStatistics(new ArrayList()); + + } else { + + header = parseHeader((List>) rawResponse.get(0)); + results = parseResult((List>) rawResponse.get(1)); + statistics = parseStatistics((List) rawResponse.get(2)); + } + } + + + /** + * + * @param rawResultSet - raw result set representation + * @return parsed result set + */ + private List parseResult(List> rawResultSet) { + List results = new ArrayList<>(); + if (rawResultSet == null || rawResultSet.isEmpty()) { + return results; + } else { + //go over each raw result + for (List row : rawResultSet) { + + List parsedRow = new ArrayList<>(row.size()); + //go over each object in the result + for (int i = 0; i < row.size(); i++) { + //get raw representation of the object + List obj = (List) row.get(i); + //get object type + Header.ResultSetColumnTypes objType = header.getSchemaTypes().get(i); + //deserialize according to type and + switch (objType) { + case COLUMN_NODE: + parsedRow.add(deserializeNode(obj)); + break; + case COLUMN_RELATION: + parsedRow.add(deserializeEdge(obj)); + break; + case COLUMN_SCALAR: { + parsedRow.add(deserializeScalar(obj)); + + } + } + + } + //create new record from deserialized objects + Record record = new RecordImpl(header.getSchemaNames(), parsedRow); + results.add(record); + } + } + return results; + } + + /** + * + * @param rawStatistics raw statistics representation + * @return parsed statistics + */ + private StatisticsImpl parseStatistics(Object rawStatistics) { + return new StatisticsImpl((List) rawStatistics); + } + + + /** + * + * @param rawHeader - raw header representation + * @return parsed header + */ + private HeaderImpl parseHeader(List> rawHeader) { + return new HeaderImpl(rawHeader); + } + + @Override + public Statistics getStatistics() { + return statistics; + } + + @Override + public Header getHeader() { + return header; + } + + + /** + * @param rawNodeData - raw node object in the form of list of object + * rawNodeData.get(0) - id (long) + * rawNodeData.get(1) - a list y which contains the labels of this node. Each entry is a label id from the type of long + * rawNodeData.get(2) - a list which contains the properties of the node. + * @return Node object + */ + private Node deserializeNode(List rawNodeData) { + Node node = new Node(); + deserializeGraphEntityId(node, rawNodeData.get(0)); + List labelsIndices = (List) rawNodeData.get(1); + for (long labelIndex : labelsIndices) { + String label = graphCache.getLabel((int) labelIndex); + node.addLabel(label); + } + deserializeGraphEntityProperties(node, (List>) rawNodeData.get(2)); + + return node; + + } + + /** + * @param graphEntity graph entity + * @param rawEntityId raw representation of entity id to be set to the graph entity + */ + private void deserializeGraphEntityId(GraphEntity graphEntity, Object rawEntityId) { + int id = (int) (long) rawEntityId; + graphEntity.setId(id); + } + + + /** + * @param rawEdgeData - a list of objects + * rawEdgeData[0] - edge id + * rawEdgeData[1] - edge relationship type + * rawEdgeData[2] - edge source + * rawEdgeData[3] - edge destination + * rawEdgeData[4] - edge properties + * @return Edge object + */ + private Edge deserializeEdge(List rawEdgeData) { + Edge edge = new Edge(); + deserializeGraphEntityId(edge, rawEdgeData.get(0)); + + String relationshipType = graphCache.getRelationshipType(((Long) rawEdgeData.get(1)).intValue()); + edge.setRelationshipType(relationshipType); + + edge.setSource((int) (long) rawEdgeData.get(2)); + edge.setDestination((int) (long) rawEdgeData.get(3)); + + deserializeGraphEntityProperties(edge, (List>) rawEdgeData.get(4)); + + return edge; + } + + /** + * @param entity graph entity for adding the properties to + * @param rawProperties raw representation of a list of graph entity properties. Each entry is a list (rawProperty) + * is a raw representation of property, as follows: + * rawProperty.get(0) - property key + * rawProperty.get(1) - property type + * rawProperty.get(2) - property value + */ + void deserializeGraphEntityProperties(GraphEntity entity, List> rawProperties) { + + + for (List rawProperty : rawProperties) { + Property property = new Property(); + property.setName(graphCache.getPropertyName(((Long) rawProperty.get(0)).intValue())); + + //trimmed for getting to value using deserializeScalar + List propertyScalar = rawProperty.subList(1, rawProperty.size()); + property.setType(getScalarTypeFromObject(propertyScalar.get(0))); + property.setValue(deserializeScalar(propertyScalar)); + + entity.addProperty(property); + + } + + } + + /** + * @param rawScalarData - a list of object. list[0] is the scalar type, list[1] is the scalar value + * @return value of the specific scalar type + */ + private Object deserializeScalar(List rawScalarData) { + ResultSetScalarTypes type = getScalarTypeFromObject(rawScalarData.get(0)); + Object obj = rawScalarData.get(1); + switch (type) { + case PROPERTY_NULL: + return null; + case PROPERTY_BOOLEAN: + return Boolean.parseBoolean(SafeEncoder.encode((byte[]) obj)); + case PROPERTY_DOUBLE: + return Double.parseDouble(SafeEncoder.encode((byte[]) obj)); + case PROPERTY_INTEGER: + return (Integer) ((Long) obj).intValue(); + case PROPERTY_STRING: + return SafeEncoder.encode((byte[]) obj); + case PROPERTY_UNKNOWN: + default: + return obj; + } + } + + /** + * Auxiliary function to retrieve scalar types + * + * @param rawScalarType + * @return scalar type + */ + private ResultSetScalarTypes getScalarTypeFromObject(Object rawScalarType) { + return ResultSetScalarTypes.values()[((Long) rawScalarType).intValue()]; + } + + @Override + public boolean hasNext() { + return position < results.size(); + } + + @Override + public Record next() { + if (!hasNext()) + throw new NoSuchElementException(); + return results.get(position++); + } -public class ResultSetImpl implements ResultSet{ - - private final int totalResults; - private final List header; - private final List results; - private final Statistics statistics; - private int position = 0; - - public ResultSetImpl(List resp) { - - this.statistics = new StatisticsImpl((List)resp.get(1)); - - ArrayList> result = (ArrayList>) resp.get(0); - - // Empty result set - if(result == null || result.isEmpty()) { - header = new ArrayList<>(0); - totalResults = 0; - results = new ArrayList<>(0); - } else { - ArrayList headers = (ArrayList)result.get(0); - header = headers.stream().map( String::new).collect(Collectors.toList()); - - // First row is a header row - totalResults = result.size()-1; - results = new ArrayList<>(totalResults); - // Skips last row (runtime info) - for (int i = 1; i <= totalResults; i++) { - ArrayList row = result.get(i); - Record record = new RecordImpl(header, row.stream().map( obj -> { - if(obj instanceof byte[]) { - return SafeEncoder.encode((byte[])obj); - } - return obj; - }).collect(Collectors.toList())); - results.add(record); - } - } - } - - @Override - public List getHeader(){ - return header; - } - - @Override - public boolean hasNext() { - return position < results.size(); - } - - @Override - public Record next() { - if (!hasNext()) - throw new NoSuchElementException(); - return results.get(position++); - } - - @Override - public Statistics getStatistics() { - return statistics; - } - - @Override - public String toString() { - return this.header + "\n" + this.results + "\n" + this.statistics; - } + + @Override + public int size() { + return results.size(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof ResultSetImpl)) return false; + ResultSetImpl resultSet = (ResultSetImpl) o; + return Objects.equals(getHeader(), resultSet.getHeader()) && + Objects.equals(getStatistics(), resultSet.getStatistics()) && + Objects.equals(results, resultSet.results); + } + + @Override + public int hashCode() { + return Objects.hash(getHeader(), getStatistics(), results); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("ResultSetImpl{"); + sb.append("header=").append(header); + sb.append(", statistics=").append(statistics); + sb.append(", results=").append(results); + sb.append('}'); + return sb.toString(); + } } diff --git a/src/main/java/com/redislabs/redisgraph/impl/StatisticsImpl.java b/src/main/java/com/redislabs/redisgraph/impl/StatisticsImpl.java index 6333fd0..68887e1 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/StatisticsImpl.java +++ b/src/main/java/com/redislabs/redisgraph/impl/StatisticsImpl.java @@ -3,20 +3,37 @@ import java.util.EnumMap; import java.util.List; import java.util.Map; +import java.util.Objects; import com.redislabs.redisgraph.Statistics; import redis.clients.jedis.util.SafeEncoder; +/** + * Query result statistics interface implementation + */ public class StatisticsImpl implements Statistics { + //members private final List raw; private final Map statistics; - + + /** + * A raw representation of query exection statistics is a list of strings + * (byte arrays which need to be de-serialized). + * Each string is built in the form of "K:V" where K is statistics label and V is its value. + * @param raw a raw representation of the query execution statistics + */ StatisticsImpl(List raw){ this.raw = raw; this.statistics = new EnumMap<>(Statistics.Label.class); // lazy loaded } - + + + /** + * + * @param label the requested statistic label as key + * @return a string with the value, if key exists, null otherwise + */ @Override public String getStringValue(Statistics.Label label) { return getStatistics().get(label); @@ -24,63 +41,113 @@ public String getStringValue(Statistics.Label label) { private Map getStatistics(){ if(statistics.size() == 0) { - for(byte[] touple : this.raw) { - String text = SafeEncoder.encode(touple); - String[] rowTouple = text.split(":"); - if(rowTouple.length == 2) { - Statistics.Label label = Statistics.Label.getEnum(rowTouple[0]); + for(byte[] tuple : this.raw) { + String text = SafeEncoder.encode(tuple); + String[] rowTuple = text.split(":"); + if(rowTuple.length == 2) { + Statistics.Label label = Statistics.Label.getEnum(rowTuple[0]); if(label != null) { - this.statistics.put( label, rowTouple[1].trim()); + this.statistics.put( label, rowTuple[1].trim()); } } } } return statistics; } - + + /** + * + * @param label the requested statistic label as key + * @return a string with the value, if key exists, 0 otherwise + */ public int getIntValue(Statistics.Label label) { String value = getStringValue(label); return value==null ? 0 : Integer.parseInt(value); } + /** + * + * @return number of nodes created after query execution + */ @Override public int nodesCreated() { return getIntValue(Label.NODES_CREATED); } + /** + * + * @return number of nodes deleted after query execution + */ @Override public int nodesDeleted() { return getIntValue(Label.NODES_DELETED); } + /** + * + * @return number of indices added after query execution + */ @Override public int indicesAdded() { return getIntValue(Label.INDICES_ADDED); } - - + + + /** + * + * @return number of lables added after query execution + */ @Override public int labelsAdded() { return getIntValue(Label.LABELS_ADDED); } + /** + * + * @return number of relationship deleted after query execution + */ @Override public int relationshipsDeleted() { return getIntValue(Label.RELATIONSHIPS_DELETED); } + /** + * + * @return number of relationship created after query execution + */ @Override public int relationshipsCreated() { return getIntValue(Label.RELATIONSHIPS_CREATED); } + /** + * + * @return number of properties set after query execution + */ @Override public int propertiesSet() { return getIntValue(Label.PROPERTIES_SET); } - + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof StatisticsImpl)) return false; + StatisticsImpl that = (StatisticsImpl) o; + return Objects.equals(raw, raw) && + Objects.equals(getStatistics(), that.getStatistics()); + } + + @Override + public int hashCode() { + return Objects.hash(raw, getStatistics()); + } + @Override public String toString() { - return getStatistics().toString(); + final StringBuilder sb = new StringBuilder("StatisticsImpl{"); + sb.append("statistics=").append(getStatistics()); + sb.append('}'); + return sb.toString(); } } diff --git a/src/test/java/com/redislabs/redisgraph/RedisGraphAPITest.java b/src/test/java/com/redislabs/redisgraph/RedisGraphAPITest.java index f11abf5..8d71363 100644 --- a/src/test/java/com/redislabs/redisgraph/RedisGraphAPITest.java +++ b/src/test/java/com/redislabs/redisgraph/RedisGraphAPITest.java @@ -1,136 +1,498 @@ package com.redislabs.redisgraph; + import java.util.Arrays; +import java.util.List; import java.util.NoSuchElementException; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import com.redislabs.redisgraph.impl.Edge; +import com.redislabs.redisgraph.impl.Node; +import com.redislabs.redisgraph.impl.Property; +import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import com.redislabs.redisgraph.Statistics.Label; +import static com.redislabs.redisgraph.Header.ResultSetColumnTypes.*; + public class RedisGraphAPITest { - RedisGraphAPI api; + RedisGraph api; public RedisGraphAPITest() { - api = new RedisGraphAPI("social"); - - /* Dummy call to generate graph so the first deleteGraph() won't fail */ - api.query("CREATE ({name:'roi',age:32})"); } @Before - public void deleteGraph(){ - api.deleteGraph(); + public void createApi(){ + api = new RedisGraph(); + } + @After + public void deleteGraph() { + + api.deleteGraph("social"); + api.close(); } - + + + @Test - public void testCreateNode(){ + public void testCreateNode() { // Create a node - ResultSet result = api.query("CREATE ({name:'roi',age:32})"); - Assert.assertFalse(result.hasNext()); - - try { - result.next(); - Assert.fail(); - }catch(NoSuchElementException e) {} - - Assert.assertEquals(1, result.getStatistics().nodesCreated()); - Assert.assertNull(result.getStatistics().getStringValue(Label.NODES_DELETED)); - Assert.assertNull(result.getStatistics().getStringValue(Label.RELATIONSHIPS_CREATED)); - Assert.assertNull(result.getStatistics().getStringValue(Label.RELATIONSHIPS_DELETED)); - Assert.assertEquals(2, result.getStatistics().propertiesSet()); - Assert.assertNotNull(result.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + ResultSet resultSet = api.query("social", "CREATE ({name:'roi',age:32})"); + + + Assert.assertEquals(1, resultSet.getStatistics().nodesCreated()); + Assert.assertNull(resultSet.getStatistics().getStringValue(Label.NODES_DELETED)); + Assert.assertNull(resultSet.getStatistics().getStringValue(Label.RELATIONSHIPS_CREATED)); + Assert.assertNull(resultSet.getStatistics().getStringValue(Label.RELATIONSHIPS_DELETED)); + Assert.assertEquals(2, resultSet.getStatistics().propertiesSet()); + Assert.assertNotNull(resultSet.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + + + Assert.assertFalse(resultSet.hasNext()); + + try { + resultSet.next(); + Assert.fail(); + } catch (NoSuchElementException e) { + } } @Test - public void testCreateLabeledNode(){ + public void testCreateLabeledNode() { // Create a node with a label - ResultSet result = api.query("CREATE (:human{name:'danny',age:12})"); - Assert.assertFalse(result.hasNext()); - - Assert.assertEquals("1", result.getStatistics().getStringValue(Label.NODES_CREATED)); - Assert.assertEquals("2", result.getStatistics().getStringValue(Label.PROPERTIES_SET)); - Assert.assertNotNull(result.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + ResultSet resultSet = api.query("social", "CREATE (:human{name:'danny',age:12})"); + Assert.assertFalse(resultSet.hasNext()); + Assert.assertEquals("1", resultSet.getStatistics().getStringValue(Label.NODES_CREATED)); + Assert.assertEquals("2", resultSet.getStatistics().getStringValue(Label.PROPERTIES_SET)); + Assert.assertNotNull(resultSet.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); } @Test - public void testConnectNodes(){ + public void testConnectNodes() { // Create both source and destination nodes - Assert.assertNotNull(api.query("CREATE (:person{name:'roi',age:32})")); - Assert.assertNotNull(api.query("CREATE (:person{name:'amit',age:30})")); - - // Connect source and destination nodes. - ResultSet matchResult = api.query("MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)"); - - Assert.assertFalse(matchResult.hasNext()); - Assert.assertNull(matchResult.getStatistics().getStringValue(Label.NODES_CREATED)); - Assert.assertNull(matchResult.getStatistics().getStringValue(Label.PROPERTIES_SET)); - Assert.assertEquals(1, matchResult.getStatistics().relationshipsCreated()); - Assert.assertEquals(0, matchResult.getStatistics().relationshipsDeleted()); - Assert.assertNotNull(matchResult.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'amit',age:30})")); + + // Connect source and destination nodes. + ResultSet resultSet = api.query("social", "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)"); + + Assert.assertFalse(resultSet.hasNext()); + Assert.assertNull(resultSet.getStatistics().getStringValue(Label.NODES_CREATED)); + Assert.assertNull(resultSet.getStatistics().getStringValue(Label.PROPERTIES_SET)); + Assert.assertEquals(1, resultSet.getStatistics().relationshipsCreated()); + Assert.assertEquals(0, resultSet.getStatistics().relationshipsDeleted()); + Assert.assertNotNull(resultSet.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + } + + @Test + public void testDeleteNodes(){ + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'amit',age:30})")); + ResultSet deleteResult = api.query("social", "MATCH (a:person) WHERE (a.name = 'roi') DELETE a"); + + Assert.assertFalse(deleteResult.hasNext()); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.NODES_CREATED)); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.PROPERTIES_SET)); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.NODES_CREATED)); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.RELATIONSHIPS_CREATED)); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.RELATIONSHIPS_DELETED)); + Assert.assertEquals(1, deleteResult.getStatistics().nodesDeleted()); + Assert.assertNotNull(deleteResult.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(api.query("social", "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)")); + deleteResult = api.query("social", "MATCH (a:person) WHERE (a.name = 'roi') DELETE a"); + + Assert.assertFalse(deleteResult.hasNext()); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.NODES_CREATED)); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.PROPERTIES_SET)); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.NODES_CREATED)); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.RELATIONSHIPS_CREATED)); + Assert.assertEquals(1, deleteResult.getStatistics().relationshipsDeleted()); + Assert.assertEquals(1, deleteResult.getStatistics().nodesDeleted()); + + Assert.assertNotNull(deleteResult.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + + + + } + + @Test + public void testDeleteRelationship(){ + + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.assertNotNull(api.query("social", "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)")); + ResultSet deleteResult = api.query("social", "MATCH (a:person)-[e]->() WHERE (a.name = 'roi') DELETE e"); + + Assert.assertFalse(deleteResult.hasNext()); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.NODES_CREATED)); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.PROPERTIES_SET)); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.NODES_CREATED)); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.RELATIONSHIPS_CREATED)); + Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.NODES_DELETED)); + Assert.assertEquals(1, deleteResult.getStatistics().relationshipsDeleted()); + + Assert.assertNotNull(deleteResult.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + } - + + @Test - public void testIndex(){ + public void testIndex() { // Create both source and destination nodes - Assert.assertNotNull(api.query("CREATE (:person{name:'roi',age:32})")); - - ResultSet createIndexResult = api.query("CREATE INDEX ON :person(age)"); - Assert.assertFalse(createIndexResult.hasNext()); - Assert.assertEquals(1, createIndexResult.getStatistics().indicesAdded()); - - ResultSet failCreateIndexResult = api.query("CREATE INDEX ON :person(age1)"); - Assert.assertFalse(failCreateIndexResult.hasNext()); - Assert.assertNull(failCreateIndexResult.getStatistics().getStringValue(Label.INDICES_ADDED)); - Assert.assertEquals(0, failCreateIndexResult.getStatistics().indicesAdded()); + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'roi',age:32})")); + + ResultSet createIndexResult = api.query("social", "CREATE INDEX ON :person(age)"); + Assert.assertFalse(createIndexResult.hasNext()); + Assert.assertEquals(1, createIndexResult.getStatistics().indicesAdded()); + + ResultSet failCreateIndexResult = api.query("social", "CREATE INDEX ON :person(age1)"); + Assert.assertFalse(failCreateIndexResult.hasNext()); + Assert.assertNull(failCreateIndexResult.getStatistics().getStringValue(Label.INDICES_ADDED)); + Assert.assertEquals(0, failCreateIndexResult.getStatistics().indicesAdded()); } + @Test + public void testHeader(){ + + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.assertNotNull(api.query("social", "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)")); + + ResultSet queryResult = api.query("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r, a.age"); + + Assert.assertNotNull(queryResult.getHeader()); + Header header = queryResult.getHeader(); + + List schemaNames = header.getSchemaNames(); + List schemaTypes = header.getSchemaTypes(); + + Assert.assertNotNull(schemaNames); + Assert.assertNotNull(schemaTypes); + + Assert.assertEquals(3, schemaNames.size()); + Assert.assertEquals(3, schemaTypes.size()); + + Assert.assertEquals("a", schemaNames.get(0)); + Assert.assertEquals("r", schemaNames.get(1)); + Assert.assertEquals("a.age", schemaNames.get(2)); + + Assert.assertEquals(COLUMN_NODE, schemaTypes.get(0)); + Assert.assertEquals(COLUMN_RELATION, schemaTypes.get(1)); + Assert.assertEquals(COLUMN_SCALAR, schemaTypes.get(2)); + + } @Test - public void testQuery(){ - - // Create both source and destination nodes - Assert.assertNotNull(api.query("CREATE (:qhuman{name:%s,age:%d})","roi", 32)); - Assert.assertNotNull(api.query("CREATE (:qhuman{name:'amit',age:30})")); - - // Connect source and destination nodes. - Assert.assertNotNull(api.query("MATCH (a:qhuman), (b:qhuman) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)")); - - // Query - ResultSet resultSet = api.query("MATCH (a:qhuman)-[:knows]->(:qhuman) RETURN a"); - - Assert.assertEquals(Arrays.asList("a.age", "a.name"), resultSet.getHeader()); - Assert.assertEquals("[a.age, a.name]\n[[32, roi]]\n" + resultSet.getStatistics(), resultSet.toString()); - - Assert.assertTrue(resultSet.hasNext()); - Assert.assertEquals(0, resultSet.getStatistics().nodesCreated()); - Assert.assertEquals(0, resultSet.getStatistics().nodesDeleted()); - Assert.assertEquals(0, resultSet.getStatistics().labelsAdded()); - Assert.assertEquals(0, resultSet.getStatistics().propertiesSet()); - Assert.assertEquals(0, resultSet.getStatistics().relationshipsCreated()); - Assert.assertEquals(0, resultSet.getStatistics().relationshipsDeleted()); - Assert.assertNotNull(resultSet.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); - - Record record = resultSet.next(); - Assert.assertEquals( Arrays.asList("a.age", "a.name"), record.keys()); - Assert.assertEquals( Arrays.asList(32L, "roi"), record.values()); - Assert.assertTrue(record.containsKey("a.name")); - Assert.assertEquals( 2, record.size()); - Assert.assertEquals( "[32, roi]", record.toString()); - - Assert.assertEquals( "roi", record.getString(1)); - Assert.assertEquals( "32", record.getString(0)); - Assert.assertEquals( 32L, ((Long)record.getValue(0)).longValue()); - Assert.assertEquals( 32L, ((Long)record.getValue("a.age")).longValue()); + public void testRecord(){ + String name = "roi"; + int age = 32; + double doubleValue = 3.14; + boolean boolValue = true; + + String place = "TLV"; + int since = 2000; + + + + Property nameProperty = new Property("name", ResultSet.ResultSetScalarTypes.PROPERTY_STRING, name); + Property ageProperty = new Property("age", ResultSet.ResultSetScalarTypes.PROPERTY_INTEGER, age); + Property doubleProperty = new Property("doubleValue", ResultSet.ResultSetScalarTypes.PROPERTY_DOUBLE, doubleValue); + Property trueBooleanProperty = new Property("boolValue", ResultSet.ResultSetScalarTypes.PROPERTY_BOOLEAN, true); + Property falseBooleanProperty = new Property("boolValue", ResultSet.ResultSetScalarTypes.PROPERTY_BOOLEAN, false); + Property nullProperty = new Property("nullValue", ResultSet.ResultSetScalarTypes.PROPERTY_NULL, null); + + Property placeProperty = new Property("place", ResultSet.ResultSetScalarTypes.PROPERTY_STRING, place); + Property sinceProperty = new Property("since", ResultSet.ResultSetScalarTypes.PROPERTY_INTEGER, since); + + Node expectedNode = new Node(); + expectedNode.setId(0); + expectedNode.addLabel("person"); + expectedNode.addProperty(nameProperty); + expectedNode.addProperty(ageProperty); + expectedNode.addProperty(doubleProperty); + expectedNode.addProperty(trueBooleanProperty); + expectedNode.addProperty(nullProperty); + + Edge expectedEdge = new Edge(); + expectedEdge.setId(0); + expectedEdge.setSource(0); + expectedEdge.setDestination(1); + expectedEdge.setRelationshipType("knows"); + expectedEdge.addProperty(placeProperty); + expectedEdge.addProperty(sinceProperty); + expectedEdge.addProperty(doubleProperty); + expectedEdge.addProperty(falseBooleanProperty); + expectedEdge.addProperty(nullProperty); + + + + Assert.assertNotNull(api.query("social", "CREATE (:person{name:%s',age:%d, doubleValue:%f, boolValue:%b, nullValue:null})", name, age, doubleValue, boolValue)); + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.assertNotNull(api.query("social", "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') " + + "CREATE (a)-[:knows{place:'TLV', since:2000,doubleValue:3.14, boolValue:false, nullValue:null}]->(b)")); + + ResultSet resultSet = api.query("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r, " + + "a.name, a.age, a.doubleValue, a.boolValue, a.nullValue, " + + "r.place, r.since, r.doubleValue, r.boolValue, r.nullValue"); + Assert.assertNotNull(resultSet); + + + Assert.assertEquals(0, resultSet.getStatistics().nodesCreated()); + Assert.assertEquals(0, resultSet.getStatistics().nodesDeleted()); + Assert.assertEquals(0, resultSet.getStatistics().labelsAdded()); + Assert.assertEquals(0, resultSet.getStatistics().propertiesSet()); + Assert.assertEquals(0, resultSet.getStatistics().relationshipsCreated()); + Assert.assertEquals(0, resultSet.getStatistics().relationshipsDeleted()); + Assert.assertNotNull(resultSet.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); + + + Assert.assertEquals(1, resultSet.size()); + Assert.assertTrue(resultSet.hasNext()); + Record record = resultSet.next(); + Assert.assertFalse(resultSet.hasNext()); + + Node node = record.getValue(0); + Assert.assertNotNull(node); + + Assert.assertEquals(expectedNode, node); + + node = record.getValue("a"); + Assert.assertEquals(expectedNode, node); + + Edge edge = record.getValue(1); + Assert.assertNotNull(edge); + Assert.assertEquals(expectedEdge, edge); + + edge = record.getValue("r"); + Assert.assertEquals(expectedEdge, edge); + + Assert.assertEquals(Arrays.asList("a", "r", "a.name", "a.age", "a.doubleValue", "a.boolValue", "a.nullValue", + "r.place", "r.since", "r.doubleValue", "r.boolValue", "r.nullValue"), record.keys()); + + Assert.assertEquals(Arrays.asList(expectedNode, expectedEdge, + name, age, doubleValue, true, null, + place, since, doubleValue, false, null), + record.values()); + + + Assert.assertEquals( "roi", record.getString(2)); + Assert.assertEquals( "32", record.getString(3)); + Assert.assertEquals( 32L, ((Integer)(record.getValue(3))).longValue()); + Assert.assertEquals( 32L, ((Integer)record.getValue("a.age")).longValue()); Assert.assertEquals( "roi", record.getString("a.name")); - Assert.assertEquals( "32", record.getString("a.age")); + Assert.assertEquals( "32", record.getString("a.age")); + + } + + + @Test + public void tinyTestMultiThread(){ + ResultSet resultSet = api.query("social", "CREATE ({name:'roi',age:32})"); + api.query("social", "MATCH (a:person) RETURN a"); + for (int i =0; i < 10000; i++){ + List resultSets = IntStream.range(0,16).parallel(). + mapToObj( + j-> api.query("social", "MATCH (a:person) RETURN a")). + collect(Collectors.toList()); + + } + + } + + @Test + public void testMultiThread(){ + + Assert.assertNotNull(api.query("social", "CREATE (:person {name:'roi', age:32})-[:knows]->(:person {name:'amit',age:30}) ")); + + List resultSets = IntStream.range(0,16).parallel(). + mapToObj(i-> api.query("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r, a.age")). + collect(Collectors.toList()); + + Property nameProperty = new Property("name", ResultSet.ResultSetScalarTypes.PROPERTY_STRING, "roi"); + Property ageProperty = new Property("age", ResultSet.ResultSetScalarTypes.PROPERTY_INTEGER, 32); + Property lastNameProperty =new Property("lastName", ResultSet.ResultSetScalarTypes.PROPERTY_STRING, "a"); + + Node expectedNode = new Node(); + expectedNode.setId(0); + expectedNode.addLabel("person"); + expectedNode.addProperty(nameProperty); + expectedNode.addProperty(ageProperty); + + + Edge expectedEdge = new Edge(); + expectedEdge.setId(0); + expectedEdge.setSource(0); + expectedEdge.setDestination(1); + expectedEdge.setRelationshipType("knows"); + + + for (ResultSet resultSet : resultSets){ + Assert.assertNotNull(resultSet.getHeader()); + Header header = resultSet.getHeader(); + List schemaNames = header.getSchemaNames(); + List schemaTypes = header.getSchemaTypes(); + Assert.assertNotNull(schemaNames); + Assert.assertNotNull(schemaTypes); + Assert.assertEquals(3, schemaNames.size()); + Assert.assertEquals(3, schemaTypes.size()); + Assert.assertEquals("a", schemaNames.get(0)); + Assert.assertEquals("r", schemaNames.get(1)); + Assert.assertEquals("a.age", schemaNames.get(2)); + Assert.assertEquals(COLUMN_NODE, schemaTypes.get(0)); + Assert.assertEquals(COLUMN_RELATION, schemaTypes.get(1)); + Assert.assertEquals(COLUMN_SCALAR, schemaTypes.get(2)); + Assert.assertEquals(1, resultSet.size()); + Assert.assertTrue(resultSet.hasNext()); + Record record = resultSet.next(); + Assert.assertFalse(resultSet.hasNext()); + Assert.assertEquals(Arrays.asList("a", "r", "a.age"), record.keys()); + Assert.assertEquals(Arrays.asList(expectedNode, expectedEdge, 32), record.values()); + } + + //test for update in local cache + expectedNode.removeProperty("name"); + expectedNode.removeProperty("age"); + expectedNode.addProperty(lastNameProperty); + expectedNode.removeLabel("person"); + expectedNode.addLabel("worker"); + expectedNode.setId(2); + + + expectedEdge.setRelationshipType("worksWith"); + expectedEdge.setSource(2); + expectedEdge.setDestination(3); + expectedEdge.setId(1); + + Assert.assertNotNull(api.query("social", "CREATE (:worker{lastName:'a'})")); + Assert.assertNotNull(api.query("social", "CREATE (:worker{lastName:'b'})")); + Assert.assertNotNull(api.query("social", "MATCH (a:worker), (b:worker) WHERE (a.lastName = 'a' AND b.lastName='b') CREATE (a)-[:worksWith]->(b)")); + + resultSets = IntStream.range(0,16).parallel(). + mapToObj(i-> api.query("social", "MATCH (a:worker)-[r:worksWith]->(b:worker) RETURN a,r")). + collect(Collectors.toList()); + + for (ResultSet resultSet : resultSets){ + Assert.assertNotNull(resultSet.getHeader()); + Header header = resultSet.getHeader(); + List schemaNames = header.getSchemaNames(); + List schemaTypes = header.getSchemaTypes(); + Assert.assertNotNull(schemaNames); + Assert.assertNotNull(schemaTypes); + Assert.assertEquals(2, schemaNames.size()); + Assert.assertEquals(2, schemaTypes.size()); + Assert.assertEquals("a", schemaNames.get(0)); + Assert.assertEquals("r", schemaNames.get(1)); + Assert.assertEquals(COLUMN_NODE, schemaTypes.get(0)); + Assert.assertEquals(COLUMN_RELATION, schemaTypes.get(1)); + Assert.assertEquals(1, resultSet.size()); + Assert.assertTrue(resultSet.hasNext()); + Record record = resultSet.next(); + Assert.assertFalse(resultSet.hasNext()); + Assert.assertEquals(Arrays.asList("a", "r"), record.keys()); + Assert.assertEquals(Arrays.asList(expectedNode, expectedEdge), record.values()); + } + } + + + @Test + public void testAdditionToProcedures(){ + + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(api.query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.assertNotNull(api.query("social", "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)")); + + + List resultSets = IntStream.range(0,16).parallel(). + mapToObj(i-> api.query("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r")). + collect(Collectors.toList()); + + //expected objects init + Property nameProperty = new Property("name", ResultSet.ResultSetScalarTypes.PROPERTY_STRING, "roi"); + Property ageProperty = new Property("age", ResultSet.ResultSetScalarTypes.PROPERTY_INTEGER, 32); + Property lastNameProperty =new Property("lastName", ResultSet.ResultSetScalarTypes.PROPERTY_STRING, "a"); + + Node expectedNode = new Node(); + expectedNode.setId(0); + expectedNode.addLabel("person"); + expectedNode.addProperty(nameProperty); + expectedNode.addProperty(ageProperty); + + + Edge expectedEdge = new Edge(); + expectedEdge.setId(0); + expectedEdge.setSource(0); + expectedEdge.setDestination(1); + expectedEdge.setRelationshipType("knows"); + + + ResultSet resultSet = api.query("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r"); + Assert.assertNotNull(resultSet.getHeader()); + Header header = resultSet.getHeader(); + List schemaNames = header.getSchemaNames(); + List schemaTypes = header.getSchemaTypes(); + Assert.assertNotNull(schemaNames); + Assert.assertNotNull(schemaTypes); + Assert.assertEquals(2, schemaNames.size()); + Assert.assertEquals(2, schemaTypes.size()); + Assert.assertEquals("a", schemaNames.get(0)); + Assert.assertEquals("r", schemaNames.get(1)); + Assert.assertEquals(COLUMN_NODE, schemaTypes.get(0)); + Assert.assertEquals(COLUMN_RELATION, schemaTypes.get(1)); + Assert.assertEquals(1, resultSet.size()); + Assert.assertTrue(resultSet.hasNext()); + Record record = resultSet.next(); + Assert.assertFalse(resultSet.hasNext()); + Assert.assertEquals(Arrays.asList("a", "r"), record.keys()); + Assert.assertEquals(Arrays.asList(expectedNode, expectedEdge), record.values()); + + //test for local cache updates + + expectedNode.removeProperty("name"); + expectedNode.removeProperty("age"); + expectedNode.addProperty(lastNameProperty); + expectedNode.removeLabel("person"); + expectedNode.addLabel("worker"); + expectedNode.setId(2); + expectedEdge.setRelationshipType("worksWith"); + expectedEdge.setSource(2); + expectedEdge.setDestination(3); + expectedEdge.setId(1); + Assert.assertNotNull(api.query("social", "CREATE (:worker{lastName:'a'})")); + Assert.assertNotNull(api.query("social", "CREATE (:worker{lastName:'b'})")); + Assert.assertNotNull(api.query("social", "MATCH (a:worker), (b:worker) WHERE (a.lastName = 'a' AND b.lastName='b') CREATE (a)-[:worksWith]->(b)")); + resultSet = api.query("social", "MATCH (a:worker)-[r:worksWith]->(b:worker) RETURN a,r"); + Assert.assertNotNull(resultSet.getHeader()); + header = resultSet.getHeader(); + schemaNames = header.getSchemaNames(); + schemaTypes = header.getSchemaTypes(); + Assert.assertNotNull(schemaNames); + Assert.assertNotNull(schemaTypes); + Assert.assertEquals(2, schemaNames.size()); + Assert.assertEquals(2, schemaTypes.size()); + Assert.assertEquals("a", schemaNames.get(0)); + Assert.assertEquals("r", schemaNames.get(1)); + Assert.assertEquals(COLUMN_NODE, schemaTypes.get(0)); + Assert.assertEquals(COLUMN_RELATION, schemaTypes.get(1)); + Assert.assertEquals(1, resultSet.size()); + Assert.assertTrue(resultSet.hasNext()); + record = resultSet.next(); + Assert.assertFalse(resultSet.hasNext()); + Assert.assertEquals(Arrays.asList("a", "r"), record.keys()); + Assert.assertEquals(Arrays.asList(expectedNode, expectedEdge), record.values()); + } - + + @Test - public void testEscapedQuery(){ - Assert.assertNotNull(api.query("CREATE (:escaped{s1:%s,s2:%s})","S\"\'", "S\\'\\\"")); - Assert.assertNotNull(api.query("MATCH (n) where n.s1=%s and n.s2=%s RETURN n", "S\"\'", "S\\'\\\"")); - Assert.assertNotNull(api.query("MATCH (n) where n.s1='S\"\\'' RETURN n")); + 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")); } }