diff --git a/.circleci.settings.xml b/.circleci.settings.xml index 709cbf4..b3e253a 100644 --- a/.circleci.settings.xml +++ b/.circleci.settings.xml @@ -2,12 +2,12 @@ ossrh - ${env.OSSRH_USERNMAE} - ${env.OSSRH_PASSWORD} + ${env.OSSH_USERNAME} + ${env.OSSH_PASSWORD} gpg.passphrase ${env.GPG_PASSPHRASE} - \ No newline at end of file + diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index abebfaf..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,64 +0,0 @@ -# Java Maven CircleCI 2.0 configuration file -# -# Check https://circleci.com/docs/2.0/language-java/ for more details -# -version: 2 -jobs: - build: - docker: - - image: circleci/openjdk:8u171-jdk - - - image: redislabs/redisgraph:2.0-edge - port: 6379:6379 - - working_directory: ~/repo - - environment: - # Customize the JVM maximum heap limit - MAVEN_OPTS: -Xmx3200m - - steps: - - - checkout - - # Download and cache dependencies - - restore_cache: - keys: - - v1-dependencies-{{ checksum "pom.xml" }} - # fallback to using the latest cache if no exact match is found - - v1-dependencies- - - - run: mvn dependency:go-offline - - - save_cache: - paths: - - ~/.m2 - key: v1-dependencies-{{ checksum "pom.xml" }} - - # run tests! - # - run: mvn pmd:check - - - run: mvn integration-test - - - run: mvn cobertura:cobertura - - - run: bash <(curl -s https://codecov.io/bash) -t ${CODECOV_TOKEN} - - - run: mvn -s .circleci.settings.xml -DskipTests deploy - -workflows: - version: 2 - commit: - jobs: - - build - nightly: - triggers: - - schedule: - cron: "0 0 * * *" - filters: - branches: - only: - - master - jobs: - - build - diff --git a/.deepsource.toml b/.deepsource.toml new file mode 100644 index 0000000..424f975 --- /dev/null +++ b/.deepsource.toml @@ -0,0 +1,8 @@ +version = 1 + +[[analyzers]] +name = "java" +enabled = true + + [analyzers.meta] + runtime_version = "8" diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ac6621f --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/release-drafter-config.yml b/.github/release-drafter-config.yml new file mode 100644 index 0000000..0eb1ee3 --- /dev/null +++ b/.github/release-drafter-config.yml @@ -0,0 +1,21 @@ +name-template: 'Version $NEXT_PATCH_VERSION' +tag-template: 'v$NEXT_PATCH_VERSION' +categories: + - title: 'Features' + labels: + - 'feature' + - 'enhancement' + - title: 'Bug Fixes' + labels: + - 'fix' + - 'bugfix' + - 'bug' + - title: 'Maintenance' + label: 'chore' +change-template: '- $TITLE (#$NUMBER)' +exclude-labels: + - 'skip-changelog' +template: | + ## Changes + + $CHANGES diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000..bf5c325 --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,41 @@ +name: "CodeQL" + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + schedule: + - cron: "56 20 * * 3" + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + + strategy: + fail-fast: false + matrix: + language: [ java ] + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + queries: +security-and-quality + + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 + with: + category: "/language:${{ matrix.language }}" diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml new file mode 100644 index 0000000..ec2d88b --- /dev/null +++ b/.github/workflows/release-drafter.yml @@ -0,0 +1,19 @@ +name: Release Drafter + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - master + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v5 + with: + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + config-name: release-drafter-config.yml + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/version-and-release.yml b/.github/workflows/version-and-release.yml new file mode 100644 index 0000000..255c634 --- /dev/null +++ b/.github/workflows/version-and-release.yml @@ -0,0 +1,46 @@ +name: Release + +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - name: get version from tag + id: get_version + run: | + realversion="${GITHUB_REF/refs\/tags\//}" + realversion="${realversion//v/}" + echo "::set-output name=VERSION::$realversion" + + - name: Set up publishing to maven central + uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'adopt' + server-id: ossrh + server-username: MAVEN_USERNAME + server-password: MAVEN_PASSWORD + + - name: mvn versions + run: mvn versions:set -DnewVersion=${{ steps.get_version.outputs.VERSION }} + + - name: Install gpg key + run: | + cat <(echo -e "${{ secrets.OSSH_GPG_SECRET_KEY }}") | gpg --batch --import + gpg --list-secret-keys --keyid-format LONG + + - name: Publish + run: | + mvn --no-transfer-progress \ + --batch-mode \ + -Dgpg.passphrase='${{ secrets.OSSH_GPG_SECRET_KEY_PASSWORD }}' \ + -DskipTests deploy -P release + env: + MAVEN_USERNAME: ${{secrets.OSSH_USERNAME}} + MAVEN_PASSWORD: ${{secrets.OSSH_TOKEN}} diff --git a/README.md b/README.md index 8ae38a7..5b492ff 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,19 @@ -[![license](https://img.shields.io/github/license/RedisGraph/JRedisGraph.svg)](https://github.com/RedisGraph/JRedisGraph) -[![CircleCI](https://circleci.com/gh/RedisGraph/JRedisGraph/tree/master.svg?style=svg)](https://circleci.com/gh/RedisGraph/JRedisGraph/tree/master) -[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.redislabs/jredisgraph/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.redislabs/jredisgraph) +[![license](https://img.shields.io/github/license/RedisGraph/JRedisGraph.svg)](https://github.com/RedisGraph/JRedisGraph/blob/master/LICENSE) [![GitHub issues](https://img.shields.io/github/release/RedisGraph/JRedisGraph.svg)](https://github.com/RedisGraph/JRedisGraph/releases/latest) +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.redislabs/jredisgraph/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.redislabs/jredisgraph) [![Javadocs](https://www.javadoc.io/badge/com.redislabs/jredisgraph.svg)](https://www.javadoc.io/doc/com.redislabs/jredisgraph) [![Codecov](https://codecov.io/gh/RedisGraph/JRedisGraph/branch/master/graph/badge.svg)](https://codecov.io/gh/RedisGraph/JRedisGraph) +[![Known Vulnerabilities](https://snyk.io/test/github/RedisGraph/JRedisGraph/badge.svg?targetFile=pom.xml)](https://snyk.io/test/github/RedisGraph/JRedisGraph?targetFile=pom.xml) # JRedisGraph +[![Forum](https://img.shields.io/badge/Forum-RedisGraph-blue)](https://forum.redislabs.com/c/modules/redisgraph) +[![Discord](https://img.shields.io/discord/697882427875393627?style=flat-square)](https://discord.gg/gWBRT6P) + RedisGraph Java client +## Deprecation notice + +As of [Jedis](https://github.com/redis/jedis) version 4.2.0, this library is deprecated. Its features have been merged into Jedis. Please either install it from [maven](https://mvnrepository.com/artifact/redis.clients/jedis) or [the repo](https://github.com/redis/jedis). ### Official Releases @@ -16,14 +22,12 @@ RedisGraph Java client com.redislabs jredisgraph - 1.0.4 + 2.5.1 ``` ### Snapshots -To be used with RedisGraph 2.0 (not officially released) - ```xml @@ -40,33 +44,81 @@ and com.redislabs jredisgraph - 2.0.0-SNAPSHOT + 2.6.0-SNAPSHOT ``` -# Example: Using the Java Client - +## Example: Using the Java Client ```java package com.redislabs.redisgraph; -public class RedisGraphExample { - public static void main(String[] args) { - - RedisGraphAPI api = new RedisGraphAPI("social"); - - api.query("CREATE (:person{name:'roi',age:32})"); - api.query("CREATE (:person{name:%s,age:%d})", "amit", 30); +import com.redislabs.redisgraph.graph_entities.Edge; +import com.redislabs.redisgraph.graph_entities.Node; +import com.redislabs.redisgraph.graph_entities.Path; +import com.redislabs.redisgraph.impl.api.RedisGraph; - api.query("MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)"); +import java.util.List; - ResultSet resultSet = api.query("MATCH (a:person)-[:knows]->(b:person) RETURN a, b"); - - while(resultSet.hasNext()){ - Record record = resultSet.next(); - System.out.println(record.getString("a.name")); - } - } +public class RedisGraphExample { + public static void main(String[] args) { + // general context api. Not bound to graph key or connection + RedisGraph graph = new RedisGraph(); + + Map params = new HashMap<>(); + params.put("age", 30); + params.put("name", "amit"); + + // send queries to a specific graph called "social" + graph.query("social","CREATE (:person{name:'roi',age:32})"); + graph.query("social","CREATE (:person{name:$name,age:$age})", params); + graph.query("social","MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)"); + + ResultSet resultSet = graph.query("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a, r, b"); + while(resultSet.hasNext()) { + Record record = resultSet.next(); + // get values + Node a = record.getValue("a"); + Edge r = record.getValue("r"); + + //print record + System.out.println(record.toString()); + } + + resultSet = graph.query("social", "MATCH p = (:person)-[:knows]->(:person) RETURN p"); + while(resultSet.hasNext()) { + Record record = resultSet.next(); + Path p = record.getValue("p"); + + // More path API at Javadoc. + System.out.println(p.nodeCount()); + } + + // delete graph + graph.deleteGraph("social"); + + // get connection context - closable object + try(RedisGraphContext context = graph.getContext()) { + context.query("contextSocial","CREATE (:person{name:'roi',age:32})"); + context.query("social","CREATE (:person{name:$name,age:$age})", params); + context.query("contextSocial", "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)"); + // WATCH/MULTI/EXEC + context.watch("contextSocial"); + RedisGraphTransaction t = context.multi(); + t.query("contextSocial", "MATCH (a:person)-[r:knows]->(b:person{name:$name,age:$age}) RETURN a, r, b", params); + // support for Redis/Jedis native commands in transaction + t.set("x", "1"); + t.get("x"); + // get multi/exec results + List execResults = t.exec(); + System.out.println(execResults.toString()); + + context.deleteGraph("contextSocial"); + } + } } ``` + +## License +[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FRedisGraph%2FJRedisGraph.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FRedisGraph%2FJRedisGraph?ref=badge_large) diff --git a/pom.xml b/pom.xml index 1bd6487..fd096c0 100644 --- a/pom.xml +++ b/pom.xml @@ -3,14 +3,14 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - + com.redislabs jredisgraph - 2.0.0-SNAPSHOT - + 2.6.0-SNAPSHOT + JRedisGraph Official client for Redis-Graph - https://oss.redislabs.com/redisgraph + https://redisgraph.io RedisLabs @@ -18,16 +18,16 @@ CircleCI - https://circleci.com/gh/RedisLabs/JRedisGraph + https://circleci.com/gh/RedisGraph/JRedisGraph - https://github.com/RedisLabs/JRedisGraph/issues + https://github.com/RedisGraph/JRedisGraph/issues Github - https://github.com/RedisLabs/JRedisGraph - scm:git:git://github.com/RedisLabs/JRedisGraph.git - scm:git:git@github.com:RedisLabs/JRedisGraph.git + https://github.com/RedisGraph/JRedisGraph + scm:git:git://github.com/RedisGraph/JRedisGraph.git + scm:git:git@github.com:RedisGraph/JRedisGraph.git @@ -46,30 +46,43 @@ repo - + - org.apache.commons - commons-pool2 - 2.0 + redis.clients + jedis + 3.9.0 + + + org.apache.commons + commons-text + 1.10.0 junit junit - 4.12 + 4.13.2 test - redis.clients - jedis - 3.0.1 + org.slf4j + slf4j-simple + 1.6.6 + test + + org.junit.jupiter + junit-jupiter + 5.7.1 + test + - org.apache.commons - commons-text - 1.6 + nl.jqno.equalsverifier + equalsverifier + 3.1.10 + test - + 8 8 @@ -90,24 +103,23 @@ - org.codehaus.mojo - cobertura-maven-plugin - 2.7 - - - html - xml - - - - - - maven-compiler-plugin - 3.0 - - 1.8 - 1.8 - + org.jacoco + jacoco-maven-plugin + 0.8.5 + + + + prepare-agent + + + + report + test + + report + + + org.sonatype.plugins @@ -159,7 +171,13 @@ org.apache.maven.plugins maven-gpg-plugin - 1.5 + 3.0.1 + + + --pinentry-mode + loopback + + sign-artifacts @@ -170,10 +188,11 @@ + org.apache.maven.plugins maven-pmd-plugin - 3.11.0 + 3.12.0 diff --git a/src/main/java/META-INF/MANIFEST.MF b/src/main/java/META-INF/MANIFEST.MF index 425ebf5..7356ba4 100644 --- a/src/main/java/META-INF/MANIFEST.MF +++ b/src/main/java/META-INF/MANIFEST.MF @@ -1,3 +1,3 @@ -Manifest-Version: 1.0 -Main-Class: com.redislabs.redisgraph.RedisGraphAPI - +Manifest-Version: 1.0 +Main-Class: com.redislabs.redisgraph.RedisGraph + diff --git a/src/main/java/com/redislabs/redisgraph/Header.java b/src/main/java/com/redislabs/redisgraph/Header.java index 43cf8b3..a8156aa 100644 --- a/src/main/java/com/redislabs/redisgraph/Header.java +++ b/src/main/java/com/redislabs/redisgraph/Header.java @@ -3,16 +3,16 @@ import java.util.List; /** - * Query response header interface. Represents the response schame (column names and types) + * Query response header interface. Represents the response schema (column names and types) */ public interface Header { - public enum ResultSetColumnTypes { + enum ResultSetColumnTypes { COLUMN_UNKNOWN, COLUMN_SCALAR, COLUMN_NODE, - COLUMN_RELATION; + COLUMN_RELATION } diff --git a/src/main/java/com/redislabs/redisgraph/RedisGraph.java b/src/main/java/com/redislabs/redisgraph/RedisGraph.java index 9c0871d..f5c9a62 100644 --- a/src/main/java/com/redislabs/redisgraph/RedisGraph.java +++ b/src/main/java/com/redislabs/redisgraph/RedisGraph.java @@ -1,172 +1,118 @@ package com.redislabs.redisgraph; -import com.redislabs.redisgraph.impl.graph_cache.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<>(); +import java.util.List; +import java.util.Map; +import redis.clients.jedis.Jedis; +public interface RedisGraph extends Closeable { - 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))); + public static RedisGraph with(Jedis jedis) { + return new com.redislabs.redisgraph.impl.api.RedisGraph(jedis); } /** - * Creates a client running on the local machine - + * Execute a Cypher query. + * @param graphId a graph to perform the query on + * @param query Cypher query + * @return a result set */ - public RedisGraph() { - this("localhost", 6379); - } + ResultSet query(String graphId, String query); /** - * Creates a client running on the specific host/post - * - * @param host Redis host - * @param port Redis port + * Execute a Cypher read-only query. + * @param graphId a graph to perform the query on + * @param query Cypher query + * @return a result set */ - public RedisGraph(String host, int port) { - this( new JedisPool(host, port)); - } + ResultSet readOnlyQuery(String graphId, String query); /** - * Creates a client using provided Jedis pool - * - * @param jedis bring your own Jedis pool + * Execute a Cypher query with timeout. + * @param graphId a graph to perform the query on + * @param query Cypher query + * @param timeout + * @return a result set */ - public RedisGraph( Pool jedis) { - - this.client = jedis; - } + ResultSet query(String graphId, String query, long timeout); - @Override - public void close(){ - this.client.close(); - } + /** + * 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 * @param query Cypher query * @param args * @return a result set + * @deprecated use {@link #query(String, String, Map)} instead. */ - 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)); + @Deprecated + ResultSet query(String graphId, String query, Object ...args); - } /** - * 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 + * Executes a cypher 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 callProcedure(String graphId, String procedure ){ - return callProcedure(graphId, procedure, new ArrayList<>(), new HashMap<>()); - } - + ResultSet query(String graphId, String query, Map params); /** - * 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 + * 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 callProcedure(String graphId, String procedure, List args ){ - return callProcedure(graphId, procedure, args, new HashMap<>()); - } - + ResultSet readOnlyQuery(String graphId, String query, Map params); /** - * Deletes the entire graph - * - * @return delete running time statistics + * Executes a cypher 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. */ - public String deleteGraph(String graphId) { - //clear local state - graphCaches.remove(graphId); - try (Jedis conn = getConnection()) { - return sendCommand(conn, Command.DELETE, graphId).getBulkReply(); - } - - } - + ResultSet query(String graphId, String query, Map params, long timeout); /** - * 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 + * 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. */ - private BinaryClient sendCommand(Jedis conn, ProtocolCommand provider, String ...args) { - BinaryClient binaryClient = conn.getClient(); - binaryClient.sendCommand(provider, args); - return binaryClient; - } - + ResultSet readOnlyQuery(String graphId, String query, Map params, long timeout); /** - * Sends the command with --COMPACT flag - * @param conn - connection - * @param provider - command type - * @param args - command arguments - * @return + * 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 */ - 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(); - } + ResultSet callProcedure(String graphId, String procedure); + /** + * 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 + */ + ResultSet callProcedure(String graphId, String procedure, List args); /** * Invoke a stored procedure @@ -174,17 +120,17 @@ private Jedis getConnection() { * @param procedure - procedure to execute * @param args - procedure arguments * @param kwargs - procedure output arguments - * @return + * @return result set with the procedure data */ - 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()); - } + ResultSet callProcedure(String graphId, String procedure, List args , Map> kwargs); + + /** + * Deletes the entire graph + * @param graphId graph to delete + * @return delete running time statistics + */ + String deleteGraph(String graphId); + + @Override + void close(); } diff --git a/src/main/java/com/redislabs/redisgraph/RedisGraphContext.java b/src/main/java/com/redislabs/redisgraph/RedisGraphContext.java new file mode 100644 index 0000000..44d6319 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/RedisGraphContext.java @@ -0,0 +1,38 @@ +package com.redislabs.redisgraph; + +import redis.clients.jedis.Jedis; + +public interface RedisGraphContext extends RedisGraph { + + + /** + * Returns implementing class connection context + * @return Jedis connection + */ + Jedis getConnectionContext(); + + /** + * Returns a Redis transactional object, over the connection context, with graph API capabilities + * @return Redis transactional object, over the connection context, with graph API capabilities + */ + RedisGraphTransaction multi(); + + /** + * Returns a Redis pipeline object, over the connection context, with graph API capabilities + * @return Redis pipeline object, over the connection context, with graph API capabilities + */ + RedisGraphPipeline pipelined(); + + /** + * Perform watch over given Redis keys + * @param keys + * @return "OK" + */ + String watch(String... keys); + + /** + * Removes watch from all keys + * @return + */ + String unwatch(); +} diff --git a/src/main/java/com/redislabs/redisgraph/RedisGraphContextGenerator.java b/src/main/java/com/redislabs/redisgraph/RedisGraphContextGenerator.java new file mode 100644 index 0000000..fe3718f --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/RedisGraphContextGenerator.java @@ -0,0 +1,11 @@ +package com.redislabs.redisgraph; + +public interface RedisGraphContextGenerator extends RedisGraph { + + /** + * Generate a connection bounded api + * @return a connection bounded api + */ + RedisGraphContext getContext(); + +} diff --git a/src/main/java/com/redislabs/redisgraph/RedisGraphPipeline.java b/src/main/java/com/redislabs/redisgraph/RedisGraphPipeline.java new file mode 100644 index 0000000..8416f66 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/RedisGraphPipeline.java @@ -0,0 +1,160 @@ +package com.redislabs.redisgraph; + +import redis.clients.jedis.Response; +import redis.clients.jedis.commands.BasicRedisPipeline; +import redis.clients.jedis.commands.BinaryRedisPipeline; +import redis.clients.jedis.commands.BinaryScriptingCommandsPipeline; +import redis.clients.jedis.commands.ClusterPipeline; +import redis.clients.jedis.commands.MultiKeyBinaryRedisPipeline; +import redis.clients.jedis.commands.MultiKeyCommandsPipeline; +import redis.clients.jedis.commands.RedisPipeline; +import redis.clients.jedis.commands.ScriptingCommandsPipeline; + +import java.io.Closeable; +import java.util.List; +import java.util.Map; + +/** + * An interface which aligned to Jedis Pipeline interface + */ +public interface RedisGraphPipeline extends + MultiKeyBinaryRedisPipeline, + MultiKeyCommandsPipeline, ClusterPipeline, + BinaryScriptingCommandsPipeline, ScriptingCommandsPipeline, + BasicRedisPipeline, BinaryRedisPipeline, RedisPipeline, Closeable { + + /** + * Execute a Cypher 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 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 + * @param query Cypher query + * @param timeout + * @return a response which builds the result set with the query answer. + */ + 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); + + /** + * Executes a cypher 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 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. + * @param query Cypher query. + * @param params parameters map. + * @param timeout + * @return a response which builds the result set with the query answer. + */ + 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 + * @param procedure procedure name to invoke + * @return a response which builds result set with the procedure data + */ + Response callProcedure(String graphId, String procedure); + + /** + * 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 a response which builds result set with the procedure data + */ + Response callProcedure(String graphId, String procedure, List args); + + /** + * 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 a response which builds result set with the procedure data + */ + Response callProcedure(String graphId, String procedure, List args , Map> kwargs); + + /** + * Deletes the entire graph + * @param graphId graph to delete + * @return a response which builds the delete running time statistics + */ + Response deleteGraph(String graphId); + + + /** + * Synchronize pipeline by reading all responses. This operation close the pipeline. Whenever + * possible try to avoid using this version and use Pipeline.sync() as it won't go through all the + * responses and generate the right response type (usually it is a waste of time). + * @return A list of all the responses in the order you executed them. + */ + List syncAndReturnAll(); + + /** + * Synchronize pipeline by reading all responses. This operation close the pipeline. In order to + * get return values from pipelined commands, capture the different Response<?> of the + * commands you execute. + */ + public void sync(); + + + /** + * Blocks until all the previous write commands are successfully transferred and acknowledged by + * at least the specified number of replicas. If the timeout, specified in milliseconds, is + * reached, the command returns even if the specified number of replicas were not yet reached. + * @param replicas successfully transferred and acknowledged by at least the specified number of + * replicas + * @param timeout the time to block in milliseconds, a timeout of 0 means to block forever + * @return the number of replicas reached by all the writes performed in the context of the + * current connection + */ + public Response waitReplicas(int replicas, long timeout); +} diff --git a/src/main/java/com/redislabs/redisgraph/RedisGraphTransaction.java b/src/main/java/com/redislabs/redisgraph/RedisGraphTransaction.java new file mode 100644 index 0000000..cbcd127 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/RedisGraphTransaction.java @@ -0,0 +1,166 @@ +package com.redislabs.redisgraph; + +import redis.clients.jedis.Response; +import redis.clients.jedis.commands.BasicRedisPipeline; +import redis.clients.jedis.commands.BinaryRedisPipeline; +import redis.clients.jedis.commands.BinaryScriptingCommandsPipeline; +import redis.clients.jedis.commands.ClusterPipeline; +import redis.clients.jedis.commands.MultiKeyBinaryRedisPipeline; +import redis.clients.jedis.commands.MultiKeyCommandsPipeline; +import redis.clients.jedis.commands.RedisPipeline; +import redis.clients.jedis.commands.ScriptingCommandsPipeline; + +import java.io.Closeable; +import java.util.List; +import java.util.Map; + +/** + * An interface which aligned to Jedis transactional interface + */ +public interface RedisGraphTransaction extends + MultiKeyBinaryRedisPipeline, + MultiKeyCommandsPipeline, ClusterPipeline, + BinaryScriptingCommandsPipeline, ScriptingCommandsPipeline, + BasicRedisPipeline, BinaryRedisPipeline, RedisPipeline, Closeable { + + /** + * Execute a Cypher 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 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 + * @param query Cypher query + * @param timeout + * @return a response which builds the result set with the query answer. + */ + 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 + * @param query Cypher query + * @param args + * @return a response which builds the result set with the query answer. + * @deprecated use {@link #query(String, String, Map)} instead. + */ + @Deprecated + Response query(String graphId, String query, Object ...args); + + /** + * Executes a cypher 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 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. + * @param query Cypher query. + * @param params parameters map. + * @param timeout + * @return a response which builds the result set with the query answer. + */ + 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 + * @param procedure procedure name to invoke + * @return a response which builds result set with the procedure data + */ + Response callProcedure(String graphId, String procedure); + + /** + * 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 a response which builds result set with the procedure data + */ + Response callProcedure(String graphId, String procedure, List args); + + /** + * 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 a response which builds result set with the procedure data + */ + Response callProcedure(String graphId, String procedure, List args , Map> kwargs); + + /** + * Deletes the entire graph + * @param graphId graph to delete + * @return a response which builds the delete running time statistics + */ + Response deleteGraph(String graphId); + + + /** + * executes the transaction + * @return a list of the executed transaction commands answers, in case of successful transaction, null otherwise + */ + List exec(); + + /** + * If object is in transaction mode, + * flushes all previously queued commands in a transaction and restores the connection state to normal + */ + void clear(); + + /** + * + * @return + */ + List> execGetResponse(); + + /** + * Flushes all previously queued commands in a transaction and restores the connection state to normal + */ + String discard(); +} diff --git a/src/main/java/com/redislabs/redisgraph/ResultSet.java b/src/main/java/com/redislabs/redisgraph/ResultSet.java index 7ac933a..d86ff19 100644 --- a/src/main/java/com/redislabs/redisgraph/ResultSet.java +++ b/src/main/java/com/redislabs/redisgraph/ResultSet.java @@ -1,23 +1,13 @@ package com.redislabs.redisgraph; import java.util.Iterator; -import java.util.List; /** * Hold a query result */ -public interface ResultSet extends Iterator { +public interface ResultSet extends Iterable, Iterator { - public enum ResultSetScalarTypes { - PROPERTY_UNKNOWN, - PROPERTY_NULL, - PROPERTY_STRING, - PROPERTY_INTEGER, - PROPERTY_BOOLEAN, - PROPERTY_DOUBLE, - } - - public int size(); + int size(); Statistics getStatistics(); diff --git a/src/main/java/com/redislabs/redisgraph/Statistics.java b/src/main/java/com/redislabs/redisgraph/Statistics.java index 10f4772..9d7453f 100644 --- a/src/main/java/com/redislabs/redisgraph/Statistics.java +++ b/src/main/java/com/redislabs/redisgraph/Statistics.java @@ -8,12 +8,14 @@ public interface Statistics { */ enum Label{ LABELS_ADDED("Labels added"), - INDICES_ADDED("Indices added"), + INDICES_ADDED("Indices created"), + INDICES_DELETED("Indices deleted"), NODES_CREATED("Nodes created"), NODES_DELETED("Nodes deleted"), RELATIONSHIPS_DELETED("Relationships deleted"), PROPERTIES_SET("Properties set"), RELATIONSHIPS_CREATED("Relationships created"), + CACHED_EXECUTION("Cached execution"), QUERY_INTERNAL_EXECUTION_TIME("Query internal execution time"); private final String text; @@ -54,6 +56,8 @@ public static Label getEnum(String value) { int nodesDeleted(); int indicesAdded(); + + int indicesDeleted(); int labelsAdded(); @@ -62,4 +66,6 @@ public static Label getEnum(String value) { int relationshipsCreated(); int propertiesSet(); + + boolean cachedExecution(); } diff --git a/src/main/java/com/redislabs/redisgraph/Utils.java b/src/main/java/com/redislabs/redisgraph/Utils.java deleted file mode 100644 index 180e8fb..0000000 --- a/src/main/java/com/redislabs/redisgraph/Utils.java +++ /dev/null @@ -1,28 +0,0 @@ -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/exceptions/JRedisGraphException.java b/src/main/java/com/redislabs/redisgraph/exceptions/JRedisGraphException.java new file mode 100644 index 0000000..db33370 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/exceptions/JRedisGraphException.java @@ -0,0 +1,23 @@ +package com.redislabs.redisgraph.exceptions; + +import redis.clients.jedis.exceptions.JedisDataException; + +/** + * RedisGraph query evaluation exception. An instance of JRedisGraphException is + * thrown when RedisGraph encounters an error during query evaluation. + */ +public class JRedisGraphException extends JedisDataException { + private static final long serialVersionUID = -476099681322055468L; + + public JRedisGraphException(String message) { + super(message); + } + + public JRedisGraphException(Throwable cause) { + super(cause); + } + + public JRedisGraphException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/graph_entities/Edge.java b/src/main/java/com/redislabs/redisgraph/graph_entities/Edge.java index fa30d03..b36db16 100644 --- a/src/main/java/com/redislabs/redisgraph/graph_entities/Edge.java +++ b/src/main/java/com/redislabs/redisgraph/graph_entities/Edge.java @@ -8,12 +8,23 @@ */ public class Edge extends GraphEntity { - //memebers - private String relationshipType; - private int source; - private int destination; + //members + private String relationshipType; + private long source; + private long destination; + public Edge() { + super(); + } + /** + * Use this constructor to reduce memory allocations + * when properties are added to the edge + * @param propertiesCapacity preallocate the capacity for the properties + */ + public Edge(int propertiesCapacity) { + super(propertiesCapacity); + } //getters & setters /** @@ -34,14 +45,14 @@ public void setRelationshipType(String relationshipType) { /** * @return The id of the source node */ - public int getSource() { + public long getSource() { return source; } /** * @param source - The id of the source node to be set */ - public void setSource(int source) { + public void setSource(long source) { this.source = source; } @@ -49,7 +60,7 @@ public void setSource(int source) { * * @return the id of the destination node */ - public int getDestination() { + public long getDestination() { return destination; } @@ -57,7 +68,7 @@ public int getDestination() { * * @param destination - The id of the destination node to be set */ - public void setDestination(int destination) { + public void setDestination(long destination) { this.destination = destination; } diff --git a/src/main/java/com/redislabs/redisgraph/graph_entities/GraphEntity.java b/src/main/java/com/redislabs/redisgraph/graph_entities/GraphEntity.java index d2083a6..2919747 100644 --- a/src/main/java/com/redislabs/redisgraph/graph_entities/GraphEntity.java +++ b/src/main/java/com/redislabs/redisgraph/graph_entities/GraphEntity.java @@ -1,8 +1,5 @@ package com.redislabs.redisgraph.graph_entities; - -import com.redislabs.redisgraph.ResultSet.ResultSetScalarTypes; - import java.util.*; @@ -11,91 +8,90 @@ * 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 long id; + protected final Map> propertyMap; - protected int id; - protected final Map propertyMap = new HashMap<>(); + public GraphEntity() { + propertyMap = new HashMap<>(); + } + /** + * Use this constructor to reduce memory allocations + * when properties are added to the edge + * @param propertiesCapacity preallocate the capacity for the properties + */ + public GraphEntity(int propertiesCapacity) { + propertyMap = new HashMap<>(propertiesCapacity); + } //setters & getters /** - * * @return entity id */ - public int getId() { + public long getId() { return id; } /** - * * @param id - entity id to be set */ - public void setId(int id) { + public void setId(long 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)); - + public void addProperty(String name, Object value) { + addProperty(new Property(name, value)); } /** - * * @return Entity's property names, as a Set */ - public Set getEntityPropertyNames(){ + public Set getEntityPropertyNames() { return propertyMap.keySet(); } /** * Add a property to the entity + * * @param property */ - public void addProperty (Property property){ + public void addProperty(Property property) { propertyMap.put(property.getName(), property); } /** - * * @return number of properties */ - public int getNumberOfProperties(){ + 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){ + public Property getProperty(String propertyName) { return propertyMap.get(propertyName); } /** - * * @param name - the name of the property to be removed */ - public void removeProperty(String name){ - + public void removeProperty(String name) { propertyMap.remove(name); - } @Override @@ -112,17 +108,5 @@ 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(); - } + public abstract String toString(); } diff --git a/src/main/java/com/redislabs/redisgraph/graph_entities/Node.java b/src/main/java/com/redislabs/redisgraph/graph_entities/Node.java index 6ede861..3185b43 100644 --- a/src/main/java/com/redislabs/redisgraph/graph_entities/Node.java +++ b/src/main/java/com/redislabs/redisgraph/graph_entities/Node.java @@ -10,7 +10,24 @@ public class Node extends GraphEntity { //members - final private List labels = new ArrayList<>(); + private final List labels; + + public Node() { + super(); + labels = new ArrayList<>(); + } + + /** + * Use this constructor to reduce memory allocations + * when labels or properties are added to the node + * @param labelsCapacity preallocate the capacity for the node labels + * @param propertiesCapacity preallocate the capacity for the properties + */ + public Node(int labelsCapacity, int propertiesCapacity) { + super(propertiesCapacity); + this.labels = new ArrayList<>(labelsCapacity); + } + /** * @param label - a label to be add @@ -28,11 +45,11 @@ public void removeLabel(String label) { /** * @param index - label index - * @return the proprty label + * @return the property label * @throws IndexOutOfBoundsException if the index is out of range * ({@code index < 0 || index >= getNumberOfLabels()}) */ - public String getLabel(int index) throws IndexOutOfBoundsException{ + public String getLabel(int index){ return labels.get(index); } diff --git a/src/main/java/com/redislabs/redisgraph/graph_entities/Path.java b/src/main/java/com/redislabs/redisgraph/graph_entities/Path.java new file mode 100644 index 0000000..a77eaea --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/graph_entities/Path.java @@ -0,0 +1,117 @@ +package com.redislabs.redisgraph.graph_entities; + +import java.util.List; +import java.util.Objects; + +/** + * This class represents a path in the graph. + */ +public final class Path { + + private final List nodes; + private final List edges; + + + /** + * Parametrized constructor + * @param nodes - List of nodes. + * @param edges - List of edges. + */ + public Path(List nodes, List edges) { + this.nodes = nodes; + this.edges = edges; + } + + /** + * Returns the nodes of the path. + * @return List of nodes. + */ + public List getNodes() { + return nodes; + } + + /** + * Returns the edges of the path. + * @return List of edges. + */ + public List getEdges() { + return edges; + } + + /** + * Returns the length of the path - number of edges. + * @return Number of edges. + */ + public int length() { + return edges.size(); + } + + /** + * Return the number of nodes in the path. + * @return Number of nodes. + */ + public int nodeCount(){ + return nodes.size(); + } + + /** + * Returns the first node in the path. + * @return First nodes in the path. + * @throws IndexOutOfBoundsException if the path is empty. + */ + public Node firstNode(){ + return nodes.get(0); + } + + /** + * Returns the last node in the path. + * @return Last nodes in the path. + * @throws IndexOutOfBoundsException if the path is empty. + */ + public Node lastNode(){ + return nodes.get(nodes.size() - 1); + } + + /** + * Returns a node with specified index in the path. + * @return Node. + * @throws IndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index >= nodesCount()}) + */ + public Node getNode(int index){ + return nodes.get(index); + } + + /** + * Returns an edge with specified index in the path. + * @return Edge. + * @throws IndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index >= length()}) + */ + public Edge getEdge(int index){ + return edges.get(index); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Path path = (Path) o; + return Objects.equals(nodes, path.nodes) && + Objects.equals(edges, path.edges); + } + + @Override + public int hashCode() { + return Objects.hash(nodes, edges); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Path{"); + sb.append("nodes=").append(nodes); + sb.append(", edges=").append(edges); + sb.append('}'); + return sb.toString(); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/graph_entities/Point.java b/src/main/java/com/redislabs/redisgraph/graph_entities/Point.java new file mode 100644 index 0000000..5aeb4d3 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/graph_entities/Point.java @@ -0,0 +1,62 @@ +package com.redislabs.redisgraph.graph_entities; + +import java.util.List; +import java.util.Objects; + +/** + * This class represents a (geographical) point in the graph. + */ +public final class Point { + + private static final double EPSILON = 1e-5; + + private final double latitude; + private final double longitude; + + /** + * @param latitude + * @param longitude + */ + public Point(double latitude, double longitude) { + this.latitude = latitude; + this.longitude = longitude; + } + + /** + * @param values {@code [latitude, longitude]} + */ + public Point(List values) { + if (values == null || values.size() != 2) { + throw new IllegalArgumentException("Point requires two doubles."); + } + this.latitude = values.get(0); + this.longitude = values.get(1); + } + + public double getLatitude() { + return latitude; + } + + public double getLongitude() { + return longitude; + } + + @Override + public boolean equals(Object other) { + if (this == other) return true; + if (!(other instanceof Point)) return false; + Point o = (Point) other; + return Math.abs(latitude - o.latitude) < EPSILON && + Math.abs(longitude - o.longitude) < EPSILON; + } + + @Override + public int hashCode() { + return Objects.hash(latitude, longitude); + } + + @Override + public String toString() { + return "Point{latitude=" + latitude + ", longitude=" + longitude + "}"; + } +} diff --git a/src/main/java/com/redislabs/redisgraph/graph_entities/Property.java b/src/main/java/com/redislabs/redisgraph/graph_entities/Property.java index 9a897f8..249bd64 100644 --- a/src/main/java/com/redislabs/redisgraph/graph_entities/Property.java +++ b/src/main/java/com/redislabs/redisgraph/graph_entities/Property.java @@ -1,18 +1,15 @@ package com.redislabs.redisgraph.graph_entities; -import com.redislabs.redisgraph.ResultSet; - import java.util.Objects; /** * A Graph entity property. Has a name, type, and value */ -public class Property { +public class Property { //members private String name; - private ResultSet.ResultSetScalarTypes type; - private Object value; + private T value; /** @@ -26,15 +23,14 @@ public Property() { * Parameterized constructor * * @param name - * @param type * @param value */ - public Property(String name, ResultSet.ResultSetScalarTypes type, Object value) { + public Property(String name, T value) { this.name = name; - this.type = type; this.value = value; } + //getters & setters /** @@ -51,25 +47,11 @@ 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() { + public T getValue() { return value; } @@ -77,24 +59,28 @@ public Object getValue() { /** * @param value property value to be set */ - public void setValue(Object value) { + public void setValue(T value) { this.value = value; } + private boolean valueEquals(Object value1, Object value2) { + if(value1 instanceof Integer) value1 = Long.valueOf(((Integer) value1).longValue()); + if(value2 instanceof Integer) value2 = Long.valueOf(((Integer) value2).longValue()); + return Objects.equals(value1, value2); + } @Override public boolean equals(Object o) { if (this == o) return true; if (!(o instanceof Property)) return false; - Property property = (Property) o; + Property property = (Property) o; return Objects.equals(name, property.name) && - type == property.type && - Objects.equals(value, property.value); + valueEquals(value, property.value); } @Override public int hashCode() { - return Objects.hash(name, type, value); + return Objects.hash(name, value); } /** @@ -105,7 +91,6 @@ public int hashCode() { 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/ResultSetImpl.java b/src/main/java/com/redislabs/redisgraph/impl/ResultSetImpl.java deleted file mode 100644 index 9cfc5b8..0000000 --- a/src/main/java/com/redislabs/redisgraph/impl/ResultSetImpl.java +++ /dev/null @@ -1,278 +0,0 @@ -package com.redislabs.redisgraph.impl; - -import com.redislabs.redisgraph.*; -import com.redislabs.redisgraph.graph_entities.Edge; -import com.redislabs.redisgraph.graph_entities.GraphEntity; -import com.redislabs.redisgraph.graph_entities.Node; -import com.redislabs.redisgraph.graph_entities.Property; -import com.redislabs.redisgraph.impl.graph_cache.GraphCache; -import redis.clients.jedis.util.SafeEncoder; - -import java.util.ArrayList; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.Objects; - -public class ResultSetImpl implements ResultSet { - - 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++); - } - - - @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/Utils.java b/src/main/java/com/redislabs/redisgraph/impl/Utils.java new file mode 100644 index 0000000..d508f9d --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/Utils.java @@ -0,0 +1,141 @@ +package com.redislabs.redisgraph.impl; + +import org.apache.commons.text.translate.AggregateTranslator; +import org.apache.commons.text.translate.CharSequenceTranslator; +import org.apache.commons.text.translate.LookupTranslator; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Utilities class + */ +public class Utils { + public static final List DUMMY_LIST = new ArrayList<>(0); + public static final Map> DUMMY_MAP = new HashMap<>(0); + public static final String COMPACT_STRING = "--COMPACT"; + public static final String TIMEOUT_STRING = "TIMEOUT"; + + 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))); + } + + private Utils() {} + + /** + * + * @param str - a string + * @return the input string surrounded with quotation marks, if needed + */ + private static String quoteString(String str){ + StringBuilder sb = new StringBuilder(str.length()+2); + sb.append('"'); + sb.append(str.replace("\"","\\\"")); + sb.append('"'); + return sb.toString(); + } + + + /** + * Prepare and formats a query and query arguments + * @param query - query + * @param args - query arguments + * @return formatted query + * @deprecated use {@link #prepareQuery(String, Map)} instead. + */ + @Deprecated + public static String prepareQuery(String query, Object ...args){ + if(args.length > 0) { + for(int i=0; i params){ + StringBuilder sb = new StringBuilder("CYPHER "); + for(Map.Entry entry : params.entrySet()) { + String key = entry.getKey(); + Object value = entry.getValue(); + sb.append(key).append('='); + sb.append(valueToString(value)); + sb.append(' '); + } + sb.append(query); + return sb.toString(); + } + + private static String arrayToString(Object[] arr) { + StringBuilder sb = new StringBuilder().append('['); + sb.append(String.join(", ", Arrays.stream(arr).map(Utils::valueToString).collect(Collectors.toList()))); + sb.append(']'); + return sb.toString(); + } + + private static String valueToString(Object value) { + if(value == null) return "null"; + + if(value instanceof String){ + return quoteString((String) value); + } + if(value instanceof Character){ + return quoteString(((Character)value).toString()); + } + + if(value instanceof Object[]){ + return arrayToString((Object[]) value); + + } + if(value instanceof List){ + @SuppressWarnings("unchecked") + List list = (List) value; + return arrayToString(list.toArray()); + } + return value.toString(); + } + + /** + * Prepare and format a procedure call and its arguments + * @param procedure - procedure to invoke + * @param args - procedure arguments + * @param kwargs - procedure output arguments + * @return formatter procedure call + */ + public static String prepareProcedure(String procedure, List args , Map> kwargs){ + args = args.stream().map( Utils::quoteString).collect(Collectors.toList()); + StringBuilder queryStringBuilder = new StringBuilder(); + queryStringBuilder.append("CALL ").append(procedure).append('('); + int i = 0; + for (; i < args.size() - 1; i++) { + queryStringBuilder.append(args.get(i)).append(','); + } + if (i == args.size()-1) { + queryStringBuilder.append(args.get(i)); + } + queryStringBuilder.append(')'); + List kwargsList = kwargs.getOrDefault("y", null); + if(kwargsList != null){ + i = 0; + for (; i < kwargsList.size() - 1; i++) { + queryStringBuilder.append(kwargsList.get(i)).append(','); + + } + queryStringBuilder.append(kwargsList.get(i)); + } + return queryStringBuilder.toString(); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/api/AbstractRedisGraph.java b/src/main/java/com/redislabs/redisgraph/impl/api/AbstractRedisGraph.java new file mode 100644 index 0000000..57fce53 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/api/AbstractRedisGraph.java @@ -0,0 +1,179 @@ +package com.redislabs.redisgraph.impl.api; + +import com.redislabs.redisgraph.RedisGraph; +import com.redislabs.redisgraph.ResultSet; +import com.redislabs.redisgraph.impl.Utils; +import redis.clients.jedis.Jedis; + +import java.util.List; +import java.util.Map; + +/** + * An abstract class to handle non implementation specific user requests + */ +public abstract class AbstractRedisGraph implements RedisGraph { + + /** + * Inherited classes should return a Jedis connection, with respect to their context + * @return Jedis connection + */ + protected abstract Jedis getConnection(); + + /** + * Sends a 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 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 + * @param preparedQuery prepared query + * @param timeout + * @return Result set + */ + 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 + * @param query Cypher query + * @return a result set + */ + 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 + * @param timeout + * @param query Cypher query + * @return a result set + */ + @Override + 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 + * @param query Cypher query + * @param args + * @return a result set + * @deprecated use {@link #query(String, String, Map)} instead. + */ + @Deprecated + public ResultSet query(String graphId, String query, Object ...args) { + String preparedQuery = Utils.prepareQuery(query, args); + return sendQuery(graphId, preparedQuery); + } + + /** + * Executes a cypher 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 query(String graphId, String query, Map params) { + String preparedQuery = Utils.prepareQuery(query, 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. + * @param timeout + * @param query Cypher query. + * @param params parameters map. + * @return a result set. + */ + @Override + public ResultSet query(String graphId, String query, Map params, long timeout) { + String preparedQuery = Utils.prepareQuery(query, 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); + } + + public ResultSet callProcedure(String graphId, String procedure, List args){ + return callProcedure(graphId, procedure, args, Utils.DUMMY_MAP); + } + + public ResultSet callProcedure(String graphId, String procedure, List args , Map> kwargs){ + + String preparedProcedure = Utils.prepareProcedure(procedure, args, kwargs); + return query(graphId, preparedProcedure); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/api/ContextedRedisGraph.java b/src/main/java/com/redislabs/redisgraph/impl/api/ContextedRedisGraph.java new file mode 100644 index 0000000..4d6fe34 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/api/ContextedRedisGraph.java @@ -0,0 +1,216 @@ +package com.redislabs.redisgraph.impl.api; + +import java.util.List; + +import com.redislabs.redisgraph.RedisGraphContext; +import com.redislabs.redisgraph.ResultSet; +import com.redislabs.redisgraph.exceptions.JRedisGraphException; +import com.redislabs.redisgraph.impl.Utils; +import com.redislabs.redisgraph.impl.graph_cache.RedisGraphCaches; +import com.redislabs.redisgraph.impl.resultset.ResultSetImpl; + +import redis.clients.jedis.Client; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisDataException; +import redis.clients.jedis.util.SafeEncoder; + +/** + * An implementation of RedisGraphContext. Allows sending RedisGraph and some Redis commands, + * within a specific connection context + */ +public class ContextedRedisGraph extends AbstractRedisGraph implements RedisGraphContext, RedisGraphCacheHolder { + + private final Jedis connectionContext; + private RedisGraphCaches caches; + + /** + * Generates a new instance with a specific Jedis connection + * @param connectionContext + */ + public ContextedRedisGraph(Jedis connectionContext) { + this.connectionContext = connectionContext; + } + + /** + * Overrides the abstract method. Return the instance only connection + * @return + */ + @Override + protected Jedis getConnection() { + return this.connectionContext; + } + + /** + * Sends the 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 sendQuery(String graphId, String preparedQuery) { + Jedis conn = getConnection(); + try { + @SuppressWarnings("unchecked") + List rawResponse = (List) conn.sendCommand(RedisGraphCommand.QUERY, graphId, preparedQuery, Utils.COMPACT_STRING); + return new ResultSetImpl(rawResponse, this, caches.getGraphCache(graphId)); + } catch (JRedisGraphException rt) { + throw rt; + } catch (JedisDataException j) { + throw new JRedisGraphException(j); + } + } + + /** + * 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 { + @SuppressWarnings("unchecked") + 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 + * @param timeout + * @param preparedQuery prepared query + * @return Result set with the query answer + */ + @Override + protected ResultSet sendQuery(String graphId, String preparedQuery, long timeout) { + Jedis conn = getConnection(); + try { + @SuppressWarnings("unchecked") + List rawResponse = (List) conn.sendBlockingCommand(RedisGraphCommand.QUERY, + graphId, preparedQuery, Utils.COMPACT_STRING, Utils.TIMEOUT_STRING, Long.toString(timeout)); + return new ResultSetImpl(rawResponse, this, caches.getGraphCache(graphId)); + } catch (JRedisGraphException rt) { + throw rt; + } catch (JedisDataException j) { + throw new JRedisGraphException(j); + } + } + + /** + * 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 { + @SuppressWarnings("unchecked") + 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. + */ + @Override + public Jedis getConnectionContext() { + return this.connectionContext; + } + + /** + * Creates a new RedisGraphTransaction transactional object + * @return new RedisGraphTransaction + */ + @Override + public RedisGraphTransaction multi() { + Jedis jedis = getConnection(); + Client client = jedis.getClient(); + client.multi(); + client.getOne(); + RedisGraphTransaction transaction = new RedisGraphTransaction(client, this); + transaction.setRedisGraphCaches(caches); + return transaction; + } + + /** + * Creates a new RedisGraphPipeline pipeline object + * @return new RedisGraphPipeline + */ + @Override + public RedisGraphPipeline pipelined() { + Jedis jedis = getConnection(); + Client client = jedis.getClient(); + RedisGraphPipeline pipeline = new RedisGraphPipeline(client, this); + pipeline.setRedisGraphCaches(caches); + return pipeline; + } + + /** + * Perfrom watch over given Redis keys + * @param keys + * @return "OK" + */ + @Override + public String watch(String... keys) { + return this.getConnection().watch(keys); + } + + /** + * Removes watch from all keys + * @return + */ + @Override + public String unwatch() { + return this.getConnection().unwatch(); + } + + /** + * Deletes the entire graph + * @param graphId graph to delete + * @return delete running time statistics + */ + @Override + public String deleteGraph(String graphId) { + Jedis conn = getConnection(); + Object response; + try { + response = conn.sendCommand(RedisGraphCommand.DELETE, graphId); + } catch (Exception e) { + conn.close(); + throw e; + } + //clear local state + caches.removeGraphCache(graphId); + return SafeEncoder.encode((byte[]) response); + } + + /** + * closes the Jedis connection + */ + @Override + public void close() { + this.connectionContext.close(); + + } + + @Override + public void setRedisGraphCaches(RedisGraphCaches caches) { + this.caches = caches; + } + +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraph.java b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraph.java new file mode 100644 index 0000000..d187f70 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraph.java @@ -0,0 +1,163 @@ +package com.redislabs.redisgraph.impl.api; + +import com.redislabs.redisgraph.RedisGraphContext; +import com.redislabs.redisgraph.RedisGraphContextGenerator; +import com.redislabs.redisgraph.ResultSet; +import com.redislabs.redisgraph.impl.graph_cache.RedisGraphCaches; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.util.Pool; +import redis.clients.jedis.util.SafeEncoder; + +/** + * + */ +public class RedisGraph extends AbstractRedisGraph implements RedisGraphContextGenerator { + + private final Pool pool; + private final Jedis jedis; + private final RedisGraphCaches caches = new RedisGraphCaches(); + + /** + * 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 pool bring your own Jedis pool + */ + public RedisGraph(Pool pool) { + this.pool = pool; + this.jedis = null; + } + + public RedisGraph(Jedis jedis) { + this.jedis = jedis; + this.pool = null; + } + + /** + * Overrides the abstract function. Gets and returns a Jedis connection from the Jedis pool + * @return a Jedis connection + */ + @Override + protected Jedis getConnection() { + return jedis != null ? jedis : pool.getResource(); + } + + /** + * Overrides the abstract function. + * Sends the 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 sendQuery(String graphId, String preparedQuery){ + try (ContextedRedisGraph contextedRedisGraph = new ContextedRedisGraph(getConnection())) { + contextedRedisGraph.setRedisGraphCaches(caches); + return contextedRedisGraph.sendQuery(graphId, 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 + * @param graphId graph to be queried + * @param preparedQuery prepared query + * @param timeout + * @return Result set with the query answer + */ + @Override + protected ResultSet sendQuery(String graphId, String preparedQuery, long timeout){ + try (ContextedRedisGraph contextedRedisGraph = new ContextedRedisGraph(getConnection())) { + contextedRedisGraph.setRedisGraphCaches(caches); + return contextedRedisGraph.sendQuery(graphId, preparedQuery, 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 + */ + @Override + public void close() { + if (pool != null) { + pool.close(); + } + if (jedis != null) { + jedis.close(); + } + } + + /** + * Deletes the entire graph + * @param graphId graph to delete + * @return delete running time statistics + */ + @Override + public String deleteGraph(String graphId) { + try (Jedis conn = getConnection()) { + Object response = conn.sendCommand(RedisGraphCommand.DELETE, graphId); + //clear local state + caches.removeGraphCache(graphId); + return SafeEncoder.encode((byte[]) response); + } + } + + /** + * Returns a new ContextedRedisGraph bounded to a Jedis connection from the Jedis pool + * @return ContextedRedisGraph + */ + @Override + public RedisGraphContext getContext() { + ContextedRedisGraph contextedRedisGraph = new ContextedRedisGraph(getConnection()); + contextedRedisGraph.setRedisGraphCaches(this.caches); + return contextedRedisGraph; + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphCacheHolder.java b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphCacheHolder.java new file mode 100644 index 0000000..97cab72 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphCacheHolder.java @@ -0,0 +1,8 @@ +package com.redislabs.redisgraph.impl.api; + +import com.redislabs.redisgraph.impl.graph_cache.RedisGraphCaches; + +public interface RedisGraphCacheHolder { + + void setRedisGraphCaches(RedisGraphCaches caches); +} diff --git a/src/main/java/com/redislabs/redisgraph/Command.java b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphCommand.java similarity index 64% rename from src/main/java/com/redislabs/redisgraph/Command.java rename to src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphCommand.java index 35dbd98..0af0f61 100644 --- a/src/main/java/com/redislabs/redisgraph/Command.java +++ b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphCommand.java @@ -1,4 +1,4 @@ -package com.redislabs.redisgraph; +package com.redislabs.redisgraph.impl.api; import redis.clients.jedis.util.SafeEncoder; import redis.clients.jedis.commands.ProtocolCommand; @@ -7,13 +7,14 @@ * * */ -public enum Command implements ProtocolCommand { +public enum RedisGraphCommand implements ProtocolCommand { QUERY("graph.QUERY"), + RO_QUERY("graph.RO_QUERY"), DELETE("graph.DELETE"); private final byte[] raw; - Command(String alt) { + RedisGraphCommand(String alt) { raw = SafeEncoder.encode(alt); } diff --git a/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphPipeline.java b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphPipeline.java new file mode 100644 index 0000000..8078afd --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphPipeline.java @@ -0,0 +1,256 @@ +package com.redislabs.redisgraph.impl.api; + +import com.redislabs.redisgraph.RedisGraph; +import com.redislabs.redisgraph.ResultSet; +import com.redislabs.redisgraph.impl.Utils; +import com.redislabs.redisgraph.impl.graph_cache.RedisGraphCaches; +import com.redislabs.redisgraph.impl.resultset.ResultSetImpl; +import redis.clients.jedis.Builder; +import redis.clients.jedis.BuilderFactory; +import redis.clients.jedis.Client; +import redis.clients.jedis.Pipeline; +import redis.clients.jedis.Response; + +import java.util.List; +import java.util.Map; + +/** + * This class is extending Jedis Pipeline + */ +public class RedisGraphPipeline extends Pipeline implements com.redislabs.redisgraph.RedisGraphPipeline, RedisGraphCacheHolder { + + private final RedisGraph redisGraph; + private RedisGraphCaches caches; + + + public RedisGraphPipeline(Client client, RedisGraph redisGraph){ + super.setClient(client); + this.redisGraph = redisGraph; + } + + /** + * Execute a Cypher 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 query(String graphId, String query) { + client.sendCommand(RedisGraphCommand.QUERY, graphId, query, Utils.COMPACT_STRING); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * 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, Utils.COMPACT_STRING); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * Execute a Cypher 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 query(String graphId, String query, long timeout) { + client.sendCommand(RedisGraphCommand.QUERY, graphId, query, Utils.COMPACT_STRING, Utils.TIMEOUT_STRING, + Long.toString(timeout)); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * 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, Utils.COMPACT_STRING, Utils.TIMEOUT_STRING, + Long.toString(timeout)); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * Executes a cypher 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 query(String graphId, String query, Map params) { + String preparedQuery = Utils.prepareQuery(query, params); + client.sendCommand(RedisGraphCommand.QUERY, graphId, preparedQuery, Utils.COMPACT_STRING); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * 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, Utils.COMPACT_STRING); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * Executes a cypher 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 query(String graphId, String query, Map params, long timeout) { + String preparedQuery = Utils.prepareQuery(query, params); + client.sendCommand(RedisGraphCommand.QUERY, graphId, preparedQuery, Utils.COMPACT_STRING, Utils.TIMEOUT_STRING, + Long.toString(timeout)); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * 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, Utils.COMPACT_STRING, + Utils.TIMEOUT_STRING, + Long.toString(timeout)); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * Invokes stored procedures without arguments + * @param graphId a graph to perform the query on + * @param procedure procedure name to invoke + * @return response with result set with the procedure data + */ + public Response callProcedure(String graphId, String procedure){ + return callProcedure(graphId, procedure, Utils.DUMMY_LIST, Utils.DUMMY_MAP); + } + + /** + * 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 response with result set with the procedure data + */ + public Response callProcedure(String graphId, String procedure, List args ){ + return callProcedure(graphId, procedure, args, Utils.DUMMY_MAP); + } + + + /** + * 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 response with result set with the procedure data + */ + public Response callProcedure(String graphId, String procedure, List args, + Map> kwargs) { + String preparedProcedure = Utils.prepareProcedure(procedure, args, kwargs); + return query(graphId, preparedProcedure); + } + + + /** + * Deletes the entire graph + * @param graphId graph to delete + * @return response with the deletion running time statistics + */ + public Response deleteGraph(String graphId){ + + client.sendCommand(RedisGraphCommand.DELETE, graphId); + Response response = getResponse(BuilderFactory.STRING); + caches.removeGraphCache(graphId); + return response; + } + + @Override + public void setRedisGraphCaches(RedisGraphCaches caches) { + this.caches = caches; + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphTransaction.java b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphTransaction.java new file mode 100644 index 0000000..5294993 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/api/RedisGraphTransaction.java @@ -0,0 +1,276 @@ +package com.redislabs.redisgraph.impl.api; + +import com.redislabs.redisgraph.RedisGraph; +import com.redislabs.redisgraph.ResultSet; +import com.redislabs.redisgraph.impl.Utils; +import com.redislabs.redisgraph.impl.graph_cache.RedisGraphCaches; +import com.redislabs.redisgraph.impl.resultset.ResultSetImpl; +import redis.clients.jedis.Builder; +import redis.clients.jedis.BuilderFactory; +import redis.clients.jedis.Client; +import redis.clients.jedis.Response; +import redis.clients.jedis.Transaction; + +import java.util.List; +import java.util.Map; + +/** + * This class is extending Jedis Transaction + */ +public class RedisGraphTransaction extends Transaction + implements com.redislabs.redisgraph.RedisGraphTransaction, RedisGraphCacheHolder { + + private final RedisGraph redisGraph; + private RedisGraphCaches caches; + + public RedisGraphTransaction(Client client, RedisGraph redisGraph) { + // init as in Jedis + super(client); + + this.redisGraph = redisGraph; + } + + /** + * Execute a Cypher 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 query(String graphId, String query) { + client.sendCommand(RedisGraphCommand.QUERY, graphId, query, Utils.COMPACT_STRING); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * 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, Utils.COMPACT_STRING); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * Execute a Cypher 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 query(String graphId, String query, long timeout) { + client.sendCommand(RedisGraphCommand.QUERY, graphId, query, Utils.COMPACT_STRING, Utils.TIMEOUT_STRING, + Long.toString(timeout)); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * 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, Utils.COMPACT_STRING, Utils.TIMEOUT_STRING, + Long.toString(timeout)); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * Execute a Cypher query with arguments + * + * @param graphId a graph to perform the query on + * @param query Cypher query + * @param args + * @return response with a result set + * @deprecated use {@link #query(String, String, Map)} instead. + */ + @Deprecated + @Override + public Response query(String graphId, String query, Object... args) { + String preparedQuery = Utils.prepareQuery(query, args); + client.sendCommand(RedisGraphCommand.QUERY, graphId, preparedQuery, Utils.COMPACT_STRING); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * Executes a cypher 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 query(String graphId, String query, Map params) { + String preparedQuery = Utils.prepareQuery(query, params); + client.sendCommand(RedisGraphCommand.QUERY, graphId, preparedQuery, Utils.COMPACT_STRING); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * 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, Utils.COMPACT_STRING); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * Executes a cypher query with parameters and 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 params parameters map. + * @param timeout + * @return a response which builds the result set with the query answer. + */ + @Override + public Response query(String graphId, String query, Map params, long timeout) { + String preparedQuery = Utils.prepareQuery(query, params); + client.sendCommand(RedisGraphCommand.QUERY, graphId, preparedQuery, Utils.COMPACT_STRING, Utils.TIMEOUT_STRING, + Long.toString(timeout)); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @Override + public ResultSet build(Object o) { + return new ResultSetImpl((List) o, redisGraph, caches.getGraphCache(graphId)); + } + }); + } + + /** + * Executes a cypher read-only query with parameters and 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 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, Utils.COMPACT_STRING, + Utils.TIMEOUT_STRING, Long.toString(timeout)); + return getResponse(new Builder() { + @SuppressWarnings("unchecked") + @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 + * @param procedure procedure name to invoke + * @return response with result set with the procedure data + */ + public Response callProcedure(String graphId, String procedure) { + return callProcedure(graphId, procedure, Utils.DUMMY_LIST, Utils.DUMMY_MAP); + } + + /** + * Invokes stored procedure with arguments, in multi/exec context + * @param graphId a graph to perform the query on + * @param procedure procedure name to invoke + * @param args procedure arguments + * @return response with result set with the procedure data + */ + public Response callProcedure(String graphId, String procedure, List args) { + return callProcedure(graphId, procedure, args, Utils.DUMMY_MAP); + } + + /** + * Invoke a stored procedure, in multi/exec context + * @param graphId a graph to perform the query on + * @param procedure - procedure to execute + * @param args - procedure arguments + * @param kwargs - procedure output arguments + * @return response with result set with the procedure data + */ + public Response callProcedure(String graphId, String procedure, List args, + Map> kwargs) { + String preparedProcedure = Utils.prepareProcedure(procedure, args, kwargs); + return query(graphId, preparedProcedure); + } + + /** + * Deletes the entire graph, in multi/exec context + * @param graphId graph to delete + * @return response with the deletion running time statistics + */ + public Response deleteGraph(String graphId) { + client.sendCommand(RedisGraphCommand.DELETE, graphId); + Response response = getResponse(BuilderFactory.STRING); + caches.removeGraphCache(graphId); + return response; + } + + @Override + public void setRedisGraphCaches(RedisGraphCaches caches) { + this.caches = caches; + } + +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/graph_cache/GraphCache.java b/src/main/java/com/redislabs/redisgraph/impl/graph_cache/GraphCache.java index 765dff8..565c916 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/graph_cache/GraphCache.java +++ b/src/main/java/com/redislabs/redisgraph/impl/graph_cache/GraphCache.java @@ -15,36 +15,35 @@ public class GraphCache { /** * * @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); + public GraphCache(String graphId) { + this.labels = new GraphCacheList(graphId, "db.labels"); + this.propertyNames = new GraphCacheList(graphId, "db.propertyKeys"); + this.relationshipTypes = new GraphCacheList(graphId, "db.relationshipTypes"); } /** * @param index - index of label * @return requested label */ - public String getLabel(int index) { - return labels.getCachedData(index); + public String getLabel(int index, RedisGraph redisGraph) { + return labels.getCachedData(index, redisGraph); } /** * @param index index of the relationship type * @return requested relationship type */ - public String getRelationshipType(int index) { - return relationshipTypes.getCachedData(index); + public String getRelationshipType(int index, RedisGraph redisGraph) { + return relationshipTypes.getCachedData(index, redisGraph); } /** * @param index index of property name * @return requested property */ - public String getPropertyName(int index) { + public String getPropertyName(int index, RedisGraph redisGraph) { - return propertyNames.getCachedData(index); + return propertyNames.getCachedData(index, redisGraph); } } diff --git a/src/main/java/com/redislabs/redisgraph/impl/graph_cache/GraphCacheList.java b/src/main/java/com/redislabs/redisgraph/impl/graph_cache/GraphCacheList.java index d608f16..55035d1 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/graph_cache/GraphCacheList.java +++ b/src/main/java/com/redislabs/redisgraph/impl/graph_cache/GraphCacheList.java @@ -7,31 +7,25 @@ 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 { +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) { + public GraphCacheList(String graphId, String procedure) { this.graphId = graphId; this.procedure = procedure; - this.redisGraph = redisGraph; } @@ -40,23 +34,22 @@ public GraphCacheList(String graphId, String procedure, RedisGraph redisGraph) { * @param index index of data item * @return The string value of the specific procedure response, at the given index. */ - public String getCachedData(int index) { + public String getCachedData(int index, RedisGraph redisGraph) { if (index >= data.size()) { - synchronized (mutex){ + synchronized (data){ if (index >= data.size()) { - getProcedureInfo(); + getProcedureInfo(redisGraph); } } } - String s = data.get(index); - return s; + return data.get(index); } /** * Auxiliary method to parse a procedure result set and refresh the cache */ - private void getProcedureInfo() { + private void getProcedureInfo(RedisGraph redisGraph) { ResultSet resultSet = redisGraph.callProcedure(graphId, procedure); List newData = new ArrayList<>(); int i = 0; diff --git a/src/main/java/com/redislabs/redisgraph/impl/graph_cache/RedisGraphCaches.java b/src/main/java/com/redislabs/redisgraph/impl/graph_cache/RedisGraphCaches.java new file mode 100644 index 0000000..528ed34 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/graph_cache/RedisGraphCaches.java @@ -0,0 +1,59 @@ +package com.redislabs.redisgraph.impl.graph_cache; + +import com.redislabs.redisgraph.RedisGraph; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class RedisGraphCaches { + + private final Map graphCaches = new ConcurrentHashMap<>(); + + public GraphCache getGraphCache(String graphId){ + if (!graphCaches.containsKey(graphId)){ + graphCaches.putIfAbsent(graphId, new GraphCache(graphId)); + } + return graphCaches.get(graphId); + } + + /** + * Returns a String which represents the name of the label mapped to the label id + * @param graphId graph to perform the query + * @param index label index + * @param redisGraph RedisGraphAPI implementation + * @return label name + */ + public String getLabel(String graphId, int index, RedisGraph redisGraph) { + return getGraphCache(graphId).getLabel(index, redisGraph); + } + + /** + * Returns a String which represents the name of the relationship mapped to the label id + * @param graphId graph to perform the query + * @param index relationship index + * @param redisGraph RedisGraphAPI implementation + * @return relationship name + */ + public String getRelationshipType(String graphId, int index, RedisGraph redisGraph){ + return getGraphCache(graphId).getRelationshipType(index, redisGraph); + } + + /** + * Returns a String which represents the name of the property mapped to the label id + * @param graphId graph to perform the query + * @param index property index + * @param redisGraph RedisGraphAPI implementation + * @return property name + */ + public String getPropertyName(String graphId, int index, RedisGraph redisGraph){ + return getGraphCache(graphId).getPropertyName(index, redisGraph); + } + + /** + * Removes a graph meta data cache + * @param graphId + */ + public void removeGraphCache(String graphId){ + graphCaches.remove(graphId); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/HeaderImpl.java b/src/main/java/com/redislabs/redisgraph/impl/resultset/HeaderImpl.java similarity index 95% rename from src/main/java/com/redislabs/redisgraph/impl/HeaderImpl.java rename to src/main/java/com/redislabs/redisgraph/impl/resultset/HeaderImpl.java index 2a7a40f..c4e6c1e 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/HeaderImpl.java +++ b/src/main/java/com/redislabs/redisgraph/impl/resultset/HeaderImpl.java @@ -1,4 +1,4 @@ -package com.redislabs.redisgraph.impl; +package com.redislabs.redisgraph.impl.resultset; import com.redislabs.redisgraph.Header; import redis.clients.jedis.util.SafeEncoder; @@ -36,7 +36,7 @@ public HeaderImpl(List> raw) { */ @Override public List getSchemaNames() { - if (schemaNames.size() == 0) { + if (schemaNames.isEmpty()) { buildSchema(); } return schemaNames; @@ -47,7 +47,7 @@ public List getSchemaNames() { */ @Override public List getSchemaTypes() { - if (schemaTypes.size() == 0) { + if (schemaTypes.isEmpty()) { buildSchema(); } return schemaTypes; diff --git a/src/main/java/com/redislabs/redisgraph/impl/RecordImpl.java b/src/main/java/com/redislabs/redisgraph/impl/resultset/RecordImpl.java similarity index 93% rename from src/main/java/com/redislabs/redisgraph/impl/RecordImpl.java rename to src/main/java/com/redislabs/redisgraph/impl/resultset/RecordImpl.java index 8aca26c..31bec44 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/RecordImpl.java +++ b/src/main/java/com/redislabs/redisgraph/impl/resultset/RecordImpl.java @@ -1,4 +1,4 @@ -package com.redislabs.redisgraph.impl; +package com.redislabs.redisgraph.impl.resultset; import java.util.List; import java.util.Objects; @@ -10,7 +10,7 @@ public class RecordImpl implements Record { private final List header; private final List values; - RecordImpl(List header, List values){ + public RecordImpl(List header, List values){ this.header=header; this.values = values; } diff --git a/src/main/java/com/redislabs/redisgraph/impl/resultset/ResultSetImpl.java b/src/main/java/com/redislabs/redisgraph/impl/resultset/ResultSetImpl.java new file mode 100644 index 0000000..62a9e2f --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/resultset/ResultSetImpl.java @@ -0,0 +1,355 @@ +package com.redislabs.redisgraph.impl.resultset; + +import com.redislabs.redisgraph.Header; +import com.redislabs.redisgraph.Record; +import com.redislabs.redisgraph.RedisGraph; +import com.redislabs.redisgraph.ResultSet; +import com.redislabs.redisgraph.Statistics; +import com.redislabs.redisgraph.exceptions.JRedisGraphException; +import com.redislabs.redisgraph.graph_entities.*; +import com.redislabs.redisgraph.impl.graph_cache.GraphCache; +import redis.clients.jedis.BuilderFactory; +import redis.clients.jedis.util.SafeEncoder; +import redis.clients.jedis.exceptions.JedisDataException; + +import java.util.*; + +public class ResultSetImpl implements ResultSet { + + private final Header header; + private final Statistics statistics; + private final List results; + + private int position = 0; + private final RedisGraph redisGraph; + private final GraphCache cache; + + /** + * @param rawResponse the raw representation of response is at most 3 lists of + * objects. The last list is the statistics list. + * @param redisGraph the graph connection + * @param cache the graph local cache + */ + @SuppressWarnings("unchecked") + public ResultSetImpl(List rawResponse, RedisGraph redisGraph, GraphCache cache) { + this.redisGraph = redisGraph; + this.cache = cache; + + // If a run-time error occurred, the last member of the rawResponse will be a + // JedisDataException. + if (rawResponse.get(rawResponse.size() - 1) instanceof JedisDataException) { + + throw new JRedisGraphException((Throwable) rawResponse.get(rawResponse.size() - 1)); + } + + if (rawResponse.size() != 3) { + + header = parseHeader(new ArrayList<>()); + results = new ArrayList<>(); + statistics = rawResponse.isEmpty() ? parseStatistics(new ArrayList()) + : parseStatistics(rawResponse.get(rawResponse.size() - 1)); + + } else { + + header = parseHeader((List>) rawResponse.get(0)); + results = parseResult((List>) rawResponse.get(1)); + statistics = parseStatistics(rawResponse.get(2)); + } + } + + /** + * @param rawResultSet - raw result set representation + * @return parsed result set + */ + @SuppressWarnings("unchecked") + private List parseResult(List> rawResultSet) { + if (rawResultSet == null || rawResultSet.isEmpty()) { + return new ArrayList<>(0); + } + + List results = new ArrayList<>(rawResultSet.size()); + // 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)); + break; + default: + parsedRow.add(null); + break; + } + } + + // 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 + */ + @SuppressWarnings("unchecked") + 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 + */ + @SuppressWarnings("unchecked") + private Node deserializeNode(List rawNodeData) { + + List labelsIndices = (List) rawNodeData.get(1); + List> rawProperties = (List>) rawNodeData.get(2); + + Node node = new Node(labelsIndices.size(), rawProperties.size()); + deserializeGraphEntityId(node, (Long) rawNodeData.get(0)); + + for (Long labelIndex : labelsIndices) { + String label = cache.getLabel(labelIndex.intValue(), redisGraph); + node.addLabel(label); + } + + deserializeGraphEntityProperties(node, rawProperties); + + return node; + } + + /** + * @param graphEntity graph entity + * @param id entity id to be set to the graph entity + */ + private void deserializeGraphEntityId(GraphEntity graphEntity, long id) { + 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 + */ + @SuppressWarnings("unchecked") + private Edge deserializeEdge(List rawEdgeData) { + + List> rawProperties = (List>) rawEdgeData.get(4); + + Edge edge = new Edge(rawProperties.size()); + deserializeGraphEntityId(edge, (Long) rawEdgeData.get(0)); + + String relationshipType = cache.getRelationshipType(((Long) rawEdgeData.get(1)).intValue(), redisGraph); + edge.setRelationshipType(relationshipType); + + edge.setSource((long) rawEdgeData.get(2)); + edge.setDestination((long) rawEdgeData.get(3)); + + deserializeGraphEntityProperties(edge, rawProperties); + + 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 + */ + private void deserializeGraphEntityProperties(GraphEntity entity, List> rawProperties) { + + for (List rawProperty : rawProperties) { + Property property = new Property<>(); + property.setName(cache.getPropertyName(((Long) rawProperty.get(0)).intValue(), redisGraph)); + + // trimmed for getting to value using deserializeScalar + List propertyScalar = rawProperty.subList(1, rawProperty.size()); + 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 + */ + @SuppressWarnings("unchecked") + private Object deserializeScalar(List rawScalarData) { + ResultSetScalarTypes type = getValueTypeFromObject(rawScalarData.get(0)); + + Object obj = rawScalarData.get(1); + switch (type) { + case VALUE_NULL: + return null; + case VALUE_BOOLEAN: + return Boolean.parseBoolean(SafeEncoder.encode((byte[]) obj)); + case VALUE_DOUBLE: + return Double.parseDouble(SafeEncoder.encode((byte[]) obj)); + case VALUE_INTEGER: + return (Long) obj; + case VALUE_STRING: + return SafeEncoder.encode((byte[]) obj); + case VALUE_ARRAY: + return deserializeArray(obj); + case VALUE_NODE: + return deserializeNode((List) obj); + case VALUE_EDGE: + return deserializeEdge((List) obj); + case VALUE_PATH: + return deserializePath(obj); + case VALUE_MAP: + return deserializeMap(obj); + case VALUE_POINT: + return deserializePoint(obj); + case VALUE_UNKNOWN: + default: + return obj; + } + } + + private Object deserializePoint(Object rawScalarData) { + return new Point(BuilderFactory.DOUBLE_LIST.build(rawScalarData)); + } + + @SuppressWarnings("unchecked") + private Map deserializeMap(Object rawScalarData) { + List keyTypeValueEntries = (List) rawScalarData; + + int size = keyTypeValueEntries.size(); + Map map = new HashMap<>(size >> 1); // set the capacity to half of the list + + for (int i = 0; i < size; i += 2) { + String key = SafeEncoder.encode((byte[]) keyTypeValueEntries.get(i)); + Object value = deserializeScalar((List) keyTypeValueEntries.get(i + 1)); + map.put(key, value); + } + return map; + } + + @SuppressWarnings("unchecked") + private Path deserializePath(Object rawScalarData) { + List> array = (List>) rawScalarData; + List nodes = (List) deserializeScalar(array.get(0)); + List edges = (List) deserializeScalar(array.get(1)); + return new Path(nodes, edges); + } + + @SuppressWarnings("unchecked") + private List deserializeArray(Object rawScalarData) { + List> array = (List>) rawScalarData; + List res = new ArrayList<>(array.size()); + for (List arrayValue : array) { + res.add(deserializeScalar(arrayValue)); + } + return res; + } + + /** + * Auxiliary function to retrieve scalar types + * + * @param rawScalarType + * @return scalar type + */ + private ResultSetScalarTypes getValueTypeFromObject(Object rawScalarType) { + return ResultSetScalarTypes.getValue(((Long) rawScalarType).intValue()); + } + + @Override + @Deprecated + public boolean hasNext() { + return position < results.size(); + } + + @Override + @Deprecated + public Record next() { + if (!hasNext()) + throw new NoSuchElementException(); + return results.get(position++); + } + + @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(); + } + + @Override + public Iterator iterator() { + // TODO Auto-generated method stub + return results.iterator(); + } +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/resultset/ResultSetScalarTypes.java b/src/main/java/com/redislabs/redisgraph/impl/resultset/ResultSetScalarTypes.java new file mode 100644 index 0000000..1a8dc43 --- /dev/null +++ b/src/main/java/com/redislabs/redisgraph/impl/resultset/ResultSetScalarTypes.java @@ -0,0 +1,29 @@ +package com.redislabs.redisgraph.impl.resultset; + +import redis.clients.jedis.exceptions.JedisDataException; + +enum ResultSetScalarTypes { + VALUE_UNKNOWN, + VALUE_NULL, + VALUE_STRING, + VALUE_INTEGER, // 64 bit long. + VALUE_BOOLEAN, + VALUE_DOUBLE, + VALUE_ARRAY, + VALUE_EDGE, + VALUE_NODE, + VALUE_PATH, + VALUE_MAP, + VALUE_POINT; + + private static final ResultSetScalarTypes[] values = values(); + + public static ResultSetScalarTypes getValue(int index) { + try { + return values[index]; + } catch(IndexOutOfBoundsException e) { + throw new JedisDataException("Unrecognized response type"); + } + } + +} diff --git a/src/main/java/com/redislabs/redisgraph/impl/StatisticsImpl.java b/src/main/java/com/redislabs/redisgraph/impl/resultset/StatisticsImpl.java similarity index 86% rename from src/main/java/com/redislabs/redisgraph/impl/StatisticsImpl.java rename to src/main/java/com/redislabs/redisgraph/impl/resultset/StatisticsImpl.java index 68887e1..1f58fe5 100644 --- a/src/main/java/com/redislabs/redisgraph/impl/StatisticsImpl.java +++ b/src/main/java/com/redislabs/redisgraph/impl/resultset/StatisticsImpl.java @@ -1,4 +1,4 @@ -package com.redislabs.redisgraph.impl; +package com.redislabs.redisgraph.impl.resultset; import java.util.EnumMap; import java.util.List; @@ -18,12 +18,12 @@ public class StatisticsImpl implements Statistics { private final Map statistics; /** - * A raw representation of query exection statistics is a list of strings + * A raw representation of query execution 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){ + public StatisticsImpl(List raw){ this.raw = raw; this.statistics = new EnumMap<>(Statistics.Label.class); // lazy loaded } @@ -93,9 +93,12 @@ public int indicesAdded() { } + @Override + public int indicesDeleted() {return getIntValue(Label.INDICES_DELETED);} + /** * - * @return number of lables added after query execution + * @return number of labels added after query execution */ @Override public int labelsAdded() { @@ -129,12 +132,21 @@ public int propertiesSet() { return getIntValue(Label.PROPERTIES_SET); } + /** + * + * @return The execution plan was cached on RedisGraph. + */ + @Override + public boolean cachedExecution() { + return getIntValue(Label.CACHED_EXECUTION) == 1; + } + @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) && + return Objects.equals(raw, that.raw) && Objects.equals(getStatistics(), that.getStatistics()); } diff --git a/src/test/java/com/redislabs/redisgraph/InstantiationTest.java b/src/test/java/com/redislabs/redisgraph/InstantiationTest.java new file mode 100644 index 0000000..5771279 --- /dev/null +++ b/src/test/java/com/redislabs/redisgraph/InstantiationTest.java @@ -0,0 +1,45 @@ +package com.redislabs.redisgraph; + +import org.junit.After; +import org.junit.Assert; + +import com.redislabs.redisgraph.impl.api.RedisGraph; + +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisPool; + +public class InstantiationTest { + private RedisGraphContextGenerator client; + + public void createDefaultClient() { + client = new RedisGraph(); + ResultSet resultSet = client.query("g", "CREATE ({name:'bsb'})"); + Assert.assertEquals(1, resultSet.getStatistics().nodesCreated()); + } + + public void createClientWithHostAndPort() { + client = new RedisGraph("localhost", 6379); + ResultSet resultSet = client.query("g", "CREATE ({name:'bsb'})"); + Assert.assertEquals(1, resultSet.getStatistics().nodesCreated()); + } + + public void createClientWithJedisInstance() { + client = new RedisGraph(new Jedis()); + ResultSet resultSet = client.query("g", "CREATE ({name:'bsb'})"); + Assert.assertEquals(1, resultSet.getStatistics().nodesCreated()); + } + + public void createClientWithJedisPool() { + client = new RedisGraph(new JedisPool()); + ResultSet resultSet = client.query("g", "CREATE ({name:'bsb'})"); + Assert.assertEquals(1, resultSet.getStatistics().nodesCreated()); + } + + @After + public void closeClient() { + if (client != null) { + client.deleteGraph("g"); + client.close(); + } + } +} diff --git a/src/test/java/com/redislabs/redisgraph/IterableTest.java b/src/test/java/com/redislabs/redisgraph/IterableTest.java new file mode 100644 index 0000000..4cec849 --- /dev/null +++ b/src/test/java/com/redislabs/redisgraph/IterableTest.java @@ -0,0 +1,67 @@ +package com.redislabs.redisgraph; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import com.redislabs.redisgraph.impl.api.RedisGraph; + +public class IterableTest { + + private RedisGraphContextGenerator api; + + @Before + public void createApi() { + api = new RedisGraph(); + } + + @After + public void deleteGraph() { + + api.deleteGraph("social"); + api.close(); + } + + @Test + public void testRecordsIterator() { + api.query("social", "UNWIND(range(0,50)) as i CREATE(:N{i:i})"); + + ResultSet rs = api.query("social", "MATCH(n) RETURN n"); + int count = 0; + while (rs.hasNext()) { + rs.next(); + count++; + } + assertEquals(rs.size(), count); + } + + @Test + public void testRecordsIterable() { + api.query("social", "UNWIND(range(0,50)) as i CREATE(:N{i:i})"); + + ResultSet rs = api.query("social", "MATCH(n) RETURN n"); + int count = 0; + for (@SuppressWarnings("unused") + Record row : rs) { + count++; + } + assertEquals(rs.size(), count); + } + + @Test + public void testRecordsIteratorAndIterable() { + api.query("social", "UNWIND(range(0,50)) as i CREATE(:N{i:i})"); + + ResultSet rs = api.query("social", "MATCH(n) RETURN n"); + rs.next(); + int count = 0; + for (@SuppressWarnings("unused") + Record row : rs) { + count++; + } + assertEquals(rs.size(), count); + } + +} diff --git a/src/test/java/com/redislabs/redisgraph/PipelineTest.java b/src/test/java/com/redislabs/redisgraph/PipelineTest.java new file mode 100644 index 0000000..1ec8beb --- /dev/null +++ b/src/test/java/com/redislabs/redisgraph/PipelineTest.java @@ -0,0 +1,203 @@ +package com.redislabs.redisgraph; + +import java.util.Arrays; +import java.util.List; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.redislabs.redisgraph.graph_entities.Node; +import com.redislabs.redisgraph.graph_entities.Property; +import com.redislabs.redisgraph.impl.api.RedisGraph; +import com.redislabs.redisgraph.impl.resultset.ResultSetImpl; + +public class PipelineTest { + + private RedisGraphContextGenerator api; + + public PipelineTest() { + } + + @Before + public void createApi() { + api = new RedisGraph(); + } + + @After + public void deleteGraph() { + api.deleteGraph("social"); + api.close(); + } + + @Test + public void testSync() { + try (RedisGraphContext c = api.getContext()) { + RedisGraphPipeline pipeline = c.pipelined(); + pipeline.set("x", "1"); + pipeline.query("social", "CREATE (:Person {name:'a'})"); + pipeline.query("g", "CREATE (:Person {name:'a'})"); + pipeline.incr("x"); + pipeline.get("x"); + pipeline.query("social", "MATCH (n:Person) RETURN n"); + pipeline.deleteGraph("g"); + pipeline.callProcedure("social", "db.labels"); + List results = pipeline.syncAndReturnAll(); + + // 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()); + + // Redis incr command + Assert.assertEquals(Long.class, results.get(3).getClass()); + Assert.assertEquals(2L, results.get(3)); + + // Redis get command + Assert.assertEquals(String.class, results.get(4).getClass()); + Assert.assertEquals("2", results.get(4)); + + // Graph query result + Assert.assertEquals(ResultSetImpl.class, results.get(5).getClass()); + resultSet = (ResultSet) results.get(5); + + 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(7).getClass()); + resultSet = (ResultSet) results.get(7); + + 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")); + } + } + + @Test + public void testReadOnlyQueries() { + try (RedisGraphContext c = api.getContext()) { + RedisGraphPipeline pipeline = c.pipelined(); + + pipeline.set("x", "1"); + pipeline.query("social", "CREATE (:Person {name:'a'})"); + pipeline.query("g", "CREATE (:Person {name:'a'})"); + pipeline.readOnlyQuery("social", "MATCH (n:Person) RETURN n"); + pipeline.deleteGraph("g"); + pipeline.callProcedure("social", "db.labels"); + List results = pipeline.syncAndReturnAll(); + + // 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")); + } + } + + @Test + public void testWaitReplicas() { + try (RedisGraphContext c = api.getContext()) { + RedisGraphPipeline pipeline = c.pipelined(); + pipeline.set("x", "1"); + pipeline.query("social", "CREATE (:Person {name:'a'})"); + pipeline.query("g", "CREATE (:Person {name:'a'})"); + pipeline.waitReplicas(0, 100L); + List results = pipeline.syncAndReturnAll(); + Assert.assertEquals(0L, results.get(3)); + } + } +} diff --git a/src/test/java/com/redislabs/redisgraph/RedisGraphAPITest.java b/src/test/java/com/redislabs/redisgraph/RedisGraphAPITest.java index 8730526..6fc132a 100644 --- a/src/test/java/com/redislabs/redisgraph/RedisGraphAPITest.java +++ b/src/test/java/com/redislabs/redisgraph/RedisGraphAPITest.java @@ -1,48 +1,42 @@ package com.redislabs.redisgraph; - -import java.util.Arrays; -import java.util.List; -import java.util.NoSuchElementException; +import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; -import com.redislabs.redisgraph.graph_entities.Edge; -import com.redislabs.redisgraph.graph_entities.Node; -import com.redislabs.redisgraph.graph_entities.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.*; +import com.redislabs.redisgraph.graph_entities.Edge; +import com.redislabs.redisgraph.graph_entities.Node; +import com.redislabs.redisgraph.graph_entities.Path; +import com.redislabs.redisgraph.graph_entities.Point; +import com.redislabs.redisgraph.graph_entities.Property; +import com.redislabs.redisgraph.impl.api.RedisGraph; +import com.redislabs.redisgraph.test.utils.PathBuilder; public class RedisGraphAPITest { - RedisGraph api; - public RedisGraphAPITest() { - } + private RedisGraphContextGenerator client; @Before - public void createApi(){ - api = new RedisGraph(); + public void createApi() { + client = new RedisGraph(); } + @After public void deleteGraph() { - - api.deleteGraph("social"); - api.close(); + client.deleteGraph("social"); + client.close(); } - - @Test public void testCreateNode() { - // Create a node - ResultSet resultSet = api.query("social", "CREATE ({name:'roi',age:32})"); - + // Create a node + ResultSet resultSet = client.query("social", "CREATE ({name:'roi',age:32})"); Assert.assertEquals(1, resultSet.getStatistics().nodesCreated()); Assert.assertNull(resultSet.getStatistics().getStringValue(Label.NODES_DELETED)); @@ -51,20 +45,19 @@ public void testCreateNode() { 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) { + } catch (NoSuchElementException ignored) { } } @Test public void testCreateLabeledNode() { // Create a node with a label - ResultSet resultSet = api.query("social", "CREATE (:human{name:'danny',age:12})"); + ResultSet resultSet = client.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)); @@ -74,11 +67,12 @@ public void testCreateLabeledNode() { @Test public void testConnectNodes() { // Create both source and destination nodes - Assert.assertNotNull(api.query("social", "CREATE (:person{name:'roi',age:32})")); - Assert.assertNotNull(api.query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.assertNotNull(client.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(client.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)"); + ResultSet resultSet = client.query("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)"); Assert.assertFalse(resultSet.hasNext()); Assert.assertNull(resultSet.getStatistics().getStringValue(Label.NODES_CREATED)); @@ -89,10 +83,10 @@ public void testConnectNodes() { } @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"); + public void testDeleteNodes() { + Assert.assertNotNull(client.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(client.query("social", "CREATE (:person{name:'amit',age:30})")); + ResultSet deleteResult = client.query("social", "MATCH (a:person) WHERE (a.name = 'roi') DELETE a"); Assert.assertFalse(deleteResult.hasNext()); Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.NODES_CREATED)); @@ -103,9 +97,10 @@ public void testDeleteNodes(){ 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.assertNotNull(client.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(client.query("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)")); + deleteResult = client.query("social", "MATCH (a:person) WHERE (a.name = 'roi') DELETE a"); Assert.assertFalse(deleteResult.hasNext()); Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.NODES_CREATED)); @@ -117,17 +112,16 @@ public void testDeleteNodes(){ Assert.assertNotNull(deleteResult.getStatistics().getStringValue(Label.QUERY_INTERNAL_EXECUTION_TIME)); - - } @Test - public void testDeleteRelationship(){ + 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.assertNotNull(client.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(client.query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.assertNotNull(client.query("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(a)")); + ResultSet deleteResult = client.query("social", "MATCH (a:person)-[e]->() WHERE (a.name = 'roi') DELETE e"); Assert.assertFalse(deleteResult.hasNext()); Assert.assertNull(deleteResult.getStatistics().getStringValue(Label.NODES_CREATED)); @@ -141,74 +135,78 @@ public void testDeleteRelationship(){ } - @Test public void testIndex() { // Create both source and destination nodes - Assert.assertNotNull(api.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(client.query("social", "CREATE (:person{name:'roi',age:32})")); - ResultSet createIndexResult = api.query("social", "CREATE INDEX ON :person(age)"); + ResultSet createIndexResult = client.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()); + // since RediSearch as index, those action are allowed + ResultSet createNonExistingIndexResult = client.query("social", "CREATE INDEX ON :person(age1)"); + Assert.assertFalse(createNonExistingIndexResult.hasNext()); + Assert.assertNotNull(createNonExistingIndexResult.getStatistics().getStringValue(Label.INDICES_ADDED)); + Assert.assertEquals(1, createNonExistingIndexResult.getStatistics().indicesAdded()); + + ResultSet createExistingIndexResult = client.query("social", "CREATE INDEX ON :person(age)"); + Assert.assertFalse(createExistingIndexResult.hasNext()); + Assert.assertNotNull(createExistingIndexResult.getStatistics().getStringValue(Label.INDICES_ADDED)); + Assert.assertEquals(0, createExistingIndexResult.getStatistics().indicesAdded()); + + ResultSet deleteExistingIndexResult = client.query("social", "DROP INDEX ON :person(age)"); + Assert.assertFalse(deleteExistingIndexResult.hasNext()); + Assert.assertNotNull(deleteExistingIndexResult.getStatistics().getStringValue(Label.INDICES_DELETED)); + Assert.assertEquals(1, deleteExistingIndexResult.getStatistics().indicesDeleted()); + } @Test - public void testHeader(){ + 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)")); + Assert.assertNotNull(client.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(client.query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.assertNotNull(client.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"); + ResultSet queryResult = client.query("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r, a.age"); - Assert.assertNotNull(queryResult.getHeader()); Header header = queryResult.getHeader(); + Assert.assertNotNull(header); + Assert.assertEquals("HeaderImpl{" + + "schemaTypes=[COLUMN_SCALAR, COLUMN_SCALAR, COLUMN_SCALAR], " + + "schemaNames=[a, r, a.age]}", header.toString()); + // Assert.assertEquals(-1901778507, header.hashCode()); 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 testRecord(){ + public void testRecord() { String name = "roi"; int age = 32; double doubleValue = 3.14; - boolean boolValue = true; + boolean boolValue = true; String place = "TLV"; int since = 2000; + Property nameProperty = new Property<>("name", name); + Property ageProperty = new Property<>("age", age); + Property doubleProperty = new Property<>("doubleValue", doubleValue); + Property trueBooleanProperty = new Property<>("boolValue", true); + Property falseBooleanProperty = new Property<>("boolValue", false); - - 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); + Property placeProperty = new Property<>("place", place); + Property sinceProperty = new Property<>("since", since); Node expectedNode = new Node(); expectedNode.setId(0); @@ -217,7 +215,13 @@ public void testRecord(){ expectedNode.addProperty(ageProperty); expectedNode.addProperty(doubleProperty); expectedNode.addProperty(trueBooleanProperty); - expectedNode.addProperty(nullProperty); + Assert.assertEquals( + "Node{labels=[person], id=0, " + + "propertyMap={name=Property{name='name', value=roi}, " + + "boolValue=Property{name='boolValue', value=true}, " + + "doubleValue=Property{name='doubleValue', value=3.14}, " + + "age=Property{name='age', value=32}}}", + expectedNode.toString()); Edge expectedEdge = new Edge(); expectedEdge.setId(0); @@ -228,21 +232,30 @@ public void testRecord(){ 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.assertEquals("Edge{relationshipType='knows', source=0, destination=1, id=0, " + + "propertyMap={boolValue=Property{name='boolValue', value=false}, " + + "place=Property{name='place', value=TLV}, " + + "doubleValue=Property{name='doubleValue', value=3.14}, " + + "since=Property{name='since', value=2000}}}", expectedEdge.toString()); + + Map params = new HashMap<>(); + params.put("name", name); + params.put("age", age); + params.put("boolValue", boolValue); + params.put("doubleValue", doubleValue); + + Assert.assertNotNull(client.query("social", + "CREATE (:person{name:$name,age:$age, doubleValue:$doubleValue, boolValue:$boolValue})", params)); + Assert.assertNotNull(client.query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.assertNotNull( + client.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}]->(b)")); + + ResultSet resultSet = client.query("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r, " + + "a.name, a.age, a.doubleValue, a.boolValue, " + + "r.place, r.since, r.doubleValue, r.boolValue"); Assert.assertNotNull(resultSet); - Assert.assertEquals(0, resultSet.getStatistics().nodesCreated()); Assert.assertEquals(0, resultSet.getStatistics().nodesDeleted()); Assert.assertEquals(0, resultSet.getStatistics().labelsAdded()); @@ -251,7 +264,6 @@ public void testRecord(){ 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(); @@ -272,55 +284,41 @@ public void testRecord(){ 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("a", "r", "a.name", "a.age", "a.doubleValue", "a.boolValue", + "r.place", "r.since", "r.doubleValue", "r.boolValue"), record.keys()); Assert.assertEquals(Arrays.asList(expectedNode, expectedEdge, - name, age, doubleValue, true, null, - place, since, doubleValue, false, null), + name, (long) age, doubleValue, true, + place, (long) since, doubleValue, false), record.values()); Node a = record.getValue("a"); - for (String propertyName : expectedNode.getEntityPropertyNames()){ - Assert.assertEquals(expectedNode.getProperty(propertyName) ,a.getProperty(propertyName)); + for (String propertyName : expectedNode.getEntityPropertyNames()) { + Assert.assertEquals(expectedNode.getProperty(propertyName), a.getProperty(propertyName)); } - 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")); - - } - - - @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()); - - } + Assert.assertEquals("roi", record.getString(2)); + Assert.assertEquals("32", record.getString(3)); + Assert.assertEquals(32L, ((Long) record.getValue(3)).longValue()); + Assert.assertEquals(32L, ((Long) record.getValue("a.age")).longValue()); + Assert.assertEquals("roi", record.getString("a.name")); + Assert.assertEquals("32", record.getString("a.age")); } @Test - public void testMultiThread(){ + public void testMultiThread() { - Assert.assertNotNull(api.query("social", "CREATE (:person {name:'roi', age:32})-[:knows]->(:person {name:'amit',age:30}) ")); + Assert.assertNotNull(client.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()); + List resultSets = IntStream.range(0, 16).parallel() + .mapToObj(i -> client.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"); + Property nameProperty = new Property<>("name", "roi"); + Property ageProperty = new Property<>("age", 32); + Property lastNameProperty = new Property<>("lastName", "a"); Node expectedNode = new Node(); expectedNode.setId(0); @@ -328,38 +326,30 @@ public void testMultiThread(){ 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){ + 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()); + Assert.assertEquals(Arrays.asList(expectedNode, expectedEdge, 32L), record.values()); } - //test for update in local cache + // test for update in local cache expectedNode.removeProperty("name"); expectedNode.removeProperty("age"); expectedNode.addProperty(lastNameProperty); @@ -367,33 +357,28 @@ public void testMultiThread(){ 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)")); + Assert.assertNotNull(client.query("social", "CREATE (:worker{lastName:'a'})")); + Assert.assertNotNull(client.query("social", "CREATE (:worker{lastName:'b'})")); + Assert.assertNotNull(client.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()); + resultSets = IntStream.range(0, 16).parallel() + .mapToObj(i -> client.query("social", "MATCH (a:worker)-[r:worksWith]->(b:worker) RETURN a,r")) + .collect(Collectors.toList()); - for (ResultSet resultSet : resultSets){ + 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(); @@ -403,18 +388,18 @@ public void testMultiThread(){ } } - @Test - public void testAdditionToProcedures(){ + 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)")); + Assert.assertNotNull(client.query("social", "CREATE (:person{name:'roi',age:32})")); + Assert.assertNotNull(client.query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.assertNotNull(client.query("social", + "MATCH (a:person), (b:person) WHERE (a.name = 'roi' AND b.name='amit') CREATE (a)-[:knows]->(b)")); - //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"); + // expected objects init + Property nameProperty = new Property<>("name", "roi"); + Property ageProperty = new Property<>("age", 32); + Property lastNameProperty = new Property<>("lastName", "a"); Node expectedNode = new Node(); expectedNode.setId(0); @@ -422,27 +407,20 @@ public void testAdditionToProcedures(){ 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"); + ResultSet resultSet = client.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(); @@ -450,7 +428,7 @@ public void testAdditionToProcedures(){ Assert.assertEquals(Arrays.asList("a", "r"), record.keys()); Assert.assertEquals(Arrays.asList(expectedNode, expectedEdge), record.values()); - //test for local cache updates + // test for local cache updates expectedNode.removeProperty("name"); expectedNode.removeProperty("age"); @@ -462,22 +440,18 @@ public void testAdditionToProcedures(){ 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(client.query("social", "CREATE (:worker{lastName:'a'})")); + Assert.assertNotNull(client.query("social", "CREATE (:worker{lastName:'b'})")); + Assert.assertNotNull(client.query("social", + "MATCH (a:worker), (b:worker) WHERE (a.lastName = 'a' AND b.lastName='b') CREATE (a)-[:worksWith]->(b)")); + resultSet = client.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(); @@ -487,11 +461,435 @@ record = resultSet.next(); } - @Test 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")); + Map params1 = new HashMap(); + params1.put("s1", "S\"'"); + params1.put("s2", "S'\""); + Assert.assertNotNull(client.query("social", "CREATE (:escaped{s1:$s1,s2:$s2})", params1)); + + Map params2 = new HashMap(); + params2.put("s1", "S\"'"); + params2.put("s2", "S'\""); + Assert.assertNotNull(client.query("social", "MATCH (n) where n.s1=$s1 and n.s2=$s2 RETURN n", params2)); + + Assert.assertNotNull(client.query("social", "MATCH (n) where n.s1='S\"' RETURN n")); + + } + + @Test + public void testContextedAPI() { + + String name = "roi"; + int age = 32; + double doubleValue = 3.14; + boolean boolValue = true; + + String place = "TLV"; + int since = 2000; + + Property nameProperty = new Property<>("name", name); + Property ageProperty = new Property<>("age", age); + Property doubleProperty = new Property<>("doubleValue", doubleValue); + Property trueBooleanProperty = new Property<>("boolValue", true); + Property falseBooleanProperty = new Property<>("boolValue", false); + + Property placeProperty = new Property<>("place", place); + Property sinceProperty = new Property<>("since", since); + + Node expectedNode = new Node(); + expectedNode.setId(0); + expectedNode.addLabel("person"); + expectedNode.addProperty(nameProperty); + expectedNode.addProperty(ageProperty); + expectedNode.addProperty(doubleProperty); + expectedNode.addProperty(trueBooleanProperty); + + 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); + + Map params = new HashMap<>(); + params.put("name", name); + params.put("age", age); + params.put("boolValue", boolValue); + params.put("doubleValue", doubleValue); + try (RedisGraphContext c = client.getContext()) { + Assert.assertNotNull(c.query("social", + "CREATE (:person{name:$name, age:$age, doubleValue:$doubleValue, boolValue:$boolValue})", params)); + Assert.assertNotNull(c.query("social", "CREATE (:person{name:'amit',age:30})")); + Assert.assertNotNull( + c.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}]->(b)")); + + ResultSet resultSet = c.query("social", "MATCH (a:person)-[r:knows]->(b:person) RETURN a,r, " + + "a.name, a.age, a.doubleValue, a.boolValue, " + + "r.place, r.since, r.doubleValue, r.boolValue"); + 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", + "r.place", "r.since", "r.doubleValue", "r.boolValue"), record.keys()); + + Assert.assertEquals(Arrays.asList(expectedNode, expectedEdge, + name, (long) age, doubleValue, true, + place, (long) since, doubleValue, false), + record.values()); + + Node a = record.getValue("a"); + for (String propertyName : expectedNode.getEntityPropertyNames()) { + Assert.assertEquals(expectedNode.getProperty(propertyName), a.getProperty(propertyName)); + } + + Assert.assertEquals("roi", record.getString(2)); + Assert.assertEquals("32", record.getString(3)); + Assert.assertEquals(32L, ((Long) (record.getValue(3))).longValue()); + Assert.assertEquals(32L, ((Long) record.getValue("a.age")).longValue()); + Assert.assertEquals("roi", record.getString("a.name")); + Assert.assertEquals("32", record.getString("a.age")); + } + } + + @Test + public void testArraySupport() { + + Node expectedANode = new Node(); + expectedANode.setId(0); + expectedANode.addLabel("person"); + Property aNameProperty = new Property<>("name", "a"); + Property aAgeProperty = new Property<>("age", 32); + Property> aListProperty = new Property<>("array", Arrays.asList(0L, 1L, 2L)); + expectedANode.addProperty(aNameProperty); + expectedANode.addProperty(aAgeProperty); + expectedANode.addProperty(aListProperty); + + Node expectedBNode = new Node(); + expectedBNode.setId(1); + expectedBNode.addLabel("person"); + Property bNameProperty = new Property<>("name", "b"); + Property bAgeProperty = new Property<>("age", 30); + Property> bListProperty = new Property<>("array", Arrays.asList(3L, 4L, 5L)); + expectedBNode.addProperty(bNameProperty); + expectedBNode.addProperty(bAgeProperty); + expectedBNode.addProperty(bListProperty); + + Assert.assertNotNull(client.query("social", "CREATE (:person{name:'a',age:32,array:[0,1,2]})")); + Assert.assertNotNull(client.query("social", "CREATE (:person{name:'b',age:30,array:[3,4,5]})")); + + // test array + + ResultSet resultSet = client.query("social", "WITH [0,1,2] as x return x"); + + // check header + Assert.assertNotNull(resultSet.getHeader()); + Header header = resultSet.getHeader(); + + List schemaNames = header.getSchemaNames(); + Assert.assertNotNull(schemaNames); + Assert.assertEquals(1, schemaNames.size()); + Assert.assertEquals("x", schemaNames.get(0)); + + // check record + Assert.assertEquals(1, resultSet.size()); + Assert.assertTrue(resultSet.hasNext()); + Record record = resultSet.next(); + Assert.assertFalse(resultSet.hasNext()); + Assert.assertEquals(Arrays.asList("x"), record.keys()); + + List x = record.getValue("x"); + Assert.assertEquals(Arrays.asList(0L, 1L, 2L), x); + + // test collect + resultSet = client.query("social", "MATCH(n) return collect(n) as x"); + + Assert.assertNotNull(resultSet.getHeader()); + header = resultSet.getHeader(); + + schemaNames = header.getSchemaNames(); + Assert.assertNotNull(schemaNames); + Assert.assertEquals(1, schemaNames.size()); + Assert.assertEquals("x", schemaNames.get(0)); + + // check record + Assert.assertEquals(1, resultSet.size()); + Assert.assertTrue(resultSet.hasNext()); + record = resultSet.next(); + Assert.assertFalse(resultSet.hasNext()); + Assert.assertEquals(Arrays.asList("x"), record.keys()); + x = record.getValue("x"); + Assert.assertEquals(Arrays.asList(expectedANode, expectedBNode), x); + + // test unwind + resultSet = client.query("social", "unwind([0,1,2]) as x return x"); + + Assert.assertNotNull(resultSet.getHeader()); + header = resultSet.getHeader(); + + schemaNames = header.getSchemaNames(); + Assert.assertNotNull(schemaNames); + Assert.assertEquals(1, schemaNames.size()); + Assert.assertEquals("x", schemaNames.get(0)); + + // check record + Assert.assertEquals(3, resultSet.size()); + + for (long i = 0; i < 3; i++) { + Assert.assertTrue(resultSet.hasNext()); + record = resultSet.next(); + Assert.assertEquals(Arrays.asList("x"), record.keys()); + Assert.assertEquals(i, (long) record.getValue("x")); + + } + + } + + @Test + public void testPath() { + List nodes = new ArrayList<>(3); + for (int i = 0; i < 3; i++) { + Node node = new Node(); + node.setId(i); + node.addLabel("L1"); + nodes.add(node); + } + + List edges = new ArrayList<>(2); + for (int i = 0; i < 2; i++) { + Edge edge = new Edge(); + edge.setId(i); + edge.setRelationshipType("R1"); + edge.setSource(i); + edge.setDestination(i + 1); + edges.add(edge); + } + + Set expectedPaths = new HashSet<>(); + + Path path01 = new PathBuilder(2).append(nodes.get(0)).append(edges.get(0)).append(nodes.get(1)).build(); + Path path12 = new PathBuilder(2).append(nodes.get(1)).append(edges.get(1)).append(nodes.get(2)).build(); + Path path02 = new PathBuilder(3).append(nodes.get(0)).append(edges.get(0)).append(nodes.get(1)) + .append(edges.get(1)).append(nodes.get(2)).build(); + + expectedPaths.add(path01); + expectedPaths.add(path12); + expectedPaths.add(path02); + + client.query("social", "CREATE (:L1)-[:R1]->(:L1)-[:R1]->(:L1)"); + + ResultSet resultSet = client.query("social", "MATCH p = (:L1)-[:R1*]->(:L1) RETURN p"); + + Assert.assertEquals(expectedPaths.size(), resultSet.size()); + for (int i = 0; i < resultSet.size(); i++) { + Path p = resultSet.next().getValue("p"); + Assert.assertTrue(expectedPaths.contains(p)); + expectedPaths.remove(p); + } + + } + + @Test + public void testNullGraphEntities() { + // Create two nodes connected by a single outgoing edge. + Assert.assertNotNull(client.query("social", "CREATE (:L)-[:E]->(:L2)")); + // Test a query that produces 1 record with 3 null values. + ResultSet resultSet = client.query("social", "OPTIONAL MATCH (a:NONEXISTENT)-[e]->(b) RETURN a, e, b"); + Assert.assertEquals(1, resultSet.size()); + Assert.assertTrue(resultSet.hasNext()); + Record record = resultSet.next(); + Assert.assertFalse(resultSet.hasNext()); + Assert.assertEquals(Arrays.asList(null, null, null), record.values()); + + // Test a query that produces 2 records, with 2 null values in the second. + resultSet = client.query("social", "MATCH (a) OPTIONAL MATCH (a)-[e]->(b) RETURN a, e, b ORDER BY ID(a)"); + Assert.assertEquals(2, resultSet.size()); + record = resultSet.next(); + Assert.assertEquals(3, record.size()); + + Assert.assertNotNull(record.getValue(0)); + Assert.assertNotNull(record.getValue(1)); + Assert.assertNotNull(record.getValue(2)); + + record = resultSet.next(); + Assert.assertEquals(3, record.size()); + + Assert.assertNotNull(record.getValue(0)); + Assert.assertNull(record.getValue(1)); + Assert.assertNull(record.getValue(2)); + + // Test a query that produces 2 records, the first containing a path and the + // second containing a null value. + resultSet = client.query("social", "MATCH (a) OPTIONAL MATCH p = (a)-[e]->(b) RETURN p"); + Assert.assertEquals(2, resultSet.size()); + + record = resultSet.next(); + Assert.assertEquals(1, record.size()); + Assert.assertNotNull(record.getValue(0)); + + record = resultSet.next(); + Assert.assertEquals(1, record.size()); + Assert.assertNull(record.getValue(0)); + } + + @Test + public void test64bitnumber() { + long value = 1 << 40; + Map params = new HashMap<>(); + params.put("val", value); + ResultSet resultSet = client.query("social", "CREATE (n {val:$val}) RETURN n.val", params); + Assert.assertEquals(1, resultSet.size()); + Record r = resultSet.next(); + Assert.assertEquals(Long.valueOf(value), r.getValue(0)); + } + + @Test + public void testCachedExecution() { + client.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 = client.query("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 = client.query("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 testMapDataType() { + Map expected = new HashMap<>(); + expected.put("a", (long) 1); + expected.put("b", "str"); + expected.put("c", null); + List d = new ArrayList<>(); + d.add((long) 1); + d.add((long) 2); + d.add((long) 3); + expected.put("d", d); + expected.put("e", true); + Map f = new HashMap<>(); + f.put("x", (long) 1); + f.put("y", (long) 2); + expected.put("f", f); + ResultSet res = client.query("social", "RETURN {a:1, b:'str', c:NULL, d:[1,2,3], e:True, f:{x:1, y:2}}"); + Assert.assertEquals(1, res.size()); + Record r = res.next(); + Map actual = r.getValue(0); + Assert.assertEquals(expected, actual); + } + + @Test + public void testGeoPointLatLon() { + ResultSet rs = client.query("social", "CREATE (:restaurant" + + " {location: point({latitude:30.27822306, longitude:-97.75134723})})"); + Assert.assertEquals(1, rs.getStatistics().nodesCreated()); + Assert.assertEquals(1, rs.getStatistics().propertiesSet()); + + assertTestGeoPoint(); + } + + @Test + public void testGeoPointLonLat() { + ResultSet rs = client.query("social", "CREATE (:restaurant" + + " {location: point({longitude:-97.75134723, latitude:30.27822306})})"); + Assert.assertEquals(1, rs.getStatistics().nodesCreated()); + Assert.assertEquals(1, rs.getStatistics().propertiesSet()); + + assertTestGeoPoint(); + } + + private void assertTestGeoPoint() { + ResultSet results = client.query("social", "MATCH (restaurant) RETURN restaurant"); + Assert.assertEquals(1, results.size()); + Record record = results.next(); + Assert.assertEquals(1, record.size()); + Assert.assertEquals(Collections.singletonList("restaurant"), record.keys()); + Node node = record.getValue(0); + Property property = node.getProperty("location"); + Assert.assertEquals(new Point(30.27822306, -97.75134723), property.getValue()); + } + + @Test + public void timeoutArgument() { + ResultSet rs = client.query("social", "UNWIND range(0,100) AS x WITH x AS x WHERE x = 100 RETURN x", 1L); + Assert.assertEquals(1, rs.size()); + Record r = rs.next(); + Assert.assertEquals(Long.valueOf(100), r.getValue(0)); + } + + @Test + public void testCachedExecutionReadOnly() { + client.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 = client.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 = client.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 testSimpleReadOnly() { + client.query("social", "CREATE (:person{name:'filipe',age:30})"); + ResultSet rsRo = client.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)); } } diff --git a/src/test/java/com/redislabs/redisgraph/TransactionTest.java b/src/test/java/com/redislabs/redisgraph/TransactionTest.java new file mode 100644 index 0000000..0df475b --- /dev/null +++ b/src/test/java/com/redislabs/redisgraph/TransactionTest.java @@ -0,0 +1,236 @@ +package com.redislabs.redisgraph; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.redislabs.redisgraph.graph_entities.Node; +import com.redislabs.redisgraph.graph_entities.Property; +import com.redislabs.redisgraph.impl.api.RedisGraph; +import com.redislabs.redisgraph.impl.resultset.ResultSetImpl; + +public class TransactionTest { + + private RedisGraphContextGenerator api; + + public TransactionTest() { + } + + @Before + public void createApi(){ + api = new RedisGraph(); + } + + @After + public void deleteGraph() { + api.deleteGraph("social"); + api.close(); + } + + @Test + public void testMultiExec(){ + try (RedisGraphContext c = api.getContext()) { + RedisGraphTransaction transaction = c.multi(); + + transaction.set("x", "1"); + transaction.query("social", "CREATE (:Person {name:'a'})"); + transaction.query("g", "CREATE (:Person {name:'a'})"); + transaction.incr("x"); + transaction.get("x"); + transaction.query("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()); + + // Redis incr command + Assert.assertEquals(Long.class, results.get(3).getClass()); + Assert.assertEquals(2L, results.get(3)); + + // Redis get command + Assert.assertEquals(String.class, results.get(4).getClass()); + Assert.assertEquals("2", results.get(4)); + + // Graph query result + Assert.assertEquals(ResultSetImpl.class, results.get(5).getClass()); + resultSet = (ResultSet) results.get(5); + + 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(7).getClass()); + resultSet = (ResultSet) results.get(7); + + 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")); + } + } + + @Test + public void testWriteTransactionWatch(){ + + RedisGraphContext c1 = api.getContext(); + RedisGraphContext c2 = api.getContext(); + + c1.watch("social"); + RedisGraphTransaction t1 = c1.multi(); + + + t1.query("social", "CREATE (:Person {name:'a'})"); + c2.query("social", "CREATE (:Person {name:'b'})"); + List returnValue = t1.exec(); + Assert.assertNull(returnValue); + c1.close(); + c2.close(); + } + + @Test + public void testReadTransactionWatch(){ + + RedisGraphContext c1 = api.getContext(); + RedisGraphContext c2 = api.getContext(); + Assert.assertNotEquals(c1.getConnectionContext(), c2.getConnectionContext()); + c1.query("social", "CREATE (:Person {name:'a'})"); + c1.watch("social"); + RedisGraphTransaction t1 = c1.multi(); + + Map params = new HashMap<>(); + params.put("name", 'b'); + t1.query("social", "CREATE (:Person {name:$name})", params); + c2.query("social", "MATCH (n) return n"); + List returnValue = t1.exec(); + + Assert.assertNotNull(returnValue); + c1.close(); + c2.close(); + } + + @Test + public void testMultiExecWithReadOnlyQueries(){ + try (RedisGraphContext c = api.getContext()) { + RedisGraphTransaction transaction = c.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")); + } + } +} diff --git a/src/test/java/com/redislabs/redisgraph/exceptions/JRedisGraphErrorTest.java b/src/test/java/com/redislabs/redisgraph/exceptions/JRedisGraphErrorTest.java new file mode 100644 index 0000000..f29bdaa --- /dev/null +++ b/src/test/java/com/redislabs/redisgraph/exceptions/JRedisGraphErrorTest.java @@ -0,0 +1,130 @@ +package com.redislabs.redisgraph.exceptions; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import java.util.HashMap; + +import org.junit.After; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; + +import com.redislabs.redisgraph.RedisGraphContext; +import com.redislabs.redisgraph.RedisGraphContextGenerator; +import com.redislabs.redisgraph.impl.api.RedisGraph; + +public class JRedisGraphErrorTest { + + private RedisGraphContextGenerator api; + + @Before + public void createApi() { + api = new RedisGraph(); + Assert.assertNotNull(api.query("social", "CREATE (:person{mixed_prop: 'strval'}), (:person{mixed_prop: 50})")); + } + + @After + public void deleteGraph() { + + api.deleteGraph("social"); + api.close(); + } + + @Test + public void testSyntaxErrorReporting() { + JRedisGraphException exception = assertThrows(JRedisGraphException.class, + () -> api.query("social", "RETURN toUpper(5)")); + assertTrue(exception.getMessage().contains("Type mismatch: expected String or Null but was Integer")); + } + + @Test + public void testRuntimeErrorReporting() { + JRedisGraphException exception = assertThrows(JRedisGraphException.class, + () -> api.query("social", "MATCH (p:person) RETURN toUpper(p.mixed_prop)")); + assertTrue(exception.getMessage().contains("Type mismatch: expected String or Null but was Integer")); + } + + @Test + public void testExceptionFlow() { + + try { + // Issue a query that causes a compile-time error + api.query("social", "RETURN toUpper(5)"); + } catch (Exception e) { + Assert.assertEquals(JRedisGraphException.class, e.getClass()); + Assert.assertTrue(e.getMessage().contains("Type mismatch: expected String or Null but was Integer")); + } + + // On general api usage, user should get a new connection + + try { + // Issue a query that causes a compile-time error + api.query("social", "MATCH (p:person) RETURN toUpper(p.mixed_prop)"); + } catch (Exception e) { + Assert.assertEquals(JRedisGraphException.class, e.getClass()); + Assert.assertTrue(e.getMessage().contains("Type mismatch: expected String or Null but was Integer")); + } + } + + @Test + public void testContextSyntaxErrorReporting() { + RedisGraphContext c = api.getContext(); + + JRedisGraphException exception = assertThrows(JRedisGraphException.class, + () -> c.query("social", "RETURN toUpper(5)")); + assertTrue(exception.getMessage().contains("Type mismatch: expected String or Null but was Integer")); + } + + @Test + public void testMissingParametersSyntaxErrorReporting() { + JRedisGraphException exception = assertThrows(JRedisGraphException.class, + () -> api.query("social", "RETURN $param")); + assertTrue(exception.getMessage().contains("Missing parameters")); + } + + @Test + public void testMissingParametersSyntaxErrorReporting2() { + JRedisGraphException exception = assertThrows(JRedisGraphException.class, + () -> api.query("social", "RETURN $param", new HashMap<>())); + assertTrue(exception.getMessage().contains("Missing parameters")); + } + + @Test + public void testContextRuntimeErrorReporting() { + RedisGraphContext c = api.getContext(); + + JRedisGraphException exception = assertThrows(JRedisGraphException.class, + () -> c.query("social", "MATCH (p:person) RETURN toUpper(p.mixed_prop)")); + assertTrue(exception.getMessage().contains("Type mismatch: expected String or Null but was Integer")); + } + + @Test + public void testContextExceptionFlow() { + + RedisGraphContext c = api.getContext(); + try { + // Issue a query that causes a compile-time error + c.query("social", "RETURN toUpper(5)"); + } catch (Exception e) { + Assert.assertEquals(JRedisGraphException.class, e.getClass()); + Assert.assertTrue(e.getMessage().contains("Type mismatch: expected String or Null but was Integer")); + } + + // On contexted api usage, connection should stay open + try { + // Issue a query that causes a compile-time error + c.query("social", "MATCH (p:person) RETURN toUpper(p.mixed_prop)"); + } catch (Exception e) { + Assert.assertEquals(JRedisGraphException.class, e.getClass()); + Assert.assertTrue(e.getMessage().contains("Type mismatch: expected String or Null but was Integer")); + } + } + + @Test + public void timeoutException() { + JRedisGraphException exception = assertThrows(JRedisGraphException.class, + () -> api.query("social", "UNWIND range(0,100000) AS x WITH x AS x WHERE x = 10000 RETURN x", 1L)); + assertTrue(exception.getMessage().contains("Query timed out")); + } +} diff --git a/src/test/java/com/redislabs/redisgraph/graph_entities/PathTest.java b/src/test/java/com/redislabs/redisgraph/graph_entities/PathTest.java new file mode 100644 index 0000000..ee65e68 --- /dev/null +++ b/src/test/java/com/redislabs/redisgraph/graph_entities/PathTest.java @@ -0,0 +1,75 @@ +package com.redislabs.redisgraph.graph_entities; + +import nl.jqno.equalsverifier.EqualsVerifier; +import org.junit.Test; + +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.*; + +public class PathTest { + + private Node buildNode(int id){ + Node n = new Node(); + n.setId(0); + return n; + } + + private Edge buildEdge(int id, int src, int dst){ + Edge e = new Edge(); + e.setId(id); + e.setSource(src); + e.setDestination(dst); + return e; + } + + private List buildNodeArray(int size) { + return IntStream.range(0, size).mapToObj(i -> buildNode(i)).collect(Collectors.toList()); + } + + private List buildEdgeArray(int size){ + return IntStream.range(0, size).mapToObj(i -> buildEdge(i, i, i+1)).collect(Collectors.toList()); + } + + private Path buildPath(int nodeCount){ + return new Path(buildNodeArray(nodeCount), buildEdgeArray(nodeCount-1)); + } + + @Test + public void testEmptyPath(){ + Path path = buildPath(0); + assertEquals(0, path.length()); + assertEquals(0, path.nodeCount()); + assertThrows(IndexOutOfBoundsException.class, ()->path.getNode(0)); + assertThrows(IndexOutOfBoundsException.class, ()->path.getEdge(0)); + } + + @Test + public void testSingleNodePath(){ + Path path = buildPath(1); + assertEquals(0, path.length()); + assertEquals(1, path.nodeCount()); + Node n = new Node(); + n.setId(0); + assertEquals(n, path.firstNode()); + assertEquals(n, path.lastNode()); + assertEquals(n, path.getNode(0)); + } + + @Test + public void testRandomLengthPath(){ + int nodeCount = ThreadLocalRandom.current().nextInt(2, 100 + 1); + Path path = buildPath(nodeCount); + assertEquals(buildNodeArray(nodeCount), path.getNodes()); + assertEquals(buildEdgeArray(nodeCount-1), path.getEdges()); + assertDoesNotThrow(()->path.getEdge(0)); + } + + @Test + public void hashCodeEqualTest(){ + EqualsVerifier.forClass(Path.class).verify(); + } +} \ No newline at end of file diff --git a/src/test/java/com/redislabs/redisgraph/impl/UtilsTest.java b/src/test/java/com/redislabs/redisgraph/impl/UtilsTest.java new file mode 100644 index 0000000..b745754 --- /dev/null +++ b/src/test/java/com/redislabs/redisgraph/impl/UtilsTest.java @@ -0,0 +1,82 @@ +package com.redislabs.redisgraph.impl; + +import static org.junit.Assert.assertThrows; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.IllegalFormatConversionException; +import java.util.List; +import java.util.Map; + +import org.junit.Assert; +import org.junit.Test; + +public class UtilsTest { + + @Test + public void testPrepareProcedure() { + Assert.assertEquals("CALL prc()", + Utils.prepareProcedure("prc", Arrays.asList(new String[]{}), new HashMap<>())); + + Assert.assertEquals("CALL prc(\"a\",\"b\")", + Utils.prepareProcedure("prc", Arrays.asList(new String[]{"a", "b"}), new HashMap<>())); + + Map> kwargs = new HashMap<>(); + kwargs.put("y", Arrays.asList(new String[]{"ka", "kb"})); + Assert.assertEquals("CALL prc(\"a\",\"b\")ka,kb", + Utils.prepareProcedure("prc", Arrays.asList(new String[]{"a", "b"}), kwargs)); + + Assert.assertEquals("CALL prc()ka,kb", Utils.prepareProcedure("prc", Arrays.asList(new String[]{}), kwargs)); + } + + @SuppressWarnings("deprecation") + @Test + public void prepareQuery() { + Assert.assertEquals("query %s %d end of query", Utils.prepareQuery("query %s %d end of query")); + + Assert.assertEquals("query 'a' 33 end of query", Utils.prepareQuery("query %s %d end of query", "a", 33)); + + assertThrows(IllegalFormatConversionException.class, + () -> Utils.prepareQuery("query %s %d end of query", "a", "b")); + } + + @Test + public void testParamsPrep() { + Map params = new HashMap<>(); + params.put("param", ""); + Assert.assertEquals("CYPHER param=\"\" RETURN $param", Utils.prepareQuery("RETURN $param", params)); + params.put("param", "\""); + Assert.assertEquals("CYPHER param=\"\\\"\" RETURN $param", Utils.prepareQuery("RETURN $param", params)); + params.put("param", "\"st"); + Assert.assertEquals("CYPHER param=\"\\\"st\" RETURN $param", Utils.prepareQuery("RETURN $param", params)); + params.put("param", 1); + Assert.assertEquals("CYPHER param=1 RETURN $param", Utils.prepareQuery("RETURN $param", params)); + params.put("param", 2.3); + Assert.assertEquals("CYPHER param=2.3 RETURN $param", Utils.prepareQuery("RETURN $param", params)); + params.put("param", true); + Assert.assertEquals("CYPHER param=true RETURN $param", Utils.prepareQuery("RETURN $param", params)); + params.put("param", false); + Assert.assertEquals("CYPHER param=false RETURN $param", Utils.prepareQuery("RETURN $param", params)); + params.put("param", null); + Assert.assertEquals("CYPHER param=null RETURN $param", Utils.prepareQuery("RETURN $param", params)); + params.put("param", "str"); + Assert.assertEquals("CYPHER param=\"str\" RETURN $param", Utils.prepareQuery("RETURN $param", params)); + params.put("param", "s\"tr"); + Assert.assertEquals("CYPHER param=\"s\\\"tr\" RETURN $param", Utils.prepareQuery("RETURN $param", params)); + Integer arr[] = {1, 2, 3}; + params.put("param", arr); + Assert.assertEquals("CYPHER param=[1, 2, 3] RETURN $param", Utils.prepareQuery("RETURN $param", params)); + List list = Arrays.asList(1, 2, 3); + params.put("param", list); + Assert.assertEquals("CYPHER param=[1, 2, 3] RETURN $param", Utils.prepareQuery("RETURN $param", params)); + String strArr[] = {"1", "2", "3"}; + params.put("param", strArr); + Assert.assertEquals("CYPHER param=[\"1\", \"2\", \"3\"] RETURN $param", + Utils.prepareQuery("RETURN $param", params)); + List stringList = Arrays.asList("1", "2", "3"); + params.put("param", stringList); + Assert.assertEquals("CYPHER param=[\"1\", \"2\", \"3\"] RETURN $param", + Utils.prepareQuery("RETURN $param", params)); + } + +} diff --git a/src/test/java/com/redislabs/redisgraph/test/utils/PathBuilder.java b/src/test/java/com/redislabs/redisgraph/test/utils/PathBuilder.java new file mode 100644 index 0000000..cf6db87 --- /dev/null +++ b/src/test/java/com/redislabs/redisgraph/test/utils/PathBuilder.java @@ -0,0 +1,50 @@ +package com.redislabs.redisgraph.test.utils; + +import com.redislabs.redisgraph.graph_entities.Edge; +import com.redislabs.redisgraph.graph_entities.Node; +import com.redislabs.redisgraph.graph_entities.Path; + +import java.util.ArrayList; +import java.util.List; + +public final class PathBuilder{ + private final List nodes; + private final List edges; + private Class currentAppendClass; + + public PathBuilder() { + this.nodes = new ArrayList<>(0); + this.edges = new ArrayList<>(0); + currentAppendClass = Node.class; + } + + public PathBuilder(int nodesCount){ + nodes = new ArrayList<>(nodesCount); + edges = new ArrayList<>(nodesCount-1 >= 0 ? nodesCount -1 : 0); + currentAppendClass = Node.class; + } + + public PathBuilder append(Object object){ + Class c = object.getClass(); + if(!currentAppendClass.equals(c)) throw new IllegalArgumentException("Path Builder expected " + currentAppendClass.getSimpleName() + " but was " + c.getSimpleName()); + if(c.equals(Node.class)) return appendNode((Node)object); + else return appendEdge((Edge)object); + } + + private PathBuilder appendEdge(Edge edge) { + edges.add(edge); + currentAppendClass = Node.class; + return this; + } + + private PathBuilder appendNode(Node node){ + nodes.add(node); + currentAppendClass = Edge.class; + return this; + } + + public Path build(){ + if(nodes.size() != edges.size() + 1) throw new IllegalArgumentException("Path builder nodes count should be edge count + 1"); + return new Path(nodes, edges); + } +} diff --git a/src/test/java/com/redislabs/redisgraph/test/utils/PathBuilderTest.java b/src/test/java/com/redislabs/redisgraph/test/utils/PathBuilderTest.java new file mode 100644 index 0000000..d6a0c03 --- /dev/null +++ b/src/test/java/com/redislabs/redisgraph/test/utils/PathBuilderTest.java @@ -0,0 +1,29 @@ +package com.redislabs.redisgraph.test.utils; + +import static org.junit.Assert.assertThrows; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +import com.redislabs.redisgraph.graph_entities.Edge; + +public class PathBuilderTest { + + @Test + public void testPathBuilderSizeException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + PathBuilder builder = new PathBuilder(0); + builder.build(); + }); + assertTrue(exception.getMessage().equalsIgnoreCase("Path builder nodes count should be edge count + 1")); + } + + @Test + public void testPathBuilderArgumentsException() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + PathBuilder builder = new PathBuilder(0); + builder.append(new Edge()); + }); + assertTrue(exception.getMessage().equalsIgnoreCase("Path Builder expected Node but was Edge")); + } +}