Skip to content

A pure Elixir driver for Neo4j graph database using the Bolt protocol. Supports authentication, query execution, transactions, and connection management.

License

Notifications You must be signed in to change notification settings

Maxino22/neo4j_ex

Repository files navigation

Neo4jEx

A pure Elixir driver for Neo4j graph database using the Bolt protocol.

Hex.pm Documentation License Stable

✔️ Stable Release: Neo4jEx is now production-ready. Core functionality is complete, actively maintained, and tested across real-world workloads. Please report any bugs or feature requests to help us continue improving the driver.

Features

  • Full Bolt Protocol Support: Complete implementation of Neo4j's Bolt protocol v5.x
  • Neo4j & Memgraph Compatible: Works with both Neo4j and Memgraph databases
  • Authentication: Support for basic authentication and no-auth scenarios
  • Connection Management: Automatic connection handling and cleanup
  • Query Execution: Simple query execution with parameter support
  • Transactions: Full transaction support with automatic commit/rollback
  • Sessions: Session-based query execution for better resource management
  • Type Safety: Proper handling of Neo4j data types and PackStream serialization
  • Error Handling: Comprehensive error handling and reporting
  • Connection Pooling: Efficient connection pooling for high-performance applications
  • Pure Elixir: No external dependencies, built entirely in Elixir

Installation

Add neo4j_ex to your list of dependencies in mix.exs:

def deps do
  [
    {:neo4j_ex, "~> 0.1.9"}
  ]
end

Then run:

mix deps.get

Quick Start

Understanding Auto-Start Behavior

Important: Neo4jEx automatically starts when included as a dependency. This means:

  • In Phoenix or Mix applications: Just add neo4j_ex to your deps and configure it - it starts automatically
  • Don't manually add Neo4j.Application to your supervision tree unless you have specific needs
  • In standalone scripts: Use Application.ensure_all_started(:neo4j_ex) before making queries

If you see an error like already started: #PID<0.327.0>, it means you're trying to start the application twice.

With Application Configuration (Recommended for Phoenix/Mix Apps)

Configure your default driver in config/config.exs:

# config/config.exs
config :neo4j_ex,
  uri: "bolt://localhost:7687",
  auth: {"neo4j", "password"}

That's it! Neo4jEx will automatically start with your application. You can now use the clean, implicit API:

# Execute queries without passing driver explicitly
{:ok, results} = Neo4jEx.run("MATCH (n:Person) RETURN n.name LIMIT 10")

# Work with sessions using default driver
result = Neo4jEx.session(fn session ->
  Neo4j.Session.run(session, "CREATE (p:Person {name: $name})", %{name: "Alice"})
end)

# Use transactions with default driver
result = Neo4jEx.transaction(fn tx ->
  Neo4j.Transaction.run(tx, "CREATE (p:Person {name: $name})", %{name: "Bob"})
  Neo4j.Transaction.run(tx, "CREATE (p:Person {name: $name})", %{name: "Carol"})
end)

# Stream large result sets using default driver
Neo4jEx.stream("MATCH (n:Person) RETURN n")
|> Stream.each(&process_record/1)
|> Stream.run()

Manual Driver Management

If you prefer explicit driver management:

# Start a driver
{:ok, driver} = Neo4jEx.start_link("bolt://localhost:7687",
  auth: {"neo4j", "password"})

# Execute a simple query
{:ok, results} = Neo4jEx.run(driver, "MATCH (n:Person) RETURN n.name LIMIT 10")

# Process results
for record <- results.records do
  name = Neo4j.Result.Record.get(record, "n.name")
  IO.puts("Person: #{name}")
end

# Clean up
Neo4jEx.close(driver)

Configuration

Basic Configuration

# Basic authentication
{:ok, driver} = Neo4jEx.start_link("bolt://localhost:7687",
  auth: {"username", "password"})

# No authentication (for development)
{:ok, driver} = Neo4jEx.start_link("bolt://localhost:7687")

# Custom timeouts
{:ok, driver} = Neo4jEx.start_link("bolt://localhost:7687",
  auth: {"neo4j", "password"},
  connection_timeout: 30_000,  # 30 seconds
  query_timeout: 60_000)       # 60 seconds

Application Configuration

You can configure Neo4jEx in your application configuration:

# config/config.exs
config :my_app, :neo4j,
  uri: "bolt://localhost:7687",
  auth: {"neo4j", "password"},
  connection_timeout: 15_000,
  query_timeout: 30_000

Then use it in your application:

