├── .gitignore ├── Makefile ├── tests ├── graph_test.rs ├── common │ └── mod.rs ├── assignments_test.rs └── conversions_test.rs ├── shell.nix ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── LICENSE ├── src ├── error.rs ├── lib.rs ├── assignments.rs ├── conversions.rs ├── graph.rs └── result_set.rs └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: tests 2 | .ONESHELL: tests 3 | 4 | tests: 5 | docker run --name redisgraph-rs-tests -d --rm -p 6379:6379 redislabs/redisgraph \ 6 | && cargo test 7 | docker stop redisgraph-rs-tests 8 | -------------------------------------------------------------------------------- /tests/graph_test.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use redisgraph::Graph; 4 | use serial_test::serial; 5 | 6 | use common::*; 7 | 8 | #[test] 9 | #[serial] 10 | fn test_open_delete() { 11 | let conn = get_connection(); 12 | 13 | let graph = Graph::open(conn, "test_open_delete_graph".to_string()).unwrap(); 14 | graph.delete().unwrap(); 15 | } 16 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { rustDate ? "2020-11-01" }: 2 | 3 | let 4 | mozillaOverlay = import (builtins.fetchTarball "https://github.com/mozilla/nixpkgs-mozilla/archive/8c007b60731c07dd7a052cce508de3bb1ae849b4.tar.gz"); 5 | pkgs = import { 6 | overlays = [ mozillaOverlay ]; 7 | }; 8 | rustChannel = pkgs.rustChannelOf { date = rustDate; channel = "nightly"; }; 9 | in pkgs.mkShell { 10 | nativeBuildInputs = with pkgs; [ 11 | rustChannel.rust 12 | rustfmt 13 | ]; 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | schedule: 6 | # * is a special character in YAML so you have to quote this string 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | services: 15 | redisgraph: 16 | image: redislabs/redisgraph:edge 17 | ports: 18 | - 6379:6379 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - name: Build 23 | run: cargo build --verbose 24 | - name: Run tests 25 | run: cargo test --verbose 26 | -------------------------------------------------------------------------------- /tests/common/mod.rs: -------------------------------------------------------------------------------- 1 | use redis::{Client, Connection}; 2 | use redisgraph::graph::Graph; 3 | 4 | pub fn get_connection() -> Connection { 5 | let client = Client::open(option_env!("TEST_REDIS_URI").unwrap_or("redis://127.0.0.1")) 6 | .expect("Failed to open client!"); 7 | client.get_connection().expect("Failed to get connection!") 8 | } 9 | 10 | #[allow(dead_code)] 11 | pub fn with_graph(action: F) { 12 | let conn = get_connection(); 13 | let mut graph = Graph::open(conn, "test_graph".to_string()).unwrap(); 14 | 15 | action(&mut graph); 16 | 17 | graph.delete().unwrap(); 18 | } 19 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redisgraph" 3 | version = "0.3.0" 4 | authors = ["Malte Voos "] 5 | keywords = ["redis", "database", "graph-database"] 6 | description = "A Rust client for RedisGraph." 7 | homepage = "https://github.com/malte-v/redisgraph-rs" 8 | repository = "https://github.com/malte-v/redisgraph-rs" 9 | documentation = "https://docs.rs/redisgraph" 10 | license = "MIT" 11 | readme = "README.md" 12 | edition = "2018" 13 | 14 | [dependencies] 15 | redis = "0.15.1" 16 | num = "0.2.1" 17 | num-derive = "0.3.0" 18 | num-traits = "0.2.11" 19 | 20 | [dev-dependencies] 21 | serial_test = "0.4.0" 22 | maplit = "1.0.2" 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Malte Voos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/assignments_test.rs: -------------------------------------------------------------------------------- 1 | mod common; 2 | 3 | use redisgraph::RedisGraphResult; 4 | use serial_test::serial; 5 | 6 | use common::*; 7 | 8 | #[test] 9 | #[serial] 10 | fn test_single() { 11 | with_graph(|graph| { 12 | let single: i64 = graph.query("RETURN 42").unwrap(); 13 | assert_eq!(single, 42); 14 | }); 15 | } 16 | 17 | #[test] 18 | #[serial] 19 | fn test_tuple() { 20 | with_graph(|graph| { 21 | let tuple: (i64, String, bool) = graph.query("RETURN 42, 'Hello, world!', true").unwrap(); 22 | assert_eq!(tuple.0, 42); 23 | assert_eq!(tuple.1, "Hello, world!"); 24 | assert_eq!(tuple.2, true); 25 | }); 26 | } 27 | 28 | #[test] 29 | #[serial] 30 | fn test_vec() { 31 | with_graph(|graph| { 32 | graph 33 | .mutate("CREATE (n1 { prop: 1 }), (n2 { prop: 2 }), (n3 { prop: 3 })") 34 | .unwrap(); 35 | let vec: Vec = graph 36 | .query("MATCH (n) RETURN n.prop ORDER BY n.prop") 37 | .unwrap(); 38 | assert_eq!(vec[0], 1); 39 | assert_eq!(vec[1], 2); 40 | assert_eq!(vec[2], 3); 41 | }); 42 | } 43 | 44 | #[test] 45 | #[serial] 46 | fn test_tuple_vec() { 47 | with_graph(|graph| { 48 | graph.mutate("CREATE (n1 { num: 1, word: 'foo' }), (n2 { num: 2, word: 'bar' }), (n3 { num: 3, word: 'baz' })").unwrap(); 49 | let tuple_vec: Vec<(i64, String)> = graph 50 | .query("MATCH (n) RETURN n.num, n.word ORDER BY n.num") 51 | .unwrap(); 52 | assert_eq!(tuple_vec[0], (1, "foo".to_string())); 53 | assert_eq!(tuple_vec[1], (2, "bar".to_string())); 54 | assert_eq!(tuple_vec[2], (3, "baz".to_string())); 55 | }); 56 | } 57 | 58 | #[test] 59 | #[serial] 60 | fn test_out_of_bounds() { 61 | with_graph(|graph| { 62 | let out_of_bounds_result: RedisGraphResult<(i64, String, bool)> = 63 | graph.query("RETURN 42, 'Hello, world!'"); 64 | assert!(out_of_bounds_result.is_err()); 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use redis::RedisError; 2 | 3 | /// Common error type for this crate. 4 | #[derive(Debug)] 5 | pub enum RedisGraphError { 6 | /// Any error originating from the `redis` crate. 7 | RedisError(RedisError), 8 | /// Result of a miscommunication between this crate and the database. 9 | /// 10 | /// *This should never happen. If it does, please open an issue at https://github.com/malte-v/redisgraph-rs/issues/new .* 11 | ServerTypeError(String), 12 | /// Returned if the data you requested is of a different type 13 | /// than the data returned by the database. 14 | ClientTypeError(String), 15 | 16 | /// Returned if a label name was not found in the graph's internal registry. 17 | /// 18 | /// This error is taken care of by the implementation and should never reach your code. 19 | LabelNotFound, 20 | /// Returned if a relationship type name was not found in the graph's internal registry. 21 | /// 22 | /// This error is taken care of by the implementation and should never reach your code. 23 | RelationshipTypeNotFound, 24 | /// Returned if a property key name was not found in the graph's internal registry. 25 | /// 26 | /// This error is taken care of by the implementation and should never reach your code. 27 | PropertyKeyNotFound, 28 | 29 | /// Returned if you requested a [`String`](https://doc.rust-lang.org/std/string/struct.String.html) and the database responded with bytes that are invalid UTF-8. 30 | /// 31 | /// If you don't care about whether the data is valid UTF-8, consider requesting a [`RedisString`](../result_set/struct.RedisString.html) instead. 32 | InvalidUtf8, 33 | } 34 | 35 | impl From for RedisGraphError { 36 | fn from(error: RedisError) -> RedisGraphError { 37 | RedisGraphError::RedisError(error) 38 | } 39 | } 40 | 41 | /// Common result type for this crate. 42 | pub type RedisGraphResult = Result; 43 | 44 | #[doc(hidden)] 45 | #[macro_export] 46 | macro_rules! client_type_error { 47 | ($($arg:tt)*) => { 48 | Err($crate::RedisGraphError::ClientTypeError(format!($($arg)*))) 49 | }; 50 | } 51 | 52 | #[doc(hidden)] 53 | #[macro_export] 54 | macro_rules! server_type_error { 55 | ($($arg:tt)*) => { 56 | Err($crate::RedisGraphError::ServerTypeError(format!($($arg)*))) 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![githubactions](https://github.com/malte-v/redisgraph-rs/workflows/Rust/badge.svg)](https://github.com/malte-v/redisgraph-rs/actions) 2 | [![Latest Version](https://img.shields.io/crates/v/redisgraph.svg)](https://crates.io/crates/redisgraph) 3 | [![documentation](https://docs.rs/redisgraph/badge.svg)](https://docs.rs/redisgraph) 4 | 5 | # redisgraph-rs 6 | 7 | `redisgraph-rs` is an idiomatic Rust client for RedisGraph, the graph database by Redis. 8 | 9 | This crate parses responses from RedisGraph and converts them into ordinary Rust values. 10 | It exposes a very flexible API that allows you to retrieve a single value, a single record 11 | or multiple records using only one function: [`Graph::query`](https://docs.rs/redisgraph/0.1.0/redisgraph/graph/struct.Graph.html#method.query). 12 | 13 | If you want to use this crate, add this to your Cargo.toml: 14 | 15 | ```ini 16 | [dependencies] 17 | redis = "0.15.1" 18 | redisgraph = "0.1.0" 19 | ``` 20 | 21 | **Warning**: This library has not been thoroughly tested yet and some features are still missing. 22 | Expect bugs and breaking changes. 23 | 24 | ## Resources 25 | 26 | - RedisGraph documentation: [redisgraph.io][] 27 | - API Reference: [docs.rs/redisgraph] 28 | 29 | ## Example 30 | 31 | First, run RedisGraph on your machine using 32 | 33 | ```sh 34 | $ docker run --name redisgraph-test -d --rm -p 6379:6379 redislabs/redisgraph 35 | ``` 36 | 37 | Then, try out this code: 38 | 39 | ```rust 40 | use redis::Client; 41 | use redisgraph::{Graph, RedisGraphResult}; 42 | 43 | fn main() -> RedisGraphResult<()> { 44 | let client = Client::open("redis://127.0.0.1")?; 45 | let mut connection = client.get_connection()?; 46 | 47 | let mut graph = Graph::open(connection, "MotoGP".to_string())?; 48 | 49 | // Create six nodes (three riders, three teams) and three relationships between them. 50 | graph.mutate("CREATE (:Rider {name: 'Valentino Rossi', birth_year: 1979})-[:rides]->(:Team {name: 'Yamaha'}), \ 51 | (:Rider {name:'Dani Pedrosa', birth_year: 1985, height: 1.58})-[:rides]->(:Team {name: 'Honda'}), \ 52 | (:Rider {name:'Andrea Dovizioso', birth_year: 1986, height: 1.67})-[:rides]->(:Team {name: 'Ducati'})")?; 53 | 54 | // Get the names and birth years of all riders in team Yamaha. 55 | let results: Vec<(String, u32)> = graph.query("MATCH (r:Rider)-[:rides]->(t:Team) WHERE t.name = 'Yamaha' RETURN r.name, r.birth_year")?; 56 | // Since we know just one rider in our graph rides for team Yamaha, 57 | // we can also write this and only get the first record: 58 | let (name, birth_year): (String, u32) = graph.query("MATCH (r:Rider)-[:rides]->(t:Team) WHERE t.name = 'Yamaha' RETURN r.name, r.birth_year")?; 59 | // Let's now get all the data about the riders we have. 60 | // Be aware of that we only know the height of some riders, and therefore we use an `Option`: 61 | let results: Vec<(String, u32, Option)> = graph.query("MATCH (r:Rider) RETURN r.name, r.birth_year, r.height")?; 62 | 63 | // That was just a demo; we don't need this graph anymore. Let's delete it from the database: 64 | graph.delete()?; 65 | 66 | Ok(()) 67 | } 68 | ``` 69 | 70 | [redisgraph.io]:https://redisgraph.io 71 | [docs.rs/redisgraph]:https://docs.rs/redisgraph 72 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![doc(html_logo_url = "https://oss.redislabs.com/redisgraph/images/logo_small.png")] 2 | 3 | //! # redisgraph-rs 4 | //! 5 | //! `redisgraph-rs` is an idiomatic Rust client for RedisGraph, the graph database by Redis. 6 | //! 7 | //! This crate parses responses from RedisGraph and converts them into ordinary Rust values. 8 | //! It exposes a very flexible API that allows you to retrieve a single value, a single record 9 | //! or multiple records using only one function: [`Graph::query`](graph/struct.Graph.html#method.query). 10 | //! 11 | //! If you want to use this crate, add this to your Cargo.toml: 12 | //! 13 | //! ```ini 14 | //! [dependencies] 15 | //! redis = "0.15.1" 16 | //! redisgraph = "0.1.0" 17 | //! ``` 18 | //! 19 | //! **Warning**: This library has not been thoroughly tested yet and some features are still missing. 20 | //! Expect bugs and breaking changes. 21 | //! 22 | //! ## Resources 23 | //! 24 | //! - RedisGraph documentation: [redisgraph.io][] 25 | //! - API Reference: [docs.rs/redisgraph] 26 | //! 27 | //! ## Example 28 | //! 29 | //! First, run RedisGraph on your machine using 30 | //! 31 | //! ```sh 32 | //! $ docker run --name redisgraph-test -d --rm -p 6379:6379 redislabs/redisgraph 33 | //! ``` 34 | //! 35 | //! Then, try out this code: 36 | //! 37 | //! ```rust 38 | //! use redis::Client; 39 | //! use redisgraph::{Graph, RedisGraphResult}; 40 | //! 41 | //! fn main() -> RedisGraphResult<()> { 42 | //! let client = Client::open(option_env!("TEST_REDIS_URI").unwrap_or("redis://127.0.0.1"))?; 43 | //! let mut connection = client.get_connection()?; 44 | //! 45 | //! let mut graph = Graph::open(connection, "MotoGP".to_string())?; 46 | //! 47 | //! // Create six nodes (three riders, three teams) and three relationships between them. 48 | //! graph.mutate("CREATE (:Rider {name: 'Valentino Rossi', birth_year: 1979})-[:rides]->(:Team {name: 'Yamaha'}), \ 49 | //! (:Rider {name:'Dani Pedrosa', birth_year: 1985, height: 1.58})-[:rides]->(:Team {name: 'Honda'}), \ 50 | //! (:Rider {name:'Andrea Dovizioso', birth_year: 1986, height: 1.67})-[:rides]->(:Team {name: 'Ducati'})")?; 51 | //! 52 | //! // Get the names and birth years of all riders in team Yamaha. 53 | //! let results: Vec<(String, u32)> = graph.query("MATCH (r:Rider)-[:rides]->(t:Team) WHERE t.name = 'Yamaha' RETURN r.name, r.birth_year")?; 54 | //! // Since we know just one rider in our graph rides for team Yamaha, 55 | //! // we can also write this and only get the first record: 56 | //! let (name, birth_year): (String, u32) = graph.query("MATCH (r:Rider)-[:rides]->(t:Team) WHERE t.name = 'Yamaha' RETURN r.name, r.birth_year")?; 57 | //! // Let's now get all the data about the riders we have. 58 | //! // Be aware of that we only know the height of some riders, and therefore we use an `Option`: 59 | //! let results: Vec<(String, u32, Option)> = graph.query("MATCH (r:Rider) RETURN r.name, r.birth_year, r.height")?; 60 | //! 61 | //! // That was just a demo; we don't need this graph anymore. Let's delete it from the database: 62 | //! graph.delete()?; 63 | //! 64 | //! Ok(()) 65 | //! } 66 | //! ``` 67 | //! 68 | //! [redisgraph.io]:https://redisgraph.io 69 | //! [docs.rs/redisgraph]:https://docs.rs/redisgraph 70 | 71 | #[macro_use] 72 | pub mod error; 73 | 74 | pub mod assignments; 75 | pub mod graph; 76 | pub mod result_set; 77 | 78 | mod conversions; 79 | 80 | pub use error::{RedisGraphError, RedisGraphResult}; 81 | pub use graph::Graph; 82 | pub use result_set::{RedisString, ResultSet}; 83 | -------------------------------------------------------------------------------- /src/assignments.rs: -------------------------------------------------------------------------------- 1 | use crate::{client_type_error, RedisGraphResult, ResultSet}; 2 | 3 | /// Implemented by types that can be constructed from a [`ResultSet`](../result_set/struct.ResultSet.html). 4 | pub trait FromTable: Sized { 5 | fn from_table(result_set: &ResultSet) -> RedisGraphResult; 6 | } 7 | 8 | /// Implemented by types that can be constructed from a row in a [`ResultSet`](../result_set/struct.ResultSet.html). 9 | pub trait FromRow: Sized { 10 | fn from_row(result_set: &ResultSet, row_idx: usize) -> RedisGraphResult; 11 | } 12 | 13 | /// Implemented by types that can be constructed from a cell in a [`ResultSet`](../result_set/struct.ResultSet.html). 14 | pub trait FromCell: Sized { 15 | fn from_cell( 16 | result_set: &ResultSet, 17 | row_idx: usize, 18 | column_idx: usize, 19 | ) -> RedisGraphResult; 20 | } 21 | 22 | impl FromTable for ResultSet { 23 | fn from_table(result_set: &ResultSet) -> RedisGraphResult { 24 | Ok(result_set.clone()) 25 | } 26 | } 27 | 28 | impl FromTable for Vec { 29 | fn from_table(result_set: &ResultSet) -> RedisGraphResult { 30 | let num_rows = result_set.num_rows(); 31 | let mut ret = Self::with_capacity(num_rows); 32 | 33 | for i in 0..num_rows { 34 | ret.push(T::from_row(result_set, i)?); 35 | } 36 | 37 | Ok(ret) 38 | } 39 | } 40 | 41 | // Altered version of https://github.com/mitsuhiko/redis-rs/blob/master/src/types.rs#L1080 42 | macro_rules! impl_row_for_tuple { 43 | () => (); 44 | ($($name:ident,)+) => ( 45 | #[doc(hidden)] 46 | impl<$($name: FromCell),*> FromRow for ($($name,)*) { 47 | // we have local variables named T1 as dummies and those 48 | // variables are unused. 49 | #[allow(non_snake_case, unused_variables, clippy::eval_order_dependence)] 50 | fn from_row(result_set: &ResultSet, row_idx: usize) -> RedisGraphResult<($($name,)*)> { 51 | // hacky way to count the tuple size 52 | let mut n = 0; 53 | $(let $name = (); n += 1;)* 54 | if result_set.num_columns() != n { 55 | return client_type_error!( 56 | "failed to construct tuple: tuple has {:?} entries but result table has {:?} columns", 57 | n, 58 | result_set.num_columns() 59 | ); 60 | } 61 | 62 | // this is pretty ugly too. The { i += 1; i - 1 } is rust's 63 | // postfix increment :) 64 | let mut i = 0; 65 | Ok(($({let $name = (); $name::from_cell(result_set, row_idx, { i += 1; i - 1 })?},)*)) 66 | } 67 | } 68 | impl_row_for_tuple_peel!($($name,)*); 69 | ) 70 | } 71 | 72 | /// This chips of the leading one and recurses for the rest. So if the first 73 | /// iteration was T1, T2, T3 it will recurse to T2, T3. It stops for tuples 74 | /// of size 1 (does not implement down to unit). 75 | macro_rules! impl_row_for_tuple_peel { 76 | ($name:ident, $($other:ident,)*) => (impl_row_for_tuple!($($other,)*);) 77 | } 78 | 79 | impl_row_for_tuple! { T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, } 80 | 81 | // Row and column indices default to zero for lower-level values 82 | impl FromRow for T { 83 | fn from_row(result_set: &ResultSet, row_idx: usize) -> RedisGraphResult { 84 | T::from_cell(result_set, row_idx, 0) 85 | } 86 | } 87 | 88 | impl FromTable for T { 89 | fn from_table(result_set: &ResultSet) -> RedisGraphResult { 90 | T::from_row(result_set, 0) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/conversions.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | assignments::FromCell, 3 | client_type_error, 4 | result_set::{Edge, Node, Path, RawPath, Scalar}, 5 | RedisGraphError, RedisGraphResult, RedisString, ResultSet, 6 | }; 7 | use std::convert::TryInto; 8 | 9 | impl FromCell for Scalar { 10 | fn from_cell( 11 | result_set: &ResultSet, 12 | row_idx: usize, 13 | column_idx: usize, 14 | ) -> RedisGraphResult { 15 | let scalar = result_set.get_scalar(row_idx, column_idx)?; 16 | Ok(scalar.clone()) 17 | } 18 | } 19 | 20 | impl FromCell for () { 21 | fn from_cell( 22 | result_set: &ResultSet, 23 | row_idx: usize, 24 | column_idx: usize, 25 | ) -> RedisGraphResult { 26 | let scalar = result_set.get_scalar(row_idx, column_idx)?; 27 | match scalar { 28 | Scalar::Nil => Ok(()), 29 | any => client_type_error!("failed to construct value: expected nil, found {:?}", any), 30 | } 31 | } 32 | } 33 | 34 | impl FromCell for Option { 35 | fn from_cell( 36 | result_set: &ResultSet, 37 | row_idx: usize, 38 | column_idx: usize, 39 | ) -> RedisGraphResult { 40 | let scalar = result_set.get_scalar(row_idx, column_idx)?; 41 | match scalar { 42 | Scalar::Nil => Ok(None), 43 | _ => T::from_cell(result_set, row_idx, column_idx).map(Some), 44 | } 45 | } 46 | } 47 | 48 | impl FromCell for bool { 49 | fn from_cell( 50 | result_set: &ResultSet, 51 | row_idx: usize, 52 | column_idx: usize, 53 | ) -> RedisGraphResult { 54 | let scalar = result_set.get_scalar(row_idx, column_idx)?; 55 | match scalar { 56 | Scalar::Boolean(boolean) => Ok(*boolean), 57 | any => client_type_error!( 58 | "failed to construct value: expected boolean, found {:?}", 59 | any 60 | ), 61 | } 62 | } 63 | } 64 | 65 | macro_rules! impl_from_scalar_for_integer { 66 | ($t:ty) => { 67 | impl FromCell for $t { 68 | fn from_cell( 69 | result_set: &ResultSet, 70 | row_idx: usize, 71 | column_idx: usize, 72 | ) -> RedisGraphResult { 73 | let scalar = result_set.get_scalar(row_idx, column_idx)?; 74 | match scalar { 75 | Scalar::Integer(int) => Ok(*int as $t), 76 | any => client_type_error!( 77 | "failed to construct value: expected integer, found {:?}", 78 | any 79 | ), 80 | } 81 | } 82 | } 83 | }; 84 | } 85 | 86 | impl_from_scalar_for_integer!(u8); 87 | impl_from_scalar_for_integer!(u16); 88 | impl_from_scalar_for_integer!(u32); 89 | impl_from_scalar_for_integer!(u64); 90 | impl_from_scalar_for_integer!(usize); 91 | 92 | impl_from_scalar_for_integer!(i8); 93 | impl_from_scalar_for_integer!(i16); 94 | impl_from_scalar_for_integer!(i32); 95 | impl_from_scalar_for_integer!(i64); 96 | impl_from_scalar_for_integer!(isize); 97 | 98 | macro_rules! impl_from_scalar_for_float { 99 | ($t:ty) => { 100 | impl FromCell for $t { 101 | fn from_cell( 102 | result_set: &ResultSet, 103 | row_idx: usize, 104 | column_idx: usize, 105 | ) -> RedisGraphResult { 106 | let scalar = result_set.get_scalar(row_idx, column_idx)?; 107 | match scalar { 108 | Scalar::Double(double) => Ok(*double as $t), 109 | any => client_type_error!( 110 | "failed to construct value: expected double, found {:?}", 111 | any 112 | ), 113 | } 114 | } 115 | } 116 | }; 117 | } 118 | 119 | impl_from_scalar_for_float!(f32); 120 | impl_from_scalar_for_float!(f64); 121 | 122 | impl FromCell for RedisString { 123 | fn from_cell( 124 | result_set: &ResultSet, 125 | row_idx: usize, 126 | column_idx: usize, 127 | ) -> RedisGraphResult { 128 | let scalar = result_set.get_scalar(row_idx, column_idx)?; 129 | match scalar { 130 | Scalar::String(data) => Ok(data.clone()), 131 | any => client_type_error!( 132 | "failed to construct value: expected string, found {:?}", 133 | any 134 | ), 135 | } 136 | } 137 | } 138 | 139 | impl FromCell for String { 140 | fn from_cell( 141 | result_set: &ResultSet, 142 | row_idx: usize, 143 | column_idx: usize, 144 | ) -> RedisGraphResult { 145 | let redis_string = RedisString::from_cell(result_set, row_idx, column_idx)?; 146 | String::from_utf8(redis_string.into()).map_err(|_| RedisGraphError::InvalidUtf8) 147 | } 148 | } 149 | 150 | impl FromCell for Node { 151 | fn from_cell( 152 | result_set: &ResultSet, 153 | row_idx: usize, 154 | column_idx: usize, 155 | ) -> RedisGraphResult { 156 | let node = result_set.get_node(row_idx, column_idx)?; 157 | Ok(node.clone()) 158 | } 159 | } 160 | 161 | impl FromCell for Edge { 162 | fn from_cell( 163 | result_set: &ResultSet, 164 | row_idx: usize, 165 | column_idx: usize, 166 | ) -> RedisGraphResult { 167 | let edge = result_set.get_edge(row_idx, column_idx)?; 168 | Ok(edge.clone()) 169 | } 170 | } 171 | 172 | impl FromCell for RawPath { 173 | fn from_cell( 174 | result_set: &ResultSet, 175 | row_idx: usize, 176 | column_idx: usize, 177 | ) -> RedisGraphResult { 178 | let path = result_set.get_path(row_idx, column_idx)?; 179 | Ok(path.clone()) 180 | } 181 | } 182 | 183 | impl FromCell for Path { 184 | fn from_cell( 185 | result_set: &ResultSet, 186 | row_idx: usize, 187 | column_idx: usize, 188 | ) -> RedisGraphResult { 189 | let path = result_set.get_path(row_idx, column_idx)?; 190 | path.clone().try_into() 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/graph.rs: -------------------------------------------------------------------------------- 1 | use redis::{Connection, Value}; 2 | 3 | use crate::{ 4 | assignments::FromTable, 5 | result_set::{Column, FromRedisValueWithGraph, Scalar, Statistics, Take}, 6 | server_type_error, RedisGraphError, RedisGraphResult, RedisString, ResultSet, 7 | }; 8 | 9 | /// Represents a single graph in the database. 10 | pub struct Graph { 11 | conn: Connection, 12 | name: String, 13 | 14 | labels: Vec, 15 | relationship_types: Vec, 16 | property_keys: Vec, 17 | } 18 | 19 | impl Graph { 20 | /// Opens the graph with the given name from the database. 21 | /// 22 | /// If the graph does not already exist, creates a new graph with the given name. 23 | pub fn open(conn: Connection, name: String) -> RedisGraphResult { 24 | let mut graph = Self { 25 | conn, 26 | name, 27 | labels: Vec::new(), 28 | relationship_types: Vec::new(), 29 | property_keys: Vec::new(), 30 | }; 31 | 32 | // Create a dummy node and delete it again. 33 | // This ensures that an empty graph is created and `delete()` 34 | // will succeed if the graph did not already exist. 35 | graph.mutate("CREATE (dummy:__DUMMY_LABEL__)")?; 36 | graph.mutate("MATCH (dummy:__DUMMY_LABEL__) DELETE dummy")?; 37 | 38 | Ok(graph) 39 | } 40 | 41 | /// Executes the given query and returns its return values. 42 | /// 43 | /// Only use this for queries with a `RETURN` statement. 44 | pub fn query(&mut self, query: &str) -> RedisGraphResult { 45 | self.query_with_statistics(query).map(|(value, _)| value) 46 | } 47 | 48 | /// Same as [`query`](#method.query), but also returns statistics about the query along with its return values. 49 | pub fn query_with_statistics( 50 | &mut self, 51 | query: &str, 52 | ) -> RedisGraphResult<(T, Statistics)> { 53 | let response: Value = self.request(query)?; 54 | let result_set = self.get_result_set(response)?; 55 | let value = T::from_table(&result_set)?; 56 | Ok((value, result_set.statistics)) 57 | } 58 | 59 | /// Executes the given query while not returning any values. 60 | /// 61 | /// If you want to mutate the graph and retrieve values from it 62 | /// using one query, use [`query`](#method.query) instead. 63 | pub fn mutate(&mut self, query: &str) -> RedisGraphResult<()> { 64 | self.mutate_with_statistics(query).map(|_| ()) 65 | } 66 | 67 | /// Same as [`mutate`](#method.mutate), but returns statistics about the query. 68 | pub fn mutate_with_statistics(&mut self, query: &str) -> RedisGraphResult { 69 | let response: Value = self.request(query)?; 70 | let result_set = self.get_result_set(response)?; 71 | Ok(result_set.statistics) 72 | } 73 | 74 | /// Deletes the entire graph from the database. 75 | /// 76 | /// *This action is not easily reversible.* 77 | pub fn delete(mut self) -> RedisGraphResult<()> { 78 | redis::cmd("GRAPH.DELETE") 79 | .arg(self.name()) 80 | .query::<()>(&mut self.conn) 81 | .map_err(RedisGraphError::from) 82 | } 83 | 84 | /// Updates the internal label names by retrieving them from the database. 85 | /// 86 | /// There is no real need to call this function manually. This implementation 87 | /// updates the label names automatically when they become outdated. 88 | pub fn update_labels(&mut self) -> RedisGraphResult<()> { 89 | let refresh_response = self.request("CALL db.labels()")?; 90 | self.labels = self.get_mapping(refresh_response)?; 91 | Ok(()) 92 | } 93 | 94 | /// Updates the internal relationship type names by retrieving them from the database. 95 | /// 96 | /// There is no real need to call this function manually. This implementation 97 | /// updates the relationship type names automatically when they become outdated. 98 | pub fn update_relationship_types(&mut self) -> RedisGraphResult<()> { 99 | let refresh_response = self.request("CALL db.relationshipTypes()")?; 100 | self.relationship_types = self.get_mapping(refresh_response)?; 101 | Ok(()) 102 | } 103 | 104 | /// Updates the internal property key names by retrieving them from the database. 105 | /// 106 | /// There is no real need to call this function manually. This implementation 107 | /// updates the property key names automatically when they become outdated. 108 | pub fn update_property_keys(&mut self) -> RedisGraphResult<()> { 109 | let refresh_response = self.request("CALL db.propertyKeys()")?; 110 | self.property_keys = self.get_mapping(refresh_response)?; 111 | Ok(()) 112 | } 113 | 114 | /// Returns the name of this graph. 115 | pub fn name(&self) -> &str { 116 | &self.name 117 | } 118 | 119 | /// Returns the graph's internal label names. 120 | pub fn labels(&self) -> &[RedisString] { 121 | &self.labels[..] 122 | } 123 | 124 | /// Returns the graph's internal relationship type names. 125 | pub fn relationship_types(&self) -> &[RedisString] { 126 | &self.relationship_types[..] 127 | } 128 | 129 | /// Returns the graph's internal property key names. 130 | pub fn property_keys(&self) -> &[RedisString] { 131 | &self.property_keys[..] 132 | } 133 | 134 | fn request(&mut self, query: &str) -> RedisGraphResult { 135 | redis::cmd("GRAPH.QUERY") 136 | .arg(self.name()) 137 | .arg(query) 138 | .arg("--compact") 139 | .query(&mut self.conn) 140 | .map_err(RedisGraphError::from) 141 | } 142 | 143 | fn get_result_set(&mut self, response: Value) -> RedisGraphResult { 144 | match ResultSet::from_redis_value_with_graph(response.clone(), self) { 145 | Ok(result_set) => Ok(result_set), 146 | Err(RedisGraphError::LabelNotFound) => { 147 | self.update_labels()?; 148 | self.get_result_set(response) 149 | } 150 | Err(RedisGraphError::RelationshipTypeNotFound) => { 151 | self.update_relationship_types()?; 152 | self.get_result_set(response) 153 | } 154 | Err(RedisGraphError::PropertyKeyNotFound) => { 155 | self.update_property_keys()?; 156 | self.get_result_set(response) 157 | } 158 | any_err => any_err, 159 | } 160 | } 161 | 162 | fn get_mapping(&self, response: Value) -> RedisGraphResult> { 163 | let mut result_set = ResultSet::from_redis_value_with_graph(response, self)?; 164 | match &mut result_set.columns[0] { 165 | Column::Scalars(scalars) => scalars 166 | .iter_mut() 167 | .map(|scalar| match scalar.take() { 168 | Scalar::String(string) => Ok(string), 169 | _ => server_type_error!("expected strings in first column of result set"), 170 | }) 171 | .collect::>>(), 172 | _ => server_type_error!("expected scalars as first column in result set"), 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /tests/conversions_test.rs: -------------------------------------------------------------------------------- 1 | use maplit::hashmap; 2 | use serial_test::serial; 3 | 4 | use common::*; 5 | use redisgraph::{ 6 | result_set::{Edge, Node, Path, RawPath, Scalar}, 7 | RedisString, 8 | }; 9 | 10 | mod common; 11 | 12 | #[test] 13 | #[serial] 14 | fn test_scalar() { 15 | with_graph(|graph| { 16 | let scalar: Scalar = graph.query("RETURN 42").unwrap(); 17 | assert_eq!(scalar, Scalar::Integer(42)); 18 | }); 19 | } 20 | 21 | #[test] 22 | #[serial] 23 | fn test_nil() { 24 | #[allow(clippy::let_unit_value)] 25 | with_graph(|graph| { 26 | let _nil: () = graph.query("RETURN null").unwrap(); 27 | }); 28 | } 29 | 30 | #[test] 31 | #[serial] 32 | fn test_option() { 33 | with_graph(|graph| { 34 | let results: (Option, Option) = graph.query("RETURN 42, null").unwrap(); 35 | assert_eq!(results.0, Some(42)); 36 | assert_eq!(results.1, None); 37 | }); 38 | } 39 | 40 | #[test] 41 | #[serial] 42 | fn test_bool() { 43 | with_graph(|graph| { 44 | let boolean: bool = graph.query("RETURN true").unwrap(); 45 | assert_eq!(boolean, true); 46 | }); 47 | } 48 | 49 | #[test] 50 | #[serial] 51 | fn test_int() { 52 | with_graph(|graph| { 53 | let integer: i64 = graph.query("RETURN 42").unwrap(); 54 | assert_eq!(integer, 42); 55 | }); 56 | } 57 | 58 | #[test] 59 | #[serial] 60 | fn test_float() { 61 | #[allow(clippy::float_cmp)] 62 | with_graph(|graph| { 63 | let float: f64 = graph.query("RETURN 12.3").unwrap(); 64 | assert_eq!(float, 12.3); 65 | }); 66 | } 67 | 68 | #[test] 69 | #[serial] 70 | fn test_redis_string() { 71 | with_graph(|graph| { 72 | let redis_string: RedisString = graph.query("RETURN 'Hello, world!'").unwrap(); 73 | assert_eq!(redis_string, "Hello, world!".to_string().into()); 74 | }); 75 | } 76 | 77 | #[test] 78 | #[serial] 79 | fn test_string() { 80 | with_graph(|graph| { 81 | let string: String = graph.query("RETURN 'Hello again, world!'").unwrap(); 82 | assert_eq!(string, "Hello again, world!".to_string()); 83 | }); 84 | } 85 | 86 | #[test] 87 | #[serial] 88 | fn test_node() { 89 | with_graph(|graph| { 90 | graph.mutate("CREATE (n:NodeLabel { prop: 42 })").unwrap(); 91 | let node: Node = graph.query("MATCH (n) RETURN n").unwrap(); 92 | assert_eq!( 93 | node, 94 | Node { 95 | labels: vec!["NodeLabel".to_string().into()], 96 | properties: hashmap! { 97 | "prop".to_string().into() => Scalar::Integer(42), 98 | }, 99 | } 100 | ); 101 | }); 102 | } 103 | 104 | #[test] 105 | #[serial] 106 | fn test_nodes() { 107 | with_graph(|graph| { 108 | graph.mutate("CREATE (n:NodeLabel { prop: 42 })").unwrap(); 109 | graph.mutate("CREATE (n:NodeLabel { prop: 84 })").unwrap(); 110 | let nodes: Vec = graph.query("MATCH (n) RETURN n").unwrap(); 111 | assert_eq!( 112 | nodes, 113 | vec![ 114 | Node { 115 | labels: vec!["NodeLabel".to_string().into()], 116 | properties: hashmap! { 117 | "prop".to_string().into() => Scalar::Integer(42), 118 | }, 119 | }, 120 | Node { 121 | labels: vec!["NodeLabel".to_string().into()], 122 | properties: hashmap! { 123 | "prop".to_string().into() => Scalar::Integer(84), 124 | }, 125 | } 126 | ] 127 | ); 128 | }); 129 | } 130 | 131 | #[test] 132 | #[serial] 133 | fn test_edge() { 134 | with_graph(|graph| { 135 | graph 136 | .mutate("CREATE (src)-[rel:RelationType { prop: 42 }]->(dst)") 137 | .unwrap(); 138 | let relation: Edge = graph.query("MATCH (src)-[rel]->(dst) RETURN rel").unwrap(); 139 | assert_eq!( 140 | relation, 141 | Edge { 142 | type_name: "RelationType".to_string().into(), 143 | properties: hashmap! { 144 | "prop".to_string().into() => Scalar::Integer(42), 145 | }, 146 | } 147 | ); 148 | }); 149 | } 150 | 151 | #[test] 152 | #[serial] 153 | fn test_path() { 154 | with_graph(|graph| { 155 | graph 156 | .mutate("CREATE (:L1 {prop: 1})-[:R1 {prop: 2}]->(:L2 {prop: 3})-[:R2 {prop: 4}]->(:L3 {prop: 5})") 157 | .unwrap(); 158 | let path: Path = graph 159 | .query("MATCH p = (:L1)-[:R1]->(:L2)-[:R2]->(:L3) RETURN p") 160 | .unwrap(); 161 | assert_eq!(path.len(), 2); 162 | let path: RawPath = path.into(); 163 | assert_eq!( 164 | path, 165 | RawPath { 166 | nodes: vec![ 167 | Node { 168 | labels: vec!["L1".to_string().into()], 169 | properties: hashmap! { 170 | "prop".to_string().into() => Scalar::Integer(1), 171 | }, 172 | }, 173 | Node { 174 | labels: vec!["L2".to_string().into()], 175 | properties: hashmap! { 176 | "prop".to_string().into() => Scalar::Integer(3), 177 | }, 178 | }, 179 | Node { 180 | labels: vec!["L3".to_string().into()], 181 | properties: hashmap! { 182 | "prop".to_string().into() => Scalar::Integer(5), 183 | }, 184 | }, 185 | ], 186 | edges: vec![ 187 | Edge { 188 | type_name: "R1".to_string().into(), 189 | properties: hashmap! { 190 | "prop".to_string().into() => Scalar::Integer(2), 191 | }, 192 | }, 193 | Edge { 194 | type_name: "R2".to_string().into(), 195 | properties: hashmap! { 196 | "prop".to_string().into() => Scalar::Integer(4), 197 | }, 198 | } 199 | ] 200 | } 201 | ); 202 | }); 203 | } 204 | 205 | #[test] 206 | #[serial] 207 | fn test_raw_path() { 208 | with_graph(|graph| { 209 | graph 210 | .mutate("CREATE (:L1 {prop: 1})-[:R1 {prop: 2}]->(:L2 {prop: 3})-[:R2 {prop: 4}]->(:L3 {prop: 5})") 211 | .unwrap(); 212 | let path: RawPath = graph 213 | .query("MATCH p = (:L1)-[:R1]->(:L2)-[:R2]->(:L3) RETURN p") 214 | .unwrap(); 215 | assert_eq!(path.len(), 2); 216 | assert_eq!( 217 | path, 218 | RawPath { 219 | nodes: vec![ 220 | Node { 221 | labels: vec!["L1".to_string().into()], 222 | properties: hashmap! { 223 | "prop".to_string().into() => Scalar::Integer(1), 224 | }, 225 | }, 226 | Node { 227 | labels: vec!["L2".to_string().into()], 228 | properties: hashmap! { 229 | "prop".to_string().into() => Scalar::Integer(3), 230 | }, 231 | }, 232 | Node { 233 | labels: vec!["L3".to_string().into()], 234 | properties: hashmap! { 235 | "prop".to_string().into() => Scalar::Integer(5), 236 | }, 237 | }, 238 | ], 239 | edges: vec![ 240 | Edge { 241 | type_name: "R1".to_string().into(), 242 | properties: hashmap! { 243 | "prop".to_string().into() => Scalar::Integer(2), 244 | }, 245 | }, 246 | Edge { 247 | type_name: "R2".to_string().into(), 248 | properties: hashmap! { 249 | "prop".to_string().into() => Scalar::Integer(4), 250 | }, 251 | } 252 | ] 253 | } 254 | ); 255 | }); 256 | } 257 | -------------------------------------------------------------------------------- /src/result_set.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::mem; 3 | use std::str; 4 | 5 | use num::FromPrimitive; 6 | use redis::{FromRedisValue, Value}; 7 | 8 | use crate::{server_type_error, Graph, RedisGraphError, RedisGraphResult}; 9 | use std::convert::TryFrom; 10 | 11 | /// Implemented by types that can be contructed from a 12 | /// Redis [`Value`](https://docs.rs/redis/0.15.1/redis/enum.Value.html) and a [`Graph`](../graph/struct.Graph.html) 13 | pub trait FromRedisValueWithGraph: Sized { 14 | fn from_redis_value_with_graph(value: Value, graph: &Graph) -> RedisGraphResult; 15 | } 16 | 17 | impl FromRedisValueWithGraph for T { 18 | fn from_redis_value_with_graph(value: Value, _graph: &Graph) -> RedisGraphResult { 19 | T::from_redis_value(&value).map_err(RedisGraphError::from) 20 | } 21 | } 22 | 23 | /// A result set returned by RedisGraph in response to a query. 24 | #[derive(Debug, Clone, PartialEq)] 25 | pub struct ResultSet { 26 | /// The columns of this result set. 27 | /// 28 | /// Empty if the response did not contain any return values. 29 | pub columns: Vec, 30 | /// Contains statistics messages from the response. 31 | pub statistics: Statistics, 32 | } 33 | 34 | /// Statistics returned by RedisGraph about a query as a list of messages. 35 | #[derive(Debug, Clone, PartialEq)] 36 | pub struct Statistics(pub Vec); 37 | 38 | impl ResultSet { 39 | /// Returns the number of rows in the result set. 40 | pub fn num_columns(&self) -> usize { 41 | self.columns.len() 42 | } 43 | 44 | /// Returns the number of columns in the result set. 45 | pub fn num_rows(&self) -> usize { 46 | match self.columns.get(0) { 47 | Some(first_column) => first_column.len(), 48 | None => 0, 49 | } 50 | } 51 | 52 | /// Returns the scalar at the given position. 53 | /// 54 | /// Returns an error if the value at the given position is not a scalar 55 | /// or if the position is out of bounds. 56 | pub fn get_scalar(&self, row_idx: usize, column_idx: usize) -> RedisGraphResult<&Scalar> { 57 | match self.columns.get(column_idx) { 58 | Some(column) => match column { 59 | Column::Scalars(cells) => match cells.get(row_idx) { 60 | Some(cell) => Ok(cell), 61 | None => client_type_error!( 62 | "failed to get scalar: row index out of bounds: the len is {:?} but the index is {:?}", self.columns.len(), column_idx, 63 | ), 64 | }, 65 | any => client_type_error!( 66 | "failed to get scalar: expected column of scalars, found {:?}", 67 | any 68 | ), 69 | } 70 | None => client_type_error!( 71 | "failed to get scalar: column index out of bounds: the len is {:?} but the index is {:?}", self.columns.len(), column_idx, 72 | ), 73 | } 74 | } 75 | 76 | /// Returns the node at the given position. 77 | /// 78 | /// Returns an error if the value at the given position is not a node 79 | /// or if the position is out of bounds. 80 | pub fn get_node(&self, row_idx: usize, column_idx: usize) -> RedisGraphResult<&Node> { 81 | match self.columns.get(column_idx) { 82 | Some(column) => match column { 83 | Column::Nodes(cells) => match cells.get(row_idx) { 84 | Some(cell) => Ok(cell), 85 | None => client_type_error!( 86 | "failed to get node: row index out of bounds: the len is {:?} but the index is {:?}", self.columns.len(), column_idx, 87 | ), 88 | }, 89 | Column::Scalars(cells) => match cells.get(row_idx) { 90 | Some(cell) => match cell { 91 | Scalar::Node(node) => Ok(node), 92 | _ => client_type_error!( 93 | "failed to get node: tried to get node in scalar column, but was actually {:?}", cell, 94 | ), 95 | }, 96 | None => client_type_error!( 97 | "failed to get node: row index out of bounds: the len is {:?} but the index is {:?}", self.columns.len(), column_idx, 98 | ), 99 | }, 100 | any => client_type_error!( 101 | "failed to get node: expected column of nodes, found {:?}", 102 | any 103 | ), 104 | } 105 | None => client_type_error!( 106 | "failed to get node: column index out of bounds: the len is {:?} but the index is {:?}", self.columns.len(), column_idx, 107 | ), 108 | } 109 | } 110 | 111 | /// Returns the edge at the given position. 112 | /// 113 | /// Returns an error if the value at the given position is not an edge 114 | /// or if the position is out of bounds. 115 | pub fn get_edge(&self, row_idx: usize, column_idx: usize) -> RedisGraphResult<&Edge> { 116 | match self.columns.get(column_idx) { 117 | Some(column) => match column { 118 | Column::Relations(cells) => match cells.get(row_idx) { 119 | Some(cell) => Ok(cell), 120 | None => client_type_error!( 121 | "failed to get edge: row index out of bounds: the len is {:?} but the index is {:?}", self.columns.len(), column_idx, 122 | ), 123 | }, 124 | Column::Scalars(cells) => match cells.get(row_idx) { 125 | Some(cell) => match cell { 126 | Scalar::Edge(edge) => Ok(edge), 127 | _ => client_type_error!( 128 | "failed to get edge: tried to get edge in scalar column, but was actually {:?}", cell, 129 | ), 130 | }, 131 | None => client_type_error!( 132 | "failed to get edge: row index out of bounds: the len is {:?} but the index is {:?}", self.columns.len(), column_idx, 133 | ), 134 | }, 135 | any => client_type_error!( 136 | "failed to get edge: expected column of relations or scalars, found {:?}", 137 | any 138 | ), 139 | }, 140 | None => client_type_error!( 141 | "failed to get edge: column index out of bounds: the len is {:?} but the index is {:?}", self.columns.len(), column_idx, 142 | ), 143 | } 144 | } 145 | 146 | /// Returns the path at the given position. 147 | /// 148 | /// Returns an error if the value at the given position is not a path 149 | /// or if the position is out of bounds. 150 | pub fn get_path(&self, row_idx: usize, column_idx: usize) -> RedisGraphResult<&RawPath> { 151 | match self.columns.get(column_idx) { 152 | Some(column) => match column { 153 | Column::Scalars(cells) => match cells.get(row_idx) { 154 | Some(cell) => match cell { 155 | Scalar::Path(path) => Ok(path), 156 | _ => client_type_error!( 157 | "failed to get path: tried to get path in scalar column, but was actually {:?}", cell, 158 | ), 159 | }, 160 | None => client_type_error!( 161 | "failed to get path: row index out of bounds: the len is {:?} but the index is {:?}", self.columns.len(), column_idx, 162 | ), 163 | }, 164 | any => client_type_error!( 165 | "failed to get path: expected column of scalars, found {:?}", 166 | any 167 | ), 168 | }, 169 | None => client_type_error!( 170 | "failed to get path: column index out of bounds: the len is {:?} but the index is {:?}", self.columns.len(), column_idx, 171 | ), 172 | } 173 | } 174 | } 175 | 176 | /// A single column of the result set. 177 | #[derive(Debug, Clone, PartialEq)] 178 | pub enum Column { 179 | Scalars(Vec), 180 | Nodes(Vec), 181 | Relations(Vec), 182 | } 183 | 184 | impl Column { 185 | /// Returns the lenghth of this column. 186 | pub fn len(&self) -> usize { 187 | match self { 188 | Self::Scalars(cells) => cells.len(), 189 | Self::Nodes(cells) => cells.len(), 190 | Self::Relations(cells) => cells.len(), 191 | } 192 | } 193 | 194 | /// Returns `true` if this column is empty. 195 | pub fn is_empty(&self) -> bool { 196 | self.len() == 0 197 | } 198 | } 199 | 200 | #[derive(num_derive::FromPrimitive)] 201 | enum ColumnType { 202 | Unknown = 0, 203 | Scalar = 1, 204 | Node = 2, 205 | Relation = 3, 206 | } 207 | 208 | impl FromRedisValueWithGraph for ResultSet { 209 | fn from_redis_value_with_graph(value: Value, graph: &Graph) -> RedisGraphResult { 210 | match value { 211 | Value::Bulk(mut values) => { 212 | match values.len() { 213 | 3 => { 214 | let header_row = values[0].take(); 215 | let result_rows = values[1].take(); 216 | let statistics = values[2].take(); 217 | 218 | match header_row { 219 | Value::Bulk(header_row) => { 220 | let column_count = header_row.len(); 221 | let mut columns = Vec::::with_capacity(column_count); 222 | 223 | // `result_table[0][1]` is row 0, column 1 224 | let mut result_table: Vec> = match result_rows { 225 | Value::Bulk(rows) => rows 226 | .into_iter() 227 | .map(|row| match row { 228 | Value::Bulk(row) => Ok(row), 229 | _ => server_type_error!( 230 | "expected array as result row representation", 231 | ), 232 | }) 233 | .collect::>>>(), 234 | _ => server_type_error!( 235 | "expected array as result table representation", 236 | ), 237 | }?; 238 | 239 | for i in 0..column_count { 240 | match &header_row[i] { 241 | Value::Bulk(header_cell) => { 242 | let column_type_i64 = match header_cell[0] { 243 | Value::Int(column_type_i64) => column_type_i64, 244 | _ => { 245 | return server_type_error!( 246 | "expected integer as column type", 247 | ) 248 | } 249 | }; 250 | 251 | let column = match ColumnType::from_i64(column_type_i64) { 252 | Some(ColumnType::Unknown) => server_type_error!("column type is unknown"), 253 | Some(ColumnType::Scalar) => Ok(Column::Scalars( 254 | result_table 255 | .iter_mut() 256 | .map(|row| { 257 | Scalar::from_redis_value_with_graph(row[i].take(), graph) 258 | .map_err(RedisGraphError::from) 259 | }) 260 | .collect::>>()?, 261 | )), 262 | Some(ColumnType::Node) => Ok(Column::Nodes( 263 | result_table 264 | .iter_mut() 265 | .map(|row| { 266 | Node::from_redis_value_with_graph(row[i].take(), graph) 267 | .map_err(RedisGraphError::from) 268 | }) 269 | .collect::>>()?, 270 | )), 271 | Some(ColumnType::Relation) => Ok(Column::Relations( 272 | result_table 273 | .iter_mut() 274 | .map(|row| { 275 | Edge::from_redis_value_with_graph(row[i].take(), graph) 276 | .map_err(RedisGraphError::from) 277 | }) 278 | .collect::>>()?, 279 | )), 280 | None => server_type_error!("expected integer between 0 and 3 as column type") 281 | }?; 282 | 283 | columns.push(column); 284 | } 285 | _ => { 286 | return server_type_error!( 287 | "expected array as header cell representation", 288 | ) 289 | } 290 | } 291 | } 292 | 293 | if let Some(first_column) = columns.get(0) { 294 | if !columns 295 | .iter() 296 | .all(|column| column.len() == first_column.len()) 297 | { 298 | return server_type_error!( 299 | "result columns have unequal lengths", 300 | ); 301 | } 302 | } 303 | 304 | let statistics = parse_statistics(statistics)?; 305 | 306 | Ok(Self { 307 | columns, 308 | statistics, 309 | }) 310 | } 311 | _ => server_type_error!("expected array as header row representation",), 312 | } 313 | } 314 | 1 => { 315 | let statistics = parse_statistics(values[0].take())?; 316 | 317 | Ok(Self { 318 | columns: Vec::new(), 319 | statistics, 320 | }) 321 | } 322 | _ => server_type_error!( 323 | "expected array of size 3 or 1 as result set representation", 324 | ), 325 | } 326 | } 327 | _ => server_type_error!("expected array as result set representation"), 328 | } 329 | } 330 | } 331 | 332 | fn parse_statistics(value: Value) -> RedisGraphResult { 333 | match value { 334 | Value::Bulk(statistics) => statistics 335 | .into_iter() 336 | .map(|entry| match entry { 337 | Value::Data(utf8) => { 338 | String::from_utf8(utf8).map_err(|_| RedisGraphError::InvalidUtf8) 339 | } 340 | _ => server_type_error!("expected string as statistics entry"), 341 | }) 342 | .collect::>>() 343 | .map(Statistics), 344 | _ => server_type_error!("expected array as statistics list"), 345 | } 346 | } 347 | 348 | /// A scalar value returned by RedisGraph. 349 | #[derive(Debug, Clone, PartialEq)] 350 | pub enum Scalar { 351 | Nil, 352 | Boolean(bool), 353 | Integer(i64), 354 | Double(f64), 355 | String(RedisString), 356 | Array(Vec), 357 | Edge(Edge), 358 | Node(Node), 359 | Path(RawPath), 360 | } 361 | 362 | /// Implemented for Redis types with a nil-like variant. 363 | pub trait Take { 364 | /// Takes the value, leaving the "nil" variant in its place. 365 | fn take(&mut self) -> Self; 366 | } 367 | 368 | impl Take for Value { 369 | fn take(&mut self) -> Self { 370 | mem::replace(self, Self::Nil) 371 | } 372 | } 373 | 374 | impl Take for Scalar { 375 | fn take(&mut self) -> Self { 376 | mem::replace(self, Self::Nil) 377 | } 378 | } 379 | 380 | /// A string returned by Redis. 381 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 382 | pub struct RedisString(pub Vec); 383 | 384 | impl From for RedisString { 385 | fn from(string: String) -> Self { 386 | Self(string.into_bytes()) 387 | } 388 | } 389 | 390 | impl From> for RedisString { 391 | fn from(bytes: Vec) -> Self { 392 | Self(bytes) 393 | } 394 | } 395 | 396 | impl From for Vec { 397 | fn from(redis_string: RedisString) -> Self { 398 | redis_string.0 399 | } 400 | } 401 | 402 | #[derive(num_derive::FromPrimitive)] 403 | enum ScalarType { 404 | Unknown = 0, 405 | Nil = 1, 406 | String = 2, 407 | Integer = 3, 408 | Boolean = 4, 409 | Double = 5, 410 | Array = 6, 411 | Edge = 7, 412 | Node = 8, 413 | Path = 9, 414 | } 415 | 416 | impl FromRedisValueWithGraph for Scalar { 417 | fn from_redis_value_with_graph(value: Value, graph: &Graph) -> RedisGraphResult { 418 | match value { 419 | Value::Bulk(mut values) => { 420 | if values.len() == 2 { 421 | let scalar_type = values[0].take(); 422 | let scalar_value = values[1].take(); 423 | match scalar_type { 424 | Value::Int(scalar_type_int) => match ScalarType::from_i64(scalar_type_int) { 425 | Some(ScalarType::Unknown) => server_type_error!("scalar type is unknown"), 426 | Some(ScalarType::Nil) => Ok(Scalar::Nil), 427 | Some(ScalarType::String) => match scalar_value { 428 | Value::Data(string_data) => Ok(Scalar::String(RedisString(string_data))), 429 | _ => server_type_error!("expected binary data as scalar value (scalar type is string)") 430 | }, 431 | Some(ScalarType::Integer) => match scalar_value { 432 | Value::Int(integer) => Ok(Scalar::Integer(integer)), 433 | _ => server_type_error!("expected integer as scalar value (scalar type is integer)") 434 | }, 435 | Some(ScalarType::Boolean) => match scalar_value { 436 | Value::Data(bool_data) => match &bool_data[..] { 437 | b"true" => Ok(Scalar::Boolean(true)), 438 | b"false" => Ok(Scalar::Boolean(false)), 439 | _ => server_type_error!("expected either \"true\" or \"false\" as scalar value (scalar type is boolean)") 440 | } 441 | _ => server_type_error!("expected binary data as scalar value (scalar type is boolean)") 442 | }, 443 | Some(ScalarType::Double) => match scalar_value { 444 | Value::Data(double_data) => match str::from_utf8(&double_data[..]) { 445 | Ok(double_string) => match double_string.parse::() { 446 | Ok(double) => Ok(Scalar::Double(double)), 447 | Err(_) => server_type_error!("expected string representation of double as scalar value (scalar type is double)") 448 | }, 449 | Err(_) => Err(RedisGraphError::InvalidUtf8), 450 | } 451 | _ => server_type_error!("expected string representing a double as scalar value (scalar type is double)") 452 | }, 453 | Some(ScalarType::Array) => match scalar_value { 454 | Value::Bulk(elements) => { 455 | let mut values = Vec::new(); 456 | for elem in elements { 457 | match Self::from_redis_value_with_graph(elem, graph) { 458 | Ok(val) => values.push(val), 459 | Err(e) => return Err(e), 460 | } 461 | } 462 | Ok(Scalar::Array(values)) 463 | }, 464 | _ => server_type_error!("expected something for array") 465 | }, 466 | Some(ScalarType::Node) => match Node::from_redis_value_with_graph(scalar_value, graph) { 467 | Ok(node) => Ok(Scalar::Node(node)), 468 | Err(e) => Err(e), 469 | }, 470 | Some(ScalarType::Edge) => match Edge::from_redis_value_with_graph(scalar_value, graph) { 471 | Ok(edge) => Ok(Scalar::Edge(edge)), 472 | Err(e) => Err(e), 473 | }, 474 | Some(ScalarType::Path) => match RawPath::from_redis_value_with_graph(scalar_value, graph) { 475 | Ok(path) => Ok(Scalar::Path(path)), 476 | Err(e) => Err(e), 477 | }, 478 | None => server_type_error!("expected integer between 0 and 9 (scalar type) as first element of scalar array, got {}", scalar_type_int) 479 | }, 480 | _ => server_type_error!("expected integer representing scalar type as first element of scalar array") 481 | } 482 | } else { 483 | server_type_error!("expected array of size 2 as scalar representation") 484 | } 485 | } 486 | _ => server_type_error!("expected array as scalar representation"), 487 | } 488 | } 489 | } 490 | 491 | /// A node returned by RedisGraph. 492 | #[derive(Debug, Clone, PartialEq)] 493 | pub struct Node { 494 | /// The labels attached to this node. 495 | pub labels: Vec, 496 | /// The properties of this node. 497 | pub properties: HashMap, 498 | } 499 | 500 | impl FromRedisValueWithGraph for Node { 501 | fn from_redis_value_with_graph(value: Value, graph: &Graph) -> RedisGraphResult { 502 | match value { 503 | Value::Bulk(mut values) => { 504 | if values.len() == 3 { 505 | let label_ids = values[1].take(); 506 | let properties = values[2].take(); 507 | 508 | let graph_labels = graph.labels(); 509 | let labels = match label_ids { 510 | Value::Bulk(label_ids) => label_ids 511 | .iter() 512 | .map(|label_id| { 513 | let label_id = match label_id { 514 | Value::Int(id) => id, 515 | _ => return server_type_error!("expected integer as label ID",), 516 | }; 517 | 518 | graph_labels 519 | .get(*label_id as usize) 520 | .cloned() 521 | .ok_or(RedisGraphError::LabelNotFound) 522 | }) 523 | .collect::>>()?, 524 | _ => return server_type_error!("expected array as label IDs"), 525 | }; 526 | 527 | let properties = parse_properties(graph, properties)?; 528 | 529 | Ok(Self { labels, properties }) 530 | } else { 531 | server_type_error!("expected array of size 3 as node representation") 532 | } 533 | } 534 | _ => server_type_error!("expected array as node representation"), 535 | } 536 | } 537 | } 538 | 539 | /// An edge returned by RedisGraph. 540 | #[derive(Debug, Clone, PartialEq)] 541 | pub struct Edge { 542 | /// The type name of this edge. 543 | pub type_name: RedisString, 544 | /// The properties of this edge. 545 | pub properties: HashMap, 546 | } 547 | 548 | impl FromRedisValueWithGraph for Edge { 549 | fn from_redis_value_with_graph(value: Value, graph: &Graph) -> RedisGraphResult { 550 | match value { 551 | Value::Bulk(mut values) => { 552 | if values.len() == 5 { 553 | let type_id = values[1].take(); 554 | let properties = values[4].take(); 555 | 556 | let type_name = match type_id { 557 | Value::Int(id) => graph 558 | .relationship_types() 559 | .get(id as usize) 560 | .cloned() 561 | .ok_or(RedisGraphError::RelationshipTypeNotFound)?, 562 | _ => return server_type_error!("expected integer as relationship type ID",), 563 | }; 564 | 565 | let properties = parse_properties(graph, properties)?; 566 | 567 | Ok(Self { 568 | type_name, 569 | properties, 570 | }) 571 | } else { 572 | server_type_error!("expected array of size 5 as edge representation",) 573 | } 574 | } 575 | _ => server_type_error!("expected array as edge representation"), 576 | } 577 | } 578 | } 579 | 580 | /// A raw path structure returned by RedisGraph. 581 | #[derive(Debug, Clone, PartialEq)] 582 | pub struct RawPath { 583 | /// Nodes in the path. 584 | pub nodes: Vec, 585 | /// Edges in the path. 586 | pub edges: Vec, 587 | } 588 | 589 | #[allow(clippy::len_without_is_empty)] 590 | impl RawPath { 591 | /// The length of the path. This is effectively the amount of [`Edge`]s in 592 | /// the path. 593 | pub fn len(&self) -> usize { 594 | self.edges.len() 595 | } 596 | } 597 | 598 | impl From for RawPath { 599 | fn from(path: Path) -> Self { 600 | let mut nodes: Vec = Vec::new(); 601 | let mut edges: Vec = Vec::new(); 602 | 603 | path.bifor_owned(|node| nodes.push(node), |edge| edges.push(edge)); 604 | 605 | RawPath { nodes, edges } 606 | } 607 | } 608 | 609 | /// A list-like representation of a path. 610 | #[derive(Debug, Clone, PartialEq)] 611 | pub enum Path { 612 | Cons(Node, Edge, Box), 613 | End(Node, Edge, Node), 614 | } 615 | 616 | #[allow(clippy::len_without_is_empty)] 617 | impl Path { 618 | /// Fold over edges and nodes. 619 | /// 620 | /// Fold order is as follows: 621 | /// - 1st node 622 | /// - 1st edge 623 | /// - 2nd node 624 | /// - ... 625 | /// - (n-1)th node 626 | /// - (n-1)th edge 627 | /// - nth node 628 | pub fn bifoldl(&self, mut fn_node: FN, mut fn_edge: FE, initial: A) -> A 629 | where 630 | FN: FnMut(A, &Node) -> A, 631 | FE: FnMut(A, &Edge) -> A, 632 | { 633 | match self { 634 | Path::Cons(start, edge, rest) => { 635 | let res_start = fn_node(initial, start); 636 | let res_edge = fn_edge(res_start, edge); 637 | rest.bifoldl(fn_node, fn_edge, res_edge) 638 | } 639 | Path::End(start, edge, end) => { 640 | let res_start = fn_node(initial, start); 641 | let res_edge = fn_edge(res_start, edge); 642 | fn_node(res_edge, end) 643 | } 644 | } 645 | } 646 | 647 | /// Fold over edges and nodes, consuming the path. 648 | /// 649 | /// Fold order is as follows: 650 | /// - 1st node 651 | /// - 1st edge 652 | /// - 2nd node 653 | /// - ... 654 | /// - (n-1)th node 655 | /// - (n-1)th edge 656 | /// - nth node 657 | pub fn bifoldl_owned(self, mut fn_node: FN, mut fn_edge: FE, initial: A) -> A 658 | where 659 | FN: FnMut(A, Node) -> A, 660 | FE: FnMut(A, Edge) -> A, 661 | { 662 | match self { 663 | Path::Cons(start, edge, rest) => { 664 | let res_start = fn_node(initial, start); 665 | let res_edge = fn_edge(res_start, edge); 666 | rest.bifoldl_owned(fn_node, fn_edge, res_edge) 667 | } 668 | Path::End(start, edge, end) => { 669 | let res_start = fn_node(initial, start); 670 | let res_edge = fn_edge(res_start, edge); 671 | fn_node(res_edge, end) 672 | } 673 | } 674 | } 675 | 676 | /// [`bifoldl`] without the accumulator. 677 | pub fn bifor(&self, mut fn_node: FN, mut fn_edge: FE) 678 | where 679 | FN: FnMut(&Node), 680 | FE: FnMut(&Edge), 681 | { 682 | self.bifoldl(|(), node| fn_node(node), |(), edge| fn_edge(edge), ()) 683 | } 684 | 685 | /// [`bifoldl_owned`] without the accumulator. 686 | pub fn bifor_owned(self, mut fn_node: FN, mut fn_edge: FE) 687 | where 688 | FN: FnMut(Node), 689 | FE: FnMut(Edge), 690 | { 691 | self.bifoldl_owned(|(), node| fn_node(node), |(), edge| fn_edge(edge), ()) 692 | } 693 | 694 | /// The length of the path. This is effectively the amount of [`Edge`]s, or segments, 695 | /// in the path. 696 | /// 697 | /// NOTE: runs in O(n) 698 | pub fn len(&self) -> usize { 699 | self.bifoldl(|acc, _| acc, |acc, _| acc + 1, 0) 700 | } 701 | } 702 | 703 | impl TryFrom for Path { 704 | type Error = RedisGraphError; 705 | 706 | fn try_from(mut path: RawPath) -> Result { 707 | let last_node = path.nodes.pop() 708 | .filter(|_| path.nodes.len() == path.edges.len()) 709 | .ok_or_else(|| RedisGraphError::ServerTypeError("failed to convert RawPath to Path: there must be one more node than there are edges".to_string()))?; 710 | let last_edge = path.edges.pop() 711 | .ok_or_else(|| RedisGraphError::ServerTypeError("failed to convert RawPath to Path: there must be at least one edge".to_string()))?; 712 | let second_to_last_node = path.nodes.pop() 713 | .ok_or_else(|| RedisGraphError::ClientTypeError("BUG in redisgraph-rs: while converting RawPath to Path".to_string()))?; 714 | 715 | let mut segment = Path::End(second_to_last_node, last_edge, last_node); 716 | for (node, edge) in path.nodes.into_iter().zip(path.edges.into_iter()).rev() { 717 | segment = Path::Cons(node, edge, Box::new(segment)); 718 | } 719 | 720 | Ok(segment) 721 | } 722 | } 723 | 724 | impl FromRedisValueWithGraph for RawPath { 725 | fn from_redis_value_with_graph(value: Value, graph: &Graph) -> RedisGraphResult { 726 | match value { 727 | Value::Bulk(mut values) => { 728 | if values.len() == 2 { 729 | let nodes = values[0].take(); 730 | let edges = values[1].take(); 731 | 732 | let nodes = match Scalar::from_redis_value_with_graph(nodes, graph)? { 733 | Scalar::Array(nodes) => nodes 734 | .into_iter() 735 | .map(|scalar| match scalar { 736 | Scalar::Node(node) => Ok(node), 737 | other => server_type_error!( 738 | "unexpected non-node in path nodes array, {:?}", 739 | other 740 | ), 741 | }) 742 | .collect::>>(), 743 | other => server_type_error!( 744 | "expected path nodes to be an array, not {:?}", 745 | other 746 | ), 747 | }?; 748 | 749 | let edges = match Scalar::from_redis_value_with_graph(edges, graph)? { 750 | Scalar::Array(edges) => edges 751 | .into_iter() 752 | .map(|scalar| match scalar { 753 | Scalar::Edge(edge) => Ok(edge), 754 | other => server_type_error!( 755 | "unexpected non-edge in path edges array, {:?}", 756 | other 757 | ), 758 | }) 759 | .collect::>>(), 760 | other => server_type_error!( 761 | "expected path nodes to be an array, not {:?}", 762 | other 763 | ), 764 | }?; 765 | 766 | Ok(Self { nodes, edges }) 767 | } else { 768 | server_type_error!("expected array of size 2 as path representation") 769 | } 770 | } 771 | _ => server_type_error!("expected array as path representation"), 772 | } 773 | } 774 | } 775 | 776 | fn parse_properties( 777 | graph: &Graph, 778 | properties: Value, 779 | ) -> RedisGraphResult> { 780 | let graph_property_keys = graph.property_keys(); 781 | match properties { 782 | Value::Bulk(properties) => properties 783 | .into_iter() 784 | .map(|property| match property { 785 | Value::Bulk(mut property) => { 786 | if property.len() == 3 { 787 | let property_key_id = property[0].take(); 788 | let property_type = property[1].take(); 789 | let property_value = property[2].take(); 790 | 791 | let property_key = match property_key_id { 792 | Value::Int(id) => graph_property_keys 793 | .get(id as usize) 794 | .cloned() 795 | .ok_or(RedisGraphError::PropertyKeyNotFound)?, 796 | _ => return server_type_error!("expected integer as property key ID",), 797 | }; 798 | 799 | let property_value = Scalar::from_redis_value_with_graph( 800 | Value::Bulk(vec![property_type, property_value]), 801 | graph, 802 | )?; 803 | 804 | Ok((property_key, property_value)) 805 | } else { 806 | server_type_error!("expected array of size 3 as properties representation",) 807 | } 808 | } 809 | _ => server_type_error!("expected array as properties representation"), 810 | }) 811 | .collect::>>(), 812 | _ => server_type_error!("expected array as properties representation"), 813 | } 814 | } 815 | --------------------------------------------------------------------------------