diff --git a/Cargo.toml b/Cargo.toml index 77bf36d..a73da46 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,14 +9,16 @@ license = "SSPL-1.0" [dependencies] log = { version = "0.4.21", default-features = false, features = ["std"] } parking_lot = { version = "0.12.3", default-features = false, features = ["deadlock_detection"] } -redis = { version = "0.25.4", default-features = false, optional = true, features = ["sentinel"] } +redis = { version = "0.25.4", default-features = false, features = ["sentinel"] } regex = { version = "1.10.5", default-features = false, features = ["std", "perf", "unicode-bool", "unicode-perl"] } strum = { version = "0.26.2", default-features = false, features = ["std", "derive"] } thiserror = "1.0.61" -tracing = { version = "0.1.40", default-features = false, features = ["std", "attributes"] } +tracing = { version = "0.1.40", default-features = false, features = ["std", "attributes"], optional = true } [features] -default = ["redis"] +default = [] + native-tls = ["redis/tls-native-tls"] rustls = ["redis/tls-rustls"] -redis = ["dep:redis"] + +tracing = ["dep:tracing"] diff --git a/README.md b/README.md index 2023efc..55e21dd 100644 --- a/README.md +++ b/README.md @@ -11,16 +11,16 @@ [![Try Free](https://img.shields.io/badge/Try%20Free-FalkorDB%20Cloud-FF8101?labelColor=FDE900&style=for-the-badge&link=https://app.falkordb.cloud)](https://app.falkordb.cloud) -FalkorDB Rust client +### FalkorDB Rust client ## Usage ### Installation -Just add it to your `Cargo.toml`, like so +Just add it to your `Cargo.toml`, like so: ```toml -falkordb = { version = "0.1.0" } +falkordb = { version = "0.1" } ``` ### Run FalkorDB instance @@ -55,3 +55,33 @@ for n in nodes.data { println!("{:?}", n[0]); } ``` + +## Features + +### SSL/TLS Support + +This client is currently built upon the [`redis`](https://docs.rs/redis/latest/redis/) crate, and therefore supports TLS using +its implementation, which uses either [`rustls`](https://docs.rs/rustls/latest/rustls/) or [`native_tls`](https://docs.rs/native-tls/latest/native_tls/). +This is not enabled by default, and the user ust opt-in by enabling the respective features: `"rustls"`/`"native-tls"`.\ + +For Rustls: + +```toml +falkordb = { version = "0.1", features = ["rustls"] } +``` + +For NativeTLS: + +```toml +falkordb = { version = "0.1", features = ["native-tls"] } +``` + +### Tracing + +This crate fully supports instrumentation using the [`tracing`](https://docs.rs/tracing/latest/tracing/) crate, to use it, simply, enable the `tracing` feature: + +```toml +falkordb = { version = "0.1", 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. diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 614eb3f..fc31ec9 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -6,8 +6,8 @@ use crate::{ client::FalkorClientProvider, connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}, - parser::utils::string_vec_from_val, - ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorResult, FalkorValue, SyncGraph, + parser::{redis_value_as_string, redis_value_as_untyped_string_vec, redis_value_as_vec}, + ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorResult, SyncGraph, }; use parking_lot::{Mutex, RwLock}; use std::{ @@ -24,6 +24,14 @@ pub(crate) struct FalkorSyncClientInner { } impl FalkorSyncClientInner { + #[cfg_attr( + feature = "tracing", + tracing::instrument( + name = "Borrow Connection From Connection Pool", + skip_all, + level = "debug" + ) + )] pub(crate) fn borrow_connection( &self, pool_owner: Arc, @@ -38,12 +46,22 @@ impl FalkorSyncClientInner { )) } + #[cfg_attr( + feature = "tracing", + tracing::instrument( + name = "Get New Sync Connection From Client", + skip_all, + level = "info" + ) + )] pub(crate) fn get_connection(&self) -> FalkorResult { self._inner.lock().get_connection() } } - -#[cfg(feature = "redis")] +#[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Check Is Sentinel", skip_all, level = "info") +)] fn is_sentinel(conn: &mut FalkorSyncConnection) -> FalkorResult { let info_map = conn.get_redis_info(Some("server"))?; Ok(info_map @@ -52,7 +70,10 @@ fn is_sentinel(conn: &mut FalkorSyncConnection) -> FalkorResult { .unwrap_or_default()) } -#[cfg(feature = "redis")] +#[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Get Sentinel Client", skip_all, level = "info") +)] pub(crate) fn get_sentinel_client( client: &mut FalkorClientProvider, connection_info: &redis::ConnectionInfo, @@ -65,8 +86,8 @@ pub(crate) fn get_sentinel_client( // This could have been so simple using the Sentinel API, but it requires a service name // Perhaps in the future we can use it if we only support the master instance to be called 'master'? let sentinel_masters = conn - .execute_command(None, "SENTINEL", Some("MASTERS"), None)? - .into_vec()?; + .execute_command(None, "SENTINEL", Some("MASTERS"), None) + .and_then(redis_value_as_vec)?; if sentinel_masters.len() != 1 { return Err(FalkorDBError::SentinelMastersCount); @@ -75,12 +96,13 @@ pub(crate) fn get_sentinel_client( let sentinel_master: HashMap<_, _> = sentinel_masters .into_iter() .next() + .and_then(|master| master.into_sequence().ok()) .ok_or(FalkorDBError::SentinelMastersCount)? - .into_vec()? .chunks_exact(2) - .flat_map(|chunk| TryInto::<[FalkorValue; 2]>::try_into(chunk.to_vec())) + .flat_map(TryInto::<&[redis::Value; 2]>::try_into) // TODO: In the future, check if this can be done with no copying, but this should be a rare function call tbh .flat_map(|[key, val]| { - Result::<_, FalkorDBError>::Ok((key.into_string()?, val.into_string()?)) + redis_value_as_string(key.to_owned()) + .and_then(|key| redis_value_as_string(val.to_owned()).map(|val| (key, val))) }) .collect(); @@ -124,6 +146,10 @@ pub struct FalkorSyncClient { } impl FalkorSyncClient { + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Create Sync Client", skip_all, level = "info") + )] pub(crate) fn create( mut client: FalkorClientProvider, connection_info: FalkorConnectionInfo, @@ -166,10 +192,14 @@ impl FalkorSyncClient { /// /// # Returns /// A [`Vec`] of [`String`]s, containing the names of available graphs + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "List Graphs", skip_all, level = "info") + )] pub fn list_graphs(&self) -> FalkorResult> { let mut conn = self.borrow_connection()?; conn.execute_command(None, "GRAPH.LIST", None, None) - .and_then(string_vec_from_val) + .and_then(redis_value_as_untyped_string_vec) } /// Return the current value of a configuration option in the database. @@ -180,38 +210,47 @@ impl FalkorSyncClient { /// /// # Returns /// A [`HashMap`] comprised of [`String`] keys, and [`ConfigValue`] values. + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Get Config Value", skip_all, level = "info") + )] pub fn config_get( &self, config_key: &str, ) -> FalkorResult> { - let mut conn = self.borrow_connection()?; - let config = conn - .execute_command(None, "GRAPH.CONFIG", Some("GET"), Some(&[config_key]))? - .into_vec()?; + let config = self + .borrow_connection() + .and_then(|mut conn| { + conn.execute_command(None, "GRAPH.CONFIG", Some("GET"), Some(&[config_key])) + }) + .and_then(redis_value_as_vec)?; if config.len() == 2 { - let [key, val]: [FalkorValue; 2] = config.try_into().map_err(|_| { + let [key, val]: [redis::Value; 2] = config.try_into().map_err(|_| { FalkorDBError::ParsingArrayToStructElementCount( - "Expected exactly 2 elements for configuration option".to_string(), + "Expected exactly 2 elements for configuration option", ) })?; - return Ok(HashMap::from([( - key.into_string()?, - ConfigValue::try_from(val)?, - )])); + return redis_value_as_string(key) + .and_then(|key| ConfigValue::try_from(val).map(|val| HashMap::from([(key, val)]))); } Ok(config .into_iter() .flat_map(|config| { - let [key, val]: [FalkorValue; 2] = config.into_vec()?.try_into().map_err(|_| { - FalkorDBError::ParsingArrayToStructElementCount( - "Expected exactly 2 elements for configuration option".to_string(), - ) - })?; - - Result::<_, FalkorDBError>::Ok((key.into_string()?, ConfigValue::try_from(val)?)) + redis_value_as_vec(config).and_then(|as_vec| { + let [key, val]: [redis::Value; 2] = as_vec.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 2 elements for configuration option", + ) + })?; + + Result::<_, FalkorDBError>::Ok(( + redis_value_as_string(key)?, + ConfigValue::try_from(val)?, + )) + }) }) .collect::>()) } @@ -222,11 +261,15 @@ impl FalkorSyncClient { /// * `config_Key`: A [`String`] representation of a configuration's key. /// The config key can also be "*", which will return ALL the configuration options. /// * `value`: The new value to set, which is anything that can be converted into a [`ConfigValue`], namely string types and i64. + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Set Config Value", skip_all, level = "info") + )] pub fn config_set>( &self, config_key: &str, value: C, - ) -> FalkorResult { + ) -> FalkorResult { self.borrow_connection()?.execute_command( None, "GRAPH.CONFIG", @@ -257,6 +300,10 @@ impl FalkorSyncClient { /// /// # Returns /// If successful, will return the new [`SyncGraph`] object. + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Copy Graph", skip_all, level = "info") + )] pub fn copy_graph( &self, graph_to_clone: &str, @@ -271,8 +318,11 @@ impl FalkorSyncClient { Ok(self.select_graph(new_graph_name)) } - #[cfg(feature = "redis")] /// Retrieves redis information + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Client Get Redis Info", skip_all, level = "info") + )] pub fn redis_info( &self, section: Option<&str>, diff --git a/src/client/builder.rs b/src/client/builder.rs index 279f274..340bb7e 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -58,7 +58,6 @@ impl FalkorClientBuilder { .try_into() .map_err(|err| FalkorDBError::InvalidConnectionInfo(err.to_string()))?; Ok(match connection_info { - #[cfg(feature = "redis")] FalkorConnectionInfo::Redis(connection_info) => FalkorClientProvider::Redis { client: redis::Client::open(connection_info.clone()) .map_err(|err| FalkorDBError::RedisError(err.to_string()))?, @@ -92,7 +91,6 @@ impl FalkorClientBuilder<'S'> { let mut client = Self::get_client(connection_info.clone())?; - #[cfg(feature = "redis")] #[allow(irrefutable_let_patterns)] if let FalkorConnectionInfo::Redis(redis_conn_info) = &connection_info { if let Some(sentinel) = @@ -121,7 +119,6 @@ mod tests { } #[test] - #[cfg(feature = "redis")] fn test_sync_builder_redis_fallback() { let client = FalkorClientBuilder::new().build(); assert!(client.is_ok()); diff --git a/src/client/mod.rs b/src/client/mod.rs index e5201b3..84e49f0 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -12,7 +12,7 @@ pub(crate) mod builder; pub(crate) enum FalkorClientProvider { #[allow(unused)] None, - #[cfg(feature = "redis")] + Redis { client: redis::Client, sentinel: Option, @@ -22,7 +22,6 @@ pub(crate) enum FalkorClientProvider { impl FalkorClientProvider { pub(crate) fn get_connection(&mut self) -> FalkorResult { Ok(match self { - #[cfg(feature = "redis")] FalkorClientProvider::Redis { sentinel: Some(sentinel), .. @@ -31,7 +30,7 @@ impl FalkorClientProvider { .get_connection() .map_err(|err| FalkorDBError::RedisError(err.to_string()))?, ), - #[cfg(feature = "redis")] + FalkorClientProvider::Redis { client, .. } => FalkorSyncConnection::Redis( client .get_connection() @@ -41,7 +40,6 @@ impl FalkorClientProvider { }) } - #[cfg(feature = "redis")] pub(crate) fn set_sentinel( &mut self, sentinel_client: redis::sentinel::SentinelClient, diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 1f65536..1f6be49 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -3,7 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{client::blocking::FalkorSyncClientInner, FalkorDBError, FalkorResult, FalkorValue}; +use crate::{ + client::blocking::FalkorSyncClientInner, parser::redis_value_as_string, FalkorDBError, + FalkorResult, +}; use std::{ collections::HashMap, sync::{mpsc, Arc}, @@ -12,20 +15,22 @@ use std::{ pub(crate) enum FalkorSyncConnection { #[allow(unused)] None, - #[cfg(feature = "redis")] Redis(redis::Connection), } impl FalkorSyncConnection { + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Connection Inner Execute Command", skip_all, level = "debug") + )] pub(crate) fn execute_command( &mut self, graph_name: Option<&str>, command: &str, subcommand: Option<&str>, params: Option<&[&str]>, - ) -> FalkorResult { + ) -> FalkorResult { match self { - #[cfg(feature = "redis")] FalkorSyncConnection::Redis(redis_conn) => { use redis::ConnectionLike as _; let mut cmd = redis::cmd(command); @@ -36,36 +41,40 @@ impl FalkorSyncConnection { cmd.arg(param.to_string()); } } - redis::FromRedisValue::from_owned_redis_value( - redis_conn - .req_command(&cmd) - .map_err(|err| match err.kind() { - redis::ErrorKind::IoError - | redis::ErrorKind::ClusterConnectionNotFound - | redis::ErrorKind::ClusterDown - | redis::ErrorKind::MasterDown => FalkorDBError::ConnectionDown, - _ => FalkorDBError::RedisError(err.to_string()), - })?, - ) - .map_err(|err| FalkorDBError::RedisParsingError(err.to_string())) + redis_conn + .req_command(&cmd) + .map_err(|err| match err.kind() { + redis::ErrorKind::IoError + | redis::ErrorKind::ClusterConnectionNotFound + | redis::ErrorKind::ClusterDown + | redis::ErrorKind::MasterDown => FalkorDBError::ConnectionDown, + _ => FalkorDBError::RedisError(err.to_string()), + }) } - FalkorSyncConnection::None => Ok(FalkorValue::None), + FalkorSyncConnection::None => Ok(redis::Value::Nil), } } - #[cfg(feature = "redis")] + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Connection Get Redis Info", skip_all, level = "info") + )] pub(crate) fn get_redis_info( &mut self, section: Option<&str>, ) -> FalkorResult> { - Ok(self - .execute_command(None, "INFO", section, None)? - .into_string()? - .split("\r\n") - .map(|info_item| info_item.split(':').collect::>()) - .flat_map(TryInto::<[&str; 2]>::try_into) - .map(|[key, val]| (key.to_string(), val.to_string())) - .collect()) + self.execute_command(None, "INFO", section, None) + .and_then(|res| { + redis_value_as_string(res) + .map(|info| { + info.split("\r\n") + .map(|info_item| info_item.split(':').collect::>()) + .flat_map(TryInto::<[&str; 2]>::try_into) + .map(|[key, val]| (key.to_string(), val.to_string())) + .collect() + }) + .map_err(|_| FalkorDBError::ParsingString) + }) } } @@ -96,13 +105,21 @@ impl BorrowedSyncConnection { self.conn.as_mut().ok_or(FalkorDBError::EmptyConnection) } + #[cfg_attr( + feature = "tracing", + tracing::instrument( + name = "Borrowed Connection Execute Command", + skip_all, + level = "trace" + ) + )] pub(crate) fn execute_command( &mut self, graph_name: Option<&str>, command: &str, subcommand: Option<&str>, params: Option<&[&str]>, - ) -> Result { + ) -> Result { match self .as_inner()? .execute_command(graph_name, command, subcommand, params) diff --git a/src/connection_info/mod.rs b/src/connection_info/mod.rs index 417cd6b..516b196 100644 --- a/src/connection_info/mod.rs +++ b/src/connection_info/mod.rs @@ -9,14 +9,12 @@ use crate::{FalkorDBError, FalkorResult}; /// The different enum variants are enabled based on compilation features #[derive(Clone, Debug)] pub enum FalkorConnectionInfo { - #[cfg(feature = "redis")] /// A Redis database connection Redis(redis::ConnectionInfo), } impl FalkorConnectionInfo { fn fallback_provider(mut full_url: String) -> FalkorResult { - #[cfg(feature = "redis")] Ok(FalkorConnectionInfo::Redis({ if full_url.starts_with("falkor://") { full_url = full_url.replace("falkor://", "redis://"); @@ -34,7 +32,6 @@ impl FalkorConnectionInfo { /// A [`String`] representation of the address and port, or a UNIX socket path pub fn address(&self) -> String { match self { - #[cfg(feature = "redis")] FalkorConnectionInfo::Redis(redis_info) => redis_info.addr.to_string(), } } @@ -52,15 +49,10 @@ impl TryFrom<&str> for FalkorConnectionInfo { .unwrap_or((format!("falkor://{value}"), "falkor")); match url_schema { - "redis" | "rediss" => { - #[cfg(feature = "redis")] - return Ok(FalkorConnectionInfo::Redis( - redis::IntoConnectionInfo::into_connection_info(value) - .map_err(|err| FalkorDBError::InvalidConnectionInfo(err.to_string()))?, - )); - #[cfg(not(feature = "redis"))] - return Err(FalkorDBError::UnavailableProvider); - } + "redis" | "rediss" => Ok(FalkorConnectionInfo::Redis( + redis::IntoConnectionInfo::into_connection_info(value) + .map_err(|err| FalkorDBError::InvalidConnectionInfo(err.to_string()))?, + )), _ => FalkorConnectionInfo::fallback_provider(url), } } @@ -90,7 +82,6 @@ mod tests { use std::{mem, str::FromStr}; #[test] - #[cfg(feature = "redis")] fn test_redis_fallback_provider() { let FalkorConnectionInfo::Redis(redis) = FalkorConnectionInfo::fallback_provider("redis://127.0.0.1:6379".to_string()).unwrap(); @@ -99,7 +90,6 @@ mod tests { } #[test] - #[cfg(feature = "redis")] fn test_try_from_redis() { let res = FalkorConnectionInfo::try_from("redis://0.0.0.0:1234"); assert!(res.is_ok()); diff --git a/src/error/mod.rs b/src/error/mod.rs index c0f82ab..ac0940f 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -57,7 +57,7 @@ pub enum FalkorDBError { ParsingError(String), /// Received malformed header. #[error("Could not parse header: {0}")] - ParsingHeader(String), + ParsingHeader(&'static str), /// The id received for this label/property/relationship was unknown. #[error("The id received for this label/property/relationship was unknown")] ParsingCompactIdUnknown, @@ -76,24 +76,24 @@ pub enum FalkorDBError { /// Element was not of type F64. #[error("Element was not of type F64")] ParsingF64, - /// Element was not of type FArray. - #[error("Element was not of type FArray")] - ParsingFArray, - /// Element was not of type FString. - #[error("Element was not of type FString")] - ParsingFString, + /// Element was not of type Array. + #[error("Element was not of type Array")] + ParsingArray, + /// Element was not of type String. + #[error("Element was not of type String")] + ParsingString, /// Element was not of type FEdge. #[error("Element was not of type FEdge")] ParsingFEdge, /// Element was not of type FNode. #[error("Element was not of type FNode")] ParsingFNode, - /// Element was not of type FPath. - #[error("Element was not of type FPath")] - ParsingFPath, - /// Element was not of type FMap. - #[error("Element was not of type FMap: {0}")] - ParsingFMap(String), + /// Element was not of type Path. + #[error("Element was not of type Path")] + ParsingPath, + /// Element was not of type Map. + #[error("Element was not of type Map")] + ParsingMap, /// Element was not of type FPoint. #[error("Element was not of type FPoint")] ParsingFPoint, @@ -106,9 +106,9 @@ pub enum FalkorDBError { /// Both key id and type marker were not of type i64. #[error("Both key id and type marker were not of type i64")] ParsingKTVTypes, - /// Attempting to parse an FArray into a struct, but the array doesn't have the expected element count. - #[error("Attempting to parse an FArray into a struct, but the array doesn't have the expected element count: {0}")] - ParsingArrayToStructElementCount(String), + /// Attempting to parse an Array into a struct, but the array doesn't have the expected element count. + #[error("Attempting to parse an Array into a struct, but the array doesn't have the expected element count: {0}")] + ParsingArrayToStructElementCount(&'static str), /// Invalid enum string variant was encountered when parsing #[error("Invalid enum string variant was encountered when parsing: {0}")] InvalidEnumType(String), diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 579809f..effc2bd 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -4,9 +4,9 @@ */ use crate::{ - client::blocking::FalkorSyncClientInner, Constraint, ConstraintType, EntityType, ExecutionPlan, - FalkorIndex, FalkorResponse, FalkorResult, FalkorValue, GraphSchema, IndexType, LazyResultSet, - ProcedureQueryBuilder, QueryBuilder, SlowlogEntry, + client::blocking::FalkorSyncClientInner, parser::redis_value_as_vec, Constraint, + ConstraintType, EntityType, ExecutionPlan, FalkorIndex, FalkorResponse, FalkorResult, + GraphSchema, IndexType, LazyResultSet, ProcedureQueryBuilder, QueryBuilder, SlowlogEntry, }; use std::{collections::HashMap, fmt::Display, sync::Arc}; @@ -41,12 +41,16 @@ impl SyncGraph { self.graph_name.as_str() } + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Graph Execute Command", skip_all, level = "info") + )] fn execute_command( &self, command: &str, subcommand: Option<&str>, params: Option<&[&str]>, - ) -> FalkorResult { + ) -> FalkorResult { self.client .borrow_connection(self.client.clone())? .execute_command(Some(self.graph_name.as_str()), command, subcommand, params) @@ -54,6 +58,10 @@ impl SyncGraph { /// Deletes the graph stored in the database, and drop all the schema caches. /// NOTE: This still maintains the graph API, operations are still viable. + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Delete Graph", skip_all, level = "info") + )] pub fn delete(&mut self) -> FalkorResult<()> { self.execute_command("GRAPH.DELETE", None, None)?; self.graph_schema.clear(); @@ -64,16 +72,24 @@ impl SyncGraph { /// /// # Returns /// A [`Vec`] of [`SlowlogEntry`], providing information about each query. + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Get Graph Slowlog", skip_all, level = "info") + )] pub fn slowlog(&self) -> FalkorResult> { - let res = self - .execute_command("GRAPH.SLOWLOG", None, None)? - .into_vec()?; - - Ok(res.into_iter().flat_map(SlowlogEntry::try_from).collect()) + self.execute_command("GRAPH.SLOWLOG", None, None) + .and_then(|res| { + redis_value_as_vec(res) + .map(|as_vec| as_vec.into_iter().flat_map(SlowlogEntry::parse).collect()) + }) } /// Resets the slowlog, all query time data will be cleared. - pub fn slowlog_reset(&self) -> FalkorResult { + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Reset Graph Slowlog", skip_all, level = "info") + )] + pub fn slowlog_reset(&self) -> FalkorResult { self.execute_command("GRAPH.SLOWLOG", None, Some(&["RESET"])) } @@ -174,6 +190,10 @@ impl SyncGraph { /// /// # Returns /// A [`Vec`] of [`FalkorIndex`] + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "List Graph Indices", skip_all, level = "info") + )] pub fn list_indices(&mut self) -> FalkorResult>> { ProcedureQueryBuilder::>>::new(self, "DB.INDEXES").execute() } @@ -189,6 +209,10 @@ impl SyncGraph { /// /// # Returns /// A [`LazyResultSet`] containing information on the created index + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Graph Create Index", skip_all, level = "info") + )] pub fn create_index( &mut self, index_field_type: IndexType, @@ -238,6 +262,10 @@ impl SyncGraph { /// /// # Arguments /// * `index_field_type` + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Graph Drop Index", skip_all, level = "info") + )] pub fn drop_index( &mut self, index_field_type: IndexType, @@ -274,6 +302,10 @@ impl SyncGraph { /// /// # Returns /// A tuple where the first element is a [`Vec`] of [`Constraint`]s, and the second element is a [`Vec`] of stats as [`String`]s + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "List Graph Constraints", skip_all, level = "info") + )] pub fn list_constraints(&mut self) -> FalkorResult>> { ProcedureQueryBuilder::>>::new(self, "DB.CONSTRAINTS") .execute() @@ -285,12 +317,16 @@ impl SyncGraph { /// * `entity_type`: Whether to apply this constraint on nodes or relationships. /// * `label`: Entities with this label will have this constraint applied to them. /// * `properties`: A slice of the names of properties this constraint will apply to. + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Create Graph Mandatory Constraint", skip_all, level = "info") + )] pub fn create_mandatory_constraint( &self, entity_type: EntityType, label: &str, properties: &[&str], - ) -> FalkorResult { + ) -> FalkorResult { let entity_type = entity_type.to_string(); let properties_count = properties.len().to_string(); @@ -313,12 +349,16 @@ impl SyncGraph { /// * `entity_type`: Whether to apply this constraint on nodes or relationships. /// * `label`: Entities with this label will have this constraint applied to them. /// * `properties`: A slice of the names of properties this constraint will apply to. + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Create Graph Unique Constraint", skip_all, level = "info") + )] pub fn create_unique_constraint( &mut self, entity_type: EntityType, label: String, properties: &[&str], - ) -> FalkorResult { + ) -> FalkorResult { self.create_index( IndexType::Range, entity_type, @@ -350,13 +390,17 @@ impl SyncGraph { /// * `entity_type`: Whether this constraint exists on nodes or relationships. /// * `label`: Remove the constraint from entities with this label. /// * `properties`: A slice of the names of properties to remove the constraint from. + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Drop Graph Constraint", skip_all, level = "info") + )] pub fn drop_constraint( &self, constraint_type: ConstraintType, entity_type: EntityType, label: &str, properties: &[&str], - ) -> FalkorResult { + ) -> FalkorResult { let constraint_type = constraint_type.to_string(); let entity_type = entity_type.to_string(); let properties_count = properties.len().to_string(); diff --git a/src/graph/query_builder.rs b/src/graph/query_builder.rs index a8cf51a..859f33c 100644 --- a/src/graph/query_builder.rs +++ b/src/graph/query_builder.rs @@ -4,12 +4,16 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, Constraint, ExecutionPlan, FalkorDBError, - FalkorIndex, FalkorParsable, FalkorResponse, FalkorResult, FalkorValue, LazyResultSet, + connection::blocking::BorrowedSyncConnection, parser::redis_value_as_vec, Constraint, + ExecutionPlan, FalkorDBError, FalkorIndex, FalkorResponse, FalkorResult, LazyResultSet, SyncGraph, }; use std::{collections::HashMap, fmt::Display, marker::PhantomData, ops::Not}; +#[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Construct Query", skip_all, level = "trace") +)] pub(crate) fn construct_query( query_str: Q, params: Option<&HashMap>, @@ -85,7 +89,11 @@ impl<'a, Output, T: Display> QueryBuilder<'a, Output, T> { } } - fn common_execute_steps(&mut self) -> FalkorResult { + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Common Query Execution Steps", skip_all, level = "trace") + )] + fn common_execute_steps(&mut self) -> FalkorResult { let mut conn = self .graph .client @@ -107,14 +115,18 @@ impl<'a, Output, T: Display> QueryBuilder<'a, Output, T> { impl<'a, T: Display> QueryBuilder<'a, FalkorResponse>, T> { /// Executes the query, retuning a [`FalkorResponse`], with a [`LazyResultSet`] as its `data` member + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Execute Lazy Result Set Query", skip_all, level = "info") + )] pub fn execute(mut self) -> FalkorResult>> { - let res = self.common_execute_steps()?.into_vec()?; + let res = self.common_execute_steps().and_then(redis_value_as_vec)?; match res.len() { 1 => { let stats = res.into_iter().next().ok_or( FalkorDBError::ParsingArrayToStructElementCount( - "One element exist but using next() failed".to_string(), + "One element exist but using next() failed", ), )?; @@ -125,9 +137,9 @@ impl<'a, T: Display> QueryBuilder<'a, FalkorResponse>, T> { ) } 2 => { - let [header, stats]: [FalkorValue; 2] = res.try_into().map_err(|_| { + let [header, stats]: [redis::Value; 2] = res.try_into().map_err(|_| { FalkorDBError::ParsingArrayToStructElementCount( - "Two elements exist but couldn't be parsed to an array".to_string(), + "Two elements exist but couldn't be parsed to an array", ) })?; @@ -138,20 +150,20 @@ impl<'a, T: Display> QueryBuilder<'a, FalkorResponse>, T> { ) } 3 => { - let [header, data, stats]: [FalkorValue; 3] = res.try_into().map_err(|_| { + let [header, data, stats]: [redis::Value; 3] = res.try_into().map_err(|_| { FalkorDBError::ParsingArrayToStructElementCount( - "3 elements exist but couldn't be parsed to an array".to_string(), + "3 elements exist but couldn't be parsed to an array", ) })?; FalkorResponse::from_response( Some(header), - LazyResultSet::new(data.into_vec()?, &mut self.graph.graph_schema), + LazyResultSet::new(redis_value_as_vec(data)?, &mut self.graph.graph_schema), stats, ) } _ => Err(FalkorDBError::ParsingArrayToStructElementCount( - "Invalid number of elements returned from query".to_string(), + "Invalid number of elements returned from query", ))?, } } @@ -162,10 +174,14 @@ impl<'a, T: Display> QueryBuilder<'a, ExecutionPlan, T> { pub fn execute(mut self) -> FalkorResult { let res = self.common_execute_steps()?; - ExecutionPlan::try_from(res) + ExecutionPlan::parse(res) } } +#[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Generate Procedure Call", skip_all, level = "trace") +)] pub(crate) fn generate_procedure_call( procedure: P, args: Option<&[T]>, @@ -273,7 +289,7 @@ impl<'a, Output> ProcedureQueryBuilder<'a, Output> { fn common_execute_steps( &mut self, conn: &mut BorrowedSyncConnection, - ) -> FalkorResult { + ) -> FalkorResult { let command = match self.readonly { true => "GRAPH.QUERY_RO", false => "GRAPH.QUERY", @@ -295,32 +311,78 @@ impl<'a, Output> ProcedureQueryBuilder<'a, Output> { impl<'a> ProcedureQueryBuilder<'a, FalkorResponse>> { /// Executes the procedure call and return a [`FalkorResponse`] type containing a result set of [`FalkorIndex`]s /// This functions consumes self + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Execute FalkorIndex Query", skip_all, level = "info") + )] pub fn execute(mut self) -> FalkorResult>> { - FalkorParsable::from_falkor_value( - self.common_execute_steps( - &mut self - .graph - .client - .borrow_connection(self.graph.client.clone())?, - )?, - &mut self.graph.graph_schema, + self.common_execute_steps( + &mut self + .graph + .client + .borrow_connection(self.graph.client.clone())?, ) + .and_then(|res| { + let [header, indices, stats]: [redis::Value; 3] = + redis_value_as_vec(res).and_then(|res_vec| { + res_vec.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 3 elements in query response", + ) + }) + })?; + + FalkorResponse::from_response( + Some(header), + redis_value_as_vec(indices).map(|indices| { + indices + .into_iter() + .flat_map(|index| FalkorIndex::parse(index, &mut self.graph.graph_schema)) + .collect() + })?, + stats, + ) + }) } } impl<'a> ProcedureQueryBuilder<'a, FalkorResponse>> { /// Executes the procedure call and return a [`FalkorResponse`] type containing a result set of [`Constraint`]s /// This functions consumes self + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Execute Constraint Procedure Call", skip_all, level = "info") + )] pub fn execute(mut self) -> FalkorResult>> { - FalkorParsable::from_falkor_value( - self.common_execute_steps( - &mut self - .graph - .client - .borrow_connection(self.graph.client.clone())?, - )?, - &mut self.graph.graph_schema, + self.common_execute_steps( + &mut self + .graph + .client + .borrow_connection(self.graph.client.clone())?, ) + .and_then(|res| { + let [header, constraints, stats]: [redis::Value; 3] = redis_value_as_vec(res) + .and_then(|res_vec| { + res_vec.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 3 elements in query response", + ) + }) + })?; + + FalkorResponse::from_response( + Some(header), + redis_value_as_vec(constraints).map(|constraints| { + constraints + .into_iter() + .flat_map(|constraint| { + Constraint::parse(constraint, &mut self.graph.graph_schema) + }) + .collect() + })?, + stats, + ) + }) } } diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index 7c618a3..7b5a589 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -4,11 +4,11 @@ */ use crate::{ - client::blocking::FalkorSyncClientInner, value::utils::parse_type, FalkorDBError, FalkorResult, - FalkorValue, + client::blocking::FalkorSyncClientInner, + parser::{parse_type, redis_value_as_int, redis_value_as_string, redis_value_as_vec}, + FalkorDBError, FalkorResult, FalkorValue, }; -use std::collections::HashMap; -use std::sync::Arc; +use std::{collections::HashMap, sync::Arc}; pub(crate) fn get_refresh_command(schema_type: SchemaType) -> &'static str { match schema_type { @@ -23,33 +23,33 @@ pub(crate) fn get_refresh_command(schema_type: SchemaType) -> &'static str { pub(crate) struct FKeyTypeVal { pub(crate) key: i64, pub(crate) type_marker: i64, - pub(crate) val: FalkorValue, + pub(crate) val: redis::Value, } -impl TryFrom for FKeyTypeVal { +impl TryFrom for FKeyTypeVal { type Error = FalkorDBError; - fn try_from(value: FalkorValue) -> FalkorResult { - let [key_raw, type_raw, val]: [FalkorValue; 3] = - value.into_vec()?.try_into().map_err(|_| { - FalkorDBError::ParsingArrayToStructElementCount( - "Expected exactly 3 elements for key-type-value property".to_string(), - ) + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "New KeyTypeValue", skip_all, level = "trace") + )] + fn try_from(value: redis::Value) -> FalkorResult { + let [key_raw, type_raw, val]: [redis::Value; 3] = + redis_value_as_vec(value).and_then(|seq| { + seq.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 3 elements for key-type-value property", + ) + }) })?; - let key = key_raw.to_i64(); - let type_marker = type_raw.to_i64(); - - match (key, type_marker) { - (Some(key), Some(type_marker)) => Ok(FKeyTypeVal { + redis_value_as_int(key_raw).and_then(|key| { + redis_value_as_int(type_raw).map(|type_marker| FKeyTypeVal { key, type_marker, val, - }), - (Some(_), None) => Err(FalkorDBError::ParsingTypeMarkerTypeMismatch)?, - (None, Some(_)) => Err(FalkorDBError::ParsingKeyIdTypeMismatch)?, - _ => Err(FalkorDBError::ParsingKTVTypes)?, - } + }) + }) } } @@ -132,6 +132,10 @@ impl GraphSchema { } } + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Refresh Schema Type", skip_all, level = "info") + )] fn refresh( &mut self, schema_type: SchemaType, @@ -143,39 +147,42 @@ impl GraphSchema { }; // This is essentially the call_procedure(), but can be done here without access to the graph(which would cause ownership issues) - let [_, keys, _]: [FalkorValue; 3] = self + let keys = self .client - .borrow_connection(self.client.clone())? - .execute_command( - Some(self.graph_name.as_str()), - "GRAPH.QUERY", - None, - Some(&[format!("CALL {}()", get_refresh_command(schema_type)).as_str()]), - )? - .into_vec()? - .try_into() - .map_err(|_| { - FalkorDBError::ParsingArrayToStructElementCount( - "Expected exactly 3 types for header-resultset-stats response from refresh query" - .to_string(), + .borrow_connection(self.client.clone()) + .and_then(|mut conn| { + conn.execute_command( + Some(self.graph_name.as_str()), + "GRAPH.QUERY", + None, + Some(&[format!("CALL {}()", get_refresh_command(schema_type)).as_str()]), ) - })?; + }) + .and_then(|res| { + redis_value_as_vec(res).and_then(|as_vec| { + as_vec.into_iter().nth(1).ok_or(FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 3 types for header-resultset-stats response from refresh query" + )) + }) + }) + .and_then(redis_value_as_vec)?; let new_keys = keys - .into_vec()? .into_iter() .enumerate() .flat_map(|(idx, item)| { FalkorResult::<(i64, String)>::Ok(( idx as i64, - item.into_vec()? - .into_iter() - .next() - .ok_or(FalkorDBError::ParsingError( + redis_value_as_vec(item) + .and_then(|item_seq| { + item_seq.into_iter().next().ok_or_else(|| { + FalkorDBError::ParsingError( "Expected new label/property to be the first element in an array" .to_string(), - )) - .and_then(|item| item.into_string())?, + ) + }) + }) + .and_then(redis_value_as_string)?, )) }) .collect::>(); @@ -184,16 +191,20 @@ impl GraphSchema { Ok(()) } + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse ID Vec To String Vec", skip_all, level = "debug") + )] pub(crate) fn parse_id_vec( &mut self, - raw_ids: Vec, + raw_ids: Vec, schema_type: SchemaType, ) -> FalkorResult> { let raw_ids_len = raw_ids.len(); raw_ids .into_iter() .try_fold(Vec::with_capacity(raw_ids_len), |mut acc, raw_id| { - let id = raw_id.to_i64().ok_or(FalkorDBError::ParsingI64)?; + let id = redis_value_as_int(raw_id)?; let value = match self .get_id_map_by_schema_type(schema_type) .get(&id) @@ -213,31 +224,36 @@ impl GraphSchema { }) } + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Properties Map", skip_all, level = "debug") + )] pub(crate) fn parse_properties_map( &mut self, - value: FalkorValue, + value: redis::Value, ) -> FalkorResult> { - let raw_properties_vec = value.into_vec()?; - let mut out_map = HashMap::with_capacity(raw_properties_vec.len()); - - for item in raw_properties_vec { - let ktv = FKeyTypeVal::try_from(item)?; - let key = match self.properties.get(&ktv.key).cloned() { - None => { - // Refresh, but this time when we try again, throw an error on failure + let raw_properties_vec = redis_value_as_vec(value)?; + + let raw_properties_len = raw_properties_vec.len(); + raw_properties_vec.into_iter().try_fold( + HashMap::with_capacity(raw_properties_len), + |mut out_map, item| { + let ktv = FKeyTypeVal::try_from(item)?; + let key = if let Some(key) = self.properties.get(&ktv.key).cloned() { + key + } else { + // Refresh the schema and attempt to retrieve the key again self.refresh(SchemaType::Properties)?; self.properties .get(&ktv.key) .cloned() .ok_or(FalkorDBError::MissingSchemaId(SchemaType::Properties))? - } - Some(key) => key, - }; - - out_map.insert(key, parse_type(ktv.type_marker, ktv.val, self)?); - } + }; - Ok(out_map) + out_map.insert(key, parse_type(ktv.type_marker, ktv.val, self)?); + Ok(out_map) + }, + ) } } @@ -272,10 +288,10 @@ pub(crate) mod tests { #[test] fn test_label_not_exists() { let mut parser = GraphSchema::new("graph_name".to_string(), create_empty_inner_client()); - let input_value = FalkorValue::Array(vec![FalkorValue::Array(vec![ - FalkorValue::I64(1), - FalkorValue::I64(2), - FalkorValue::String("test".to_string()), + let input_value = redis::Value::Bulk(vec![redis::Value::Bulk(vec![ + redis::Value::Int(1), + redis::Value::Int(2), + redis::Value::Status("test".to_string()), ])]); let result = parser.parse_properties_map(input_value); @@ -292,21 +308,21 @@ pub(crate) mod tests { ]); // Create a FalkorValue to test - let input_value = FalkorValue::Array(vec![ - FalkorValue::Array(vec![ - FalkorValue::I64(1), - FalkorValue::I64(2), - FalkorValue::String("test".to_string()), + let input_value = redis::Value::Bulk(vec![ + redis::Value::Bulk(vec![ + redis::Value::Int(1), + redis::Value::Int(2), + redis::Value::Status("test".to_string()), ]), - FalkorValue::Array(vec![ - FalkorValue::I64(2), - FalkorValue::I64(3), - FalkorValue::I64(42), + redis::Value::Bulk(vec![ + redis::Value::Int(2), + redis::Value::Int(3), + redis::Value::Int(42), ]), - FalkorValue::Array(vec![ - FalkorValue::I64(3), - FalkorValue::I64(4), - FalkorValue::Bool(true), + redis::Value::Bulk(vec![ + redis::Value::Int(3), + redis::Value::Int(4), + redis::Value::Status("true".to_string()), ]), ]); @@ -333,8 +349,14 @@ pub(crate) mod tests { (3, "property3".to_string()), ]); - let labels_ok_res = - parser.parse_id_vec(vec![3.into(), 1.into(), 2.into()], SchemaType::Labels); + let labels_ok_res = parser.parse_id_vec( + vec![ + redis::Value::Int(3), + redis::Value::Int(1), + redis::Value::Int(2), + ], + SchemaType::Labels, + ); assert!(labels_ok_res.is_ok()); assert_eq!( labels_ok_res.unwrap(), @@ -343,7 +365,11 @@ pub(crate) mod tests { // Should fail, these are not relationships let labels_not_ok_res = parser.parse_id_vec( - vec![3.into(), 1.into(), 2.into()], + vec![ + redis::Value::Int(3), + redis::Value::Int(1), + redis::Value::Int(2), + ], SchemaType::Relationships, ); assert!(labels_not_ok_res.is_err()); @@ -357,7 +383,11 @@ pub(crate) mod tests { ]); let rels_ok_res = parser.parse_id_vec( - vec![3.into(), 1.into(), 2.into()], + vec![ + redis::Value::Int(3), + redis::Value::Int(1), + redis::Value::Int(2), + ], SchemaType::Relationships, ); assert!(rels_ok_res.is_ok()); diff --git a/src/lib.rs b/src/lib.rs index e4acb0c..ba1d3d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,9 +7,6 @@ #![deny(rustdoc::broken_intra_doc_links)] #![doc = include_str!("../README.md")] -#[cfg(not(feature = "redis"))] -compile_error!("The `redis` feature must be enabled."); - mod client; mod connection; mod connection_info; @@ -20,9 +17,6 @@ mod parser; mod response; mod value; -#[cfg(feature = "redis")] -mod redis_ext; - /// A [`Result`] which only returns [`FalkorDBError`] as its E type pub type FalkorResult = Result; @@ -34,7 +28,6 @@ pub use graph::{ query_builder::{ProcedureQueryBuilder, QueryBuilder}, }; pub use graph_schema::{GraphSchema, SchemaType}; -pub use parser::FalkorParsable; pub use response::{ constraint::{Constraint, ConstraintStatus, ConstraintType}, execution_plan::ExecutionPlan, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 2c1edb1..aa60006 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3,15 +3,617 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -pub mod utils; +use crate::{Edge, FalkorDBError, FalkorResult, FalkorValue, GraphSchema, Node, Path, Point}; +use std::collections::HashMap; -use crate::{FalkorResult, FalkorValue, GraphSchema}; +pub(crate) fn redis_value_as_string(value: redis::Value) -> FalkorResult { + match value { + redis::Value::Data(data) => { + String::from_utf8(data).map_err(|_| FalkorDBError::ParsingString) + } + redis::Value::Status(status) => Ok(status), + _ => Err(FalkorDBError::ParsingString), + } +} + +pub(crate) fn redis_value_as_int(value: redis::Value) -> FalkorResult { + match value { + redis::Value::Int(int_val) => Ok(int_val), + _ => Err(FalkorDBError::ParsingI64), + } +} + +pub(crate) fn redis_value_as_bool(value: redis::Value) -> FalkorResult { + redis_value_as_string(value).and_then(|string_val| match string_val.as_str() { + "true" => Ok(true), + "false" => Ok(false), + _ => Err(FalkorDBError::ParsingBool), + }) +} + +pub(crate) fn redis_value_as_double(value: redis::Value) -> FalkorResult { + redis_value_as_string(value) + .and_then(|string_val| string_val.parse().map_err(|_| FalkorDBError::ParsingF64)) +} + +pub(crate) fn redis_value_as_vec(value: redis::Value) -> FalkorResult> { + match value { + redis::Value::Bulk(bulk_val) => Ok(bulk_val), + _ => Err(FalkorDBError::ParsingArray), + } +} + +#[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Falkor Enum", skip_all, level = "trace") +)] +pub(crate) fn parse_falkor_enum TryFrom<&'a str, Error = impl ToString>>( + value: redis::Value +) -> FalkorResult { + type_val_from_value(value) + .and_then(|(type_marker, val)| { + if type_marker == 2 { + redis_value_as_string(val) + } else { + Err(FalkorDBError::ParsingArray) + } + }) + .and_then(|val_string| { + T::try_from(val_string.as_str()) + .map_err(|err| FalkorDBError::InvalidEnumType(err.to_string())) + }) +} + +#[cfg_attr( + feature = "tracing", + tracing::instrument(name = "String Vec From Redis Value", skip_all, level = "debug") +)] +pub(crate) fn redis_value_as_typed_string_vec(value: redis::Value) -> FalkorResult> { + type_val_from_value(value) + .and_then(|(type_marker, val)| { + if type_marker == 6 { + redis_value_as_vec(val) + } else { + Err(FalkorDBError::ParsingArray) + } + }) + .map(|val_vec| { + val_vec + .into_iter() + .flat_map(redis_value_as_string) + .collect() + }) +} + +#[cfg_attr( + feature = "tracing", + tracing::instrument(name = "String Vec From Untyped Value", skip_all, level = "trace") +)] +pub(crate) fn redis_value_as_untyped_string_vec(value: redis::Value) -> FalkorResult> { + redis_value_as_vec(value) + .map(|as_vec| as_vec.into_iter().flat_map(redis_value_as_string).collect()) +} + +#[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Header", skip_all, level = "info") +)] +pub(crate) fn parse_header(header: redis::Value) -> FalkorResult> { + // Convert the header into a sequence + let header_sequence = redis_value_as_vec(header)?; + + // Initialize a vector with the capacity of the header sequence length + let header_sequence_len = header_sequence.len(); + + header_sequence.into_iter().try_fold( + Vec::with_capacity(header_sequence_len), + |mut result, item| { + // Convert the item into a sequence + let item_sequence = redis_value_as_vec(item)?; + + // Determine the key based on the length of the item sequence + let key = if item_sequence.len() == 2 { + // Extract the key from a 2-element array + let [_val, key]: [redis::Value; 2] = item_sequence.try_into().map_err(|_| { + FalkorDBError::ParsingHeader( + "Could not get 2-sized array despite there being 2 elements", + ) + })?; + key + } else { + // Get the first element from the item sequence + item_sequence.into_iter().next().ok_or({ + FalkorDBError::ParsingHeader("Expected at least one item in header vector") + })? + }; + + // Convert the key to a string and push it to the result vector + result.push(redis_value_as_string(key)?); + Ok(result) + }, + ) +} +#[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Raw Redis Value", skip_all, level = "debug") +)] +pub(crate) fn parse_raw_redis_value( + value: redis::Value, + graph_schema: &mut GraphSchema, +) -> FalkorResult { + type_val_from_value(value) + .and_then(|(type_marker, val)| parse_type(type_marker, val, graph_schema)) +} + +#[cfg_attr( + feature = "tracing", + tracing::instrument(name = "TypeVal From Value", skip_all, level = "trace") +)] +pub(crate) fn type_val_from_value( + value: redis::Value +) -> Result<(i64, redis::Value), FalkorDBError> { + redis_value_as_vec(value).and_then(|val_vec| { + val_vec + .try_into() + .map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 2 elements: type marker, and value", + ) + }) + .and_then(|[type_marker_raw, val]: [redis::Value; 2]| { + redis_value_as_int(type_marker_raw).map(|type_marker| (type_marker, val)) + }) + }) +} + +#[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Regular Falkor Map", skip_all, level = "debug") +)] +fn parse_regular_falkor_map( + value: redis::Value, + graph_schema: &mut GraphSchema, +) -> FalkorResult> { + value + .into_map_iter() + .map_err(|_| FalkorDBError::ParsingMap)? + .try_fold(HashMap::new(), |mut out_map, (key, val)| { + out_map.insert( + redis_value_as_string(key)?, + parse_raw_redis_value(val, graph_schema)?, + ); + Ok(out_map) + }) +} + +#[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Element With Type Marker", skip_all, level = "trace") +)] +pub(crate) fn parse_type( + type_marker: i64, + val: redis::Value, + graph_schema: &mut GraphSchema, +) -> Result { + let res = match type_marker { + 1 => FalkorValue::None, + 2 => FalkorValue::String(redis_value_as_string(val)?), + 3 => FalkorValue::I64(redis_value_as_int(val)?), + 4 => FalkorValue::Bool(redis_value_as_bool(val)?), + 5 => FalkorValue::F64(redis_value_as_double(val)?), + 6 => FalkorValue::Array(redis_value_as_vec(val).and_then(|val_vec| { + let len = val_vec.len(); + val_vec + .into_iter() + .try_fold(Vec::with_capacity(len), |mut acc, item| { + acc.push(parse_raw_redis_value(item, graph_schema)?); + Ok(acc) + }) + })?), + // The following types are sent as an array and require specific parsing functions + 7 => FalkorValue::Edge(Edge::parse(val, graph_schema)?), + 8 => FalkorValue::Node(Node::parse(val, graph_schema)?), + 9 => FalkorValue::Path(Path::parse(val, graph_schema)?), + 10 => FalkorValue::Map(parse_regular_falkor_map(val, graph_schema)?), + 11 => FalkorValue::Point(Point::parse(val)?), + _ => Err(FalkorDBError::ParsingUnknownType)?, + }; + + Ok(res) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + client::blocking::create_empty_inner_client, + graph_schema::tests::open_readonly_graph_with_modified_schema, FalkorDBError, + }; + + #[test] + fn test_parse_header_valid_single_key() { + let header = redis::Value::Bulk(vec![redis::Value::Bulk(vec![redis::Value::Data( + "key1".as_bytes().to_vec(), + )])]); + let result = parse_header(header); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), vec!["key1".to_string()]); + } + + #[test] + fn test_parse_header_valid_multiple_keys() { + let header = redis::Value::Bulk(vec![ + redis::Value::Bulk(vec![ + redis::Value::Data("type".as_bytes().to_vec()), + redis::Value::Data("header1".as_bytes().to_vec()), + ]), + redis::Value::Bulk(vec![redis::Value::Data("key2".as_bytes().to_vec())]), + ]); + let result = parse_header(header); + assert!(result.is_ok()); + assert_eq!( + result.unwrap(), + vec!["header1".to_string(), "key2".to_string()] + ); + } + + #[test] + fn test_parse_header_empty_header() { + let header = redis::Value::Bulk(vec![]); + let result = parse_header(header); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), Vec::::new()); + } + + #[test] + fn test_parse_header_empty_vec() { + let header = redis::Value::Bulk(vec![redis::Value::Bulk(vec![])]); + let result = parse_header(header); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + FalkorDBError::ParsingHeader("Expected at least one item in header vector") + ); + } + + #[test] + fn test_parse_header_many_elements() { + let header = redis::Value::Bulk(vec![redis::Value::Bulk(vec![ + redis::Value::Data("just_some_header".as_bytes().to_vec()), + redis::Value::Data("header1".as_bytes().to_vec()), + redis::Value::Data("extra".as_bytes().to_vec()), + ])]); + let result = parse_header(header); + assert!(result.is_ok()); + assert_eq!(result.unwrap()[0], "just_some_header"); + } + + #[test] + fn test_parse_edge() { + let mut graph = open_readonly_graph_with_modified_schema(); + + let res = parse_type( + 7, + redis::Value::Bulk(vec![ + redis::Value::Int(100), // edge id + redis::Value::Int(0), // edge type + redis::Value::Int(51), // src node + redis::Value::Int(52), // dst node + redis::Value::Bulk(vec![ + redis::Value::Bulk(vec![ + redis::Value::Int(0), + redis::Value::Int(3), + redis::Value::Int(20), + ]), + redis::Value::Bulk(vec![ + redis::Value::Int(1), + redis::Value::Int(4), + redis::Value::Status("false".to_string()), + ]), + ]), + ]), + &mut graph.graph_schema, + ); + assert!(res.is_ok()); + + let falkor_edge = res.unwrap(); + + let FalkorValue::Edge(edge) = falkor_edge else { + panic!("Was not of type edge") + }; + assert_eq!(edge.entity_id, 100); + assert_eq!(edge.relationship_type, "very".to_string()); + assert_eq!(edge.src_node_id, 51); + assert_eq!(edge.dst_node_id, 52); + + assert_eq!(edge.properties.len(), 2); + assert_eq!(edge.properties.get("age"), Some(&FalkorValue::I64(20))); + assert_eq!( + edge.properties.get("is_boring"), + Some(&FalkorValue::Bool(false)) + ); + } + + #[test] + fn test_parse_node() { + let mut graph = open_readonly_graph_with_modified_schema(); + + let res = parse_type( + 8, + redis::Value::Bulk(vec![ + redis::Value::Int(51), // node id + redis::Value::Bulk(vec![redis::Value::Int(0), redis::Value::Int(1)]), // node type + redis::Value::Bulk(vec![ + redis::Value::Bulk(vec![ + redis::Value::Int(0), + redis::Value::Int(3), + redis::Value::Int(15), + ]), + redis::Value::Bulk(vec![ + redis::Value::Int(2), + redis::Value::Int(2), + redis::Value::Status("the something".to_string()), + ]), + redis::Value::Bulk(vec![ + redis::Value::Int(3), + redis::Value::Int(5), + redis::Value::Status("105.5".to_string()), + ]), + ]), + ]), + &mut graph.graph_schema, + ); + assert!(res.is_ok()); + + let falkor_node = res.unwrap(); + let FalkorValue::Node(node) = falkor_node else { + panic!("Was not of type node") + }; + + assert_eq!(node.entity_id, 51); + assert_eq!(node.labels, vec!["much".to_string(), "actor".to_string()]); + assert_eq!(node.properties.len(), 3); + assert_eq!(node.properties.get("age"), Some(&FalkorValue::I64(15))); + assert_eq!( + node.properties.get("something_else"), + Some(&FalkorValue::String("the something".to_string())) + ); + assert_eq!( + node.properties.get("secs_since_login"), + Some(&FalkorValue::F64(105.5)) + ); + } + + #[test] + fn test_parse_path() { + let mut graph = open_readonly_graph_with_modified_schema(); + + let res = parse_type( + 9, + redis::Value::Bulk(vec![ + redis::Value::Bulk(vec![ + redis::Value::Bulk(vec![ + redis::Value::Int(51), + redis::Value::Bulk(vec![redis::Value::Int(0)]), + redis::Value::Bulk(vec![]), + ]), + redis::Value::Bulk(vec![ + redis::Value::Int(52), + redis::Value::Bulk(vec![redis::Value::Int(0)]), + redis::Value::Bulk(vec![]), + ]), + redis::Value::Bulk(vec![ + redis::Value::Int(53), + redis::Value::Bulk(vec![redis::Value::Int(0)]), + redis::Value::Bulk(vec![]), + ]), + ]), + redis::Value::Bulk(vec![ + redis::Value::Bulk(vec![ + redis::Value::Int(100), + redis::Value::Int(0), + redis::Value::Int(51), + redis::Value::Int(52), + redis::Value::Bulk(vec![]), + ]), + redis::Value::Bulk(vec![ + redis::Value::Int(101), + redis::Value::Int(1), + redis::Value::Int(52), + redis::Value::Int(53), + redis::Value::Bulk(vec![]), + ]), + ]), + ]), + &mut graph.graph_schema, + ); + assert!(res.is_ok()); + + let falkor_path = res.unwrap(); + let FalkorValue::Path(path) = falkor_path else { + panic!("Is not of type path") + }; + + assert_eq!(path.nodes.len(), 3); + assert_eq!(path.nodes[0].entity_id, 51); + assert_eq!(path.nodes[1].entity_id, 52); + assert_eq!(path.nodes[2].entity_id, 53); + + assert_eq!(path.relationships.len(), 2); + assert_eq!(path.relationships[0].entity_id, 100); + assert_eq!(path.relationships[1].entity_id, 101); + + assert_eq!(path.relationships[0].src_node_id, 51); + assert_eq!(path.relationships[0].dst_node_id, 52); + + assert_eq!(path.relationships[1].src_node_id, 52); + assert_eq!(path.relationships[1].dst_node_id, 53); + } + + #[test] + fn test_parse_map() { + let mut graph = open_readonly_graph_with_modified_schema(); + + let res = parse_type( + 10, + redis::Value::Bulk(vec![ + redis::Value::Status("key0".to_string()), + redis::Value::Bulk(vec![ + redis::Value::Int(2), + redis::Value::Status("val0".to_string()), + ]), + redis::Value::Status("key1".to_string()), + redis::Value::Bulk(vec![redis::Value::Int(3), redis::Value::Int(1)]), + redis::Value::Status("key2".to_string()), + redis::Value::Bulk(vec![ + redis::Value::Int(4), + redis::Value::Status("true".to_string()), + ]), + ]), + &mut graph.graph_schema, + ); + assert!(res.is_ok()); + + let falkor_map = res.unwrap(); + let FalkorValue::Map(map) = falkor_map else { + panic!("Is not of type map") + }; + + assert_eq!(map.len(), 3); + assert_eq!( + map.get("key0"), + Some(&FalkorValue::String("val0".to_string())) + ); + assert_eq!(map.get("key1"), Some(&FalkorValue::I64(1))); + assert_eq!(map.get("key2"), Some(&FalkorValue::Bool(true))); + } + + #[test] + fn test_parse_point() { + let mut graph = open_readonly_graph_with_modified_schema(); + + let res = parse_type( + 11, + redis::Value::Bulk(vec![ + redis::Value::Status("102.0".to_string()), + redis::Value::Status("15.2".to_string()), + ]), + &mut graph.graph_schema, + ); + assert!(res.is_ok()); + + let falkor_point = res.unwrap(); + let FalkorValue::Point(point) = falkor_point else { + panic!("Is not of type point") + }; + assert_eq!(point.latitude, 102.0); + assert_eq!(point.longitude, 15.2); + } + + #[test] + fn test_map_not_a_vec() { + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); + + let res = + parse_regular_falkor_map(redis::Value::Status("Hello".to_string()), &mut graph_schema); + + assert!(res.is_err()) + } + + #[test] + fn test_map_vec_odd_element_count() { + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); + + let res = parse_regular_falkor_map( + redis::Value::Bulk(vec![redis::Value::Nil; 7]), + &mut graph_schema, + ); + + assert!(res.is_err()) + } + + #[test] + fn test_map_val_element_is_not_array() { + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); + + let res = parse_regular_falkor_map( + redis::Value::Bulk(vec![ + redis::Value::Status("Key".to_string()), + redis::Value::Status("false".to_string()), + ]), + &mut graph_schema, + ); + + assert!(res.is_err()) + } + + #[test] + fn test_map_val_element_has_only_1_element() { + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); + + let res = parse_regular_falkor_map( + redis::Value::Bulk(vec![ + redis::Value::Status("Key".to_string()), + redis::Value::Bulk(vec![redis::Value::Int(7)]), + ]), + &mut graph_schema, + ); + + assert!(res.is_err()) + } + + #[test] + fn test_map_val_element_has_ge_2_elements() { + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); + + let res = parse_regular_falkor_map( + redis::Value::Bulk(vec![ + redis::Value::Status("Key".to_string()), + redis::Value::Bulk(vec![redis::Value::Int(3); 3]), + ]), + &mut graph_schema, + ); + + assert!(res.is_err()) + } + + #[test] + fn test_map_val_element_mismatch_type_marker() { + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); + + let res = parse_regular_falkor_map( + redis::Value::Bulk(vec![ + redis::Value::Status("Key".to_string()), + redis::Value::Bulk(vec![ + redis::Value::Int(3), + redis::Value::Status("true".to_string()), + ]), + ]), + &mut graph_schema, + ); + + assert!(res.is_err()) + } + + #[test] + fn test_map_ok_values() { + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); + + let res = parse_regular_falkor_map( + redis::Value::Bulk(vec![ + redis::Value::Status("IntKey".to_string()), + redis::Value::Bulk(vec![redis::Value::Int(3), redis::Value::Int(1)]), + redis::Value::Status("BoolKey".to_string()), + redis::Value::Bulk(vec![ + redis::Value::Int(4), + redis::Value::Status("true".to_string()), + ]), + ]), + &mut graph_schema, + ) + .expect("Could not parse map"); -/// This trait allows implementing a parser from the table-style result sent by the database, to any other struct -pub trait FalkorParsable: Sized { - /// Parse the following value, using the graph schem owned by the graph object, and the connection used to make the request - fn from_falkor_value( - value: FalkorValue, - graph_schema: &mut GraphSchema, - ) -> FalkorResult; + assert_eq!(res.get("IntKey"), Some(FalkorValue::I64(1)).as_ref()); + assert_eq!(res.get("BoolKey"), Some(FalkorValue::Bool(true)).as_ref()); + } } diff --git a/src/parser/utils.rs b/src/parser/utils.rs deleted file mode 100644 index f02b6fc..0000000 --- a/src/parser/utils.rs +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{FalkorDBError, FalkorResult, FalkorValue}; - -pub(crate) fn string_vec_from_val(value: FalkorValue) -> FalkorResult> { - value.into_vec().map(|value_as_vec| { - value_as_vec - .into_iter() - .flat_map(FalkorValue::into_string) - .collect() - }) -} - -pub(crate) fn parse_header(header: FalkorValue) -> FalkorResult> { - let in_vec = header.into_vec()?; - let in_vec_len = in_vec.len(); - in_vec - .into_iter() - .try_fold(Vec::with_capacity(in_vec_len), |mut acc, item| { - let item_vec = item.into_vec()?; - - acc.push( - if item_vec.len() == 2 { - let [_, key]: [FalkorValue; 2] = item_vec.try_into().map_err(|_| { - FalkorDBError::ParsingHeader( - "Could not get 2-sized array despite there being 2 elements" - .to_string(), - ) - })?; - key - } else { - item_vec - .into_iter() - .next() - .ok_or(FalkorDBError::ParsingHeader( - "Expected at least one item in header vector".to_string(), - ))? - } - .into_string()?, - ); - Ok(acc) - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{FalkorDBError, FalkorValue}; - - #[test] - fn test_parse_header_valid_single_key() { - let header = FalkorValue::Array(vec![FalkorValue::Array(vec![FalkorValue::String( - "key1".to_string(), - )])]); - let result = parse_header(header); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), vec!["key1".to_string()]); - } - - #[test] - fn test_parse_header_valid_multiple_keys() { - let header = FalkorValue::Array(vec![ - FalkorValue::Array(vec![ - FalkorValue::String("type".to_string()), - FalkorValue::String("header1".to_string()), - ]), - FalkorValue::Array(vec![FalkorValue::String("key2".to_string())]), - ]); - let result = parse_header(header); - assert!(result.is_ok()); - assert_eq!( - result.unwrap(), - vec!["header1".to_string(), "key2".to_string()] - ); - } - - #[test] - fn test_parse_header_empty_header() { - let header = FalkorValue::Array(vec![]); - let result = parse_header(header); - assert!(result.is_ok()); - assert_eq!(result.unwrap(), Vec::::new()); - } - - #[test] - fn test_parse_header_empty_vec() { - let header = FalkorValue::Array(vec![FalkorValue::Array(vec![])]); - let result = parse_header(header); - assert!(result.is_err()); - assert_eq!( - result.unwrap_err(), - FalkorDBError::ParsingHeader("Expected at least one item in header vector".to_string()) - ); - } - - #[test] - fn test_parse_header_many_elements() { - let header = FalkorValue::Array(vec![FalkorValue::Array(vec![ - FalkorValue::String("just_some_header".to_string()), - FalkorValue::String("header1".to_string()), - FalkorValue::String("extra".to_string()), - ])]); - let result = parse_header(header); - assert!(result.is_ok()); - assert_eq!(result.unwrap()[0], "just_some_header"); - } -} diff --git a/src/redis_ext.rs b/src/redis_ext.rs deleted file mode 100644 index 88feba0..0000000 --- a/src/redis_ext.rs +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{ConfigValue, FalkorDBError, FalkorValue}; -use redis::{FromRedisValue, RedisResult, RedisWrite, ToRedisArgs}; - -impl ToRedisArgs for ConfigValue { - fn write_redis_args( - &self, - out: &mut W, - ) where - W: ?Sized + RedisWrite, - { - match self { - ConfigValue::String(str_val) => str_val.write_redis_args(out), - ConfigValue::Int64(int_val) => int_val.write_redis_args(out), - } - } -} - -impl TryFrom<&redis::Value> for ConfigValue { - type Error = FalkorDBError; - fn try_from(value: &redis::Value) -> Result { - Ok(match value { - redis::Value::Int(int_val) => ConfigValue::Int64(*int_val), - redis::Value::Data(str_data) => { - ConfigValue::String(String::from_utf8_lossy(str_data.as_slice()).to_string()) - } - _ => return Err(FalkorDBError::InvalidDataReceived), - }) - } -} - -impl FromRedisValue for FalkorValue { - fn from_redis_value(v: &redis::Value) -> RedisResult { - Ok(match v { - redis::Value::Nil => FalkorValue::None, - redis::Value::Int(int_val) => FalkorValue::I64(*int_val), - redis::Value::Data(str_val) => { - FalkorValue::String(String::from_utf8_lossy(str_val.as_slice()).to_string()) - } - redis::Value::Bulk(bulk) => FalkorValue::Array( - bulk.iter() - .flat_map(FalkorValue::from_redis_value) - .collect(), - ), - redis::Value::Status(status) => FalkorValue::String(status.to_string()), - redis::Value::Okay => FalkorValue::None, - }) - } -} diff --git a/src/response/constraint.rs b/src/response/constraint.rs index 8d1aadf..da03a05 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -4,8 +4,11 @@ */ use crate::{ - value::utils::parse_type, EntityType, FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, - GraphSchema, + parser::{ + parse_falkor_enum, parse_raw_redis_value, redis_value_as_typed_string_vec, + redis_value_as_vec, + }, + EntityType, FalkorDBError, FalkorResult, FalkorValue, GraphSchema, }; /// The type of restriction to apply for the property @@ -47,26 +50,26 @@ pub struct Constraint { pub status: ConstraintStatus, } -impl FalkorParsable for Constraint { - fn from_falkor_value( - value: FalkorValue, +impl Constraint { + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Constraint", skip_all, level = "info") + )] + pub(crate) fn parse( + value: redis::Value, graph_schema: &mut GraphSchema, ) -> FalkorResult { - let value_vec = parse_type(6, value, graph_schema)?.into_vec()?; + let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [redis::Value; 5] = redis_value_as_vec(value) + .and_then(|res| res.try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount("Expected exactly 5 elements in constraint object")))?; - let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = value_vec.try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount("Expected exactly 5 elements in constraint object".to_string()))?; - - Ok(Constraint { - constraint_type: constraint_type_raw.into_string()?.as_str().try_into()?, - label: label_raw.into_string()?, - properties: properties_raw - .into_vec()? - .into_iter() - .flat_map(FalkorValue::into_string) - .collect(), - entity_type: entity_type_raw.into_string()?.as_str().try_into()?, - status: status_raw.into_string()?.as_str().try_into()?, + Ok(Self { + constraint_type: parse_falkor_enum(constraint_type_raw)?, + label: parse_raw_redis_value(label_raw, graph_schema) + .and_then(FalkorValue::into_string)?, + properties: redis_value_as_typed_string_vec(properties_raw)?, + entity_type: parse_falkor_enum(entity_type_raw)?, + status: parse_falkor_enum(status_raw)?, }) } } diff --git a/src/response/execution_plan.rs b/src/response/execution_plan.rs index d410f78..44b4fa6 100644 --- a/src/response/execution_plan.rs +++ b/src/response/execution_plan.rs @@ -3,13 +3,18 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{FalkorDBError, FalkorResult, FalkorValue}; +use crate::{ + parser::{redis_value_as_string, redis_value_as_vec}, + FalkorDBError, FalkorResult, +}; use regex::Regex; -use std::cell::RefCell; -use std::cmp::Ordering; -use std::collections::{HashMap, VecDeque}; -use std::ops::Not; -use std::rc::Rc; +use std::{ + cell::RefCell, + cmp::Ordering, + collections::{HashMap, VecDeque}, + ops::Not, + rc::Rc, +}; #[derive(Debug)] struct IntermediateOperation { @@ -22,6 +27,10 @@ struct IntermediateOperation { } impl IntermediateOperation { + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Create New Operation", skip_all, level = "trace") + )] fn new( depth: usize, operation_string: &str, @@ -116,6 +125,10 @@ impl ExecutionPlan { self.string_representation.as_str() } + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Create Node", skip_all, level = "debug") + )] fn create_node( depth: usize, operation_string: &str, @@ -130,6 +143,10 @@ impl ExecutionPlan { Ok(()) } + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Finalize Operation", skip_all, level = "debug") + )] fn finalize_operation( current_refcell: Rc> ) -> FalkorResult> { @@ -154,6 +171,10 @@ impl ExecutionPlan { })) } + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Operation Tree To Map", skip_all, level = "trace") + )] fn operations_map_from_tree( current_branch: &Rc, map: &mut HashMap>>, @@ -166,34 +187,28 @@ impl ExecutionPlan { Self::operations_map_from_tree(child, map); } } -} - -impl TryFrom for ExecutionPlan { - type Error = FalkorDBError; - fn try_from(value: FalkorValue) -> Result { - let execution_plan_operations: Vec<_> = value - .into_vec()? - .into_iter() - .flat_map(FalkorValue::into_string) - .collect(); - - let string_representation = ["".to_string()] - .into_iter() - .chain(execution_plan_operations.iter().cloned()) - .collect::>() - .join("\n"); + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Execution Plan", skip_all, level = "info") + )] + pub(crate) fn parse(value: redis::Value) -> FalkorResult { + let redis_value_vec = redis_value_as_vec(value)?; + let mut string_representation = Vec::with_capacity(redis_value_vec.len() + 1); let mut current_traversal_stack = vec![]; - for node in execution_plan_operations.iter().map(String::as_str) { - let depth = node.matches(" ").count(); - let node = node.trim(); + for node in redis_value_vec { + let node_string = redis_value_as_string(node)?; + + let depth = node_string.matches(" ").count(); + let node = node_string.trim(); let current_node = match current_traversal_stack.last().cloned() { None => { current_traversal_stack.push(Rc::new(RefCell::new( IntermediateOperation::new(depth, node)?, ))); + string_representation.push(node_string); continue; } Some(current_node) => current_node, @@ -231,6 +246,8 @@ impl TryFrom for ExecutionPlan { current_node.borrow_mut().children.push(new_node); } } + + string_representation.push(node_string); } // Must drop traversal stack first @@ -244,8 +261,8 @@ impl TryFrom for ExecutionPlan { Self::operations_map_from_tree(&operation_tree, &mut operations); Ok(ExecutionPlan { - string_representation, - plan: execution_plan_operations, + string_representation: format!("\n{}", string_representation.join("\n")), + plan: string_representation, operations, operation_tree, }) diff --git a/src/response/index.rs b/src/response/index.rs index ea391dd..ffdb7f8 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -4,8 +4,11 @@ */ use crate::{ - value::utils::{parse_type, parse_vec, type_val_from_value}, - EntityType, FalkorDBError, FalkorParsable, FalkorValue, GraphSchema, + parser::{ + parse_falkor_enum, parse_raw_redis_value, redis_value_as_typed_string_vec, + redis_value_as_vec, + }, + EntityType, FalkorDBError, FalkorValue, GraphSchema, }; use std::collections::HashMap; @@ -33,21 +36,32 @@ pub enum IndexType { Fulltext, } -fn parse_types_map(value: FalkorValue) -> Result>, FalkorDBError> { - let value: HashMap = value.try_into()?; - - let mut out_map = HashMap::with_capacity(value.len()); - for (key, val) in value.into_iter() { - let val = val.into_vec()?; - let mut field_types = Vec::with_capacity(val.len()); - for field_type in val { - field_types.push(field_type.into_string()?.as_str().try_into()?); - } - - out_map.insert(key, field_types); - } - - Ok(out_map) +fn parse_types_map( + value: redis::Value, + graph_schema: &mut GraphSchema, +) -> Result>, FalkorDBError> { + parse_raw_redis_value(value, graph_schema) + .and_then(|map_val| map_val.into_map()) + .map(|map_val| { + map_val + .into_iter() + .flat_map(|(key, val)| { + val.into_vec().map(|as_vec| { + ( + key, + as_vec + .into_iter() + .flat_map(|item| { + item.into_string().and_then(|as_str| { + IndexType::try_from(as_str.as_str()).map_err(Into::into) + }) + }) + .collect(), + ) + }) + }) + .collect() + }) } /// Contains all the info regarding an index on the database @@ -68,40 +82,45 @@ pub struct FalkorIndex { /// Words to avoid indexing as they are very common and will just be a waste of resources pub stopwords: Vec, /// Various other information for querying by the user - pub info: HashMap, + pub info: HashMap, } impl FalkorIndex { - fn from_raw_values(items: Vec) -> Result { - let [label, fields, field_types, language, stopwords, entity_type, status, info]: [FalkorValue; 8] = items.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount("Expected exactly 8 elements in index object".to_string()))?; - - Ok(Self { - entity_type: entity_type.into_string()?.as_str().try_into()?, - status: status.into_string()?.as_str().try_into()?, - index_label: label.try_into()?, - fields: parse_vec(fields)?, - field_types: parse_types_map(field_types)?, - language: language.try_into()?, - stopwords: parse_vec(stopwords)?, - info: info.try_into()?, - }) - } -} - -impl FalkorParsable for FalkorIndex { - fn from_falkor_value( - value: FalkorValue, + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Index", skip_all, level = "info") + )] + pub(crate) fn parse( + value: redis::Value, graph_schema: &mut GraphSchema, ) -> Result { - let semi_parsed_items = value - .into_vec()? - .into_iter() - .flat_map(|item| { - let (type_marker, val) = type_val_from_value(item)?; - parse_type(type_marker, val, graph_schema) - }) - .collect::>(); + let [label, fields, field_types, language, stopwords, entity_type, status, info] = + redis_value_as_vec(value).and_then(|as_vec| { + as_vec.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 8 elements in index object", + ) + }) + })?; - Self::from_raw_values(semi_parsed_items) + Ok(Self { + entity_type: parse_falkor_enum(entity_type)?, + status: parse_falkor_enum(status)?, + index_label: parse_raw_redis_value(label, graph_schema) + .and_then(FalkorValue::into_string)?, + fields: redis_value_as_typed_string_vec(fields)?, + field_types: parse_types_map(field_types, graph_schema)?, + language: parse_raw_redis_value(language, graph_schema) + .and_then(FalkorValue::into_string)?, + stopwords: redis_value_as_typed_string_vec(stopwords)?, + info: parse_raw_redis_value(info, graph_schema) + .and_then(FalkorValue::into_map) + .map(|as_map| { + as_map + .into_iter() + .flat_map(|(key, val)| val.into_string().map(|val_str| (key, val_str))) + .collect() + })?, + }) } } diff --git a/src/response/lazy_result_set.rs b/src/response/lazy_result_set.rs index 6f13667..b1e5e73 100644 --- a/src/response/lazy_result_set.rs +++ b/src/response/lazy_result_set.rs @@ -3,19 +3,19 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{value::utils::parse_type, FalkorValue, GraphSchema}; +use crate::{parser::parse_raw_redis_value, FalkorValue, GraphSchema}; use std::collections::VecDeque; /// A wrapper around the returned raw data, allowing parsing on demand of each result /// This implements Iterator, so can simply be collect()'ed into any desired container pub struct LazyResultSet<'a> { - data: VecDeque, + data: VecDeque, graph_schema: &'a mut GraphSchema, } impl<'a> LazyResultSet<'a> { pub(crate) fn new( - data: Vec, + data: Vec, graph_schema: &'a mut GraphSchema, ) -> Self { Self { @@ -38,10 +38,14 @@ impl<'a> LazyResultSet<'a> { impl<'a> Iterator for LazyResultSet<'a> { type Item = Vec; + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Next Result", skip_all) + )] fn next(&mut self) -> Option { self.data.pop_front().map(|current_result| { - parse_type(6, current_result, self.graph_schema) - .and_then(|parsed_result| parsed_result.into_vec()) + parse_raw_redis_value(current_result, self.graph_schema) + .and_then(FalkorValue::into_vec) .unwrap_or(vec![FalkorValue::Unparseable]) }) } diff --git a/src/response/mod.rs b/src/response/mod.rs index 031a2f3..5a29f16 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -4,8 +4,8 @@ */ use crate::{ - parser::utils::{parse_header, string_vec_from_val}, - FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, + parser::{parse_header, redis_value_as_untyped_string_vec}, + FalkorResult, }; pub(crate) mod constraint; @@ -29,13 +29,17 @@ impl FalkorResponse { /// Creates a [`FalkorResponse`] from the specified data, and raw stats, where raw headers are optional /// /// # Arguments - /// * `headers`: a [`FalkorValue`] that is expected to be of variant [`FalkorValue::Array`], where each element is expected to be of variant [`FalkorValue::String`] + /// * `headers`: a [`redis::Value`] that is expected to be of variant [`redis::Value::Bulk`], where each element is expected to be of variant [`redis::Value::Data`] or [`redis::Value::Status`] /// * `data`: The actual data - /// * `stats`: a [`FalkorValue`] that is expected to be of variant [`FalkorValue::Array`], where each element is expected to be of variant [`FalkorValue::String`] + /// * `stats`: a [`redis::Value`] that is expected to be of variant [`redis::Value::Bulk`], where each element is expected to be of variant [`redis::Value::Data`] or [`redis::Value::Status`] + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "New Falkor Response", skip_all, level = "trace") + )] pub fn from_response( - headers: Option, + headers: Option, data: T, - stats: FalkorValue, + stats: redis::Value, ) -> FalkorResult { Ok(Self { header: match headers { @@ -43,31 +47,7 @@ impl FalkorResponse { None => vec![], }, data, - stats: string_vec_from_val(stats)?, + stats: redis_value_as_untyped_string_vec(stats)?, }) } } - -impl FalkorParsable for FalkorResponse> { - fn from_falkor_value( - value: FalkorValue, - graph_schema: &mut GraphSchema, - ) -> FalkorResult { - let [header, indices, stats]: [FalkorValue; 3] = - value.into_vec()?.try_into().map_err(|_| { - FalkorDBError::ParsingArrayToStructElementCount( - "Expected exactly 3 elements in query response".to_string(), - ) - })?; - - FalkorResponse::from_response( - Some(header), - indices - .into_vec()? - .into_iter() - .flat_map(|index| FalkorParsable::from_falkor_value(index, graph_schema)) - .collect(), - stats, - ) - } -} diff --git a/src/response/slowlog_entry.rs b/src/response/slowlog_entry.rs index f425e2f..96a302d 100644 --- a/src/response/slowlog_entry.rs +++ b/src/response/slowlog_entry.rs @@ -3,7 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{FalkorDBError, FalkorValue}; +use crate::{ + parser::{redis_value_as_double, redis_value_as_string, redis_value_as_vec}, + FalkorDBError, FalkorResult, +}; /// A slowlog entry, representing one of the N slowest queries in the current log #[derive(Clone, Debug, PartialEq)] @@ -18,28 +21,29 @@ pub struct SlowlogEntry { pub time_taken: f64, } -impl TryFrom for SlowlogEntry { - type Error = FalkorDBError; - - fn try_from(value: FalkorValue) -> Result { - let [timestamp, command, arguments, time_taken] = - value.into_vec()?.try_into().map_err(|_| { - FalkorDBError::ParsingArrayToStructElementCount( - "Expected exactly 4 elements of slowlog entry".to_string(), - ) +impl SlowlogEntry { + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Slowlog Entry", skip_all, level = "info") + )] + pub(crate) fn parse(value: redis::Value) -> FalkorResult { + let [timestamp, command, arguments, time_taken]: [redis::Value; 4] = + redis_value_as_vec(value).and_then(|as_vec| { + as_vec.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 4 elements of slowlog entry", + ) + }) })?; Ok(Self { - timestamp: timestamp - .into_string()? - .parse() - .map_err(|_| FalkorDBError::ParsingI64)?, - command: command.into_string()?, - arguments: arguments.into_string()?, - time_taken: time_taken - .into_string()? - .parse() - .map_err(|_| FalkorDBError::ParsingF64)?, + timestamp: redis_value_as_string(timestamp) + .ok() + .and_then(|timestamp| timestamp.parse().ok()) + .ok_or(FalkorDBError::ParsingI64)?, + command: redis_value_as_string(command)?, + arguments: redis_value_as_string(arguments)?, + time_taken: redis_value_as_double(time_taken)?, }) } } diff --git a/src/value/config.rs b/src/value/config.rs index 80baeac..2a3729f 100644 --- a/src/value/config.rs +++ b/src/value/config.rs @@ -4,6 +4,7 @@ */ use crate::{FalkorDBError, FalkorValue}; +use redis::{RedisWrite, ToRedisArgs}; use std::fmt::{Display, Formatter}; /// An enum representing the two viable types for a config value @@ -66,3 +67,44 @@ impl TryFrom for ConfigValue { } } } + +impl ToRedisArgs for ConfigValue { + fn write_redis_args( + &self, + out: &mut W, + ) where + W: ?Sized + RedisWrite, + { + match self { + ConfigValue::String(str_val) => str_val.write_redis_args(out), + ConfigValue::Int64(int_val) => int_val.write_redis_args(out), + } + } +} + +impl TryFrom<&redis::Value> for ConfigValue { + type Error = FalkorDBError; + fn try_from(value: &redis::Value) -> Result { + Ok(match value { + redis::Value::Int(int_val) => ConfigValue::Int64(*int_val), + redis::Value::Data(str_data) => { + ConfigValue::String(String::from_utf8_lossy(str_data.as_slice()).to_string()) + } + _ => return Err(FalkorDBError::InvalidDataReceived), + }) + } +} + +impl TryFrom for ConfigValue { + type Error = FalkorDBError; + + fn try_from(value: redis::Value) -> Result { + Ok(match value { + redis::Value::Int(int_val) => ConfigValue::Int64(int_val), + redis::Value::Data(str_data) => ConfigValue::String( + String::from_utf8(str_data).map_err(|_| FalkorDBError::ParsingString)?, + ), + _ => return Err(FalkorDBError::InvalidDataReceived), + }) + } +} diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index ddd742f..cdb353c 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -3,8 +3,11 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, SchemaType}; -use std::collections::{HashMap, HashSet}; +use crate::{ + parser::{redis_value_as_int, redis_value_as_vec}, + FalkorDBError, FalkorResult, FalkorValue, GraphSchema, SchemaType, +}; +use std::collections::HashMap; /// Whether this element is a node or edge in the graph #[derive(Copy, Clone, Debug, Eq, PartialEq, strum::EnumString, strum::Display)] @@ -28,30 +31,27 @@ pub struct Node { pub properties: HashMap, } -impl FalkorParsable for Node { - fn from_falkor_value( - value: FalkorValue, +impl Node { + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Node", skip_all, level = "debug") + )] + pub(crate) fn parse( + value: redis::Value, graph_schema: &mut GraphSchema, ) -> FalkorResult { - let [entity_id, labels, properties]: [FalkorValue; 3] = - value.into_vec()?.try_into().map_err(|_| { - FalkorDBError::ParsingArrayToStructElementCount( - "Expected exactly 3 elements in node object".to_string(), - ) + let [entity_id, labels, properties]: [redis::Value; 3] = redis_value_as_vec(value) + .and_then(|val_vec| { + TryInto::try_into(val_vec).map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 3 elements in node object", + ) + }) })?; - let labels = labels.into_vec()?; - let mut ids_hashset = HashSet::with_capacity(labels.len()); - for label in labels.iter() { - ids_hashset.insert( - label - .to_i64() - .ok_or(FalkorDBError::ParsingCompactIdUnknown)?, - ); - } Ok(Node { - entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - labels: graph_schema.parse_id_vec(labels, SchemaType::Labels)?, + entity_id: redis_value_as_int(entity_id)?, + labels: graph_schema.parse_id_vec(redis_value_as_vec(labels)?, SchemaType::Labels)?, properties: graph_schema.parse_properties_map(properties)?, }) } @@ -72,29 +72,34 @@ pub struct Edge { pub properties: HashMap, } -impl FalkorParsable for Edge { - fn from_falkor_value( - value: FalkorValue, +impl Edge { + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Edge", skip_all, level = "debug") + )] + pub(crate) fn parse( + value: redis::Value, graph_schema: &mut GraphSchema, ) -> FalkorResult { - let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = - value.into_vec()?.try_into().map_err(|_| { + let [entity_id, relationship_id_raw, src_node_id, dst_node_id, properties]: [redis::Value; + 5] = redis_value_as_vec(value).and_then(|val_vec| { + val_vec.try_into().map_err(|_| { FalkorDBError::ParsingArrayToStructElementCount( - "Expected exactly 5 elements in edge object".to_string(), + "Expected exactly 5 elements in edge object", ) - })?; + }) + })?; - let relation = relations.to_i64().ok_or(FalkorDBError::ParsingI64)?; let relationship = graph_schema .relationships() - .get(&relation) + .get(&redis_value_as_int(relationship_id_raw)?) .ok_or(FalkorDBError::MissingSchemaId(SchemaType::Relationships))?; Ok(Edge { - entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + entity_id: redis_value_as_int(entity_id)?, relationship_type: relationship.to_string(), - src_node_id: src_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - dst_node_id: dst_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + src_node_id: redis_value_as_int(src_node_id)?, + dst_node_id: redis_value_as_int(dst_node_id)?, properties: graph_schema.parse_properties_map(properties)?, }) } diff --git a/src/value/map.rs b/src/value/map.rs deleted file mode 100644 index e6fd140..0000000 --- a/src/value/map.rs +++ /dev/null @@ -1,161 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{ - value::utils::parse_type, FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, -}; -use std::collections::HashMap; - -impl FalkorParsable for HashMap { - fn from_falkor_value( - value: FalkorValue, - graph_schema: &mut GraphSchema, - ) -> FalkorResult { - let val_vec = value.into_vec()?; - if val_vec.len() % 2 != 0 { - Err(FalkorDBError::ParsingFMap( - "Map should have an even amount of elements".to_string(), - ))?; - } - - let mut out_map = HashMap::with_capacity(val_vec.len()); - for pair in val_vec.chunks_exact(2) { - let [key, val]: [FalkorValue; 2] = pair.to_vec().try_into().map_err(|_| { - FalkorDBError::ParsingFMap( - "The vec returned from using chunks_exact(2) should be comprised of 2 elements" - .to_string(), - ) - })?; - - let [type_marker, val]: [FalkorValue; 2] = - val.into_vec()?.try_into().map_err(|_| { - FalkorDBError::ParsingFMap( - "The value in a map should be comprised of a type marker and value" - .to_string(), - ) - })?; - - out_map.insert( - key.into_string()?, - parse_type( - type_marker.to_i64().ok_or(FalkorDBError::ParsingKTVTypes)?, - val, - graph_schema, - )?, - ); - } - - Ok(out_map) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{client::blocking::create_empty_inner_client, GraphSchema}; - - #[test] - fn test_not_a_vec() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); - - let res: FalkorResult> = FalkorParsable::from_falkor_value( - FalkorValue::String("Hello".to_string()), - &mut graph_schema, - ); - - assert!(res.is_err()) - } - - #[test] - fn test_vec_odd_element_count() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); - - let res: FalkorResult> = FalkorParsable::from_falkor_value( - FalkorValue::Array(vec![FalkorValue::None; 7]), - &mut graph_schema, - ); - - assert!(res.is_err()) - } - - #[test] - fn test_val_element_is_not_array() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); - - let res: FalkorResult> = FalkorParsable::from_falkor_value( - FalkorValue::Array(vec![ - FalkorValue::String("Key".to_string()), - FalkorValue::Bool(false), - ]), - &mut graph_schema, - ); - - assert!(res.is_err()) - } - - #[test] - fn test_val_element_has_only_1_element() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); - - let res: FalkorResult> = FalkorParsable::from_falkor_value( - FalkorValue::Array(vec![ - FalkorValue::String("Key".to_string()), - FalkorValue::Array(vec![FalkorValue::I64(7)]), - ]), - &mut graph_schema, - ); - - assert!(res.is_err()) - } - - #[test] - fn test_val_element_has_ge_2_elements() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); - - let res: FalkorResult> = FalkorParsable::from_falkor_value( - FalkorValue::Array(vec![ - FalkorValue::String("Key".to_string()), - FalkorValue::Array(vec![FalkorValue::I64(3); 3]), - ]), - &mut graph_schema, - ); - - assert!(res.is_err()) - } - - #[test] - fn test_val_element_mismatch_type_marker() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); - - let res: FalkorResult> = FalkorParsable::from_falkor_value( - FalkorValue::Array(vec![ - FalkorValue::String("Key".to_string()), - FalkorValue::Array(vec![FalkorValue::I64(3), FalkorValue::Bool(true)]), - ]), - &mut graph_schema, - ); - - assert!(res.is_err()) - } - - #[test] - fn test_ok_values() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); - - let res: HashMap = FalkorParsable::from_falkor_value( - FalkorValue::Array(vec![ - FalkorValue::String("IntKey".to_string()), - FalkorValue::Array(vec![FalkorValue::I64(3), FalkorValue::I64(1)]), - FalkorValue::String("BoolKey".to_string()), - FalkorValue::Array(vec![FalkorValue::I64(4), FalkorValue::Bool(true)]), - ]), - &mut graph_schema, - ) - .expect("Could not parse map"); - - assert_eq!(res.get("IntKey"), Some(FalkorValue::I64(1)).as_ref()); - assert_eq!(res.get("BoolKey"), Some(FalkorValue::Bool(true)).as_ref()); - } -} diff --git a/src/value/mod.rs b/src/value/mod.rs index 8527b4f..84c9ea3 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -3,7 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{FalkorDBError, FalkorParsable, FalkorResult, GraphSchema}; +use crate::{FalkorDBError, FalkorResult}; use graph_entities::{Edge, Node}; use path::Path; use point::Point; @@ -11,10 +11,8 @@ use std::{collections::HashMap, fmt::Debug}; pub(crate) mod config; pub(crate) mod graph_entities; -pub(crate) mod map; pub(crate) mod path; pub(crate) mod point; -pub(crate) mod utils; /// An enum of all the supported Falkor types #[derive(Clone, Debug, PartialEq)] @@ -74,113 +72,8 @@ impl From<&str> for FalkorValue { } } -impl From> for FalkorValue -where - FalkorValue: From, -{ - fn from(value: Vec) -> Self { - Self::Array( - value - .into_iter() - .map(|element| FalkorValue::from(element)) - .collect(), - ) - } -} - -impl TryFrom for Vec { - type Error = FalkorDBError; - - fn try_from(value: FalkorValue) -> FalkorResult { - match value { - FalkorValue::Array(val) => Ok(val), - _ => Err(FalkorDBError::ParsingFArray), - } - } -} - -impl TryFrom for f64 { - type Error = FalkorDBError; - - fn try_from(value: FalkorValue) -> FalkorResult { - match value { - FalkorValue::String(f64_str) => f64_str.parse().map_err(|_| FalkorDBError::ParsingF64), - FalkorValue::F64(f64_val) => Ok(f64_val), - _ => Err(FalkorDBError::ParsingF64), - } - } -} - -impl TryFrom for String { - type Error = FalkorDBError; - - fn try_from(value: FalkorValue) -> FalkorResult { - match value { - FalkorValue::String(val) => Ok(val), - _ => Err(FalkorDBError::ParsingFString), - } - } -} - -impl TryFrom for Edge { - type Error = FalkorDBError; - - fn try_from(value: FalkorValue) -> FalkorResult { - match value { - FalkorValue::Edge(edge) => Ok(edge), - _ => Err(FalkorDBError::ParsingFEdge), - } - } -} - -impl TryFrom for Node { - type Error = FalkorDBError; - - fn try_from(value: FalkorValue) -> FalkorResult { - match value { - FalkorValue::Node(node) => Ok(node), - _ => Err(FalkorDBError::ParsingFNode), - } - } -} - -impl TryFrom for Path { - type Error = FalkorDBError; - - fn try_from(value: FalkorValue) -> FalkorResult { - match value { - FalkorValue::Path(path) => Ok(path), - _ => Err(FalkorDBError::ParsingFPath), - } - } -} - -impl TryFrom for HashMap { - type Error = FalkorDBError; - - fn try_from(value: FalkorValue) -> FalkorResult { - match value { - FalkorValue::Map(map) => Ok(map), - _ => Err(FalkorDBError::ParsingFMap( - "Attempting to get a non-map element as a map".to_string(), - )), - } - } -} - -impl TryFrom for Point { - type Error = FalkorDBError; - - fn try_from(value: FalkorValue) -> FalkorResult { - match value { - FalkorValue::Point(point) => Ok(point), - _ => Err(FalkorDBError::ParsingFPoint), - } - } -} - impl FalkorValue { - /// Returns a reference to the internal [`Vec`] if this is an FArray variant. + /// Returns a reference to the internal [`Vec`] if this is an Array variant. /// /// # Returns /// A reference to the internal [`Vec`] @@ -191,7 +84,7 @@ impl FalkorValue { } } - /// Returns a reference to the internal [`String`] if this is an FString variant. + /// Returns a reference to the internal [`String`] if this is an String variant. /// /// # Returns /// A reference to the internal [`String`] @@ -224,7 +117,7 @@ impl FalkorValue { } } - /// Returns a reference to the internal [`Path`] if this is an FPath variant. + /// Returns a reference to the internal [`Path`] if this is an Path variant. /// /// # Returns /// A reference to the internal [`Path`] @@ -235,7 +128,7 @@ impl FalkorValue { } } - /// Returns a reference to the internal [`HashMap`] if this is an FMap variant. + /// Returns a reference to the internal [`HashMap`] if this is an Map variant. /// /// # Returns /// A reference to the internal [`HashMap`] @@ -295,29 +188,36 @@ impl FalkorValue { } } - /// Consumes itself and returns the inner [`Vec`] if this is an FArray variant + /// Consumes itself and returns the inner [`Vec`] if this is an Array variant /// /// # Returns /// The inner [`Vec`] pub fn into_vec(self) -> FalkorResult> { - self.try_into() + match self { + FalkorValue::Array(array) => Ok(array), + _ => Err(FalkorDBError::ParsingArray), + } } - /// Consumes itself and returns the inner [`String`] if this is an FString variant + /// Consumes itself and returns the inner [`String`] if this is an String variant /// /// # Returns /// The inner [`String`] pub fn into_string(self) -> FalkorResult { - self.try_into() + match self { + FalkorValue::String(string) => Ok(string), + _ => Err(FalkorDBError::ParsingString), + } } -} - -impl FalkorParsable for FalkorValue { - fn from_falkor_value( - value: FalkorValue, - _: &mut GraphSchema, - ) -> FalkorResult { - Ok(value) + /// Consumes itself and returns the inner [`HashMap`] if this is a Map variant + /// + /// # Returns + /// The inner [`HashMap`] + pub fn into_map(self) -> FalkorResult> { + match self { + FalkorValue::Map(map) => Ok(map), + _ => Err(FalkorDBError::ParsingMap), + } } } diff --git a/src/value/path.rs b/src/value/path.rs index 6f499fe..627176e 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -3,7 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{Edge, FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, Node}; +use crate::{parser::redis_value_as_vec, Edge, FalkorDBError, FalkorResult, GraphSchema, Node}; /// Represents a path between two nodes, contains all the nodes, and the relationships between them along the path #[derive(Clone, Debug, Default, PartialEq)] @@ -14,28 +14,32 @@ pub struct Path { pub relationships: Vec, } -impl FalkorParsable for Path { - fn from_falkor_value( - value: FalkorValue, +impl Path { + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Path", skip_all, level = "debug") + )] + pub(crate) fn parse( + value: redis::Value, graph_schema: &mut GraphSchema, ) -> FalkorResult { - let [nodes, relationships]: [FalkorValue; 2] = - value.into_vec()?.try_into().map_err(|_| { - FalkorDBError::ParsingArrayToStructElementCount( - "Expected exactly 2 elements for path".to_string(), - ) + let [nodes, relationships]: [redis::Value; 2] = + redis_value_as_vec(value).and_then(|vec_val| { + vec_val.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 2 elements for path", + ) + }) })?; Ok(Self { - nodes: nodes - .into_vec()? + nodes: redis_value_as_vec(nodes)? .into_iter() - .flat_map(|node| Node::from_falkor_value(node, graph_schema)) + .flat_map(|node| Node::parse(node, graph_schema)) .collect(), - relationships: relationships - .into_vec()? + relationships: redis_value_as_vec(relationships)? .into_iter() - .flat_map(|edge| Edge::from_falkor_value(edge, graph_schema)) + .flat_map(|edge| Edge::parse(edge, graph_schema)) .collect(), }) } diff --git a/src/value/point.rs b/src/value/point.rs index afa1a54..f186717 100644 --- a/src/value/point.rs +++ b/src/value/point.rs @@ -3,7 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{FalkorDBError, FalkorResult, FalkorValue}; +use crate::{ + parser::{redis_value_as_double, redis_value_as_vec}, + FalkorDBError, FalkorResult, +}; /// A point in the world. #[derive(Clone, Debug, Default, PartialEq)] @@ -15,7 +18,7 @@ pub struct Point { } impl Point { - /// Parses a point from a FalkorValue::Array, + /// Parses a point from a redis::Value::Bulk, /// taking the first element as an f64 latitude, and second element as an f64 longitude /// /// # Arguments @@ -23,16 +26,22 @@ impl Point { /// /// # Returns /// Self, if successful - pub fn parse(value: FalkorValue) -> FalkorResult { - let [lat, long]: [FalkorValue; 2] = value.into_vec()?.try_into().map_err(|_| { - FalkorDBError::ParsingArrayToStructElementCount( - "Expected exactly 2 element in point - latitude and longitude".to_string(), - ) + #[cfg_attr( + feature = "tracing", + tracing::instrument(name = "Parse Point", skip_all, level = "trace") + )] + pub fn parse(value: redis::Value) -> FalkorResult { + let [lat, long]: [redis::Value; 2] = redis_value_as_vec(value).and_then(|val_vec| { + val_vec.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 2 element in point - latitude and longitude", + ) + }) })?; Ok(Point { - latitude: lat.to_f64().ok_or(FalkorDBError::ParsingF64)?, - longitude: long.to_f64().ok_or(FalkorDBError::ParsingF64)?, + latitude: redis_value_as_double(lat)?, + longitude: redis_value_as_double(long)?, }) } } @@ -43,7 +52,10 @@ mod tests { #[test] fn test_parse_valid_point() { - let value = FalkorValue::Array(vec![FalkorValue::F64(45.0), FalkorValue::F64(90.0)]); + let value = redis::Value::Bulk(vec![ + redis::Value::Status("45.0".to_string()), + redis::Value::Status("90.0".to_string()), + ]); let result = Point::parse(value); assert!(result.is_ok()); let point = result.unwrap(); @@ -53,7 +65,7 @@ mod tests { #[test] fn test_parse_invalid_point_missing_elements() { - let value = FalkorValue::Array(vec![FalkorValue::F64(45.0)]); + let value = redis::Value::Bulk(vec![redis::Value::Status("45.0".to_string())]); let result = Point::parse(value); assert!(result.is_err()); match result { @@ -69,10 +81,10 @@ mod tests { #[test] fn test_parse_invalid_point_extra_elements() { - let value = FalkorValue::Array(vec![ - FalkorValue::F64(45.0), - FalkorValue::F64(90.0), - FalkorValue::F64(30.0), + let value = redis::Value::Bulk(vec![ + redis::Value::Status("45.0".to_string()), + redis::Value::Status("90.0".to_string()), + redis::Value::Status("30.0".to_string()), ]); let result = Point::parse(value); assert!(result.is_err()); @@ -87,23 +99,9 @@ mod tests { } } - #[test] - fn test_parse_invalid_point_non_f64_elements() { - let value = FalkorValue::Array(vec![ - FalkorValue::String("45.0".to_string()), - FalkorValue::String("90.0".to_string()), - ]); - let result = Point::parse(value); - assert!(result.is_err()); - match result { - Err(FalkorDBError::ParsingF64) => {} - _ => panic!("Expected ParsingF64 error"), - } - } - #[test] fn test_parse_invalid_point_not_an_array() { - let value = FalkorValue::String("not an array".to_string()); + let value = redis::Value::Status("not an array".to_string()); let result = Point::parse(value); assert!(result.is_err()); // Check for the specific error type if needed diff --git a/src/value/utils.rs b/src/value/utils.rs deleted file mode 100644 index cefd39d..0000000 --- a/src/value/utils.rs +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{FalkorDBError, FalkorParsable, FalkorValue, GraphSchema, Point}; - -pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValue), FalkorDBError> { - let [type_marker, val]: [FalkorValue; 2] = value.into_vec()?.try_into().map_err(|_| { - FalkorDBError::ParsingArrayToStructElementCount( - "Expected exactly 2 elements: type marker, and value".to_string(), - ) - })?; - let type_marker = type_marker.to_i64().ok_or(FalkorDBError::ParsingI64)?; - - Ok((type_marker, val)) -} - -pub(crate) fn parse_type( - type_marker: i64, - val: FalkorValue, - graph_schema: &mut GraphSchema, -) -> Result { - let res = match type_marker { - 1 => FalkorValue::None, - 2 => FalkorValue::String(val.into_string()?), - 3 => FalkorValue::I64(val.to_i64().ok_or(FalkorDBError::ParsingI64)?), - 4 => FalkorValue::Bool(val.to_bool().ok_or(FalkorDBError::ParsingBool)?), - 5 => FalkorValue::F64(val.try_into()?), - 6 => FalkorValue::Array( - val.into_vec()? - .into_iter() - .flat_map(|item| { - type_val_from_value(item) - .and_then(|(type_marker, val)| parse_type(type_marker, val, graph_schema)) - }) - .collect(), - ), - // The following types are sent as an array and require specific parsing functions - 7 => FalkorValue::Edge(FalkorParsable::from_falkor_value(val, graph_schema)?), - 8 => FalkorValue::Node(FalkorParsable::from_falkor_value(val, graph_schema)?), - 9 => FalkorValue::Path(FalkorParsable::from_falkor_value(val, graph_schema)?), - 10 => FalkorValue::Map(FalkorParsable::from_falkor_value(val, graph_schema)?), - 11 => FalkorValue::Point(Point::parse(val)?), - _ => Err(FalkorDBError::ParsingUnknownType)?, - }; - - Ok(res) -} - -pub(crate) fn parse_vec>( - value: FalkorValue -) -> Result, FalkorDBError> { - Ok(value - .into_vec()? - .into_iter() - .flat_map(TryFrom::try_from) - .collect()) -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::graph_schema::tests::open_readonly_graph_with_modified_schema; - - #[test] - fn test_parse_edge() { - let mut graph = open_readonly_graph_with_modified_schema(); - - let res = parse_type( - 7, - FalkorValue::Array(vec![ - FalkorValue::I64(100), // edge id - FalkorValue::I64(0), // edge type - FalkorValue::I64(51), // src node - FalkorValue::I64(52), // dst node - FalkorValue::Array(vec![ - FalkorValue::Array(vec![ - FalkorValue::I64(0), - FalkorValue::I64(3), - FalkorValue::I64(20), - ]), - FalkorValue::Array(vec![ - FalkorValue::I64(1), - FalkorValue::I64(4), - FalkorValue::Bool(false), - ]), - ]), - ]), - &mut graph.graph_schema, - ); - assert!(res.is_ok()); - - let falkor_edge = res.unwrap(); - - let FalkorValue::Edge(edge) = falkor_edge else { - panic!("Was not of type edge") - }; - assert_eq!(edge.entity_id, 100); - assert_eq!(edge.relationship_type, "very".to_string()); - assert_eq!(edge.src_node_id, 51); - assert_eq!(edge.dst_node_id, 52); - - assert_eq!(edge.properties.len(), 2); - assert_eq!(edge.properties.get("age"), Some(&FalkorValue::I64(20))); - assert_eq!( - edge.properties.get("is_boring"), - Some(&FalkorValue::Bool(false)) - ); - } - - #[test] - fn test_parse_node() { - let mut graph = open_readonly_graph_with_modified_schema(); - - let res = parse_type( - 8, - FalkorValue::Array(vec![ - FalkorValue::I64(51), // node id - FalkorValue::Array(vec![FalkorValue::I64(0), FalkorValue::I64(1)]), // node type - FalkorValue::Array(vec![ - FalkorValue::Array(vec![ - FalkorValue::I64(0), - FalkorValue::I64(3), - FalkorValue::I64(15), - ]), - FalkorValue::Array(vec![ - FalkorValue::I64(2), - FalkorValue::I64(2), - FalkorValue::String("the something".to_string()), - ]), - FalkorValue::Array(vec![ - FalkorValue::I64(3), - FalkorValue::I64(5), - FalkorValue::F64(105.5), - ]), - ]), - ]), - &mut graph.graph_schema, - ); - assert!(res.is_ok()); - - let falkor_node = res.unwrap(); - let FalkorValue::Node(node) = falkor_node else { - panic!("Was not of type node") - }; - - assert_eq!(node.entity_id, 51); - assert_eq!(node.labels, vec!["much".to_string(), "actor".to_string()]); - assert_eq!(node.properties.len(), 3); - assert_eq!(node.properties.get("age"), Some(&FalkorValue::I64(15))); - assert_eq!( - node.properties.get("something_else"), - Some(&FalkorValue::String("the something".to_string())) - ); - assert_eq!( - node.properties.get("secs_since_login"), - Some(&FalkorValue::F64(105.5)) - ); - } - - #[test] - fn test_parse_path() { - let mut graph = open_readonly_graph_with_modified_schema(); - - let res = parse_type( - 9, - FalkorValue::Array(vec![ - FalkorValue::Array(vec![ - FalkorValue::Array(vec![ - FalkorValue::I64(51), - FalkorValue::Array(vec![FalkorValue::I64(0)]), - FalkorValue::Array(vec![]), - ]), - FalkorValue::Array(vec![ - FalkorValue::I64(52), - FalkorValue::Array(vec![FalkorValue::I64(0)]), - FalkorValue::Array(vec![]), - ]), - FalkorValue::Array(vec![ - FalkorValue::I64(53), - FalkorValue::Array(vec![FalkorValue::I64(0)]), - FalkorValue::Array(vec![]), - ]), - ]), - FalkorValue::Array(vec![ - FalkorValue::Array(vec![ - FalkorValue::I64(100), - FalkorValue::I64(0), - FalkorValue::I64(51), - FalkorValue::I64(52), - FalkorValue::Array(vec![]), - ]), - FalkorValue::Array(vec![ - FalkorValue::I64(101), - FalkorValue::I64(1), - FalkorValue::I64(52), - FalkorValue::I64(53), - FalkorValue::Array(vec![]), - ]), - ]), - ]), - &mut graph.graph_schema, - ); - assert!(res.is_ok()); - - let falkor_path = res.unwrap(); - let FalkorValue::Path(path) = falkor_path else { - panic!("Is not of type path") - }; - - assert_eq!(path.nodes.len(), 3); - assert_eq!(path.nodes[0].entity_id, 51); - assert_eq!(path.nodes[1].entity_id, 52); - assert_eq!(path.nodes[2].entity_id, 53); - - assert_eq!(path.relationships.len(), 2); - assert_eq!(path.relationships[0].entity_id, 100); - assert_eq!(path.relationships[1].entity_id, 101); - - assert_eq!(path.relationships[0].src_node_id, 51); - assert_eq!(path.relationships[0].dst_node_id, 52); - - assert_eq!(path.relationships[1].src_node_id, 52); - assert_eq!(path.relationships[1].dst_node_id, 53); - } - - #[test] - fn test_parse_map() { - let mut graph = open_readonly_graph_with_modified_schema(); - - let res = parse_type( - 10, - FalkorValue::Array(vec![ - FalkorValue::String("key0".to_string()), - FalkorValue::Array(vec![ - FalkorValue::I64(2), - FalkorValue::String("val0".to_string()), - ]), - FalkorValue::String("key1".to_string()), - FalkorValue::Array(vec![FalkorValue::I64(3), FalkorValue::I64(1)]), - FalkorValue::String("key2".to_string()), - FalkorValue::Array(vec![FalkorValue::I64(4), FalkorValue::Bool(true)]), - ]), - &mut graph.graph_schema, - ); - assert!(res.is_ok()); - - let falkor_map = res.unwrap(); - let FalkorValue::Map(map) = falkor_map else { - panic!("Is not of type map") - }; - - assert_eq!(map.len(), 3); - assert_eq!( - map.get("key0"), - Some(&FalkorValue::String("val0".to_string())) - ); - assert_eq!(map.get("key1"), Some(&FalkorValue::I64(1))); - assert_eq!(map.get("key2"), Some(&FalkorValue::Bool(true))); - } - - #[test] - fn test_parse_point() { - let mut graph = open_readonly_graph_with_modified_schema(); - - let res = parse_type( - 11, - FalkorValue::Array(vec![FalkorValue::F64(102.0), FalkorValue::F64(15.2)]), - &mut graph.graph_schema, - ); - assert!(res.is_ok()); - - let falkor_point = res.unwrap(); - let FalkorValue::Point(point) = falkor_point else { - panic!("Is not of type point") - }; - assert_eq!(point.latitude, 102.0); - assert_eq!(point.longitude, 15.2); - } -}