# In your application or supervisor
config = Application.get_env(:my_app, :neo4j)
{:ok, driver} = Neo4jEx.start_link(config[:uri],
  auth: config[:auth],
  connection_timeout: config[:connection_timeout],
  query_timeout: config[:query_timeout])

Environment Variables

For development and testing, you can use environment variables:

export NEO4J_HOST=localhost
export NEO4J_PORT=7687
export NEO4J_USER=neo4j
export NEO4J_PASS=password

Supervision Tree

Option 1: Using the Application Module (Recommended)

Configure drivers in your application config and let Neo4j.Application manage them:

# config/config.exs
config :neo4j_ex,
  drivers: [
    default: [
      uri: "bolt://localhost:7687",
      auth: {"neo4j", "password"},
      connection_timeout: 15_000,
      query_timeout: 30_000
    ],
    secondary: [
      uri: "bolt://secondary:7687",
      auth: {"neo4j", "password"}
    ]
  ]

# lib/my_app/application.ex
defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      # Other children...
      Neo4j.Application
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Then use the named drivers:

{:ok, results} = Neo4jEx.run(:default, "MATCH (n) RETURN count(n)")
{:ok, results} = Neo4jEx.run(:secondary, "MATCH (n) RETURN count(n)")

Option 2: Single Driver Configuration

For a single driver, you can configure it directly:

# config/config.exs
config :neo4j_ex,
  uri: "bolt://localhost:7687",
  auth: {"neo4j", "password"}

# The Neo4j.Application will automatically start a :default driver

Option 3: Manual Driver Management

Start specific drivers manually in your supervision tree:

# lib/my_app/application.ex
defmodule MyApp.Application do
  use Application

  def start(_type, _args) do
    children = [
      # Other children...
      {Neo4j.Driver, [
        "bolt://localhost:7687",
        [
          name: MyApp.Neo4j,
          auth: {"neo4j", "password"}
        ]
      ]}
    ]

    opts = [strategy: :one_for_one, name: MyApp.Supervisor]
    Supervisor.start_link(children, opts)
  end
end

Then use the named driver:

{:ok, results} = Neo4jEx.run(MyApp.Neo4j, "MATCH (n) RETURN count(n)")

Usage Examples

Simple Queries

# With implicit default driver (recommended)
{:ok, _} = Neo4jEx.run("""
  CREATE (alice:Person {name: "Alice", age: 30})
  CREATE (bob:Person {name: "Bob", age: 25})
  CREATE (alice)-[:KNOWS]->(bob)
""")

# Query with parameters using default driver
{:ok, results} = Neo4jEx.run(
  "MATCH (p:Person {name: $name}) RETURN p",
  %{name: "Alice"})

# Or with explicit driver
{:ok, _} = Neo4jEx.run(driver, """
  CREATE (alice:Person {name: "Alice", age: 30})
  CREATE (bob:Person {name: "Bob", age: 25})
  CREATE (alice)-[:KNOWS]->(bob)
""")

# Query with parameters and explicit driver
{:ok, results} = Neo4jEx.run(driver,
  "MATCH (p:Person {name: $name}) RETURN p",
  %{name: "Alice"})

# Process results
for record <- results.records do
  person = Neo4j.Result.Record.get(record, "p")
  IO.inspect(person)
end

Multiple Named Drivers

# Configure multiple drivers in config/config.exs
config :neo4j_ex,
  drivers: [
    default: [
      uri: "bolt://localhost:7687",
      auth: {"neo4j", "password"}
    ],
    analytics: [
      uri: "bolt://analytics-server:7687",
      auth: {"neo4j", "analytics_password"}
    ]
  ]

# Use different drivers for different purposes
{:ok, results} = Neo4jEx.run("MATCH (n:Person) RETURN count(n)")  # Uses :default
{:ok, results} = Neo4jEx.run(:analytics, "MATCH (n:Event) RETURN count(n)")  # Uses :analytics

Working with Sessions

result = Neo4jEx.session(driver, fn session ->
  # Multiple queries in the same session
  {:ok, _} = Neo4j.Session.run(session,
    "CREATE (p:Person {name: $name})", %{name: "Charlie"})

  {:ok, results} = Neo4j.Session.run(session,
    "MATCH (p:Person) RETURN count(p) AS total")

  # Return the count
  record = List.first(results.records)
  Neo4j.Result.Record.get(record, "total")
end)

IO.puts("Total persons: #{result}")

Transactions

