falkordb 0.3.0

A FalkorDB Rust client
Documentation

Release crates.io license
GitHub Issues or Pull Requests Pipeline Codecov Docs
Forum Discord

falkordb-rs

Try Free

FalkorDB Rust client

Usage

Installation

Just add it to your Cargo.toml, like so:

falkordb = { version = "0.3.0" }

Run FalkorDB instance

Docker:

docker run --rm -p 6379:6379 falkordb/falkordb

Code Example

use falkordb::{FalkorClientBuilder, FalkorConnectionInfo};

// Connect to FalkorDB
let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
            .expect("Invalid connection info");

let client = FalkorClientBuilder::new()
           .with_connection_info(connection_info)
           .build()
           .expect("Failed to build client");

// Select the social graph
let mut graph = client.select_graph("social");

// Create 100 nodes and return a handful
let mut nodes = graph.query("UNWIND range(0, 100) AS i CREATE (n { v:1 }) RETURN n LIMIT 10")
            .with_timeout(5000)
            .execute()
            .expect("Failed executing query");

// Can also be collected, like any other iterator
while let Some(node) = nodes.data.next() {
   println ! ("{:?}", node);
}

Features

Waiting for background operations

Some FalkorDB operations finish after the command that starts them returns: when you create or drop an index or constraint, the request returns immediately while the index is populated (or the constraint is enforced) on a background worker thread, and GRAPH.COPY can fail transiently while the server is unable to fork. The eager methods (create_index, create_unique_constraint, copy_graph, …) stay fire-and-forget, but every one of them now has an additive *_op builder that adds explicit, opt-in waiting while keeping full backward compatibility.

