From da221d99b935d278358ce6fdfe6929d51ce83448 Mon Sep 17 00:00:00 2001 From: lutovich Date: Thu, 28 Sep 2017 18:59:15 +0200 Subject: [PATCH 1/5] Added `StatementResultCursor#singleAsync()` Which returns future of a record if result contains a single record. Otherwise it returns a failed future. This method is convenient and safe shortcut for consuming a single-record result. --- .../internal/InternalStatementResult.java | 5 +- .../async/InternalStatementResultCursor.java | 22 +++++ .../driver/v1/StatementResultCursor.java | 2 + .../driver/v1/integration/SessionAsyncIT.java | 77 ++++++++++++++++- .../v1/integration/TransactionAsyncIT.java | 84 ++++++++++++++++++- 5 files changed, 183 insertions(+), 7 deletions(-) diff --git a/driver/src/main/java/org/neo4j/driver/internal/InternalStatementResult.java b/driver/src/main/java/org/neo4j/driver/internal/InternalStatementResult.java index 5eac6f1e6a..98547d4f47 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/InternalStatementResult.java +++ b/driver/src/main/java/org/neo4j/driver/internal/InternalStatementResult.java @@ -106,9 +106,8 @@ public Record single() if ( hasMoreThanOne ) { - throw new NoSuchRecordException( "Expected a result with a single record, but this result contains at least one more. " + - "Ensure your query returns only one record, or use `first` instead of `single` if " + - "you do not care about the number of records in the result." ); + throw new NoSuchRecordException( "Expected result with a single record, but it contains " + + "at least one more. Ensure your query returns only one record." ); } return single; diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java b/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java index 004ddceafb..6ae65b932c 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java @@ -29,6 +29,7 @@ import org.neo4j.driver.internal.handlers.RunResponseHandler; import org.neo4j.driver.v1.Record; import org.neo4j.driver.v1.StatementResultCursor; +import org.neo4j.driver.v1.exceptions.NoSuchRecordException; import org.neo4j.driver.v1.summary.ResultSummary; import static java.util.Objects.requireNonNull; @@ -84,6 +85,27 @@ public CompletionStage peekAsync() return peekedRecordFuture; } + @Override + public CompletionStage singleAsync() + { + return nextAsync().thenCompose( firstRecord -> + { + if ( firstRecord == null ) + { + throw new NoSuchRecordException( "Cannot retrieve a single record, because this cursor is empty." ); + } + return nextAsync().thenApply( secondRecord -> + { + if ( secondRecord != null ) + { + throw new NoSuchRecordException( "Expected cursor with a single record, but it contains " + + "at least one more. Ensure your query returns only one record." ); + } + return firstRecord; + } ); + } ); + } + @Override public CompletionStage forEachAsync( Consumer action ) { diff --git a/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java b/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java index 610c8e7f5d..6e70903aaf 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java +++ b/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java @@ -39,6 +39,8 @@ public interface StatementResultCursor CompletionStage peekAsync(); + CompletionStage singleAsync(); + CompletionStage forEachAsync( Consumer action ); CompletionStage> listAsync(); diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java index 6b03af217a..1015184f95 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java @@ -42,6 +42,7 @@ import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.exceptions.ClientException; import org.neo4j.driver.v1.exceptions.DatabaseException; +import org.neo4j.driver.v1.exceptions.NoSuchRecordException; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; import org.neo4j.driver.v1.exceptions.SessionExpiredException; import org.neo4j.driver.v1.exceptions.TransientException; @@ -54,6 +55,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.startsWith; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -196,7 +198,6 @@ public void shouldFailWhenServerIsRestarted() } catch ( Throwable t ) { - t.printStackTrace(); assertThat( t, instanceOf( ServiceUnavailableException.class ) ); } } @@ -496,6 +497,80 @@ public void shouldConvertToListWithNonEmptyCursor() Arrays.asList( 1L, 11L, 21L, 31L, 41L, 51L, 61L, 71L, 81L, 91L ) ); } + @Test + public void shouldFailSingleWithEmptyCursor() + { + StatementResultCursor cursor = await( session.runAsync( "CREATE ()" ) ); + + try + { + await( cursor.singleAsync() ); + fail( "Exception expected" ); + } + catch ( NoSuchRecordException e ) + { + assertThat( e.getMessage(), containsString( "cursor is empty" ) ); + } + } + + @Test + public void shouldFailSingleWithMultiRecordCursor() + { + StatementResultCursor cursor = await( session.runAsync( "UNWIND [1, 2, 3] AS x RETURN x" ) ); + + try + { + await( cursor.singleAsync() ); + fail( "Exception expected" ); + } + catch ( NoSuchRecordException e ) + { + assertThat( e.getMessage(), startsWith( "Expected cursor with a single record" ) ); + } + } + + @Test + public void shouldReturnSingleWithSingleRecordCursor() + { + StatementResultCursor cursor = await( session.runAsync( "RETURN 42" ) ); + + Record record = await( cursor.singleAsync() ); + + assertEquals( 42, record.get( 0 ).asInt() ); + } + + @Test + public void shouldPropagateFailureFromFirstRecordInSingleAsync() + { + StatementResultCursor cursor = await( session.runAsync( "UNWIND [0] AS x RETURN 10 / x" ) ); + + try + { + await( cursor.singleAsync() ); + fail( "Exception expected" ); + } + catch ( ClientException e ) + { + assertThat( e.getMessage(), containsString( "/ by zero" ) ); + } + } + + @Test + public void shouldNotPropagateFailureFromSecondRecordInSingleAsync() + { + StatementResultCursor cursor = await( session.runAsync( "UNWIND [1, 0] AS x RETURN 10 / x" ) ); + + try + { + await( cursor.singleAsync() ); + fail( "Exception expected" ); + } + catch ( ClientException e ) + { + assertThat( e.getMessage(), containsString( "/ by zero" ) ); + } + } + private Future>> runNestedQueries( StatementResultCursor inputCursor ) { CompletableFuture>> resultFuture = new CompletableFuture<>(); diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java index d364fe6549..93d5d91ea1 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java @@ -23,7 +23,6 @@ import org.junit.Rule; import org.junit.Test; -import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -39,6 +38,7 @@ import org.neo4j.driver.v1.Transaction; import org.neo4j.driver.v1.Value; import org.neo4j.driver.v1.exceptions.ClientException; +import org.neo4j.driver.v1.exceptions.NoSuchRecordException; import org.neo4j.driver.v1.exceptions.ServiceUnavailableException; import org.neo4j.driver.v1.summary.ResultSummary; import org.neo4j.driver.v1.summary.StatementType; @@ -578,7 +578,7 @@ public void shouldConvertToListWithNonEmptyCursor() } @Test - public void shouldFailWhenServerIsRestarted() throws IOException + public void shouldFailWhenServerIsRestarted() { Transaction tx = await( session.beginTransactionAsync() ); @@ -592,11 +592,89 @@ public void shouldFailWhenServerIsRestarted() throws IOException } catch ( Throwable t ) { - t.printStackTrace(); assertThat( t, instanceOf( ServiceUnavailableException.class ) ); } } + @Test + public void shouldFailSingleWithEmptyCursor() + { + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( "MATCH (n:NoSuchLabel) RETURN n" ) ); + + try + { + await( cursor.singleAsync() ); + fail( "Exception expected" ); + } + catch ( NoSuchRecordException e ) + { + assertThat( e.getMessage(), containsString( "cursor is empty" ) ); + } + } + + @Test + public void shouldFailSingleWithMultiRecordCursor() + { + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( "UNWIND ['a', 'b'] AS x RETURN x" ) ); + + try + { + await( cursor.singleAsync() ); + fail( "Exception expected" ); + } + catch ( NoSuchRecordException e ) + { + assertThat( e.getMessage(), startsWith( "Expected cursor with a single record" ) ); + } + } + + @Test + public void shouldReturnSingleWithSingleRecordCursor() + { + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( "RETURN 'Hello!'" ) ); + + Record record = await( cursor.singleAsync() ); + + assertEquals( "Hello!", record.get( 0 ).asString() ); + } + + @Test + public void shouldPropagateFailureFromFirstRecordInSingleAsync() + { + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( "UNWIND [0] AS x RETURN 10 / x" ) ); + + try + { + await( cursor.singleAsync() ); + fail( "Exception expected" ); + } + catch ( ClientException e ) + { + assertThat( e.getMessage(), containsString( "/ by zero" ) ); + } + } + + @Test + public void shouldNotPropagateFailureFromSecondRecordInSingleAsync() + { + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( "UNWIND [1, 0] AS x RETURN 10 / x" ) ); + + try + { + await( cursor.singleAsync() ); + fail( "Exception expected" ); + } + catch ( ClientException e ) + { + assertThat( e.getMessage(), containsString( "/ by zero" ) ); + } + } + private int countNodes( Object id ) { StatementResult result = session.run( "MATCH (n:Node {id: $id}) RETURN count(n)", parameters( "id", id ) ); From e645f9c7cde756cf47604c30b85fa628f367b096 Mon Sep 17 00:00:00 2001 From: lutovich Date: Thu, 28 Sep 2017 23:18:36 +0200 Subject: [PATCH 2/5] Make `#forEachAsync()` return summary `StatementResultCursor#forEachAsync()` will now return future of `ResultSummary` instead of future of void. This simplifies the API a bit an makes it more convenient to use summary after forEach is completed. --- .../internal/async/InternalStatementResultCursor.java | 4 ++-- .../org/neo4j/driver/v1/StatementResultCursor.java | 2 +- .../neo4j/driver/v1/integration/SessionAsyncIT.java | 10 +++++++--- .../driver/v1/integration/TransactionAsyncIT.java | 10 +++++++--- 4 files changed, 17 insertions(+), 9 deletions(-) diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java b/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java index 6ae65b932c..67bce881b2 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java @@ -107,11 +107,11 @@ public CompletionStage singleAsync() } @Override - public CompletionStage forEachAsync( Consumer action ) + public CompletionStage forEachAsync( Consumer action ) { CompletableFuture resultFuture = new CompletableFuture<>(); internalForEachAsync( action, resultFuture ); - return resultFuture; + return resultFuture.thenCompose( ignore -> summaryAsync() ); } @Override diff --git a/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java b/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java index 6e70903aaf..d8c98e1233 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java +++ b/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java @@ -41,7 +41,7 @@ public interface StatementResultCursor CompletionStage singleAsync(); - CompletionStage forEachAsync( Consumer action ); + CompletionStage forEachAsync( Consumer action ); CompletionStage> listAsync(); } diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java index 1015184f95..5ca44cd135 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java @@ -52,6 +52,7 @@ import org.neo4j.driver.v1.util.TestNeo4j; import static java.util.Collections.emptyIterator; +import static java.util.Collections.emptyMap; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; @@ -635,10 +636,13 @@ private void testForEach( String query, int expectedSeenRecords ) { StatementResultCursor cursor = await( session.runAsync( query ) ); - final AtomicInteger recordsSeen = new AtomicInteger(); - CompletionStage forEachDone = cursor.forEachAsync( record -> recordsSeen.incrementAndGet() ); + AtomicInteger recordsSeen = new AtomicInteger(); + CompletionStage forEachDone = cursor.forEachAsync( record -> recordsSeen.incrementAndGet() ); + ResultSummary summary = await( forEachDone ); - assertNull( await( forEachDone ) ); + assertNotNull( summary ); + assertEquals( query, summary.statement().text() ); + assertEquals( emptyMap(), summary.statement().parameters().asMap() ); assertEquals( expectedSeenRecords, recordsSeen.get() ); } diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java index 93d5d91ea1..7290a1b416 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java @@ -45,6 +45,7 @@ import org.neo4j.driver.v1.types.Node; import org.neo4j.driver.v1.util.TestNeo4j; +import static java.util.Collections.emptyMap; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; @@ -686,10 +687,13 @@ private void testForEach( String query, int expectedSeenRecords ) Transaction tx = await( session.beginTransactionAsync() ); StatementResultCursor cursor = await( tx.runAsync( query ) ); - final AtomicInteger recordsSeen = new AtomicInteger(); - CompletionStage forEachDone = cursor.forEachAsync( record -> recordsSeen.incrementAndGet() ); + AtomicInteger recordsSeen = new AtomicInteger(); + CompletionStage forEachDone = cursor.forEachAsync( record -> recordsSeen.incrementAndGet() ); + ResultSummary summary = await( forEachDone ); - assertNull( await( forEachDone ) ); + assertNotNull( summary ); + assertEquals( query, summary.statement().text() ); + assertEquals( emptyMap(), summary.statement().parameters().asMap() ); assertEquals( expectedSeenRecords, recordsSeen.get() ); } From b894f7967084048c5c09d45f6047a0a1decdc535 Mon Sep 17 00:00:00 2001 From: lutovich Date: Thu, 28 Sep 2017 23:49:19 +0200 Subject: [PATCH 3/5] Added `#listAsync(Function)` New method allows application of a transformation function to each record. Returned list will contain transformed values. This allows writing a bit more concise code when records need to be converted to some other objects. Returned future will be failed if given function ever throws. Also improved error handling in `#forEachAsync(Consumer)`. --- .../async/InternalStatementResultCursor.java | 40 +++++++++-- .../driver/v1/StatementResultCursor.java | 3 + .../driver/v1/integration/SessionAsyncIT.java | 64 ++++++++++++++++- .../v1/integration/TransactionAsyncIT.java | 69 ++++++++++++++++++- 4 files changed, 167 insertions(+), 9 deletions(-) diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java b/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java index 67bce881b2..2efda4e23f 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java @@ -24,6 +24,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; +import java.util.function.Function; import org.neo4j.driver.internal.handlers.PullAllResponseHandler; import org.neo4j.driver.internal.handlers.RunResponseHandler; @@ -117,8 +118,14 @@ public CompletionStage forEachAsync( Consumer action ) @Override public CompletionStage> listAsync() { - CompletableFuture> resultFuture = new CompletableFuture<>(); - internalListAsync( new ArrayList<>(), resultFuture ); + return listAsync( Function.identity() ); + } + + @Override + public CompletionStage> listAsync( Function mapFunction ) + { + CompletableFuture> resultFuture = new CompletableFuture<>(); + internalListAsync( new ArrayList<>(), resultFuture, mapFunction ); return resultFuture; } @@ -136,7 +143,15 @@ private void internalForEachAsync( Consumer action, CompletableFuture records, CompletableFuture> resultFuture ) + private void internalListAsync( List result, CompletableFuture> resultFuture, + Function mapFunction ) { CompletionStage recordFuture = nextAsync(); @@ -160,12 +176,22 @@ private void internalListAsync( List records, CompletableFuture forEachAsync( Consumer action ); CompletionStage> listAsync(); + + CompletionStage> listAsync( Function mapFunction ); } diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java index 5ca44cd135..9baff7332d 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java @@ -22,13 +22,17 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.Timeout; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; @@ -74,8 +78,10 @@ public class SessionAsyncIT { + private final TestNeo4j neo4j = new TestNeo4j(); + @Rule - public final TestNeo4j neo4j = new TestNeo4j(); + public final RuleChain ruleChain = RuleChain.outerRule( Timeout.seconds( 20 ) ).around( neo4j ); private Session session; @@ -485,6 +491,26 @@ public void shouldForEachWithNonEmptyCursor() testForEach( "UNWIND range(1, 100000) AS x RETURN x", 100000 ); } + @Test + public void shouldFailForEachWhenActionFails() + { + StatementResultCursor cursor = await( session.runAsync( "RETURN 42" ) ); + IOException error = new IOException( "Hi" ); + + try + { + await( cursor.forEachAsync( record -> + { + throw new CompletionException( error ); + } ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertEquals( error, e ); + } + } + @Test public void shouldConvertToListWithEmptyCursor() { @@ -498,6 +524,42 @@ public void shouldConvertToListWithNonEmptyCursor() Arrays.asList( 1L, 11L, 21L, 31L, 41L, 51L, 61L, 71L, 81L, 91L ) ); } + @Test + public void shouldConvertToTransformedListWithEmptyCursor() + { + StatementResultCursor cursor = await( session.runAsync( "CREATE ()" ) ); + List strings = await( cursor.listAsync( record -> "Hi!" ) ); + assertEquals( 0, strings.size() ); + } + + @Test + public void shouldConvertToTransformedListWithNonEmptyCursor() + { + StatementResultCursor cursor = await( session.runAsync( "UNWIND [1,2,3] AS x RETURN x" ) ); + List ints = await( cursor.listAsync( record -> record.get( 0 ).asInt() + 1 ) ); + assertEquals( Arrays.asList( 2, 3, 4 ), ints ); + } + + @Test + public void shouldFailWhenListTransformationFunctionFails() + { + StatementResultCursor cursor = await( session.runAsync( "RETURN 42" ) ); + RuntimeException error = new RuntimeException( "Hi!" ); + + try + { + await( cursor.listAsync( record -> + { + throw error; + } ) ); + fail( "Exception expected" ); + } + catch ( RuntimeException e ) + { + assertEquals( error, e ); + } + } + @Test public void shouldFailSingleWithEmptyCursor() { diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java index 7290a1b416..f9f8e2db60 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java @@ -22,11 +22,16 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.RuleChain; +import org.junit.rules.Timeout; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.concurrent.atomic.AtomicInteger; @@ -69,8 +74,10 @@ public class TransactionAsyncIT { + private final TestNeo4j neo4j = new TestNeo4j(); + @Rule - public final TestNeo4j neo4j = new TestNeo4j(); + public final RuleChain ruleChain = RuleChain.outerRule( Timeout.seconds( 60 ) ).around( neo4j ); private Session session; @@ -566,6 +573,27 @@ public void shouldForEachWithNonEmptyCursor() testForEach( "UNWIND range(1, 12555) AS x CREATE (n:Node {id: x}) RETURN n", 12555 ); } + @Test + public void shouldFailForEachWhenActionFails() + { + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( "RETURN 'Hi!'" ) ); + RuntimeException error = new RuntimeException(); + + try + { + await( cursor.forEachAsync( record -> + { + throw error; + } ) ); + fail( "Exception expected" ); + } + catch ( RuntimeException e ) + { + assertEquals( error, e ); + } + } + @Test public void shouldConvertToListWithEmptyCursor() { @@ -578,6 +606,45 @@ public void shouldConvertToListWithNonEmptyCursor() testList( "UNWIND [1, '1', 2, '2', 3, '3'] AS x RETURN x", Arrays.asList( 1L, "1", 2L, "2", 3L, "3" ) ); } + @Test + public void shouldConvertToTransformedListWithEmptyCursor() + { + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( "CREATE ()" ) ); + List> maps = await( cursor.listAsync( record -> record.get( 0 ).asMap() ) ); + assertEquals( 0, maps.size() ); + } + + @Test + public void shouldConvertToTransformedListWithNonEmptyCursor() + { + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( "UNWIND ['a', 'b', 'c'] AS x RETURN x" ) ); + List strings = await( cursor.listAsync( record -> record.get( 0 ).asString() + "!" ) ); + assertEquals( Arrays.asList( "a!", "b!", "c!" ), strings ); + } + + @Test + public void shouldFailWhenListTransformationFunctionFails() + { + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( "RETURN 'Hello'" ) ); + IOException error = new IOException( "World" ); + + try + { + await( cursor.listAsync( record -> + { + throw new CompletionException( error ); + } ) ); + fail( "Exception expected" ); + } + catch ( Exception e ) + { + assertEquals( error, e ); + } + } + @Test public void shouldFailWhenServerIsRestarted() { From c6b25bf5ddcfcd6781bc8e1ebe706ed0e8164453 Mon Sep 17 00:00:00 2001 From: lutovich Date: Thu, 28 Sep 2017 23:59:05 +0200 Subject: [PATCH 4/5] Added `StatementResultCursor#consumeAsync()` Which allows skip/drop all incoming records and returns future of `ResultSummary`. --- .../async/InternalStatementResultCursor.java | 8 ++++++ .../driver/v1/StatementResultCursor.java | 2 ++ .../driver/v1/integration/SessionAsyncIT.java | 25 ++++++++++++++++++ .../v1/integration/TransactionAsyncIT.java | 26 +++++++++++++++++++ 4 files changed, 61 insertions(+) diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java b/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java index 2efda4e23f..92b86b01c8 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java @@ -107,6 +107,14 @@ public CompletionStage singleAsync() } ); } + @Override + public CompletionStage consumeAsync() + { + return forEachAsync( record -> + { + } ); + } + @Override public CompletionStage forEachAsync( Consumer action ) { diff --git a/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java b/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java index 9a1c46a642..6f326afd16 100644 --- a/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java +++ b/driver/src/main/java/org/neo4j/driver/v1/StatementResultCursor.java @@ -42,6 +42,8 @@ public interface StatementResultCursor CompletionStage singleAsync(); + CompletionStage consumeAsync(); + CompletionStage forEachAsync( Consumer action ); CompletionStage> listAsync(); diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java index 9baff7332d..4781e77663 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java @@ -634,6 +634,18 @@ public void shouldNotPropagateFailureFromSecondRecordInSingleAsync() } } + @Test + public void shouldConsumeEmptyCursor() + { + testConsume( "CREATE ()" ); + } + + @Test + public void shouldConsumeNonEmptyCursor() + { + testConsume( "UNWIND [42, 42] AS x RETURN x" ); + } + private Future>> runNestedQueries( StatementResultCursor inputCursor ) { CompletableFuture>> resultFuture = new CompletableFuture<>(); @@ -720,6 +732,19 @@ private void testList( String query, List expectedList ) assertEquals( expectedList, actualList ); } + private void testConsume( String query ) + { + StatementResultCursor cursor = await( session.runAsync( query ) ); + ResultSummary summary = await( cursor.consumeAsync() ); + + assertNotNull( summary ); + assertEquals( query, summary.statement().text() ); + assertEquals( emptyMap(), summary.statement().parameters().asMap() ); + + // no records should be available, they should all be consumed + assertNull( await( cursor.nextAsync() ) ); + } + private static class InvocationTrackingWork implements TransactionWork> { final String query; diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java index f9f8e2db60..34beab6038 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java @@ -743,6 +743,18 @@ public void shouldNotPropagateFailureFromSecondRecordInSingleAsync() } } + @Test + public void shouldConsumeEmptyCursor() + { + testConsume( "MATCH (n:NoSuchLabel) RETURN n" ); + } + + @Test + public void shouldConsumeNonEmptyCursor() + { + testConsume( "RETURN 42" ); + } + private int countNodes( Object id ) { StatementResult result = session.run( "MATCH (n:Node {id: $id}) RETURN count(n)", parameters( "id", id ) ); @@ -776,4 +788,18 @@ private void testList( String query, List expectedList ) } assertEquals( expectedList, actualList ); } + + private void testConsume( String query ) + { + Transaction tx = await( session.beginTransactionAsync() ); + StatementResultCursor cursor = await( tx.runAsync( query ) ); + ResultSummary summary = await( cursor.consumeAsync() ); + + assertNotNull( summary ); + assertEquals( query, summary.statement().text() ); + assertEquals( emptyMap(), summary.statement().parameters().asMap() ); + + // no records should be available, they should all be consumed + assertNull( await( cursor.nextAsync() ) ); + } } From fe68cf4413c7c76051bb16cba2e1d75bf55671cf Mon Sep 17 00:00:00 2001 From: lutovich Date: Fri, 29 Sep 2017 10:30:31 +0200 Subject: [PATCH 5/5] Fixed exception messages Otherwise TCK tests fail. --- .../org/neo4j/driver/internal/InternalStatementResult.java | 2 +- .../driver/internal/async/InternalStatementResultCursor.java | 5 +++-- .../java/org/neo4j/driver/v1/integration/SessionAsyncIT.java | 2 +- .../org/neo4j/driver/v1/integration/TransactionAsyncIT.java | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/driver/src/main/java/org/neo4j/driver/internal/InternalStatementResult.java b/driver/src/main/java/org/neo4j/driver/internal/InternalStatementResult.java index 98547d4f47..e2e768269e 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/InternalStatementResult.java +++ b/driver/src/main/java/org/neo4j/driver/internal/InternalStatementResult.java @@ -106,7 +106,7 @@ public Record single() if ( hasMoreThanOne ) { - throw new NoSuchRecordException( "Expected result with a single record, but it contains " + + throw new NoSuchRecordException( "Expected a result with a single record, but this result contains " + "at least one more. Ensure your query returns only one record." ); } diff --git a/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java b/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java index 92b86b01c8..90a0848dfd 100644 --- a/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java +++ b/driver/src/main/java/org/neo4j/driver/internal/async/InternalStatementResultCursor.java @@ -99,8 +99,9 @@ public CompletionStage singleAsync() { if ( secondRecord != null ) { - throw new NoSuchRecordException( "Expected cursor with a single record, but it contains " + - "at least one more. Ensure your query returns only one record." ); + throw new NoSuchRecordException( "Expected a cursor with a single record, but this cursor " + + "contains at least one more. Ensure your query returns only " + + "one record." ); } return firstRecord; } ); diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java index 4781e77663..3fe52d7f8d 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/SessionAsyncIT.java @@ -588,7 +588,7 @@ public void shouldFailSingleWithMultiRecordCursor() } catch ( NoSuchRecordException e ) { - assertThat( e.getMessage(), startsWith( "Expected cursor with a single record" ) ); + assertThat( e.getMessage(), startsWith( "Expected a cursor with a single record" ) ); } } diff --git a/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java b/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java index 34beab6038..ac256c4f12 100644 --- a/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java +++ b/driver/src/test/java/org/neo4j/driver/v1/integration/TransactionAsyncIT.java @@ -694,7 +694,7 @@ public void shouldFailSingleWithMultiRecordCursor() } catch ( NoSuchRecordException e ) { - assertThat( e.getMessage(), startsWith( "Expected cursor with a single record" ) ); + assertThat( e.getMessage(), startsWith( "Expected a cursor with a single record" ) ); } }