# Automatic transaction management
result = Neo4jEx.transaction(driver, fn tx ->
  # All operations in this block are part of the same transaction
  {:ok, _} = Neo4j.Transaction.run(tx,
    "CREATE (p:Person {name: $name})", %{name: "David"})

  {:ok, _} = Neo4j.Transaction.run(tx,
    "CREATE (p:Person {name: $name})", %{name: "Eve"})

  # If this block completes successfully, transaction is committed
  # If an exception is raised, transaction is rolled back
  :success
end)

Manual Transaction Control

Neo4jEx.session(driver, fn session ->
  {:ok, tx} = Neo4j.Session.begin_transaction(session)

  try do
    {:ok, _} = Neo4j.Transaction.run(tx,
      "CREATE (p:Person {name: $name})", %{name: "Frank"})

    # Manually commit
    :ok = Neo4j.Transaction.commit(tx)
  rescue
    _error ->
      # Manually rollback on error
      :ok = Neo4j.Transaction.rollback(tx)
      reraise
  end
end)

Working with Results

{:ok, results} = Neo4jEx.run(driver, """
  MATCH (p:Person)-[:KNOWS]->(friend:Person)
  RETURN p.name AS person, friend.name AS friend, p.age AS age
""")

# Access by field name
for record <- results.records do
  person = Neo4j.Result.Record.get(record, "person")
  friend = Neo4j.Result.Record.get(record, "friend")
  age = Neo4j.Result.Record.get(record, "age")

  IO.puts("#{person} (#{age}) knows #{friend}")
end

# Access by index
for record <- results.records do
  person = Neo4j.Result.Record.get(record, 0)
  friend = Neo4j.Result.Record.get(record, 1)
  age = Neo4j.Result.Record.get(record, 2)

  IO.puts("#{person} (#{age}) knows #{friend}")
end

# Convert to map
for record <- results.records do
  map = Neo4j.Result.Record.to_map(record)
  IO.inspect(map)
  # %{"person" => "Alice", "friend" => "Bob", "age" => 30}
end

# Query statistics
summary = results.summary
if Neo4j.Result.Summary.contains_updates?(summary) do
  nodes_created = Neo4j.Result.Summary.get_counter(summary, "nodes_created")
  IO.puts("Created #{nodes_created} nodes")
end

Streaming Large Result Sets

For large datasets, Neo4jEx provides a streaming interface that allows you to process results without loading everything into memory at once:

# Stream all Person nodes without loading them all into memory
driver
|> Neo4jEx.stream("MATCH (n:Person) RETURN n")
|> Stream.each(&process_person/1)
|> Stream.run()

# Stream with custom batch size and timeout
driver
|> Neo4jEx.stream("MATCH (n:BigData) RETURN n", %{}, batch_size: 500, timeout: 60_000)
|> Stream.chunk_every(100)
|> Enum.each(&batch_process/1)

# Memory-efficient aggregation
total = driver
|> Neo4jEx.stream("MATCH (n:Transaction) RETURN n.amount")
|> Stream.map(fn record -> record |> Neo4j.Result.Record.get("n.amount") end)
|> Enum.sum()

# Stream with custom processing function
driver
|> Neo4jEx.Stream.run_with("MATCH (n:Person) RETURN n.name", %{}, 
   fn record -> 
     name = Neo4j.Result.Record.get(record, "n.name")
     String.upcase(name)
   end)
|> Enum.each(&IO.puts/1)

The streaming interface uses cursor-based pagination under the hood, automatically fetching data in batches to minimize memory usage while maintaining good performance.

Connection Pooling

Neo4jEx provides efficient connection pooling for high-performance applications. Connection pooling allows you to reuse connections across multiple queries, reducing connection overhead and improving throughput.

Starting a Connection Pool

# Start a connection pool
{:ok, _pool} = Neo4jEx.start_pool([
  uri: "bolt://localhost:7687",
  auth: {"neo4j", "password"},
  pool_size: 15,        # Maximum number of connections
  max_overflow: 5       # Additional connections when pool is full
])

# Or with a custom name
{:ok, _pool} = Neo4jEx.start_pool([
  uri: "bolt://localhost:7687",
  auth: {"neo4j", "password"},
  pool_size: 10,
  name: :my_pool
])

Using Pooled Connections

# Execute queries using the default pool
{:ok, results} = Neo4jEx.Pool.run("MATCH (n:Person) RETURN n")

# Execute queries with parameters
{:ok, results} = Neo4jEx.Pool.run(
  "CREATE (p:Person {name: $name}) RETURN p", 
  %{name: "Alice"}
)

# Execute transactions using the pool
result = Neo4jEx.Pool.transaction(fn ->
  Neo4jEx.Pool.run("CREATE (p:Person {name: 'Bob'})")
  Neo4jEx.Pool.run("CREATE (p:Person {name: 'Carol'})")
  :success
end)

