Skip to content

Instantly share code, notes, and snippets.

@fbiville
Last active July 1, 2022 11:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save fbiville/db472db5807fd1f39eecbc83bfa01323 to your computer and use it in GitHub Desktop.
Save fbiville/db472db5807fd1f39eecbc83bfa01323 to your computer and use it in GitHub Desktop.
Cypher merge is not sufficient to guarantee uniqueness
package container
import (
"context"
"fmt"
"github.com/neo4j/neo4j-go-driver/v4/neo4j"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
type ContainerConfiguration struct {
Neo4jVersion string
Username string
Password string
Scheme string
}
func (config ContainerConfiguration) neo4jAuthEnvVar() string {
return fmt.Sprintf("%s/%s", config.Username, config.Password)
}
func (config ContainerConfiguration) neo4jAuthToken() neo4j.AuthToken {
return neo4j.BasicAuth(config.Username, config.Password, "")
}
func StartSingleInstance(ctx context.Context, config ContainerConfiguration) (testcontainers.Container, neo4j.Driver, error) {
version := config.Neo4jVersion
request := testcontainers.ContainerRequest{
Image: fmt.Sprintf("neo4j:%s", version),
ExposedPorts: []string{"7687/tcp"},
Env: map[string]string{
"NEO4J_AUTH": config.neo4jAuthEnvVar(),
"NEO4J_ACCEPT_LICENSE_AGREEMENT": "yes",
},
WaitingFor: boltReadyStrategy(),
}
container, err := testcontainers.GenericContainer(ctx,
testcontainers.GenericContainerRequest{
ContainerRequest: request,
Started: true,
})
if err != nil {
return nil, nil, err
}
driver, err := newNeo4jDriver(ctx, config.Scheme, container, config.neo4jAuthToken())
return container, driver, err
}
func boltReadyStrategy() *wait.LogStrategy {
return wait.ForLog("Bolt enabled")
}
func newNeo4jDriver(ctx context.Context, scheme string, container testcontainers.Container, auth neo4j.AuthToken) (neo4j.Driver, error) {
port, err := container.MappedPort(ctx, "7687")
if err != nil {
return nil, err
}
return newDriver(scheme, port.Int(), auth)
}
func newDriver(scheme string, port int, auth neo4j.AuthToken) (neo4j.Driver, error) {
uri := fmt.Sprintf("%s://localhost:%d", scheme, port)
return neo4j.NewDriver(uri, auth)
}
package main
import (
"context"
"fmt"
"github.com/fbiville/neo4j-merge-gotcha/pkg/container"
"github.com/fbiville/neo4j-merge-gotcha/pkg/errors"
"github.com/neo4j/neo4j-go-driver/v4/neo4j"
"sync"
)
func main() {
ctx := context.Background()
instance, driver, err := container.StartSingleInstance(ctx, container.ContainerConfiguration{
Neo4jVersion: "4.4",
Username: "neo4j",
Password: "s3cr3t",
Scheme: "neo4j",
})
errors.PanicOnErr(err)
defer func() {
errors.PanicOnErr(instance.Terminate(ctx))
}()
defer func() {
errors.PanicOnErr(driver.Close())
}()
params := map[string]interface{}{
"name": "Jane Doe",
}
goRoutines := 10_000
group := sync.WaitGroup{}
group.Add(goRoutines)
for i := 0; i < goRoutines; i++ {
go func() {
session := driver.NewSession(neo4j.SessionConfig{})
result, err := session.Run("MERGE (:Person {name: $name})", params)
errors.PanicOnErr(err)
_, err = result.Consume()
errors.PanicOnErr(err)
errors.PanicOnErr(session.Close())
group.Done()
}()
}
group.Wait()
session := driver.NewSession(neo4j.SessionConfig{})
defer func() {
errors.PanicOnErr(session.Close())
}()
result, err := session.Run("MATCH (:Person {name: $name}) RETURN COUNT(*) AS count", params)
errors.PanicOnErr(err)
record, err := result.Single()
errors.PanicOnErr(err)
count, _ := record.Get("count")
fmt.Printf("Got %d person node(s)", count) // this will likely be larger than 1
// use https://neo4j.com/docs/cypher-manual/current/constraints/syntax/#administration-constraints-syntax-create-unique to make sure uniqueness is guaranteed
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment