diff --git a/.gitignore b/.gitignore index a1dcdcc..f93bc29 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ /codecov.json /dump.rdb venv/ +.history +.DS_Store diff --git a/1 b/1 new file mode 100644 index 0000000..733bb96 --- /dev/null +++ b/1 @@ -0,0 +1,6 @@ +Adding #[must_use] attributes to ~50+ methods +Adding # Errors sections to ~30+ functions +Fixing format string inlining in ~20+ places +Updating pub(crate) to pub in ~15+ places +Breaking long doc comments in ~10+ places +Fixing literal formatting in ~5+ places \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index bc281bb..d041f42 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,6 +26,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "approx" version = "0.5.1" @@ -62,6 +71,12 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "bumpalo" +version = "3.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" + [[package]] name = "bytes" version = "1.10.1" @@ -83,6 +98,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link 0.2.0", +] + [[package]] name = "combine" version = "4.6.7" @@ -155,6 +183,7 @@ name = "falkordb" version = "0.1.11" dependencies = [ "approx", + "chrono", "parking_lot", "redis", "regex", @@ -273,6 +302,30 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -396,6 +449,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.174" @@ -693,9 +756,9 @@ dependencies = [ [[package]] name = "redis" -version = "0.32.2" +version = "0.32.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f6fd3fd5bb3a9a48819ae5cbad501968f208c1abd139649e7e2cf5d7f4b40b" +checksum = "7cd3650deebc68526b304898b192fa4102a4ef0b9ada24da096559cb60e0eef8" dependencies = [ "bytes", "cfg-if", @@ -710,7 +773,7 @@ dependencies = [ "rustls", "rustls-native-certs", "ryu", - "socket2", + "socket2 0.6.0", "tokio", "tokio-native-tls", "tokio-rustls", @@ -925,9 +988,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -945,6 +1008,16 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" @@ -953,9 +1026,9 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "strum" -version = "0.27.1" +version = "0.27.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" dependencies = [ "strum_macros", ] @@ -1016,18 +1089,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", @@ -1065,7 +1138,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "socket2", + "socket2 0.5.10", "tokio-macros", "windows-sys 0.52.0", ] @@ -1195,6 +1268,64 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1217,6 +1348,71 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link 0.1.3", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link 0.1.3", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link 0.1.3", +] + [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index b43c7c0..174e3ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "falkordb" version = "0.1.11" -edition = "2021" +edition = "2024" description = "A FalkorDB Rust client" homepage = "https://www.falkordb.com/" readme = "README.md" @@ -16,11 +16,12 @@ all-features = true [lib] [dependencies] +chrono = "0.4.42" parking_lot = { version = "0.12.4", default-features = false, features = ["deadlock_detection"] } -redis = { version = "0.32.2", default-features = false, features = ["sentinel"] } +redis = { version = "0.32.5", default-features = false, features = ["sentinel"] } regex = { version = "1.11.1", default-features = false, features = ["std", "perf", "unicode-bool", "unicode-perl"] } -strum = { version = "0.27.1", default-features = false, features = ["std", "derive"] } -thiserror = "2.0.12" +strum = { version = "0.27.2", default-features = false, features = ["std", "derive"] } +thiserror = "2.0.16" tokio = { version = "1.45.1", default-features = false, features = ["macros", "sync", "rt-multi-thread"], optional = true } tracing = { version = "0.1.41", default-features = false, features = ["std", "attributes"], optional = true } diff --git a/deny.toml b/deny.toml index c542af0..714acf1 100644 --- a/deny.toml +++ b/deny.toml @@ -3,30 +3,35 @@ [bans] multiple-versions = "deny" skip = [ - "wasi", - "getrandom", - "windows-sys", - "core-foundation", - "security-framework", - "windows-targets", - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc" + "windows-link", # Allow multiple versions due to chrono dependency conflicts + "core-foundation", # Allow multiple versions due to security-framework conflicts + "security-framework", # Allow multiple versions due to native-tls/rustls conflicts + "socket2", # Allow multiple versions due to tokio/redis conflicts + "wasi", # Allow multiple versions due to mio/getrandom conflicts + "windows-sys", # Allow multiple versions due to various Windows dependencies + "windows-targets", # Allow multiple versions due to windows-sys conflicts + "windows_aarch64_gnullvm", # Windows target specific + "windows_aarch64_msvc", # Windows target specific + "windows_i686_gnu", # Windows target specific + "windows_i686_gnullvm", # Windows target specific + "windows_i686_msvc", # Windows target specific + "windows_x86_64_gnu", # Windows target specific + "windows_x86_64_gnullvm", # Windows target specific + "windows_x86_64_msvc", # Windows target specific ] [sources] unknown-registry = "deny" unknown-git = "deny" -[licenses] -exceptions = [ - { name = "ring", allow = ["LicenseRef-ring"] } # ring uses a specific BoringSSL license that does not match the standard text so requires allowing the specific hash +[[licenses.clarify]] +name = "ring" +expression = "MIT AND ISC AND OpenSSL" +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 } ] + +[licenses] unused-allowed-license = "allow" confidence-threshold = 0.93 allow = [ @@ -38,11 +43,3 @@ allow = [ "ISC", "Unicode-3.0", ] - - -[[licenses.clarify]] -name = "ring" -expression = "LicenseRef-ring" -license-files = [ - { path = "LICENSE", hash = 0xbd0eed23 }, -] diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index 48374db..c8cd6ff 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -4,18 +4,18 @@ */ use crate::{ + AsyncGraph, ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorResult, client::{FalkorClientProvider, ProvidesSyncConnections}, connection::{ asynchronous::{BorrowedAsyncConnection, FalkorAsyncConnection}, blocking::FalkorSyncConnection, }, parser::{parse_config_hashmap, redis_value_as_untyped_string_vec}, - AsyncGraph, ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorResult, }; use std::{collections::HashMap, sync::Arc}; use tokio::{ runtime::{Handle, RuntimeFlavor}, - sync::{mpsc, Mutex}, + sync::{Mutex, mpsc}, task, }; @@ -271,8 +271,8 @@ impl FalkorAsyncClient { mod tests { use super::*; use crate::{ - test_utils::{create_async_test_client, TestAsyncGraphHandle}, FalkorClientBuilder, + test_utils::{TestAsyncGraphHandle, create_async_test_client}, }; use std::{mem, num::NonZeroU8, thread}; use tokio::sync::mpsc::error::TryRecvError; diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 4cf4443..9e3f533 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -4,15 +4,15 @@ */ use crate::{ + ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorResult, SyncGraph, client::{FalkorClientProvider, ProvidesSyncConnections}, connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}, parser::{parse_config_hashmap, redis_value_as_untyped_string_vec}, - ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorResult, SyncGraph, }; use parking_lot::Mutex; use std::{ collections::HashMap, - sync::{mpsc, Arc}, + sync::{Arc, mpsc}, }; /// A user-opaque inner struct, containing the actual implementation of the blocking client @@ -138,7 +138,7 @@ impl FalkorSyncClient { /// /// # Arguments /// * `config_Key`: A [`String`] representation of a configuration's key. - /// The config key can also be "*", which will return ALL the configuration options. + /// * The config key can also be "*", which will return ALL the configuration options. /// /// # Returns /// A [`HashMap`] comprised of [`String`] keys, and [`ConfigValue`] values. @@ -161,7 +161,7 @@ impl FalkorSyncClient { /// /// # Arguments /// * `config_Key`: A [`String`] representation of a configuration's key. - /// The config key can also be "*", which will return ALL the configuration options. + /// * 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", @@ -254,10 +254,11 @@ mod tests { use super::*; use crate::FalkorValue::Node; use crate::{ - test_utils::{create_test_client, TestSyncGraphHandle}, FalkorClientBuilder, FalkorValue, LazyResultSet, QueryResult, + test_utils::{TestSyncGraphHandle, create_test_client}, }; use approx::assert_relative_eq; + use chrono::{Datelike, Timelike}; use std::{mem, num::NonZeroU8, sync::mpsc::TryRecvError, thread}; #[test] @@ -321,8 +322,7 @@ mod tests { assert!( e.to_string() .contains("is to be executed only on read-only queries"), - "Unexpected error message: {}", - e + "Unexpected error message: {e}" ); } @@ -341,7 +341,7 @@ mod tests { .query("MATCH (p:Document) RETURN p") .execute() .expect("Could not get document"); - while let Some(falkor_value) = res.data.next() { + for falkor_value in res.data.by_ref() { // iterate on a node value for value in falkor_value { if let Node(node) = value { @@ -363,6 +363,151 @@ mod tests { } } + #[test] + fn test_get_time() { + let client = create_test_client(); + + let mut graph = client.select_graph("imdb"); + let mut res = graph + .query("RETURN localtime({hour: 12, minute:0, second:0}) as time") + .execute() + .expect("Could not return localtime hour 12"); + let Some(falkor_value) = res.data.next() else { + panic!("No data returned from query"); + }; + let Some(value) = falkor_value.first() else { + panic!("No value returned from query"); + }; + assert_eq!(value.as_time().unwrap().hour(), 12); + assert_eq!(value.as_time().unwrap().minute(), 0); + assert_eq!(value.as_time().unwrap().second(), 0); + } + + #[test] + fn test_get_date() { + let client = create_test_client(); + + let mut graph = client.select_graph("imdb"); + let mut res = graph + .query("RETURN date({year : 1984}) as date") + .execute() + .expect("Could not return 1984"); + let Some(falkor_value) = res.data.next() else { + panic!("No data returned from query"); + }; + let Some(value) = falkor_value.first() else { + panic!("No value returned from query"); + }; + assert_eq!(value.as_date().unwrap().year(), 1984); + } + + #[test] + fn test_get_date_time() { + let client = create_test_client(); + + let mut graph = client.select_graph("imdb"); + let mut res = graph + .query("RETURN localdatetime({year : 1984}) as date") + .execute() + .expect("Could not return localdatetime"); + let Some(falkor_value) = res.data.next() else { + panic!("No data returned from query"); + }; + let Some(value) = falkor_value.first() else { + panic!("No value returned from query"); + }; + + assert_eq!(value.as_date_time().unwrap().year(), 1984); + } + + #[test] + fn test_duration_component_construction() { + use std::collections::HashMap; + + let client = create_test_client(); + let mut graph = client.select_graph("duration_test"); + + // Test cases: (input map, expected total seconds) + let test_cases: Vec<(HashMap<&str, i32>, i64)> = vec![ + // Single components + (HashMap::from([("years", 2)]), 2 * 365 * 24 * 3600), // ~2 years in seconds + (HashMap::from([("months", 3)]), 3 * 30 * 24 * 3600), // ~3 months in seconds + (HashMap::from([("weeks", 1)]), 1 * 7 * 24 * 3600), // 1 week in seconds + (HashMap::from([("hours", 6)]), 6 * 3600), // 6 hours in seconds + (HashMap::from([("minutes", 23)]), 23 * 60), // 23 minutes in seconds + (HashMap::from([("seconds", 15)]), 15), // 15 seconds + // Multiple components + ( + HashMap::from([("years", 2), ("months", 3)]), + 2 * 365 * 24 * 3600 + 3 * 30 * 24 * 3600, + ), + ( + HashMap::from([("years", 2), ("months", 3), ("days", 4)]), + 2 * 365 * 24 * 3600 + 3 * 30 * 24 * 3600 + 4 * 24 * 3600, + ), + ( + HashMap::from([("hours", 5), ("minutes", 22)]), + 5 * 3600 + 22 * 60, + ), + ( + HashMap::from([("hours", 5), ("minutes", 22), ("seconds", 7)]), + 5 * 3600 + 22 * 60 + 7, + ), + // Negative values + (HashMap::from([("years", -2)]), -2 * 365 * 24 * 3600), + (HashMap::from([("months", -3)]), -3 * 30 * 24 * 3600), + (HashMap::from([("hours", -6)]), -6 * 3600), + (HashMap::from([("minutes", -23)]), -23 * 60), + (HashMap::from([("seconds", -15)]), -15), + ( + HashMap::from([("years", -2), ("months", -3)]), + -2 * 365 * 24 * 3600 - 3 * 30 * 24 * 3600, + ), + ]; + + for (input_map, expected_seconds) in test_cases { + // Build the parameter string for the Cypher query + let mut params = Vec::new(); + for (key, value) in &input_map { + params.push(format!("{}: {}", key, value)); + } + let param_string = params.join(", "); + + let query = format!("RETURN duration({{{}}})", param_string); + + let mut res = graph + .query(&query) + .execute() + .expect("Could not execute duration query"); + + let Some(falkor_value) = res.data.next() else { + panic!("No data returned from query for input: {:?}", input_map); + }; + let Some(value) = falkor_value.first() else { + panic!("No value returned from query for input: {:?}", input_map); + }; + + let duration = value.as_duration().expect("Expected duration value"); + + // Allow some tolerance for year/month approximations (within 10% for large durations) + let actual_seconds = duration.num_seconds(); + let tolerance = if expected_seconds.abs() > 3600 * 24 * 30 { + // For durations > 1 month + (expected_seconds.abs() as f64 * 0.1) as i64 // 10% tolerance + } else { + 0 // Exact match for smaller durations + }; + + assert!( + (actual_seconds - expected_seconds).abs() <= tolerance, + "Duration mismatch for {:?}: expected ~{} seconds, got {} seconds", + input_map, + expected_seconds, + actual_seconds + ); + } + } + #[test] fn test_select_graph_and_query() { let client = create_test_client(); diff --git a/src/client/builder.rs b/src/client/builder.rs index bfe6e35..cbba727 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -4,8 +4,8 @@ */ use crate::{ - client::FalkorClientProvider, FalkorConnectionInfo, FalkorDBError, FalkorResult, - FalkorSyncClient, + FalkorConnectionInfo, FalkorDBError, FalkorResult, FalkorSyncClient, + client::FalkorClientProvider, }; use std::num::NonZeroU8; @@ -147,10 +147,12 @@ mod tests { let connection_info = "falkor://127.0.0.1:6379".try_into(); assert!(connection_info.is_ok()); - assert!(FalkorClientBuilder::new() - .with_connection_info(connection_info.unwrap()) - .build() - .is_ok()); + assert!( + FalkorClientBuilder::new() + .with_connection_info(connection_info.unwrap()) + .build() + .is_ok() + ); } #[test] diff --git a/src/client/mod.rs b/src/client/mod.rs index 17a1514..3b37a1f 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -4,9 +4,9 @@ */ use crate::{ + FalkorDBError, FalkorResult, connection::blocking::FalkorSyncConnection, parser::{redis_value_as_string, redis_value_as_vec}, - FalkorDBError, FalkorResult, }; use std::collections::HashMap; diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs index 05f13f2..ee4f1aa 100644 --- a/src/connection/asynchronous.rs +++ b/src/connection/asynchronous.rs @@ -4,8 +4,8 @@ */ use crate::{ - client::asynchronous::FalkorAsyncClientInner, connection::map_redis_err, - parser::parse_redis_info, FalkorDBError, FalkorResult, + FalkorDBError, FalkorResult, client::asynchronous::FalkorAsyncClientInner, + connection::map_redis_err, parser::parse_redis_info, }; use std::{collections::HashMap, sync::Arc}; use tokio::sync::mpsc; diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 0860a5c..d7ac727 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -4,14 +4,14 @@ */ use crate::{ - client::{blocking::FalkorSyncClientInner, ProvidesSyncConnections}, + FalkorDBError, FalkorResult, + client::{ProvidesSyncConnections, blocking::FalkorSyncClientInner}, connection::map_redis_err, parser::parse_redis_info, - FalkorDBError, FalkorResult, }; use std::{ collections::HashMap, - sync::{mpsc, Arc}, + sync::{Arc, mpsc}, }; pub(crate) enum FalkorSyncConnection { diff --git a/src/error/mod.rs b/src/error/mod.rs index b43ae28..3fc1d87 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -21,7 +21,9 @@ pub enum FalkorDBError { #[error("Received unsupported number of sentinel masters in list, there can be only one")] SentinelMastersCount, ///This requested returned a connection error, however, we may be able to create a new connection to the server, this operation should probably be retried in a bit. - #[error("This requested returned a connection error, however, we may be able to create a new connection to the server, this operation should probably be retried in a bit.")] + #[error( + "This requested returned a connection error, however, we may be able to create a new connection to the server, this operation should probably be retried in a bit." + )] ConnectionDown, /// An error occurred while sending the request to Redis. #[error("An error occurred while sending the request to Redis: {0}")] @@ -36,7 +38,9 @@ pub enum FalkorDBError { #[error("The connection returned invalid data for this command")] InvalidDataReceived, /// The provided URL scheme points at a database provider that is currently unavailable, make sure the correct feature is enabled. - #[error("The provided URL scheme points at a database provider that is currently unavailable, make sure the correct feature is enabled")] + #[error( + "The provided URL scheme points at a database provider that is currently unavailable, make sure the correct feature is enabled" + )] UnavailableProvider, /// An error occurred when dealing with reference counts or RefCells, perhaps mutual borrows? #[error( @@ -113,17 +117,24 @@ pub enum FalkorDBError { #[error("Both key id and type marker were not of type i64")] ParsingKTVTypes, /// 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}")] + #[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), /// Running in a single-threaded tokio runtime! Running async operations in a blocking context will cause a panic, aborting operation - #[error("Running in a single-threaded tokio runtime! Running async operations in a blocking context will cause a panic, aborting operation")] + #[error( + "Running in a single-threaded tokio runtime! Running async operations in a blocking context will cause a panic, aborting operation" + )] SingleThreadedRuntime, /// No runtime detected, you are trying to run an async operation from a sync context #[error("No runtime detected, you are trying to run an async operation from a sync context")] NoRuntime, + /// Invalid temporal value encountered, such as an invalid date or time format. + #[error("Invalid temporal value: {0}")] + ParseTemporalError(String), } impl From for FalkorDBError { diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index 9610300..3fd9fbb 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -4,12 +4,12 @@ */ use crate::{ + Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorIndex, FalkorResult, GraphSchema, + IndexType, LazyResultSet, ProcedureQueryBuilder, QueryBuilder, QueryResult, SlowlogEntry, client::asynchronous::FalkorAsyncClientInner, graph::HasGraphSchema, graph::{generate_create_index_query, generate_drop_index_query}, parser::redis_value_as_vec, - Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorIndex, FalkorResult, GraphSchema, - IndexType, LazyResultSet, ProcedureQueryBuilder, QueryBuilder, QueryResult, SlowlogEntry, }; use std::{collections::HashMap, fmt::Display, sync::Arc}; @@ -403,8 +403,8 @@ impl HasGraphSchema for AsyncGraph { mod tests { use super::*; use crate::{ - test_utils::{create_async_test_client, open_empty_async_test_graph}, IndexType, + test_utils::{create_async_test_client, open_empty_async_test_graph}, }; #[tokio::test(flavor = "multi_thread")] diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 231eee6..995289a 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -4,11 +4,11 @@ */ use crate::{ - client::blocking::FalkorSyncClientInner, - graph::{generate_create_index_query, generate_drop_index_query, HasGraphSchema}, - parser::redis_value_as_vec, Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorIndex, FalkorResult, GraphSchema, IndexType, LazyResultSet, ProcedureQueryBuilder, QueryBuilder, QueryResult, SlowlogEntry, + client::blocking::FalkorSyncClientInner, + graph::{HasGraphSchema, generate_create_index_query, generate_drop_index_query}, + parser::redis_value_as_vec, }; use std::{collections::HashMap, fmt::Display, sync::Arc}; @@ -391,8 +391,8 @@ impl HasGraphSchema for SyncGraph { mod tests { use super::*; use crate::{ - test_utils::{create_test_client, open_empty_test_graph}, FalkorDBError, IndexType, + test_utils::{create_test_client, open_empty_test_graph}, }; #[test] diff --git a/src/graph/mod.rs b/src/graph/mod.rs index 00011c3..2c8773c 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -25,13 +25,13 @@ pub(crate) fn generate_create_index_query( ) -> String { let properties_string = properties .iter() - .map(|element| format!("l.{}", element)) + .map(|element| format!("l.{element}")) .collect::>() .join(", "); let pattern = match entity_type { - EntityType::Node => format!("(l:{})", label), - EntityType::Edge => format!("()-[l:{}]->()", label), + EntityType::Node => format!("(l:{label})"), + EntityType::Edge => format!("()-[l:{label}]->()"), }; let idx_type = match index_field_type { @@ -48,13 +48,10 @@ pub(crate) fn generate_create_index_query( .collect::>() .join(",") }) - .map(|options_string| format!(" OPTIONS {{ {} }}", options_string)) + .map(|options_string| format!(" OPTIONS {{ {options_string} }}")) .unwrap_or_default(); - format!( - "CREATE {idx_type}INDEX FOR {pattern} ON ({}){}", - properties_string, options_string - ) + format!("CREATE {idx_type}INDEX FOR {pattern} ON ({properties_string}){options_string}",) } pub(crate) fn generate_drop_index_query( @@ -65,13 +62,13 @@ pub(crate) fn generate_drop_index_query( ) -> String { let properties_string = properties .iter() - .map(|element| format!("e.{}", element)) + .map(|element| format!("e.{element}")) .collect::>() .join(", "); let pattern = match entity_type { - EntityType::Node => format!("(e:{})", label), - EntityType::Edge => format!("()-[e:{}]->()", label), + EntityType::Node => format!("(e:{label})"), + EntityType::Edge => format!("()-[e:{label}]->()"), }; let idx_type = match index_field_type { @@ -81,8 +78,5 @@ pub(crate) fn generate_drop_index_query( } .to_string(); - format!( - "DROP {idx_type} INDEX for {pattern} ON ({})", - properties_string - ) + format!("DROP {idx_type} INDEX for {pattern} ON ({properties_string})") } diff --git a/src/graph/query_builder.rs b/src/graph/query_builder.rs index 9eb8ac1..700d8f2 100644 --- a/src/graph/query_builder.rs +++ b/src/graph/query_builder.rs @@ -4,10 +4,10 @@ */ use crate::{ - graph::HasGraphSchema, - parser::{redis_value_as_vec, SchemaParsable}, Constraint, ExecutionPlan, FalkorDBError, FalkorIndex, FalkorResult, LazyResultSet, QueryResult, SyncGraph, + graph::HasGraphSchema, + parser::{SchemaParsable, redis_value_as_vec}, }; use std::{collections::HashMap, fmt::Display, marker::PhantomData, ops::Not}; @@ -83,7 +83,7 @@ impl<'a, Output, T: Display, G: HasGraphSchema> QueryBuilder<'a, Output, T, G> { /// /// # Arguments /// * `timeout`: the timeout after which the server is allowed to abort or throw this request, - /// in milliseconds, when that happens the server will return a timeout error + /// * in milliseconds, when that happens the server will return a timeout error pub fn with_timeout( self, timeout: i64, @@ -264,10 +264,10 @@ pub(crate) fn generate_procedure_call( let args_str = args .unwrap_or_default() .iter() - .map(|e| format!("${}", e)) + .map(|e| format!("${e}")) .collect::>() .join(","); - let mut query_string = format!("CALL {}({})", procedure, args_str); + let mut query_string = format!("CALL {procedure}({args_str})"); let params = args.map(|args| { args.iter() diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index 70b7185..309656e 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -4,11 +4,11 @@ */ use crate::{ + FalkorDBError, FalkorResult, FalkorValue, client::ProvidesSyncConnections, parser::{ - parse_type, redis_value_as_int, redis_value_as_string, redis_value_as_vec, ParserTypeMarker, + ParserTypeMarker, parse_type, redis_value_as_int, redis_value_as_string, redis_value_as_vec, }, - FalkorDBError, FalkorResult, FalkorValue, }; use std::{collections::HashMap, sync::Arc}; @@ -276,8 +276,8 @@ impl GraphSchema { pub(crate) mod tests { use super::*; use crate::{ - client::blocking::create_empty_inner_sync_client, graph::HasGraphSchema, - test_utils::create_test_client, SyncGraph, + SyncGraph, client::blocking::create_empty_inner_sync_client, graph::HasGraphSchema, + test_utils::create_test_client, }; use std::collections::HashMap; diff --git a/src/lib.rs b/src/lib.rs index 989de4b..6af6203 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,19 +31,19 @@ pub use graph::{ }; pub use graph_schema::{GraphSchema, SchemaType}; pub use response::{ + QueryResult, constraint::{Constraint, ConstraintStatus, ConstraintType}, execution_plan::ExecutionPlan, index::{FalkorIndex, IndexStatus, IndexType}, lazy_result_set::LazyResultSet, slowlog_entry::SlowlogEntry, - QueryResult, }; pub use value::{ + FalkorValue, config::ConfigValue, graph_entities::{Edge, EntityType, Node}, path::Path, point::Point, - FalkorValue, }; #[cfg(feature = "tokio")] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c0e7b96..d1c981e 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3,9 +3,11 @@ * Licensed under the MIT License. */ +use chrono::{DateTime, Utc}; + use crate::{ - value::vec32::Vec32, ConfigValue, Edge, FalkorDBError, FalkorResult, FalkorValue, GraphSchema, - Node, Path, Point, + ConfigValue, Edge, FalkorDBError, FalkorResult, FalkorValue, GraphSchema, Node, Path, Point, + value::vec32::Vec32, }; use std::collections::HashMap; @@ -24,6 +26,10 @@ pub(crate) enum ParserTypeMarker { Map = 10, Point = 11, Vec32 = 12, + DateTime = 13, + Date = 14, + Time = 15, + Duration = 16, } impl TryFrom for ParserTypeMarker { @@ -43,6 +49,10 @@ impl TryFrom for ParserTypeMarker { 10 => Self::Map, 11 => Self::Point, 12 => Self::Vec32, + 13 => Self::DateTime, + 14 => Self::Date, + 15 => Self::Time, + 16 => Self::Duration, _ => Err(FalkorDBError::ParsingUnknownType)?, }) } @@ -91,6 +101,95 @@ pub(crate) fn redis_value_as_vec(value: redis::Value) -> FalkorResult FalkorResult { + // Remove 'P' prefix if present + let duration_str = duration_str.strip_prefix('P').unwrap_or(duration_str); + + let mut years = 0i64; + let mut months = 0i64; + let mut days = 0i64; + let mut hours = 0i64; + let mut minutes = 0i64; + let mut seconds = 0i64; + + let mut current_number = String::new(); + let mut in_time_part = false; + + for ch in duration_str.chars() { + match ch { + 'T' => { + in_time_part = true; + } + 'Y' if !in_time_part => { + years = current_number.parse().map_err(|_| { + FalkorDBError::ParseTemporalError("Invalid year value in duration".to_string()) + })?; + current_number.clear(); + } + 'M' if !in_time_part => { + months = current_number.parse().map_err(|_| { + FalkorDBError::ParseTemporalError("Invalid month value in duration".to_string()) + })?; + current_number.clear(); + } + 'D' if !in_time_part => { + days = current_number.parse().map_err(|_| { + FalkorDBError::ParseTemporalError("Invalid day value in duration".to_string()) + })?; + current_number.clear(); + } + 'H' if in_time_part => { + hours = current_number.parse().map_err(|_| { + FalkorDBError::ParseTemporalError("Invalid hour value in duration".to_string()) + })?; + current_number.clear(); + } + 'M' if in_time_part => { + minutes = current_number.parse().map_err(|_| { + FalkorDBError::ParseTemporalError( + "Invalid minute value in duration".to_string(), + ) + })?; + current_number.clear(); + } + 'S' if in_time_part => { + seconds = current_number.parse().map_err(|_| { + FalkorDBError::ParseTemporalError( + "Invalid second value in duration".to_string(), + ) + })?; + current_number.clear(); + } + '0'..='9' | '.' => { + current_number.push(ch); + } + _ => { + return Err(FalkorDBError::ParseTemporalError(format!( + "Invalid character '{}' in duration string", + ch + ))); + } + } + } + + // Convert to total seconds (approximate for years/months) + let total_seconds = seconds + + minutes * 60 + + hours * 3600 + + days * 86400 + + months * 30 * 86400 + // Approximate: 30 days per month + years * 365 * 86400; // Approximate: 365 days per year + + chrono::Duration::try_seconds(total_seconds).ok_or(FalkorDBError::ParseTemporalError( + "Duration value out of range".to_string(), + )) +} + #[cfg_attr( feature = "tracing", tracing::instrument(name = "Parse Redis Info", skip_all, level = "info") @@ -341,6 +440,30 @@ pub(crate) fn parse_type( ParserTypeMarker::Map => FalkorValue::Map(parse_regular_falkor_map(val, graph_schema)?), ParserTypeMarker::Point => FalkorValue::Point(Point::parse(val)?), ParserTypeMarker::Vec32 => FalkorValue::Vec32(Vec32::parse(val)?), + ParserTypeMarker::DateTime => FalkorValue::DateTime( + DateTime::::from_timestamp(redis_value_as_int(val)?, 0).ok_or( + FalkorDBError::ParseTemporalError( + "Could not parse date time from timestamp".to_string(), + ), + )?, + ), + ParserTypeMarker::Date => FalkorValue::Date( + DateTime::::from_timestamp(redis_value_as_int(val)?, 0) + .map(|dt| dt.date_naive()) + .ok_or(FalkorDBError::ParseTemporalError( + "Could not parse date from timestamp".to_string(), + ))?, + ), + ParserTypeMarker::Time => FalkorValue::Time( + DateTime::::from_timestamp(redis_value_as_int(val)?, 0) + .map(|dt| dt.time()) + .ok_or(FalkorDBError::ParseTemporalError( + "Could not parse time from timestamp".to_string(), + ))?, + ), + ParserTypeMarker::Duration => { + FalkorValue::Duration(chrono::Duration::seconds(redis_value_as_int(val)?)) + } }; Ok(res) @@ -357,8 +480,8 @@ pub(crate) trait SchemaParsable: Sized { mod tests { use super::*; use crate::{ - client::blocking::create_empty_inner_sync_client, graph::HasGraphSchema, - graph_schema::tests::open_readonly_graph_with_modified_schema, FalkorDBError, + FalkorDBError, client::blocking::create_empty_inner_sync_client, graph::HasGraphSchema, + graph_schema::tests::open_readonly_graph_with_modified_schema, }; #[test] @@ -750,4 +873,100 @@ mod tests { assert_eq!(res.get("IntKey"), Some(FalkorValue::I64(1)).as_ref()); assert_eq!(res.get("BoolKey"), Some(FalkorValue::Bool(true)).as_ref()); } + + #[test] + fn test_parse_duration_simple() { + let mut graph = open_readonly_graph_with_modified_schema(); + + let res = parse_type( + ParserTypeMarker::Duration, + redis::Value::SimpleString("P1Y2M3DT4H5M6S".to_string()), + graph.get_graph_schema_mut(), + ); + assert!(res.is_ok()); + + let falkor_duration = res.unwrap(); + let FalkorValue::Duration(duration) = falkor_duration else { + panic!("Is not of type duration") + }; + + // Should be approximately 1 year + 2 months + 3 days + 4 hours + 5 minutes + 6 seconds + // = 365*24*3600 + 60*24*3600 + 3*24*3600 + 4*3600 + 5*60 + 6 seconds + let expected_seconds = + 365 * 24 * 3600 + 60 * 24 * 3600 + 3 * 24 * 3600 + 4 * 3600 + 5 * 60 + 6; + assert_eq!(duration.num_seconds(), expected_seconds); + } + + #[test] + fn test_parse_duration_time_only() { + let mut graph = open_readonly_graph_with_modified_schema(); + + let res = parse_type( + ParserTypeMarker::Duration, + redis::Value::SimpleString("PT2H30M15S".to_string()), + graph.get_graph_schema_mut(), + ); + assert!(res.is_ok()); + + let falkor_duration = res.unwrap(); + let FalkorValue::Duration(duration) = falkor_duration else { + panic!("Is not of type duration") + }; + + // Should be 2 hours + 30 minutes + 15 seconds = 2*3600 + 30*60 + 15 = 9015 seconds + assert_eq!(duration.num_seconds(), 9015); + } + + #[test] + fn test_parse_duration_date_only() { + let mut graph = open_readonly_graph_with_modified_schema(); + + let res = parse_type( + ParserTypeMarker::Duration, + redis::Value::SimpleString("P1Y6M".to_string()), + graph.get_graph_schema_mut(), + ); + assert!(res.is_ok()); + + let falkor_duration = res.unwrap(); + let FalkorValue::Duration(duration) = falkor_duration else { + panic!("Is not of type duration") + }; + + // Should be 1 year + 6 months = 365*24*3600 + 6*30*24*3600 seconds + let expected_seconds = 365 * 24 * 3600 + 6 * 30 * 24 * 3600; + assert_eq!(duration.num_seconds(), expected_seconds); + } + + #[test] + fn test_parse_duration_invalid() { + let mut graph = open_readonly_graph_with_modified_schema(); + + let res = parse_type( + ParserTypeMarker::Duration, + redis::Value::SimpleString("INVALID".to_string()), + graph.get_graph_schema_mut(), + ); + assert!(res.is_err()); + } + + #[test] + fn test_parse_duration_from_string_function() { + // Test the parse_duration_from_string function directly + let duration = parse_duration_from_string("P1DT2H3M4S").unwrap(); + let expected_seconds = 1 * 24 * 3600 + 2 * 3600 + 3 * 60 + 4; + assert_eq!(duration.num_seconds(), expected_seconds); + + // Test with P prefix + let duration2 = parse_duration_from_string("P1DT2H3M4S").unwrap(); + assert_eq!(duration2.num_seconds(), expected_seconds); + + // Test without P prefix + let duration3 = parse_duration_from_string("1DT2H3M4S").unwrap(); + assert_eq!(duration3.num_seconds(), expected_seconds); + + // Test invalid input + let result = parse_duration_from_string("INVALID"); + assert!(result.is_err()); + } } diff --git a/src/response/constraint.rs b/src/response/constraint.rs index 22e8730..e2e574f 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -4,11 +4,11 @@ */ use crate::{ + EntityType, FalkorDBError, FalkorResult, GraphSchema, parser::{ - parse_falkor_enum, redis_value_as_typed_string, redis_value_as_typed_string_vec, - redis_value_as_vec, SchemaParsable, + SchemaParsable, parse_falkor_enum, redis_value_as_typed_string, + redis_value_as_typed_string_vec, redis_value_as_vec, }, - EntityType, FalkorDBError, FalkorResult, GraphSchema, }; /// The type of restriction to apply for the property @@ -59,9 +59,19 @@ impl SchemaParsable for Constraint { value: redis::Value, _: &mut GraphSchema, ) -> FalkorResult { - 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, + ]: [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", + ) + }) + })?; Ok(Self { constraint_type: parse_falkor_enum(constraint_type_raw)?, diff --git a/src/response/execution_plan.rs b/src/response/execution_plan.rs index ffbacb5..546f94b 100644 --- a/src/response/execution_plan.rs +++ b/src/response/execution_plan.rs @@ -4,8 +4,8 @@ */ use crate::{ - parser::{redis_value_as_string, redis_value_as_vec}, FalkorDBError, FalkorResult, + parser::{redis_value_as_string, redis_value_as_vec}, }; use regex::Regex; use std::{ diff --git a/src/response/index.rs b/src/response/index.rs index c0ef525..0a951dc 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -3,13 +3,13 @@ * Licensed under the MIT License. */ -use crate::parser::{parse_type, ParserTypeMarker}; +use crate::parser::{ParserTypeMarker, parse_type}; use crate::{ + EntityType, FalkorDBError, FalkorValue, GraphSchema, parser::{ - parse_falkor_enum, parse_raw_redis_value, redis_value_as_string, - redis_value_as_typed_string, redis_value_as_vec, type_val_from_value, SchemaParsable, + SchemaParsable, parse_falkor_enum, parse_raw_redis_value, redis_value_as_string, + redis_value_as_typed_string, redis_value_as_vec, type_val_from_value, }, - EntityType, FalkorDBError, FalkorValue, GraphSchema, }; use std::collections::HashMap; @@ -118,14 +118,23 @@ impl SchemaParsable for FalkorIndex { value: redis::Value, graph_schema: &mut GraphSchema, ) -> Result { - let [label, fields, field_types, options, 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 9 elements in index object", - ) - }) - })?; + let [ + label, + fields, + field_types, + options, + 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 9 elements in index object", + ) + }) + })?; Ok(Self { entity_type: parse_falkor_enum(entity_type)?, diff --git a/src/response/lazy_result_set.rs b/src/response/lazy_result_set.rs index 8fa5054..5ec5b35 100644 --- a/src/response/lazy_result_set.rs +++ b/src/response/lazy_result_set.rs @@ -4,7 +4,7 @@ */ use crate::parser::ParserTypeMarker; -use crate::{parser::parse_type, FalkorValue, GraphSchema}; +use crate::{FalkorValue, GraphSchema, parser::parse_type}; use std::collections::VecDeque; /// A wrapper around the returned raw data, allowing parsing on demand of each result diff --git a/src/response/mod.rs b/src/response/mod.rs index 40b2cbc..9874451 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -4,8 +4,8 @@ */ use crate::{ - parser::{parse_header, redis_value_as_untyped_string_vec}, FalkorResult, + parser::{parse_header, redis_value_as_untyped_string_vec}, }; use std::str::FromStr; diff --git a/src/response/slowlog_entry.rs b/src/response/slowlog_entry.rs index 4ed233d..68ffc6c 100644 --- a/src/response/slowlog_entry.rs +++ b/src/response/slowlog_entry.rs @@ -4,8 +4,8 @@ */ use crate::{ - parser::{redis_value_as_double, redis_value_as_string, redis_value_as_vec}, FalkorDBError, FalkorResult, + parser::{redis_value_as_double, redis_value_as_string, redis_value_as_vec}, }; /// A slowlog entry, representing one of the N slowest queries in the current log diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index f3cfe24..e48f49b 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -4,8 +4,8 @@ */ use crate::{ - parser::{redis_value_as_int, redis_value_as_vec}, FalkorDBError, FalkorResult, FalkorValue, GraphSchema, SchemaType, + parser::{redis_value_as_int, redis_value_as_vec}, }; use std::collections::HashMap; @@ -81,8 +81,13 @@ impl Edge { value: redis::Value, graph_schema: &mut GraphSchema, ) -> FalkorResult { - 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| { + 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", diff --git a/src/value/mod.rs b/src/value/mod.rs index c30fdc2..3d5b455 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -45,6 +45,14 @@ pub enum FalkorValue { None, /// Failed parsing this value Unparseable(String), + /// A DateTime value, using chrono's DateTime + DateTime(chrono::DateTime), + /// A Date value, using chrono's NaiveDate + Date(chrono::NaiveDate), + /// A Time value, using chrono's NaiveTime + Time(chrono::NaiveTime), + /// A Duration value, using chrono's Duration + Duration(chrono::Duration), } macro_rules! impl_to_falkordb_value { @@ -76,6 +84,30 @@ impl From<&str> for FalkorValue { } } +impl From for FalkorValue { + fn from(value: chrono::Duration) -> Self { + FalkorValue::Duration(value) + } +} + +impl From> for FalkorValue { + fn from(value: chrono::DateTime) -> Self { + FalkorValue::DateTime(value) + } +} + +impl From for FalkorValue { + fn from(value: chrono::NaiveDate) -> Self { + FalkorValue::Date(value) + } +} + +impl From for FalkorValue { + fn from(value: chrono::NaiveTime) -> Self { + FalkorValue::Time(value) + } +} + impl FalkorValue { /// Returns a reference to the internal [`Vec`] if this is an Array variant. /// @@ -154,6 +186,47 @@ impl FalkorValue { } } + /// Returns a reference to the internal [`chrono::DateTime`] if this is a DateTime variant. + /// + /// # Returns + /// A reference to the internal [`chrono::DateTime`] + pub fn as_date_time(&self) -> Option<&chrono::DateTime> { + match self { + FalkorValue::DateTime(val) => Some(val), + _ => None, + } + } + + /// Returns a reference to the internal [`chrono::NaiveDate`] if this is a Date variant. + /// # Returns + /// A reference to the internal [`chrono::NaiveDate`] + pub fn as_date(&self) -> Option<&chrono::NaiveDate> { + match self { + FalkorValue::Date(val) => Some(val), + _ => None, + } + } + + /// Returns a reference to the internal [`chrono::NaiveTime`] if this is a Time variant. + /// # Returns + /// A reference to the internal [`chrono::NaiveTime`] + pub fn as_time(&self) -> Option<&chrono::NaiveTime> { + match self { + FalkorValue::Time(val) => Some(val), + _ => None, + } + } + + /// Returns a reference to the internal [`chrono::Duration`] if this is a Duration variant. + /// # Returns + /// A reference to the internal [`chrono::Duration`] + pub fn as_duration(&self) -> Option<&chrono::Duration> { + match self { + FalkorValue::Duration(val) => Some(val), + _ => None, + } + } + /// Returns a Copy of the inner [`i64`] if this is an Int64 variant /// /// # Returns @@ -225,6 +298,58 @@ impl FalkorValue { } } +impl TryFrom for chrono::Duration { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::Duration(duration) => Ok(duration), + _ => Err(FalkorDBError::ParseTemporalError( + "Not a Duration value".to_string(), + )), + } + } +} + +impl TryFrom for chrono::DateTime { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::DateTime(datetime) => Ok(datetime), + _ => Err(FalkorDBError::ParseTemporalError( + "Not a DateTime value".to_string(), + )), + } + } +} + +impl TryFrom for chrono::NaiveDate { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::Date(date) => Ok(date), + _ => Err(FalkorDBError::ParseTemporalError( + "Not a Date value".to_string(), + )), + } + } +} + +impl TryFrom for chrono::NaiveTime { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::Time(time) => Ok(time), + _ => Err(FalkorDBError::ParseTemporalError( + "Not a Time value".to_string(), + )), + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -349,4 +474,71 @@ mod tests { let non_string_val = FalkorValue::I64(42); assert!(non_string_val.into_string().is_err()); } + + #[test] + fn test_as_duration() { + let duration = chrono::Duration::hours(2); + let duration_val = FalkorValue::Duration(duration); + assert_eq!(duration_val.as_duration().unwrap(), &duration); + + let non_duration_val = FalkorValue::I64(42); + assert!(non_duration_val.as_duration().is_none()); + } + + #[test] + fn test_as_datetime() { + let datetime = chrono::DateTime::from_timestamp(1234567890, 0).unwrap(); + let datetime_val = FalkorValue::DateTime(datetime); + assert_eq!(datetime_val.as_date_time().unwrap(), &datetime); + + let non_datetime_val = FalkorValue::I64(42); + assert!(non_datetime_val.as_date_time().is_none()); + } + + #[test] + fn test_as_date() { + let date = chrono::NaiveDate::from_ymd_opt(2023, 12, 25).unwrap(); + let date_val = FalkorValue::Date(date); + assert_eq!(date_val.as_date().unwrap(), &date); + + let non_date_val = FalkorValue::I64(42); + assert!(non_date_val.as_date().is_none()); + } + + #[test] + fn test_as_time() { + let time = chrono::NaiveTime::from_hms_opt(14, 30, 0).unwrap(); + let time_val = FalkorValue::Time(time); + assert_eq!(time_val.as_time().unwrap(), &time); + + let non_time_val = FalkorValue::I64(42); + assert!(non_time_val.as_time().is_none()); + } + + #[test] + fn test_duration_from_into() { + let duration = chrono::Duration::minutes(45); + let duration_val = FalkorValue::from(duration); + assert_eq!(duration_val.as_duration().unwrap(), &duration); + + let converted_duration: chrono::Duration = duration_val.try_into().unwrap(); + assert_eq!(converted_duration, duration); + } + + #[test] + fn test_datetime_from_into() { + let datetime = chrono::DateTime::from_timestamp(1234567890, 0).unwrap(); + let datetime_val = FalkorValue::from(datetime); + assert_eq!(datetime_val.as_date_time().unwrap(), &datetime); + + let converted_datetime: chrono::DateTime = datetime_val.try_into().unwrap(); + assert_eq!(converted_datetime, datetime); + } + + #[test] + fn test_duration_conversion_error() { + let non_duration_val = FalkorValue::I64(42); + let result: Result = non_duration_val.try_into(); + assert!(result.is_err()); + } } diff --git a/src/value/path.rs b/src/value/path.rs index a8bd756..0d3a53e 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -use crate::{parser::redis_value_as_vec, Edge, FalkorDBError, FalkorResult, GraphSchema, Node}; +use crate::{Edge, FalkorDBError, FalkorResult, GraphSchema, Node, parser::redis_value_as_vec}; /// Represents a path between two nodes, contains all the nodes, and the relationships between them along the path #[derive(Clone, Debug, Default, PartialEq)] diff --git a/src/value/point.rs b/src/value/point.rs index 87529c1..e1388e6 100644 --- a/src/value/point.rs +++ b/src/value/point.rs @@ -4,8 +4,8 @@ */ use crate::{ - parser::{redis_value_as_double, redis_value_as_vec}, FalkorDBError, FalkorResult, + parser::{redis_value_as_double, redis_value_as_vec}, }; /// A point in the world. diff --git a/src/value/vec32.rs b/src/value/vec32.rs index 382c2f5..6573247 100644 --- a/src/value/vec32.rs +++ b/src/value/vec32.rs @@ -4,9 +4,9 @@ */ use crate::{ - parser::{redis_value_as_float, redis_value_as_vec}, FalkorDBError::ParsingVec32, FalkorResult, + parser::{redis_value_as_float, redis_value_as_vec}, }; #[derive(Clone, Debug, Default, PartialEq)]