# Use a named pool
{:ok, results} = Neo4jEx.Pool.run(
  "MATCH (n) RETURN count(n)", 
  %{}, 
  pool_name: :my_pool
)

Pool Configuration Options

Option Type Default Description
:uri string() - Neo4j connection URI (required)
:auth tuple() nil Authentication credentials
:pool_size integer() 10 Maximum number of connections
:max_overflow integer() 5 Additional connections when pool full
:name atom() - Pool name for multiple pools

Pool Management

# Check pool status
status = Neo4jEx.Pool.status()
# Returns: {:ready, pool_size, checked_out, overflow}

# Stop a pool
Neo4jEx.stop_pool()

# Stop a named pool
Neo4jEx.stop_pool(:my_pool)

Benefits of Connection Pooling

  • Improved Performance: Reuse existing connections instead of creating new ones
  • Resource Management: Control the maximum number of concurrent connections
  • Scalability: Handle high-concurrency scenarios efficiently
  • Automatic Recovery: Connections are automatically recreated if they fail
  • Load Distribution: Distribute queries across multiple connections

Testing Your Connection

Use the included test script to verify your Neo4j connection:

# With default settings (localhost:7687, neo4j/password)
elixir scripts/test_connection.exs

# With custom settings
NEO4J_HOST=myhost NEO4J_PORT=7687 NEO4J_USER=myuser NEO4J_PASS=mypass elixir scripts/test_connection.exs

Configuration Options

Option Type Default Description
:auth {username, password} or map() nil Authentication credentials
:user_agent string() "neo4j_ex/0.1.0" Client identification
:connection_timeout integer() 15_000 Connection timeout (ms)
:query_timeout integer() 30_000 Query timeout (ms)
:max_pool_size integer() 10 Max connections (future)

Authentication Options

# Tuple format (recommended)
auth: {"username", "password"}

# Map format (advanced)
auth: %{
  "scheme" => "basic",
  "principal" => "username",
  "credentials" => "password"
}

# No authentication
auth: nil
# or simply omit the :auth option

Error Handling

case Neo4jEx.run(driver, "INVALID CYPHER") do
  {:ok, results} ->
    # Handle success
    IO.puts("Query succeeded")

  {:error, {:query_failed, message}} ->
    # Handle query errors
    IO.puts("Query failed: #{message}")

  {:error, {:connection_failed, reason}} ->
    # Handle connection errors
    IO.puts("Connection failed: #{inspect(reason)}")

  {:error, reason} ->
    # Handle other errors
    IO.puts("Error: #{inspect(reason)}")
end

Development

Prerequisites

  • Elixir 1.12+
  • Neo4j 4.0+ or Memgraph running on localhost:7687

Running Tests

# Run unit tests
mix test

# Run with a specific Neo4j instance
NEO4J_USER=neo4j NEO4J_PASS=yourpassword mix test

# Test connection
elixir scripts/test_connection.exs

Architecture

The driver is built with a layered architecture:

Neo4jEx (Public API)
├── Neo4j.Driver (Driver Management)
├── Neo4j.Session (Session Management)
├── Neo4j.Transaction (Transaction Management)
├── Neo4j.Protocol.* (Bolt Protocol Implementation)
│   ├── Messages (Bolt Messages)
│   └── PackStream (Serialization)
├── Neo4j.Connection.* (Connection Layer)
│   ├── Socket (TCP Communication)
│   └── Handshake (Bolt Handshake)
└── Neo4j.Result.* & Neo4j.Types.* (Data Types)
    ├── Record (Query Results)
    ├── Summary (Query Metadata)
    └── Node, Relationship, Path (Graph Types)

Roadmap

  • Week 1: Basic connection and Bolt handshake
  • Week 2: PackStream serialization and basic messaging
  • Week 3: Query execution and result parsing
  • Week 4: Polish, testing, and documentation
  • v0.2.0: Result streaming support for large datasets
  • v0.3.0: Connection pooling and improved performance
  • v0.4.0: Clustering support and routing
  • v1.0.0: Advanced Neo4j types (Point, Duration, etc.) and production readiness

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -am 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgments

  • Neo4j team for the excellent Bolt protocol documentation
  • Elixir community for the amazing ecosystem
  • Contributors and testers who helped make this driver possible

Support


Made with ❤️ for the Elixir and Neo4j communities.

About

A pure Elixir driver for Neo4j graph database using the Bolt protocol. Supports authentication, query execution, transactions, and connection management.

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published

Languages