Each builder offers .execute() (non-blocking, identical to the eager method) and .wait() / .wait_with(WaitOptions) terminals. For index and constraint builders, .wait() blocks until the operation has actually taken effect (the index/constraint becomes operational or is dropped), returning FalkorDBError::Timeout if it does not happen in time. For the copy builder, GRAPH.COPY is already blocking on the server, so .wait() simply retries transient could not fork failures with backoff; it does not verify the copied contents (that remains the caller's responsibility).

use falkordb::{EntityType, FalkorClientBuilder, FalkorConnectionInfo, IndexType, WaitOptions};
use std::time::Duration;

let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
            .expect("Invalid connection info");
let client = FalkorClientBuilder::new()
           .with_connection_info(connection_info)
           .build()
           .expect("Failed to build client");
let mut graph = client.select_graph("social");

// Fire-and-forget, exactly like `create_index` (returns as soon as the server accepts it):
graph.create_index_op(IndexType::Range, EntityType::Node, "Person", &["age"], None)
     .execute()
     .expect("Failed to request index creation");

// Block until the index is actually operational (default 30s readiness timeout):
graph.create_index_op(IndexType::Range, EntityType::Node, "Person", &["name"], None)
     .wait()
     .expect("Index did not become operational");

// A unique constraint reports a *distinct* error if existing data violates it:
match graph.create_unique_constraint_op(EntityType::Node, "Person", &["email"])
           .wait_with(WaitOptions::with_timeout(Duration::from_secs(10)))
{
    Ok(()) => println!("constraint is enforced"),
    Err(falkordb::FalkorDBError::ConstraintFailed { .. }) => println!("data violates the constraint"),
    Err(other) => panic!("unexpected error: {other}"),
}

// Copy a graph, retrying transient `could not fork` failures:
let _copy = client.copy_graph_op("social", "social_backup")
                  .wait()
                  .expect("Failed to copy graph");

The same builders exist on the async client/graph; just await the terminals:

use falkordb::{EntityType, FalkorClientBuilder, FalkorConnectionInfo, IndexType};

let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
            .expect("Invalid connection info");
let client = FalkorClientBuilder::new_async()
           .with_connection_info(connection_info)
           .build()
           .await
           .expect("Failed to build client");
let mut graph = client.select_graph("social");

graph.create_index_op(IndexType::Range, EntityType::Node, "Person", &["name"], None)
     .wait()
     .await
     .expect("Index did not become operational");

tokio support

This client supports nonblocking API using the tokio runtime. It can be enabled like so:

falkordb = { version = "0.3.0", features = ["tokio"] }

Currently, this API requires running within a multi_threaded tokio scheduler, and does not support the current_thread one, but this will probably be supported in the future.

The API uses an almost identical API, but the various functions need to be awaited:

use falkordb::{FalkorClientBuilder, FalkorConnectionInfo};

// Connect to FalkorDB
let connection_info: FalkorConnectionInfo = "falkor://127.0.0.1:6379".try_into()
            .expect("Invalid connection info");

let client = FalkorClientBuilder::new_async()
            .with_connection_info(connection_info)
            .build()
            .await
            .expect("Failed to build client");

// Select the social graph
let mut graph = client.select_graph("social");

// Create 100 nodes and return a handful
let mut nodes = graph.query("UNWIND range(0, 100) AS i CREATE (n { v:1 }) RETURN n LIMIT 10")
            .with_timeout(5000)
            .execute()
            .await
            .expect("Failed executing query");

// Graph operations are asynchronous, but parsing is still concurrent:
while let Some(node) = nodes.data.next() {
     println ! ("{:?}", node);
}

Note that thread safety is still up to the user to ensure, I.e. an AsyncGraph cannot simply be sent to a task spawned by tokio and expected to be used later, it must be wrapped in an Arc<Mutex<_>> or something similar.

SSL/TLS Support

This client is currently built upon the redis crate, and therefore supports TLS using its implementation, which uses either rustls or native_tls. This is not enabled by default, and the user just opt-in by enabling the respective features: "rustls"/"native-tls" ( when using tokio: "tokio-rustls"/"tokio-native-tls").

For Rustls:

falkordb = { version = "0.3.0", features = ["rustls"] }
falkordb = { version = "0.3.0", features = ["tokio-rustls"] }

For Native TLS:

falkordb = { version = "0.3.0", features = ["native-tls"] }
falkordb = { version = "0.3.0", features = ["tokio-native-tls"] }

TCP Keepalive

Long-lived clients behind NATs, stateful firewalls, or idle-timeout-enforcing proxies can silently lose their TCP sessions. The builder exposes TCP-level socket settings to prevent this:

use falkordb::FalkorClientBuilder;
use std::time::Duration;

// Convenience: just enable keepalive with a 30-second idle timeout
let client = FalkorClientBuilder::new()
    .with_tcp_keepalive(Duration::from_secs(30))
    .build()
    .expect("Failed to build client");

// Or full control via redis::io::tcp::TcpSettings
let settings = redis::io::tcp::TcpSettings::default()
    .set_nodelay(true)
    .set_keepalive(
        redis::io::tcp::socket2::TcpKeepalive::new()
            .with_time(Duration::from_secs(60)),
    );
let client = FalkorClientBuilder::new()
    .with_tcp_settings(settings)
    .build()
    .expect("Failed to build client");

Note: TCP settings apply to direct Redis TCP connections only. Unix-domain socket / embedded connections and the Sentinel connection path are not affected.

Read-only Queries and Replica Routing

Read-only queries (ro_query and call_procedure_ro) can be served from replica nodes, taking read load off the primary. When the client connects to a Redis Sentinel deployment that exposes readable replicas, it automatically builds a dedicated read-only connection pool that routes those queries to a replica. Writes always go to the primary.

Connection pool sizing: When readable replicas are present the client opens a second pool of up to num_connections additional connections (one per slot) alongside the primary pool. Size your pool limits and file-descriptor limits accordingly.

use falkordb::FalkorClientBuilder;

let client = FalkorClientBuilder::new()
    // A Sentinel endpoint, e.g. falkor://127.0.0.1:26379
    .with_connection_info("falkor://127.0.0.1:26379".try_into().expect("Invalid connection info"))
    .build()
    .expect("Failed to build client");

// `true` only when readable replicas are available.
if client.reads_from_replicas() {
    println!("Read-only queries are routed to replicas");
}

let mut graph = client.select_graph("imdb");

// Writes go to the primary.
graph.query("CREATE (:Actor {name: 'Tom Hanks'})").execute().expect("Failed to write");

// Read-only queries are served from a replica when one is available.
let mut nodes = graph.ro_query("MATCH (a:Actor) RETURN a.name").execute().expect("Failed to read");

This behavior is fully backward compatible: against a single node (or any deployment without readable replicas), ro_query / call_procedure_ro transparently fall back to the primary connection, and reads_from_replicas() returns false. See examples/readonly_replica.rs for a complete working example.

Tracing

This crate fully supports instrumentation using the tracing crate, to use it, simply, enable the tracing feature:

falkordb = { version = "0.3.0", features = ["tracing"] }

Note that different functions use different filtration levels, to avoid spamming your tests, be sure to enable the correct level as you desire it.

Embedded FalkorDB Server

This client supports running an embedded FalkorDB server, which is useful for:

  • Testing without external dependencies
  • Embedded applications
  • Quick prototyping and development

To use the embedded feature, enable it in your Cargo.toml:

falkordb = { version = "0.3.0", features = ["embedded"] }

Requirements

  • redis-server must be installed and available in PATH (or you can specify a custom path)
  • falkordb.so module must be installed (or you can specify a custom path)

You can install these from:

Usage Example

use falkordb::{EmbeddedConfig, FalkorClientBuilder, FalkorConnectionInfo};

// Create an embedded configuration with defaults
let embedded_config = EmbeddedConfig::default();

// Or customize the configuration:
// let embedded_config = EmbeddedConfig {
//     redis_server_path: Some(PathBuf::from("/path/to/redis-server")),
//     falkordb_module_path: Some(PathBuf::from("/path/to/falkordb.so")),
//     db_dir: Some(PathBuf::from("/tmp/my_falkordb")),
//     ..Default::default()
// };

// Build a client with embedded FalkorDB
let client = FalkorClientBuilder::new()
    .with_connection_info(FalkorConnectionInfo::Embedded(embedded_config))
    .build()
    .expect("Failed to build client");

// Use the client normally
let mut graph = client.select_graph("social");
graph.query("CREATE (:Person {name: 'Alice', age: 30})").execute().expect("Failed to execute query");

// The embedded server will be automatically shut down when the client is dropped

The embedded server:

  • Spawns a redis-server process with the FalkorDB module loaded
  • Uses Unix socket for communication (no network port)
  • Automatically cleans up when the client is dropped
  • Can be configured with custom paths, database directory, and socket location

Testing

Running Tests

This project includes both unit tests and integration tests.

Unit Tests

Unit tests don't require a running FalkorDB instance:

# Run all unit tests
cargo test --lib

# Run unit tests with embedded feature
cargo test --lib --features embedded

Integration Tests

Integration tests require a running FalkorDB instance. The easiest way to run them is using Docker:

# Using the provided script (requires Docker)
./run_integration_tests.sh

# Or manually start FalkorDB and run tests
docker run -d --name falkordb-test -p 6379:6379 falkordb/falkordb:latest
cargo test --test integration_tests

# With async support
cargo test --test integration_tests --features tokio

# Clean up
docker stop falkordb-test && docker rm falkordb-test

CI Integration Tests

Integration tests are automatically run in GitHub Actions using Docker services. See .github/workflows/integration-tests.yml for the CI configuration.