diff --git a/driver/clirr-ignored-differences.xml b/driver/clirr-ignored-differences.xml
index 424fb74730..e601ce6ce8 100644
--- a/driver/clirr-ignored-differences.xml
+++ b/driver/clirr-ignored-differences.xml
@@ -409,4 +409,16 @@
org.neo4j.driver.BaseSession session(java.lang.Class, org.neo4j.driver.SessionConfig)
+
+ org/neo4j/driver/Driver
+ 7012
+ org.neo4j.driver.QueryTask queryTask(java.lang.String)
+
+
+
+ org/neo4j/driver/Driver
+ 7012
+ org.neo4j.driver.BookmarkManager queryBookmarkManager()
+
+
diff --git a/driver/src/main/java/org/neo4j/driver/Config.java b/driver/src/main/java/org/neo4j/driver/Config.java
index 45746cbe62..5c424d586f 100644
--- a/driver/src/main/java/org/neo4j/driver/Config.java
+++ b/driver/src/main/java/org/neo4j/driver/Config.java
@@ -75,6 +75,8 @@ public final class Config implements Serializable {
private static final Config EMPTY = builder().build();
+ private final BookmarkManager queryBookmarkManager;
+
/**
* User defined logging
*/
@@ -102,6 +104,7 @@ public final class Config implements Serializable {
private final MetricsAdapter metricsAdapter;
private Config(ConfigBuilder builder) {
+ this.queryBookmarkManager = builder.queryBookmarkManager;
this.logging = builder.logging;
this.logLeakedSessions = builder.logLeakedSessions;
@@ -123,6 +126,19 @@ private Config(ConfigBuilder builder) {
this.metricsAdapter = builder.metricsAdapter;
}
+ /**
+ * A {@link BookmarkManager} implementation for the driver to use on
+ * {@link Driver#queryTask(String)} method and its variants by default.
+ *
+ * Please note that sessions will not use this automatically, but it is possible to enable it explicitly
+ * using {@link SessionConfig.Builder#withBookmarkManager(BookmarkManager)}.
+ *
+ * @return bookmark manager, must not be {@code null}
+ */
+ public BookmarkManager queryBookmarkManager() {
+ return queryBookmarkManager;
+ }
+
/**
* Logging provider
*
@@ -262,6 +278,8 @@ public String userAgent() {
* Used to build new config instances
*/
public static final class ConfigBuilder {
+ private BookmarkManager queryBookmarkManager =
+ BookmarkManagers.defaultManager(BookmarkManagerConfig.builder().build());
private Logging logging = DEV_NULL_LOGGING;
private boolean logLeakedSessions;
private int maxConnectionPoolSize = PoolSettings.DEFAULT_MAX_CONNECTION_POOL_SIZE;
@@ -281,6 +299,22 @@ public static final class ConfigBuilder {
private ConfigBuilder() {}
+ /**
+ * Sets a {@link BookmarkManager} implementation for the driver to use on
+ * {@link Driver#queryTask(String)} method and its variants by default.
+ *
+ * Please note that sessions will not use this automatically, but it is possible to enable it explicitly
+ * using {@link SessionConfig.Builder#withBookmarkManager(BookmarkManager)}.
+ *
+ * @param queryBookmarkManager bookmark manager, must not be {@code null}
+ * @return this builder
+ */
+ public ConfigBuilder withQueryBookmarkManager(BookmarkManager queryBookmarkManager) {
+ Objects.requireNonNull(queryBookmarkManager, "queryBookmarkManager must not be null");
+ this.queryBookmarkManager = queryBookmarkManager;
+ return this;
+ }
+
/**
* Provide a logging implementation for the driver to use. Java logging framework {@link java.util.logging} with {@link Level#INFO} is used by default.
* Callers are expected to either implement {@link Logging} interface or provide one of the existing implementations available from static factory
diff --git a/driver/src/main/java/org/neo4j/driver/Driver.java b/driver/src/main/java/org/neo4j/driver/Driver.java
index 3e9a485312..2701ba7c51 100644
--- a/driver/src/main/java/org/neo4j/driver/Driver.java
+++ b/driver/src/main/java/org/neo4j/driver/Driver.java
@@ -63,6 +63,24 @@
* @since 1.0 (Modified and Added {@link AsyncSession} and {@link RxSession} since 2.0)
*/
public interface Driver extends AutoCloseable {
+ /**
+ * Creates a new {@link QueryTask} instance that executes an idempotent query in a managed transaction with
+ * automatic retries on retryable errors.
+ *
+ * @param query query string
+ * @return new query task instance
+ * @since 5.5
+ */
+ @Experimental
+ QueryTask queryTask(String query);
+
+ /**
+ * Returns an instance of {@link BookmarkManager} used by {@link QueryTask} instances by default.
+ *
+ * @return bookmark manager, must not be {@code null}
+ */
+ BookmarkManager queryBookmarkManager();
+
/**
* Return a flag to indicate whether or not encryption is used for this driver.
*
@@ -84,6 +102,7 @@ default Session session() {
/**
* Instantiate a new {@link Session} with a specified {@link SessionConfig session configuration}.
* Use {@link SessionConfig#forDatabase(String)} to obtain a general purpose session configuration for the specified database.
+ *
* @param sessionConfig specifies session configurations for this session.
* @return a new {@link Session} object.
* @see SessionConfig
@@ -257,6 +276,7 @@ default AsyncSession asyncSession(SessionConfig sessionConfig) {
/**
* Returns the driver metrics if metrics reporting is enabled via {@link Config.ConfigBuilder#withDriverMetrics()}.
* Otherwise, a {@link ClientException} will be thrown.
+ *
* @return the driver metrics if enabled.
* @throws ClientException if the driver metrics reporting is not enabled.
*/
@@ -281,7 +301,7 @@ default AsyncSession asyncSession(SessionConfig sessionConfig) {
/**
* This verifies if the driver can connect to a remote server or a cluster
* by establishing a network connection with the remote and possibly exchanging a few data before closing the connection.
- *
+ *
* It throws exception if fails to connect. Use the exception to further understand the cause of the connectivity problem.
* Note: Even if this method throws an exception, the driver still need to be closed via {@link #close()} to free up all resources.
*/
@@ -290,7 +310,7 @@ default AsyncSession asyncSession(SessionConfig sessionConfig) {
/**
* This verifies if the driver can connect to a remote server or cluster
* by establishing a network connection with the remote and possibly exchanging a few data before closing the connection.
- *
+ *
* This operation is asynchronous and returns a {@link CompletionStage}. This stage is completed with
* {@code null} when the driver connects to the remote server or cluster successfully.
* It is completed exceptionally if the driver failed to connect the remote server or cluster.
@@ -303,12 +323,14 @@ default AsyncSession asyncSession(SessionConfig sessionConfig) {
/**
* Returns true if the server or cluster the driver connects to supports multi-databases, otherwise false.
+ *
* @return true if the server or cluster the driver connects to supports multi-databases, otherwise false.
*/
boolean supportsMultiDb();
/**
* Asynchronous check if the server or cluster the driver connects to supports multi-databases.
+ *
* @return a {@link CompletionStage completion stage} that returns true if the server or cluster
* the driver connects to supports multi-databases, otherwise false.
*/
diff --git a/driver/src/main/java/org/neo4j/driver/EagerResult.java b/driver/src/main/java/org/neo4j/driver/EagerResult.java
new file mode 100644
index 0000000000..006c67684f
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/EagerResult.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [http://neo4j.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.neo4j.driver;
+
+import java.util.List;
+import org.neo4j.driver.summary.ResultSummary;
+import org.neo4j.driver.util.Experimental;
+
+/**
+ * An in-memory result of executing a Cypher query that has been consumed in full.
+ * @since 5.5
+ */
+@Experimental
+public interface EagerResult {
+ /**
+ * Returns the keys of the records this result contains.
+ *
+ * @return list of keys
+ */
+ List keys();
+
+ /**
+ * Returns the list of records this result contains.
+ *
+ * @return list of records
+ */
+ List records();
+
+ /**
+ * Returns the result summary.
+ *
+ * @return result summary
+ */
+ ResultSummary summary();
+}
diff --git a/driver/src/main/java/org/neo4j/driver/QueryConfig.java b/driver/src/main/java/org/neo4j/driver/QueryConfig.java
new file mode 100644
index 0000000000..2abce49f52
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/QueryConfig.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [http://neo4j.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.neo4j.driver;
+
+import static java.util.Objects.requireNonNull;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Objects;
+import java.util.Optional;
+import org.neo4j.driver.util.Experimental;
+
+/**
+ * Query configuration used by {@link Driver#queryTask(String)} and its variants.
+ * @since 5.5
+ */
+@Experimental
+public final class QueryConfig implements Serializable {
+ @Serial
+ private static final long serialVersionUID = -2632780731598141754L;
+
+ private static final QueryConfig DEFAULT = builder().build();
+
+ private final RoutingControl routing;
+ private final String database;
+ private final String impersonatedUser;
+ private final BookmarkManager bookmarkManager;
+ private final boolean useDefaultBookmarkManager;
+
+ /**
+ * Returns default config value.
+ *
+ * @return config value
+ */
+ public static QueryConfig defaultConfig() {
+ return DEFAULT;
+ }
+
+ private QueryConfig(Builder builder) {
+ this.routing = builder.routing;
+ this.database = builder.database;
+ this.impersonatedUser = builder.impersonatedUser;
+ this.bookmarkManager = builder.bookmarkManager;
+ this.useDefaultBookmarkManager = builder.useDefaultBookmarkManager;
+ }
+
+ /**
+ * Creates a new {@link Builder} used to construct a configuration object with default implementation returning
+ * {@link EagerResult}.
+ *
+ * @return a query configuration builder
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns routing mode for the query.
+ *
+ * @return routing mode
+ */
+ public RoutingControl routing() {
+ return routing;
+ }
+
+ /**
+ * Returns target database for the query.
+ *
+ * @return target database
+ */
+ public Optional database() {
+ return Optional.ofNullable(database);
+ }
+
+ /**
+ * Returns impersonated user for the query.
+ *
+ * @return impersonated user
+ */
+ public Optional impersonatedUser() {
+ return Optional.ofNullable(impersonatedUser);
+ }
+
+ /**
+ * Returns bookmark manager for the query.
+ *
+ * @param defaultBookmarkManager default bookmark manager to use when none has been configured explicitly,
+ * {@link Config#queryBookmarkManager()} as a default value by the driver
+ * @return bookmark manager
+ */
+ public Optional bookmarkManager(BookmarkManager defaultBookmarkManager) {
+ requireNonNull(defaultBookmarkManager, "defaultBookmarkManager must not be null");
+ return useDefaultBookmarkManager ? Optional.of(defaultBookmarkManager) : Optional.ofNullable(bookmarkManager);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ QueryConfig that = (QueryConfig) o;
+ return useDefaultBookmarkManager == that.useDefaultBookmarkManager
+ && routing == that.routing
+ && Objects.equals(database, that.database)
+ && Objects.equals(impersonatedUser, that.impersonatedUser)
+ && Objects.equals(bookmarkManager, that.bookmarkManager);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(routing, database, impersonatedUser, bookmarkManager, useDefaultBookmarkManager);
+ }
+
+ @Override
+ public String toString() {
+ return "QueryConfig{" + "routing="
+ + routing + ", database='"
+ + database + '\'' + ", impersonatedUser='"
+ + impersonatedUser + '\'' + ", bookmarkManager="
+ + bookmarkManager + ", useDefaultBookmarkManager="
+ + useDefaultBookmarkManager + '}';
+ }
+
+ /**
+ * Builder used to configure {@link QueryConfig} which will be used to execute a query.
+ */
+ public static final class Builder {
+ private RoutingControl routing = RoutingControl.WRITERS;
+ private String database;
+ private String impersonatedUser;
+ private BookmarkManager bookmarkManager;
+ private boolean useDefaultBookmarkManager = true;
+
+ private Builder() {}
+
+ /**
+ * Set routing mode for the query.
+ *
+ * @param routing routing mode
+ * @return this builder
+ */
+ public Builder withRouting(RoutingControl routing) {
+ requireNonNull(routing, "routing must not be null");
+ this.routing = routing;
+ return this;
+ }
+
+ /**
+ * Set target database for the query.
+ *
+ * @param database database
+ * @return this builder
+ */
+ public Builder withDatabase(String database) {
+ requireNonNull(database, "database must not be null");
+ if (database.isEmpty()) {
+ // Empty string is an illegal database
+ throw new IllegalArgumentException(String.format("Illegal database '%s'", database));
+ }
+ this.database = database;
+ return this;
+ }
+
+ /**
+ * Set impersonated user for the query.
+ *
+ * @param impersonatedUser impersonated user
+ * @return this builder
+ */
+ public Builder withImpersonatedUser(String impersonatedUser) {
+ requireNonNull(impersonatedUser, "impersonatedUser must not be null");
+ if (impersonatedUser.isEmpty()) {
+ // Empty string is an illegal user
+ throw new IllegalArgumentException(String.format("Illegal impersonated user '%s'", impersonatedUser));
+ }
+ this.impersonatedUser = impersonatedUser;
+ return this;
+ }
+
+ /**
+ * Set bookmark manager for the query.
+ *
+ * @param bookmarkManager bookmark manager
+ * @return this builder
+ */
+ public Builder withBookmarkManager(BookmarkManager bookmarkManager) {
+ useDefaultBookmarkManager = false;
+ this.bookmarkManager = bookmarkManager;
+ return this;
+ }
+
+ /**
+ * Create a config instance from this builder.
+ *
+ * @return a new {@link QueryConfig} instance.
+ */
+ public QueryConfig build() {
+ return new QueryConfig(this);
+ }
+ }
+}
diff --git a/driver/src/main/java/org/neo4j/driver/QueryTask.java b/driver/src/main/java/org/neo4j/driver/QueryTask.java
new file mode 100644
index 0000000000..da30387819
--- /dev/null
+++ b/driver/src/main/java/org/neo4j/driver/QueryTask.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) "Neo4j"
+ * Neo4j Sweden AB [http://neo4j.com]
+ *
+ * This file is part of Neo4j.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.neo4j.driver;
+
+import java.util.Map;
+import java.util.function.BiFunction;
+import java.util.function.Consumer;
+import java.util.stream.Collector;
+import org.neo4j.driver.summary.ResultSummary;
+import org.neo4j.driver.util.Experimental;
+
+/**
+ * A task that executes an idempotent query in a managed transaction with automatic retries on retryable errors.
+ *
+ * This is a high-level API for executing an idempotent query. There are more advanced APIs available.
+ * For instance, {@link Session}, {@link Transaction} and transaction functions that are accessible via
+ * methods like {@link Session#executeWrite(TransactionCallback)}, {@link Session#executeWriteWithoutResult(Consumer)}
+ * and {@link Session#executeRead(TransactionCallback)} (there are also overloaded options available).
+ *
+ * Causal consistency is managed via driver's {@link BookmarkManager} that is enabled by default and may
+ * be replaced using {@link Config.ConfigBuilder#withQueryBookmarkManager(BookmarkManager)}. It is also possible
+ * to use a different {@link BookmarkManager} or disable it via
+ * {@link QueryConfig.Builder#withBookmarkManager(BookmarkManager)} on individual basis.
+ *
+ * The above sample is functionally similar to the following use of the more advanced APIs:
+ *
+ * {@code
+ * var query = new Query("CREATE (n{field: $value}) RETURN n", Map.of("$value", "5"));
+ * var sessionConfig = SessionConfig.builder()
+ * .withBookmarkManager(driverConfig.queryBookmarkManager())
+ * .build();
+ * try (var session = driver.session(sessionConfig)) {
+ * var eagerResult = session.executeWrite(tx -> {
+ * var result = tx.run(query);
+ * return new EagerResultValue(result.keys(), result.stream().toList(), result.consume());
+ * });
+ * }
+ * }
+ *
+ * In addition, it is possible to transform query result by using a supplied {@link Collector} implementation.
+ *
+ * It is strongly recommended to use Cypher query language capabilities where possible. The examples below just
+ * provide a sample usage of the API.
+ *
+ * {@code
+ * import static java.util.stream.Collectors.*;
+ *
+ * var averagingLong = driver.queryTask("UNWIND range(0, 5) as N RETURN N")
+ * .execute(averagingLong(record -> record.get("N").asLong()));
+ *
+ * var filteredValues = driver.queryTask("UNWIND range(0, 5) as N RETURN N")
+ * .execute(mapping(record -> record.get("N").asLong(), filtering(value -> value > 2, toList())));
+ *
+ * var maxValue = driver.queryTask("UNWIND range(0, 5) as N RETURN N")
+ * .execute(mapping(record -> record.get("N").asLong(), maxBy(Long::compare)));
+ * }
+ *
+ * If there is a need to access {@link ResultSummary} value, another method option is available:
+ *
+ * {@code
+ * import static java.util.stream.Collectors.*;
+ *
+ * private record ResultValue(Set values, ResultSummary summary) {}
+ *
+ * var result = driver.queryTask("UNWIND range(0, 5) as N RETURN N")
+ * .execute(Collectors.mapping(record -> record.get("N").asLong(), toSet()), ResultValue::new);
+ * }
+ *