Skip to content

Commit

Permalink
Stabilize ExecuteQuery and BookmarkManager APIs
Browse files Browse the repository at this point in the history
 * Renaming `neo4j.Writers` to `Write` and `Readers` to `Read`.
 * Renaming `driver.DefaultExecuteQueryBookmarkManager()` to
   `ExecuteQueryBookmarkManager()`.
 * Remove experimental tag from ExecuteQuery and its related APIs
   * `ExecuteQuery()`
   * `driver.DefaultExecuteQueryBookmarkManager()`
   * `BookmarkManager`, the corresponding config option
     (session's `BookmarkManager`) and factory method
     (`neo4j.NewBookmarkManager()`)
   * `RoutingControl` enum
   * `EagerResult`
   * `ExecuteQueryWith...` config helpers

Signed-off-by: Florent Biville <florent.biville@neo4j.com>
  • Loading branch information
robsdedude authored and fbiville committed Apr 13, 2023
1 parent c25cc31 commit dec206f
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 99 deletions.
3 changes: 0 additions & 3 deletions neo4j/bookmarks.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ import (
type Bookmarks = []string

// BookmarkManager centralizes bookmark manager supply and notification
// This is currently a preview feature (see README on what it means in terms of support and compatibility guarantees)
type BookmarkManager interface {
// UpdateBookmarks updates the bookmark tracked by this bookmark manager
// previousBookmarks are the initial bookmarks of the bookmark holder (like a Session)
Expand All @@ -44,8 +43,6 @@ type BookmarkManager interface {
GetBookmarks(ctx context.Context) (Bookmarks, error)
}

// BookmarkManagerConfig is part of the BookmarkManager preview feature (see README on what it means in terms of support
// and compatibility guarantees)
type BookmarkManagerConfig struct {
// Initial bookmarks per database
InitialBookmarks Bookmarks
Expand Down
95 changes: 24 additions & 71 deletions neo4j/driver_with_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,18 @@ const (
// DriverWithContext represents a pool of connections to a neo4j server or cluster. It's
// safe for concurrent use.
type DriverWithContext interface {
// DefaultExecuteQueryBookmarkManager returns the bookmark manager instance used by ExecuteQuery by default.
//
// DefaultExecuteQueryBookmarkManager is part of the BookmarkManager preview feature (see README on what it means in
// terms of support and compatibility guarantees)
// ExecuteQueryBookmarkManager returns the bookmark manager instance used by ExecuteQuery by default.
//
// This is useful when ExecuteQuery is called without custom bookmark managers and the lower-level
// neo4j.SessionWithContext APIs are called as well.
// In that case, the recommended approach is as follows:
// results, err := driver.ExecuteQuery(ctx, query, params)
// // [...] do something with results and error
// bookmarkManager := driver.DefaultExecuteQueryBookmarkManager()
// bookmarkManager := driver.ExecuteQueryBookmarkManager()
// // maintain consistency with sessions as well
// session := driver.NewSession(ctx, neo4j.SessionConfig {BookmarkManager: bookmarkManager})
// // [...] run something within the session
DefaultExecuteQueryBookmarkManager() BookmarkManager
ExecuteQueryBookmarkManager() BookmarkManager
// Target returns the url this driver is bootstrapped
Target() url.URL
// NewSession creates a new session based on the specified session configuration.
Expand All @@ -87,9 +84,6 @@ type DriverWithContext interface {
}

// ResultTransformer is a record accumulator that produces an instance of T when the processing of records is over.
//
// ResultTransformer is part of the ExecuteQuery preview feature (see README on what it means in terms of support
// and compatibility guarantees)
type ResultTransformer[T any] interface {
// Accept is called whenever a new record is fetched from the server
// Implementers are free to accumulate or discard the specified record
Expand Down Expand Up @@ -261,13 +255,13 @@ func routingContextFromUrl(useRouting bool, u *url.URL) (map[string]string, erro
}

type sessionRouter interface {
// Readers returns the list of servers that can serve reads on the requested database.
// Read returns the list of servers that can serve reads on the requested database.
// note: bookmarks are lazily supplied, only when a new routing table needs to be fetched
// this is needed because custom bookmark managers may provide bookmarks from external systems
// they should not be called when it is not needed (e.g. when a routing table is cached)
Readers(ctx context.Context, bookmarks func(context.Context) ([]string, error), database string, boltLogger log.BoltLogger) ([]string, error)
// Writers returns the list of servers that can serve writes on the requested database.
// note: bookmarks are lazily supplied, see Readers documentation to learn why
// Write returns the list of servers that can serve writes on the requested database.
// note: bookmarks are lazily supplied, see Read documentation to learn why
Writers(ctx context.Context, bookmarks func(context.Context) ([]string, error), database string, boltLogger log.BoltLogger) ([]string, error)
// GetNameOfDefaultDatabase returns the name of the default database for the specified user.
// The correct database name is needed when requesting readers or writers.
Expand All @@ -292,7 +286,7 @@ type driverWithContext struct {
executeQueryBookmarkManagerInitializer sync.Once
// instance of the bookmark manager only used by default by managed sessions of ExecuteQuery
// this is *not* used by default by user-created session (see NewSession)
defaultExecuteQueryBookmarkManager BookmarkManager
executeQueryBookmarkManager BookmarkManager
}

func (d *driverWithContext) Target() url.URL {
Expand Down Expand Up @@ -352,8 +346,6 @@ func (d *driverWithContext) Close(ctx context.Context) error {
// ExecuteQuery runs the specified query with its parameters and returns the query result, transformed by the specified
// ResultTransformer function.
//
// This is currently a preview feature (see README on what it means in terms of support and compatibility guarantees)
//
// result, err := ExecuteQuery[*EagerResult](ctx, driver, query, params, EagerResultTransformer)
//
// Passing a nil ResultTransformer function is invalid and will return an error.
Expand Down Expand Up @@ -388,7 +380,7 @@ func (d *driverWithContext) Close(ctx context.Context) error {
//
// ExecuteQuery[T](ctx, driver, query, params, transformerFunc, func(config *neo4j.ExecuteQueryConfiguration) {
// config.Database = "my-db"
// config.RoutingControl = neo4j.Writers
// config.RoutingControl = neo4j.Write
// config.ImpersonatedUser = "selda_bağcan"
// })
//
Expand All @@ -413,7 +405,7 @@ func (d *driverWithContext) Close(ctx context.Context) error {
// BookmarkManager: bookmarkManager,
// })
// defer handleClose(ctx, session)
// // session.ExecuteRead is called if the routing is set to neo4j.Readers
// // session.ExecuteRead is called if the routing is set to neo4j.Read
// result, _ := session.ExecuteWrite(ctx, func(tx neo4j.ManagedTransaction) (any, error) {
// result, _ := tx.Run(ctx, "<CYPHER>", parameters)
// records, _ := result.Collect(ctx) // real implementation does not use Collect
Expand Down Expand Up @@ -457,7 +449,7 @@ func ExecuteQuery[T any](
"ResultTransformer implementation"}
}

bookmarkManager := driver.DefaultExecuteQueryBookmarkManager()
bookmarkManager := driver.ExecuteQueryBookmarkManager()
configuration := &ExecuteQueryConfiguration{
BookmarkManager: bookmarkManager,
}
Expand All @@ -479,13 +471,13 @@ func ExecuteQuery[T any](
return result.(T), err
}

func (d *driverWithContext) DefaultExecuteQueryBookmarkManager() BookmarkManager {
func (d *driverWithContext) ExecuteQueryBookmarkManager() BookmarkManager {
d.executeQueryBookmarkManagerInitializer.Do(func() {
if d.defaultExecuteQueryBookmarkManager == nil { // this allows tests to init the field themselves
d.defaultExecuteQueryBookmarkManager = NewBookmarkManager(BookmarkManagerConfig{})
if d.executeQueryBookmarkManager == nil { // this allows tests to init the field themselves
d.executeQueryBookmarkManager = NewBookmarkManager(BookmarkManagerConfig{})
}
})
return d.defaultExecuteQueryBookmarkManager
return d.executeQueryBookmarkManager
}

func executeQueryCallback[T any](
Expand Down Expand Up @@ -546,85 +538,58 @@ func (e *eagerResultTransformer) Complete(keys []string, summary ResultSummary)
}

// ExecuteQueryConfigurationOption is a callback that configures the execution of DriverWithContext.ExecuteQuery
//
// ExecuteQueryConfigurationOption is part of the ExecuteQuery preview feature (see README on what it means in terms of
// support and compatibility guarantees)
type ExecuteQueryConfigurationOption func(*ExecuteQueryConfiguration)

// ExecuteQueryWithReadersRouting configures DriverWithContext.ExecuteQuery to route to reader members of the cluster
//
// ExecuteQueryWithReadersRouting is part of the ExecuteQuery preview feature (see README on what it means in terms of
// support and compatibility guarantees)
func ExecuteQueryWithReadersRouting() ExecuteQueryConfigurationOption {
return func(configuration *ExecuteQueryConfiguration) {
configuration.Routing = Readers
configuration.Routing = Read
}
}

// ExecuteQueryWithWritersRouting configures DriverWithContext.ExecuteQuery to route to writer members of the cluster
//
// ExecuteQueryWithWritersRouting is part of the ExecuteQuery preview feature (see README on what it means in terms of
// support and compatibility guarantees)
func ExecuteQueryWithWritersRouting() ExecuteQueryConfigurationOption {
return func(configuration *ExecuteQueryConfiguration) {
configuration.Routing = Writers
configuration.Routing = Write
}
}

// ExecuteQueryWithImpersonatedUser configures DriverWithContext.ExecuteQuery to impersonate the specified user
//
// ExecuteQueryWithImpersonatedUser is part of the ExecuteQuery preview feature (see README on what it means in terms of
// support and compatibility guarantees)
func ExecuteQueryWithImpersonatedUser(user string) ExecuteQueryConfigurationOption {
return func(configuration *ExecuteQueryConfiguration) {
configuration.ImpersonatedUser = user
}
}

// ExecuteQueryWithDatabase configures DriverWithContext.ExecuteQuery to target the specified database
//
// ExecuteQueryWithDatabase is part of the ExecuteQuery preview feature (see README on what it means in terms of
// support and compatibility guarantees)
func ExecuteQueryWithDatabase(db string) ExecuteQueryConfigurationOption {
return func(configuration *ExecuteQueryConfiguration) {
configuration.Database = db
}
}

// ExecuteQueryWithBookmarkManager configures DriverWithContext.ExecuteQuery to rely on the specified BookmarkManager
//
// ExecuteQueryWithBookmarkManager is part of the ExecuteQuery preview feature (see README on what it means in terms of
// support and compatibility guarantees)
func ExecuteQueryWithBookmarkManager(bookmarkManager BookmarkManager) ExecuteQueryConfigurationOption {
return func(configuration *ExecuteQueryConfiguration) {
configuration.BookmarkManager = bookmarkManager
}
}

// ExecuteQueryWithoutBookmarkManager configures DriverWithContext.ExecuteQuery to not rely on any BookmarkManager
//
// ExecuteQueryWithoutBookmarkManager is part of the ExecuteQuery preview feature (see README on what it means in terms of
// support and compatibility guarantees)
func ExecuteQueryWithoutBookmarkManager() ExecuteQueryConfigurationOption {
return func(configuration *ExecuteQueryConfiguration) {
configuration.BookmarkManager = nil
}
}

// ExecuteQueryWithBoltLogger configures DriverWithContext.ExecuteQuery to log Bolt messages with the provided BoltLogger
//
// ExecuteQueryWithBoltLogger is part of the ExecuteQuery preview feature (see README on what it means in terms of
// support and compatibility guarantees)
func ExecuteQueryWithBoltLogger(boltLogger log.BoltLogger) ExecuteQueryConfigurationOption {
return func(configuration *ExecuteQueryConfiguration) {
configuration.BoltLogger = boltLogger
}
}

// ExecuteQueryConfiguration holds all the possible configuration settings for DriverWithContext.ExecuteQuery
//
// ExecuteQueryConfiguration is part of the ExecuteQuery preview feature (see README on what it means in terms of
// support and compatibility guarantees)
type ExecuteQueryConfiguration struct {
Routing RoutingControl
ImpersonatedUser string
Expand All @@ -634,22 +599,13 @@ type ExecuteQueryConfiguration struct {
}

// RoutingControl specifies how the query executed by DriverWithContext.ExecuteQuery is to be routed
//
// RoutingControl is part of the ExecuteQuery preview feature (see README on what it means in terms of support and
// compatibility guarantees)
type RoutingControl int

const (
// Writers routes the query to execute to a writer member of the cluster
//
// Writers is part of the ExecuteQuery preview feature (see README on what it means in terms of
// support and compatibility guarantees)
Writers RoutingControl = iota
// Readers routes the query to execute to a writer member of the cluster
//
// Readers is part of the ExecuteQuery preview feature (see README on what it means in terms of
// support and compatibility guarantees)
Readers
// Write routes the query to execute to a writer member of the cluster
Write RoutingControl = iota
// Read routes the query to execute to a writer member of the cluster
Read
)

func (c *ExecuteQueryConfiguration) toSessionConfig() SessionConfig {
Expand All @@ -665,19 +621,16 @@ type transactionFunction func(context.Context, ManagedTransactionWork, ...func(*

func (c *ExecuteQueryConfiguration) selectTxFunctionApi(session SessionWithContext) (transactionFunction, error) {
switch c.Routing {
case Readers:
case Read:
return session.ExecuteRead, nil
case Writers:
case Write:
return session.ExecuteWrite, nil
}
return nil, fmt.Errorf("unsupported routing control, expected %d (Writers) or %d (Readers) "+
"but got: %d", Writers, Readers, c.Routing)
return nil, fmt.Errorf("unsupported routing control, expected %d (Write) or %d (Read) "+
"but got: %d", Write, Read, c.Routing)
}

// EagerResult holds the result and result metadata of the query executed via DriverWithContext.ExecuteQuery
//
// EagerResult is part of the ExecuteQuery preview feature (see README on what it means in terms of
// support and compatibility guarantees)
type EagerResult struct {
Keys []string
Records []*Record
Expand Down
21 changes: 20 additions & 1 deletion neo4j/driver_with_context_examples_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [https://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
*
* https://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 neo4j

import (
Expand Down Expand Up @@ -63,7 +82,7 @@ func ExampleExecuteQuery_default_bookmark_manager_explicit_reuse() {

// retrieve the default bookmark manager used by the previous call (since there was no bookmark manager explicitly
// configured)
bookmarkManager := myDriver.DefaultExecuteQueryBookmarkManager()
bookmarkManager := myDriver.ExecuteQueryBookmarkManager()
session := myDriver.NewSession(ctx, SessionConfig{BookmarkManager: bookmarkManager})

// the following transaction function is guaranteed to see the result of the previous query
Expand Down
35 changes: 27 additions & 8 deletions neo4j/driver_with_context_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,22 @@
/*
* Copyright (c) "Neo4j"
* Neo4j Sweden AB [https://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
*
* https://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 neo4j

import (
Expand Down Expand Up @@ -182,7 +201,7 @@ func TestDriverExecuteQuery(outer *testing.T) {
summary: summary,
}},
expectedSessionConfig: defaultSessionConfig,
expectedErr: fmt.Errorf("unsupported routing control, expected 0 (Writers) or 1 (Readers) but got: 42"),
expectedErr: fmt.Errorf("unsupported routing control, expected 0 (Write) or 1 (Read) but got: 42"),
},
{
description: "returns error when result transformer function is nil",
Expand Down Expand Up @@ -397,8 +416,8 @@ func TestDriverExecuteQuery(outer *testing.T) {
return testCase.createSession
},
delegate: &driverWithContext{
defaultExecuteQueryBookmarkManager: defaultBookmarkManager,
mut: racing.NewMutex(),
executeQueryBookmarkManager: defaultBookmarkManager,
mut: racing.NewMutex(),
},
}

Expand Down Expand Up @@ -431,7 +450,7 @@ func TestDriverExecuteQuery(outer *testing.T) {
callExecuteQueryOrBookmarkManagerGetter(driver, i)
storeBookmarkManagerAddress(
&bookmarkManagerAddresses,
driver.delegate.defaultExecuteQueryBookmarkManager.(*bookmarkManager))
driver.delegate.executeQueryBookmarkManager.(*bookmarkManager))
wait.Done()
}(i)
}
Expand All @@ -445,7 +464,7 @@ func TestDriverExecuteQuery(outer *testing.T) {
if len(addressCounts) != 1 {
t.Errorf("expected exactly 1 bookmark manager pointer to have been created, got %v", addressCounts)
}
address := uintptr(unsafe.Pointer(driver.delegate.defaultExecuteQueryBookmarkManager.(*bookmarkManager)))
address := uintptr(unsafe.Pointer(driver.delegate.executeQueryBookmarkManager.(*bookmarkManager)))
if count, found := addressCounts[address]; !found || count != int32(goroutineCount) {
t.Errorf("expected pointer address %v to be seen %d time(s), got these instead %v", address, count, addressCounts)
}
Expand All @@ -455,7 +474,7 @@ func TestDriverExecuteQuery(outer *testing.T) {
func callExecuteQueryOrBookmarkManagerGetter(driver DriverWithContext, i int) {
if i%2 == 0 {
// this lazily initializes the default bookmark manager
_ = driver.DefaultExecuteQueryBookmarkManager()
_ = driver.ExecuteQueryBookmarkManager()
} else {
// this as well
_, _ = ExecuteQuery[*EagerResult](context.Background(), driver, "RETURN 42", nil, EagerResultTransformer)
Expand Down Expand Up @@ -500,8 +519,8 @@ type driverDelegate struct {
newSession func(context.Context, SessionConfig) SessionWithContext
}

func (d *driverDelegate) DefaultExecuteQueryBookmarkManager() BookmarkManager {
return d.delegate.DefaultExecuteQueryBookmarkManager()
func (d *driverDelegate) ExecuteQueryBookmarkManager() BookmarkManager {
return d.delegate.ExecuteQueryBookmarkManager()
}

func (d *driverDelegate) Target() url.URL {
Expand Down
Loading

0 comments on commit dec206f

Please sign in to comment.