neo4rs

repository·main·Indexed 43 days ago

https://github.com/neo4j-labs/neo4rs

An asynchronous Neo4j driver for Rust implementing the Bolt protocol (compatible with Neo4j 4.x). Built on the Tokio runtime, it provides high-level APIs for executing Cypher queries, managing transactions, and handling graph data types including Nodes, Paths, and Points. Version 0.9.0-rc.10.

Tokens
10.9K
Snippets
35
Records
54
Agent score
54%

What's inside neo4rs

  1. Handle RowStream and lazy evaluation

    main

    When using Graph::execute or Txn::execute, the returned RowStream is lazy. The query is not actually processed by the server until you begin consuming the stream (e.g., via RowStream::next).

    Streams within the same transaction are well-isolated, allowing you to consume multiple streams sequentially within the transaction lifecycle.

  2. Consume query results with DetachedRowStream

    main

    A DetachedRowStream is returned when executing a query directly on the graph (e.g., via crate::Graph::execute). Unlike RowStream, it manages its own connection from the pool. The connection is automatically released back to the pool when the stream is dropped.

    Methods on DetachedRowStream (like next, single, into_stream) do not require passing a TransactionHandle because the connection is held internally.

    // Example of consuming a DetachedRowStream
    let mut stream = graph.execute("MATCH (n) RETURN n").await?;
    while let Some(row) = stream.next().await? {
        // process row
    }
    let summary = stream.finish().await?;
  3. Work with Graph Nodes using NodeRef and Node

    main

    The neo4rs driver provides two ways to represent a graph node: NodeRef<'de> and Node.

    • NodeRef<'de>: A zero-copy, borrowed representation. It is highly efficient because it borrows data (like labels and property strings) directly from the underlying buffer. Use this when performance is critical and you can manage the lifetime of the source data.
    • Node: An owned representation. It contains owned String and Vec types. Use this when you need to pass node data across thread boundaries or keep it alive after the original network buffer has been dropped.

    Common Node Metadata

    Both types provide access to core node identity:

    • id(): Returns the internal u64 ID. Note that these IDs are stable only for the duration of the session and may be reused. For persistent identity, use a custom property.
    • element_id(): Returns an Option<&str> (or Option<String>) representing the unique element ID.
    • labels(): Returns a slice of the node's labels.
    • keys(): Returns a Vec<&str> containing the names of the node's properties.
    // Example of converting a borrowed NodeRef to an owned Node
    let owned_node: Node = node_ref.into_owned();
    // or via From trait
    let owned_node: Node = node_ref.into();
  4. Manage transactions with Txn

    main

    To ensure atomicity, use Graph::start_txn to obtain a Txn handle.

    • Use Txn::commit to finalize changes.
    • Use Txn::rollback to abort changes.
    • Connection Reuse: Unlike Graph::run or Graph::execute (which take a connection from the pool and release it immediately), a Txn holds onto a single connection from the pool until the transaction is committed, rolled back, or the Txn handle is dropped.
    use neo4rs::*;
    
    #[tokio::main]
    async fn main() {
       let uri = "127.0.0.1:7687";
       let user = "neo4j";
       let pass = "neo";
       let graph = Graph::new(uri, user, pass).unwrap();
    
       let txn = graph.start_txn().await.unwrap();
       // ... execute queries using txn.run() or txn.execute() ...
       txn.commit().await.unwrap();
    }
  5. Difference between Relationship and RelationshipRef

    main

    The driver provides two ways to represent a relationship, primarily differing in how they handle memory:

    FeatureRelationshipRef<'de>Relationship
    MemoryBorrows data from the underlying buffer ('de)Owns its own data (allocates String, etc.)
    PerformanceHigher (zero-copy where possible)Lower (due to allocations)
    ConversionCan be converted to Relationship via .to_owned() or .into_owned()N/A

    Use RelationshipRef when you are iterating over results and want maximum performance by borrowing strings and bytes directly from the network buffer. Use Relationship if you need to store the relationship in a long-lived structure that outlives the initial database response buffer.

  6. Work with PathRef and Path types

    main

    The PathRef<'de> and Path types represent a sequence of nodes and relationships in a graph traversal.

    • PathRef<'de>: A zero-copy version that holds references to data (like NodeRef and RelationshipRef) within a specific lifetime. Use this when performance is critical and you are working with data deserialized from a buffer.
    • Path: An owned version that contains full Node and Relationship objects. Use this when you need to store the path independently of the original data source.

    Both types provide methods to access the start and end nodes, the number of segments, and iterators over nodes and relationships.

    // Example of accessing path components
    let start_node = path.start();
    let end_node = path.end();
    let segment_count = path.len();
    
    // Iterating over nodes
    for node in path.nodes() {
        println!("Node ID: {:?}", node.id());
    }
    
    // Iterating over relationships
    for rel in path.relationships() {
        println!("Relationship ID: {:?}", rel.id());
    }
  7. Consume query results with RowStream

    main

    A RowStream is returned when executing a query within a transaction (e.g., via crate::Txn::execute). It requires an active TransactionHandle to fetch rows from the server.

    Important: You must stream through the results using next or similar methods to ensure the query is fully executed and the server-side state is managed correctly. If you stop consuming early, you should call finish to discard remaining messages and retrieve a summary.

    // Example of consuming a RowStream within a transaction
    // Note: This is a conceptual usage pattern based on the API
    let mut stream = txn.execute("MATCH (n) RETURN n").await?;
    while let Some(row) = stream.next(&mut txn).await? {
        // process row
    }
    let summary = stream.finish(&mut txn).await?;
  8. Use Serde newtypes for Neo4j data extraction

    main

    When deserializing Neo4j Bolt results using serde, you can use specific newtype wrappers to target particular parts of the graph data structure. These types allow you to extract specific fields like IDs, labels, or properties directly into your Rust structs.

    Commonly used types include:

    • Id(u64): Extracts a node or relationship ID.
    • StartNodeId(u64) and EndNodeId(u64): Extracts the start or end node ID of a relationship.
    • Labels<Coll>: Extracts node labels (defaults to Vec<String>).
    • Type<T>: Extracts a relationship type (defaults to String).
    • Keys<Coll>: Extracts property keys (defaults to HashSet<String>).
    • Properties<Coll>: Extracts element properties (defaults to HashMap<String, BoltType>).
    • Nodes<T> and Relationships<T>: Extracts collections of nodes or relationships from a path.
  9. Iterate over path segments using Segment

    main

    A Segment combines a relationship with its specific start and end nodes, describing the direction of traversal. This is useful for reconstructing the exact path structure.

    Use the .segments() method to get an iterator of Segment<'path, N, R>. Each segment provides:

    • start: A reference to the node where the relationship begins in the traversal.
    • relationship: The relationship object.
    • end: A reference to the node where the relationship ends in the traversal.

    Note that for a relationship (n1)-[r]->(n2), if the path traverses it in reverse (n2)<-[r]-(n1), the Segment::start will be n2 and Segment::end will be n1 to reflect the traversal direction.

    // Iterating over segments to see directionality
    for segment in path.segments() {
        println!("From {:?} via {:?} to {:?}", 
            segment.start.id(), 
            segment.relationship.id(), 
            segment.end.id()
        );
    }
  10. Configure the driver with ConfigBuilder

    main

    For fine-grained control over the connection pool and protocol settings, use ConfigBuilder. This allows you to specify connection details and performance tuning parameters.

    use neo4rs::*;
    use std::time::Duration;
    
    #[tokio::main]
    async fn main() {
       let config = ConfigBuilder::default()
           .uri("127.0.0.1:7687")
           .user("neo4j")
           .password("neo")
           .db("neo4j")
           .fetch_size(500)
           .max_connections(10)
           .connection_timeout(Duration::from_secs(10))
           .build()
           .unwrap();
       let graph = Graph::connect(config).unwrap();
    }
  11. Configure the Neo4j connection using ConfigBuilder

    main

    Use ConfigBuilder to construct a Config object required for connecting to a Neo4j instance. You must provide a uri, user, and password. The builder provides several methods to tune connection pooling, timeouts, and TLS settings.

    use neo4rs::ConfigBuilder;
    use std::time::Duration;
    
    let config = ConfigBuilder::new()
        .uri("127.0.0.1:7687")
        .user("neo4j")
        .password("password")
        .db("neo4j")
        .max_connections(10)
        .fetch_size(500)
        .connection_timeout(Duration::from_secs(10))
        .build()
        .expect("Failed to build config");
  12. Connect to Neo4j using Graph::new

    main

    The simplest way to connect to a Neo4j instance is using Graph::new(uri, user, pass). This creates a connection pool and allows you to execute queries immediately.

    use neo4rs::*;
    
    #[tokio::main]
    async fn main() {
       let uri = "127.0.0.1:7687";
       let user = "neo4j";
       let pass = "neo";
    
       let graph = Graph::new(uri, user, pass).unwrap();
       // ...
    }