From 35bb73301e8a30c2cc28c95ef7a8fb1c03d200e3 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Sun, 19 May 2024 17:21:54 +0300 Subject: [PATCH 01/62] Initial development --- .gitignore | 3 +- Cargo.lock | 1175 ++++++++++++++++++++++++++++++++ Cargo.toml | 24 + src/client/asynchronous.rs | 28 + src/client/blocking.rs | 152 +++++ src/client/builder.rs | 117 ++++ src/client/mod.rs | 47 ++ src/connection/asynchronous.rs | 29 + src/connection/blocking.rs | 28 + src/connection/mod.rs | 3 + src/connection_info/mod.rs | 75 ++ src/error/mod.rs | 15 + src/graph/asynchronous.rs | 5 + src/graph/blocking.rs | 78 +++ src/graph/mod.rs | 3 + src/lib.rs | 116 ++++ src/main.rs | 32 + src/redis_ext.rs | 113 +++ src/value/config.rs | 34 + src/value/mod.rs | 176 +++++ src/value/query_result.rs | 87 +++ src/value/slowlog_entry.rs | 22 + 22 files changed, 2361 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/client/asynchronous.rs create mode 100644 src/client/blocking.rs create mode 100644 src/client/builder.rs create mode 100644 src/client/mod.rs create mode 100644 src/connection/asynchronous.rs create mode 100644 src/connection/blocking.rs create mode 100644 src/connection/mod.rs create mode 100644 src/connection_info/mod.rs create mode 100644 src/error/mod.rs create mode 100644 src/graph/asynchronous.rs create mode 100644 src/graph/blocking.rs create mode 100644 src/graph/mod.rs create mode 100644 src/lib.rs create mode 100644 src/main.rs create mode 100644 src/redis_ext.rs create mode 100644 src/value/config.rs create mode 100644 src/value/mod.rs create mode 100644 src/value/query_result.rs create mode 100644 src/value/slowlog_entry.rs diff --git a/.gitignore b/.gitignore index 0408c77..eccec5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -target/ +/target .idea/ .vscode/ +.vs/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..150648c --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1175 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" + +[[package]] +name = "async-trait" +version = "0.1.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bytes" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" + +[[package]] +name = "cc" +version = "1.0.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "colored" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" +dependencies = [ + "lazy_static", + "windows-sys 0.48.0", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "falkordb-client-rs" +version = "0.1.0" +dependencies = [ + "anyhow", + "log", + "parking_lot", + "redis", + "simple_logger", + "thiserror", + "tokio", + "url-parse", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "pin-utils", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "2.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "openssl" +version = "0.10.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "parking_lot" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "backtrace", + "cfg-if", + "libc", + "petgraph", + "redox_syscall", + "smallvec", + "thread-id", + "windows-targets 0.52.5", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "petgraph" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redis" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6472825949c09872e8f2c50bde59fcefc17748b6be5c90fd67cd8b4daca73bfd" +dependencies = [ + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "native-tls", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "rustls-pemfile", + "rustls-pki-types", + "ryu", + "tokio", + "tokio-native-tls", + "tokio-rustls", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469052894dcb553421e483e4209ee581a45100d31b4018de03e5a7ad86374a7e" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4ef73721ac7bcd79b2b315da7779d8fc09718c6b3d2d1b2d94850eb8c18432" +dependencies = [ + "log", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" + +[[package]] +name = "rustls-webpki" +version = "0.102.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3bce581c0dd41bce533ce695a1437fa16a7ab5ac3ccfa99fe1a620a7885eabf" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "security-framework" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c627723fd09706bacdb5cf41499e95098555af3c3c29d014dc3c458ef6be11c0" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317936bbbd05227752583946b9e66d7ce3b489f84e11a94a510b4437fef407d7" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.202" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "simple_logger" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb" +dependencies = [ + "colored", + "log", + "time", + "windows-sys 0.48.0", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" + +[[package]] +name = "syn" +version = "2.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys 0.52.0", +] + +[[package]] +name = "thiserror" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread-id" +version = "4.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0ec81c46e9eb50deaa257be2f148adf052d1fb7701cfd55ccfab2525280b70b" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" +dependencies = [ + "rustls", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "url-parse" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "865ece61c15cae30f180636ae551daa25c318c181938da07f3ab3ed06750bdd2" +dependencies = [ + "regex", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f7d245f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "falkordb-client-rs" +version = "0.1.0" +edition = "2021" + + +[dependencies] +anyhow = { version = "1.0.83", default-features = false, features = ["std"] } +log = { version = "0.4.21", default-features = false } +parking_lot = { version = "0.12.2", default-features = false, features = ["deadlock_detection"] } +redis = { version = "0.25.3", default-features = false, optional = true } +tokio = { version = "1.37.0", default-features = false, features = ["rt-multi-thread", "sync"], optional = true } +thiserror = "1.0.60" +url-parse = "1.0.8" +simple_logger = "5.0.0" + +[features] +default = ["redis", "tokio"] +native-tls = ["redis/tls-native-tls"] +rustls = ["redis/tls-rustls"] +tokio = ["dep:tokio", "redis/tokio-comp"] +tokio-native-tls = ["redis/tokio-native-tls-comp"] +tokio-rustls = ["redis/tokio-rustls-comp"] +redis = ["dep:redis"] \ No newline at end of file diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs new file mode 100644 index 0000000..9a188b4 --- /dev/null +++ b/src/client/asynchronous.rs @@ -0,0 +1,28 @@ +use crate::client::FalkorClientImpl; +use crate::connection::asynchronous::FalkorAsyncConnection; +use anyhow::Result; +use std::collections::VecDeque; +use tokio::runtime::Runtime; +use tokio::sync::Mutex; + +pub(crate) struct AsyncFalkorClient { + _inner: FalkorClientImpl, + num_connections: u8, + connection_pool: Mutex>, + runtime: Runtime, +} + +impl AsyncFalkorClient { + pub(crate) async fn create( + client: FalkorClientImpl, + num_connections: u8, + runtime: Runtime, + ) -> Result { + Ok(Self { + _inner: client, + num_connections, + connection_pool: Default::default(), + runtime, + }) + } +} diff --git a/src/client/blocking.rs b/src/client/blocking.rs new file mode 100644 index 0000000..4efbeb2 --- /dev/null +++ b/src/client/blocking.rs @@ -0,0 +1,152 @@ +use crate::client::FalkorClientImpl; +use crate::connection::blocking::{BorrowedSyncConnectionGuard, FalkorSyncConnection}; +use crate::error::FalkorDBError; +use crate::graph::blocking::SyncGraph; +use crate::value::config::ConfigValue; +use crate::value::FalkorValue; +use anyhow::Result; +use std::collections::HashMap; +use std::sync::mpsc; + +pub(crate) struct SyncFalkorClient { + _inner: FalkorClientImpl, + num_connections: u8, + connection_pool_tx: mpsc::SyncSender, + connection_pool_rx: mpsc::Receiver, +} + +impl SyncFalkorClient { + pub(crate) fn create(client: FalkorClientImpl, num_connections: u8) -> Result { + let (connection_pool_tx, connection_pool_rx) = mpsc::sync_channel(num_connections as usize); + for _ in 0..num_connections { + connection_pool_tx.send(client.get_connection(None)?)?; + } + + Ok(Self { + _inner: client, + num_connections, + connection_pool_tx, + connection_pool_rx, + }) + } + + pub(crate) fn borrow_connection(&self) -> Result { + Ok(BorrowedSyncConnectionGuard { + return_tx: self.connection_pool_tx.clone(), + conn: Some(self.connection_pool_rx.recv()?), + }) + } + + pub fn list_graphs(&self) -> Result> { + let mut conn = self.borrow_connection()?; + + let graph_list = match conn.as_inner()? { + #[cfg(feature = "redis")] + FalkorSyncConnection::Redis(redis_conn) => { + use redis::ConnectionLike as _; + let res = match redis_conn.req_command(&redis::cmd("GRAPH.LIST"))? { + redis::Value::Bulk(data) => data, + _ => { + return Err(FalkorDBError::InvalidDataReceived.into()); + } + }; + + res.iter() + .filter_map(|element| { + if let redis::Value::Data(v) = element { + Some(String::from_utf8_lossy(v.as_slice()).to_string()) + } else { + None + } + }) + .collect() + } + }; + + Ok(graph_list) + } + + /// This function returns either an [`FVec`] containing the requested key and val, + /// or an [`FVec`] of [`FVec`]s, each one containing a key and val pair + pub fn config_get>( + &self, + config_key: T, + ) -> Result> { + let mut conn = self.borrow_connection()?; + + Ok(match conn.as_inner()? { + #[cfg(feature = "redis")] + FalkorSyncConnection::Redis(redis_conn) => { + use redis::ConnectionLike as _; + + let bulk_data = match redis_conn + .req_command(redis::cmd("GRAPH.CONFIG").arg("GET").arg(config_key.into()))? + { + redis::Value::Bulk(bulk_data) => bulk_data, + _ => return Err(FalkorDBError::InvalidDataReceived.into()), + }; + + if bulk_data.is_empty() { + return Err(FalkorDBError::InvalidDataReceived.into()); + } else if bulk_data.len() == 2 { + return if let Some(redis::Value::Status(config_key)) = bulk_data.first() { + Ok(HashMap::from([( + config_key.to_string(), + ConfigValue::try_from(&bulk_data[1])?, + )])) + } else { + Err(FalkorDBError::InvalidDataReceived.into()) + }; + } + + bulk_data + .into_iter() + .flat_map(redis::Value::into_map_iter) + .fold(HashMap::new(), |mut acc, it| { + acc.extend(it.flat_map(|(key, val)| match key { + redis::Value::Status(config_key) => { + Ok((config_key, ConfigValue::try_from(&val)?)) + } + redis::Value::Data(config_key) => Ok(( + String::from_utf8_lossy(config_key.as_slice()).to_string(), + ConfigValue::try_from(&val)?, + )), + _ => Err(Into::::into( + FalkorDBError::InvalidDataReceived, + )), + })); + acc + }) + } + }) + } + + pub fn config_set, C: Into>( + &self, + config_key: T, + value: C, + ) -> Result<()> { + let mut conn = self.borrow_connection()?; + match conn.as_inner()? { + #[cfg(feature = "redis")] + FalkorSyncConnection::Redis(redis_conn) => { + use redis::ConnectionLike as _; + redis_conn.req_command( + redis::cmd("GRAPH.CONFIG") + .arg("SET") + .arg(config_key.into()) + .arg(value.into()), + )?; + } + } + + Ok(()) + } + + pub fn open_graph(&self, graph_name: T) -> SyncGraph { + SyncGraph { + client: self, + graph_name: graph_name.to_string(), + } + } +} diff --git a/src/client/builder.rs b/src/client/builder.rs new file mode 100644 index 0000000..64e10dd --- /dev/null +++ b/src/client/builder.rs @@ -0,0 +1,117 @@ +use crate::client::asynchronous::AsyncFalkorClient; +use crate::client::blocking::SyncFalkorClient; +use crate::client::FalkorClientImpl; +use crate::connection_info::FalkorConnectionInfo; +use crate::error::FalkorDBError; +use anyhow::Result; + +// This doesn't have a default implementation because a specific const char is required +// and I don't want to leave that up to the user +pub struct FalkorDBClientBuilder { + connection_info: Option, + num_connections: u8, + multithreaded_rt: bool, + #[cfg(feature = "tokio")] + runtime: Option, +} + +impl FalkorDBClientBuilder { + pub fn with_connection_info(self, falkor_connection_info: FalkorConnectionInfo) -> Self { + Self { + connection_info: Some(falkor_connection_info), + ..self + } + } + + pub fn with_num_connections(self, num_connections: u8) -> Self { + Self { + num_connections, + ..self + } + } +} + +fn get_client>(connection_info: T) -> Result +where + anyhow::Error: From, +{ + let connection_info = connection_info.try_into()?; + Ok(match connection_info { + FalkorConnectionInfo::Redis(connection_info) => { + FalkorClientImpl::Redis(redis::Client::open(connection_info.clone())?) + } + }) +} + +impl FalkorDBClientBuilder<'S'> { + pub fn new() -> Self { + FalkorDBClientBuilder { + connection_info: None, + num_connections: 4, + multithreaded_rt: false, + #[cfg(feature = "tokio")] + runtime: None, + } + } + + pub fn build(self) -> Result { + if self.num_connections < 1 || self.num_connections > 32 { + return Err(FalkorDBError::InvalidConnectionPoolSize.into()); + } + + let connection_info = self + .connection_info + .unwrap_or("falkor://127.0.0.1:6379".try_into()?); + + SyncFalkorClient::create(get_client(connection_info.clone())?, self.num_connections) + } +} + +#[cfg(feature = "tokio")] +impl FalkorDBClientBuilder<'A'> { + pub fn new_async() -> Self { + FalkorDBClientBuilder { + connection_info: None, + num_connections: 4, + multithreaded_rt: false, + #[cfg(feature = "tokio")] + runtime: None, + } + } + + pub fn with_multithreaded_runtime(self) -> Self { + Self { + multithreaded_rt: true, + ..self + } + } + + /// This overrides with_multithreaded_runtime() + pub fn with_runtime(self, runtime: tokio::runtime::Runtime) -> Self { + Self { + runtime: Some(runtime), + ..self + } + } + + pub async fn build(self) -> Result { + if self.num_connections < 1 || self.num_connections > 32 { + return Err(FalkorDBError::InvalidConnectionPoolSize.into()); + } + + let connection_info = self + .connection_info + .unwrap_or("falkor://127.0.0.1:6379".try_into()?); + + let runtime = self.runtime.unwrap_or( + if self.multithreaded_rt { + tokio::runtime::Builder::new_multi_thread() + } else { + tokio::runtime::Builder::new_current_thread() + } + .build()?, + ); + + AsyncFalkorClient::create(get_client(connection_info)?, self.num_connections, runtime).await + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..94670e5 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,47 @@ +use crate::connection::asynchronous::FalkorAsyncConnection; +use crate::connection::blocking::FalkorSyncConnection; +use anyhow::Result; +use std::time::Duration; + +#[cfg(feature = "tokio")] +pub mod asynchronous; +pub mod blocking; +pub mod builder; + +pub(crate) enum FalkorClientImpl { + #[cfg(feature = "redis")] + Redis(redis::Client), +} + +impl FalkorClientImpl { + pub(crate) fn get_connection( + &self, + connection_timeout: Option, + ) -> Result { + Ok(match self { + #[cfg(feature = "redis")] + FalkorClientImpl::Redis(redis_client) => connection_timeout + .map(|timeout| redis_client.get_connection_with_timeout(timeout)) + .unwrap_or_else(|| redis_client.get_connection())? + .into(), + }) + } + + #[cfg(feature = "tokio")] + pub(crate) async fn get_async_connection( + &self, + connection_timeout: Option, + ) -> Result { + Ok(match self { + FalkorClientImpl::Redis(redis_client) => match connection_timeout { + Some(timeout) => { + redis_client + .get_multiplexed_tokio_connection_with_response_timeouts(timeout, timeout) + .await? + } + None => redis_client.get_multiplexed_tokio_connection().await?, + } + .into(), + }) + } +} diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs new file mode 100644 index 0000000..4b73a79 --- /dev/null +++ b/src/connection/asynchronous.rs @@ -0,0 +1,29 @@ +use crate::error::FalkorDBError; +use anyhow::Result; +use tokio::sync::mpsc; + +pub enum FalkorAsyncConnection { + #[cfg(feature = "redis")] + Redis(redis::aio::MultiplexedConnection), +} + +pub(crate) struct BorrowedAsyncConnectionGuard { + conn: Option, + return_tx: mpsc::Sender, +} + +impl BorrowedAsyncConnectionGuard { + pub(crate) fn as_inner(&mut self) -> Result<&mut FalkorAsyncConnection> { + self.conn + .as_mut() + .ok_or(FalkorDBError::EmptyConnection.into()) + } +} + +impl Drop for BorrowedAsyncConnectionGuard { + fn drop(&mut self) { + if let Some(conn) = self.conn.take() { + self.return_tx.blocking_send(conn).ok(); + } + } +} diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs new file mode 100644 index 0000000..c8895b8 --- /dev/null +++ b/src/connection/blocking.rs @@ -0,0 +1,28 @@ +use crate::error::FalkorDBError; +use std::sync::mpsc; + +pub enum FalkorSyncConnection { + #[cfg(feature = "redis")] + Redis(redis::Connection), +} + +pub(crate) struct BorrowedSyncConnectionGuard { + pub(crate) conn: Option, + pub(crate) return_tx: mpsc::SyncSender, +} + +impl BorrowedSyncConnectionGuard { + pub(crate) fn as_inner(&mut self) -> anyhow::Result<&mut FalkorSyncConnection> { + self.conn + .as_mut() + .ok_or(FalkorDBError::EmptyConnection.into()) + } +} + +impl Drop for BorrowedSyncConnectionGuard { + fn drop(&mut self) { + if let Some(conn) = self.conn.take() { + self.return_tx.send(conn).ok(); + } + } +} diff --git a/src/connection/mod.rs b/src/connection/mod.rs new file mode 100644 index 0000000..89d8b0e --- /dev/null +++ b/src/connection/mod.rs @@ -0,0 +1,3 @@ +#[cfg(feature = "tokio")] +pub mod asynchronous; +pub mod blocking; diff --git a/src/connection_info/mod.rs b/src/connection_info/mod.rs new file mode 100644 index 0000000..5363a55 --- /dev/null +++ b/src/connection_info/mod.rs @@ -0,0 +1,75 @@ +use anyhow::Result; + +#[derive(Clone)] +pub(crate) enum FalkorConnectionInfo { + #[cfg(feature = "redis")] + Redis(redis::ConnectionInfo), +} + +impl FalkorConnectionInfo { + /// Redis is currently only option, and I feel will be the default for a while + /// So any new option should be set here should be added before redis + /// as a #[cfg(and(feature = , not(feature = "redis"))] + fn fallback_provider(full_url: String) -> Result { + #[cfg(feature = "redis")] + Ok(FalkorConnectionInfo::Redis( + redis::IntoConnectionInfo::into_connection_info(format!("redis://{full_url}"))?, + )) + } +} + +impl TryFrom<&str> for FalkorConnectionInfo { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + let url = url_parse::core::Parser::new(None).parse(value)?; + + let scheme = url.scheme.unwrap_or("falkor".to_string()); + let addr = url.domain.unwrap_or("127.0.0.1".to_string()); + let port = url.port.unwrap_or(6379); // Might need to change in accordance with the default fallback + + let user_pass_string = match url.user_pass { + (Some(pass), None) => format!("{}@", pass), // Password-only authentication is allowed in legacy auth + (Some(user), Some(pass)) => format!("{user}:{pass}@"), + _ => "".to_string(), + }; + + match scheme.as_str() { + "redis" | "rediss" => { + #[cfg(feature = "redis")] + return Ok(FalkorConnectionInfo::Redis( + redis::IntoConnectionInfo::into_connection_info(format!( + "{}://{}{}:{}", + scheme, user_pass_string, addr, port + ))?, + )); + #[cfg(not(feature = "redis"))] + return Err(FalkorDBError::UnavailableProvider.into()); + } + _ => FalkorConnectionInfo::fallback_provider(format!( + "{}{}:{}", + user_pass_string, addr, port + )), + } + } +} + +// Calls TryFrom<&str> +impl TryFrom for FalkorConnectionInfo { + type Error = anyhow::Error; + + #[inline] + fn try_from(value: String) -> Result { + Self::try_from(value.as_str()) + } +} + +// Calls TryFrom +impl TryFrom<(T, u16)> for FalkorConnectionInfo { + type Error = anyhow::Error; + + #[inline] + fn try_from(value: (T, u16)) -> Result { + Self::try_from(format!("{}:{}", value.0.to_string(), value.1)) + } +} diff --git a/src/error/mod.rs b/src/error/mod.rs new file mode 100644 index 0000000..070ccf3 --- /dev/null +++ b/src/error/mod.rs @@ -0,0 +1,15 @@ +#[derive(thiserror::Error, Debug)] +pub enum FalkorDBError { + #[error("The provided connection info is invalid")] + InvalidConnectionInfo, + #[error("The connection returned invalid data for this command")] + InvalidDataReceived, + #[error("The provided URL scheme points at a database provider that is currently unavailable, make sure the correct feature is enabled")] + UnavailableProvider, + #[error("The number of connections for the client has to be between 1 and 32")] + InvalidConnectionPoolSize, + #[error("Attempting to use an empty connection object")] + EmptyConnection, + #[error("Parsing error due to invalid types, or argument count")] + ParsingError, +} diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs new file mode 100644 index 0000000..0a08366 --- /dev/null +++ b/src/graph/asynchronous.rs @@ -0,0 +1,5 @@ +use crate::client::asynchronous::AsyncFalkorClient; + +pub struct AsyncGraph<'a> { + pub(crate) client: &'a AsyncFalkorClient, +} diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs new file mode 100644 index 0000000..ec8e01d --- /dev/null +++ b/src/graph/blocking.rs @@ -0,0 +1,78 @@ +use crate::client::blocking::SyncFalkorClient; +use crate::connection::blocking::FalkorSyncConnection; +use crate::value::slowlog_entry::SlowlogEntry; +use crate::value::FalkorValue; +use anyhow::Result; +use redis::ConnectionLike; + +pub struct SyncGraph<'a> { + pub(crate) client: &'a SyncFalkorClient, + pub(crate) graph_name: String, +} + +impl SyncGraph<'_> { + pub fn graph_name(&self) -> &str { + self.graph_name.as_str() + } + + fn send_command(&self, command: &str, args: &[FalkorValue]) -> Result { + let mut conn = self.client.borrow_connection()?; + Ok(match conn.as_inner()? { + #[cfg(feature = "redis")] + FalkorSyncConnection::Redis(redis_conn) => { + let mut cmd = redis::cmd(command); + cmd.arg(self.graph_name.as_str()); + for arg in args { + let _ = cmd.arg(arg); + } + redis::FromRedisValue::from_owned_redis_value(redis_conn.req_command(&cmd)?)? + } + }) + } + + pub fn copy(&self, cloned_graph_name: T) -> Result { + self.send_command("GRAPH.COPY", &[cloned_graph_name.to_string().into()])?; + Ok(self.client.open_graph(cloned_graph_name)) + } + + pub fn delete(&self) -> Result<()> { + self.send_command("GRAPH.DELETE", &[self.graph_name.as_str().into()])?; + Ok(()) + } + + pub fn slowlog(&self) -> Result> { + let res = self.send_command("GRAPH.SLOWLOG", &[])?.into_vec()?; + + if res.is_empty() { + return Ok(vec![]); + } + + Ok(res + .into_iter() + .flat_map(FalkorValue::into_vec) + .flat_map(TryInto::<[FalkorValue; 4]>::try_into) + .flat_map(SlowlogEntry::from_value_array) + .collect::>()) + } + + pub fn slowlog_reset(&self) -> Result<()> { + self.send_command("GRAPH.SLOWLOG", &["RESET".into()])?; + Ok(()) + } + + pub fn profile>(&self, args: T) -> Result { + self.send_command("GRAPH.PROFILE", &[args.into()]) + } + + pub fn explain>(&self, args: T) -> Result { + self.send_command("GRAPH.EXPLAIN", &[args.into()]) + } + + pub fn query>(&self, args: T) -> Result { + self.send_command("GRAPH.QUERY", &[args.into(), "--compact".into()]) + } + + pub fn query_readonly>(&self, args: T) -> Result { + self.send_command("GRAPH.RO_QUERY", &[args.into(), "--compact".into()]) + } +} diff --git a/src/graph/mod.rs b/src/graph/mod.rs new file mode 100644 index 0000000..89d8b0e --- /dev/null +++ b/src/graph/mod.rs @@ -0,0 +1,3 @@ +#[cfg(feature = "tokio")] +pub mod asynchronous; +pub mod blocking; diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b16baa3 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,116 @@ +#[cfg(not(feature = "redis"))] +compile_error!("The `redis` feature must be enabled."); + +mod client; +mod connection; +mod connection_info; +mod error; +mod graph; +mod value; + +#[cfg(feature = "redis")] +mod redis_ext; + +// Basic tests for now, to be removed in the future and moved to their respective places, currently requires the server to already be up +#[cfg(test)] +mod tests { + use crate::client::blocking::SyncFalkorClient; + use crate::client::builder::FalkorDBClientBuilder; + use crate::value::config::ConfigValue; + + fn create_client() -> SyncFalkorClient { + FalkorDBClientBuilder::new() + .with_num_connections(4) + .build() + .expect("Could not create falkor client") + } + + #[test] + fn test_config_get_set() { + let client = create_client(); + + let res = client.config_get("CMD_INFO"); + assert!(res.is_ok()); + assert_eq!(res.unwrap()["CMD_INFO"], ConfigValue::Int64(1)); + + assert!(client.config_set("CMD_INFO", "no").is_ok()); + + let res = client.config_get("CMD_INFO"); + assert!(res.is_ok()); + assert_eq!(res.unwrap()["CMD_INFO"], ConfigValue::Int64(0)); + } + + #[test] + fn test_config_get_all() { + let client = create_client(); + + let res = client.config_get("*"); + assert!(res.is_ok()); + + // Add some more here, maybe even test all values, but make sure it stays BC + assert_eq!( + res.as_ref().unwrap()["EFFECTS_THRESHOLD"], + ConfigValue::Int64(300) + ); + assert_eq!( + res.as_ref().unwrap()["MAX_QUEUED_QUERIES"], + ConfigValue::Int64(4294967295) + ); + } + + // #[test] + // fn test_graph_query() { + // let client = create_client(); + // let graph = client.open_graph("imdb"); + // + // let res = graph.query( + // "MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100", + // ); + // assert!(res.is_ok()); + // + // let res = res.unwrap().into_vec(); + // assert!(res.is_ok()); + // + // let res = res.unwrap(); + // assert_eq!(res.len(), 3); + // + // let res = res[2].clone().into_vec(); + // assert!(res.is_ok()); + // + // assert_eq!(res.unwrap().len(), 100); + // } + + // Race condition here with slowlog_reset test, So ignore this, will be fixed with multi-instance testing + #[test] + #[ignore] + fn test_graph_slowlog() { + let client = create_client(); + let graph = client.open_graph("imdb"); + + assert!(graph.query( + "MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 2", + ).is_ok()); + + let res = graph.slowlog(); + assert!(res.is_ok()); + + assert_eq!(res.unwrap()[0].arguments, "MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 2".to_string()); + } + + #[test] + fn test_graph_slowlog_reset() { + let client = create_client(); + let graph = client.open_graph("imdb"); + + assert!(graph.query( + "MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 10", + ).is_ok()); + + assert!(graph.slowlog_reset().is_ok()); + + let res = graph.slowlog(); + assert!(res.is_ok()); + + assert!(res.unwrap().is_empty()); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3814222 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,32 @@ +use anyhow::Result; + +mod client; +mod connection; +mod connection_info; +mod error; +mod graph; +mod value; + +#[cfg(feature = "redis")] +mod redis_ext; + +fn main() -> Result<()> { + simple_logger::init_utc().expect("Could not start logger"); + + let client = client::builder::FalkorDBClientBuilder::new() + .with_num_connections(4) + .build()?; + + client.config_set("CMD_INFO", "no")?; + let res = client.config_get("*")?; + + let graph = client.open_graph("imdb"); + + // let res = graph.query( + // "MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 10", + // )?; + let res = graph.slowlog()?; + log::info!("{res:?}"); + + Ok(()) +} diff --git a/src/redis_ext.rs b/src/redis_ext.rs new file mode 100644 index 0000000..5bdd7f1 --- /dev/null +++ b/src/redis_ext.rs @@ -0,0 +1,113 @@ +use crate::client::FalkorClientImpl; +use crate::connection::{asynchronous::FalkorAsyncConnection, blocking::FalkorSyncConnection}; +use crate::connection_info::FalkorConnectionInfo; +use crate::error::FalkorDBError; +use crate::value::config::ConfigValue; +use crate::value::FalkorValue; +use redis::{FromRedisValue, RedisResult, RedisWrite, ToRedisArgs}; + +impl From for FalkorSyncConnection { + fn from(value: redis::Connection) -> Self { + Self::Redis(value) + } +} + +#[cfg(feature = "tokio")] +impl From for FalkorAsyncConnection { + fn from(value: redis::aio::MultiplexedConnection) -> Self { + Self::Redis(value) + } +} + +impl From for FalkorConnectionInfo { + fn from(value: redis::ConnectionInfo) -> Self { + Self::Redis(value) + } +} + +impl From for FalkorClientImpl { + fn from(value: redis::Client) -> Self { + Self::Redis(value) + } +} + +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 ToRedisArgs for FalkorValue { + fn write_redis_args(&self, out: &mut W) + where + W: ?Sized + RedisWrite, + { + match self { + FalkorValue::None => {} + FalkorValue::Int64(int_data) => int_data.write_redis_args(out), + FalkorValue::UInt64(uint_data) => uint_data.write_redis_args(out), + FalkorValue::F64(f_data) => f_data.write_redis_args(out), + FalkorValue::FString(str_data) => str_data.write_redis_args(out), + FalkorValue::FVec(bulk_data) => { + ToRedisArgs::write_args_from_slice(bulk_data.as_slice(), out) + } + } + } + + fn describe_numeric_behavior(&self) -> redis::NumericBehavior { + match self { + FalkorValue::Int64(_) => redis::NumericBehavior::NumberIsInteger, + FalkorValue::UInt64(_) => redis::NumericBehavior::NumberIsInteger, + FalkorValue::F64(_) => redis::NumericBehavior::NumberIsFloat, + _ => redis::NumericBehavior::NonNumeric, + } + } + + fn is_single_arg(&self) -> bool { + match self { + FalkorValue::None => false, + FalkorValue::FVec(bulk_data) => bulk_data.is_single_arg(), + _ => true, + } + } +} + +impl TryFrom<&redis::Value> for ConfigValue { + type Error = anyhow::Error; + 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.into()), + }) + } +} + +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::Int64(*int_val), + redis::Value::Data(str_val) => { + FalkorValue::FString(String::from_utf8_lossy(str_val.as_slice()).to_string()) + } + redis::Value::Bulk(bulk) => FalkorValue::FVec({ + let mut new_vec = Vec::with_capacity(bulk.len()); + for element in bulk { + new_vec.push(FalkorValue::from_redis_value(element)?); + } + new_vec + }), + redis::Value::Status(status) => FalkorValue::FString(status.to_string()), + redis::Value::Okay => FalkorValue::None, + }) + } +} diff --git a/src/value/config.rs b/src/value/config.rs new file mode 100644 index 0000000..7e89edc --- /dev/null +++ b/src/value/config.rs @@ -0,0 +1,34 @@ +use std::fmt::{Display, Formatter}; + +#[derive(Clone, Debug, PartialEq)] +pub enum ConfigValue { + String(String), + Int64(i64), +} + +impl Display for ConfigValue { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + ConfigValue::String(str_val) => str_val.fmt(f), + ConfigValue::Int64(int_val) => int_val.fmt(f), + } + } +} + +impl From for ConfigValue { + fn from(value: i64) -> Self { + ConfigValue::Int64(value) + } +} + +impl From for ConfigValue { + fn from(value: String) -> Self { + ConfigValue::String(value) + } +} + +impl From<&str> for ConfigValue { + fn from(value: &str) -> Self { + ConfigValue::String(value.to_string()) + } +} diff --git a/src/value/mod.rs b/src/value/mod.rs new file mode 100644 index 0000000..1c10746 --- /dev/null +++ b/src/value/mod.rs @@ -0,0 +1,176 @@ +use crate::error::FalkorDBError; +use anyhow::Result; +use std::fmt::{Debug, Display, Formatter}; + +pub mod config; +mod query_result; +pub mod slowlog_entry; + +#[derive(Clone, Debug)] +pub enum FalkorValue { + None, + Int64(i64), + UInt64(u64), + F64(f64), + FString(String), + FVec(Vec), +} + +macro_rules! impl_to_falkordb_value { + ($t:ty, $falkordbtype:expr) => { + impl From<$t> for FalkorValue { + fn from(value: $t) -> Self { + $falkordbtype(value as _) + } + } + }; +} + +impl_to_falkordb_value!(i8, Self::Int64); +impl_to_falkordb_value!(i32, Self::Int64); +impl_to_falkordb_value!(i64, Self::Int64); + +impl_to_falkordb_value!(u8, Self::UInt64); +impl_to_falkordb_value!(u32, Self::UInt64); +impl_to_falkordb_value!(u64, Self::UInt64); + +impl_to_falkordb_value!(f32, Self::F64); +impl_to_falkordb_value!(f64, Self::F64); + +impl Display for FalkorValue { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FalkorValue::None => Ok(()), + FalkorValue::Int64(val) => Display::fmt(val, f), + FalkorValue::UInt64(val) => Display::fmt(val, f), + FalkorValue::F64(val) => Display::fmt(val, f), + FalkorValue::FString(val) => f.write_fmt(format_args!("\"{val}\"")), + FalkorValue::FVec(val) => { + f.write_str("[")?; + let val_len = val.len() - 1; + for (idx, element) in val.iter().enumerate() { + Display::fmt(element, f)?; + if idx < val_len { + f.write_str(",")?; + } + } + f.write_str("]") + } + } + } +} + +impl TryFrom<&FalkorValue> for i64 { + type Error = FalkorDBError; + + fn try_from(value: &FalkorValue) -> Result { + match value { + FalkorValue::Int64(val) => Some(*val), + FalkorValue::UInt64(val) => (*val).try_into().ok(), + FalkorValue::FString(val) => val.as_str().parse().ok(), + _ => None, + } + .ok_or(FalkorDBError::ParsingError) + } +} + +impl TryFrom for i64 { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::Int64(val) => Some(val), + FalkorValue::UInt64(val) => val.try_into().ok(), + FalkorValue::FString(val) => val.as_str().parse().ok(), + _ => None, + } + .ok_or(FalkorDBError::ParsingError) + } +} + +impl TryFrom<&FalkorValue> for u64 { + type Error = FalkorDBError; + + fn try_from(value: &FalkorValue) -> Result { + match value { + FalkorValue::UInt64(val) => Some(*val), + FalkorValue::Int64(val) => (*val).try_into().ok(), + FalkorValue::FString(val) => val.as_str().parse().ok(), + _ => None, + } + .ok_or(FalkorDBError::ParsingError) + } +} + +impl TryFrom for u64 { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::UInt64(val) => Some(val), + FalkorValue::Int64(val) => val.try_into().ok(), + FalkorValue::FString(val) => val.as_str().parse().ok(), + _ => None, + } + .ok_or(FalkorDBError::ParsingError) + } +} + +impl FalkorValue { + pub fn as_vec(&self) -> Result<&Vec> { + match self { + FalkorValue::FVec(val) => Some(val), + _ => None, + } + .ok_or(FalkorDBError::ParsingError.into()) + } + pub fn into_vec(self) -> Result> { + match self { + FalkorValue::FVec(val) => Some(val), + _ => None, + } + .ok_or(FalkorDBError::ParsingError.into()) + } + + pub fn as_string(&self) -> Result<&String> { + match self { + FalkorValue::FString(val) => Some(val), + _ => None, + } + .ok_or(FalkorDBError::ParsingError.into()) + } + + pub fn into_string(self) -> Result { + match self { + FalkorValue::FString(val) => Some(val), + _ => None, + } + .ok_or(FalkorDBError::ParsingError.into()) + } +} + +impl From for FalkorValue { + fn from(value: String) -> Self { + Self::FString(value) + } +} + +impl From<&str> for FalkorValue { + fn from(value: &str) -> Self { + Self::FString(value.to_string()) + } +} + +impl From> for FalkorValue +where + FalkorValue: From, +{ + fn from(value: Vec) -> Self { + Self::FVec( + value + .into_iter() + .map(|element| FalkorValue::from(element)) + .collect(), + ) + } +} diff --git a/src/value/query_result.rs b/src/value/query_result.rs new file mode 100644 index 0000000..cb60518 --- /dev/null +++ b/src/value/query_result.rs @@ -0,0 +1,87 @@ +use crate::error::FalkorDBError; +use crate::value::FalkorValue; +use anyhow::Result; + +#[derive(Default)] +pub struct QueryResult { + raw_stats: Vec, + header: Vec, + result_set: Vec, +} + +fn filter_stat_only_response(res: &[FalkorValue]) -> Result> { + Ok( + match match res.len() { + 0 => return Err(FalkorDBError::InvalidDataReceived.into()), + 1 => res.first(), + _ => res.last(), + } { + Some(FalkorValue::FVec(val)) => val.clone(), + _ => return Err(FalkorDBError::InvalidDataReceived.into()), + }, + ) +} + +impl QueryResult { + pub fn from_result(res: FalkorValue) -> Result { + let res = res.into_vec()?; + if res.is_empty() { + return Err(FalkorDBError::InvalidDataReceived.into()); + } + + match res.len() { + 1 => res.first(), + _ => res.last(), + }; + + let header = filter_stat_only_response(res.as_slice())?; + if header.is_empty() { + return Ok(Default::default()); + } + + // let records = res.into_iter().enumerate().skip(1).fold(Vec::with_capacity()) { + // + // } + + // match res.len() { + // 0 => { + // return Err(FalkorDBError::InvalidDataReceived.into()); + // } + // 1 => { + // return Ok(Self { + // raw_stats: match res.first() { + // Some(FalkorValue::FString(str_val)) => { + // panic!("{str_val}"); // Want to catch errors here before release, in case this is actually a possibility + // } + // Some(FalkorValue::FVec(vec_val)) => vec_val.clone(), + // _ => return Err(FalkorDBError::InvalidDataReceived.into()), + // }, + // header: vec![], + // result_set: vec![], + // }); + // } + // _ => { + // let mut new_result = Self { + // raw_stats: vec![], + // header: match res.first() { + // Some(FalkorValue::FVec(vec_val)) => { + // if vec_val.is_empty() { + // return Ok(Default::default()); + // } + // + // vec_val.clone() + // } + // _ => { + // return Err(FalkorDBError::InvalidDataReceived); + // } + // }, + // result_set: vec![], + // }; + // } + // } + + Err(FalkorDBError::InvalidDataReceived.into()) + + // Ok(new_result) + } +} diff --git a/src/value/slowlog_entry.rs b/src/value/slowlog_entry.rs new file mode 100644 index 0000000..81be311 --- /dev/null +++ b/src/value/slowlog_entry.rs @@ -0,0 +1,22 @@ +use crate::value::FalkorValue; +use anyhow::Result; + +#[derive(Clone, Debug)] +pub struct SlowlogEntry { + pub timestamp: u64, + pub command: String, + pub arguments: String, + pub time_taken: u64, +} + +impl SlowlogEntry { + pub fn from_value_array(values: [FalkorValue; 4]) -> Result { + let [timestamp, command, arguments, time_taken] = values; + Ok(Self { + timestamp: timestamp.try_into()?, + command: command.into_string()?, + arguments: arguments.into_string()?, + time_taken: time_taken.try_into()?, + }) + } +} From 1005d127bec0a4206ca3c11cbabebfad3ca9db3a Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Sun, 19 May 2024 17:26:03 +0300 Subject: [PATCH 02/62] Add licensing, delete main rs --- src/client/asynchronous.rs | 5 +++++ src/client/blocking.rs | 5 +++++ src/client/builder.rs | 5 +++++ src/client/mod.rs | 5 +++++ src/connection/asynchronous.rs | 5 +++++ src/connection/blocking.rs | 5 +++++ src/connection/mod.rs | 5 +++++ src/connection_info/mod.rs | 5 +++++ src/error/mod.rs | 5 +++++ src/graph/asynchronous.rs | 5 +++++ src/graph/blocking.rs | 5 +++++ src/graph/mod.rs | 5 +++++ src/lib.rs | 5 +++++ src/main.rs | 32 -------------------------------- src/redis_ext.rs | 5 +++++ src/value/config.rs | 5 +++++ src/value/mod.rs | 5 +++++ src/value/query_result.rs | 5 +++++ src/value/slowlog_entry.rs | 5 +++++ 19 files changed, 90 insertions(+), 32 deletions(-) delete mode 100644 src/main.rs diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index 9a188b4..d8a4b91 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use crate::client::FalkorClientImpl; use crate::connection::asynchronous::FalkorAsyncConnection; use anyhow::Result; diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 4efbeb2..726a729 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use crate::client::FalkorClientImpl; use crate::connection::blocking::{BorrowedSyncConnectionGuard, FalkorSyncConnection}; use crate::error::FalkorDBError; diff --git a/src/client/builder.rs b/src/client/builder.rs index 64e10dd..3eb521c 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use crate::client::asynchronous::AsyncFalkorClient; use crate::client::blocking::SyncFalkorClient; use crate::client::FalkorClientImpl; diff --git a/src/client/mod.rs b/src/client/mod.rs index 94670e5..611389b 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use crate::connection::asynchronous::FalkorAsyncConnection; use crate::connection::blocking::FalkorSyncConnection; use anyhow::Result; diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs index 4b73a79..5adcd7f 100644 --- a/src/connection/asynchronous.rs +++ b/src/connection/asynchronous.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use crate::error::FalkorDBError; use anyhow::Result; use tokio::sync::mpsc; diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index c8895b8..4729d0d 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use crate::error::FalkorDBError; use std::sync::mpsc; diff --git a/src/connection/mod.rs b/src/connection/mod.rs index 89d8b0e..a6d5b94 100644 --- a/src/connection/mod.rs +++ b/src/connection/mod.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + #[cfg(feature = "tokio")] pub mod asynchronous; pub mod blocking; diff --git a/src/connection_info/mod.rs b/src/connection_info/mod.rs index 5363a55..430a616 100644 --- a/src/connection_info/mod.rs +++ b/src/connection_info/mod.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use anyhow::Result; #[derive(Clone)] diff --git a/src/error/mod.rs b/src/error/mod.rs index 070ccf3..356ec09 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + #[derive(thiserror::Error, Debug)] pub enum FalkorDBError { #[error("The provided connection info is invalid")] diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index 0a08366..d3fa71e 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use crate::client::asynchronous::AsyncFalkorClient; pub struct AsyncGraph<'a> { diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index ec8e01d..18e0e7f 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use crate::client::blocking::SyncFalkorClient; use crate::connection::blocking::FalkorSyncConnection; use crate::value::slowlog_entry::SlowlogEntry; diff --git a/src/graph/mod.rs b/src/graph/mod.rs index 89d8b0e..a6d5b94 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + #[cfg(feature = "tokio")] pub mod asynchronous; pub mod blocking; diff --git a/src/lib.rs b/src/lib.rs index b16baa3..3e82a4c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + #[cfg(not(feature = "redis"))] compile_error!("The `redis` feature must be enabled."); diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index 3814222..0000000 --- a/src/main.rs +++ /dev/null @@ -1,32 +0,0 @@ -use anyhow::Result; - -mod client; -mod connection; -mod connection_info; -mod error; -mod graph; -mod value; - -#[cfg(feature = "redis")] -mod redis_ext; - -fn main() -> Result<()> { - simple_logger::init_utc().expect("Could not start logger"); - - let client = client::builder::FalkorDBClientBuilder::new() - .with_num_connections(4) - .build()?; - - client.config_set("CMD_INFO", "no")?; - let res = client.config_get("*")?; - - let graph = client.open_graph("imdb"); - - // let res = graph.query( - // "MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 10", - // )?; - let res = graph.slowlog()?; - log::info!("{res:?}"); - - Ok(()) -} diff --git a/src/redis_ext.rs b/src/redis_ext.rs index 5bdd7f1..406c13f 100644 --- a/src/redis_ext.rs +++ b/src/redis_ext.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use crate::client::FalkorClientImpl; use crate::connection::{asynchronous::FalkorAsyncConnection, blocking::FalkorSyncConnection}; use crate::connection_info::FalkorConnectionInfo; diff --git a/src/value/config.rs b/src/value/config.rs index 7e89edc..aefa383 100644 --- a/src/value/config.rs +++ b/src/value/config.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use std::fmt::{Display, Formatter}; #[derive(Clone, Debug, PartialEq)] diff --git a/src/value/mod.rs b/src/value/mod.rs index 1c10746..e59ff2f 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use crate::error::FalkorDBError; use anyhow::Result; use std::fmt::{Debug, Display, Formatter}; diff --git a/src/value/query_result.rs b/src/value/query_result.rs index cb60518..cdac43b 100644 --- a/src/value/query_result.rs +++ b/src/value/query_result.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use crate::error::FalkorDBError; use crate::value::FalkorValue; use anyhow::Result; diff --git a/src/value/slowlog_entry.rs b/src/value/slowlog_entry.rs index 81be311..c7c9eb0 100644 --- a/src/value/slowlog_entry.rs +++ b/src/value/slowlog_entry.rs @@ -1,3 +1,8 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + use crate::value::FalkorValue; use anyhow::Result; From bb036a5a0111504ac5bd4c2590ea671476ebaa79 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Mon, 20 May 2024 14:41:10 +0300 Subject: [PATCH 03/62] More work done --- src/client/asynchronous.rs | 2 +- src/client/blocking.rs | 29 +++--- src/client/builder.rs | 5 +- src/connection_info/mod.rs | 2 +- src/graph/blocking.rs | 167 +++++++++++++++++++++++++++++---- src/lib.rs | 13 +-- src/redis_ext.rs | 187 +++++++++++++++++++++++++++++-------- src/value/mod.rs | 29 +----- src/value/query_result.rs | 92 ++++-------------- 9 files changed, 345 insertions(+), 181 deletions(-) diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index d8a4b91..dab4962 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -10,7 +10,7 @@ use std::collections::VecDeque; use tokio::runtime::Runtime; use tokio::sync::Mutex; -pub(crate) struct AsyncFalkorClient { +pub struct AsyncFalkorClient { _inner: FalkorClientImpl, num_connections: u8, connection_pool: Mutex>, diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 726a729..5b94ce0 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -8,31 +8,31 @@ use crate::connection::blocking::{BorrowedSyncConnectionGuard, FalkorSyncConnect use crate::error::FalkorDBError; use crate::graph::blocking::SyncGraph; use crate::value::config::ConfigValue; -use crate::value::FalkorValue; use anyhow::Result; use std::collections::HashMap; -use std::sync::mpsc; +use std::sync::{mpsc, Arc}; -pub(crate) struct SyncFalkorClient { +pub struct SyncFalkorClient { _inner: FalkorClientImpl, - num_connections: u8, connection_pool_tx: mpsc::SyncSender, connection_pool_rx: mpsc::Receiver, } +unsafe impl Sync for SyncFalkorClient {} +unsafe impl Send for SyncFalkorClient {} + impl SyncFalkorClient { - pub(crate) fn create(client: FalkorClientImpl, num_connections: u8) -> Result { + pub(crate) fn create(client: FalkorClientImpl, num_connections: u8) -> Result> { let (connection_pool_tx, connection_pool_rx) = mpsc::sync_channel(num_connections as usize); for _ in 0..num_connections { connection_pool_tx.send(client.get_connection(None)?)?; } - Ok(Self { + Ok(Arc::new(Self { _inner: client, - num_connections, connection_pool_tx, connection_pool_rx, - }) + })) } pub(crate) fn borrow_connection(&self) -> Result { @@ -73,10 +73,7 @@ impl SyncFalkorClient { /// This function returns either an [`FVec`] containing the requested key and val, /// or an [`FVec`] of [`FVec`]s, each one containing a key and val pair - pub fn config_get>( - &self, - config_key: T, - ) -> Result> { + pub fn config_get(&self, config_key: T) -> Result> { let mut conn = self.borrow_connection()?; Ok(match conn.as_inner()? { @@ -84,9 +81,11 @@ impl SyncFalkorClient { FalkorSyncConnection::Redis(redis_conn) => { use redis::ConnectionLike as _; - let bulk_data = match redis_conn - .req_command(redis::cmd("GRAPH.CONFIG").arg("GET").arg(config_key.into()))? - { + let bulk_data = match redis_conn.req_command( + redis::cmd("GRAPH.CONFIG") + .arg("GET") + .arg(config_key.to_string()), + )? { redis::Value::Bulk(bulk_data) => bulk_data, _ => return Err(FalkorDBError::InvalidDataReceived.into()), }; diff --git a/src/client/builder.rs b/src/client/builder.rs index 3eb521c..ea89b33 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -9,6 +9,7 @@ use crate::client::FalkorClientImpl; use crate::connection_info::FalkorConnectionInfo; use crate::error::FalkorDBError; use anyhow::Result; +use std::sync::Arc; // This doesn't have a default implementation because a specific const char is required // and I don't want to leave that up to the user @@ -49,6 +50,8 @@ where } impl FalkorDBClientBuilder<'S'> { + // We wish this to be explicit, and implementing Default is pub + #[allow(clippy::new_without_default)] pub fn new() -> Self { FalkorDBClientBuilder { connection_info: None, @@ -59,7 +62,7 @@ impl FalkorDBClientBuilder<'S'> { } } - pub fn build(self) -> Result { + pub fn build(self) -> Result> { if self.num_connections < 1 || self.num_connections > 32 { return Err(FalkorDBError::InvalidConnectionPoolSize.into()); } diff --git a/src/connection_info/mod.rs b/src/connection_info/mod.rs index 430a616..daae061 100644 --- a/src/connection_info/mod.rs +++ b/src/connection_info/mod.rs @@ -6,7 +6,7 @@ use anyhow::Result; #[derive(Clone)] -pub(crate) enum FalkorConnectionInfo { +pub enum FalkorConnectionInfo { #[cfg(feature = "redis")] Redis(redis::ConnectionInfo), } diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 18e0e7f..8c570ba 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -5,48 +5,65 @@ use crate::client::blocking::SyncFalkorClient; use crate::connection::blocking::FalkorSyncConnection; +use crate::value::query_result::QueryResult; use crate::value::slowlog_entry::SlowlogEntry; use crate::value::FalkorValue; use anyhow::Result; use redis::ConnectionLike; +use std::collections::HashMap; pub struct SyncGraph<'a> { pub(crate) client: &'a SyncFalkorClient, pub(crate) graph_name: String, } +fn construct_query( + query_str: Q, + params: Option<&HashMap>, +) -> String { + params + .map(|params| { + params.iter().fold(String::new(), |acc, (key, val)| { + acc + format!("{}={}", key.to_string(), val.to_string()).as_str() + }) + }) + .unwrap_or_default() + + query_str.to_string().as_str() +} + impl SyncGraph<'_> { pub fn graph_name(&self) -> &str { self.graph_name.as_str() } - fn send_command(&self, command: &str, args: &[FalkorValue]) -> Result { + fn send_command(&self, command: &str, params: Option) -> Result { let mut conn = self.client.borrow_connection()?; Ok(match conn.as_inner()? { #[cfg(feature = "redis")] FalkorSyncConnection::Redis(redis_conn) => { - let mut cmd = redis::cmd(command); - cmd.arg(self.graph_name.as_str()); - for arg in args { - let _ = cmd.arg(arg); - } - redis::FromRedisValue::from_owned_redis_value(redis_conn.req_command(&cmd)?)? + redis::FromRedisValue::from_owned_redis_value( + redis_conn.req_command( + redis::cmd(command) + .arg(self.graph_name.as_str()) + .arg(params), + )?, + )? } }) } pub fn copy(&self, cloned_graph_name: T) -> Result { - self.send_command("GRAPH.COPY", &[cloned_graph_name.to_string().into()])?; + self.send_command("GRAPH.COPY", Some(cloned_graph_name.to_string()))?; Ok(self.client.open_graph(cloned_graph_name)) } pub fn delete(&self) -> Result<()> { - self.send_command("GRAPH.DELETE", &[self.graph_name.as_str().into()])?; + self.send_command("GRAPH.DELETE", None)?; Ok(()) } pub fn slowlog(&self) -> Result> { - let res = self.send_command("GRAPH.SLOWLOG", &[])?.into_vec()?; + let res = self.send_command("GRAPH.SLOWLOG", None)?.into_vec()?; if res.is_empty() { return Ok(vec![]); @@ -61,23 +78,135 @@ impl SyncGraph<'_> { } pub fn slowlog_reset(&self) -> Result<()> { - self.send_command("GRAPH.SLOWLOG", &["RESET".into()])?; + self.send_command("GRAPH.SLOWLOG", Some("RESET".to_string()))?; Ok(()) } - pub fn profile>(&self, args: T) -> Result { - self.send_command("GRAPH.PROFILE", &[args.into()]) + pub fn profile_with_params( + &self, + query_string: Q, + params: Option<&HashMap>, + ) -> Result> { + let query = construct_query(query_string, params); + Ok(self + .send_command("GRAPH.PROFILE", Some(query)) + .and_then(|res| res.into_vec())? + .into_iter() + .flat_map(FalkorValue::into_string) + .map(|step| step.trim().to_string()) + .collect::>()) + } + + pub fn profile(&self, query_string: Q) -> Result> { + self.profile_with_params::(query_string, None) + } + + pub fn explain_with_params( + &self, + query_string: Q, + params: Option<&HashMap>, + ) -> Result> { + let query = construct_query(query_string, params); + Ok(self + .send_command("GRAPH.EXPLAIN", Some(query)) + .and_then(|res| res.into_vec())? + .into_iter() + .flat_map(FalkorValue::into_string) + .map(|step| step.trim().to_string()) + .collect::>()) + } + + pub fn explain(&self, query_string: Q) -> Result> { + self.explain_with_params::(query_string, None) + } + + fn query_with_parser( + &self, + command: &str, + query_string: Q, + params: Option<&HashMap>, + ) -> Result { + let query = construct_query(query_string, params); + + let mut conn = self.client.borrow_connection()?; + Ok(match conn.as_inner()? { + #[cfg(feature = "redis")] + FalkorSyncConnection::Redis(redis_conn) => { + let redis_val = redis_conn.req_command( + redis::cmd(command) + .arg(self.graph_name.as_str()) + .arg(query) + .arg("--compact"), + )?; + QueryResult::try_from(redis_val)? + } + }) + } + + pub fn query_with_params( + &self, + query_string: Q, + params: Option<&HashMap>, + readonly: bool, + ) -> Result { + self.query_with_parser( + if readonly { + "GRAPH.RO_QUERY" + } else { + "GRAPH.QUERY" + }, + query_string, + params, + ) } - pub fn explain>(&self, args: T) -> Result { - self.send_command("GRAPH.EXPLAIN", &[args.into()]) + pub fn query(&self, query_string: Q) -> Result { + self.query_with_params::(query_string, None, false) } - pub fn query>(&self, args: T) -> Result { - self.send_command("GRAPH.QUERY", &[args.into(), "--compact".into()]) + pub fn query_readonly(&self, query_string: Q) -> Result { + self.query_with_params::(query_string, None, true) } - pub fn query_readonly>(&self, args: T) -> Result { - self.send_command("GRAPH.RO_QUERY", &[args.into(), "--compact".into()]) + pub fn call_procedure( + &self, + procedure: P, + args: Option<&[A]>, + yields: Option<&[E]>, + read_only: Option, + ) -> Result { + let params = args.map(|args| { + args.iter() + .enumerate() + .fold(HashMap::new(), |mut acc, (idx, param)| { + acc.insert(format!("param{idx}"), param.to_string()); + acc + }) + }); + + let mut query_string = format!( + "CALL {}({})", + procedure.to_string(), + args.unwrap_or_default() + .iter() + .map(|element| format!("${}", element.to_string())) + .collect::>() + .join(",") + ); + + let yields = yields.unwrap_or_default(); + if !yields.is_empty() { + query_string += format!( + "YIELD {}", + yields + .iter() + .map(|element| element.to_string()) + .collect::>() + .join(",") + ) + .as_str(); + } + + self.query_with_params(query_string, params.as_ref(), read_only.unwrap_or_default()) } } diff --git a/src/lib.rs b/src/lib.rs index 3e82a4c..22f5b2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,12 +6,12 @@ #[cfg(not(feature = "redis"))] compile_error!("The `redis` feature must be enabled."); -mod client; -mod connection; -mod connection_info; +pub mod client; +pub mod connection; +pub mod connection_info; mod error; -mod graph; -mod value; +pub mod graph; +pub mod value; #[cfg(feature = "redis")] mod redis_ext; @@ -22,8 +22,9 @@ mod tests { use crate::client::blocking::SyncFalkorClient; use crate::client::builder::FalkorDBClientBuilder; use crate::value::config::ConfigValue; + use std::sync::Arc; - fn create_client() -> SyncFalkorClient { + fn create_client() -> Arc { FalkorDBClientBuilder::new() .with_num_connections(4) .build() diff --git a/src/redis_ext.rs b/src/redis_ext.rs index 406c13f..3d8356b 100644 --- a/src/redis_ext.rs +++ b/src/redis_ext.rs @@ -8,8 +8,11 @@ use crate::connection::{asynchronous::FalkorAsyncConnection, blocking::FalkorSyn use crate::connection_info::FalkorConnectionInfo; use crate::error::FalkorDBError; use crate::value::config::ConfigValue; +use crate::value::query_result::QueryResult; use crate::value::FalkorValue; +use anyhow::Result; use redis::{FromRedisValue, RedisResult, RedisWrite, ToRedisArgs}; +use std::collections::HashMap; impl From for FalkorSyncConnection { fn from(value: redis::Connection) -> Self { @@ -48,50 +51,15 @@ impl ToRedisArgs for ConfigValue { } } -impl ToRedisArgs for FalkorValue { - fn write_redis_args(&self, out: &mut W) - where - W: ?Sized + RedisWrite, - { - match self { - FalkorValue::None => {} - FalkorValue::Int64(int_data) => int_data.write_redis_args(out), - FalkorValue::UInt64(uint_data) => uint_data.write_redis_args(out), - FalkorValue::F64(f_data) => f_data.write_redis_args(out), - FalkorValue::FString(str_data) => str_data.write_redis_args(out), - FalkorValue::FVec(bulk_data) => { - ToRedisArgs::write_args_from_slice(bulk_data.as_slice(), out) - } - } - } - - fn describe_numeric_behavior(&self) -> redis::NumericBehavior { - match self { - FalkorValue::Int64(_) => redis::NumericBehavior::NumberIsInteger, - FalkorValue::UInt64(_) => redis::NumericBehavior::NumberIsInteger, - FalkorValue::F64(_) => redis::NumericBehavior::NumberIsFloat, - _ => redis::NumericBehavior::NonNumeric, - } - } - - fn is_single_arg(&self) -> bool { - match self { - FalkorValue::None => false, - FalkorValue::FVec(bulk_data) => bulk_data.is_single_arg(), - _ => true, - } - } -} - impl TryFrom<&redis::Value> for ConfigValue { - type Error = anyhow::Error; + 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.into()), + _ => return Err(FalkorDBError::InvalidDataReceived), }) } } @@ -116,3 +84,148 @@ impl FromRedisValue for FalkorValue { }) } } + +fn query_parse_header(header: redis::Value) -> Result> { + let header_vec = header + .into_sequence() + .map_err(|_| FalkorDBError::ParsingError)?; + let header_len = header_vec.len(); + + let keys = header_vec + .into_iter() + .flat_map(redis::Value::into_sequence) + .flat_map(TryInto::<[redis::Value; 2]>::try_into) + .map(|element| element.into_iter().nth(1).unwrap()) + .filter_map(|element| match element { + redis::Value::Data(data) => Some(String::from_utf8_lossy(data.as_slice()).to_string()), + redis::Value::Status(data) => Some(data), + _ => None, + }) + .collect::>(); + + if keys.len() != header_len { + Err(FalkorDBError::ParsingError)?; + } + + Ok(keys) +} + +fn query_parse_stats(stats: redis::Value) -> Result> { + let stats_vec = stats + .into_sequence() + .map_err(|_| FalkorDBError::ParsingError)?; + let stats_len = stats_vec.len(); + + let stats_strings = stats_vec + .into_iter() + .filter_map(|element| match element { + redis::Value::Data(data) => Some(String::from_utf8_lossy(data.as_slice()).to_string()), + redis::Value::Status(data) => Some(data), + _ => None, + }) + .collect::>(); + + if stats_strings.len() != stats_len { + Err(FalkorDBError::ParsingError)?; + } + + Ok(stats_strings) +} + +// TODO: add support for map here +fn parse_toplevel_result(value: Vec) -> Result { + let [type_marker, val]: [redis::Value; 2] = + value.try_into().map_err(|_| FalkorDBError::ParsingError)?; + + let _type_marker = match type_marker { + redis::Value::Int(type_marker) => Ok(type_marker), + _ => Err(FalkorDBError::ParsingError), + }?; + + Ok(FalkorValue::from_owned_redis_value(val)?) +} + +impl TryFrom for QueryResult { + type Error = anyhow::Error; + + fn try_from(value: redis::Value) -> Result { + let value_vec = value + .into_sequence() + .map_err(|_| FalkorDBError::ParsingError)?; + + // Empty result + if value_vec.is_empty() { + return Ok(QueryResult::default()); + } + + // Result only contains stats + if value_vec.len() == 1 { + let first_element = value_vec.into_iter().nth(0).unwrap(); + return Ok(QueryResult { + stats: query_parse_stats(first_element)?, + ..Default::default() + }); + } + + // Invalid response + if value_vec.len() == 2 { + Err(FalkorDBError::InvalidDataReceived)?; + } + + // Full result + let [header, data, stats]: [redis::Value; 3] = value_vec + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + + let header_keys = query_parse_header(header)?; + let stats_strings = query_parse_stats(stats)?; + + let data_vec = data + .into_sequence() + .map_err(|_| FalkorDBError::ParsingError)?; + let data_len = data_vec.len(); + + let result_set: Vec<_> = data_vec + .into_iter() + .flat_map(|element| { + let element_as_vec = match element.into_sequence() { + Ok(element_as_vec) => element_as_vec, + Err(_) => { + return Err(FalkorDBError::ParsingError); + } + }; + + let element_falkor_vals = element_as_vec + .into_iter() + .flat_map(|element| { + let top_level_element = element + .into_sequence() + .map_err(|_| FalkorDBError::ParsingError)?; + + parse_toplevel_result(top_level_element) + }) + .collect::>(); + + if element_falkor_vals.len() != header_keys.len() { + return Err(FalkorDBError::ParsingError); + } + + Ok(header_keys + .iter() + .cloned() + .zip(element_falkor_vals) + .collect::>()) + }) + .collect(); + + if result_set.len() != data_len { + Err(FalkorDBError::ParsingError)?; + } + + Ok(QueryResult { + stats: stats_strings, + header: header_keys, + result_set, + }) + } +} diff --git a/src/value/mod.rs b/src/value/mod.rs index e59ff2f..fb6d14d 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -5,10 +5,11 @@ use crate::error::FalkorDBError; use anyhow::Result; -use std::fmt::{Debug, Display, Formatter}; +use std::collections::HashMap; +use std::fmt::Debug; pub mod config; -mod query_result; +pub mod query_result; pub mod slowlog_entry; #[derive(Clone, Debug)] @@ -19,6 +20,7 @@ pub enum FalkorValue { F64(f64), FString(String), FVec(Vec), + FMap(HashMap), } macro_rules! impl_to_falkordb_value { @@ -42,29 +44,6 @@ impl_to_falkordb_value!(u64, Self::UInt64); impl_to_falkordb_value!(f32, Self::F64); impl_to_falkordb_value!(f64, Self::F64); -impl Display for FalkorValue { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - FalkorValue::None => Ok(()), - FalkorValue::Int64(val) => Display::fmt(val, f), - FalkorValue::UInt64(val) => Display::fmt(val, f), - FalkorValue::F64(val) => Display::fmt(val, f), - FalkorValue::FString(val) => f.write_fmt(format_args!("\"{val}\"")), - FalkorValue::FVec(val) => { - f.write_str("[")?; - let val_len = val.len() - 1; - for (idx, element) in val.iter().enumerate() { - Display::fmt(element, f)?; - if idx < val_len { - f.write_str(",")?; - } - } - f.write_str("]") - } - } - } -} - impl TryFrom<&FalkorValue> for i64 { type Error = FalkorDBError; diff --git a/src/value/query_result.rs b/src/value/query_result.rs index cdac43b..fc83f74 100644 --- a/src/value/query_result.rs +++ b/src/value/query_result.rs @@ -3,90 +3,30 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::error::FalkorDBError; use crate::value::FalkorValue; -use anyhow::Result; +use std::collections::HashMap; -#[derive(Default)] +#[derive(Clone, Debug, Default)] pub struct QueryResult { - raw_stats: Vec, - header: Vec, - result_set: Vec, -} - -fn filter_stat_only_response(res: &[FalkorValue]) -> Result> { - Ok( - match match res.len() { - 0 => return Err(FalkorDBError::InvalidDataReceived.into()), - 1 => res.first(), - _ => res.last(), - } { - Some(FalkorValue::FVec(val)) => val.clone(), - _ => return Err(FalkorDBError::InvalidDataReceived.into()), - }, - ) + pub(crate) stats: Vec, + pub(crate) header: Vec, + pub(crate) result_set: Vec>, } impl QueryResult { - pub fn from_result(res: FalkorValue) -> Result { - let res = res.into_vec()?; - if res.is_empty() { - return Err(FalkorDBError::InvalidDataReceived.into()); - } - - match res.len() { - 1 => res.first(), - _ => res.last(), - }; - - let header = filter_stat_only_response(res.as_slice())?; - if header.is_empty() { - return Ok(Default::default()); - } - - // let records = res.into_iter().enumerate().skip(1).fold(Vec::with_capacity()) { - // - // } + pub fn stats(&self) -> &[String] { + self.stats.as_slice() + } - // match res.len() { - // 0 => { - // return Err(FalkorDBError::InvalidDataReceived.into()); - // } - // 1 => { - // return Ok(Self { - // raw_stats: match res.first() { - // Some(FalkorValue::FString(str_val)) => { - // panic!("{str_val}"); // Want to catch errors here before release, in case this is actually a possibility - // } - // Some(FalkorValue::FVec(vec_val)) => vec_val.clone(), - // _ => return Err(FalkorDBError::InvalidDataReceived.into()), - // }, - // header: vec![], - // result_set: vec![], - // }); - // } - // _ => { - // let mut new_result = Self { - // raw_stats: vec![], - // header: match res.first() { - // Some(FalkorValue::FVec(vec_val)) => { - // if vec_val.is_empty() { - // return Ok(Default::default()); - // } - // - // vec_val.clone() - // } - // _ => { - // return Err(FalkorDBError::InvalidDataReceived); - // } - // }, - // result_set: vec![], - // }; - // } - // } + pub fn header(&self) -> &[String] { + self.header.as_slice() + } - Err(FalkorDBError::InvalidDataReceived.into()) + pub fn result_set(&self) -> &[HashMap] { + self.result_set.as_slice() + } - // Ok(new_result) + pub fn take(self) -> (Vec, Vec, Vec>) { + (self.stats, self.header, self.result_set) } } From aca71cfafe60b647482f47be51281bcf50f8ac15 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 21 May 2024 15:35:45 +0300 Subject: [PATCH 04/62] Sync is mostly working now, some call procedures are failing --- src/client/asynchronous.rs | 56 +++++++-- src/client/blocking.rs | 67 ++++++----- src/client/builder.rs | 8 +- src/connection/asynchronous.rs | 26 +--- src/connection/blocking.rs | 29 ++++- src/error/mod.rs | 30 ++++- src/graph/blocking.rs | 121 +++++++++++-------- src/graph/mod.rs | 1 + src/graph/schema.rs | 77 ++++++++++++ src/redis_ext.rs | 149 +---------------------- src/value/constraint.rs | 14 +++ src/value/execution_plan.rs | 44 +++++++ src/value/graph_entities.rs | 84 +++++++++++++ src/value/map.rs | 170 ++++++++++++++++++++++++++ src/value/mod.rs | 212 +++++++++++++++++++++------------ src/value/path.rs | 39 ++++++ src/value/point.rs | 27 +++++ src/value/query_result.rs | 113 +++++++++++++++++- src/value/slowlog_entry.rs | 9 +- 19 files changed, 915 insertions(+), 361 deletions(-) create mode 100644 src/graph/schema.rs create mode 100644 src/value/constraint.rs create mode 100644 src/value/execution_plan.rs create mode 100644 src/value/graph_entities.rs create mode 100644 src/value/map.rs create mode 100644 src/value/path.rs create mode 100644 src/value/point.rs diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index dab4962..3069484 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -5,29 +5,59 @@ use crate::client::FalkorClientImpl; use crate::connection::asynchronous::FalkorAsyncConnection; +use crate::error::FalkorDBError; use anyhow::Result; -use std::collections::VecDeque; +use std::sync::Arc; use tokio::runtime::Runtime; -use tokio::sync::Mutex; pub struct AsyncFalkorClient { _inner: FalkorClientImpl, - num_connections: u8, - connection_pool: Mutex>, + connection: FalkorAsyncConnection, runtime: Runtime, } impl AsyncFalkorClient { - pub(crate) async fn create( - client: FalkorClientImpl, - num_connections: u8, - runtime: Runtime, - ) -> Result { - Ok(Self { + pub(crate) async fn create(client: FalkorClientImpl, runtime: Runtime) -> Result> { + Ok(Arc::new(Self { + connection: client.get_async_connection(None).await?, _inner: client, - num_connections, - connection_pool: Default::default(), runtime, - }) + })) + } + pub(crate) fn clone_connection(&self) -> FalkorAsyncConnection { + self.connection.clone() + } + + pub async fn list_graphs(&self) -> Result> { + let conn = self.clone_connection(); + + let graph_list = match conn { + #[cfg(feature = "redis")] + FalkorAsyncConnection::Redis(mut redis_conn) => { + let res = match redis::cmd("GRAPH.LIST") + .query_async(&mut redis_conn) + .await? + { + redis::Value::Bulk(data) => data, + _ => Err(FalkorDBError::InvalidDataReceived)?, + }; + + let mut graph_list = Vec::with_capacity(res.len()); + for graph in res { + let graph = match graph { + redis::Value::Data(data) => { + Ok(String::from_utf8_lossy(data.as_slice()).to_string()) + } + redis::Value::Status(data) => Ok(data), + _ => Err(FalkorDBError::ParsingError), + }?; + + graph_list.push(graph); + } + graph_list + } + }; + + Ok(graph_list) } } diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 5b94ce0..4de109d 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -4,9 +4,10 @@ */ use crate::client::FalkorClientImpl; -use crate::connection::blocking::{BorrowedSyncConnectionGuard, FalkorSyncConnection}; +use crate::connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}; use crate::error::FalkorDBError; use crate::graph::blocking::SyncGraph; +use crate::graph::schema::GraphSchema; use crate::value::config::ConfigValue; use anyhow::Result; use std::collections::HashMap; @@ -35,8 +36,8 @@ impl SyncFalkorClient { })) } - pub(crate) fn borrow_connection(&self) -> Result { - Ok(BorrowedSyncConnectionGuard { + pub(crate) fn borrow_connection(&self) -> Result { + Ok(BorrowedSyncConnection { return_tx: self.connection_pool_tx.clone(), conn: Some(self.connection_pool_rx.recv()?), }) @@ -51,20 +52,22 @@ impl SyncFalkorClient { use redis::ConnectionLike as _; let res = match redis_conn.req_command(&redis::cmd("GRAPH.LIST"))? { redis::Value::Bulk(data) => data, - _ => { - return Err(FalkorDBError::InvalidDataReceived.into()); - } + _ => Err(FalkorDBError::InvalidDataReceived)?, }; - res.iter() - .filter_map(|element| { - if let redis::Value::Data(v) = element { - Some(String::from_utf8_lossy(v.as_slice()).to_string()) - } else { - None + let mut graph_list = Vec::with_capacity(res.len()); + for graph in res { + let graph = match graph { + redis::Value::Data(data) => { + Ok(String::from_utf8_lossy(data.as_slice()).to_string()) } - }) - .collect() + redis::Value::Status(data) => Ok(data), + _ => Err(FalkorDBError::ParsingError), + }?; + + graph_list.push(graph); + } + graph_list } }; @@ -103,24 +106,25 @@ impl SyncFalkorClient { }; } - bulk_data - .into_iter() - .flat_map(redis::Value::into_map_iter) - .fold(HashMap::new(), |mut acc, it| { - acc.extend(it.flat_map(|(key, val)| match key { - redis::Value::Status(config_key) => { - Ok((config_key, ConfigValue::try_from(&val)?)) + let mut config_map = HashMap::with_capacity(bulk_data.len()); + for raw_map in bulk_data { + for (key, val) in raw_map + .into_map_iter() + .map_err(|_| FalkorDBError::ParsingError)? + { + let key = match key { + redis::Value::Status(config_key) => Ok(config_key), + redis::Value::Data(config_key) => { + Ok(String::from_utf8_lossy(config_key.as_slice()).to_string()) } - redis::Value::Data(config_key) => Ok(( - String::from_utf8_lossy(config_key.as_slice()).to_string(), - ConfigValue::try_from(&val)?, - )), - _ => Err(Into::::into( - FalkorDBError::InvalidDataReceived, - )), - })); - acc - }) + _ => Err(FalkorDBError::InvalidDataReceived), + }?; + + config_map.insert(key, ConfigValue::try_from(&val)?); + } + } + + config_map } }) } @@ -151,6 +155,7 @@ impl SyncFalkorClient { SyncGraph { client: self, graph_name: graph_name.to_string(), + graph_schema: GraphSchema::new(graph_name.to_string()), } } } diff --git a/src/client/builder.rs b/src/client/builder.rs index ea89b33..5ee7183 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -102,11 +102,7 @@ impl FalkorDBClientBuilder<'A'> { } } - pub async fn build(self) -> Result { - if self.num_connections < 1 || self.num_connections > 32 { - return Err(FalkorDBError::InvalidConnectionPoolSize.into()); - } - + pub async fn build(self) -> Result> { let connection_info = self .connection_info .unwrap_or("falkor://127.0.0.1:6379".try_into()?); @@ -120,6 +116,6 @@ impl FalkorDBClientBuilder<'A'> { .build()?, ); - AsyncFalkorClient::create(get_client(connection_info)?, self.num_connections, runtime).await + AsyncFalkorClient::create(get_client(connection_info)?, runtime).await } } diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs index 5adcd7f..59ee586 100644 --- a/src/connection/asynchronous.rs +++ b/src/connection/asynchronous.rs @@ -3,32 +3,8 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::error::FalkorDBError; -use anyhow::Result; -use tokio::sync::mpsc; - +#[derive(Clone)] pub enum FalkorAsyncConnection { #[cfg(feature = "redis")] Redis(redis::aio::MultiplexedConnection), } - -pub(crate) struct BorrowedAsyncConnectionGuard { - conn: Option, - return_tx: mpsc::Sender, -} - -impl BorrowedAsyncConnectionGuard { - pub(crate) fn as_inner(&mut self) -> Result<&mut FalkorAsyncConnection> { - self.conn - .as_mut() - .ok_or(FalkorDBError::EmptyConnection.into()) - } -} - -impl Drop for BorrowedAsyncConnectionGuard { - fn drop(&mut self) { - if let Some(conn) = self.conn.take() { - self.return_tx.blocking_send(conn).ok(); - } - } -} diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 4729d0d..57b65ff 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -4,6 +4,9 @@ */ use crate::error::FalkorDBError; +use crate::value::FalkorValue; +use anyhow::Result; +use redis::ConnectionLike; use std::sync::mpsc; pub enum FalkorSyncConnection { @@ -11,20 +14,38 @@ pub enum FalkorSyncConnection { Redis(redis::Connection), } -pub(crate) struct BorrowedSyncConnectionGuard { +pub(crate) struct BorrowedSyncConnection { pub(crate) conn: Option, pub(crate) return_tx: mpsc::SyncSender, } -impl BorrowedSyncConnectionGuard { - pub(crate) fn as_inner(&mut self) -> anyhow::Result<&mut FalkorSyncConnection> { +impl BorrowedSyncConnection { + pub(crate) fn as_inner(&mut self) -> Result<&mut FalkorSyncConnection> { self.conn .as_mut() .ok_or(FalkorDBError::EmptyConnection.into()) } + + pub(crate) fn send_command( + &mut self, + graph_name: Option, + command: &str, + params: Option, + ) -> Result { + Ok( + match self.conn.as_mut().ok_or(FalkorDBError::EmptyConnection)? { + #[cfg(feature = "redis")] + FalkorSyncConnection::Redis(redis_conn) => { + redis::FromRedisValue::from_owned_redis_value( + redis_conn.req_command(redis::cmd(command).arg(graph_name).arg(params))?, + )? + } + }, + ) + } } -impl Drop for BorrowedSyncConnectionGuard { +impl Drop for BorrowedSyncConnection { fn drop(&mut self) { if let Some(conn) = self.conn.take() { self.return_tx.send(conn).ok(); diff --git a/src/error/mod.rs b/src/error/mod.rs index 356ec09..d2c06ce 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -15,6 +15,34 @@ pub enum FalkorDBError { InvalidConnectionPoolSize, #[error("Attempting to use an empty connection object")] EmptyConnection, - #[error("Parsing error due to invalid types, or argument count")] + #[error("General parsing error")] ParsingError, + #[error("Received malformed header")] + ParsingHeader, + #[error("Unknown type")] + ParsingUnknownType, + #[error("Element was not of type FArray")] + ParsingFArray, + #[error("Element was not of type FString")] + ParsingFString, + #[error("Element was not of type FEdge")] + ParsingFEdge, + #[error("Element was not of type FNode")] + ParsingFNode, + #[error("Element was not of type FPath")] + ParsingFPath, + #[error("Element was not of type FMap")] + ParsingFMap, + #[error("Element was not of type FPoint")] + ParsingFPoint, + #[error("Key id was not of type i64")] + ParsingKeyIdTypeMismatch, + #[error("Type marker was not of type i64")] + ParsingTypeMarkerTypeMismatch, + #[error("Both key id and type marker were not of type i64")] + ParsingKTVTypes, + #[error("Could not form slowlog entry, element count invalid")] + ParsingSlowlogEntryElementCount, + #[error("Could not parse node, element count invalid")] + ParsingNodeElementCount, } diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 8c570ba..0294dd5 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -5,6 +5,9 @@ use crate::client::blocking::SyncFalkorClient; use crate::connection::blocking::FalkorSyncConnection; +use crate::error::FalkorDBError; +use crate::graph::schema::GraphSchema; +use crate::value::execution_plan::ExecutionPlan; use crate::value::query_result::QueryResult; use crate::value::slowlog_entry::SlowlogEntry; use crate::value::FalkorValue; @@ -15,6 +18,7 @@ use std::collections::HashMap; pub struct SyncGraph<'a> { pub(crate) client: &'a SyncFalkorClient, pub(crate) graph_name: String, + pub(crate) graph_schema: GraphSchema, } fn construct_query( @@ -38,18 +42,7 @@ impl SyncGraph<'_> { fn send_command(&self, command: &str, params: Option) -> Result { let mut conn = self.client.borrow_connection()?; - Ok(match conn.as_inner()? { - #[cfg(feature = "redis")] - FalkorSyncConnection::Redis(redis_conn) => { - redis::FromRedisValue::from_owned_redis_value( - redis_conn.req_command( - redis::cmd(command) - .arg(self.graph_name.as_str()) - .arg(params), - )?, - )? - } - }) + conn.send_command(Some(self.graph_name.clone()), command, params) } pub fn copy(&self, cloned_graph_name: T) -> Result { @@ -69,12 +62,17 @@ impl SyncGraph<'_> { return Ok(vec![]); } - Ok(res - .into_iter() - .flat_map(FalkorValue::into_vec) - .flat_map(TryInto::<[FalkorValue; 4]>::try_into) - .flat_map(SlowlogEntry::from_value_array) - .collect::>()) + let mut slowlog_entries = Vec::with_capacity(res.len()); + for entry_raw in res { + slowlog_entries.push(SlowlogEntry::from_value_array( + entry_raw + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingSlowlogEntryElementCount)?, + )?); + } + + Ok(slowlog_entries) } pub fn slowlog_reset(&self) -> Result<()> { @@ -86,18 +84,13 @@ impl SyncGraph<'_> { &self, query_string: Q, params: Option<&HashMap>, - ) -> Result> { + ) -> Result { let query = construct_query(query_string, params); - Ok(self - .send_command("GRAPH.PROFILE", Some(query)) - .and_then(|res| res.into_vec())? - .into_iter() - .flat_map(FalkorValue::into_string) - .map(|step| step.trim().to_string()) - .collect::>()) + + ExecutionPlan::try_from(self.send_command("GRAPH.PROFILE", Some(query))?) } - pub fn profile(&self, query_string: Q) -> Result> { + pub fn profile(&self, query_string: Q) -> Result { self.profile_with_params::(query_string, None) } @@ -105,18 +98,12 @@ impl SyncGraph<'_> { &self, query_string: Q, params: Option<&HashMap>, - ) -> Result> { + ) -> Result { let query = construct_query(query_string, params); - Ok(self - .send_command("GRAPH.EXPLAIN", Some(query)) - .and_then(|res| res.into_vec())? - .into_iter() - .flat_map(FalkorValue::into_string) - .map(|step| step.trim().to_string()) - .collect::>()) + ExecutionPlan::try_from(self.send_command("GRAPH.EXPLAIN", Some(query))?) } - pub fn explain(&self, query_string: Q) -> Result> { + pub fn explain(&self, query_string: Q) -> Result { self.explain_with_params::(query_string, None) } @@ -125,22 +112,27 @@ impl SyncGraph<'_> { command: &str, query_string: Q, params: Option<&HashMap>, + timeout: Option, ) -> Result { let query = construct_query(query_string, params); let mut conn = self.client.borrow_connection()?; - Ok(match conn.as_inner()? { + let falkor_result = match conn.as_inner()? { #[cfg(feature = "redis")] FalkorSyncConnection::Redis(redis_conn) => { + use redis::FromRedisValue as _; let redis_val = redis_conn.req_command( redis::cmd(command) .arg(self.graph_name.as_str()) .arg(query) - .arg("--compact"), + .arg("--compact") + .arg(timeout.map(|timeout| format!("timeout {timeout}"))), )?; - QueryResult::try_from(redis_val)? + FalkorValue::from_owned_redis_value(redis_val)? } - }) + }; + + QueryResult::from_falkor_value(falkor_result, &self.graph_schema, &mut conn) } pub fn query_with_params( @@ -148,6 +140,7 @@ impl SyncGraph<'_> { query_string: Q, params: Option<&HashMap>, readonly: bool, + timeout: Option, ) -> Result { self.query_with_parser( if readonly { @@ -157,23 +150,29 @@ impl SyncGraph<'_> { }, query_string, params, + timeout, ) } - pub fn query(&self, query_string: Q) -> Result { - self.query_with_params::(query_string, None, false) + pub fn query(&self, query_string: Q, timeout: Option) -> Result { + self.query_with_params::(query_string, None, false, timeout) } - pub fn query_readonly(&self, query_string: Q) -> Result { - self.query_with_params::(query_string, None, true) + pub fn query_readonly( + &self, + query_string: Q, + timeout: Option, + ) -> Result { + self.query_with_params::(query_string, None, true, timeout) } - pub fn call_procedure( + pub fn call_procedure( &self, procedure: P, - args: Option<&[A]>, - yields: Option<&[E]>, + args: Option<&[String]>, + yields: Option<&[String]>, read_only: Option, + timeout: Option, ) -> Result { let params = args.map(|args| { args.iter() @@ -189,7 +188,7 @@ impl SyncGraph<'_> { procedure.to_string(), args.unwrap_or_default() .iter() - .map(|element| format!("${}", element.to_string())) + .map(|element| format!("${}", element)) .collect::>() .join(",") ); @@ -207,6 +206,30 @@ impl SyncGraph<'_> { .as_str(); } - self.query_with_params(query_string, params.as_ref(), read_only.unwrap_or_default()) + self.query_with_params( + query_string, + params.as_ref(), + read_only.unwrap_or_default(), + timeout, + ) + } + + pub fn list_indices(&self) -> Result>> { + let query_res = self + .call_procedure("DB.INDEXES", None, None, None, None)? + .result_set; + + Ok(query_res) + } + + pub fn list_constraints(&self) -> Result>> { + let query_res = self + .call_procedure("DB.CONSTRAINTS", None, None, None, None)? + .result_set; + + let query_res_len = query_res.len(); + Ok(query_res + .into_iter() + .fold(Vec::with_capacity(query_res_len), |acc, it| acc)) } } diff --git a/src/graph/mod.rs b/src/graph/mod.rs index a6d5b94..dbe4761 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -6,3 +6,4 @@ #[cfg(feature = "tokio")] pub mod asynchronous; pub mod blocking; +pub mod schema; diff --git a/src/graph/schema.rs b/src/graph/schema.rs new file mode 100644 index 0000000..c0b34bb --- /dev/null +++ b/src/graph/schema.rs @@ -0,0 +1,77 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use parking_lot::RwLock; +use std::collections::{HashMap, HashSet}; +use std::sync::atomic::AtomicI64; +use std::sync::atomic::Ordering::SeqCst; +use std::sync::Arc; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum SchemaType { + Labels, + Properties, + Relationships, +} + +pub(crate) type LockableIdMap = Arc>>; + +#[derive(Clone, Debug, Default)] +pub struct GraphSchema { + graph_name: String, + version: Arc, + labels: LockableIdMap, + properties: LockableIdMap, + relationships: LockableIdMap, +} + +impl GraphSchema { + pub fn new(graph_name: String) -> Self { + Self { + graph_name, + ..Default::default() + } + } + + pub fn clear(&mut self) { + self.version.store(0, SeqCst); + self.labels.write().clear(); + self.properties.write().clear(); + self.relationships.write().clear(); + } + + pub fn graph_name(&self) -> String { + self.graph_name.clone() + } + + pub fn relationships(&self) -> LockableIdMap { + self.relationships.clone() + } + + pub fn labels(&self) -> LockableIdMap { + self.labels.clone() + } + + pub fn properties(&self) -> LockableIdMap { + self.properties.clone() + } + + pub fn verify_id_set(&self, id_set: &HashSet, schema_type: SchemaType) -> bool { + match schema_type { + SchemaType::Labels => { + let labels = self.labels.read(); + id_set.iter().all(|id| labels.contains_key(id)) + } + SchemaType::Properties => id_set.iter().all(|id| { + let properties = self.properties.read(); + properties.contains_key(id) + }), + SchemaType::Relationships => id_set.iter().all(|id| { + let relationships = self.relationships.read(); + relationships.contains_key(id) + }), + } + } +} diff --git a/src/redis_ext.rs b/src/redis_ext.rs index 3d8356b..e1c1b51 100644 --- a/src/redis_ext.rs +++ b/src/redis_ext.rs @@ -8,11 +8,9 @@ use crate::connection::{asynchronous::FalkorAsyncConnection, blocking::FalkorSyn use crate::connection_info::FalkorConnectionInfo; use crate::error::FalkorDBError; use crate::value::config::ConfigValue; -use crate::value::query_result::QueryResult; use crate::value::FalkorValue; use anyhow::Result; use redis::{FromRedisValue, RedisResult, RedisWrite, ToRedisArgs}; -use std::collections::HashMap; impl From for FalkorSyncConnection { fn from(value: redis::Connection) -> Self { @@ -72,7 +70,7 @@ impl FromRedisValue for FalkorValue { redis::Value::Data(str_val) => { FalkorValue::FString(String::from_utf8_lossy(str_val.as_slice()).to_string()) } - redis::Value::Bulk(bulk) => FalkorValue::FVec({ + redis::Value::Bulk(bulk) => FalkorValue::FArray({ let mut new_vec = Vec::with_capacity(bulk.len()); for element in bulk { new_vec.push(FalkorValue::from_redis_value(element)?); @@ -84,148 +82,3 @@ impl FromRedisValue for FalkorValue { }) } } - -fn query_parse_header(header: redis::Value) -> Result> { - let header_vec = header - .into_sequence() - .map_err(|_| FalkorDBError::ParsingError)?; - let header_len = header_vec.len(); - - let keys = header_vec - .into_iter() - .flat_map(redis::Value::into_sequence) - .flat_map(TryInto::<[redis::Value; 2]>::try_into) - .map(|element| element.into_iter().nth(1).unwrap()) - .filter_map(|element| match element { - redis::Value::Data(data) => Some(String::from_utf8_lossy(data.as_slice()).to_string()), - redis::Value::Status(data) => Some(data), - _ => None, - }) - .collect::>(); - - if keys.len() != header_len { - Err(FalkorDBError::ParsingError)?; - } - - Ok(keys) -} - -fn query_parse_stats(stats: redis::Value) -> Result> { - let stats_vec = stats - .into_sequence() - .map_err(|_| FalkorDBError::ParsingError)?; - let stats_len = stats_vec.len(); - - let stats_strings = stats_vec - .into_iter() - .filter_map(|element| match element { - redis::Value::Data(data) => Some(String::from_utf8_lossy(data.as_slice()).to_string()), - redis::Value::Status(data) => Some(data), - _ => None, - }) - .collect::>(); - - if stats_strings.len() != stats_len { - Err(FalkorDBError::ParsingError)?; - } - - Ok(stats_strings) -} - -// TODO: add support for map here -fn parse_toplevel_result(value: Vec) -> Result { - let [type_marker, val]: [redis::Value; 2] = - value.try_into().map_err(|_| FalkorDBError::ParsingError)?; - - let _type_marker = match type_marker { - redis::Value::Int(type_marker) => Ok(type_marker), - _ => Err(FalkorDBError::ParsingError), - }?; - - Ok(FalkorValue::from_owned_redis_value(val)?) -} - -impl TryFrom for QueryResult { - type Error = anyhow::Error; - - fn try_from(value: redis::Value) -> Result { - let value_vec = value - .into_sequence() - .map_err(|_| FalkorDBError::ParsingError)?; - - // Empty result - if value_vec.is_empty() { - return Ok(QueryResult::default()); - } - - // Result only contains stats - if value_vec.len() == 1 { - let first_element = value_vec.into_iter().nth(0).unwrap(); - return Ok(QueryResult { - stats: query_parse_stats(first_element)?, - ..Default::default() - }); - } - - // Invalid response - if value_vec.len() == 2 { - Err(FalkorDBError::InvalidDataReceived)?; - } - - // Full result - let [header, data, stats]: [redis::Value; 3] = value_vec - .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; - - let header_keys = query_parse_header(header)?; - let stats_strings = query_parse_stats(stats)?; - - let data_vec = data - .into_sequence() - .map_err(|_| FalkorDBError::ParsingError)?; - let data_len = data_vec.len(); - - let result_set: Vec<_> = data_vec - .into_iter() - .flat_map(|element| { - let element_as_vec = match element.into_sequence() { - Ok(element_as_vec) => element_as_vec, - Err(_) => { - return Err(FalkorDBError::ParsingError); - } - }; - - let element_falkor_vals = element_as_vec - .into_iter() - .flat_map(|element| { - let top_level_element = element - .into_sequence() - .map_err(|_| FalkorDBError::ParsingError)?; - - parse_toplevel_result(top_level_element) - }) - .collect::>(); - - if element_falkor_vals.len() != header_keys.len() { - return Err(FalkorDBError::ParsingError); - } - - Ok(header_keys - .iter() - .cloned() - .zip(element_falkor_vals) - .collect::>()) - }) - .collect(); - - if result_set.len() != data_len { - Err(FalkorDBError::ParsingError)?; - } - - Ok(QueryResult { - stats: stats_strings, - header: header_keys, - result_set, - }) - } -} diff --git a/src/value/constraint.rs b/src/value/constraint.rs new file mode 100644 index 0000000..8dc95d6 --- /dev/null +++ b/src/value/constraint.rs @@ -0,0 +1,14 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use super::FalkorValue; + +pub struct Constraint { + _type: String, + label: String, + properties: FalkorValue, + entity_type: FalkorValue, + status: String, +} diff --git a/src/value/execution_plan.rs b/src/value/execution_plan.rs new file mode 100644 index 0000000..8efa34a --- /dev/null +++ b/src/value/execution_plan.rs @@ -0,0 +1,44 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use crate::value::FalkorValue; + +pub struct ExecutionPlan { + text: String, + steps: Vec, +} + +impl ExecutionPlan { + pub fn text(&self) -> &str { + self.text.as_str() + } + + pub fn steps(&self) -> &[String] { + self.steps.as_slice() + } +} + +impl TryFrom for ExecutionPlan { + type Error = anyhow::Error; + + fn try_from(value: FalkorValue) -> Result { + let string_vec = value.into_vec()?; + + let (mut execution_plan, mut execution_plan_text) = ( + Vec::with_capacity(string_vec.len()), + Vec::with_capacity(string_vec.len()), + ); + for item in string_vec { + let raw_text = item.into_string()?; + execution_plan.push(raw_text.trim().to_string()); + execution_plan_text.push(raw_text); + } + + Ok(ExecutionPlan { + steps: execution_plan, + text: execution_plan_text.join("\n"), + }) + } +} diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs new file mode 100644 index 0000000..ea6a097 --- /dev/null +++ b/src/value/graph_entities.rs @@ -0,0 +1,84 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use crate::connection::blocking::BorrowedSyncConnection; +use crate::error::FalkorDBError; +use crate::graph::schema::{GraphSchema, SchemaType}; +use crate::value::map::parse_map_with_schema; +use crate::value::FalkorValue; +use anyhow::Result; +use std::collections::HashMap; + +#[derive(Clone, Debug)] +pub struct Node { + pub entity_id: i64, + pub labels: Vec, + pub properties: HashMap, +} + +impl Node { + pub(crate) fn parse( + value: FalkorValue, + graph_schema: &GraphSchema, + conn: &mut BorrowedSyncConnection, + ) -> Result { + let [entity_id, labels, properties]: [FalkorValue; 3] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingNodeElementCount)?; + let labels = labels.into_vec()?; + + let mut parsed_labels = Vec::with_capacity(labels.len()); + for label in labels { + parsed_labels.push(label.to_i64().ok_or(FalkorDBError::ParsingError)?); + } + + Ok(Node { + entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingError)?, + labels: parsed_labels, + properties: parse_map_with_schema( + properties, + graph_schema, + conn, + SchemaType::Properties, + )?, + }) + } +} + +#[derive(Clone, Debug)] +pub struct Edge { + pub entity_id: i64, + pub labels: i64, + pub src_node_id: i64, + pub dst_node_id: i64, + pub properties: HashMap, +} + +impl Edge { + pub(crate) fn parse( + value: FalkorValue, + graph_schema: &GraphSchema, + conn: &mut BorrowedSyncConnection, + ) -> Result { + let [entity_id, labels, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + + Ok(Edge { + entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingError)?, + labels: labels.to_i64().ok_or(FalkorDBError::ParsingError)?, + src_node_id: src_node_id.to_i64().ok_or(FalkorDBError::ParsingError)?, + dst_node_id: dst_node_id.to_i64().ok_or(FalkorDBError::ParsingError)?, + properties: parse_map_with_schema( + properties, + graph_schema, + conn, + SchemaType::Properties, + )?, + }) + } +} diff --git a/src/value/map.rs b/src/value/map.rs new file mode 100644 index 0000000..f07feb2 --- /dev/null +++ b/src/value/map.rs @@ -0,0 +1,170 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use crate::connection::blocking::BorrowedSyncConnection; +use crate::error::FalkorDBError; +use crate::graph::schema::{GraphSchema, SchemaType}; +use crate::value::{parse_type, FalkorValue}; +use std::collections::{HashMap, HashSet}; +use std::ops::{Deref, DerefMut}; + +// Intermediate type for map parsing +pub(crate) struct FKeyTypeVal { + key: i64, + type_marker: i64, + val: FalkorValue, +} + +impl TryFrom for FKeyTypeVal { + type Error = anyhow::Error; + + fn try_from(value: FalkorValue) -> Result { + let [key_raw, type_raw, val]: [FalkorValue; 3] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingKTVTypes)?; + + let key = key_raw.to_i64(); + let type_marker = type_raw.to_i64(); + + match (key, type_marker) { + (Some(key), Some(type_marker)) => Ok(FKeyTypeVal { + key, + type_marker, + val, + }), + (Some(_), None) => Err(FalkorDBError::ParsingTypeMarkerTypeMismatch)?, + (None, Some(_)) => Err(FalkorDBError::ParsingKeyIdTypeMismatch)?, + _ => Err(FalkorDBError::ParsingKTVTypes)?, + } + } +} + +pub(crate) fn get_schema_subset( + ids: &HashSet, + schema: &HashMap, +) -> Option> { + let mut hashmap = HashMap::new(); + for id in ids { + hashmap.insert(*id, schema.get(id).cloned()?); + } + + Some(hashmap) +} + +// TODO: This does NOT support nested attributes yet +pub(crate) fn parse_map_with_schema( + value: FalkorValue, + graph_schema: &GraphSchema, + conn: &mut BorrowedSyncConnection, + schema_type: SchemaType, +) -> anyhow::Result> { + let val_vec = value.into_vec()?; + let (mut id_hashset, mut map_vec) = ( + HashSet::with_capacity(val_vec.len()), + Vec::with_capacity(val_vec.len()), + ); + + for item in val_vec { + let mut fktv = FKeyTypeVal::try_from(item)?; + id_hashset.insert(fktv.key); + map_vec.push(fktv); + } + + let locked_id_to_string_map = match schema_type { + SchemaType::Labels => graph_schema.labels(), + SchemaType::Properties => graph_schema.properties(), + SchemaType::Relationships => graph_schema.relationships(), + }; + + let relevant_ids_map = { + let read_lock = locked_id_to_string_map.read(); + get_schema_subset(&id_hashset, read_lock.deref()) + }; + + if let Some(relevant_ids_map) = relevant_ids_map { + let mut new_map = HashMap::with_capacity(map_vec.len()); + for fktv in map_vec { + new_map.insert( + relevant_ids_map + .get(&fktv.key) + .cloned() + .ok_or(FalkorDBError::ParsingError)?, + parse_type(fktv.type_marker, fktv.val, graph_schema, conn)?, + ); + } + + return Ok(new_map); + } + + // If we reached here, schema validation failed and we need to refresh our schema + let command = match schema_type { + SchemaType::Labels => "DB.LABELS", + SchemaType::Properties => "DB.PROPERTYKEYS", + SchemaType::Relationships => "DB.RELATIONSHIPTYPES", + }; + let [_, keys, _]: [FalkorValue; 3] = conn + .send_command( + Some(graph_schema.graph_name()), + "GRAPH.QUERY", + Some(format!("CALL {command}()")), + )? + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + let keys_vec = keys.into_vec()?; + + let mut new_keys = HashMap::with_capacity(keys_vec.len()); + for (idx, item) in keys_vec.into_iter().enumerate() { + let key = item + .into_vec()? + .into_iter() + .next() + .ok_or(FalkorDBError::ParsingError)? + .into_string()?; + new_keys.insert(idx as i64, key); + } + + let id_map = match schema_type { + SchemaType::Labels => graph_schema.labels(), + SchemaType::Properties => graph_schema.properties(), + SchemaType::Relationships => graph_schema.relationships(), + }; + + let mut lock = id_map.write(); + *(lock.deref_mut()) = new_keys; + + let mut new_map = HashMap::with_capacity(map_vec.len()); + for fktv in map_vec { + new_map.insert( + lock.deref() + .get(&fktv.key) + .cloned() + .ok_or(FalkorDBError::ParsingError)?, + parse_type(fktv.type_marker, fktv.val, graph_schema, conn)?, + ); + } + + Ok(new_map) +} + +pub(crate) fn parse_map( + value: FalkorValue, + graph_schema: &GraphSchema, + conn: &mut BorrowedSyncConnection, +) -> anyhow::Result> { + let val_vec = value.into_vec()?; + + let mut new_map = HashMap::with_capacity(val_vec.len()); + for val in val_vec { + let fktv = FKeyTypeVal::try_from(val)?; + new_map.insert( + fktv.key.to_string(), + parse_type(fktv.type_marker, fktv.val, graph_schema, conn)?, + ); + } + + Ok(new_map) +} diff --git a/src/value/mod.rs b/src/value/mod.rs index fb6d14d..a8f2036 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -3,24 +3,41 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use crate::connection::blocking::BorrowedSyncConnection; use crate::error::FalkorDBError; +use crate::graph::schema::GraphSchema; +use crate::value::map::parse_map; +use crate::value::point::Point; use anyhow::Result; +use graph_entities::{Edge, Node}; +use path::Path; use std::collections::HashMap; use std::fmt::Debug; pub mod config; +pub mod constraint; +pub mod execution_plan; +pub mod graph_entities; +pub mod map; +pub mod path; +pub mod point; pub mod query_result; pub mod slowlog_entry; #[derive(Clone, Debug)] pub enum FalkorValue { - None, + FNode(Node), + FEdge(Edge), + FArray(Vec), + FMap(HashMap), + FString(String), + FBool(bool), Int64(i64), - UInt64(u64), F64(f64), - FString(String), - FVec(Vec), - FMap(HashMap), + FPoint(Point), + FVector(Vec), + FPath(Path), + None, } macro_rules! impl_to_falkordb_value { @@ -37,91 +54,98 @@ impl_to_falkordb_value!(i8, Self::Int64); impl_to_falkordb_value!(i32, Self::Int64); impl_to_falkordb_value!(i64, Self::Int64); -impl_to_falkordb_value!(u8, Self::UInt64); -impl_to_falkordb_value!(u32, Self::UInt64); -impl_to_falkordb_value!(u64, Self::UInt64); +impl_to_falkordb_value!(u8, Self::Int64); +impl_to_falkordb_value!(u32, Self::Int64); +impl_to_falkordb_value!(u64, Self::Int64); impl_to_falkordb_value!(f32, Self::F64); impl_to_falkordb_value!(f64, Self::F64); -impl TryFrom<&FalkorValue> for i64 { - type Error = FalkorDBError; +impl_to_falkordb_value!(String, Self::FString); - fn try_from(value: &FalkorValue) -> Result { - match value { - FalkorValue::Int64(val) => Some(*val), - FalkorValue::UInt64(val) => (*val).try_into().ok(), - FalkorValue::FString(val) => val.as_str().parse().ok(), - _ => None, - } - .ok_or(FalkorDBError::ParsingError) +impl From<&str> for FalkorValue { + fn from(value: &str) -> Self { + Self::FString(value.to_string()) } } -impl TryFrom for i64 { - type Error = FalkorDBError; - - fn try_from(value: FalkorValue) -> Result { - match value { - FalkorValue::Int64(val) => Some(val), - FalkorValue::UInt64(val) => val.try_into().ok(), - FalkorValue::FString(val) => val.as_str().parse().ok(), - _ => None, - } - .ok_or(FalkorDBError::ParsingError) +impl From> for FalkorValue +where + FalkorValue: From, +{ + fn from(value: Vec) -> Self { + Self::FArray( + value + .into_iter() + .map(|element| FalkorValue::from(element)) + .collect(), + ) } } -impl TryFrom<&FalkorValue> for u64 { - type Error = FalkorDBError; +pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValue)> { + let [type_marker, val]: [FalkorValue; 2] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + let type_marker = type_marker.to_i64().ok_or(FalkorDBError::ParsingError)?; - fn try_from(value: &FalkorValue) -> Result { - match value { - FalkorValue::UInt64(val) => Some(*val), - FalkorValue::Int64(val) => (*val).try_into().ok(), - FalkorValue::FString(val) => val.as_str().parse().ok(), - _ => None, - } - .ok_or(FalkorDBError::ParsingError) - } + Ok((type_marker, val)) } -impl TryFrom for u64 { - type Error = FalkorDBError; +pub(crate) fn parse_type( + type_marker: i64, + val: FalkorValue, + graph_schema: &GraphSchema, + conn: &mut BorrowedSyncConnection, +) -> Result { + let res = match type_marker { + 1 => FalkorValue::None, + 2 => FalkorValue::FString(val.into_string()?), + 3 => FalkorValue::Int64(val.to_i64().ok_or(FalkorDBError::ParsingError)?), + 4 => FalkorValue::FBool(val.to_bool().ok_or(FalkorDBError::ParsingError)?), + 5 => FalkorValue::F64(val.to_f64().ok_or(FalkorDBError::ParsingError)?), + 6 => FalkorValue::FArray({ + let val = val.into_vec()?; + let mut parsed_vec = Vec::with_capacity(val.len()); + for item in val { + let (type_marker, val) = type_val_from_value(item)?; + parsed_vec.push(parse_type(type_marker, val, graph_schema, conn)?); + } + parsed_vec + }), + // The following types are sent as an array and require specific parsing functions + 7 => FalkorValue::FEdge(Edge::parse(val, graph_schema, conn)?), + 8 => FalkorValue::FNode(Node::parse(val, graph_schema, conn)?), + 9 => FalkorValue::FPath(Path::parse(val)?), + 10 => FalkorValue::FMap(parse_map(val, graph_schema, conn)?), + 11 => FalkorValue::FPoint(Point::parse(val)?), + _ => Err(FalkorDBError::ParsingError)?, + }; - fn try_from(value: FalkorValue) -> Result { - match value { - FalkorValue::UInt64(val) => Some(val), - FalkorValue::Int64(val) => val.try_into().ok(), - FalkorValue::FString(val) => val.as_str().parse().ok(), - _ => None, - } - .ok_or(FalkorDBError::ParsingError) - } + Ok(res) } impl FalkorValue { - pub fn as_vec(&self) -> Result<&Vec> { + pub fn as_vec(&self) -> Option<&Vec> { match self { - FalkorValue::FVec(val) => Some(val), + FalkorValue::FArray(val) => Some(val), _ => None, } - .ok_or(FalkorDBError::ParsingError.into()) } pub fn into_vec(self) -> Result> { match self { - FalkorValue::FVec(val) => Some(val), + FalkorValue::FArray(val) => Some(val), _ => None, } - .ok_or(FalkorDBError::ParsingError.into()) + .ok_or_else(|| FalkorDBError::ParsingFArray.into()) } - pub fn as_string(&self) -> Result<&String> { + pub fn as_string(&self) -> Option<&String> { match self { FalkorValue::FString(val) => Some(val), _ => None, } - .ok_or(FalkorDBError::ParsingError.into()) } pub fn into_string(self) -> Result { @@ -129,32 +153,62 @@ impl FalkorValue { FalkorValue::FString(val) => Some(val), _ => None, } - .ok_or(FalkorDBError::ParsingError.into()) + .ok_or(FalkorDBError::ParsingFString.into()) } -} -impl From for FalkorValue { - fn from(value: String) -> Self { - Self::FString(value) + pub fn into_edge(self) -> Result { + match self { + Self::FEdge(edge) => Ok(edge), + _ => Err(FalkorDBError::ParsingFEdge)?, + } } -} -impl From<&str> for FalkorValue { - fn from(value: &str) -> Self { - Self::FString(value.to_string()) + pub fn into_node(self) -> Result { + match self { + Self::FNode(node) => Ok(node), + _ => Err(FalkorDBError::ParsingFNode)?, + } } -} -impl From> for FalkorValue -where - FalkorValue: From, -{ - fn from(value: Vec) -> Self { - Self::FVec( - value - .into_iter() - .map(|element| FalkorValue::from(element)) - .collect(), - ) + pub fn into_path(self) -> Result { + match self { + Self::FPath(path) => Ok(path), + _ => Err(FalkorDBError::ParsingFPath)?, + } + } + + pub fn into_map(self) -> Result> { + match self { + FalkorValue::FMap(map) => Ok(map), + _ => Err(FalkorDBError::ParsingFMap)?, + } + } + + pub fn into_point(self) -> Result { + match self { + Self::FPoint(point) => Ok(point), + _ => Err(FalkorDBError::ParsingFPoint)?, + } + } + + pub fn to_i64(&self) -> Option { + match self { + FalkorValue::Int64(val) => Some(*val), + _ => None, + } + } + + pub fn to_bool(&self) -> Option { + match self { + FalkorValue::FBool(val) => Some(*val), + _ => None, + } + } + + pub fn to_f64(&self) -> Option { + match self { + FalkorValue::F64(val) => Some(*val), + _ => None, + } } } diff --git a/src/value/path.rs b/src/value/path.rs new file mode 100644 index 0000000..566de26 --- /dev/null +++ b/src/value/path.rs @@ -0,0 +1,39 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use crate::error::FalkorDBError; +use crate::value::graph_entities::{Edge, Node}; +use crate::value::FalkorValue; + +#[derive(Clone, Debug)] +pub struct Path { + nodes: Vec, + relationships: Vec, +} + +impl Path { + pub fn parse(value: FalkorValue) -> anyhow::Result { + let [nodes, relationships]: [FalkorValue; 2] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + let (nodes, relationships) = (nodes.into_vec()?, relationships.into_vec()?); + + let mut parsed_nodes = Vec::with_capacity(nodes.len()); + for node_raw in nodes { + parsed_nodes.push(node_raw.into_node()?); + } + + let mut parsed_edges = Vec::with_capacity(relationships.len()); + for edge_raw in relationships { + parsed_edges.push(edge_raw.into_edge()?); + } + + Ok(Path { + nodes: parsed_nodes, + relationships: parsed_edges, + }) + } +} diff --git a/src/value/point.rs b/src/value/point.rs new file mode 100644 index 0000000..eec4f9e --- /dev/null +++ b/src/value/point.rs @@ -0,0 +1,27 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use crate::error::FalkorDBError; +use crate::value::FalkorValue; + +#[derive(Clone, Debug)] +pub struct Point { + latitude: f64, + longitude: f64, +} + +impl Point { + pub fn parse(value: FalkorValue) -> anyhow::Result { + let [lat, long]: [FalkorValue; 2] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + + Ok(Point { + latitude: lat.to_f64().ok_or(FalkorDBError::ParsingError)?, + longitude: long.to_f64().ok_or(FalkorDBError::ParsingError)?, + }) + } +} diff --git a/src/value/query_result.rs b/src/value/query_result.rs index fc83f74..749c022 100644 --- a/src/value/query_result.rs +++ b/src/value/query_result.rs @@ -3,7 +3,11 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::value::FalkorValue; +use crate::connection::blocking::BorrowedSyncConnection; +use crate::error::FalkorDBError; +use crate::graph::schema::GraphSchema; +use crate::value::{parse_type, type_val_from_value, FalkorValue}; +use anyhow::Result; use std::collections::HashMap; #[derive(Clone, Debug, Default)] @@ -13,6 +17,28 @@ pub struct QueryResult { pub(crate) result_set: Vec>, } +fn parse_result_set( + data_vec: Vec, + graph_schema: &GraphSchema, + conn: &mut BorrowedSyncConnection, + header_keys: &[String], +) -> Result>> { + let mut parsed_result_set = Vec::with_capacity(data_vec.len()); + for column in data_vec { + let column_vec = column.into_vec()?; + + let mut parsed_column = Vec::with_capacity(column_vec.len()); + for column_item in column_vec { + let (type_marker, val) = type_val_from_value(column_item)?; + parsed_column.push(parse_type(type_marker, val, graph_schema, conn)?); + } + + parsed_result_set.push(header_keys.iter().cloned().zip(parsed_column).collect()) + } + + Ok(parsed_result_set) +} + impl QueryResult { pub fn stats(&self) -> &[String] { self.stats.as_slice() @@ -29,4 +55,89 @@ impl QueryResult { pub fn take(self) -> (Vec, Vec, Vec>) { (self.stats, self.header, self.result_set) } + + pub(crate) fn from_falkor_value( + value: FalkorValue, + graph_schema: &GraphSchema, + conn: &mut BorrowedSyncConnection, + ) -> Result { + let value_vec = value.into_vec()?; + + // Empty result + if value_vec.is_empty() { + return Ok(QueryResult::default()); + } + + // Result only contains stats + if value_vec.len() == 1 { + let first_element = value_vec.into_iter().nth(0).unwrap(); + return Ok(QueryResult { + stats: query_parse_stats(first_element)?, + ..Default::default() + }); + } + + // Invalid response + if value_vec.len() == 2 { + Err(FalkorDBError::InvalidDataReceived)?; + } + + // Full result + let [header, data, stats]: [FalkorValue; 3] = value_vec + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + + let header_keys = query_parse_header(header)?; + let stats_strings = query_parse_stats(stats)?; + + let data_vec = data.into_vec()?; + let data_len = data_vec.len(); + + let result_set = parse_result_set(data_vec, graph_schema, conn, &header_keys)?; + + if result_set.len() != data_len { + Err(FalkorDBError::ParsingError)?; + } + + Ok(QueryResult { + stats: stats_strings, + header: header_keys, + result_set, + }) + } +} + +fn query_parse_header(header: FalkorValue) -> Result> { + let header_vec = header.into_vec()?; + + let mut keys = Vec::with_capacity(header_vec.len()); + for item in header_vec { + let item_vec = item.into_vec()?; + let key = if item_vec.len() == 2 { + let [_, key]: [FalkorValue; 2] = item_vec + .try_into() + .map_err(|_| FalkorDBError::ParsingHeader)?; + key + } else { + item_vec + .into_iter() + .nth(0) + .ok_or(FalkorDBError::ParsingHeader)? + } + .into_string()?; + keys.push(key); + } + + Ok(keys) +} + +fn query_parse_stats(stats: FalkorValue) -> Result> { + let stats_vec = stats.into_vec()?; + + let mut stats_strings = Vec::with_capacity(stats_vec.len()); + for element in stats_vec { + stats_strings.push(element.into_string()?); + } + + Ok(stats_strings) } diff --git a/src/value/slowlog_entry.rs b/src/value/slowlog_entry.rs index c7c9eb0..916e723 100644 --- a/src/value/slowlog_entry.rs +++ b/src/value/slowlog_entry.rs @@ -3,25 +3,26 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use crate::error::FalkorDBError; use crate::value::FalkorValue; use anyhow::Result; #[derive(Clone, Debug)] pub struct SlowlogEntry { - pub timestamp: u64, + pub timestamp: i64, pub command: String, pub arguments: String, - pub time_taken: u64, + pub time_taken: i64, } impl SlowlogEntry { pub fn from_value_array(values: [FalkorValue; 4]) -> Result { let [timestamp, command, arguments, time_taken] = values; Ok(Self { - timestamp: timestamp.try_into()?, + timestamp: timestamp.to_i64().ok_or(FalkorDBError::ParsingError)?, command: command.into_string()?, arguments: arguments.into_string()?, - time_taken: time_taken.try_into()?, + time_taken: time_taken.to_i64().ok_or(FalkorDBError::ParsingError)?, }) } } From c29dcea9f19c7e16160c7d9440debc5b918b8db4 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 21 May 2024 15:40:15 +0300 Subject: [PATCH 05/62] some more pub fields --- src/graph/blocking.rs | 5 ++++- src/lib.rs | 2 ++ src/value/constraint.rs | 5 +++-- src/value/map.rs | 2 +- src/value/path.rs | 4 ++-- src/value/point.rs | 4 ++-- src/value/query_result.rs | 2 +- 7 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 0294dd5..0cc25d9 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -230,6 +230,9 @@ impl SyncGraph<'_> { let query_res_len = query_res.len(); Ok(query_res .into_iter() - .fold(Vec::with_capacity(query_res_len), |acc, it| acc)) + .fold(Vec::with_capacity(query_res_len), |mut acc, it| { + acc.push(it); + acc + })) } } diff --git a/src/lib.rs b/src/lib.rs index 22f5b2a..7c29cac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,6 +16,8 @@ pub mod value; #[cfg(feature = "redis")] mod redis_ext; +pub use error::FalkorDBError; + // Basic tests for now, to be removed in the future and moved to their respective places, currently requires the server to already be up #[cfg(test)] mod tests { diff --git a/src/value/constraint.rs b/src/value/constraint.rs index 8dc95d6..0f0af06 100644 --- a/src/value/constraint.rs +++ b/src/value/constraint.rs @@ -4,11 +4,12 @@ */ use super::FalkorValue; +use std::collections::HashMap; pub struct Constraint { _type: String, - label: String, - properties: FalkorValue, + pub label: String, + properties: HashMap, entity_type: FalkorValue, status: String, } diff --git a/src/value/map.rs b/src/value/map.rs index f07feb2..7008850 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -68,7 +68,7 @@ pub(crate) fn parse_map_with_schema( ); for item in val_vec { - let mut fktv = FKeyTypeVal::try_from(item)?; + let fktv = FKeyTypeVal::try_from(item)?; id_hashset.insert(fktv.key); map_vec.push(fktv); } diff --git a/src/value/path.rs b/src/value/path.rs index 566de26..30c512c 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -9,8 +9,8 @@ use crate::value::FalkorValue; #[derive(Clone, Debug)] pub struct Path { - nodes: Vec, - relationships: Vec, + pub nodes: Vec, + pub relationships: Vec, } impl Path { diff --git a/src/value/point.rs b/src/value/point.rs index eec4f9e..2d88f97 100644 --- a/src/value/point.rs +++ b/src/value/point.rs @@ -8,8 +8,8 @@ use crate::value::FalkorValue; #[derive(Clone, Debug)] pub struct Point { - latitude: f64, - longitude: f64, + pub latitude: f64, + pub longitude: f64, } impl Point { diff --git a/src/value/query_result.rs b/src/value/query_result.rs index 749c022..d180d84 100644 --- a/src/value/query_result.rs +++ b/src/value/query_result.rs @@ -121,7 +121,7 @@ fn query_parse_header(header: FalkorValue) -> Result> { } else { item_vec .into_iter() - .nth(0) + .next() .ok_or(FalkorDBError::ParsingHeader)? } .into_string()?; From c2ac93dbd13e303245cd9262326d0f7fbb46badc Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 22 May 2024 10:38:53 +0300 Subject: [PATCH 06/62] Expanded error handling and parsing --- src/connection/blocking.rs | 2 +- src/error/mod.rs | 8 +++ src/graph/blocking.rs | 92 ++++++++++------------------ src/graph/mod.rs | 1 + src/graph/schema.rs | 103 ++++++++++++++++++++++++++----- src/graph/utils.rs | 46 ++++++++++++++ src/lib.rs | 117 +++--------------------------------- src/parser/mod.rs | 17 ++++++ src/value/execution_plan.rs | 1 + src/value/graph_entities.rs | 107 +++++++++++++++++++++++++++------ src/value/map.rs | 107 ++++++++------------------------- src/value/mod.rs | 70 ++++++--------------- src/value/query_result.rs | 114 ++++++++++++++++++----------------- src/value/slowlog_entry.rs | 4 +- src/value/utils.rs | 57 ++++++++++++++++++ 15 files changed, 450 insertions(+), 396 deletions(-) create mode 100644 src/graph/utils.rs create mode 100644 src/parser/mod.rs create mode 100644 src/value/utils.rs diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 57b65ff..44c39e1 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -14,7 +14,7 @@ pub enum FalkorSyncConnection { Redis(redis::Connection), } -pub(crate) struct BorrowedSyncConnection { +pub struct BorrowedSyncConnection { pub(crate) conn: Option, pub(crate) return_tx: mpsc::SyncSender, } diff --git a/src/error/mod.rs b/src/error/mod.rs index d2c06ce..5c499cd 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -19,8 +19,16 @@ pub enum FalkorDBError { ParsingError, #[error("Received malformed header")] ParsingHeader, + #[error("The id received for this label/property/relationship was unknown")] + ParsingCompactIdUnknown, #[error("Unknown type")] ParsingUnknownType, + #[error("Element was not of type Bool")] + ParsingBool, + #[error("Element was not of type I64")] + ParsingI64, + #[error("Element was not of type F64")] + ParsingF64, #[error("Element was not of type FArray")] ParsingFArray, #[error("Element was not of type FString")] diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 0cc25d9..40a51d9 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -7,6 +7,8 @@ use crate::client::blocking::SyncFalkorClient; use crate::connection::blocking::FalkorSyncConnection; use crate::error::FalkorDBError; use crate::graph::schema::GraphSchema; +use crate::graph::utils::generate_procedure_call; +use crate::parser::FalkorParsable; use crate::value::execution_plan::ExecutionPlan; use crate::value::query_result::QueryResult; use crate::value::slowlog_entry::SlowlogEntry; @@ -27,9 +29,11 @@ fn construct_query( ) -> String { params .map(|params| { - params.iter().fold(String::new(), |acc, (key, val)| { - acc + format!("{}={}", key.to_string(), val.to_string()).as_str() - }) + params + .iter() + .fold("CYPHER ".to_string(), |acc, (key, val)| { + acc + format!("{}={}", key.to_string(), val.to_string()).as_str() + }) }) .unwrap_or_default() + query_str.to_string().as_str() @@ -107,13 +111,13 @@ impl SyncGraph<'_> { self.explain_with_params::(query_string, None) } - fn query_with_parser( + fn query_with_parser( &self, command: &str, query_string: Q, params: Option<&HashMap>, timeout: Option, - ) -> Result { + ) -> Result

{ let query = construct_query(query_string, params); let mut conn = self.client.borrow_connection()?; @@ -132,16 +136,16 @@ impl SyncGraph<'_> { } }; - QueryResult::from_falkor_value(falkor_result, &self.graph_schema, &mut conn) + P::from_falkor_value(falkor_result, &self.graph_schema, &mut conn) } - pub fn query_with_params( + pub fn query_with_params( &self, query_string: Q, params: Option<&HashMap>, readonly: bool, timeout: Option, - ) -> Result { + ) -> Result

{ self.query_with_parser( if readonly { "GRAPH.RO_QUERY" @@ -155,7 +159,7 @@ impl SyncGraph<'_> { } pub fn query(&self, query_string: Q, timeout: Option) -> Result { - self.query_with_params::(query_string, None, false, timeout) + self.query_with_params::(query_string, None, false, timeout) } pub fn query_readonly( @@ -163,48 +167,18 @@ impl SyncGraph<'_> { query_string: Q, timeout: Option, ) -> Result { - self.query_with_params::(query_string, None, true, timeout) + self.query_with_params::(query_string, None, true, timeout) } - pub fn call_procedure( + pub fn call_procedure( &self, - procedure: P, + procedure: C, args: Option<&[String]>, yields: Option<&[String]>, read_only: Option, timeout: Option, - ) -> Result { - let params = args.map(|args| { - args.iter() - .enumerate() - .fold(HashMap::new(), |mut acc, (idx, param)| { - acc.insert(format!("param{idx}"), param.to_string()); - acc - }) - }); - - let mut query_string = format!( - "CALL {}({})", - procedure.to_string(), - args.unwrap_or_default() - .iter() - .map(|element| format!("${}", element)) - .collect::>() - .join(",") - ); - - let yields = yields.unwrap_or_default(); - if !yields.is_empty() { - query_string += format!( - "YIELD {}", - yields - .iter() - .map(|element| element.to_string()) - .collect::>() - .join(",") - ) - .as_str(); - } + ) -> Result

{ + let (query_string, params) = generate_procedure_call(procedure, args, yields); self.query_with_params( query_string, @@ -214,25 +188,25 @@ impl SyncGraph<'_> { ) } - pub fn list_indices(&self) -> Result>> { + pub fn list_indices(&self) -> Result { let query_res = self - .call_procedure("DB.INDEXES", None, None, None, None)? - .result_set; + .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, None, None)? + .into_vec()?; - Ok(query_res) + for item in query_res { + log::info!("{item:?}"); + } + Ok(FalkorValue::None) } - pub fn list_constraints(&self) -> Result>> { + pub fn list_constraints(&self) -> Result { let query_res = self - .call_procedure("DB.CONSTRAINTS", None, None, None, None)? - .result_set; - - let query_res_len = query_res.len(); - Ok(query_res - .into_iter() - .fold(Vec::with_capacity(query_res_len), |mut acc, it| { - acc.push(it); - acc - })) + .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, None, None)? + .into_vec()?; + + for item in query_res { + log::info!("{item:?}"); + } + Ok(FalkorValue::None) } } diff --git a/src/graph/mod.rs b/src/graph/mod.rs index dbe4761..44bc4b1 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -7,3 +7,4 @@ pub mod asynchronous; pub mod blocking; pub mod schema; +mod utils; diff --git a/src/graph/schema.rs b/src/graph/schema.rs index c0b34bb..e267bfd 100644 --- a/src/graph/schema.rs +++ b/src/graph/schema.rs @@ -3,8 +3,13 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use crate::connection::blocking::BorrowedSyncConnection; +use crate::value::FalkorValue; +use crate::FalkorDBError; +use anyhow::Result; use parking_lot::RwLock; use std::collections::{HashMap, HashSet}; +use std::ops::DerefMut; use std::sync::atomic::AtomicI64; use std::sync::atomic::Ordering::SeqCst; use std::sync::Arc; @@ -46,32 +51,98 @@ impl GraphSchema { self.graph_name.clone() } - pub fn relationships(&self) -> LockableIdMap { + pub(crate) fn relationships(&self) -> LockableIdMap { self.relationships.clone() } - pub fn labels(&self) -> LockableIdMap { + pub(crate) fn labels(&self) -> LockableIdMap { self.labels.clone() } - pub fn properties(&self) -> LockableIdMap { + pub(crate) fn properties(&self) -> LockableIdMap { self.properties.clone() } - pub fn verify_id_set(&self, id_set: &HashSet, schema_type: SchemaType) -> bool { - match schema_type { - SchemaType::Labels => { - let labels = self.labels.read(); - id_set.iter().all(|id| labels.contains_key(id)) + pub fn verify_id_set( + &self, + id_set: &HashSet, + schema_type: SchemaType, + ) -> Option> { + let read_lock = match schema_type { + SchemaType::Labels => &self.labels, + SchemaType::Properties => &self.properties, + SchemaType::Relationships => &self.relationships, + } + .read(); + + // Returns the write lock if + let mut id_hashmap = HashMap::new(); + for id in id_set { + if let Some(id_val) = read_lock.get(id).cloned() { + id_hashmap.insert(*id, id_val); + continue; + } + + return None; + } + + Some(id_hashmap) + } + + pub(crate) fn refresh( + &self, + schema_type: SchemaType, + conn: &mut BorrowedSyncConnection, + id_hashset: Option<&HashSet>, + ) -> Result>> { + let (map, command) = match schema_type { + SchemaType::Labels => (&self.labels, "DB.LABELS"), + SchemaType::Properties => (&self.properties, "DB.PROPERTYKEYS"), + SchemaType::Relationships => (&self.relationships, "DB.RELATIONSHIPTYPES"), + }; + + let mut write_lock = map.write(); + + let [_, keys, _]: [FalkorValue; 3] = conn + .send_command( + Some(self.graph_name.clone()), + "GRAPH.QUERY", + Some(format!("CALL {command}()")), + )? + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + let keys_vec = keys.into_vec()?; + + let mut new_keys = HashMap::with_capacity(keys_vec.len()); + for (idx, item) in keys_vec.into_iter().enumerate() { + let key = item + .into_vec()? + .into_iter() + .next() + .ok_or(FalkorDBError::ParsingError)? + .into_string()?; + new_keys.insert(idx as i64, key); + } + + *write_lock.deref_mut() = new_keys; + + match id_hashset { + None => Ok(None), + Some(id_hashset) => { + let mut relevant_ids = HashMap::with_capacity(id_hashset.len()); + for id in id_hashset { + relevant_ids.insert( + *id, + write_lock + .get(id) + .cloned() + .ok_or(FalkorDBError::ParsingError)?, + ); + } + + Ok(Some(relevant_ids)) } - SchemaType::Properties => id_set.iter().all(|id| { - let properties = self.properties.read(); - properties.contains_key(id) - }), - SchemaType::Relationships => id_set.iter().all(|id| { - let relationships = self.relationships.read(); - relationships.contains_key(id) - }), } } } diff --git a/src/graph/utils.rs b/src/graph/utils.rs new file mode 100644 index 0000000..8cfe81a --- /dev/null +++ b/src/graph/utils.rs @@ -0,0 +1,46 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use std::collections::HashMap; + +pub(crate) fn generate_procedure_call( + procedure: P, + args: Option<&[String]>, + yields: Option<&[String]>, +) -> (String, Option>) { + let params = args.map(|args| { + args.iter() + .enumerate() + .fold(HashMap::new(), |mut acc, (idx, param)| { + acc.insert(format!("param{idx}"), param.to_string()); + acc + }) + }); + + let mut query_string = format!( + "CALL {}({})", + procedure.to_string(), + args.unwrap_or_default() + .iter() + .map(|element| format!("${}", element)) + .collect::>() + .join(",") + ); + + let yields = yields.unwrap_or_default(); + if !yields.is_empty() { + query_string += format!( + "YIELD {}", + yields + .iter() + .map(|element| element.to_string()) + .collect::>() + .join(",") + ) + .as_str(); + } + + (query_string, params) +} diff --git a/src/lib.rs b/src/lib.rs index 7c29cac..5ea79ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -6,119 +6,16 @@ #[cfg(not(feature = "redis"))] compile_error!("The `redis` feature must be enabled."); -pub mod client; -pub mod connection; -pub mod connection_info; +mod client; +mod connection; +mod connection_info; mod error; -pub mod graph; -pub mod value; +mod graph; +pub(crate) mod parser; +mod value; #[cfg(feature = "redis")] mod redis_ext; +pub use client::builder::FalkorDBClientBuilder; pub use error::FalkorDBError; - -// Basic tests for now, to be removed in the future and moved to their respective places, currently requires the server to already be up -#[cfg(test)] -mod tests { - use crate::client::blocking::SyncFalkorClient; - use crate::client::builder::FalkorDBClientBuilder; - use crate::value::config::ConfigValue; - use std::sync::Arc; - - fn create_client() -> Arc { - FalkorDBClientBuilder::new() - .with_num_connections(4) - .build() - .expect("Could not create falkor client") - } - - #[test] - fn test_config_get_set() { - let client = create_client(); - - let res = client.config_get("CMD_INFO"); - assert!(res.is_ok()); - assert_eq!(res.unwrap()["CMD_INFO"], ConfigValue::Int64(1)); - - assert!(client.config_set("CMD_INFO", "no").is_ok()); - - let res = client.config_get("CMD_INFO"); - assert!(res.is_ok()); - assert_eq!(res.unwrap()["CMD_INFO"], ConfigValue::Int64(0)); - } - - #[test] - fn test_config_get_all() { - let client = create_client(); - - let res = client.config_get("*"); - assert!(res.is_ok()); - - // Add some more here, maybe even test all values, but make sure it stays BC - assert_eq!( - res.as_ref().unwrap()["EFFECTS_THRESHOLD"], - ConfigValue::Int64(300) - ); - assert_eq!( - res.as_ref().unwrap()["MAX_QUEUED_QUERIES"], - ConfigValue::Int64(4294967295) - ); - } - - // #[test] - // fn test_graph_query() { - // let client = create_client(); - // let graph = client.open_graph("imdb"); - // - // let res = graph.query( - // "MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100", - // ); - // assert!(res.is_ok()); - // - // let res = res.unwrap().into_vec(); - // assert!(res.is_ok()); - // - // let res = res.unwrap(); - // assert_eq!(res.len(), 3); - // - // let res = res[2].clone().into_vec(); - // assert!(res.is_ok()); - // - // assert_eq!(res.unwrap().len(), 100); - // } - - // Race condition here with slowlog_reset test, So ignore this, will be fixed with multi-instance testing - #[test] - #[ignore] - fn test_graph_slowlog() { - let client = create_client(); - let graph = client.open_graph("imdb"); - - assert!(graph.query( - "MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 2", - ).is_ok()); - - let res = graph.slowlog(); - assert!(res.is_ok()); - - assert_eq!(res.unwrap()[0].arguments, "MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 2".to_string()); - } - - #[test] - fn test_graph_slowlog_reset() { - let client = create_client(); - let graph = client.open_graph("imdb"); - - assert!(graph.query( - "MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 10", - ).is_ok()); - - assert!(graph.slowlog_reset().is_ok()); - - let res = graph.slowlog(); - assert!(res.is_ok()); - - assert!(res.unwrap().is_empty()); - } -} diff --git a/src/parser/mod.rs b/src/parser/mod.rs new file mode 100644 index 0000000..ebb62c0 --- /dev/null +++ b/src/parser/mod.rs @@ -0,0 +1,17 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use crate::connection::blocking::BorrowedSyncConnection; +use crate::graph::schema::GraphSchema; +use crate::value::FalkorValue; +use anyhow::Result; + +pub trait FalkorParsable: Sized { + fn from_falkor_value( + value: FalkorValue, + graph_schema: &GraphSchema, + conn: &mut BorrowedSyncConnection, + ) -> Result; +} diff --git a/src/value/execution_plan.rs b/src/value/execution_plan.rs index 8efa34a..b194c86 100644 --- a/src/value/execution_plan.rs +++ b/src/value/execution_plan.rs @@ -30,6 +30,7 @@ impl TryFrom for ExecutionPlan { Vec::with_capacity(string_vec.len()), Vec::with_capacity(string_vec.len()), ); + execution_plan_text.push(String::new()); for item in string_vec { let raw_text = item.into_string()?; execution_plan.push(raw_text.trim().to_string()); diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index ea6a097..1a6af15 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -9,15 +9,51 @@ use crate::graph::schema::{GraphSchema, SchemaType}; use crate::value::map::parse_map_with_schema; use crate::value::FalkorValue; use anyhow::Result; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; #[derive(Clone, Debug)] pub struct Node { pub entity_id: i64, - pub labels: Vec, + pub labels: Vec, pub properties: HashMap, } +pub(crate) fn parse_labels( + raw_ids: Vec, + graph_schema: &GraphSchema, + conn: &mut BorrowedSyncConnection, + schema_type: SchemaType, +) -> Result> { + let mut ids_hashset = HashSet::with_capacity(raw_ids.len()); + for label in raw_ids.iter() { + ids_hashset.insert(label.to_i64().ok_or(FalkorDBError::ParsingI64)?); + } + + match match graph_schema.verify_id_set(&ids_hashset, schema_type) { + None => graph_schema.refresh(schema_type, conn, Some(&ids_hashset))?, + relevant_ids => relevant_ids, + } { + Some(relevant_ids) => { + let mut parsed_ids = Vec::with_capacity(raw_ids.len()); + for id in raw_ids { + parsed_ids.push( + id.to_i64() + .ok_or(FalkorDBError::ParsingI64) + .and_then(|id| { + relevant_ids + .get(&id) + .cloned() + .ok_or(FalkorDBError::ParsingCompactIdUnknown) + })?, + ); + } + + Ok(parsed_ids) + } + _ => Err(FalkorDBError::ParsingError)?, + } +} + impl Node { pub(crate) fn parse( value: FalkorValue, @@ -30,13 +66,18 @@ impl Node { .map_err(|_| FalkorDBError::ParsingNodeElementCount)?; let labels = labels.into_vec()?; - let mut parsed_labels = Vec::with_capacity(labels.len()); - for label in labels { - parsed_labels.push(label.to_i64().ok_or(FalkorDBError::ParsingError)?); + let mut ids_hashset = HashSet::with_capacity(labels.len()); + for label in labels.iter() { + ids_hashset.insert( + label + .to_i64() + .ok_or(FalkorDBError::ParsingCompactIdUnknown)?, + ); } + let parsed_labels = parse_labels(labels, graph_schema, conn, SchemaType::Labels)?; Ok(Node { - entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingError)?, + entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, labels: parsed_labels, properties: parse_map_with_schema( properties, @@ -51,7 +92,7 @@ impl Node { #[derive(Clone, Debug)] pub struct Edge { pub entity_id: i64, - pub labels: i64, + pub relationship_type: String, pub src_node_id: i64, pub dst_node_id: i64, pub properties: HashMap, @@ -63,22 +104,48 @@ impl Edge { graph_schema: &GraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { - let [entity_id, labels, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value + let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingError)?; - Ok(Edge { - entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingError)?, - labels: labels.to_i64().ok_or(FalkorDBError::ParsingError)?, - src_node_id: src_node_id.to_i64().ok_or(FalkorDBError::ParsingError)?, - dst_node_id: dst_node_id.to_i64().ok_or(FalkorDBError::ParsingError)?, - properties: parse_map_with_schema( - properties, - graph_schema, - conn, - SchemaType::Properties, - )?, - }) + let relation = relations.to_i64().ok_or(FalkorDBError::ParsingI64)?; + if let Some(relationship) = graph_schema.relationships().read().get(&relation).cloned() { + return Ok(Edge { + entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + relationship_type: relationship, + src_node_id: src_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + dst_node_id: dst_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + properties: parse_map_with_schema( + properties, + graph_schema, + conn, + SchemaType::Properties, + )?, + }); + } + + match graph_schema.refresh( + SchemaType::Relationships, + conn, + Some(&HashSet::from([relation])), + )? { + None => Err(FalkorDBError::ParsingCompactIdUnknown)?, + Some(id) => Ok(Edge { + entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + relationship_type: id + .get(&relation) + .cloned() + .ok_or(FalkorDBError::ParsingCompactIdUnknown)?, + src_node_id: src_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + dst_node_id: dst_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + properties: parse_map_with_schema( + properties, + graph_schema, + conn, + SchemaType::Properties, + )?, + }), + } } } diff --git a/src/value/map.rs b/src/value/map.rs index 7008850..5021dbb 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -6,9 +6,10 @@ use crate::connection::blocking::BorrowedSyncConnection; use crate::error::FalkorDBError; use crate::graph::schema::{GraphSchema, SchemaType}; -use crate::value::{parse_type, FalkorValue}; +use crate::value::utils::parse_type; +use crate::value::FalkorValue; +use anyhow::Result; use std::collections::{HashMap, HashSet}; -use std::ops::{Deref, DerefMut}; // Intermediate type for map parsing pub(crate) struct FKeyTypeVal { @@ -42,16 +43,24 @@ impl TryFrom for FKeyTypeVal { } } -pub(crate) fn get_schema_subset( - ids: &HashSet, - schema: &HashMap, -) -> Option> { - let mut hashmap = HashMap::new(); - for id in ids { - hashmap.insert(*id, schema.get(id).cloned()?); +fn ktv_vec_to_map( + map_vec: Vec, + relevant_ids_map: HashMap, + graph_schema: &GraphSchema, + conn: &mut BorrowedSyncConnection, +) -> Result> { + let mut new_map = HashMap::with_capacity(map_vec.len()); + for fktv in map_vec { + new_map.insert( + relevant_ids_map + .get(&fktv.key) + .cloned() + .ok_or(FalkorDBError::ParsingError)?, + parse_type(fktv.type_marker, fktv.val, graph_schema, conn)?, + ); } - Some(hashmap) + Ok(new_map) } // TODO: This does NOT support nested attributes yet @@ -60,7 +69,7 @@ pub(crate) fn parse_map_with_schema( graph_schema: &GraphSchema, conn: &mut BorrowedSyncConnection, schema_type: SchemaType, -) -> anyhow::Result> { +) -> Result> { let val_vec = value.into_vec()?; let (mut id_hashset, mut map_vec) = ( HashSet::with_capacity(val_vec.len()), @@ -73,81 +82,15 @@ pub(crate) fn parse_map_with_schema( map_vec.push(fktv); } - let locked_id_to_string_map = match schema_type { - SchemaType::Labels => graph_schema.labels(), - SchemaType::Properties => graph_schema.properties(), - SchemaType::Relationships => graph_schema.relationships(), - }; - - let relevant_ids_map = { - let read_lock = locked_id_to_string_map.read(); - get_schema_subset(&id_hashset, read_lock.deref()) - }; - - if let Some(relevant_ids_map) = relevant_ids_map { - let mut new_map = HashMap::with_capacity(map_vec.len()); - for fktv in map_vec { - new_map.insert( - relevant_ids_map - .get(&fktv.key) - .cloned() - .ok_or(FalkorDBError::ParsingError)?, - parse_type(fktv.type_marker, fktv.val, graph_schema, conn)?, - ); - } - - return Ok(new_map); + if let Some(relevant_ids_map) = graph_schema.verify_id_set(&id_hashset, schema_type) { + return ktv_vec_to_map(map_vec, relevant_ids_map, graph_schema, conn); } // If we reached here, schema validation failed and we need to refresh our schema - let command = match schema_type { - SchemaType::Labels => "DB.LABELS", - SchemaType::Properties => "DB.PROPERTYKEYS", - SchemaType::Relationships => "DB.RELATIONSHIPTYPES", - }; - let [_, keys, _]: [FalkorValue; 3] = conn - .send_command( - Some(graph_schema.graph_name()), - "GRAPH.QUERY", - Some(format!("CALL {command}()")), - )? - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; - let keys_vec = keys.into_vec()?; - - let mut new_keys = HashMap::with_capacity(keys_vec.len()); - for (idx, item) in keys_vec.into_iter().enumerate() { - let key = item - .into_vec()? - .into_iter() - .next() - .ok_or(FalkorDBError::ParsingError)? - .into_string()?; - new_keys.insert(idx as i64, key); - } - - let id_map = match schema_type { - SchemaType::Labels => graph_schema.labels(), - SchemaType::Properties => graph_schema.properties(), - SchemaType::Relationships => graph_schema.relationships(), - }; - - let mut lock = id_map.write(); - *(lock.deref_mut()) = new_keys; - - let mut new_map = HashMap::with_capacity(map_vec.len()); - for fktv in map_vec { - new_map.insert( - lock.deref() - .get(&fktv.key) - .cloned() - .ok_or(FalkorDBError::ParsingError)?, - parse_type(fktv.type_marker, fktv.val, graph_schema, conn)?, - ); + match graph_schema.refresh(schema_type, conn, Some(&id_hashset))? { + Some(relevant_ids_map) => ktv_vec_to_map(map_vec, relevant_ids_map, graph_schema, conn), + None => Err(FalkorDBError::ParsingError)?, } - - Ok(new_map) } pub(crate) fn parse_map( diff --git a/src/value/mod.rs b/src/value/mod.rs index a8f2036..6fe04ea 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -6,7 +6,7 @@ use crate::connection::blocking::BorrowedSyncConnection; use crate::error::FalkorDBError; use crate::graph::schema::GraphSchema; -use crate::value::map::parse_map; +use crate::parser::FalkorParsable; use crate::value::point::Point; use anyhow::Result; use graph_entities::{Edge, Node}; @@ -14,15 +14,16 @@ use path::Path; use std::collections::HashMap; use std::fmt::Debug; -pub mod config; -pub mod constraint; -pub mod execution_plan; -pub mod graph_entities; -pub mod map; -pub mod path; -pub mod point; -pub mod query_result; -pub mod slowlog_entry; +pub(crate) mod config; +pub(crate) mod constraint; +pub(crate) mod execution_plan; +pub(crate) mod graph_entities; +pub(crate) mod map; +pub(crate) mod path; +pub(crate) mod point; +pub(crate) mod query_result; +pub(crate) mod slowlog_entry; +pub(crate) mod utils; #[derive(Clone, Debug)] pub enum FalkorValue { @@ -83,47 +84,14 @@ where } } -pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValue)> { - let [type_marker, val]: [FalkorValue; 2] = value - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; - let type_marker = type_marker.to_i64().ok_or(FalkorDBError::ParsingError)?; - - Ok((type_marker, val)) -} - -pub(crate) fn parse_type( - type_marker: i64, - val: FalkorValue, - graph_schema: &GraphSchema, - conn: &mut BorrowedSyncConnection, -) -> Result { - let res = match type_marker { - 1 => FalkorValue::None, - 2 => FalkorValue::FString(val.into_string()?), - 3 => FalkorValue::Int64(val.to_i64().ok_or(FalkorDBError::ParsingError)?), - 4 => FalkorValue::FBool(val.to_bool().ok_or(FalkorDBError::ParsingError)?), - 5 => FalkorValue::F64(val.to_f64().ok_or(FalkorDBError::ParsingError)?), - 6 => FalkorValue::FArray({ - let val = val.into_vec()?; - let mut parsed_vec = Vec::with_capacity(val.len()); - for item in val { - let (type_marker, val) = type_val_from_value(item)?; - parsed_vec.push(parse_type(type_marker, val, graph_schema, conn)?); - } - parsed_vec - }), - // The following types are sent as an array and require specific parsing functions - 7 => FalkorValue::FEdge(Edge::parse(val, graph_schema, conn)?), - 8 => FalkorValue::FNode(Node::parse(val, graph_schema, conn)?), - 9 => FalkorValue::FPath(Path::parse(val)?), - 10 => FalkorValue::FMap(parse_map(val, graph_schema, conn)?), - 11 => FalkorValue::FPoint(Point::parse(val)?), - _ => Err(FalkorDBError::ParsingError)?, - }; - - Ok(res) +impl FalkorParsable for FalkorValue { + fn from_falkor_value( + value: FalkorValue, + _graph_schema: &GraphSchema, + _conn: &mut BorrowedSyncConnection, + ) -> Result { + Ok(value) + } } impl FalkorValue { diff --git a/src/value/query_result.rs b/src/value/query_result.rs index d180d84..0104891 100644 --- a/src/value/query_result.rs +++ b/src/value/query_result.rs @@ -6,7 +6,9 @@ use crate::connection::blocking::BorrowedSyncConnection; use crate::error::FalkorDBError; use crate::graph::schema::GraphSchema; -use crate::value::{parse_type, type_val_from_value, FalkorValue}; +use crate::parser::FalkorParsable; +use crate::value::utils::{parse_type, type_val_from_value}; +use crate::value::FalkorValue; use anyhow::Result; use std::collections::HashMap; @@ -17,6 +19,59 @@ pub struct QueryResult { pub(crate) result_set: Vec>, } +impl QueryResult { + pub fn stats(&self) -> &[String] { + self.stats.as_slice() + } + + pub fn header(&self) -> &[String] { + self.header.as_slice() + } + + pub fn result_set(&self) -> &[HashMap] { + self.result_set.as_slice() + } + + pub fn take(self) -> (Vec, Vec, Vec>) { + (self.stats, self.header, self.result_set) + } +} + +fn query_parse_header(header: FalkorValue) -> Result> { + let header_vec = header.into_vec()?; + + let mut keys = Vec::with_capacity(header_vec.len()); + for item in header_vec { + let item_vec = item.into_vec()?; + let key = if item_vec.len() == 2 { + let [_, key]: [FalkorValue; 2] = item_vec + .try_into() + .map_err(|_| FalkorDBError::ParsingHeader)?; + key + } else { + item_vec + .into_iter() + .next() + .ok_or(FalkorDBError::ParsingHeader)? + } + .into_string()?; + keys.push(key); + } + + Ok(keys) +} + +fn query_parse_stats(stats: FalkorValue) -> Result> { + let stats_vec = stats.into_vec()?; + + let mut stats_strings = Vec::with_capacity(stats_vec.len()); + for element in stats_vec { + stats_strings.push(element.into_string()?); + } + + Ok(stats_strings) +} + fn parse_result_set( data_vec: Vec, graph_schema: &GraphSchema, @@ -39,24 +94,8 @@ fn parse_result_set( Ok(parsed_result_set) } -impl QueryResult { - pub fn stats(&self) -> &[String] { - self.stats.as_slice() - } - - pub fn header(&self) -> &[String] { - self.header.as_slice() - } - - pub fn result_set(&self) -> &[HashMap] { - self.result_set.as_slice() - } - - pub fn take(self) -> (Vec, Vec, Vec>) { - (self.stats, self.header, self.result_set) - } - - pub(crate) fn from_falkor_value( +impl FalkorParsable for QueryResult { + fn from_falkor_value( value: FalkorValue, graph_schema: &GraphSchema, conn: &mut BorrowedSyncConnection, @@ -99,45 +138,10 @@ impl QueryResult { Err(FalkorDBError::ParsingError)?; } - Ok(QueryResult { + Ok(Self { stats: stats_strings, header: header_keys, result_set, }) } } - -fn query_parse_header(header: FalkorValue) -> Result> { - let header_vec = header.into_vec()?; - - let mut keys = Vec::with_capacity(header_vec.len()); - for item in header_vec { - let item_vec = item.into_vec()?; - let key = if item_vec.len() == 2 { - let [_, key]: [FalkorValue; 2] = item_vec - .try_into() - .map_err(|_| FalkorDBError::ParsingHeader)?; - key - } else { - item_vec - .into_iter() - .next() - .ok_or(FalkorDBError::ParsingHeader)? - } - .into_string()?; - keys.push(key); - } - - Ok(keys) -} - -fn query_parse_stats(stats: FalkorValue) -> Result> { - let stats_vec = stats.into_vec()?; - - let mut stats_strings = Vec::with_capacity(stats_vec.len()); - for element in stats_vec { - stats_strings.push(element.into_string()?); - } - - Ok(stats_strings) -} diff --git a/src/value/slowlog_entry.rs b/src/value/slowlog_entry.rs index 916e723..fe34afd 100644 --- a/src/value/slowlog_entry.rs +++ b/src/value/slowlog_entry.rs @@ -19,10 +19,10 @@ impl SlowlogEntry { pub fn from_value_array(values: [FalkorValue; 4]) -> Result { let [timestamp, command, arguments, time_taken] = values; Ok(Self { - timestamp: timestamp.to_i64().ok_or(FalkorDBError::ParsingError)?, + timestamp: timestamp.to_i64().ok_or(FalkorDBError::ParsingI64)?, command: command.into_string()?, arguments: arguments.into_string()?, - time_taken: time_taken.to_i64().ok_or(FalkorDBError::ParsingError)?, + time_taken: time_taken.to_i64().ok_or(FalkorDBError::ParsingI64)?, }) } } diff --git a/src/value/utils.rs b/src/value/utils.rs new file mode 100644 index 0000000..883c282 --- /dev/null +++ b/src/value/utils.rs @@ -0,0 +1,57 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use crate::connection::blocking::BorrowedSyncConnection; +use crate::graph::schema::GraphSchema; +use crate::value::graph_entities::{Edge, Node}; +use crate::value::map::parse_map; +use crate::value::path::Path; +use crate::value::point::Point; +use crate::value::FalkorValue; +use crate::FalkorDBError; +use anyhow::Result; + +pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValue)> { + let [type_marker, val]: [FalkorValue; 2] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + 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: &GraphSchema, + conn: &mut BorrowedSyncConnection, +) -> Result { + let res = match type_marker { + 1 => FalkorValue::None, + 2 => FalkorValue::FString(val.into_string()?), + 3 => FalkorValue::Int64(val.to_i64().ok_or(FalkorDBError::ParsingI64)?), + 4 => FalkorValue::FBool(val.to_bool().ok_or(FalkorDBError::ParsingBool)?), + 5 => FalkorValue::F64(val.to_f64().ok_or(FalkorDBError::ParsingF64)?), + 6 => FalkorValue::FArray({ + let val = val.into_vec()?; + let mut parsed_vec = Vec::with_capacity(val.len()); + for item in val { + let (type_marker, val) = type_val_from_value(item)?; + parsed_vec.push(parse_type(type_marker, val, graph_schema, conn)?); + } + parsed_vec + }), + // The following types are sent as an array and require specific parsing functions + 7 => FalkorValue::FEdge(Edge::parse(val, graph_schema, conn)?), + 8 => FalkorValue::FNode(Node::parse(val, graph_schema, conn)?), + 9 => FalkorValue::FPath(Path::parse(val)?), + 10 => FalkorValue::FMap(parse_map(val, graph_schema, conn)?), + 11 => FalkorValue::FPoint(Point::parse(val)?), + _ => Err(FalkorDBError::ParsingUnknownType)?, + }; + + Ok(res) +} From 5d4ed38a08445c985b97a27cf92063b03d5cd790 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 22 May 2024 11:50:42 +0300 Subject: [PATCH 07/62] Update visibilities, begin impl asynchronous --- src/client/asynchronous.rs | 4 +- src/client/blocking.rs | 18 +-- src/client/builder.rs | 10 +- src/client/mod.rs | 10 +- src/connection/asynchronous.rs | 22 ++++ src/connection/blocking.rs | 3 +- src/connection/mod.rs | 5 +- src/graph/blocking.rs | 19 ++-- src/graph/mod.rs | 8 +- src/graph_schema/asynchronous.rs | 106 ++++++++++++++++++ .../schema.rs => graph_schema/blocking.rs} | 82 ++++---------- src/graph_schema/mod.rs | 17 +++ src/graph_schema/utils.rs | 72 ++++++++++++ src/lib.rs | 26 ++++- src/parser/mod.rs | 24 +++- src/redis_ext.rs | 12 +- src/value/graph_entities.rs | 9 +- src/value/map.rs | 10 +- src/value/mod.rs | 13 +-- src/value/query_result.rs | 11 +- src/value/utils.rs | 8 +- 21 files changed, 343 insertions(+), 146 deletions(-) create mode 100644 src/graph_schema/asynchronous.rs rename src/{graph/schema.rs => graph_schema/blocking.rs} (51%) create mode 100644 src/graph_schema/mod.rs create mode 100644 src/graph_schema/utils.rs diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index 3069484..f928e41 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -3,9 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::client::FalkorClientImpl; -use crate::connection::asynchronous::FalkorAsyncConnection; -use crate::error::FalkorDBError; +use crate::{client::FalkorClientImpl, FalkorAsyncConnection, FalkorDBError}; use anyhow::Result; use std::sync::Arc; use tokio::runtime::Runtime; diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 4de109d..e239326 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -3,15 +3,17 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::client::FalkorClientImpl; -use crate::connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}; -use crate::error::FalkorDBError; -use crate::graph::blocking::SyncGraph; -use crate::graph::schema::GraphSchema; -use crate::value::config::ConfigValue; +use crate::{ + client::FalkorClientImpl, + connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}, + graph_schema::blocking::GraphSchema, + ConfigValue, FalkorDBError, SyncGraph, +}; use anyhow::Result; -use std::collections::HashMap; -use std::sync::{mpsc, Arc}; +use std::{ + collections::HashMap, + sync::{mpsc, Arc}, +}; pub struct SyncFalkorClient { _inner: FalkorClientImpl, diff --git a/src/client/builder.rs b/src/client/builder.rs index 5ee7183..200d1b3 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -3,11 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::client::asynchronous::AsyncFalkorClient; -use crate::client::blocking::SyncFalkorClient; -use crate::client::FalkorClientImpl; -use crate::connection_info::FalkorConnectionInfo; -use crate::error::FalkorDBError; +use crate::{client::FalkorClientImpl, FalkorConnectionInfo, FalkorDBError, SyncFalkorClient}; use anyhow::Result; use std::sync::Arc; @@ -102,7 +98,7 @@ impl FalkorDBClientBuilder<'A'> { } } - pub async fn build(self) -> Result> { + pub async fn build(self) -> Result> { let connection_info = self .connection_info .unwrap_or("falkor://127.0.0.1:6379".try_into()?); @@ -116,6 +112,6 @@ impl FalkorDBClientBuilder<'A'> { .build()?, ); - AsyncFalkorClient::create(get_client(connection_info)?, runtime).await + crate::AsyncFalkorClient::create(get_client(connection_info)?, runtime).await } } diff --git a/src/client/mod.rs b/src/client/mod.rs index 611389b..340e440 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -3,15 +3,15 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::asynchronous::FalkorAsyncConnection; use crate::connection::blocking::FalkorSyncConnection; use anyhow::Result; use std::time::Duration; +pub(crate) mod blocking; +pub(crate) mod builder; + #[cfg(feature = "tokio")] -pub mod asynchronous; -pub mod blocking; -pub mod builder; +pub(crate) mod asynchronous; pub(crate) enum FalkorClientImpl { #[cfg(feature = "redis")] @@ -36,7 +36,7 @@ impl FalkorClientImpl { pub(crate) async fn get_async_connection( &self, connection_timeout: Option, - ) -> Result { + ) -> Result { Ok(match self { FalkorClientImpl::Redis(redis_client) => match connection_timeout { Some(timeout) => { diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs index 59ee586..bbdcf0e 100644 --- a/src/connection/asynchronous.rs +++ b/src/connection/asynchronous.rs @@ -3,8 +3,30 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use crate::FalkorValue; + #[derive(Clone)] pub enum FalkorAsyncConnection { #[cfg(feature = "redis")] Redis(redis::aio::MultiplexedConnection), } + +impl FalkorAsyncConnection { + pub(crate) async fn send_command( + &mut self, + graph_name: Option, + command: &str, + params: Option, + ) -> anyhow::Result { + Ok(match self { + #[cfg(feature = "redis")] + FalkorAsyncConnection::Redis(redis_conn) => { + redis::FromRedisValue::from_owned_redis_value( + redis_conn + .send_packed_command(redis::cmd(command).arg(graph_name).arg(params)) + .await?, + )? + } + }) + } +} diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 44c39e1..1c198ca 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -3,8 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::error::FalkorDBError; -use crate::value::FalkorValue; +use crate::{FalkorDBError, FalkorValue}; use anyhow::Result; use redis::ConnectionLike; use std::sync::mpsc; diff --git a/src/connection/mod.rs b/src/connection/mod.rs index a6d5b94..e4630fb 100644 --- a/src/connection/mod.rs +++ b/src/connection/mod.rs @@ -3,6 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +pub(crate) mod blocking; + #[cfg(feature = "tokio")] -pub mod asynchronous; -pub mod blocking; +pub(crate) mod asynchronous; diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 40a51d9..bbcf417 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -3,16 +3,11 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::client::blocking::SyncFalkorClient; -use crate::connection::blocking::FalkorSyncConnection; -use crate::error::FalkorDBError; -use crate::graph::schema::GraphSchema; -use crate::graph::utils::generate_procedure_call; -use crate::parser::FalkorParsable; -use crate::value::execution_plan::ExecutionPlan; -use crate::value::query_result::QueryResult; -use crate::value::slowlog_entry::SlowlogEntry; -use crate::value::FalkorValue; +use super::utils::generate_procedure_call; +use crate::{ + connection::blocking::FalkorSyncConnection, graph_schema::blocking::GraphSchema, ExecutionPlan, + FalkorDBError, FalkorParsable, FalkorValue, QueryResult, SlowlogEntry, SyncFalkorClient, +}; use anyhow::Result; use redis::ConnectionLike; use std::collections::HashMap; @@ -205,7 +200,9 @@ impl SyncGraph<'_> { .into_vec()?; for item in query_res { - log::info!("{item:?}"); + for sub_item in item.into_vec()? { + log::info!("{sub_item:?}"); + } } Ok(FalkorValue::None) } diff --git a/src/graph/mod.rs b/src/graph/mod.rs index 44bc4b1..a0e8ac6 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -3,8 +3,8 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -#[cfg(feature = "tokio")] -pub mod asynchronous; -pub mod blocking; -pub mod schema; +pub(crate) mod blocking; mod utils; + +#[cfg(feature = "tokio")] +pub(crate) mod asynchronous; diff --git a/src/graph_schema/asynchronous.rs b/src/graph_schema/asynchronous.rs new file mode 100644 index 0000000..857c564 --- /dev/null +++ b/src/graph_schema/asynchronous.rs @@ -0,0 +1,106 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use super::utils::{get_refresh_command, get_relevant_hashmap, update_map}; +use crate::{value::FalkorValue, FalkorAsyncConnection, FalkorDBError, SchemaType}; +use anyhow::Result; +use std::{ + collections::{HashMap, HashSet}, + ops::{Deref, DerefMut}, + sync::{ + atomic::{AtomicI64, Ordering::SeqCst}, + Arc, + }, +}; +use tokio::sync::RwLock; + +pub(crate) type LockableIdMap = Arc>>; + +#[derive(Clone, Debug, Default)] +pub struct GraphSchema { + graph_name: String, + version: Arc, + labels: LockableIdMap, + properties: LockableIdMap, + relationships: LockableIdMap, +} + +impl GraphSchema { + pub fn new(graph_name: String) -> Self { + Self { + graph_name, + ..Default::default() + } + } + + pub async fn clear(&mut self) { + self.version.store(0, SeqCst); + self.labels.write().await.clear(); + self.properties.write().await.clear(); + self.relationships.write().await.clear(); + } + + pub fn graph_name(&self) -> String { + self.graph_name.clone() + } + + pub fn relationships(&self) -> LockableIdMap { + self.relationships.clone() + } + + pub fn labels(&self) -> LockableIdMap { + self.labels.clone() + } + + pub fn properties(&self) -> LockableIdMap { + self.properties.clone() + } + + pub(crate) async fn verify_id_set( + &self, + id_set: &HashSet, + schema_type: SchemaType, + ) -> Option> { + let read_lock = match schema_type { + SchemaType::Labels => &self.labels, + SchemaType::Properties => &self.properties, + SchemaType::Relationships => &self.relationships, + } + .read() + .await; + + get_relevant_hashmap(id_set, read_lock.deref()) + } + + pub(crate) async fn refresh( + &self, + schema_type: SchemaType, + conn: &mut FalkorAsyncConnection, + id_hashset: Option<&HashSet>, + ) -> Result>> { + let command = get_refresh_command(schema_type); + let map = match schema_type { + SchemaType::Labels => &self.labels, + SchemaType::Properties => &self.properties, + SchemaType::Relationships => &self.relationships, + }; + + let mut write_lock = map.write().await; + + // 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] = conn + .send_command( + Some(self.graph_name.clone()), + "GRAPH.QUERY", + Some(format!("CALL {command}()")), + ) + .await? + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + + update_map(write_lock.deref_mut(), keys, id_hashset) + } +} diff --git a/src/graph/schema.rs b/src/graph_schema/blocking.rs similarity index 51% rename from src/graph/schema.rs rename to src/graph_schema/blocking.rs index e267bfd..bc4be9d 100644 --- a/src/graph/schema.rs +++ b/src/graph_schema/blocking.rs @@ -3,23 +3,20 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::BorrowedSyncConnection; -use crate::value::FalkorValue; -use crate::FalkorDBError; +use super::utils::{get_refresh_command, get_relevant_hashmap, update_map}; +use crate::{ + connection::blocking::BorrowedSyncConnection, value::FalkorValue, FalkorDBError, SchemaType, +}; use anyhow::Result; use parking_lot::RwLock; -use std::collections::{HashMap, HashSet}; -use std::ops::DerefMut; -use std::sync::atomic::AtomicI64; -use std::sync::atomic::Ordering::SeqCst; -use std::sync::Arc; - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum SchemaType { - Labels, - Properties, - Relationships, -} +use std::{ + collections::{HashMap, HashSet}, + ops::{Deref, DerefMut}, + sync::{ + atomic::{AtomicI64, Ordering::SeqCst}, + Arc, + }, +}; pub(crate) type LockableIdMap = Arc>>; @@ -75,18 +72,7 @@ impl GraphSchema { } .read(); - // Returns the write lock if - let mut id_hashmap = HashMap::new(); - for id in id_set { - if let Some(id_val) = read_lock.get(id).cloned() { - id_hashmap.insert(*id, id_val); - continue; - } - - return None; - } - - Some(id_hashmap) + get_relevant_hashmap(id_set, read_lock.deref()) } pub(crate) fn refresh( @@ -95,14 +81,16 @@ impl GraphSchema { conn: &mut BorrowedSyncConnection, id_hashset: Option<&HashSet>, ) -> Result>> { - let (map, command) = match schema_type { - SchemaType::Labels => (&self.labels, "DB.LABELS"), - SchemaType::Properties => (&self.properties, "DB.PROPERTYKEYS"), - SchemaType::Relationships => (&self.relationships, "DB.RELATIONSHIPTYPES"), + let command = get_refresh_command(schema_type); + let map = match schema_type { + SchemaType::Labels => &self.labels, + SchemaType::Properties => &self.properties, + SchemaType::Relationships => &self.relationships, }; let mut write_lock = map.write(); + // 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] = conn .send_command( Some(self.graph_name.clone()), @@ -112,37 +100,7 @@ impl GraphSchema { .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingError)?; - let keys_vec = keys.into_vec()?; - - let mut new_keys = HashMap::with_capacity(keys_vec.len()); - for (idx, item) in keys_vec.into_iter().enumerate() { - let key = item - .into_vec()? - .into_iter() - .next() - .ok_or(FalkorDBError::ParsingError)? - .into_string()?; - new_keys.insert(idx as i64, key); - } - *write_lock.deref_mut() = new_keys; - - match id_hashset { - None => Ok(None), - Some(id_hashset) => { - let mut relevant_ids = HashMap::with_capacity(id_hashset.len()); - for id in id_hashset { - relevant_ids.insert( - *id, - write_lock - .get(id) - .cloned() - .ok_or(FalkorDBError::ParsingError)?, - ); - } - - Ok(Some(relevant_ids)) - } - } + update_map(write_lock.deref_mut(), keys, id_hashset) } } diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs new file mode 100644 index 0000000..dbab24d --- /dev/null +++ b/src/graph_schema/mod.rs @@ -0,0 +1,17 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +pub(crate) mod blocking; +mod utils; + +#[cfg(feature = "tokio")] +pub(crate) mod asynchronous; + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum SchemaType { + Labels, + Properties, + Relationships, +} diff --git a/src/graph_schema/utils.rs b/src/graph_schema/utils.rs new file mode 100644 index 0000000..18f75c5 --- /dev/null +++ b/src/graph_schema/utils.rs @@ -0,0 +1,72 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use crate::{FalkorDBError, FalkorValue, SchemaType}; +use anyhow::Result; +use std::collections::{HashMap, HashSet}; + +pub(crate) fn get_refresh_command(schema_type: SchemaType) -> &'static str { + match schema_type { + SchemaType::Labels => "DB.LABELS", + SchemaType::Properties => "DB.PROPERTYKEYS", + SchemaType::Relationships => "DB.RELATIONSHIPTYPES", + } +} + +pub(crate) fn update_map( + map_to_update: &mut HashMap, + keys: FalkorValue, + id_hashset: Option<&HashSet>, +) -> Result>> { + let keys_vec = keys.into_vec()?; + + let mut new_keys = HashMap::with_capacity(keys_vec.len()); + for (idx, item) in keys_vec.into_iter().enumerate() { + let key = item + .into_vec()? + .into_iter() + .next() + .ok_or(FalkorDBError::ParsingError)? + .into_string()?; + new_keys.insert(idx as i64, key); + } + + *map_to_update = new_keys; + + match id_hashset { + None => Ok(None), + Some(id_hashset) => { + let mut relevant_ids = HashMap::with_capacity(id_hashset.len()); + for id in id_hashset { + relevant_ids.insert( + *id, + map_to_update + .get(id) + .cloned() + .ok_or(FalkorDBError::ParsingError)?, + ); + } + + Ok(Some(relevant_ids)) + } + } +} + +pub(crate) fn get_relevant_hashmap( + id_set: &HashSet, + locked_map: &HashMap, +) -> Option> { + let mut id_hashmap = HashMap::new(); + for id in id_set { + if let Some(id_val) = locked_map.get(id).cloned() { + id_hashmap.insert(*id, id_val); + continue; + } + + return None; + } + + Some(id_hashmap) +} diff --git a/src/lib.rs b/src/lib.rs index 5ea79ca..597493d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -11,11 +11,33 @@ mod connection; mod connection_info; mod error; mod graph; -pub(crate) mod parser; +mod graph_schema; +mod parser; mod value; #[cfg(feature = "redis")] mod redis_ext; -pub use client::builder::FalkorDBClientBuilder; +pub use client::{blocking::SyncFalkorClient, builder::FalkorDBClientBuilder}; +pub use connection_info::FalkorConnectionInfo; pub use error::FalkorDBError; +pub use graph::blocking::SyncGraph; +pub use graph_schema::SchemaType; +pub use parser::FalkorParsable; +pub use value::{ + config::ConfigValue, + constraint::Constraint, + execution_plan::ExecutionPlan, + graph_entities::{Edge, Node}, + path::Path, + point::Point, + query_result::QueryResult, + slowlog_entry::SlowlogEntry, + FalkorValue, +}; + +#[cfg(feature = "tokio")] +pub use { + client::asynchronous::AsyncFalkorClient, connection::asynchronous::FalkorAsyncConnection, + graph::asynchronous::AsyncGraph, +}; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index ebb62c0..9a6db23 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3,15 +3,31 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::BorrowedSyncConnection; -use crate::graph::schema::GraphSchema; -use crate::value::FalkorValue; +use crate::{ + connection::blocking::BorrowedSyncConnection, + graph_schema::blocking::GraphSchema as SyncGraphSchema, FalkorValue, +}; use anyhow::Result; +#[cfg(feature = "tokio")] +use crate::{ + connection::asynchronous::FalkorAsyncConnection, + graph_schema::asynchronous::GraphSchema as AsyncGraphSchema, +}; + pub trait FalkorParsable: Sized { fn from_falkor_value( value: FalkorValue, - graph_schema: &GraphSchema, + graph_schema: &SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result; } + +#[cfg(feature = "tokio")] +pub trait FalkorAsyncParseable: Sized { + async fn from_falkor_value( + value: FalkorValue, + graph_schema: &AsyncGraphSchema, + conn: &mut FalkorAsyncConnection, + ) -> Result; +} diff --git a/src/redis_ext.rs b/src/redis_ext.rs index e1c1b51..e52d5f8 100644 --- a/src/redis_ext.rs +++ b/src/redis_ext.rs @@ -3,12 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::client::FalkorClientImpl; -use crate::connection::{asynchronous::FalkorAsyncConnection, blocking::FalkorSyncConnection}; -use crate::connection_info::FalkorConnectionInfo; -use crate::error::FalkorDBError; -use crate::value::config::ConfigValue; -use crate::value::FalkorValue; +use crate::{ + client::FalkorClientImpl, connection::blocking::FalkorSyncConnection, ConfigValue, + FalkorConnectionInfo, FalkorDBError, FalkorValue, +}; use anyhow::Result; use redis::{FromRedisValue, RedisResult, RedisWrite, ToRedisArgs}; @@ -19,7 +17,7 @@ impl From for FalkorSyncConnection { } #[cfg(feature = "tokio")] -impl From for FalkorAsyncConnection { +impl From for crate::FalkorAsyncConnection { fn from(value: redis::aio::MultiplexedConnection) -> Self { Self::Redis(value) } diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index 1a6af15..f8d19e7 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -3,11 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::BorrowedSyncConnection; -use crate::error::FalkorDBError; -use crate::graph::schema::{GraphSchema, SchemaType}; -use crate::value::map::parse_map_with_schema; -use crate::value::FalkorValue; +use crate::{ + connection::blocking::BorrowedSyncConnection, graph_schema::blocking::GraphSchema, + value::map::parse_map_with_schema, FalkorDBError, FalkorValue, SchemaType, +}; use anyhow::Result; use std::collections::{HashMap, HashSet}; diff --git a/src/value/map.rs b/src/value/map.rs index 5021dbb..ee041b3 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -3,11 +3,11 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::BorrowedSyncConnection; -use crate::error::FalkorDBError; -use crate::graph::schema::{GraphSchema, SchemaType}; -use crate::value::utils::parse_type; -use crate::value::FalkorValue; +use super::utils::parse_type; +use crate::{ + connection::blocking::BorrowedSyncConnection, graph_schema::blocking::GraphSchema, + FalkorDBError, FalkorValue, SchemaType, +}; use anyhow::Result; use std::collections::{HashMap, HashSet}; diff --git a/src/value/mod.rs b/src/value/mod.rs index 6fe04ea..ad53424 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -3,16 +3,15 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::BorrowedSyncConnection; -use crate::error::FalkorDBError; -use crate::graph::schema::GraphSchema; -use crate::parser::FalkorParsable; -use crate::value::point::Point; +use crate::{ + connection::blocking::BorrowedSyncConnection, graph_schema::blocking::GraphSchema, + FalkorDBError, FalkorParsable, +}; use anyhow::Result; use graph_entities::{Edge, Node}; use path::Path; -use std::collections::HashMap; -use std::fmt::Debug; +use point::Point; +use std::{collections::HashMap, fmt::Debug}; pub(crate) mod config; pub(crate) mod constraint; diff --git a/src/value/query_result.rs b/src/value/query_result.rs index 0104891..e15f19b 100644 --- a/src/value/query_result.rs +++ b/src/value/query_result.rs @@ -3,12 +3,11 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::BorrowedSyncConnection; -use crate::error::FalkorDBError; -use crate::graph::schema::GraphSchema; -use crate::parser::FalkorParsable; -use crate::value::utils::{parse_type, type_val_from_value}; -use crate::value::FalkorValue; +use super::utils::{parse_type, type_val_from_value}; +use crate::{ + connection::blocking::BorrowedSyncConnection, graph_schema::blocking::GraphSchema, + FalkorDBError, FalkorParsable, FalkorValue, +}; use anyhow::Result; use std::collections::HashMap; diff --git a/src/value/utils.rs b/src/value/utils.rs index 883c282..f93d584 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -4,13 +4,9 @@ */ use crate::connection::blocking::BorrowedSyncConnection; -use crate::graph::schema::GraphSchema; -use crate::value::graph_entities::{Edge, Node}; +use crate::graph_schema::blocking::GraphSchema; use crate::value::map::parse_map; -use crate::value::path::Path; -use crate::value::point::Point; -use crate::value::FalkorValue; -use crate::FalkorDBError; +use crate::{Edge, FalkorDBError, FalkorValue, Node, Path, Point}; use anyhow::Result; pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValue)> { From 64f65f290f990e4faf3840fea3e012323e683df5 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 22 May 2024 12:58:31 +0300 Subject: [PATCH 08/62] naive async works --- Cargo.lock | 24 ++++ Cargo.toml | 5 +- src/client/asynchronous.rs | 100 +++++++++++++-- src/client/blocking.rs | 5 +- src/client/builder.rs | 30 +---- src/graph/asynchronous.rs | 209 ++++++++++++++++++++++++++++++- src/graph/blocking.rs | 24 +--- src/graph/utils.rs | 16 +++ src/graph_schema/asynchronous.rs | 4 +- src/graph_schema/blocking.rs | 4 +- src/lib.rs | 5 +- src/parser/mod.rs | 20 +-- src/value/execution_plan.rs | 2 +- src/value/graph_entities.rs | 156 ++++++++++++++++------- src/value/map.rs | 114 ++++++++++++++--- src/value/mod.rs | 19 ++- src/value/path.rs | 58 +++++++-- src/value/query_result.rs | 87 ++++++++++++- src/value/slowlog_entry.rs | 3 +- src/value/utils.rs | 51 ++++++-- src/value/utils_async.rs | 87 +++++++++++++ 21 files changed, 857 insertions(+), 166 deletions(-) create mode 100644 src/value/utils_async.rs diff --git a/Cargo.lock b/Cargo.lock index 150648c..6f26428 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,6 +32,17 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-trait" version = "0.1.80" @@ -164,6 +175,7 @@ name = "falkordb-client-rs" version = "0.1.0" dependencies = [ "anyhow", + "async-recursion", "log", "parking_lot", "redis", @@ -911,9 +923,21 @@ dependencies = [ "num_cpus", "pin-project-lite", "socket2", + "tokio-macros", "windows-sys 0.48.0", ] +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "tokio-native-tls" version = "0.3.1" diff --git a/Cargo.toml b/Cargo.toml index f7d245f..dcf0da0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,11 +5,12 @@ edition = "2021" [dependencies] +async-recursion = { version = "1.1.1" } anyhow = { version = "1.0.83", default-features = false, features = ["std"] } log = { version = "0.4.21", default-features = false } parking_lot = { version = "0.12.2", default-features = false, features = ["deadlock_detection"] } redis = { version = "0.25.3", default-features = false, optional = true } -tokio = { version = "1.37.0", default-features = false, features = ["rt-multi-thread", "sync"], optional = true } +tokio = { version = "1.37.0", default-features = false, features = ["rt-multi-thread", "sync", "macros"], optional = true } thiserror = "1.0.60" url-parse = "1.0.8" simple_logger = "5.0.0" @@ -21,4 +22,4 @@ rustls = ["redis/tls-rustls"] tokio = ["dep:tokio", "redis/tokio-comp"] tokio-native-tls = ["redis/tokio-native-tls-comp"] tokio-rustls = ["redis/tokio-rustls-comp"] -redis = ["dep:redis"] \ No newline at end of file +redis = ["dep:redis"] diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index f928e41..9364a85 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -3,23 +3,23 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{client::FalkorClientImpl, FalkorAsyncConnection, FalkorDBError}; +use crate::{ + client::FalkorClientImpl, AsyncGraph, AsyncGraphSchema, ConfigValue, FalkorAsyncConnection, + FalkorDBError, +}; use anyhow::Result; -use std::sync::Arc; -use tokio::runtime::Runtime; +use std::{collections::HashMap, sync::Arc}; pub struct AsyncFalkorClient { _inner: FalkorClientImpl, connection: FalkorAsyncConnection, - runtime: Runtime, } impl AsyncFalkorClient { - pub(crate) async fn create(client: FalkorClientImpl, runtime: Runtime) -> Result> { + pub(crate) async fn create(client: FalkorClientImpl) -> Result> { Ok(Arc::new(Self { connection: client.get_async_connection(None).await?, _inner: client, - runtime, })) } pub(crate) fn clone_connection(&self) -> FalkorAsyncConnection { @@ -28,7 +28,6 @@ impl AsyncFalkorClient { pub async fn list_graphs(&self) -> Result> { let conn = self.clone_connection(); - let graph_list = match conn { #[cfg(feature = "redis")] FalkorAsyncConnection::Redis(mut redis_conn) => { @@ -58,4 +57,91 @@ impl AsyncFalkorClient { Ok(graph_list) } + + pub async fn config_get( + &self, + config_key: T, + ) -> Result> { + let conn = self.clone_connection(); + Ok(match conn { + #[cfg(feature = "redis")] + FalkorAsyncConnection::Redis(mut redis_conn) => { + let bulk_data = match redis_conn + .send_packed_command( + redis::cmd("GRAPH.CONFIG") + .arg("GET") + .arg(config_key.to_string()), + ) + .await? + { + redis::Value::Bulk(bulk_data) => bulk_data, + _ => return Err(FalkorDBError::InvalidDataReceived.into()), + }; + + if bulk_data.is_empty() { + return Err(FalkorDBError::InvalidDataReceived.into()); + } else if bulk_data.len() == 2 { + return if let Some(redis::Value::Status(config_key)) = bulk_data.first() { + Ok(HashMap::from([( + config_key.to_string(), + ConfigValue::try_from(&bulk_data[1])?, + )])) + } else { + Err(FalkorDBError::InvalidDataReceived.into()) + }; + } + + let mut config_map = HashMap::with_capacity(bulk_data.len()); + for raw_map in bulk_data { + for (key, val) in raw_map + .into_map_iter() + .map_err(|_| FalkorDBError::ParsingError)? + { + let key = match key { + redis::Value::Status(config_key) => Ok(config_key), + redis::Value::Data(config_key) => { + Ok(String::from_utf8_lossy(config_key.as_slice()).to_string()) + } + _ => Err(FalkorDBError::InvalidDataReceived), + }?; + + config_map.insert(key, ConfigValue::try_from(&val)?); + } + } + + config_map + } + }) + } + + pub async fn config_set, C: Into>( + &self, + config_key: T, + value: C, + ) -> Result<()> { + let conn = self.clone_connection(); + match conn { + #[cfg(feature = "redis")] + FalkorAsyncConnection::Redis(mut redis_conn) => { + redis_conn + .send_packed_command( + redis::cmd("GRAPH.CONFIG") + .arg("SET") + .arg(config_key.into()) + .arg(value.into()), + ) + .await?; + } + } + + Ok(()) + } + + pub async fn open_graph(&self, graph_name: T) -> AsyncGraph { + AsyncGraph { + client: self, + graph_name: graph_name.to_string(), + graph_schema: AsyncGraphSchema::new(graph_name.to_string()), + } + } } diff --git a/src/client/blocking.rs b/src/client/blocking.rs index e239326..63df6f1 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -6,8 +6,7 @@ use crate::{ client::FalkorClientImpl, connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}, - graph_schema::blocking::GraphSchema, - ConfigValue, FalkorDBError, SyncGraph, + ConfigValue, FalkorDBError, SyncGraph, SyncGraphSchema, }; use anyhow::Result; use std::{ @@ -157,7 +156,7 @@ impl SyncFalkorClient { SyncGraph { client: self, graph_name: graph_name.to_string(), - graph_schema: GraphSchema::new(graph_name.to_string()), + graph_schema: SyncGraphSchema::new(graph_name.to_string()), } } } diff --git a/src/client/builder.rs b/src/client/builder.rs index 200d1b3..72d09bb 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -12,7 +12,6 @@ use std::sync::Arc; pub struct FalkorDBClientBuilder { connection_info: Option, num_connections: u8, - multithreaded_rt: bool, #[cfg(feature = "tokio")] runtime: Option, } @@ -52,7 +51,6 @@ impl FalkorDBClientBuilder<'S'> { FalkorDBClientBuilder { connection_info: None, num_connections: 4, - multithreaded_rt: false, #[cfg(feature = "tokio")] runtime: None, } @@ -77,41 +75,15 @@ impl FalkorDBClientBuilder<'A'> { FalkorDBClientBuilder { connection_info: None, num_connections: 4, - multithreaded_rt: false, - #[cfg(feature = "tokio")] runtime: None, } } - pub fn with_multithreaded_runtime(self) -> Self { - Self { - multithreaded_rt: true, - ..self - } - } - - /// This overrides with_multithreaded_runtime() - pub fn with_runtime(self, runtime: tokio::runtime::Runtime) -> Self { - Self { - runtime: Some(runtime), - ..self - } - } - pub async fn build(self) -> Result> { let connection_info = self .connection_info .unwrap_or("falkor://127.0.0.1:6379".try_into()?); - let runtime = self.runtime.unwrap_or( - if self.multithreaded_rt { - tokio::runtime::Builder::new_multi_thread() - } else { - tokio::runtime::Builder::new_current_thread() - } - .build()?, - ); - - crate::AsyncFalkorClient::create(get_client(connection_info)?, runtime).await + crate::AsyncFalkorClient::create(get_client(connection_info)?).await } } diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index d3fa71e..3a88745 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -3,8 +3,215 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::client::asynchronous::AsyncFalkorClient; +use super::utils::{construct_query, generate_procedure_call}; +use crate::{ + AsyncFalkorClient, AsyncGraphSchema, ExecutionPlan, FalkorAsyncConnection, + FalkorAsyncParseable, FalkorDBError, FalkorValue, QueryResult, SlowlogEntry, +}; +use std::collections::HashMap; pub struct AsyncGraph<'a> { pub(crate) client: &'a AsyncFalkorClient, + pub(crate) graph_name: String, + pub(crate) graph_schema: AsyncGraphSchema, +} + +impl AsyncGraph<'_> { + pub fn graph_name(&self) -> &str { + self.graph_name.as_str() + } + + async fn send_command( + &self, + command: &str, + params: Option, + ) -> anyhow::Result { + let mut conn = self.client.clone_connection(); + conn.send_command(Some(self.graph_name.clone()), command, params) + .await + } + + pub async fn copy(&self, cloned_graph_name: T) -> anyhow::Result { + self.send_command("GRAPH.COPY", Some(cloned_graph_name.to_string())) + .await?; + Ok(self.client.open_graph(cloned_graph_name).await) + } + + pub async fn delete(&self) -> anyhow::Result<()> { + self.send_command("GRAPH.DELETE", None).await?; + Ok(()) + } + + pub async fn slowlog(&self) -> anyhow::Result> { + let res = self.send_command("GRAPH.SLOWLOG", None).await?.into_vec()?; + + if res.is_empty() { + return Ok(vec![]); + } + + let mut slowlog_entries = Vec::with_capacity(res.len()); + for entry_raw in res { + slowlog_entries.push(SlowlogEntry::from_value_array( + entry_raw + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingSlowlogEntryElementCount)?, + )?); + } + + Ok(slowlog_entries) + } + + pub async fn slowlog_reset(&self) -> anyhow::Result<()> { + self.send_command("GRAPH.SLOWLOG", Some("RESET".to_string())) + .await?; + Ok(()) + } + + pub async fn profile_with_params( + &self, + query_string: Q, + params: Option<&HashMap>, + ) -> anyhow::Result { + let query = construct_query(query_string, params); + + ExecutionPlan::try_from(self.send_command("GRAPH.PROFILE", Some(query)).await?) + } + + pub async fn profile(&self, query_string: Q) -> anyhow::Result { + self.profile_with_params::(query_string, None) + .await + } + + pub async fn explain_with_params( + &self, + query_string: Q, + params: Option<&HashMap>, + ) -> anyhow::Result { + let query = construct_query(query_string, params); + ExecutionPlan::try_from(self.send_command("GRAPH.EXPLAIN", Some(query)).await?) + } + + pub async fn explain(&self, query_string: Q) -> anyhow::Result { + self.explain_with_params::(query_string, None) + .await + } + + async fn query_with_parser( + &self, + command: &str, + query_string: Q, + params: Option<&HashMap>, + timeout: Option, + ) -> anyhow::Result

{ + let query = construct_query(query_string, params); + + let mut conn = self.client.clone_connection(); + let falkor_result = match &mut conn { + #[cfg(feature = "redis")] + FalkorAsyncConnection::Redis(redis_conn) => { + use redis::FromRedisValue; + let redis_val = redis_conn + .send_packed_command( + redis::cmd(command) + .arg(self.graph_name.as_str()) + .arg(query) + .arg("--compact") + .arg(timeout.map(|timeout| format!("timeout {timeout}"))), + ) + .await?; + FalkorValue::from_owned_redis_value(redis_val)? + } + }; + + P::from_falkor_value_async(falkor_result, &self.graph_schema, &mut conn).await + } + + pub async fn query_with_params< + Q: ToString, + T: ToString, + Z: ToString, + P: FalkorAsyncParseable, + >( + &self, + query_string: Q, + params: Option<&HashMap>, + readonly: bool, + timeout: Option, + ) -> anyhow::Result

{ + self.query_with_parser( + if readonly { + "GRAPH.RO_QUERY" + } else { + "GRAPH.QUERY" + }, + query_string, + params, + timeout, + ) + .await + } + + pub async fn query( + &self, + query_string: Q, + timeout: Option, + ) -> anyhow::Result { + self.query_with_params::(query_string, None, false, timeout) + .await + } + + pub async fn query_readonly( + &self, + query_string: Q, + timeout: Option, + ) -> anyhow::Result { + self.query_with_params::(query_string, None, true, timeout) + .await + } + + pub async fn call_procedure( + &self, + procedure: C, + args: Option<&[String]>, + yields: Option<&[String]>, + read_only: Option, + timeout: Option, + ) -> anyhow::Result

{ + let (query_string, params) = generate_procedure_call(procedure, args, yields); + + self.query_with_params( + query_string, + params.as_ref(), + read_only.unwrap_or_default(), + timeout, + ) + .await + } + + pub async fn list_indices(&self) -> anyhow::Result { + let query_res = self + .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, None, None) + .await? + .into_vec()?; + + for item in query_res { + log::info!("{item:?}"); + } + Ok(FalkorValue::None) + } + + pub async fn list_constraints(&self) -> anyhow::Result { + let query_res = self + .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, None, None) + .await? + .into_vec()?; + + for item in query_res { + for sub_item in item.into_vec()? { + log::info!("{sub_item:?}"); + } + } + Ok(FalkorValue::None) + } } diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index bbcf417..687f4b7 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -3,10 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use super::utils::generate_procedure_call; +use super::utils::{construct_query, generate_procedure_call}; use crate::{ - connection::blocking::FalkorSyncConnection, graph_schema::blocking::GraphSchema, ExecutionPlan, - FalkorDBError, FalkorParsable, FalkorValue, QueryResult, SlowlogEntry, SyncFalkorClient, + connection::blocking::FalkorSyncConnection, ExecutionPlan, FalkorDBError, FalkorParsable, + FalkorValue, QueryResult, SlowlogEntry, SyncFalkorClient, SyncGraphSchema, }; use anyhow::Result; use redis::ConnectionLike; @@ -15,23 +15,7 @@ use std::collections::HashMap; pub struct SyncGraph<'a> { pub(crate) client: &'a SyncFalkorClient, pub(crate) graph_name: String, - pub(crate) graph_schema: GraphSchema, -} - -fn construct_query( - query_str: Q, - params: Option<&HashMap>, -) -> String { - params - .map(|params| { - params - .iter() - .fold("CYPHER ".to_string(), |acc, (key, val)| { - acc + format!("{}={}", key.to_string(), val.to_string()).as_str() - }) - }) - .unwrap_or_default() - + query_str.to_string().as_str() + pub(crate) graph_schema: SyncGraphSchema, } impl SyncGraph<'_> { diff --git a/src/graph/utils.rs b/src/graph/utils.rs index 8cfe81a..c255353 100644 --- a/src/graph/utils.rs +++ b/src/graph/utils.rs @@ -44,3 +44,19 @@ pub(crate) fn generate_procedure_call( (query_string, params) } + +pub(crate) fn construct_query( + query_str: Q, + params: Option<&HashMap>, +) -> String { + params + .map(|params| { + params + .iter() + .fold("CYPHER ".to_string(), |acc, (key, val)| { + acc + format!("{}={}", key.to_string(), val.to_string()).as_str() + }) + }) + .unwrap_or_default() + + query_str.to_string().as_str() +} diff --git a/src/graph_schema/asynchronous.rs b/src/graph_schema/asynchronous.rs index 857c564..77f4c56 100644 --- a/src/graph_schema/asynchronous.rs +++ b/src/graph_schema/asynchronous.rs @@ -19,7 +19,7 @@ use tokio::sync::RwLock; pub(crate) type LockableIdMap = Arc>>; #[derive(Clone, Debug, Default)] -pub struct GraphSchema { +pub struct AsyncGraphSchema { graph_name: String, version: Arc, labels: LockableIdMap, @@ -27,7 +27,7 @@ pub struct GraphSchema { relationships: LockableIdMap, } -impl GraphSchema { +impl AsyncGraphSchema { pub fn new(graph_name: String) -> Self { Self { graph_name, diff --git a/src/graph_schema/blocking.rs b/src/graph_schema/blocking.rs index bc4be9d..9f706fe 100644 --- a/src/graph_schema/blocking.rs +++ b/src/graph_schema/blocking.rs @@ -21,7 +21,7 @@ use std::{ pub(crate) type LockableIdMap = Arc>>; #[derive(Clone, Debug, Default)] -pub struct GraphSchema { +pub struct SyncGraphSchema { graph_name: String, version: Arc, labels: LockableIdMap, @@ -29,7 +29,7 @@ pub struct GraphSchema { relationships: LockableIdMap, } -impl GraphSchema { +impl SyncGraphSchema { pub fn new(graph_name: String) -> Self { Self { graph_name, diff --git a/src/lib.rs b/src/lib.rs index 597493d..fdb112f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -22,7 +22,7 @@ pub use client::{blocking::SyncFalkorClient, builder::FalkorDBClientBuilder}; pub use connection_info::FalkorConnectionInfo; pub use error::FalkorDBError; pub use graph::blocking::SyncGraph; -pub use graph_schema::SchemaType; +pub use graph_schema::{blocking::SyncGraphSchema, SchemaType}; pub use parser::FalkorParsable; pub use value::{ config::ConfigValue, @@ -39,5 +39,6 @@ pub use value::{ #[cfg(feature = "tokio")] pub use { client::asynchronous::AsyncFalkorClient, connection::asynchronous::FalkorAsyncConnection, - graph::asynchronous::AsyncGraph, + graph::asynchronous::AsyncGraph, graph_schema::asynchronous::AsyncGraphSchema, + parser::FalkorAsyncParseable, }; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 9a6db23..e873685 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3,17 +3,9 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{ - connection::blocking::BorrowedSyncConnection, - graph_schema::blocking::GraphSchema as SyncGraphSchema, FalkorValue, -}; +use crate::{connection::blocking::BorrowedSyncConnection, FalkorValue, SyncGraphSchema}; use anyhow::Result; - -#[cfg(feature = "tokio")] -use crate::{ - connection::asynchronous::FalkorAsyncConnection, - graph_schema::asynchronous::GraphSchema as AsyncGraphSchema, -}; +use std::future::Future; pub trait FalkorParsable: Sized { fn from_falkor_value( @@ -25,9 +17,9 @@ pub trait FalkorParsable: Sized { #[cfg(feature = "tokio")] pub trait FalkorAsyncParseable: Sized { - async fn from_falkor_value( + fn from_falkor_value_async( value: FalkorValue, - graph_schema: &AsyncGraphSchema, - conn: &mut FalkorAsyncConnection, - ) -> Result; + graph_schema: &crate::AsyncGraphSchema, + conn: &mut crate::FalkorAsyncConnection, + ) -> impl Future> + Send; } diff --git a/src/value/execution_plan.rs b/src/value/execution_plan.rs index b194c86..b47c563 100644 --- a/src/value/execution_plan.rs +++ b/src/value/execution_plan.rs @@ -3,7 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::value::FalkorValue; +use crate::FalkorValue; pub struct ExecutionPlan { text: String, diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index f8d19e7..31c2d4f 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -3,13 +3,17 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use super::utils::parse_labels; use crate::{ - connection::blocking::BorrowedSyncConnection, graph_schema::blocking::GraphSchema, - value::map::parse_map_with_schema, FalkorDBError, FalkorValue, SchemaType, + connection::blocking::BorrowedSyncConnection, value::map::parse_map_with_schema, FalkorDBError, + FalkorParsable, FalkorValue, SchemaType, SyncGraphSchema, }; use anyhow::Result; use std::collections::{HashMap, HashSet}; +#[cfg(feature = "tokio")] +use crate::value::{map::parse_map_with_schema_async, utils_async::parse_labels_async}; + #[derive(Clone, Debug)] pub struct Node { pub entity_id: i64, @@ -17,47 +21,47 @@ pub struct Node { pub properties: HashMap, } -pub(crate) fn parse_labels( - raw_ids: Vec, - graph_schema: &GraphSchema, - conn: &mut BorrowedSyncConnection, - schema_type: SchemaType, -) -> Result> { - let mut ids_hashset = HashSet::with_capacity(raw_ids.len()); - for label in raw_ids.iter() { - ids_hashset.insert(label.to_i64().ok_or(FalkorDBError::ParsingI64)?); - } +impl FalkorParsable for Node { + fn from_falkor_value( + value: FalkorValue, + graph_schema: &SyncGraphSchema, + conn: &mut BorrowedSyncConnection, + ) -> Result { + let [entity_id, labels, properties]: [FalkorValue; 3] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingNodeElementCount)?; + let labels = labels.into_vec()?; - match match graph_schema.verify_id_set(&ids_hashset, schema_type) { - None => graph_schema.refresh(schema_type, conn, Some(&ids_hashset))?, - relevant_ids => relevant_ids, - } { - Some(relevant_ids) => { - let mut parsed_ids = Vec::with_capacity(raw_ids.len()); - for id in raw_ids { - parsed_ids.push( - id.to_i64() - .ok_or(FalkorDBError::ParsingI64) - .and_then(|id| { - relevant_ids - .get(&id) - .cloned() - .ok_or(FalkorDBError::ParsingCompactIdUnknown) - })?, - ); - } - - Ok(parsed_ids) + let mut ids_hashset = HashSet::with_capacity(labels.len()); + for label in labels.iter() { + ids_hashset.insert( + label + .to_i64() + .ok_or(FalkorDBError::ParsingCompactIdUnknown)?, + ); } - _ => Err(FalkorDBError::ParsingError)?, + + let parsed_labels = parse_labels(labels, graph_schema, conn, SchemaType::Labels)?; + Ok(Node { + entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + labels: parsed_labels, + properties: parse_map_with_schema( + properties, + graph_schema, + conn, + SchemaType::Properties, + )?, + }) } } -impl Node { - pub(crate) fn parse( +#[cfg(feature = "tokio")] +impl crate::FalkorAsyncParseable for Node { + async fn from_falkor_value_async( value: FalkorValue, - graph_schema: &GraphSchema, - conn: &mut BorrowedSyncConnection, + graph_schema: &crate::AsyncGraphSchema, + conn: &mut crate::FalkorAsyncConnection, ) -> Result { let [entity_id, labels, properties]: [FalkorValue; 3] = value .into_vec()? @@ -74,16 +78,18 @@ impl Node { ); } - let parsed_labels = parse_labels(labels, graph_schema, conn, SchemaType::Labels)?; + let parsed_labels = + parse_labels_async(labels, graph_schema, conn, SchemaType::Labels).await?; Ok(Node { entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, labels: parsed_labels, - properties: parse_map_with_schema( + properties: parse_map_with_schema_async( properties, graph_schema, conn, SchemaType::Properties, - )?, + ) + .await?, }) } } @@ -97,10 +103,10 @@ pub struct Edge { pub properties: HashMap, } -impl Edge { - pub(crate) fn parse( +impl FalkorParsable for Edge { + fn from_falkor_value( value: FalkorValue, - graph_schema: &GraphSchema, + graph_schema: &SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value @@ -148,3 +154,67 @@ impl Edge { } } } + +#[cfg(feature = "tokio")] +impl crate::FalkorAsyncParseable for Edge { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &crate::AsyncGraphSchema, + conn: &mut crate::FalkorAsyncConnection, + ) -> Result { + let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + + let relation = relations.to_i64().ok_or(FalkorDBError::ParsingI64)?; + if let Some(relationship) = graph_schema + .relationships() + .read() + .await + .get(&relation) + .cloned() + { + return Ok(Edge { + entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + relationship_type: relationship, + src_node_id: src_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + dst_node_id: dst_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + properties: parse_map_with_schema_async( + properties, + graph_schema, + conn, + SchemaType::Properties, + ) + .await?, + }); + } + + match graph_schema + .refresh( + SchemaType::Relationships, + conn, + Some(&HashSet::from([relation])), + ) + .await? + { + None => Err(FalkorDBError::ParsingCompactIdUnknown)?, + Some(id) => Ok(Edge { + entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + relationship_type: id + .get(&relation) + .cloned() + .ok_or(FalkorDBError::ParsingCompactIdUnknown)?, + src_node_id: src_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + dst_node_id: dst_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + properties: parse_map_with_schema_async( + properties, + graph_schema, + conn, + SchemaType::Properties, + ) + .await?, + }), + } + } +} diff --git a/src/value/map.rs b/src/value/map.rs index ee041b3..18d86b8 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -5,12 +5,15 @@ use super::utils::parse_type; use crate::{ - connection::blocking::BorrowedSyncConnection, graph_schema::blocking::GraphSchema, - FalkorDBError, FalkorValue, SchemaType, + connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, FalkorValue, + SchemaType, SyncGraphSchema, }; use anyhow::Result; use std::collections::{HashMap, HashSet}; +#[cfg(feature = "tokio")] +use super::utils_async::parse_type_async; + // Intermediate type for map parsing pub(crate) struct FKeyTypeVal { key: i64, @@ -46,7 +49,7 @@ impl TryFrom for FKeyTypeVal { fn ktv_vec_to_map( map_vec: Vec, relevant_ids_map: HashMap, - graph_schema: &GraphSchema, + graph_schema: &SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result> { let mut new_map = HashMap::with_capacity(map_vec.len()); @@ -63,10 +66,9 @@ fn ktv_vec_to_map( Ok(new_map) } -// TODO: This does NOT support nested attributes yet pub(crate) fn parse_map_with_schema( value: FalkorValue, - graph_schema: &GraphSchema, + graph_schema: &SyncGraphSchema, conn: &mut BorrowedSyncConnection, schema_type: SchemaType, ) -> Result> { @@ -93,21 +95,101 @@ pub(crate) fn parse_map_with_schema( } } -pub(crate) fn parse_map( - value: FalkorValue, - graph_schema: &GraphSchema, - conn: &mut BorrowedSyncConnection, -) -> anyhow::Result> { - let val_vec = value.into_vec()?; +impl FalkorParsable for HashMap { + fn from_falkor_value( + value: FalkorValue, + graph_schema: &SyncGraphSchema, + conn: &mut BorrowedSyncConnection, + ) -> Result { + let val_vec = value.into_vec()?; + + let mut new_map = HashMap::with_capacity(val_vec.len()); + for val in val_vec { + let fktv = FKeyTypeVal::try_from(val)?; + new_map.insert( + fktv.key.to_string(), + parse_type(fktv.type_marker, fktv.val, graph_schema, conn)?, + ); + } - let mut new_map = HashMap::with_capacity(val_vec.len()); - for val in val_vec { - let fktv = FKeyTypeVal::try_from(val)?; + Ok(new_map) + } +} + +#[cfg(feature = "tokio")] +async fn ktv_vec_to_map_async( + map_vec: Vec, + relevant_ids_map: HashMap, + graph_schema: &crate::AsyncGraphSchema, + conn: &mut crate::FalkorAsyncConnection, +) -> Result> { + let mut new_map = HashMap::with_capacity(map_vec.len()); + for fktv in map_vec { new_map.insert( - fktv.key.to_string(), - parse_type(fktv.type_marker, fktv.val, graph_schema, conn)?, + relevant_ids_map + .get(&fktv.key) + .cloned() + .ok_or(FalkorDBError::ParsingError)?, + parse_type_async(fktv.type_marker, fktv.val, graph_schema, conn).await?, ); } Ok(new_map) } + +#[cfg(feature = "tokio")] +pub(crate) async fn parse_map_with_schema_async( + value: FalkorValue, + graph_schema: &crate::AsyncGraphSchema, + conn: &mut crate::FalkorAsyncConnection, + schema_type: SchemaType, +) -> Result> { + let val_vec = value.into_vec()?; + let (mut id_hashset, mut map_vec) = ( + HashSet::with_capacity(val_vec.len()), + Vec::with_capacity(val_vec.len()), + ); + + for item in val_vec { + let fktv = FKeyTypeVal::try_from(item)?; + id_hashset.insert(fktv.key); + map_vec.push(fktv); + } + + if let Some(relevant_ids_map) = graph_schema.verify_id_set(&id_hashset, schema_type).await { + return ktv_vec_to_map_async(map_vec, relevant_ids_map, graph_schema, conn).await; + } + + // If we reached here, schema validation failed and we need to refresh our schema + match graph_schema + .refresh(schema_type, conn, Some(&id_hashset)) + .await? + { + Some(relevant_ids_map) => { + ktv_vec_to_map_async(map_vec, relevant_ids_map, graph_schema, conn).await + } + None => Err(FalkorDBError::ParsingError)?, + } +} + +#[cfg(feature = "tokio")] +impl crate::FalkorAsyncParseable for HashMap { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &crate::AsyncGraphSchema, + conn: &mut crate::FalkorAsyncConnection, + ) -> Result { + let val_vec = value.into_vec()?; + + let mut new_map = HashMap::with_capacity(val_vec.len()); + for val in val_vec { + let fktv = FKeyTypeVal::try_from(val)?; + new_map.insert( + fktv.key.to_string(), + parse_type_async(fktv.type_marker, fktv.val, graph_schema, conn).await?, + ); + } + + Ok(new_map) + } +} diff --git a/src/value/mod.rs b/src/value/mod.rs index ad53424..f9a66bc 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -4,8 +4,7 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, graph_schema::blocking::GraphSchema, - FalkorDBError, FalkorParsable, + connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, SyncGraphSchema, }; use anyhow::Result; use graph_entities::{Edge, Node}; @@ -24,6 +23,9 @@ pub(crate) mod query_result; pub(crate) mod slowlog_entry; pub(crate) mod utils; +#[cfg(feature = "tokio")] +pub(crate) mod utils_async; + #[derive(Clone, Debug)] pub enum FalkorValue { FNode(Node), @@ -86,13 +88,24 @@ where impl FalkorParsable for FalkorValue { fn from_falkor_value( value: FalkorValue, - _graph_schema: &GraphSchema, + _graph_schema: &SyncGraphSchema, _conn: &mut BorrowedSyncConnection, ) -> Result { Ok(value) } } +#[cfg(feature = "tokio")] +impl crate::FalkorAsyncParseable for FalkorValue { + async fn from_falkor_value_async( + value: FalkorValue, + _graph_schema: &crate::AsyncGraphSchema, + _conn: &mut crate::FalkorAsyncConnection, + ) -> Result { + Ok(value) + } +} + impl FalkorValue { pub fn as_vec(&self) -> Option<&Vec> { match self { diff --git a/src/value/path.rs b/src/value/path.rs index 30c512c..db65511 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -3,9 +3,11 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::error::FalkorDBError; -use crate::value::graph_entities::{Edge, Node}; -use crate::value::FalkorValue; +use crate::{ + connection::blocking::BorrowedSyncConnection, Edge, FalkorDBError, FalkorParsable, FalkorValue, + Node, SyncGraphSchema, +}; +use anyhow::Result; #[derive(Clone, Debug)] pub struct Path { @@ -13,8 +15,12 @@ pub struct Path { pub relationships: Vec, } -impl Path { - pub fn parse(value: FalkorValue) -> anyhow::Result { +impl FalkorParsable for Path { + fn from_falkor_value( + value: FalkorValue, + graph_schema: &SyncGraphSchema, + conn: &mut BorrowedSyncConnection, + ) -> Result { let [nodes, relationships]: [FalkorValue; 2] = value .into_vec()? .try_into() @@ -23,12 +29,50 @@ impl Path { let mut parsed_nodes = Vec::with_capacity(nodes.len()); for node_raw in nodes { - parsed_nodes.push(node_raw.into_node()?); + parsed_nodes.push(FalkorParsable::from_falkor_value( + node_raw, + graph_schema, + conn, + )?); } let mut parsed_edges = Vec::with_capacity(relationships.len()); for edge_raw in relationships { - parsed_edges.push(edge_raw.into_edge()?); + parsed_edges.push(FalkorParsable::from_falkor_value( + edge_raw, + graph_schema, + conn, + )?); + } + + Ok(Path { + nodes: parsed_nodes, + relationships: parsed_edges, + }) + } +} + +#[cfg(feature = "tokio")] +impl crate::FalkorAsyncParseable for Path { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &crate::AsyncGraphSchema, + conn: &mut crate::FalkorAsyncConnection, + ) -> Result { + let [nodes, relationships]: [FalkorValue; 2] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + let (nodes, relationships) = (nodes.into_vec()?, relationships.into_vec()?); + + let mut parsed_nodes = Vec::with_capacity(nodes.len()); + for node_raw in nodes { + parsed_nodes.push(Node::from_falkor_value_async(node_raw, graph_schema, conn).await?); + } + + let mut parsed_edges = Vec::with_capacity(relationships.len()); + for edge_raw in relationships { + parsed_edges.push(Edge::from_falkor_value_async(edge_raw, graph_schema, conn).await?); } Ok(Path { diff --git a/src/value/query_result.rs b/src/value/query_result.rs index e15f19b..f19f8c1 100644 --- a/src/value/query_result.rs +++ b/src/value/query_result.rs @@ -5,12 +5,15 @@ use super::utils::{parse_type, type_val_from_value}; use crate::{ - connection::blocking::BorrowedSyncConnection, graph_schema::blocking::GraphSchema, - FalkorDBError, FalkorParsable, FalkorValue, + connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, FalkorValue, + SyncGraphSchema, }; use anyhow::Result; use std::collections::HashMap; +#[cfg(feature = "tokio")] +use super::utils_async::parse_type_async; + #[derive(Clone, Debug, Default)] pub struct QueryResult { pub(crate) stats: Vec, @@ -73,7 +76,7 @@ fn query_parse_stats(stats: FalkorValue) -> Result> { fn parse_result_set( data_vec: Vec, - graph_schema: &GraphSchema, + graph_schema: &SyncGraphSchema, conn: &mut BorrowedSyncConnection, header_keys: &[String], ) -> Result>> { @@ -96,7 +99,7 @@ fn parse_result_set( impl FalkorParsable for QueryResult { fn from_falkor_value( value: FalkorValue, - graph_schema: &GraphSchema, + graph_schema: &SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let value_vec = value.into_vec()?; @@ -144,3 +147,79 @@ impl FalkorParsable for QueryResult { }) } } + +#[cfg(feature = "tokio")] +async fn parse_result_set_async( + data_vec: Vec, + graph_schema: &crate::AsyncGraphSchema, + conn: &mut crate::FalkorAsyncConnection, + header_keys: &[String], +) -> Result>> { + let mut parsed_result_set = Vec::with_capacity(data_vec.len()); + for column in data_vec { + let column_vec = column.into_vec()?; + + let mut parsed_column = Vec::with_capacity(column_vec.len()); + for column_item in column_vec { + let (type_marker, val) = type_val_from_value(column_item)?; + parsed_column.push(parse_type_async(type_marker, val, graph_schema, conn).await?); + } + + parsed_result_set.push(header_keys.iter().cloned().zip(parsed_column).collect()) + } + + Ok(parsed_result_set) +} + +#[cfg(feature = "tokio")] +impl crate::FalkorAsyncParseable for QueryResult { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &crate::AsyncGraphSchema, + conn: &mut crate::FalkorAsyncConnection, + ) -> Result { + let value_vec = value.into_vec()?; + + // Empty result + if value_vec.is_empty() { + return Ok(QueryResult::default()); + } + + // Result only contains stats + if value_vec.len() == 1 { + let first_element = value_vec.into_iter().nth(0).unwrap(); + return Ok(QueryResult { + stats: query_parse_stats(first_element)?, + ..Default::default() + }); + } + + // Invalid response + if value_vec.len() == 2 { + Err(FalkorDBError::InvalidDataReceived)?; + } + + // Full result + let [header, data, stats]: [FalkorValue; 3] = value_vec + .try_into() + .map_err(|_| FalkorDBError::ParsingError)?; + + let header_keys = query_parse_header(header)?; + let stats_strings = query_parse_stats(stats)?; + + let data_vec = data.into_vec()?; + let data_len = data_vec.len(); + + let result_set = parse_result_set_async(data_vec, graph_schema, conn, &header_keys).await?; + + if result_set.len() != data_len { + Err(FalkorDBError::ParsingError)?; + } + + Ok(Self { + stats: stats_strings, + header: header_keys, + result_set, + }) + } +} diff --git a/src/value/slowlog_entry.rs b/src/value/slowlog_entry.rs index fe34afd..6ea716f 100644 --- a/src/value/slowlog_entry.rs +++ b/src/value/slowlog_entry.rs @@ -3,8 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::error::FalkorDBError; -use crate::value::FalkorValue; +use crate::{FalkorDBError, FalkorValue}; use anyhow::Result; #[derive(Clone, Debug)] diff --git a/src/value/utils.rs b/src/value/utils.rs index f93d584..18a3d1d 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -4,10 +4,45 @@ */ use crate::connection::blocking::BorrowedSyncConnection; -use crate::graph_schema::blocking::GraphSchema; -use crate::value::map::parse_map; -use crate::{Edge, FalkorDBError, FalkorValue, Node, Path, Point}; +use crate::{FalkorDBError, FalkorParsable, FalkorValue, Point, SchemaType, SyncGraphSchema}; use anyhow::Result; +use std::collections::HashSet; + +pub(crate) fn parse_labels( + raw_ids: Vec, + graph_schema: &SyncGraphSchema, + conn: &mut BorrowedSyncConnection, + schema_type: SchemaType, +) -> Result> { + let mut ids_hashset = HashSet::with_capacity(raw_ids.len()); + for label in raw_ids.iter() { + ids_hashset.insert(label.to_i64().ok_or(FalkorDBError::ParsingI64)?); + } + + match match graph_schema.verify_id_set(&ids_hashset, schema_type) { + None => graph_schema.refresh(schema_type, conn, Some(&ids_hashset))?, + relevant_ids => relevant_ids, + } { + Some(relevant_ids) => { + let mut parsed_ids = Vec::with_capacity(raw_ids.len()); + for id in raw_ids { + parsed_ids.push( + id.to_i64() + .ok_or(FalkorDBError::ParsingI64) + .and_then(|id| { + relevant_ids + .get(&id) + .cloned() + .ok_or(FalkorDBError::ParsingCompactIdUnknown) + })?, + ); + } + + Ok(parsed_ids) + } + _ => Err(FalkorDBError::ParsingError)?, + } +} pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValue)> { let [type_marker, val]: [FalkorValue; 2] = value @@ -22,7 +57,7 @@ pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValu pub(crate) fn parse_type( type_marker: i64, val: FalkorValue, - graph_schema: &GraphSchema, + graph_schema: &SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let res = match type_marker { @@ -41,10 +76,10 @@ pub(crate) fn parse_type( parsed_vec }), // The following types are sent as an array and require specific parsing functions - 7 => FalkorValue::FEdge(Edge::parse(val, graph_schema, conn)?), - 8 => FalkorValue::FNode(Node::parse(val, graph_schema, conn)?), - 9 => FalkorValue::FPath(Path::parse(val)?), - 10 => FalkorValue::FMap(parse_map(val, graph_schema, conn)?), + 7 => FalkorValue::FEdge(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), + 8 => FalkorValue::FNode(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), + 9 => FalkorValue::FPath(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), + 10 => FalkorValue::FMap(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), 11 => FalkorValue::FPoint(Point::parse(val)?), _ => Err(FalkorDBError::ParsingUnknownType)?, }; diff --git a/src/value/utils_async.rs b/src/value/utils_async.rs new file mode 100644 index 0000000..a04c0ab --- /dev/null +++ b/src/value/utils_async.rs @@ -0,0 +1,87 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use super::utils::type_val_from_value; +use crate::{ + AsyncGraphSchema, Edge, FalkorAsyncConnection, FalkorAsyncParseable, FalkorDBError, + FalkorValue, Node, Path, Point, SchemaType, +}; +use anyhow::Result; +use async_recursion::async_recursion; +use std::collections::{HashMap, HashSet}; + +pub(crate) async fn parse_labels_async( + raw_ids: Vec, + graph_schema: &AsyncGraphSchema, + conn: &mut FalkorAsyncConnection, + schema_type: SchemaType, +) -> Result> { + let mut ids_hashset = HashSet::with_capacity(raw_ids.len()); + for label in raw_ids.iter() { + ids_hashset.insert(label.to_i64().ok_or(FalkorDBError::ParsingI64)?); + } + + match match graph_schema.verify_id_set(&ids_hashset, schema_type).await { + None => { + graph_schema + .refresh(schema_type, conn, Some(&ids_hashset)) + .await? + } + relevant_ids => relevant_ids, + } { + Some(relevant_ids) => { + let mut parsed_ids = Vec::with_capacity(raw_ids.len()); + for id in raw_ids { + parsed_ids.push( + id.to_i64() + .ok_or(FalkorDBError::ParsingI64) + .and_then(|id| { + relevant_ids + .get(&id) + .cloned() + .ok_or(FalkorDBError::ParsingCompactIdUnknown) + })?, + ); + } + + Ok(parsed_ids) + } + _ => Err(FalkorDBError::ParsingError)?, + } +} + +#[async_recursion] +pub(crate) async fn parse_type_async( + type_marker: i64, + val: FalkorValue, + graph_schema: &AsyncGraphSchema, + conn: &mut FalkorAsyncConnection, +) -> Result { + let res = match type_marker { + 1 => FalkorValue::None, + 2 => FalkorValue::FString(val.into_string()?), + 3 => FalkorValue::Int64(val.to_i64().ok_or(FalkorDBError::ParsingI64)?), + 4 => FalkorValue::FBool(val.to_bool().ok_or(FalkorDBError::ParsingBool)?), + 5 => FalkorValue::F64(val.to_f64().ok_or(FalkorDBError::ParsingF64)?), + 6 => FalkorValue::FArray({ + let val = val.into_vec()?; + let mut parsed_vec = Vec::with_capacity(val.len()); + for item in val { + let (type_marker, val) = type_val_from_value(item)?; + parsed_vec.push(parse_type_async(type_marker, val, graph_schema, conn).await?); + } + parsed_vec + }), + // The following types are sent as an array and require specific parsing functions + 7 => FalkorValue::FEdge(Edge::from_falkor_value_async(val, graph_schema, conn).await?), + 8 => FalkorValue::FNode(Node::from_falkor_value_async(val, graph_schema, conn).await?), + 9 => FalkorValue::FPath(Path::from_falkor_value_async(val, graph_schema, conn).await?), + 10 => FalkorValue::FMap(HashMap::from_falkor_value_async(val, graph_schema, conn).await?), + 11 => FalkorValue::FPoint(Point::parse(val)?), + _ => Err(FalkorDBError::ParsingUnknownType)?, + }; + + Ok(res) +} From 69d818e05e143731303f3334adad5c48905b7b52 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 22 May 2024 17:03:54 +0300 Subject: [PATCH 09/62] Documented all public functions --- resources/imdb.rdb | Bin 0 -> 109007 bytes src/client/asynchronous.rs | 35 ++-- src/client/blocking.rs | 113 +++++++++--- src/client/builder.rs | 62 ++++--- src/client/mod.rs | 8 +- src/connection/asynchronous.rs | 56 +++++- src/connection/blocking.rs | 6 +- src/connection_info/mod.rs | 7 +- src/error/mod.rs | 2 + src/graph/asynchronous.rs | 48 +---- src/graph/blocking.rs | 208 +++++++++++++++++----- src/graph/mod.rs | 2 +- src/graph/utils.rs | 4 +- src/graph_schema/blocking.rs | 28 +-- src/graph_schema/mod.rs | 3 + src/lib.rs | 12 +- src/parser/mod.rs | 4 +- src/redis_ext.rs | 4 +- src/{value => response}/execution_plan.rs | 4 + src/response/mod.rs | 21 +++ src/{value => response}/query_result.rs | 23 +-- src/{value => response}/slowlog_entry.rs | 5 + src/value/config.rs | 1 + src/value/constraint.rs | 17 +- src/value/graph_entities.rs | 9 + src/value/mod.rs | 126 ++++++++++++- src/value/path.rs | 1 + src/value/point.rs | 6 +- 28 files changed, 620 insertions(+), 195 deletions(-) create mode 100644 resources/imdb.rdb rename src/{value => response}/execution_plan.rs (77%) create mode 100644 src/response/mod.rs rename src/{value => response}/query_result.rs (88%) rename src/{value => response}/slowlog_entry.rs (75%) diff --git a/resources/imdb.rdb b/resources/imdb.rdb new file mode 100644 index 0000000000000000000000000000000000000000..d8421d042c55fe2ff9a9b28ee4c943e67a462f1f GIT binary patch literal 109007 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 z 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, } -impl AsyncFalkorClient { - pub(crate) async fn create(client: FalkorClientImpl) -> Result> { - Ok(Arc::new(Self { +impl FalkorAsyncClient { + pub(crate) async fn create(client: FalkorClientProvider) -> Result>> { + Ok(Arc::new(Mutex::new(Self { connection: client.get_async_connection(None).await?, _inner: client, - })) + graph_cache: Default::default(), + }))) } pub(crate) fn clone_connection(&self) -> FalkorAsyncConnection { self.connection.clone() @@ -137,11 +140,23 @@ impl AsyncFalkorClient { Ok(()) } - pub async fn open_graph(&self, graph_name: T) -> AsyncGraph { + pub fn open_graph(&mut self, graph_name: T) -> AsyncGraph { AsyncGraph { - client: self, + connection: self.connection.clone(), graph_name: graph_name.to_string(), - graph_schema: AsyncGraphSchema::new(graph_name.to_string()), + graph_schema: self + .graph_cache + .entry(graph_name.to_string()) + .or_insert(AsyncGraphSchema::new(graph_name.to_string())) + .clone(), } } + + pub async fn copy_graph(&mut self, cloned_graph_name: T) -> Result { + self.connection + .clone() + .send_command(Some(cloned_graph_name.to_string()), "GRAPH.COPY", None) + .await?; + Ok(self.open_graph(cloned_graph_name)) + } } diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 63df6f1..235c8bb 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -4,46 +4,73 @@ */ use crate::{ - client::FalkorClientImpl, + client::FalkorClientProvider, connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}, ConfigValue, FalkorDBError, SyncGraph, SyncGraphSchema, }; use anyhow::Result; +use parking_lot::Mutex; use std::{ collections::HashMap, sync::{mpsc, Arc}, }; -pub struct SyncFalkorClient { - _inner: FalkorClientImpl, +pub(crate) struct FalkorSyncClientInner { + _inner: Mutex, + graph_cache: Mutex>, connection_pool_tx: mpsc::SyncSender, connection_pool_rx: mpsc::Receiver, } -unsafe impl Sync for SyncFalkorClient {} -unsafe impl Send for SyncFalkorClient {} +impl FalkorSyncClientInner { + pub(crate) fn borrow_connection(&self) -> Result { + Ok(BorrowedSyncConnection { + return_tx: self.connection_pool_tx.clone(), + conn: Some(self.connection_pool_rx.recv()?), + }) + } +} + +unsafe impl Sync for FalkorSyncClientInner {} +unsafe impl Send for FalkorSyncClientInner {} + +/// This is the publicly exposed API of the sync Falkor Client +/// It makes no assumptions in regard to which database the Falkor module is running on, +/// and will select it based on enabled features and url connection +/// +/// # Thread Safety +/// This struct is fully thread safe, it can be cloned and passed within threads without constraints, +/// Its API uses only immutable references +#[derive(Clone)] +pub struct FalkorSyncClient { + inner: Arc, +} -impl SyncFalkorClient { - pub(crate) fn create(client: FalkorClientImpl, num_connections: u8) -> Result> { +impl FalkorSyncClient { + pub(crate) fn create(client: FalkorClientProvider, num_connections: u8) -> Result { let (connection_pool_tx, connection_pool_rx) = mpsc::sync_channel(num_connections as usize); for _ in 0..num_connections { connection_pool_tx.send(client.get_connection(None)?)?; } - Ok(Arc::new(Self { - _inner: client, - connection_pool_tx, - connection_pool_rx, - })) + Ok(Self { + inner: Arc::new(FalkorSyncClientInner { + _inner: client.into(), + graph_cache: Default::default(), + connection_pool_tx, + connection_pool_rx, + }), + }) } pub(crate) fn borrow_connection(&self) -> Result { - Ok(BorrowedSyncConnection { - return_tx: self.connection_pool_tx.clone(), - conn: Some(self.connection_pool_rx.recv()?), - }) + self.inner.borrow_connection() } + /// Return a list of graphs currently residing in the database + /// + /// # Returns + /// A [`Vec`] of [`String`]s, containing the names of available graphs pub fn list_graphs(&self) -> Result> { let mut conn = self.borrow_connection()?; @@ -75,8 +102,14 @@ impl SyncFalkorClient { Ok(graph_list) } - /// This function returns either an [`FVec`] containing the requested key and val, - /// or an [`FVec`] of [`FVec`]s, each one containing a key and val pair + /// Return the current value of a configuration option in the database. + /// + /// # Arguments + /// * `config_Key`: A [`String`] representation of a configuration's key. + /// The config key can also be "*", which will return ALL the configuration options. + /// + /// # Returns + /// A [`HashMap`] comprised of [`String`] keys, and [`ConfigValue`] values. pub fn config_get(&self, config_key: T) -> Result> { let mut conn = self.borrow_connection()?; @@ -130,6 +163,12 @@ impl SyncFalkorClient { }) } + /// Return the current value of a configuration option in the database. + /// + /// # Arguments + /// * `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. pub fn config_set, C: Into>( &self, config_key: T, @@ -152,11 +191,45 @@ impl SyncFalkorClient { Ok(()) } + /// Opens a graph context for queries and operations + /// + /// # Arguments + /// * `graph_name`: A string identifier of the graph to open. + /// + /// # Returns + /// a [`SyncGraph`] object, allowing various graph operations. pub fn open_graph(&self, graph_name: T) -> SyncGraph { SyncGraph { - client: self, + client: self.inner.clone(), graph_name: graph_name.to_string(), - graph_schema: SyncGraphSchema::new(graph_name.to_string()), + graph_schema: self + .inner + .graph_cache + .lock() + .entry(graph_name.to_string()) + .or_insert(SyncGraphSchema::new(graph_name.to_string())) + .clone(), } } + + /// Copies an entire graph and returns the [`SyncGraph`] for the new copied graph. + /// + /// # Arguments + /// * `graph_to_clone`: A string identifier of the graph to copy. + /// * `new_graph_name`: The name to give the new graph. + /// + /// # Returns + /// If successful, will return the new [`SyncGraph`] object. + pub fn copy_graph( + &self, + graph_to_clone: T, + new_graph_name: T, + ) -> Result { + self.borrow_connection()?.send_command( + Some(graph_to_clone.to_string()), + "GRAPH.COPY", + None, + )?; + Ok(self.open_graph(new_graph_name.to_string())) + } } diff --git a/src/client/builder.rs b/src/client/builder.rs index 72d09bb..9a76587 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -3,20 +3,24 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{client::FalkorClientImpl, FalkorConnectionInfo, FalkorDBError, SyncFalkorClient}; +use crate::{client::FalkorClientProvider, FalkorConnectionInfo, FalkorDBError, FalkorSyncClient}; use anyhow::Result; -use std::sync::Arc; -// This doesn't have a default implementation because a specific const char is required -// and I don't want to leave that up to the user -pub struct FalkorDBClientBuilder { +/// A Builder-pattern implementation struct for creating a new Falkor client, sync or async. +pub struct FalkorClientBuilder { connection_info: Option, num_connections: u8, - #[cfg(feature = "tokio")] - runtime: Option, } -impl FalkorDBClientBuilder { +impl FalkorClientBuilder { + /// Provide a connection info for the database connection + /// Will otherwise use the default connection details. + /// + /// # Arguments + /// * `falkor_connection_info`: the [`FalkorConnectionInfo`] to provide + /// + /// # Returns + /// The consumed and modified self. pub fn with_connection_info(self, falkor_connection_info: FalkorConnectionInfo) -> Self { Self { connection_info: Some(falkor_connection_info), @@ -24,6 +28,13 @@ impl FalkorDBClientBuilder { } } + /// Specify how large a connection pool to maintain, for concurrent operations. + /// + /// # Arguments + /// * `num_connections`: the numer of connections, a non-negative integer, between 1 and 32 + /// + /// # Returns + /// The consumed and modified self. pub fn with_num_connections(self, num_connections: u8) -> Self { Self { num_connections, @@ -32,31 +43,35 @@ impl FalkorDBClientBuilder { } } -fn get_client>(connection_info: T) -> Result +fn get_client>(connection_info: T) -> Result where anyhow::Error: From, { let connection_info = connection_info.try_into()?; Ok(match connection_info { FalkorConnectionInfo::Redis(connection_info) => { - FalkorClientImpl::Redis(redis::Client::open(connection_info.clone())?) + FalkorClientProvider::Redis(redis::Client::open(connection_info.clone())?) } }) } -impl FalkorDBClientBuilder<'S'> { - // We wish this to be explicit, and implementing Default is pub - #[allow(clippy::new_without_default)] +impl FalkorClientBuilder<'S'> { + /// Creates a new [`FalkorClientBuilder`] for a sync client. + /// + /// # Returns + /// The new [`FalkorClientBuilder`] pub fn new() -> Self { - FalkorDBClientBuilder { + FalkorClientBuilder { connection_info: None, num_connections: 4, - #[cfg(feature = "tokio")] - runtime: None, } } - pub fn build(self) -> Result> { + /// Consume the builder, returning the newly constructed sync client + /// + /// # Returns + /// a new [`FalkorSyncClient`] + pub fn build(self) -> Result { if self.num_connections < 1 || self.num_connections > 32 { return Err(FalkorDBError::InvalidConnectionPoolSize.into()); } @@ -65,25 +80,26 @@ impl FalkorDBClientBuilder<'S'> { .connection_info .unwrap_or("falkor://127.0.0.1:6379".try_into()?); - SyncFalkorClient::create(get_client(connection_info.clone())?, self.num_connections) + FalkorSyncClient::create(get_client(connection_info.clone())?, self.num_connections) } } #[cfg(feature = "tokio")] -impl FalkorDBClientBuilder<'A'> { +impl FalkorClientBuilder<'A'> { pub fn new_async() -> Self { - FalkorDBClientBuilder { + FalkorClientBuilder { connection_info: None, num_connections: 4, - runtime: None, } } - pub async fn build(self) -> Result> { + pub async fn build( + self, + ) -> Result>> { let connection_info = self .connection_info .unwrap_or("falkor://127.0.0.1:6379".try_into()?); - crate::AsyncFalkorClient::create(get_client(connection_info)?).await + crate::FalkorAsyncClient::create(get_client(connection_info)?).await } } diff --git a/src/client/mod.rs b/src/client/mod.rs index 340e440..0729614 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -13,19 +13,19 @@ pub(crate) mod builder; #[cfg(feature = "tokio")] pub(crate) mod asynchronous; -pub(crate) enum FalkorClientImpl { +pub(crate) enum FalkorClientProvider { #[cfg(feature = "redis")] Redis(redis::Client), } -impl FalkorClientImpl { +impl FalkorClientProvider { pub(crate) fn get_connection( &self, connection_timeout: Option, ) -> Result { Ok(match self { #[cfg(feature = "redis")] - FalkorClientImpl::Redis(redis_client) => connection_timeout + FalkorClientProvider::Redis(redis_client) => connection_timeout .map(|timeout| redis_client.get_connection_with_timeout(timeout)) .unwrap_or_else(|| redis_client.get_connection())? .into(), @@ -38,7 +38,7 @@ impl FalkorClientImpl { connection_timeout: Option, ) -> Result { Ok(match self { - FalkorClientImpl::Redis(redis_client) => match connection_timeout { + FalkorClientProvider::Redis(redis_client) => match connection_timeout { Some(timeout) => { redis_client .get_multiplexed_tokio_connection_with_response_timeouts(timeout, timeout) diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs index bbdcf0e..78f4add 100644 --- a/src/connection/asynchronous.rs +++ b/src/connection/asynchronous.rs @@ -3,7 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use crate::graph::utils::construct_query; use crate::FalkorValue; +use anyhow::Result; +use std::collections::HashMap; #[derive(Clone)] pub enum FalkorAsyncConnection { @@ -17,7 +20,7 @@ impl FalkorAsyncConnection { graph_name: Option, command: &str, params: Option, - ) -> anyhow::Result { + ) -> Result { Ok(match self { #[cfg(feature = "redis")] FalkorAsyncConnection::Redis(redis_conn) => { @@ -29,4 +32,55 @@ impl FalkorAsyncConnection { } }) } + + async fn query_internal( + &mut self, + graph_name: String, + command: &str, + query_string: Q, + params: Option<&HashMap>, + timeout: Option, + ) -> Result { + let query = construct_query(query_string, params); + + Ok(match self { + #[cfg(feature = "redis")] + FalkorAsyncConnection::Redis(redis_conn) => { + use redis::FromRedisValue; + let redis_val = redis_conn + .send_packed_command( + redis::cmd(command) + .arg(graph_name.as_str()) + .arg(query) + .arg("--compact") + .arg(timeout.map(|timeout| format!("timeout {timeout}"))), + ) + .await?; + + FalkorValue::from_owned_redis_value(redis_val)? + } + }) + } + + pub(crate) async fn query( + &mut self, + graph_name: String, + query_string: Q, + params: Option<&HashMap>, + timeout: Option, + ) -> Result { + self.query_internal(graph_name, "GRAPH.QUERY", query_string, params, timeout) + .await + } + + pub(crate) async fn query_readonly( + &mut self, + graph_name: String, + query_string: Q, + params: Option<&HashMap>, + timeout: Option, + ) -> Result { + self.query_internal(graph_name, "GRAPH.QUERY_RO", query_string, params, timeout) + .await + } } diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 1c198ca..9f2138f 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -8,11 +8,15 @@ use anyhow::Result; use redis::ConnectionLike; use std::sync::mpsc; -pub enum FalkorSyncConnection { +pub(crate) enum FalkorSyncConnection { #[cfg(feature = "redis")] Redis(redis::Connection), } +/// A container for a connection that is borrowed from the pool. +/// Upon going out of scope, it will return the connection to the pool. +/// +/// This is publicly exposed for user-implementations of [`FalkorParsable`](crate::FalkorParsable) pub struct BorrowedSyncConnection { pub(crate) conn: Option, pub(crate) return_tx: mpsc::SyncSender, diff --git a/src/connection_info/mod.rs b/src/connection_info/mod.rs index daae061..73c8d4b 100644 --- a/src/connection_info/mod.rs +++ b/src/connection_info/mod.rs @@ -5,6 +5,8 @@ use anyhow::Result; +/// An agnostic container which allows maintaining of various connection details. +/// The different enum variants are enabled based on compilation features #[derive(Clone)] pub enum FalkorConnectionInfo { #[cfg(feature = "redis")] @@ -12,9 +14,6 @@ pub enum FalkorConnectionInfo { } impl FalkorConnectionInfo { - /// Redis is currently only option, and I feel will be the default for a while - /// So any new option should be set here should be added before redis - /// as a #[cfg(and(feature = , not(feature = "redis"))] fn fallback_provider(full_url: String) -> Result { #[cfg(feature = "redis")] Ok(FalkorConnectionInfo::Redis( @@ -59,7 +58,6 @@ impl TryFrom<&str> for FalkorConnectionInfo { } } -// Calls TryFrom<&str> impl TryFrom for FalkorConnectionInfo { type Error = anyhow::Error; @@ -69,7 +67,6 @@ impl TryFrom for FalkorConnectionInfo { } } -// Calls TryFrom impl TryFrom<(T, u16)> for FalkorConnectionInfo { type Error = anyhow::Error; diff --git a/src/error/mod.rs b/src/error/mod.rs index 5c499cd..aceba48 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -3,6 +3,8 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +/// A verbose error enum used throughout the client, messages are static string slices. +/// this allows easy [`anyhow`] integration using [`thiserror`] #[derive(thiserror::Error, Debug)] pub enum FalkorDBError { #[error("The provided connection info is invalid")] diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index 3a88745..c200df8 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -5,18 +5,18 @@ use super::utils::{construct_query, generate_procedure_call}; use crate::{ - AsyncFalkorClient, AsyncGraphSchema, ExecutionPlan, FalkorAsyncConnection, - FalkorAsyncParseable, FalkorDBError, FalkorValue, QueryResult, SlowlogEntry, + AsyncGraphSchema, ExecutionPlan, FalkorAsyncConnection, FalkorAsyncParseable, FalkorDBError, + FalkorValue, QueryResult, SlowlogEntry, }; use std::collections::HashMap; -pub struct AsyncGraph<'a> { - pub(crate) client: &'a AsyncFalkorClient, +pub struct AsyncGraph { pub(crate) graph_name: String, + pub(crate) connection: FalkorAsyncConnection, pub(crate) graph_schema: AsyncGraphSchema, } -impl AsyncGraph<'_> { +impl AsyncGraph { pub fn graph_name(&self) -> &str { self.graph_name.as_str() } @@ -26,17 +26,11 @@ impl AsyncGraph<'_> { command: &str, params: Option, ) -> anyhow::Result { - let mut conn = self.client.clone_connection(); + let mut conn = self.connection.clone(); conn.send_command(Some(self.graph_name.clone()), command, params) .await } - pub async fn copy(&self, cloned_graph_name: T) -> anyhow::Result { - self.send_command("GRAPH.COPY", Some(cloned_graph_name.to_string())) - .await?; - Ok(self.client.open_graph(cloned_graph_name).await) - } - pub async fn delete(&self) -> anyhow::Result<()> { self.send_command("GRAPH.DELETE", None).await?; Ok(()) @@ -97,36 +91,6 @@ impl AsyncGraph<'_> { .await } - async fn query_with_parser( - &self, - command: &str, - query_string: Q, - params: Option<&HashMap>, - timeout: Option, - ) -> anyhow::Result

{ - let query = construct_query(query_string, params); - - let mut conn = self.client.clone_connection(); - let falkor_result = match &mut conn { - #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(redis_conn) => { - use redis::FromRedisValue; - let redis_val = redis_conn - .send_packed_command( - redis::cmd(command) - .arg(self.graph_name.as_str()) - .arg(query) - .arg("--compact") - .arg(timeout.map(|timeout| format!("timeout {timeout}"))), - ) - .await?; - FalkorValue::from_owned_redis_value(redis_val)? - } - }; - - P::from_falkor_value_async(falkor_result, &self.graph_schema, &mut conn).await - } - pub async fn query_with_params< Q: ToString, T: ToString, diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 687f4b7..8bf0ea9 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -5,20 +5,33 @@ use super::utils::{construct_query, generate_procedure_call}; use crate::{ - connection::blocking::FalkorSyncConnection, ExecutionPlan, FalkorDBError, FalkorParsable, - FalkorValue, QueryResult, SlowlogEntry, SyncFalkorClient, SyncGraphSchema, + client::blocking::FalkorSyncClientInner, connection::blocking::FalkorSyncConnection, + Constraint, ExecutionPlan, FalkorDBError, FalkorParsable, FalkorValue, QueryResult, + SlowlogEntry, SyncGraphSchema, }; use anyhow::Result; use redis::ConnectionLike; -use std::collections::HashMap; +use std::{collections::HashMap, sync::Arc}; -pub struct SyncGraph<'a> { - pub(crate) client: &'a SyncFalkorClient, +/// The main graph API, this allows the user to perform graph operations while exposing as little details as possible. +/// +/// # Thread Safety +/// This struct is fully thread safe, it can be cloned and passed within threads without constraints, +/// Its API uses only immutable references +#[derive(Clone)] +pub struct SyncGraph { + pub(crate) client: Arc, pub(crate) graph_name: String, - pub(crate) graph_schema: SyncGraphSchema, + /// Provides user with access to the current graph schema, + /// which contains a safe cache of id to labels/properties/relationship maps + pub graph_schema: SyncGraphSchema, } -impl SyncGraph<'_> { +impl SyncGraph { + /// Returns the name of the graph for which this API performs operations. + /// + /// # Returns + /// The graph name as a string slice, without cloning. pub fn graph_name(&self) -> &str { self.graph_name.as_str() } @@ -28,16 +41,18 @@ impl SyncGraph<'_> { conn.send_command(Some(self.graph_name.clone()), command, params) } - pub fn copy(&self, cloned_graph_name: T) -> Result { - self.send_command("GRAPH.COPY", Some(cloned_graph_name.to_string()))?; - Ok(self.client.open_graph(cloned_graph_name)) - } - + /// Deletes the graph stored in the database, and drop all the schema caches. + /// NOTE: This still maintains the graph API, operations are still viable. pub fn delete(&self) -> Result<()> { self.send_command("GRAPH.DELETE", None)?; + self.graph_schema.clear(); Ok(()) } + /// Retrieves the slowlog data, which contains info about the N slowest queries. + /// + /// # Returns + /// A [`Vec`] of [`SlowlogEntry`], providing information about each query. pub fn slowlog(&self) -> Result> { let res = self.send_command("GRAPH.SLOWLOG", None)?.into_vec()?; @@ -58,43 +73,80 @@ impl SyncGraph<'_> { Ok(slowlog_entries) } + /// Resets the slowlog, all query time data will be cleared. pub fn slowlog_reset(&self) -> Result<()> { self.send_command("GRAPH.SLOWLOG", Some("RESET".to_string()))?; Ok(()) } + /// Returns an [`ExecutionPlan`] object for the selected query, + /// showing how long each step took to perform. + /// This function variant allows adding extra parameters after the query + /// + /// # Arguments + /// * `query_string`: The query to profile + /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// + /// # Returns + /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. pub fn profile_with_params( &self, query_string: Q, - params: Option<&HashMap>, + params: Option>, ) -> Result { let query = construct_query(query_string, params); ExecutionPlan::try_from(self.send_command("GRAPH.PROFILE", Some(query))?) } + /// Returns an [`ExecutionPlan`] object for the selected query, + /// showing how long each step took to perform. + /// + /// # Arguments + /// * `query_string`: The query to profile + /// + /// # Returns + /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. pub fn profile(&self, query_string: Q) -> Result { self.profile_with_params::(query_string, None) } + /// Returns an [`ExecutionPlan`] object for the selected query, + /// showing the internals steps the database will go through to perform the query. + /// This function variant allows adding extra parameters after the query + /// + /// # Arguments + /// * `query_string`: The query to explain + /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// + /// # Returns + /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. pub fn explain_with_params( &self, query_string: Q, - params: Option<&HashMap>, + params: Option>, ) -> Result { let query = construct_query(query_string, params); ExecutionPlan::try_from(self.send_command("GRAPH.EXPLAIN", Some(query))?) } + /// Returns an [`ExecutionPlan`] object for the selected query, + /// showing the internals steps the database will go through to perform the query. + /// + /// # Arguments + /// * `query_string`: The query to explain + /// + /// # Returns + /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. pub fn explain(&self, query_string: Q) -> Result { self.explain_with_params::(query_string, None) } - fn query_with_parser( + fn query_inner( &self, command: &str, query_string: Q, - params: Option<&HashMap>, + params: Option>, timeout: Option, ) -> Result

{ let query = construct_query(query_string, params); @@ -118,58 +170,117 @@ impl SyncGraph<'_> { P::from_falkor_value(falkor_result, &self.graph_schema, &mut conn) } - pub fn query_with_params( + /// Run a query on the graph + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub fn query(&self, query_string: Q, timeout: Option) -> Result { + self.query_inner::("GRAPH.QUERY", query_string, None, timeout) + } + + /// Run a query on the graph + /// This function variant allows adding extra parameters after the query + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub fn query_with_params( &self, query_string: Q, - params: Option<&HashMap>, - readonly: bool, timeout: Option, - ) -> Result

{ - self.query_with_parser( - if readonly { - "GRAPH.RO_QUERY" - } else { - "GRAPH.QUERY" - }, + params: HashMap, + ) -> Result { + self.query_inner("GRAPH.QUERY", query_string, Some(params), timeout) + } + + /// Run a query on the graph + /// Read-only queries are more limited with the operations they are allowed to perform. + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub fn query_readonly( + &self, + query_string: Q, + timeout: Option, + ) -> Result { + self.query_inner::( + "GRAPH.QUERY_RO", query_string, - params, + None, timeout, ) } - pub fn query(&self, query_string: Q, timeout: Option) -> Result { - self.query_with_params::(query_string, None, false, timeout) - } - - pub fn query_readonly( + /// Run a read-only query on the graph + /// Read-only queries are more limited with the operations they are allowed to perform. + /// This function variant allows adding extra parameters after the query + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub fn query_readonly_with_params( &self, query_string: Q, timeout: Option, + params: HashMap, ) -> Result { - self.query_with_params::(query_string, None, true, timeout) + self.query_inner("GRAPH.QUERY_RO", query_string, Some(params), timeout) } + /// Run a query which calls a procedure on the graph, read-only, or otherwise. + /// Read-only queries are more limited with the operations they are allowed to perform. + /// This function allows adding extra parameters after the query, and adding a YIELD block afterwards + /// + /// # Arguments + /// * `procedure`: The procedure to call + /// * `args`: An optional slice of strings containing the parameters. + /// * `yields`: The optional yield block arguments. + /// * `read_only`: Whether this procedure is read-only. + /// * `timeout`: If provided, the query will abort if overruns the timeout. + /// + /// # Returns + /// A caller-provided type which implements [`FalkorParsable`] pub fn call_procedure( &self, procedure: C, args: Option<&[String]>, yields: Option<&[String]>, - read_only: Option, + read_only: bool, timeout: Option, ) -> Result

{ let (query_string, params) = generate_procedure_call(procedure, args, yields); - self.query_with_params( + self.query_inner( + read_only + .then_some("GRAPH.QUERY_RO") + .unwrap_or("GRAPH.QUERY"), query_string, - params.as_ref(), - read_only.unwrap_or_default(), + params, timeout, ) } + /// Calls the DB.INDICES procedure on the graph + /// TODO: pub fn list_indices(&self) -> Result { let query_res = self - .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, None, None)? + .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, true, None)? .into_vec()?; for item in query_res { @@ -178,16 +289,25 @@ impl SyncGraph<'_> { Ok(FalkorValue::None) } - pub fn list_constraints(&self) -> Result { + /// Calls the DB.CONSTRAINTS procedure on the graph, returning an array of the graph's constraints + /// + /// # Returns + /// A [`Vec`] of [`Constraint`]s + pub fn list_constraints(&self) -> Result> { + let mut conn = self.client.borrow_connection()?; let query_res = self - .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, None, None)? + .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, true, None)? .into_vec()?; + let mut constraints_vec = Vec::with_capacity(query_res.len()); for item in query_res { - for sub_item in item.into_vec()? { - log::info!("{sub_item:?}"); - } + constraints_vec.push(Constraint::from_falkor_value( + item, + &self.graph_schema, + &mut conn, + )?); } - Ok(FalkorValue::None) + + Ok(constraints_vec) } } diff --git a/src/graph/mod.rs b/src/graph/mod.rs index a0e8ac6..eec005f 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -4,7 +4,7 @@ */ pub(crate) mod blocking; -mod utils; +pub(crate) mod utils; #[cfg(feature = "tokio")] pub(crate) mod asynchronous; diff --git a/src/graph/utils.rs b/src/graph/utils.rs index c255353..9d72ca6 100644 --- a/src/graph/utils.rs +++ b/src/graph/utils.rs @@ -47,12 +47,12 @@ pub(crate) fn generate_procedure_call( pub(crate) fn construct_query( query_str: Q, - params: Option<&HashMap>, + params: Option>, ) -> String { params .map(|params| { params - .iter() + .into_iter() .fold("CYPHER ".to_string(), |acc, (key, val)| { acc + format!("{}={}", key.to_string(), val.to_string()).as_str() }) diff --git a/src/graph_schema/blocking.rs b/src/graph_schema/blocking.rs index 9f706fe..d06d8d5 100644 --- a/src/graph_schema/blocking.rs +++ b/src/graph_schema/blocking.rs @@ -20,6 +20,11 @@ use std::{ pub(crate) type LockableIdMap = Arc>>; +/// A struct containing the various schema maps, allowing conversions between ids and their string representations. +/// +/// # Thread Safety +/// This struct is fully thread safe, it can be cloned and passed within threads without constraints, +/// Its API uses only immutable references #[derive(Clone, Debug, Default)] pub struct SyncGraphSchema { graph_name: String, @@ -30,37 +35,40 @@ pub struct SyncGraphSchema { } impl SyncGraphSchema { - pub fn new(graph_name: String) -> Self { + pub(crate) fn new(graph_name: String) -> Self { Self { graph_name, ..Default::default() } } - pub fn clear(&mut self) { + /// Clears all cached schemas, this will cause a refresh when next attempting to parse a compact query. + pub fn clear(&self) { self.version.store(0, SeqCst); self.labels.write().clear(); self.properties.write().clear(); self.relationships.write().clear(); } - pub fn graph_name(&self) -> String { - self.graph_name.clone() - } - - pub(crate) fn relationships(&self) -> LockableIdMap { + /// Returns a read-write-locked map, of the relationship ids to their respective string representations. + /// Minimize locking these to avoid starvation. + pub fn relationships(&self) -> LockableIdMap { self.relationships.clone() } - pub(crate) fn labels(&self) -> LockableIdMap { + /// Returns a read-write-locked map, of the label ids to their respective string representations. + /// Minimize locking these to avoid starvation. + pub fn labels(&self) -> LockableIdMap { self.labels.clone() } - pub(crate) fn properties(&self) -> LockableIdMap { + /// Returns a read-write-locked map, of the property ids to their respective string representations. + /// Minimize locking these to avoid starvation. + pub fn properties(&self) -> LockableIdMap { self.properties.clone() } - pub fn verify_id_set( + pub(crate) fn verify_id_set( &self, id_set: &HashSet, schema_type: SchemaType, diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index dbab24d..ef4f26d 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -9,6 +9,9 @@ mod utils; #[cfg(feature = "tokio")] pub(crate) mod asynchronous; +/// An enum specifying which schema type we are addressing +/// When querying using the compact parser, ids are returned for the various schema entities instead of strings +/// Using this enum we know which of the schema maps to access in order to convert these ids to strings #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum SchemaType { Labels, diff --git a/src/lib.rs b/src/lib.rs index fdb112f..ed75929 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,32 +13,34 @@ mod error; mod graph; mod graph_schema; mod parser; +mod response; mod value; #[cfg(feature = "redis")] mod redis_ext; -pub use client::{blocking::SyncFalkorClient, builder::FalkorDBClientBuilder}; +pub use client::{blocking::FalkorSyncClient, builder::FalkorClientBuilder}; pub use connection_info::FalkorConnectionInfo; pub use error::FalkorDBError; pub use graph::blocking::SyncGraph; pub use graph_schema::{blocking::SyncGraphSchema, SchemaType}; pub use parser::FalkorParsable; +pub use response::{ + execution_plan::ExecutionPlan, query_result::QueryResult, slowlog_entry::SlowlogEntry, + ResponseVariant, +}; pub use value::{ config::ConfigValue, constraint::Constraint, - execution_plan::ExecutionPlan, graph_entities::{Edge, Node}, path::Path, point::Point, - query_result::QueryResult, - slowlog_entry::SlowlogEntry, FalkorValue, }; #[cfg(feature = "tokio")] pub use { - client::asynchronous::AsyncFalkorClient, connection::asynchronous::FalkorAsyncConnection, + client::asynchronous::FalkorAsyncClient, connection::asynchronous::FalkorAsyncConnection, graph::asynchronous::AsyncGraph, graph_schema::asynchronous::AsyncGraphSchema, parser::FalkorAsyncParseable, }; diff --git a/src/parser/mod.rs b/src/parser/mod.rs index e873685..a740cd3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5,8 +5,8 @@ use crate::{connection::blocking::BorrowedSyncConnection, FalkorValue, SyncGraphSchema}; use anyhow::Result; -use std::future::Future; +/// This trait allows implementing a parser from the table-style result sent by the database, to any other struct pub trait FalkorParsable: Sized { fn from_falkor_value( value: FalkorValue, @@ -21,5 +21,5 @@ pub trait FalkorAsyncParseable: Sized { value: FalkorValue, graph_schema: &crate::AsyncGraphSchema, conn: &mut crate::FalkorAsyncConnection, - ) -> impl Future> + Send; + ) -> impl std::future::Future> + Send; } diff --git a/src/redis_ext.rs b/src/redis_ext.rs index e52d5f8..ce15e9a 100644 --- a/src/redis_ext.rs +++ b/src/redis_ext.rs @@ -4,7 +4,7 @@ */ use crate::{ - client::FalkorClientImpl, connection::blocking::FalkorSyncConnection, ConfigValue, + client::FalkorClientProvider, connection::blocking::FalkorSyncConnection, ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorValue, }; use anyhow::Result; @@ -29,7 +29,7 @@ impl From for FalkorConnectionInfo { } } -impl From for FalkorClientImpl { +impl From for FalkorClientProvider { fn from(value: redis::Client) -> Self { Self::Redis(value) } diff --git a/src/value/execution_plan.rs b/src/response/execution_plan.rs similarity index 77% rename from src/value/execution_plan.rs rename to src/response/execution_plan.rs index b47c563..0a7cebb 100644 --- a/src/value/execution_plan.rs +++ b/src/response/execution_plan.rs @@ -5,16 +5,20 @@ use crate::FalkorValue; +/// An execution plan, storing both the specific string details for each step, and also a formatted plaintext string for pretty display +#[derive(Debug, Clone)] pub struct ExecutionPlan { text: String, steps: Vec, } impl ExecutionPlan { + /// Returns a pretty-print version of the execution plan pub fn text(&self) -> &str { self.text.as_str() } + /// Returns a slice of strings representing each step in the execution plan, which can be iterated. pub fn steps(&self) -> &[String] { self.steps.as_slice() } diff --git a/src/response/mod.rs b/src/response/mod.rs new file mode 100644 index 0000000..ca85f9d --- /dev/null +++ b/src/response/mod.rs @@ -0,0 +1,21 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use execution_plan::ExecutionPlan; +use query_result::QueryResult; +use slowlog_entry::SlowlogEntry; + +pub(crate) mod execution_plan; +pub(crate) mod query_result; +pub(crate) mod slowlog_entry; + +/// A naive wrapper for the various possible responses from queries +/// Its main usecase is for creating things like [tokio::task::JoinSet]s, which require all the tasks to return the same type +#[derive(Debug, Clone)] +pub enum ResponseVariant { + ExecutionPlan(ExecutionPlan), + QueryResult(QueryResult), + SlowlogEntry(SlowlogEntry), +} diff --git a/src/value/query_result.rs b/src/response/query_result.rs similarity index 88% rename from src/value/query_result.rs rename to src/response/query_result.rs index f19f8c1..7dc64a4 100644 --- a/src/value/query_result.rs +++ b/src/response/query_result.rs @@ -3,40 +3,41 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use super::utils::{parse_type, type_val_from_value}; use crate::{ - connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, FalkorValue, - SyncGraphSchema, + connection::blocking::BorrowedSyncConnection, + value::utils::{parse_type, type_val_from_value}, + FalkorDBError, FalkorParsable, FalkorValue, SyncGraphSchema, }; use anyhow::Result; use std::collections::HashMap; #[cfg(feature = "tokio")] -use super::utils_async::parse_type_async; +use crate::value::utils_async::parse_type_async; +/// A struct returned by the various queries, containing the result set, header, and stats #[derive(Clone, Debug, Default)] pub struct QueryResult { - pub(crate) stats: Vec, - pub(crate) header: Vec, - pub(crate) result_set: Vec>, + /// The statistics for this query, such as how long it took + pub stats: Vec, + pub header: Vec, + pub result_set: Vec>, } impl QueryResult { + /// Returns a slice of the statistics for this query, such as how long it took pub fn stats(&self) -> &[String] { self.stats.as_slice() } + /// Returns a slice of header for this query result, usually contains the verbose names of each column of the result set pub fn header(&self) -> &[String] { self.header.as_slice() } + /// Returns the result set as a slice which can be iterated without taking ownership pub fn result_set(&self) -> &[HashMap] { self.result_set.as_slice() } - - pub fn take(self) -> (Vec, Vec, Vec>) { - (self.stats, self.header, self.result_set) - } } fn query_parse_header(header: FalkorValue) -> Result> { diff --git a/src/value/slowlog_entry.rs b/src/response/slowlog_entry.rs similarity index 75% rename from src/value/slowlog_entry.rs rename to src/response/slowlog_entry.rs index 6ea716f..76f2596 100644 --- a/src/value/slowlog_entry.rs +++ b/src/response/slowlog_entry.rs @@ -6,11 +6,16 @@ use crate::{FalkorDBError, FalkorValue}; use anyhow::Result; +/// A slowlog entry, representing one of the N slowest queries in the current log #[derive(Clone, Debug)] pub struct SlowlogEntry { + /// At which time was this query received pub timestamp: i64, + /// Which command was used to perform this query pub command: String, + /// The query itself pub arguments: String, + /// How long did performing this query take. pub time_taken: i64, } diff --git a/src/value/config.rs b/src/value/config.rs index aefa383..8ea67dc 100644 --- a/src/value/config.rs +++ b/src/value/config.rs @@ -5,6 +5,7 @@ use std::fmt::{Display, Formatter}; +/// An enum representing the two viable types for a config value #[derive(Clone, Debug, PartialEq)] pub enum ConfigValue { String(String), diff --git a/src/value/constraint.rs b/src/value/constraint.rs index 0f0af06..db7c119 100644 --- a/src/value/constraint.rs +++ b/src/value/constraint.rs @@ -3,13 +3,26 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use super::FalkorValue; +use crate::{ + connection::blocking::BorrowedSyncConnection, FalkorParsable, FalkorValue, SyncGraphSchema, +}; use std::collections::HashMap; +/// TODO: I...honestly don't know what this it pub struct Constraint { _type: String, - pub label: String, + label: String, properties: HashMap, entity_type: FalkorValue, status: String, } + +impl FalkorParsable for Constraint { + fn from_falkor_value( + _value: FalkorValue, + _graph_schema: &SyncGraphSchema, + _conn: &mut BorrowedSyncConnection, + ) -> anyhow::Result { + todo!() + } +} diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index 31c2d4f..7fee2bc 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -14,10 +14,14 @@ use std::collections::{HashMap, HashSet}; #[cfg(feature = "tokio")] use crate::value::{map::parse_map_with_schema_async, utils_async::parse_labels_async}; +/// A node in the graph #[derive(Clone, Debug)] pub struct Node { + /// The internal entity ID pub entity_id: i64, + /// A [`Vec`] of the labels this node answers to pub labels: Vec, + /// A [`HashMap`] of the properties in key-val form pub properties: HashMap, } @@ -96,10 +100,15 @@ impl crate::FalkorAsyncParseable for Node { #[derive(Clone, Debug)] pub struct Edge { + /// The internal entity ID pub entity_id: i64, + /// What type is this relationship pub relationship_type: String, + /// The entity ID of the origin node pub src_node_id: i64, + /// The entity ID of the destination node pub dst_node_id: i64, + /// A [`HashMap`] of the properties in key-val form pub properties: HashMap, } diff --git a/src/value/mod.rs b/src/value/mod.rs index f9a66bc..3e997fc 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -14,18 +14,16 @@ use std::{collections::HashMap, fmt::Debug}; pub(crate) mod config; pub(crate) mod constraint; -pub(crate) mod execution_plan; pub(crate) mod graph_entities; pub(crate) mod map; pub(crate) mod path; pub(crate) mod point; -pub(crate) mod query_result; -pub(crate) mod slowlog_entry; pub(crate) mod utils; #[cfg(feature = "tokio")] pub(crate) mod utils_async; +/// An enum of all the supported Falkor types #[derive(Clone, Debug)] pub enum FalkorValue { FNode(Node), @@ -106,28 +104,105 @@ impl crate::FalkorAsyncParseable for FalkorValue { } } +// By-reference conversions impl FalkorValue { + /// Returns a reference to the internal [`Vec`] if this is an FArray variant. + /// + /// # Returns + /// A reference to the internal [`Vec`] pub fn as_vec(&self) -> Option<&Vec> { match self { FalkorValue::FArray(val) => Some(val), _ => None, } } - pub fn into_vec(self) -> Result> { + + /// Returns a reference to the internal [`String`] if this is an FString variant. + /// + /// # Returns + /// A reference to the internal [`String`] + pub fn as_string(&self) -> Option<&String> { match self { - FalkorValue::FArray(val) => Some(val), + FalkorValue::FString(val) => Some(val), _ => None, } - .ok_or_else(|| FalkorDBError::ParsingFArray.into()) } - pub fn as_string(&self) -> Option<&String> { + /// Returns a reference to the internal [`Edge`] if this is an FEdge variant. + /// + /// # Returns + /// A reference to the internal [`Edge`] + pub fn as_edge(&self) -> Option<&Edge> { match self { - FalkorValue::FString(val) => Some(val), + FalkorValue::FEdge(val) => Some(val), + _ => None, + } + } + + /// Returns a reference to the internal [`Node`] if this is an FNode variant. + /// + /// # Returns + /// A reference to the internal [`Node`] + pub fn as_node(&self) -> Option<&Node> { + match self { + FalkorValue::FNode(val) => Some(val), + _ => None, + } + } + + /// Returns a reference to the internal [`Path`] if this is an FPath variant. + /// + /// # Returns + /// A reference to the internal [`Path`] + pub fn as_path(&self) -> Option<&Path> { + match self { + FalkorValue::FPath(val) => Some(val), + _ => None, + } + } + + /// Returns a reference to the internal [`HashMap`] if this is an FMap variant. + /// + /// # Returns + /// A reference to the internal [`HashMap`] + pub fn as_map(&self) -> Option<&HashMap> { + match self { + FalkorValue::FMap(val) => Some(val), + _ => None, + } + } + + /// Returns a reference to the internal [`Point`] if this is an FPoint variant. + /// + /// # Returns + /// A reference to the internal [`Point`] + pub fn as_point(&self) -> Option<&Point> { + match self { + FalkorValue::FPoint(val) => Some(val), + _ => None, + } + } +} + +// Consuming conversions +// TODO: convert these to TryFrom/TryInto traits? +impl FalkorValue { + /// Consumes this variant and returns the underlying [`Vec`] if this is an FArray variant + /// + /// # Returns + /// The inner [`Vec`] + pub fn into_vec(self) -> Result> { + match self { + FalkorValue::FArray(val) => Some(val), _ => None, } + .ok_or_else(|| FalkorDBError::ParsingFArray.into()) } + /// Consumes this variant and returns the underlying [`String`] if this is an FString variant + /// + /// # Returns + /// The inner [`String`] pub fn into_string(self) -> Result { match self { FalkorValue::FString(val) => Some(val), @@ -136,6 +211,10 @@ impl FalkorValue { .ok_or(FalkorDBError::ParsingFString.into()) } + /// Consumes this variant and returns the underlying [`Edge`] if this is an FEdge variant + /// + /// # Returns + /// The inner [`Edge`] pub fn into_edge(self) -> Result { match self { Self::FEdge(edge) => Ok(edge), @@ -143,6 +222,10 @@ impl FalkorValue { } } + /// Consumes this variant and returns the underlying [`Node`] if this is an FNode variant + /// + /// # Returns + /// The inner [`Node`] pub fn into_node(self) -> Result { match self { Self::FNode(node) => Ok(node), @@ -150,6 +233,10 @@ impl FalkorValue { } } + /// Consumes this variant and returns the underlying [`Path`] if this is an FPath variant + /// + /// # Returns + /// The inner [`Path`] pub fn into_path(self) -> Result { match self { Self::FPath(path) => Ok(path), @@ -157,6 +244,10 @@ impl FalkorValue { } } + /// Consumes this variant and returns the underlying [`HashMap`] if this is an FMap variant + /// + /// # Returns + /// The inner [`HashMap`] pub fn into_map(self) -> Result> { match self { FalkorValue::FMap(map) => Ok(map), @@ -164,13 +255,24 @@ impl FalkorValue { } } + /// Consumes this variant and returns the underlying [`Point`] if this is an FPoint variant + /// + /// # Returns + /// The inner [`Point`] pub fn into_point(self) -> Result { match self { Self::FPoint(point) => Ok(point), _ => Err(FalkorDBError::ParsingFPoint)?, } } +} +// For types implementing Copy +impl FalkorValue { + /// Returns a Copy of the inner [`i64`] if this is an Int64 variant + /// + /// # Returns + /// A copy of the inner [`i64`] pub fn to_i64(&self) -> Option { match self { FalkorValue::Int64(val) => Some(*val), @@ -178,6 +280,10 @@ impl FalkorValue { } } + /// Returns a Copy of the inner [`bool`] if this is an FBool variant + /// + /// # Returns + /// A copy of the inner [`bool`] pub fn to_bool(&self) -> Option { match self { FalkorValue::FBool(val) => Some(*val), @@ -185,6 +291,10 @@ impl FalkorValue { } } + /// Returns a Copy of the inner [`F64`] if this is an F64 variant + /// + /// # Returns + /// A copy of the inner [`f64`] pub fn to_f64(&self) -> Option { match self { FalkorValue::F64(val) => Some(*val), diff --git a/src/value/path.rs b/src/value/path.rs index db65511..a891fdc 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -9,6 +9,7 @@ use crate::{ }; use anyhow::Result; +/// TODO: not exactly sure what this represents #[derive(Clone, Debug)] pub struct Path { pub nodes: Vec, diff --git a/src/value/point.rs b/src/value/point.rs index 2d88f97..8ac1a1b 100644 --- a/src/value/point.rs +++ b/src/value/point.rs @@ -3,12 +3,14 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::error::FalkorDBError; -use crate::value::FalkorValue; +use crate::{FalkorDBError, FalkorValue}; +/// A point in the world. #[derive(Clone, Debug)] pub struct Point { + /// The latitude coordinate pub latitude: f64, + /// The longitude coordinate pub longitude: f64, } From a472f0b7b944e707920b5120666b3d6904536f40 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Thu, 23 May 2024 11:44:18 +0300 Subject: [PATCH 10/62] Docs, fixes, github actions, clippy --- .github/dependabot.yml | 15 + .github/workflows/build.yml | 19 + .github/workflows/codecov.yml | 25 ++ .github/workflows/doc.yml | 16 + .github/workflows/pr-checks.yml | 49 +++ Cargo.toml | 3 +- LICENSE | 655 +++++++++++++++++++++++++++++++ clippy.toml | 5 + deny.toml | 24 ++ rustfmt.toml | 1 + src/client/asynchronous.rs | 10 +- src/client/blocking.rs | 59 ++- src/client/builder.rs | 74 +++- src/connection_info/mod.rs | 18 + src/error/mod.rs | 10 +- src/graph/asynchronous.rs | 39 +- src/graph/blocking.rs | 122 +++++- src/graph/utils.rs | 4 +- src/graph_schema/asynchronous.rs | 2 +- src/graph_schema/blocking.rs | 2 +- src/lib.rs | 8 +- src/redis_ext.rs | 6 +- src/response/constraint.rs | 150 +++++++ src/response/execution_plan.rs | 4 +- src/response/mod.rs | 5 +- src/response/query_result.rs | 4 +- src/value/config.rs | 5 +- src/value/constraint.rs | 28 -- src/value/graph_entities.rs | 63 ++- src/value/map.rs | 4 +- src/value/mod.rs | 34 +- src/value/path.rs | 4 +- src/value/point.rs | 2 +- src/value/utils.rs | 2 +- 34 files changed, 1350 insertions(+), 121 deletions(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/codecov.yml create mode 100644 .github/workflows/doc.yml create mode 100644 .github/workflows/pr-checks.yml create mode 100644 LICENSE create mode 100644 clippy.toml create mode 100644 deny.toml create mode 100644 rustfmt.toml create mode 100644 src/response/constraint.rs delete mode 100644 src/value/constraint.rs diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..e5b6219 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,15 @@ +version: 2 +updates: + # This is mostly for actions/checkout + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + target-branch: "main" + + # This is actually for cargo crates + - package-ecosystem: cargo + directory: "/" + schedule: + interval: "monthly" + target-branch: "main" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..a78136a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,19 @@ +name: Linux Build + Tests + +on: + push: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build + - uses: taiki-e/install-action@nextest + - name: Test + run: cargo nextest run --all \ No newline at end of file diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml new file mode 100644 index 0000000..0189d56 --- /dev/null +++ b/.github/workflows/codecov.yml @@ -0,0 +1,25 @@ +name: Code Coverage + +on: + push: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + coverage: + runs-on: linux-latest + steps: + - uses: actions/checkout@v4 + - name: Install cargo-llvm-cov + run: cargo install cargo-llvm-cov + - uses: taiki-e/install-action@nextest + - name: Generate Code Coverage + run: cargo llvm-cov nextest --all --codecov --output-path codecov.json + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v4 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: codecov.json + fail_ci_if_error: true \ No newline at end of file diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml new file mode 100644 index 0000000..f3c21d1 --- /dev/null +++ b/.github/workflows/doc.yml @@ -0,0 +1,16 @@ +name: Documentation + +on: + push: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + doc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Doc + run: cargo doc --all \ No newline at end of file diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml new file mode 100644 index 0000000..6930161 --- /dev/null +++ b/.github/workflows/pr-checks.yml @@ -0,0 +1,49 @@ +name: Mandatory Pull Request Checks + +on: + pull_request: + branches: [ "main" ] + +env: + CARGO_TERM_COLOR: always + +jobs: + check-clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check Clippy + run: cargo clippy --all + + check-deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Deny + run: cargo install cargo-deny + - name: Check Deny + run: cargo deny --log-level info check + + check-doc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Doc + run: cargo doc --all + + check-fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check Rustfmt + run: cargo fmt --all --check + + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build + - uses: taiki-e/install-action@nextest + - name: Test + run: cargo nextest run --all \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index dcf0da0..64a9d22 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,10 +13,9 @@ redis = { version = "0.25.3", default-features = false, optional = true } tokio = { version = "1.37.0", default-features = false, features = ["rt-multi-thread", "sync", "macros"], optional = true } thiserror = "1.0.60" url-parse = "1.0.8" -simple_logger = "5.0.0" [features] -default = ["redis", "tokio"] +default = ["redis"] native-tls = ["redis/tls-native-tls"] rustls = ["redis/tls-rustls"] tokio = ["dep:tokio", "redis/tokio-comp"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5851192 --- /dev/null +++ b/LICENSE @@ -0,0 +1,655 @@ + Server Side Public License + VERSION 1, OCTOBER 16, 2018 + + Copyright © 2018 MongoDB, Inc. + + Everyone is permitted to copy and distribute verbatim copies of this + license document, but changing it is not allowed. + + TERMS AND CONDITIONS + + 0. Definitions. + + “This License” refers to Server Side Public License. + + “Copyright” also means copyright-like laws that apply to other kinds of + works, such as semiconductor masks. + + “The Program” refers to any copyrightable work licensed under this + License. Each licensee is addressed as “you”. “Licensees” and + “recipients” may be individuals or organizations. + + To “modify” a work means to copy from or adapt all or part of the work in + a fashion requiring copyright permission, other than the making of an + exact copy. The resulting work is called a “modified version” of the + earlier work or a work “based on” the earlier work. + + A “covered work” means either the unmodified Program or a work based on + the Program. + + To “propagate” a work means to do anything with it that, without + permission, would make you directly or secondarily liable for + infringement under applicable copyright law, except executing it on a + computer or modifying a private copy. Propagation includes copying, + distribution (with or without modification), making available to the + public, and in some countries other activities as well. + + To “convey” a work means any kind of propagation that enables other + parties to make or receive copies. Mere interaction with a user through a + computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays “Appropriate Legal Notices” to the + extent that it includes a convenient and prominently visible feature that + (1) displays an appropriate copyright notice, and (2) tells the user that + there is no warranty for the work (except to the extent that warranties + are provided), that licensees may convey the work under this License, and + how to view a copy of this License. If the interface presents a list of + user commands or options, such as a menu, a prominent item in the list + meets this criterion. + + 1. Source Code. + + The “source code” for a work means the preferred form of the work for + making modifications to it. “Object code” means any non-source form of a + work. + + A “Standard Interface” means an interface that either is an official + standard defined by a recognized standards body, or, in the case of + interfaces specified for a particular programming language, one that is + widely used among developers working in that language. The “System + Libraries” of an executable work include anything, other than the work as + a whole, that (a) is included in the normal form of packaging a Major + Component, but which is not part of that Major Component, and (b) serves + only to enable use of the work with that Major Component, or to implement + a Standard Interface for which an implementation is available to the + public in source code form. A “Major Component”, in this context, means a + major essential component (kernel, window system, and so on) of the + specific operating system (if any) on which the executable work runs, or + a compiler used to produce the work, or an object code interpreter used + to run it. + + The “Corresponding Source” for a work in object code form means all the + source code needed to generate, install, and (for an executable work) run + the object code and to modify the work, including scripts to control + those activities. However, it does not include the work's System + Libraries, or general-purpose tools or generally available free programs + which are used unmodified in performing those activities but which are + not part of the work. For example, Corresponding Source includes + interface definition files associated with source files for the work, and + the source code for shared libraries and dynamically linked subprograms + that the work is specifically designed to require, such as by intimate + data communication or control flow between those subprograms and other + parts of the work. + + The Corresponding Source need not include anything that users can + regenerate automatically from other parts of the Corresponding Source. + + The Corresponding Source for a work in source code form is that same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of + copyright on the Program, and are irrevocable provided the stated + conditions are met. This License explicitly affirms your unlimited + permission to run the unmodified Program, subject to section 13. The + output from running a covered work is covered by this License only if the + output, given its content, constitutes a covered work. This License + acknowledges your rights of fair use or other equivalent, as provided by + copyright law. Subject to section 13, you may make, run and propagate + covered works that you do not convey, without conditions so long as your + license otherwise remains in force. You may convey covered works to + others for the sole purpose of having them make modifications exclusively + for you, or provide you with facilities for running those works, provided + that you comply with the terms of this License in conveying all + material for which you do not control copyright. Those thus making or + running the covered works for you must do so exclusively on your + behalf, under your direction and control, on terms that prohibit them + from making any copies of your copyrighted material outside their + relationship with you. + + Conveying under any other circumstances is permitted solely under the + conditions stated below. Sublicensing is not allowed; section 10 makes it + unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological + measure under any applicable law fulfilling obligations under article 11 + of the WIPO copyright treaty adopted on 20 December 1996, or similar laws + prohibiting or restricting circumvention of such measures. + + When you convey a covered work, you waive any legal power to forbid + circumvention of technological measures to the extent such circumvention is + effected by exercising rights under this License with respect to the + covered work, and you disclaim any intention to limit operation or + modification of the work as a means of enforcing, against the work's users, + your or third parties' legal rights to forbid circumvention of + technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you + receive it, in any medium, provided that you conspicuously and + appropriately publish on each copy an appropriate copyright notice; keep + intact all notices stating that this License and any non-permissive terms + added in accord with section 7 apply to the code; keep intact all notices + of the absence of any warranty; and give all recipients a copy of this + License along with the Program. You may charge any price or no price for + each copy that you convey, and you may offer support or warranty + protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to + produce it from the Program, in the form of source code under the terms + of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified it, + and giving a relevant date. + + b) The work must carry prominent notices stating that it is released + under this License and any conditions added under section 7. This + requirement modifies the requirement in section 4 to “keep intact all + notices”. + + c) You must license the entire work, as a whole, under this License to + anyone who comes into possession of a copy. This License will therefore + apply, along with any applicable section 7 additional terms, to the + whole of the work, and all its parts, regardless of how they are + packaged. This License gives no permission to license the work in any + other way, but it does not invalidate such permission if you have + separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your work + need not make them do so. + + A compilation of a covered work with other separate and independent + works, which are not by their nature extensions of the covered work, and + which are not combined with it such as to form a larger program, in or on + a volume of a storage or distribution medium, is called an “aggregate” if + the compilation and its resulting copyright are not used to limit the + access or legal rights of the compilation's users beyond what the + individual works permit. Inclusion of a covered work in an aggregate does + not cause this License to apply to the other parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms of + sections 4 and 5, provided that you also convey the machine-readable + Corresponding Source under the terms of this License, in one of these + ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium customarily + used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a written + offer, valid for at least three years and valid for as long as you + offer spare parts or customer support for that product model, to give + anyone who possesses the object code either (1) a copy of the + Corresponding Source for all the software in the product that is + covered by this License, on a durable physical medium customarily used + for software interchange, for a price no more than your reasonable cost + of physically performing this conveying of source, or (2) access to + copy the Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This alternative is + allowed only occasionally and noncommercially, and only if you received + the object code with such an offer, in accord with subsection 6b. + + d) Convey the object code by offering access from a designated place + (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to copy + the object code is a network server, the Corresponding Source may be on + a different server (operated by you or a third party) that supports + equivalent copying facilities, provided you maintain clear directions + next to the object code saying where to find the Corresponding Source. + Regardless of what server hosts the Corresponding Source, you remain + obligated to ensure that it is available for as long as needed to + satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided you + inform other peers where the object code and Corresponding Source of + the work are being offered to the general public at no charge under + subsection 6d. + + A separable portion of the object code, whose source code is excluded + from the Corresponding Source as a System Library, need not be included + in conveying the object code work. + + A “User Product” is either (1) a “consumer product”, which means any + tangible personal property which is normally used for personal, family, + or household purposes, or (2) anything designed or sold for incorporation + into a dwelling. In determining whether a product is a consumer product, + doubtful cases shall be resolved in favor of coverage. For a particular + product received by a particular user, “normally used” refers to a + typical or common use of that class of product, regardless of the status + of the particular user or of the way in which the particular user + actually uses, or expects or is expected to use, the product. A product + is a consumer product regardless of whether the product has substantial + commercial, industrial or non-consumer uses, unless such uses represent + the only significant mode of use of the product. + + “Installation Information” for a User Product means any methods, + procedures, authorization keys, or other information required to install + and execute modified versions of a covered work in that User Product from + a modified version of its Corresponding Source. The information must + suffice to ensure that the continued functioning of the modified object + code is in no case prevented or interfered with solely because + modification has been made. + + If you convey an object code work under this section in, or with, or + specifically for use in, a User Product, and the conveying occurs as part + of a transaction in which the right of possession and use of the User + Product is transferred to the recipient in perpetuity or for a fixed term + (regardless of how the transaction is characterized), the Corresponding + Source conveyed under this section must be accompanied by the + Installation Information. But this requirement does not apply if neither + you nor any third party retains the ability to install modified object + code on the User Product (for example, the work has been installed in + ROM). + + The requirement to provide Installation Information does not include a + requirement to continue to provide support service, warranty, or updates + for a work that has been modified or installed by the recipient, or for + the User Product in which it has been modified or installed. Access + to a network may be denied when the modification itself materially + and adversely affects the operation of the network or violates the + rules and protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, in + accord with this section must be in a format that is publicly documented + (and with an implementation available to the public in source code form), + and must require no special password or key for unpacking, reading or + copying. + + 7. Additional Terms. + + “Additional permissions” are terms that supplement the terms of this + License by making exceptions from one or more of its conditions. + Additional permissions that are applicable to the entire Program shall be + treated as though they were included in this License, to the extent that + they are valid under applicable law. If additional permissions apply only + to part of the Program, that part may be used separately under those + permissions, but the entire Program remains governed by this License + without regard to the additional permissions. When you convey a copy of + a covered work, you may at your option remove any additional permissions + from that copy, or from any part of it. (Additional permissions may be + written to require their own removal in certain cases when you modify the + work.) You may place additional permissions on material, added by you to + a covered work, for which you have or can give appropriate copyright + permission. + + Notwithstanding any other provision of this License, for material you add + to a covered work, you may (if authorized by the copyright holders of + that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some trade + names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that material + by anyone who conveys the material (or modified versions of it) with + contractual assumptions of liability to the recipient, for any + liability that these contractual assumptions directly impose on those + licensors and authors. + + All other non-permissive additional terms are considered “further + restrictions” within the meaning of section 10. If the Program as you + received it, or any part of it, contains a notice stating that it is + governed by this License along with a term that is a further restriction, + you may remove that term. If a license document contains a further + restriction but permits relicensing or conveying under this License, you + may add to a covered work material governed by the terms of that license + document, provided that the further restriction does not survive such + relicensing or conveying. + + If you add terms to a covered work in accord with this section, you must + place, in the relevant source files, a statement of the additional terms + that apply to those files, or a notice indicating where to find the + applicable terms. Additional terms, permissive or non-permissive, may be + stated in the form of a separately written license, or stated as + exceptions; the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly + provided under this License. Any attempt otherwise to propagate or modify + it is void, and will automatically terminate your rights under this + License (including any patent licenses granted under the third paragraph + of section 11). + + However, if you cease all violation of this License, then your license + from a particular copyright holder is reinstated (a) provisionally, + unless and until the copyright holder explicitly and finally terminates + your license, and (b) permanently, if the copyright holder fails to + notify you of the violation by some reasonable means prior to 60 days + after the cessation. + + Moreover, your license from a particular copyright holder is reinstated + permanently if the copyright holder notifies you of the violation by some + reasonable means, this is the first time you have received notice of + violation of this License (for any work) from that copyright holder, and + you cure the violation prior to 30 days after your receipt of the notice. + + Termination of your rights under this section does not terminate the + licenses of parties who have received copies or rights from you under + this License. If your rights have been terminated and not permanently + reinstated, you do not qualify to receive new licenses for the same + material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or run a + copy of the Program. Ancillary propagation of a covered work occurring + solely as a consequence of using peer-to-peer transmission to receive a + copy likewise does not require acceptance. However, nothing other than + this License grants you permission to propagate or modify any covered + work. These actions infringe copyright if you do not accept this License. + Therefore, by modifying or propagating a covered work, you indicate your + acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically receives + a license from the original licensors, to run, modify and propagate that + work, subject to this License. You are not responsible for enforcing + compliance by third parties with this License. + + An “entity transaction” is a transaction transferring control of an + organization, or substantially all assets of one, or subdividing an + organization, or merging organizations. If propagation of a covered work + results from an entity transaction, each party to that transaction who + receives a copy of the work also receives whatever licenses to the work + the party's predecessor in interest had or could give under the previous + paragraph, plus a right to possession of the Corresponding Source of the + work from the predecessor in interest, if the predecessor has it or can + get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the rights + granted or affirmed under this License. For example, you may not impose a + license fee, royalty, or other charge for exercise of rights granted + under this License, and you may not initiate litigation (including a + cross-claim or counterclaim in a lawsuit) alleging that any patent claim + is infringed by making, using, selling, offering for sale, or importing + the Program or any portion of it. + + 11. Patents. + + A “contributor” is a copyright holder who authorizes use under this + License of the Program or a work on which the Program is based. The work + thus licensed is called the contributor's “contributor version”. + + A contributor's “essential patent claims” are all patent claims owned or + controlled by the contributor, whether already acquired or hereafter + acquired, that would be infringed by some manner, permitted by this + License, of making, using, or selling its contributor version, but do not + include claims that would be infringed only as a consequence of further + modification of the contributor version. For purposes of this definition, + “control” includes the right to grant patent sublicenses in a manner + consistent with the requirements of this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free + patent license under the contributor's essential patent claims, to make, + use, sell, offer for sale, import and otherwise run, modify and propagate + the contents of its contributor version. + + In the following three paragraphs, a “patent license” is any express + agreement or commitment, however denominated, not to enforce a patent + (such as an express permission to practice a patent or covenant not to + sue for patent infringement). To “grant” such a patent license to a party + means to make such an agreement or commitment not to enforce a patent + against the party. + + If you convey a covered work, knowingly relying on a patent license, and + the Corresponding Source of the work is not available for anyone to copy, + free of charge and under the terms of this License, through a publicly + available network server or other readily accessible means, then you must + either (1) cause the Corresponding Source to be so available, or (2) + arrange to deprive yourself of the benefit of the patent license for this + particular work, or (3) arrange, in a manner consistent with the + requirements of this License, to extend the patent license to downstream + recipients. “Knowingly relying” means you have actual knowledge that, but + for the patent license, your conveying the covered work in a country, or + your recipient's use of the covered work in a country, would infringe + one or more identifiable patents in that country that you have reason + to believe are valid. + + If, pursuant to or in connection with a single transaction or + arrangement, you convey, or propagate by procuring conveyance of, a + covered work, and grant a patent license to some of the parties receiving + the covered work authorizing them to use, propagate, modify or convey a + specific copy of the covered work, then the patent license you grant is + automatically extended to all recipients of the covered work and works + based on it. + + A patent license is “discriminatory” if it does not include within the + scope of its coverage, prohibits the exercise of, or is conditioned on + the non-exercise of one or more of the rights that are specifically + granted under this License. You may not convey a covered work if you are + a party to an arrangement with a third party that is in the business of + distributing software, under which you make payment to the third party + based on the extent of your activity of conveying the work, and under + which the third party grants, to any of the parties who would receive the + covered work from you, a discriminatory patent license (a) in connection + with copies of the covered work conveyed by you (or copies made from + those copies), or (b) primarily for and in connection with specific + products or compilations that contain the covered work, unless you + entered into that arrangement, or that patent license was granted, prior + to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting any + implied license or other defenses to infringement that may otherwise be + available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or + otherwise) that contradict the conditions of this License, they do not + excuse you from the conditions of this License. If you cannot use, + propagate or convey a covered work so as to satisfy simultaneously your + obligations under this License and any other pertinent obligations, then + as a consequence you may not use, propagate or convey it at all. For + example, if you agree to terms that obligate you to collect a royalty for + further conveying from those to whom you convey the Program, the only way + you could satisfy both those terms and this License would be to refrain + entirely from conveying the Program. + + 13. Offering the Program as a Service. + + If you make the functionality of the Program or a modified version + available to third parties as a service, you must make the Service Source + Code available via network download to everyone at no charge, under the + terms of this License. Making the functionality of the Program or + modified version available to third parties as a service includes, + without limitation, enabling third parties to interact with the + functionality of the Program or modified version remotely through a + computer network, offering a service the value of which entirely or + primarily derives from the value of the Program or modified version, or + offering a service that accomplishes for users the primary purpose of the + Program or modified version. + + “Service Source Code” means the Corresponding Source for the Program or + the modified version, and the Corresponding Source for all programs that + you use to make the Program or modified version available as a service, + including, without limitation, management software, user interfaces, + application program interfaces, automation software, monitoring software, + backup software, storage software and hosting software, all such that a + user could run an instance of the service using the Service Source Code + you make available. + + 14. Revised Versions of this License. + + MongoDB, Inc. may publish revised and/or new versions of the Server Side + Public License from time to time. Such new versions will be similar in + spirit to the present version, but may differ in detail to address new + problems or concerns. + + Each version is given a distinguishing version number. If the Program + specifies that a certain numbered version of the Server Side Public + License “or any later version” applies to it, you have the option of + following the terms and conditions either of that numbered version or of + any later version published by MongoDB, Inc. If the Program does not + specify a version number of the Server Side Public License, you may + choose any version ever published by MongoDB, Inc. + + If the Program specifies that a proxy can decide which future versions of + the Server Side Public License can be used, that proxy's public statement + of acceptance of a version permanently authorizes you to choose that + version for the Program. + + Later license versions may give you additional or different permissions. + However, no additional obligations are imposed on any author or copyright + holder as a result of your choosing to follow a later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY + APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT + HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY + OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, + THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR + PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM + IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF + ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING + WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS + THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING + ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF + THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO + LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU + OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER + PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE + POSSIBILITY OF SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided above + cannot be given local legal effect according to their terms, reviewing + courts shall apply local law that most closely approximates an absolute + waiver of all civil liability in connection with the Program, unless a + warranty or assumption of liability accompanies a copy of the Program in + return for a fee. + + END OF TERMS AND CONDITIONS + + + + + +Elastic License 2.0 + +URL: https://www.elastic.co/licensing/elastic-license + +## Acceptance + +By using the software, you agree to all of the terms and conditions below. + +## Copyright License + +The licensor grants you a non-exclusive, royalty-free, worldwide, +non-sublicensable, non-transferable license to use, copy, distribute, make +available, and prepare derivative works of the software, in each case subject to +the limitations and conditions below. + +## Limitations + +You may not provide the software to third parties as a hosted or managed +service, where the service provides users with access to any substantial set of +the features or functionality of the software. + +You may not move, change, disable, or circumvent the license key functionality +in the software, and you may not remove or obscure any functionality in the +software that is protected by the license key. + +You may not alter, remove, or obscure any licensing, copyright, or other notices +of the licensor in the software. Any use of the licensor’s trademarks is subject +to applicable law. + +## Patents + +The licensor grants you a license, under any patent claims the licensor can +license, or becomes able to license, to make, have made, use, sell, offer for +sale, import and have imported the software, in each case subject to the +limitations and conditions in this license. This license does not cover any +patent claims that you cause to be infringed by modifications or additions to +the software. If you or your company make any written claim that the software +infringes or contributes to infringement of any patent, your patent license for +the software granted under these terms ends immediately. If your company makes +such a claim, your patent license ends immediately for work on behalf of your +company. + +## Notices + +You must ensure that anyone who gets a copy of any part of the software from you +also gets a copy of these terms. + +If you modify the software, you must include in any modified copies of the +software prominent notices stating that you have modified the software. + +## No Other Rights + +These terms do not imply any licenses other than those expressly granted in +these terms. + +## Termination + +If you use the software in violation of these terms, such use is not licensed, +and your licenses will automatically terminate. If the licensor provides you +with a notice of your violation, and you cease all violation of this license no +later than 30 days after you receive that notice, your licenses will be +reinstated retroactively. However, if you violate these terms after such +reinstatement, any additional violation of these terms will cause your licenses +to terminate automatically and permanently. + +## No Liability + +*As far as the law allows, the software comes as is, without any warranty or +condition, and the licensor will not be liable to you for any damages arising +out of these terms or the use or nature of the software, under any kind of +legal claim.* + +## Definitions + +The **licensor** is the entity offering these terms, and the **software** is the +software the licensor makes available under these terms, including any portion +of it. + +**you** refers to the individual or entity agreeing to these terms. + +**your company** is any legal entity, sole proprietorship, or other kind of +organization that you work for, plus all organizations that have control over, +are under the control of, or are under common control with that +organization. **control** means ownership of substantially all the assets of an +entity, or the power to direct its management and policies by vote, contract, or +otherwise. Control can be direct or indirect. + +**your licenses** are all the licenses granted to you for the software under +these terms. + +**use** means anything you do with the software requiring one of your licenses. + +**trademark** means trademarks, service marks, and similar rights. diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 0000000..d46845e --- /dev/null +++ b/clippy.toml @@ -0,0 +1,5 @@ +# Environment +msrv = "1.72" + +# Documentation +missing-docs-in-crate-items = true \ No newline at end of file diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..4465bf8 --- /dev/null +++ b/deny.toml @@ -0,0 +1,24 @@ +[advisories] +vulnerability = "deny" +unmaintained = "deny" +notice = "deny" +unsound = "deny" + +[bans] +multiple-versions = "deny" +skip = [] + +[sources] +unknown-registry = "deny" +unknown-git = "deny" + +[licenses] +unlicensed = "deny" +allow-osi-fsf-free = "neither" +copyleft = "deny" +confidence-threshold = 0.93 +allow = [ + "Apache-2.0", + "MIT", + "BSD-3-Clause", +] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..99c6ded --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +fn_params_layout = "Vertical" \ No newline at end of file diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index b686fa4..9722ebe 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -140,7 +140,10 @@ impl FalkorAsyncClient { Ok(()) } - pub fn open_graph(&mut self, graph_name: T) -> AsyncGraph { + pub fn open_graph( + &mut self, + graph_name: T, + ) -> AsyncGraph { AsyncGraph { connection: self.connection.clone(), graph_name: graph_name.to_string(), @@ -152,7 +155,10 @@ impl FalkorAsyncClient { } } - pub async fn copy_graph(&mut self, cloned_graph_name: T) -> Result { + pub async fn copy_graph( + &mut self, + cloned_graph_name: T, + ) -> Result { self.connection .clone() .send_command(Some(cloned_graph_name.to_string()), "GRAPH.COPY", None) diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 235c8bb..b8c9559 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -6,7 +6,7 @@ use crate::{ client::FalkorClientProvider, connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}, - ConfigValue, FalkorDBError, SyncGraph, SyncGraphSchema, + ConfigValue, FalkorConnectionInfo, FalkorDBError, SyncGraph, SyncGraphSchema, }; use anyhow::Result; use parking_lot::Mutex; @@ -18,6 +18,7 @@ use std::{ pub(crate) struct FalkorSyncClientInner { _inner: Mutex, graph_cache: Mutex>, + connection_pool_size: u8, connection_pool_tx: mpsc::SyncSender, connection_pool_rx: mpsc::Receiver, } @@ -44,10 +45,15 @@ unsafe impl Send for FalkorSyncClientInner {} #[derive(Clone)] pub struct FalkorSyncClient { inner: Arc, + pub(crate) _connection_info: FalkorConnectionInfo, } impl FalkorSyncClient { - pub(crate) fn create(client: FalkorClientProvider, num_connections: u8) -> Result { + pub(crate) fn create( + client: FalkorClientProvider, + connection_info: FalkorConnectionInfo, + num_connections: u8, + ) -> Result { let (connection_pool_tx, connection_pool_rx) = mpsc::sync_channel(num_connections as usize); for _ in 0..num_connections { connection_pool_tx.send(client.get_connection(None)?)?; @@ -57,12 +63,19 @@ impl FalkorSyncClient { inner: Arc::new(FalkorSyncClientInner { _inner: client.into(), graph_cache: Default::default(), + connection_pool_size: num_connections, connection_pool_tx, connection_pool_rx, }), + _connection_info: connection_info, }) } + /// Get the max number of connections in the client's connection pool + pub fn connection_pool_size(&self) -> u8 { + self.inner.connection_pool_size + } + pub(crate) fn borrow_connection(&self) -> Result { self.inner.borrow_connection() } @@ -110,7 +123,10 @@ impl FalkorSyncClient { /// /// # Returns /// A [`HashMap`] comprised of [`String`] keys, and [`ConfigValue`] values. - pub fn config_get(&self, config_key: T) -> Result> { + pub fn config_get( + &self, + config_key: T, + ) -> Result> { let mut conn = self.borrow_connection()?; Ok(match conn.as_inner()? { @@ -198,7 +214,10 @@ impl FalkorSyncClient { /// /// # Returns /// a [`SyncGraph`] object, allowing various graph operations. - pub fn open_graph(&self, graph_name: T) -> SyncGraph { + pub fn open_graph( + &self, + graph_name: T, + ) -> SyncGraph { SyncGraph { client: self.inner.clone(), graph_name: graph_name.to_string(), @@ -233,3 +252,35 @@ impl FalkorSyncClient { Ok(self.open_graph(new_graph_name.to_string())) } } + +#[cfg(test)] +mod tests { + use crate::connection::blocking::BorrowedSyncConnection; + use crate::FalkorClientBuilder; + use std::sync::mpsc::TryRecvError; + + fn test_borrow_connection() { + let client = FalkorClientBuilder::new() + .with_num_connections(6) + .build() + .expect("Could not create client for this test"); + + // Client was created with 6 connections + let conn_vec: Vec> = (0..6) + .into_iter() + .map(|_| { + let conn = client.borrow_connection(); + assert!(conn.is_ok()); + conn + }) + .collect(); + + let non_existing_conn = client.inner.connection_pool_rx.try_recv(); + assert!(non_existing_conn.is_err()); + + if let Err(TryRecvError::Empty) = non_existing_conn { + return; + } + assert!(false); + } +} diff --git a/src/client/builder.rs b/src/client/builder.rs index 9a76587..688e923 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -21,7 +21,10 @@ impl FalkorClientBuilder { /// /// # Returns /// The consumed and modified self. - pub fn with_connection_info(self, falkor_connection_info: FalkorConnectionInfo) -> Self { + pub fn with_connection_info( + self, + falkor_connection_info: FalkorConnectionInfo, + ) -> Self { Self { connection_info: Some(falkor_connection_info), ..self @@ -35,7 +38,10 @@ impl FalkorClientBuilder { /// /// # Returns /// The consumed and modified self. - pub fn with_num_connections(self, num_connections: u8) -> Self { + pub fn with_num_connections( + self, + num_connections: u8, + ) -> Self { Self { num_connections, ..self @@ -49,6 +55,7 @@ where { let connection_info = connection_info.try_into()?; Ok(match connection_info { + #[cfg(feature = "redis")] FalkorConnectionInfo::Redis(connection_info) => { FalkorClientProvider::Redis(redis::Client::open(connection_info.clone())?) } @@ -60,6 +67,7 @@ impl FalkorClientBuilder<'S'> { /// /// # Returns /// The new [`FalkorClientBuilder`] + #[allow(clippy::new_without_default)] pub fn new() -> Self { FalkorClientBuilder { connection_info: None, @@ -80,7 +88,11 @@ impl FalkorClientBuilder<'S'> { .connection_info .unwrap_or("falkor://127.0.0.1:6379".try_into()?); - FalkorSyncClient::create(get_client(connection_info.clone())?, self.num_connections) + FalkorSyncClient::create( + get_client(connection_info.clone())?, + connection_info, + self.num_connections, + ) } } @@ -94,7 +106,7 @@ impl FalkorClientBuilder<'A'> { } pub async fn build( - self, + self ) -> Result>> { let connection_info = self .connection_info @@ -103,3 +115,57 @@ impl FalkorClientBuilder<'A'> { crate::FalkorAsyncClient::create(get_client(connection_info)?).await } } + +#[cfg(test)] +mod tests { + use crate::{FalkorClientBuilder, FalkorConnectionInfo}; + + #[test] + fn test_sync_builder() { + let conneciton_info = "redis://127.0.0.1:6379".try_into(); + assert!(conneciton_info.is_ok()); + + assert!(FalkorClientBuilder::new() + .with_num_connections(4) + .with_connection_info(conneciton_info.unwrap()) + .build() + .is_ok()); + } + + #[test] + #[cfg(feature = "redis")] + fn test_sync_builder_redis_fallback() { + let client = FalkorClientBuilder::new().build(); + assert!(client.is_ok()); + + if let FalkorConnectionInfo::Redis(redis_info) = client.unwrap().connection_info { + assert_eq!(redis_info.addr.to_string().as_str(), "127.0.0.1:6379"); + return; + } + + assert!(false); + } + + #[test] + fn test_connection_pool_size() { + let client = FalkorClientBuilder::new().with_num_connections(16).build(); + assert!(client.is_ok()); + + assert_eq!(client.unwrap().connection_pool_size(), 16); + } + + #[test] + fn test_invalid_connection_pool_size() { + // Connection pool size must be between 0 and 32 + + assert!(FalkorClientBuilder::new() + .with_num_connections(0) + .build() + .is_err()); + + assert!(FalkorClientBuilder::new() + .with_num_connections(36) + .build() + .is_err()); + } +} diff --git a/src/connection_info/mod.rs b/src/connection_info/mod.rs index 73c8d4b..79c356c 100644 --- a/src/connection_info/mod.rs +++ b/src/connection_info/mod.rs @@ -75,3 +75,21 @@ impl TryFrom<(T, u16)> for FalkorConnectionInfo { Self::try_from(format!("{}:{}", value.0.to_string(), value.1)) } } + +#[cfg(test)] +mod tests { + use crate::FalkorConnectionInfo; + + #[test] + #[cfg(feature = "redis")] + fn test_redis_fallback_provider() { + if let FalkorConnectionInfo::Redis(redis) = + FalkorConnectionInfo::fallback_provider("127.0.0.1:6379".to_string()).unwrap() + { + assert_eq!(redis.addr.to_string(), "127.0.0.1:6379".to_string()); + return; + } + + assert!(false); + } +} diff --git a/src/error/mod.rs b/src/error/mod.rs index aceba48..725da8a 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -51,8 +51,10 @@ pub enum FalkorDBError { ParsingTypeMarkerTypeMismatch, #[error("Both key id and type marker were not of type i64")] ParsingKTVTypes, - #[error("Could not form slowlog entry, element count invalid")] - ParsingSlowlogEntryElementCount, - #[error("Could not parse node, element count invalid")] - ParsingNodeElementCount, + #[error("Attempting to parse an FArray into a struct, but the array doesn't have the expected element count")] + ParsingArrayToStructElementCount, + #[error("Invalid constraint type, expected 'UNIQUE' or 'MANDATORY'")] + ConstraintType, + #[error("Invalid constraint status, expected 'OPERATIONAL', 'UNDER CONSTRUCTION' or 'FAILED'")] + ConstraintStatus, } diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index c200df8..30bfb29 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -8,6 +8,7 @@ use crate::{ AsyncGraphSchema, ExecutionPlan, FalkorAsyncConnection, FalkorAsyncParseable, FalkorDBError, FalkorValue, QueryResult, SlowlogEntry, }; +use anyhow::Result; use std::collections::HashMap; pub struct AsyncGraph { @@ -25,18 +26,18 @@ impl AsyncGraph { &self, command: &str, params: Option, - ) -> anyhow::Result { + ) -> Result { let mut conn = self.connection.clone(); conn.send_command(Some(self.graph_name.clone()), command, params) .await } - pub async fn delete(&self) -> anyhow::Result<()> { + pub async fn delete(&self) -> Result<()> { self.send_command("GRAPH.DELETE", None).await?; Ok(()) } - pub async fn slowlog(&self) -> anyhow::Result> { + pub async fn slowlog(&self) -> Result> { let res = self.send_command("GRAPH.SLOWLOG", None).await?.into_vec()?; if res.is_empty() { @@ -49,14 +50,14 @@ impl AsyncGraph { entry_raw .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingSlowlogEntryElementCount)?, + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?, )?); } Ok(slowlog_entries) } - pub async fn slowlog_reset(&self) -> anyhow::Result<()> { + pub async fn slowlog_reset(&self) -> Result<()> { self.send_command("GRAPH.SLOWLOG", Some("RESET".to_string())) .await?; Ok(()) @@ -66,13 +67,17 @@ impl AsyncGraph { &self, query_string: Q, params: Option<&HashMap>, - ) -> anyhow::Result { + ) -> Result { let query = construct_query(query_string, params); ExecutionPlan::try_from(self.send_command("GRAPH.PROFILE", Some(query)).await?) + .map_err(Into::into) } - pub async fn profile(&self, query_string: Q) -> anyhow::Result { + pub async fn profile( + &self, + query_string: Q, + ) -> Result { self.profile_with_params::(query_string, None) .await } @@ -81,12 +86,16 @@ impl AsyncGraph { &self, query_string: Q, params: Option<&HashMap>, - ) -> anyhow::Result { + ) -> Result { let query = construct_query(query_string, params); ExecutionPlan::try_from(self.send_command("GRAPH.EXPLAIN", Some(query)).await?) + .map_err(Into::into) } - pub async fn explain(&self, query_string: Q) -> anyhow::Result { + pub async fn explain( + &self, + query_string: Q, + ) -> Result { self.explain_with_params::(query_string, None) .await } @@ -102,7 +111,7 @@ impl AsyncGraph { params: Option<&HashMap>, readonly: bool, timeout: Option, - ) -> anyhow::Result

{ + ) -> Result

{ self.query_with_parser( if readonly { "GRAPH.RO_QUERY" @@ -120,7 +129,7 @@ impl AsyncGraph { &self, query_string: Q, timeout: Option, - ) -> anyhow::Result { + ) -> Result { self.query_with_params::(query_string, None, false, timeout) .await } @@ -129,7 +138,7 @@ impl AsyncGraph { &self, query_string: Q, timeout: Option, - ) -> anyhow::Result { + ) -> Result { self.query_with_params::(query_string, None, true, timeout) .await } @@ -141,7 +150,7 @@ impl AsyncGraph { yields: Option<&[String]>, read_only: Option, timeout: Option, - ) -> anyhow::Result

{ + ) -> Result

{ let (query_string, params) = generate_procedure_call(procedure, args, yields); self.query_with_params( @@ -153,7 +162,7 @@ impl AsyncGraph { .await } - pub async fn list_indices(&self) -> anyhow::Result { + pub async fn list_indices(&self) -> Result { let query_res = self .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, None, None) .await? @@ -165,7 +174,7 @@ impl AsyncGraph { Ok(FalkorValue::None) } - pub async fn list_constraints(&self) -> anyhow::Result { + pub async fn list_constraints(&self) -> Result { let query_res = self .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, None, None) .await? diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 8bf0ea9..8ece559 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -6,12 +6,12 @@ use super::utils::{construct_query, generate_procedure_call}; use crate::{ client::blocking::FalkorSyncClientInner, connection::blocking::FalkorSyncConnection, - Constraint, ExecutionPlan, FalkorDBError, FalkorParsable, FalkorValue, QueryResult, - SlowlogEntry, SyncGraphSchema, + Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorDBError, FalkorParsable, + FalkorValue, QueryResult, SlowlogEntry, SyncGraphSchema, }; use anyhow::Result; use redis::ConnectionLike; -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, fmt::Debug, sync::Arc}; /// The main graph API, this allows the user to perform graph operations while exposing as little details as possible. /// @@ -36,7 +36,11 @@ impl SyncGraph { self.graph_name.as_str() } - fn send_command(&self, command: &str, params: Option) -> Result { + fn send_command( + &self, + command: &str, + params: Option, + ) -> Result { let mut conn = self.client.borrow_connection()?; conn.send_command(Some(self.graph_name.clone()), command, params) } @@ -66,7 +70,7 @@ impl SyncGraph { entry_raw .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingSlowlogEntryElementCount)?, + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?, )?); } @@ -92,11 +96,12 @@ impl SyncGraph { pub fn profile_with_params( &self, query_string: Q, - params: Option>, + params: Option<&HashMap>, ) -> Result { let query = construct_query(query_string, params); ExecutionPlan::try_from(self.send_command("GRAPH.PROFILE", Some(query))?) + .map_err(Into::into) } /// Returns an [`ExecutionPlan`] object for the selected query, @@ -107,7 +112,10 @@ impl SyncGraph { /// /// # Returns /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. - pub fn profile(&self, query_string: Q) -> Result { + pub fn profile( + &self, + query_string: Q, + ) -> Result { self.profile_with_params::(query_string, None) } @@ -124,10 +132,11 @@ impl SyncGraph { pub fn explain_with_params( &self, query_string: Q, - params: Option>, + params: Option<&HashMap>, ) -> Result { let query = construct_query(query_string, params); ExecutionPlan::try_from(self.send_command("GRAPH.EXPLAIN", Some(query))?) + .map_err(Into::into) } /// Returns an [`ExecutionPlan`] object for the selected query, @@ -138,7 +147,10 @@ impl SyncGraph { /// /// # Returns /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. - pub fn explain(&self, query_string: Q) -> Result { + pub fn explain( + &self, + query_string: Q, + ) -> Result { self.explain_with_params::(query_string, None) } @@ -146,7 +158,7 @@ impl SyncGraph { &self, command: &str, query_string: Q, - params: Option>, + params: Option<&HashMap>, timeout: Option, ) -> Result

{ let query = construct_query(query_string, params); @@ -178,7 +190,11 @@ impl SyncGraph { /// /// # Returns /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query - pub fn query(&self, query_string: Q, timeout: Option) -> Result { + pub fn query( + &self, + query_string: Q, + timeout: Option, + ) -> Result { self.query_inner::("GRAPH.QUERY", query_string, None, timeout) } @@ -196,7 +212,7 @@ impl SyncGraph { &self, query_string: Q, timeout: Option, - params: HashMap, + params: &HashMap, ) -> Result { self.query_inner("GRAPH.QUERY", query_string, Some(params), timeout) } @@ -238,7 +254,7 @@ impl SyncGraph { &self, query_string: Q, timeout: Option, - params: HashMap, + params: &HashMap, ) -> Result { self.query_inner("GRAPH.QUERY_RO", query_string, Some(params), timeout) } @@ -267,11 +283,13 @@ impl SyncGraph { let (query_string, params) = generate_procedure_call(procedure, args, yields); self.query_inner( - read_only - .then_some("GRAPH.QUERY_RO") - .unwrap_or("GRAPH.QUERY"), + if read_only { + "GRAPH.QUERY_RO" + } else { + "GRAPH.QUERY" + }, query_string, - params, + params.as_ref(), timeout, ) } @@ -310,4 +328,74 @@ impl SyncGraph { Ok(constraints_vec) } + + /// Creates a new constraint for this graph + /// + /// # Arguments + /// * `constraint_type`: Which constraint to apply. + /// * `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. + pub fn create_constraint( + &self, + constraint_type: ConstraintType, + entity_type: EntityType, + label: L, + properties: &[P], + ) -> Result { + self.send_command( + "GRAPH.CONSTRAINT", + Some(format!( + "CREATE {constraint_type} {entity_type} {} {} {:?}", + label.to_string(), + properties.len(), + properties + )), + ) + } + + /// Drop an existing constraint from the graph + /// + /// # Arguments + /// * `constraint_type`: Which type of constraint to remove. + /// * `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. + pub fn drop_constraint( + &self, + constraint_type: ConstraintType, + entity_type: EntityType, + label: L, + properties: &[P], + ) -> Result { + self.send_command( + "GRAPH.CONSTRAINT", + Some(format!( + "DROP {constraint_type} {entity_type} {} {} {:?}", + label.to_string(), + properties.len(), + properties + )), + ) + } +} + +#[cfg(test)] +mod tests { + use crate::{ConstraintType, EntityType, FalkorClientBuilder}; + + #[test] + fn test_create_constraint() { + let client = FalkorClientBuilder::new() + .with_num_connections(4) + .build() + .expect("Could not create client"); + + let graph = client.open_graph("imdb"); + let res = graph + .create_constraint(ConstraintType::Unique, EntityType::Edge, "act", &["hello"]) + .expect("Could not create constraint"); + + panic!("{res:?}"); + } } diff --git a/src/graph/utils.rs b/src/graph/utils.rs index 9d72ca6..c255353 100644 --- a/src/graph/utils.rs +++ b/src/graph/utils.rs @@ -47,12 +47,12 @@ pub(crate) fn generate_procedure_call( pub(crate) fn construct_query( query_str: Q, - params: Option>, + params: Option<&HashMap>, ) -> String { params .map(|params| { params - .into_iter() + .iter() .fold("CYPHER ".to_string(), |acc, (key, val)| { acc + format!("{}={}", key.to_string(), val.to_string()).as_str() }) diff --git a/src/graph_schema/asynchronous.rs b/src/graph_schema/asynchronous.rs index 77f4c56..8dc45f1 100644 --- a/src/graph_schema/asynchronous.rs +++ b/src/graph_schema/asynchronous.rs @@ -99,7 +99,7 @@ impl AsyncGraphSchema { .await? .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; update_map(write_lock.deref_mut(), keys, id_hashset) } diff --git a/src/graph_schema/blocking.rs b/src/graph_schema/blocking.rs index d06d8d5..5797b69 100644 --- a/src/graph_schema/blocking.rs +++ b/src/graph_schema/blocking.rs @@ -107,7 +107,7 @@ impl SyncGraphSchema { )? .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; update_map(write_lock.deref_mut(), keys, id_hashset) } diff --git a/src/lib.rs b/src/lib.rs index ed75929..7ba1e93 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,13 +26,15 @@ pub use graph::blocking::SyncGraph; pub use graph_schema::{blocking::SyncGraphSchema, SchemaType}; pub use parser::FalkorParsable; pub use response::{ - execution_plan::ExecutionPlan, query_result::QueryResult, slowlog_entry::SlowlogEntry, + constraint::{Constraint, ConstraintStatus, ConstraintType}, + execution_plan::ExecutionPlan, + query_result::QueryResult, + slowlog_entry::SlowlogEntry, ResponseVariant, }; pub use value::{ config::ConfigValue, - constraint::Constraint, - graph_entities::{Edge, Node}, + graph_entities::{Edge, EntityType, Node}, path::Path, point::Point, FalkorValue, diff --git a/src/redis_ext.rs b/src/redis_ext.rs index ce15e9a..0938604 100644 --- a/src/redis_ext.rs +++ b/src/redis_ext.rs @@ -36,8 +36,10 @@ impl From for FalkorClientProvider { } impl ToRedisArgs for ConfigValue { - fn write_redis_args(&self, out: &mut W) - where + fn write_redis_args( + &self, + out: &mut W, + ) where W: ?Sized + RedisWrite, { match self { diff --git a/src/response/constraint.rs b/src/response/constraint.rs new file mode 100644 index 0000000..40ddf11 --- /dev/null +++ b/src/response/constraint.rs @@ -0,0 +1,150 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use crate::{ + connection::blocking::BorrowedSyncConnection, EntityType, FalkorDBError, FalkorParsable, + FalkorValue, SyncGraphSchema, +}; +use anyhow::Result; +use std::fmt::{Display, Formatter}; + +/// The type of restriction to apply for the property +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ConstraintType { + /// This property may only appear once on entities of this type and label. + Unique, + /// This property must be provided when creating a new entity of this type and label, + /// and must exist on all entities of this type and label. + Mandatory, +} + +impl Display for ConstraintType { + fn fmt( + &self, + f: &mut Formatter<'_>, + ) -> std::fmt::Result { + let str = match self { + ConstraintType::Unique => "UNIQUE", + ConstraintType::Mandatory => "MANDATORY", + }; + f.write_str(str) + } +} + +impl TryFrom<&str> for ConstraintType { + type Error = FalkorDBError; + + fn try_from(value: &str) -> Result { + Ok(match value.to_uppercase().as_str() { + "MANDATORY" => Self::Mandatory, + "UNIQUE" => Self::Unique, + _ => Err(FalkorDBError::ConstraintType)?, + }) + } +} + +impl TryFrom for ConstraintType { + type Error = FalkorDBError; + + fn try_from(value: String) -> Result { + value.as_str().try_into() + } +} + +impl TryFrom<&String> for ConstraintType { + type Error = FalkorDBError; + + fn try_from(value: &String) -> Result { + value.as_str().try_into() + } +} + +/// The status of this constraint +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ConstraintStatus { + /// This constraint is active on all entities of this type and label. + Active, + /// This constraint is still being applied and verified. + Pending, + /// This constraint could not be applied, not all entities of this type and label are compliant. + Failed, +} + +impl TryFrom<&str> for ConstraintStatus { + type Error = FalkorDBError; + + fn try_from(value: &str) -> Result { + Ok(match value.to_uppercase().as_str() { + "OPERATIONAL" => Self::Active, + "UNDER CONSTRUCTION" => Self::Pending, + "FAILED" => Self::Failed, + _ => Err(FalkorDBError::ConstraintStatus)?, + }) + } +} + +impl TryFrom for ConstraintStatus { + type Error = FalkorDBError; + + fn try_from(value: String) -> Result { + value.as_str().try_into() + } +} + +impl TryFrom<&String> for ConstraintStatus { + type Error = FalkorDBError; + + fn try_from(value: &String) -> Result { + value.as_str().try_into() + } +} + +/// A constraint applied on all 'properties' of the graph entity 'label' in this graph +#[derive(Clone, Debug)] +pub struct Constraint { + /// Is this constraint applies the 'unique' modifier or the 'mandatory' modifier + pub constraint_type: ConstraintType, + /// The name of this constraint + pub label: String, + /// The properties this constraint applies to + pub properties: Vec, + /// Whether this constraint applies to nodes or relationships + pub entity_type: EntityType, + /// Whether this constraint status is already active, still under construction, or failed construction + pub status: ConstraintStatus, +} + +impl FalkorParsable for Constraint { + fn from_falkor_value( + value: FalkorValue, + _graph_schema: &SyncGraphSchema, + _conn: &mut BorrowedSyncConnection, + ) -> Result { + let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; + 5] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + let constraint_type = constraint_type_raw.into_string()?.try_into()?; + let label = label_raw.into_string()?; + let entity_type = entity_type_raw.into_string()?.try_into()?; + let status = status_raw.into_string()?.try_into()?; + + let properties_vec = properties_raw.into_vec()?; + let mut properties = Vec::with_capacity(properties_vec.len()); + for property in properties_vec { + properties.push(property.into_string()?); + } + + Ok(Constraint { + constraint_type, + label, + properties, + entity_type, + status, + }) + } +} diff --git a/src/response/execution_plan.rs b/src/response/execution_plan.rs index 0a7cebb..c60991b 100644 --- a/src/response/execution_plan.rs +++ b/src/response/execution_plan.rs @@ -3,7 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::FalkorValue; +use crate::{FalkorDBError, FalkorValue}; /// An execution plan, storing both the specific string details for each step, and also a formatted plaintext string for pretty display #[derive(Debug, Clone)] @@ -25,7 +25,7 @@ impl ExecutionPlan { } impl TryFrom for ExecutionPlan { - type Error = anyhow::Error; + type Error = FalkorDBError; fn try_from(value: FalkorValue) -> Result { let string_vec = value.into_vec()?; diff --git a/src/response/mod.rs b/src/response/mod.rs index ca85f9d..5e9490a 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -3,19 +3,22 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use constraint::Constraint; use execution_plan::ExecutionPlan; use query_result::QueryResult; use slowlog_entry::SlowlogEntry; +pub(crate) mod constraint; pub(crate) mod execution_plan; pub(crate) mod query_result; pub(crate) mod slowlog_entry; /// A naive wrapper for the various possible responses from queries -/// Its main usecase is for creating things like [tokio::task::JoinSet]s, which require all the tasks to return the same type +/// Its main usecase is for creating things like [`JoinSet`](tokio::task::JoinSet)s, which require all the tasks to return the same type #[derive(Debug, Clone)] pub enum ResponseVariant { ExecutionPlan(ExecutionPlan), QueryResult(QueryResult), SlowlogEntry(SlowlogEntry), + Constraints(Vec), } diff --git a/src/response/query_result.rs b/src/response/query_result.rs index 7dc64a4..989b56a 100644 --- a/src/response/query_result.rs +++ b/src/response/query_result.rs @@ -127,7 +127,7 @@ impl FalkorParsable for QueryResult { // Full result let [header, data, stats]: [FalkorValue; 3] = value_vec .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let header_keys = query_parse_header(header)?; let stats_strings = query_parse_stats(stats)?; @@ -203,7 +203,7 @@ impl crate::FalkorAsyncParseable for QueryResult { // Full result let [header, data, stats]: [FalkorValue; 3] = value_vec .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let header_keys = query_parse_header(header)?; let stats_strings = query_parse_stats(stats)?; diff --git a/src/value/config.rs b/src/value/config.rs index 8ea67dc..12a6004 100644 --- a/src/value/config.rs +++ b/src/value/config.rs @@ -13,7 +13,10 @@ pub enum ConfigValue { } impl Display for ConfigValue { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt( + &self, + f: &mut Formatter<'_>, + ) -> std::fmt::Result { match self { ConfigValue::String(str_val) => str_val.fmt(f), ConfigValue::Int64(int_val) => int_val.fmt(f), diff --git a/src/value/constraint.rs b/src/value/constraint.rs deleted file mode 100644 index db7c119..0000000 --- a/src/value/constraint.rs +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{ - connection::blocking::BorrowedSyncConnection, FalkorParsable, FalkorValue, SyncGraphSchema, -}; -use std::collections::HashMap; - -/// TODO: I...honestly don't know what this it -pub struct Constraint { - _type: String, - label: String, - properties: HashMap, - entity_type: FalkorValue, - status: String, -} - -impl FalkorParsable for Constraint { - fn from_falkor_value( - _value: FalkorValue, - _graph_schema: &SyncGraphSchema, - _conn: &mut BorrowedSyncConnection, - ) -> anyhow::Result { - todo!() - } -} diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index 7fee2bc..bfaeb1c 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -9,12 +9,62 @@ use crate::{ FalkorParsable, FalkorValue, SchemaType, SyncGraphSchema, }; use anyhow::Result; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + fmt::{Display, Formatter}, +}; #[cfg(feature = "tokio")] use crate::value::{map::parse_map_with_schema_async, utils_async::parse_labels_async}; -/// A node in the graph +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum EntityType { + Node, + Edge, +} + +impl Display for EntityType { + fn fmt( + &self, + f: &mut Formatter<'_>, + ) -> std::fmt::Result { + let str = match self { + EntityType::Node => "NODE", + EntityType::Edge => "RELATIONSHIP", + }; + f.write_str(str) + } +} + +impl TryFrom<&str> for EntityType { + type Error = FalkorDBError; + + fn try_from(value: &str) -> Result { + Ok(match value.to_uppercase().as_str() { + "NODE" => Self::Node, + "RELATIONSHIP" => Self::Edge, + _ => Err(FalkorDBError::ConstraintType)?, + }) + } +} + +impl TryFrom for EntityType { + type Error = FalkorDBError; + + fn try_from(value: String) -> Result { + value.as_str().try_into() + } +} + +impl TryFrom<&String> for EntityType { + type Error = FalkorDBError; + + fn try_from(value: &String) -> Result { + value.as_str().try_into() + } +} + +/// A node in the graph, containing a unique id, various labels describing it, and its own property. #[derive(Clone, Debug)] pub struct Node { /// The internal entity ID @@ -34,7 +84,7 @@ impl FalkorParsable for Node { let [entity_id, labels, properties]: [FalkorValue; 3] = value .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingNodeElementCount)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let labels = labels.into_vec()?; let mut ids_hashset = HashSet::with_capacity(labels.len()); @@ -70,7 +120,7 @@ impl crate::FalkorAsyncParseable for Node { let [entity_id, labels, properties]: [FalkorValue; 3] = value .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingNodeElementCount)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let labels = labels.into_vec()?; let mut ids_hashset = HashSet::with_capacity(labels.len()); @@ -98,6 +148,7 @@ impl crate::FalkorAsyncParseable for Node { } } +/// An edge in the graph, representing a relationship between two [`Node`]s. #[derive(Clone, Debug)] pub struct Edge { /// The internal entity ID @@ -121,7 +172,7 @@ impl FalkorParsable for Edge { let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let relation = relations.to_i64().ok_or(FalkorDBError::ParsingI64)?; if let Some(relationship) = graph_schema.relationships().read().get(&relation).cloned() { @@ -174,7 +225,7 @@ impl crate::FalkorAsyncParseable for Edge { let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let relation = relations.to_i64().ok_or(FalkorDBError::ParsingI64)?; if let Some(relationship) = graph_schema diff --git a/src/value/map.rs b/src/value/map.rs index 18d86b8..d5e1d0e 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -22,13 +22,13 @@ pub(crate) struct FKeyTypeVal { } impl TryFrom for FKeyTypeVal { - type Error = anyhow::Error; + type Error = FalkorDBError; fn try_from(value: FalkorValue) -> Result { let [key_raw, type_raw, val]: [FalkorValue; 3] = value .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingKTVTypes)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let key = key_raw.to_i64(); let type_marker = type_raw.to_i64(); diff --git a/src/value/mod.rs b/src/value/mod.rs index 3e997fc..991cb47 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -13,7 +13,6 @@ use point::Point; use std::{collections::HashMap, fmt::Debug}; pub(crate) mod config; -pub(crate) mod constraint; pub(crate) mod graph_entities; pub(crate) mod map; pub(crate) mod path; @@ -191,34 +190,33 @@ impl FalkorValue { /// /// # Returns /// The inner [`Vec`] - pub fn into_vec(self) -> Result> { + pub fn into_vec(self) -> Result, FalkorDBError> { match self { FalkorValue::FArray(val) => Some(val), _ => None, } - .ok_or_else(|| FalkorDBError::ParsingFArray.into()) + .ok_or(FalkorDBError::ParsingFArray) } /// Consumes this variant and returns the underlying [`String`] if this is an FString variant /// /// # Returns /// The inner [`String`] - pub fn into_string(self) -> Result { + pub fn into_string(self) -> Result { match self { - FalkorValue::FString(val) => Some(val), - _ => None, + FalkorValue::FString(val) => Ok(val), + _ => Err(FalkorDBError::ParsingFString), } - .ok_or(FalkorDBError::ParsingFString.into()) } /// Consumes this variant and returns the underlying [`Edge`] if this is an FEdge variant /// /// # Returns /// The inner [`Edge`] - pub fn into_edge(self) -> Result { + pub fn into_edge(self) -> Result { match self { Self::FEdge(edge) => Ok(edge), - _ => Err(FalkorDBError::ParsingFEdge)?, + _ => Err(FalkorDBError::ParsingFEdge), } } @@ -226,10 +224,10 @@ impl FalkorValue { /// /// # Returns /// The inner [`Node`] - pub fn into_node(self) -> Result { + pub fn into_node(self) -> Result { match self { Self::FNode(node) => Ok(node), - _ => Err(FalkorDBError::ParsingFNode)?, + _ => Err(FalkorDBError::ParsingFNode), } } @@ -237,10 +235,10 @@ impl FalkorValue { /// /// # Returns /// The inner [`Path`] - pub fn into_path(self) -> Result { + pub fn into_path(self) -> Result { match self { Self::FPath(path) => Ok(path), - _ => Err(FalkorDBError::ParsingFPath)?, + _ => Err(FalkorDBError::ParsingFPath), } } @@ -248,10 +246,10 @@ impl FalkorValue { /// /// # Returns /// The inner [`HashMap`] - pub fn into_map(self) -> Result> { + pub fn into_map(self) -> Result, FalkorDBError> { match self { FalkorValue::FMap(map) => Ok(map), - _ => Err(FalkorDBError::ParsingFMap)?, + _ => Err(FalkorDBError::ParsingFMap), } } @@ -259,10 +257,10 @@ impl FalkorValue { /// /// # Returns /// The inner [`Point`] - pub fn into_point(self) -> Result { + pub fn into_point(self) -> Result { match self { Self::FPoint(point) => Ok(point), - _ => Err(FalkorDBError::ParsingFPoint)?, + _ => Err(FalkorDBError::ParsingFPoint), } } } @@ -291,7 +289,7 @@ impl FalkorValue { } } - /// Returns a Copy of the inner [`F64`] if this is an F64 variant + /// Returns a Copy of the inner [`f64`] if this is an F64 variant /// /// # Returns /// A copy of the inner [`f64`] diff --git a/src/value/path.rs b/src/value/path.rs index a891fdc..b1ea467 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -25,7 +25,7 @@ impl FalkorParsable for Path { let [nodes, relationships]: [FalkorValue; 2] = value .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let (nodes, relationships) = (nodes.into_vec()?, relationships.into_vec()?); let mut parsed_nodes = Vec::with_capacity(nodes.len()); @@ -63,7 +63,7 @@ impl crate::FalkorAsyncParseable for Path { let [nodes, relationships]: [FalkorValue; 2] = value .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let (nodes, relationships) = (nodes.into_vec()?, relationships.into_vec()?); let mut parsed_nodes = Vec::with_capacity(nodes.len()); diff --git a/src/value/point.rs b/src/value/point.rs index 8ac1a1b..9ae16d6 100644 --- a/src/value/point.rs +++ b/src/value/point.rs @@ -19,7 +19,7 @@ impl Point { let [lat, long]: [FalkorValue; 2] = value .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; Ok(Point { latitude: lat.to_f64().ok_or(FalkorDBError::ParsingError)?, diff --git a/src/value/utils.rs b/src/value/utils.rs index 18a3d1d..0b2970f 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -48,7 +48,7 @@ pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValu let [type_marker, val]: [FalkorValue; 2] = value .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingError)?; + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let type_marker = type_marker.to_i64().ok_or(FalkorDBError::ParsingI64)?; Ok((type_marker, val)) From d820f024291f2d5bd69ea6a8d536cd8683e8eaf1 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Thu, 23 May 2024 11:56:34 +0300 Subject: [PATCH 11/62] update deny and libc yanked crate --- Cargo.lock | 110 +---------------------------------------------------- deny.toml | 17 +++++---- 2 files changed, 11 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6f26428..7898f44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -105,16 +105,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "colored" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8" -dependencies = [ - "lazy_static", - "windows-sys 0.48.0", -] - [[package]] name = "combine" version = "4.6.7" @@ -145,15 +135,6 @@ version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" -[[package]] -name = "deranged" -version = "0.3.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" -dependencies = [ - "powerfmt", -] - [[package]] name = "equivalent" version = "1.0.1" @@ -179,7 +160,6 @@ dependencies = [ "log", "parking_lot", "redis", - "simple_logger", "thiserror", "tokio", "url-parse", @@ -315,9 +295,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.154" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "linux-raw-sys" @@ -385,12 +365,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - [[package]] name = "num_cpus" version = "1.16.0" @@ -401,15 +375,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_threads" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" -dependencies = [ - "libc", -] - [[package]] name = "object" version = "0.32.2" @@ -529,12 +494,6 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "proc-macro2" version = "1.0.82" @@ -749,38 +708,6 @@ dependencies = [ "libc", ] -[[package]] -name = "serde" -version = "1.0.202" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.202" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "simple_logger" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8c5dfa5e08767553704aa0ffd9d9794d527103c736aba9854773851fd7497eb" -dependencies = [ - "colored", - "log", - "time", - "windows-sys 0.48.0", -] - [[package]] name = "smallvec" version = "1.13.2" @@ -862,39 +789,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "time" -version = "0.3.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" -dependencies = [ - "deranged", - "itoa", - "libc", - "num-conv", - "num_threads", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" -dependencies = [ - "num-conv", - "time-core", -] - [[package]] name = "tinyvec" version = "1.6.0" diff --git a/deny.toml b/deny.toml index 4465bf8..008ef05 100644 --- a/deny.toml +++ b/deny.toml @@ -1,8 +1,4 @@ [advisories] -vulnerability = "deny" -unmaintained = "deny" -notice = "deny" -unsound = "deny" [bans] multiple-versions = "deny" @@ -13,12 +9,17 @@ unknown-registry = "deny" unknown-git = "deny" [licenses] -unlicensed = "deny" -allow-osi-fsf-free = "neither" -copyleft = "deny" +unused-allowed-license = "allow" confidence-threshold = 0.93 allow = [ + "SSPL-1.0", + "Apache-2.0 WITH LLVM-exception", "Apache-2.0", - "MIT", + "BSD-2-Clause", "BSD-3-Clause", + "MIT", + "Unicode-DFS-2016", + "GPL-3.0", + "LGPL-3.0", + "AGPL-3.0" ] From dfa1069626dcc972d486f81f6d0e0f722cb15652 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Thu, 23 May 2024 16:36:29 +0300 Subject: [PATCH 12/62] It workssss finally --- .github/workflows/codecov.yml | 3 +- .gitignore | 3 +- src/client/blocking.rs | 131 ++++++++++++++++++++-- src/client/builder.rs | 15 +-- src/connection/asynchronous.rs | 8 +- src/connection/blocking.rs | 17 ++- src/connection_info/mod.rs | 1 + src/graph/asynchronous.rs | 3 +- src/graph/blocking.rs | 197 +++++++++++++++++++++++---------- src/graph/utils.rs | 19 ++-- src/graph_schema/blocking.rs | 3 +- src/lib.rs | 33 ++++++ src/response/constraint.rs | 2 +- src/response/execution_plan.rs | 2 +- src/response/mod.rs | 2 +- src/response/query_result.rs | 2 +- src/response/slowlog_entry.rs | 2 +- src/value/config.rs | 9 ++ src/value/graph_entities.rs | 4 +- src/value/mod.rs | 2 +- src/value/path.rs | 2 +- src/value/point.rs | 2 +- 22 files changed, 360 insertions(+), 102 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 0189d56..5d1d6d3 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -12,8 +12,7 @@ jobs: runs-on: linux-latest steps: - uses: actions/checkout@v4 - - name: Install cargo-llvm-cov - run: cargo install cargo-llvm-cov + - uses: taiki-e/install-action@cargo-llvm-cov - uses: taiki-e/install-action@nextest - name: Generate Code Coverage run: cargo llvm-cov nextest --all --codecov --output-path codecov.json diff --git a/.gitignore b/.gitignore index eccec5a..304e18b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ /target .idea/ .vscode/ -.vs/ \ No newline at end of file +.vs/ +codecov.json \ No newline at end of file diff --git a/src/client/blocking.rs b/src/client/blocking.rs index b8c9559..3888f14 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -146,7 +146,11 @@ impl FalkorSyncClient { if bulk_data.is_empty() { return Err(FalkorDBError::InvalidDataReceived.into()); } else if bulk_data.len() == 2 { - return if let Some(redis::Value::Status(config_key)) = bulk_data.first() { + return if let Some(ConfigValue::String(config_key)) = bulk_data + .first() + .map(ConfigValue::try_from) + .and_then(Result::ok) + { Ok(HashMap::from([( config_key.to_string(), ConfigValue::try_from(&bulk_data[1])?, @@ -239,15 +243,16 @@ impl FalkorSyncClient { /// /// # Returns /// If successful, will return the new [`SyncGraph`] object. - pub fn copy_graph( + pub fn copy_graph( &self, graph_to_clone: T, - new_graph_name: T, + new_graph_name: Z, ) -> Result { self.borrow_connection()?.send_command( Some(graph_to_clone.to_string()), "GRAPH.COPY", None, + Some(&[new_graph_name.to_string()]), )?; Ok(self.open_graph(new_graph_name.to_string())) } @@ -255,10 +260,14 @@ impl FalkorSyncClient { #[cfg(test)] mod tests { - use crate::connection::blocking::BorrowedSyncConnection; - use crate::FalkorClientBuilder; - use std::sync::mpsc::TryRecvError; + use crate::test_utils::TestGraphHandle; + use crate::{ + connection::blocking::BorrowedSyncConnection, test_utils::create_test_client, ConfigValue, + FalkorClientBuilder, + }; + use std::{mem, sync::mpsc::TryRecvError, thread}; + #[test] fn test_borrow_connection() { let client = FalkorClientBuilder::new() .with_num_connections(6) @@ -266,7 +275,7 @@ mod tests { .expect("Could not create client for this test"); // Client was created with 6 connections - let conn_vec: Vec> = (0..6) + let _conn_vec: Vec> = (0..6) .into_iter() .map(|_| { let conn = client.borrow_connection(); @@ -283,4 +292,112 @@ mod tests { } assert!(false); } + + #[test] + fn test_open_graph_and_query() { + let client = create_test_client(); + + let graph = client.open_graph("imdb"); + assert_eq!(graph.graph_name(), "imdb".to_string()); + + let res = graph + .query("MATCH (a:actor) return a".to_string(), None) + .expect("Could not get actors from unmodified graph"); + + assert_eq!(res.result_set.len(), 1317); + } + + #[test] + fn test_copy_graph() { + let client = create_test_client(); + + client.open_graph("imdb_ro_copy").delete().ok(); + + let graph = client.copy_graph("imdb", "imdb_ro_copy"); + assert!(graph.is_ok()); + + let graph = TestGraphHandle { + inner: graph.unwrap(), + }; + + let original_graph = client.open_graph("imdb"); + + assert_eq!( + graph + .inner + .query("MATCH (a:actor) RETURN a".to_string(), None) + .expect("Could not get actors from unmodified graph") + .result_set, + original_graph + .query("MATCH (a:actor) RETURN a".to_string(), None) + .expect("Could not get actors from unmodified graph") + .result_set + ) + } + + #[test] + fn test_get_config() { + let client = create_test_client(); + + let config = client + .config_get("QUERY_MEM_CAPACITY") + .expect("Could not get configuration"); + + assert_eq!(config.len(), 1); + assert!(config.contains_key("QUERY_MEM_CAPACITY")); + assert_eq!( + mem::discriminant(config.get("QUERY_MEM_CAPACITY").unwrap()), + mem::discriminant(&ConfigValue::Int64(0)) + ); + } + + #[test] + fn test_get_config_all() { + let client = create_test_client(); + let configuration = client.config_get("*").expect("Could not get configuration"); + assert_eq!( + configuration.get("THREAD_COUNT").cloned().unwrap(), + ConfigValue::Int64(thread::available_parallelism().unwrap().get() as i64) + ); + } + + #[test] + fn test_set_config() { + let client = create_test_client(); + + let config = client + .config_get("DELTA_MAX_PENDING_CHANGES") + .expect("Could not get configuration"); + + let current_val = config + .get("DELTA_MAX_PENDING_CHANGES") + .cloned() + .unwrap() + .as_i64() + .unwrap(); + + let desired_val = if current_val == 10000 { 50000 } else { 10000 }; + + client + .config_set("DELTA_MAX_PENDING_CHANGES", desired_val) + .expect("Could not set config value"); + + let new_config = client + .config_get("DELTA_MAX_PENDING_CHANGES") + .expect("Could not get configuration"); + + assert_eq!( + new_config + .get("DELTA_MAX_PENDING_CHANGES") + .cloned() + .unwrap() + .as_i64() + .unwrap(), + desired_val + ); + + client + .config_set("DELTA_MAX_PENDING_CHANGES", current_val) + .ok(); + } } diff --git a/src/client/builder.rs b/src/client/builder.rs index 688e923..70f0d20 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -134,11 +134,12 @@ mod tests { #[test] #[cfg(feature = "redis")] + #[allow(irrefutable_let_patterns)] fn test_sync_builder_redis_fallback() { let client = FalkorClientBuilder::new().build(); assert!(client.is_ok()); - if let FalkorConnectionInfo::Redis(redis_info) = client.unwrap().connection_info { + if let FalkorConnectionInfo::Redis(redis_info) = client.unwrap()._connection_info { assert_eq!(redis_info.addr.to_string().as_str(), "127.0.0.1:6379"); return; } @@ -158,14 +159,10 @@ mod tests { fn test_invalid_connection_pool_size() { // Connection pool size must be between 0 and 32 - assert!(FalkorClientBuilder::new() - .with_num_connections(0) - .build() - .is_err()); + let zero = FalkorClientBuilder::new().with_num_connections(0).build(); - assert!(FalkorClientBuilder::new() - .with_num_connections(36) - .build() - .is_err()); + let too_many = FalkorClientBuilder::new().with_num_connections(36).build(); + + assert!(zero.is_err() && too_many.is_err()); } } diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs index 78f4add..7e83624 100644 --- a/src/connection/asynchronous.rs +++ b/src/connection/asynchronous.rs @@ -19,6 +19,7 @@ impl FalkorAsyncConnection { &mut self, graph_name: Option, command: &str, + subcommand: Option<&str>, params: Option, ) -> Result { Ok(match self { @@ -26,7 +27,12 @@ impl FalkorAsyncConnection { FalkorAsyncConnection::Redis(redis_conn) => { redis::FromRedisValue::from_owned_redis_value( redis_conn - .send_packed_command(redis::cmd(command).arg(graph_name).arg(params)) + .send_packed_command( + redis::cmd(command) + .arg(subcommand) + .arg(graph_name) + .arg(params), + ) .await?, )? } diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 9f2138f..5f98cff 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -5,7 +5,6 @@ use crate::{FalkorDBError, FalkorValue}; use anyhow::Result; -use redis::ConnectionLike; use std::sync::mpsc; pub(crate) enum FalkorSyncConnection { @@ -33,15 +32,23 @@ impl BorrowedSyncConnection { &mut self, graph_name: Option, command: &str, - params: Option, + subcommand: Option<&str>, + params: Option<&[String]>, ) -> Result { Ok( match self.conn.as_mut().ok_or(FalkorDBError::EmptyConnection)? { #[cfg(feature = "redis")] FalkorSyncConnection::Redis(redis_conn) => { - redis::FromRedisValue::from_owned_redis_value( - redis_conn.req_command(redis::cmd(command).arg(graph_name).arg(params))?, - )? + use redis::ConnectionLike as _; + let mut cmd = redis::cmd(command); + cmd.arg(subcommand); + cmd.arg(graph_name); + if let Some(params) = params { + for param in params { + cmd.arg(param); + } + } + redis::FromRedisValue::from_owned_redis_value(redis_conn.req_command(&cmd)?)? } }, ) diff --git a/src/connection_info/mod.rs b/src/connection_info/mod.rs index 79c356c..98545da 100644 --- a/src/connection_info/mod.rs +++ b/src/connection_info/mod.rs @@ -82,6 +82,7 @@ mod tests { #[test] #[cfg(feature = "redis")] + #[allow(irrefutable_let_patterns)] fn test_redis_fallback_provider() { if let FalkorConnectionInfo::Redis(redis) = FalkorConnectionInfo::fallback_provider("127.0.0.1:6379".to_string()).unwrap() diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index 30bfb29..0271b28 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -25,10 +25,11 @@ impl AsyncGraph { async fn send_command( &self, command: &str, + subcommand: Option<&str>, params: Option, ) -> Result { let mut conn = self.connection.clone(); - conn.send_command(Some(self.graph_name.clone()), command, params) + conn.send_command(Some(self.graph_name.clone()), command, subcommand, params) .await } diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 8ece559..4cc7d04 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -4,14 +4,18 @@ */ use super::utils::{construct_query, generate_procedure_call}; +use crate::connection::blocking::FalkorSyncConnection; use crate::{ - client::blocking::FalkorSyncClientInner, connection::blocking::FalkorSyncConnection, - Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorDBError, FalkorParsable, - FalkorValue, QueryResult, SlowlogEntry, SyncGraphSchema, + client::blocking::FalkorSyncClientInner, Constraint, ConstraintType, EntityType, ExecutionPlan, + FalkorDBError, FalkorParsable, FalkorValue, QueryResult, SlowlogEntry, SyncGraphSchema, }; use anyhow::Result; -use redis::ConnectionLike; -use std::{collections::HashMap, fmt::Debug, sync::Arc}; +use std::collections::HashMap; +use std::fmt::Display; +use std::{ + fmt::{Debug, Formatter}, + sync::Arc, +}; /// The main graph API, this allows the user to perform graph operations while exposing as little details as possible. /// @@ -27,6 +31,17 @@ pub struct SyncGraph { pub graph_schema: SyncGraphSchema, } +impl Debug for SyncGraph { + fn fmt( + &self, + f: &mut Formatter<'_>, + ) -> std::fmt::Result { + f.debug_struct("SyncGraph") + .field("client", &"") + .finish() + } +} + impl SyncGraph { /// Returns the name of the graph for which this API performs operations. /// @@ -39,16 +54,17 @@ impl SyncGraph { fn send_command( &self, command: &str, - params: Option, + subcommand: Option<&str>, + params: Option<&[String]>, ) -> Result { let mut conn = self.client.borrow_connection()?; - conn.send_command(Some(self.graph_name.clone()), command, params) + conn.send_command(Some(self.graph_name.clone()), command, subcommand, params) } /// Deletes the graph stored in the database, and drop all the schema caches. /// NOTE: This still maintains the graph API, operations are still viable. pub fn delete(&self) -> Result<()> { - self.send_command("GRAPH.DELETE", None)?; + self.send_command("GRAPH.DELETE", None, None)?; self.graph_schema.clear(); Ok(()) } @@ -58,7 +74,7 @@ impl SyncGraph { /// # Returns /// A [`Vec`] of [`SlowlogEntry`], providing information about each query. pub fn slowlog(&self) -> Result> { - let res = self.send_command("GRAPH.SLOWLOG", None)?.into_vec()?; + let res = self.send_command("GRAPH.SLOWLOG", None, None)?.into_vec()?; if res.is_empty() { return Ok(vec![]); @@ -79,7 +95,7 @@ impl SyncGraph { /// Resets the slowlog, all query time data will be cleared. pub fn slowlog_reset(&self) -> Result<()> { - self.send_command("GRAPH.SLOWLOG", Some("RESET".to_string()))?; + self.send_command("GRAPH.SLOWLOG", None, Some(&["RESET".to_string()]))?; Ok(()) } @@ -100,7 +116,7 @@ impl SyncGraph { ) -> Result { let query = construct_query(query_string, params); - ExecutionPlan::try_from(self.send_command("GRAPH.PROFILE", Some(query))?) + ExecutionPlan::try_from(self.send_command("GRAPH.PROFILE", None, Some(&[query]))?) .map_err(Into::into) } @@ -135,7 +151,7 @@ impl SyncGraph { params: Option<&HashMap>, ) -> Result { let query = construct_query(query_string, params); - ExecutionPlan::try_from(self.send_command("GRAPH.EXPLAIN", Some(query))?) + ExecutionPlan::try_from(self.send_command("GRAPH.EXPLAIN", None, Some(&[query]))?) .map_err(Into::into) } @@ -159,7 +175,7 @@ impl SyncGraph { command: &str, query_string: Q, params: Option<&HashMap>, - timeout: Option, + timeout: Option, ) -> Result

{ let query = construct_query(query_string, params); @@ -167,6 +183,7 @@ impl SyncGraph { let falkor_result = match conn.as_inner()? { #[cfg(feature = "redis")] FalkorSyncConnection::Redis(redis_conn) => { + use redis::ConnectionLike as _; use redis::FromRedisValue as _; let redis_val = redis_conn.req_command( redis::cmd(command) @@ -190,10 +207,10 @@ impl SyncGraph { /// /// # Returns /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query - pub fn query( + pub fn query( &self, query_string: Q, - timeout: Option, + timeout: Option, ) -> Result { self.query_inner::("GRAPH.QUERY", query_string, None, timeout) } @@ -208,10 +225,10 @@ impl SyncGraph { /// /// # Returns /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query - pub fn query_with_params( + pub fn query_with_params( &self, query_string: Q, - timeout: Option, + timeout: Option, params: &HashMap, ) -> Result { self.query_inner("GRAPH.QUERY", query_string, Some(params), timeout) @@ -226,10 +243,10 @@ impl SyncGraph { /// /// # Returns /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query - pub fn query_readonly( + pub fn query_readonly( &self, query_string: Q, - timeout: Option, + timeout: Option, ) -> Result { self.query_inner::( "GRAPH.QUERY_RO", @@ -253,10 +270,10 @@ impl SyncGraph { pub fn query_readonly_with_params( &self, query_string: Q, - timeout: Option, - params: &HashMap, + timeout: Option, + params: Option<&HashMap>, ) -> Result { - self.query_inner("GRAPH.QUERY_RO", query_string, Some(params), timeout) + self.query_inner("GRAPH.QUERY_RO", query_string, params, timeout) } /// Run a query which calls a procedure on the graph, read-only, or otherwise. @@ -278,7 +295,7 @@ impl SyncGraph { args: Option<&[String]>, yields: Option<&[String]>, read_only: bool, - timeout: Option, + timeout: Option, ) -> Result

{ let (query_string, params) = generate_procedure_call(procedure, args, yields); @@ -329,29 +346,70 @@ impl SyncGraph { Ok(constraints_vec) } - /// Creates a new constraint for this graph + /// Creates a new constraint for this graph, making the provided properties mandatory /// /// # Arguments - /// * `constraint_type`: Which constraint to apply. /// * `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. - pub fn create_constraint( + pub fn create_mandatory_constraint( &self, - constraint_type: ConstraintType, entity_type: EntityType, label: L, properties: &[P], ) -> Result { - self.send_command( - "GRAPH.CONSTRAINT", - Some(format!( - "CREATE {constraint_type} {entity_type} {} {} {:?}", + let mut params = Vec::with_capacity(5 + properties.len()); + params.extend([ + "MANDATORY".to_string(), + entity_type.to_string(), + label.to_string(), + "PROPERTIES".to_string(), + properties.len().to_string(), + ]); + params.extend(properties.iter().map(|property| property.to_string())); + + self.send_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) + } + + /// Creates a new constraint for this graph, making the provided properties unique + /// + /// # Arguments + /// * `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. + pub fn create_unique_constraint( + &self, + entity_type: EntityType, + label: L, + properties: &[P], + ) -> Result { + // Create index from these properties + let properties_string = properties + .iter() + .map(|element| format!("l.{}", element.to_string())) + .collect::>() + .join(", "); + self.query( + format!( + "CREATE INDEX FOR (l:{}) ON ({})", label.to_string(), - properties.len(), - properties - )), - ) + properties_string + ), + None, + )?; + + let mut params: Vec = Vec::with_capacity(5 + properties.len()); + params.extend([ + "UNIQUE".to_string(), + entity_type.to_string(), + label.to_string(), + "PROPERTIES".to_string(), + properties.len().to_string(), + ]); + params.extend(properties.into_iter().map(|property| property.to_string())); + + // create constraint using index + self.send_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) } /// Drop an existing constraint from the graph @@ -361,41 +419,68 @@ 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. - pub fn drop_constraint( + pub fn drop_constraint( &self, constraint_type: ConstraintType, entity_type: EntityType, label: L, properties: &[P], ) -> Result { - self.send_command( - "GRAPH.CONSTRAINT", - Some(format!( - "DROP {constraint_type} {entity_type} {} {} {:?}", - label.to_string(), - properties.len(), - properties - )), - ) + let mut params = Vec::with_capacity(5 + properties.len()); + params.extend([ + constraint_type.to_string(), + entity_type.to_string(), + label.to_string(), + "PROPERTIES".to_string(), + properties.len().to_string(), + ]); + params.extend(properties.iter().map(|property| property.to_string())); + + self.send_command("GRAPH.CONSTRAINT", Some("DROP"), Some(params.as_slice())) } } #[cfg(test)] mod tests { - use crate::{ConstraintType, EntityType, FalkorClientBuilder}; + use crate::{test_utils::open_test_graph, ConstraintType, EntityType}; #[test] - fn test_create_constraint() { - let client = FalkorClientBuilder::new() - .with_num_connections(4) - .build() - .expect("Could not create client"); - - let graph = client.open_graph("imdb"); - let res = graph - .create_constraint(ConstraintType::Unique, EntityType::Edge, "act", &["hello"]) + fn test_create_drop_mandatory_constraint() { + let graph = open_test_graph("test_mandatory_constraint"); + + graph + .inner + .create_mandatory_constraint(EntityType::Edge, "act", &["hello", "goodbye"]) + .expect("Could not create constraint"); + + graph + .inner + .drop_constraint( + ConstraintType::Mandatory, + EntityType::Edge, + "act", + &["hello", "goodbye"], + ) + .expect("Could not drop constraint"); + } + + #[test] + fn test_create_drop_unique_constraint() { + let graph = open_test_graph("test_unique_constraint"); + + graph + .inner + .create_unique_constraint(EntityType::Node, "actor", &["first_name", "last_name"]) .expect("Could not create constraint"); - panic!("{res:?}"); + graph + .inner + .drop_constraint( + ConstraintType::Unique, + EntityType::Node, + "actor", + &["first_name", "last_name"], + ) + .expect("Could not drop constraint"); } } diff --git a/src/graph/utils.rs b/src/graph/utils.rs index c255353..4dc0d77 100644 --- a/src/graph/utils.rs +++ b/src/graph/utils.rs @@ -3,7 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use std::collections::HashMap; +use std::{collections::HashMap, ops::Not}; pub(crate) fn generate_procedure_call( procedure: P, @@ -49,14 +49,15 @@ pub(crate) fn construct_query( query_str: Q, params: Option<&HashMap>, ) -> String { - params - .map(|params| { - params + format!( + "{}{}", + params + .and_then(|params| params.is_empty().not().then(|| params .iter() .fold("CYPHER ".to_string(), |acc, (key, val)| { - acc + format!("{}={}", key.to_string(), val.to_string()).as_str() - }) - }) - .unwrap_or_default() - + query_str.to_string().as_str() + format!("{} {}={}", acc, key.to_string(), val.to_string()) + }))) + .unwrap_or_default(), + query_str.to_string() + ) } diff --git a/src/graph_schema/blocking.rs b/src/graph_schema/blocking.rs index 5797b69..fefe9b4 100644 --- a/src/graph_schema/blocking.rs +++ b/src/graph_schema/blocking.rs @@ -103,7 +103,8 @@ impl SyncGraphSchema { .send_command( Some(self.graph_name.clone()), "GRAPH.QUERY", - Some(format!("CALL {command}()")), + None, + Some(&[format!("CALL {command}()")]), )? .into_vec()? .try_into() diff --git a/src/lib.rs b/src/lib.rs index 7ba1e93..eec3d71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -46,3 +46,36 @@ pub use { graph::asynchronous::AsyncGraph, graph_schema::asynchronous::AsyncGraphSchema, parser::FalkorAsyncParseable, }; + +#[cfg(test)] +pub(crate) mod test_utils { + use crate::{FalkorClientBuilder, FalkorSyncClient, SyncGraph}; + + pub(crate) struct TestGraphHandle { + pub(crate) inner: SyncGraph, + } + + impl Drop for TestGraphHandle { + fn drop(&mut self) { + self.inner.delete().ok(); + } + } + + pub(crate) fn create_test_client() -> FalkorSyncClient { + FalkorClientBuilder::new() + .build() + .expect("Could not create client") + } + + pub(crate) fn open_test_graph(graph_name: &str) -> TestGraphHandle { + let client = create_test_client(); + + client.open_graph(graph_name).delete().ok(); + + TestGraphHandle { + inner: client + .copy_graph("imdb", graph_name) + .expect("Could not copy graph for test"), + } + } +} diff --git a/src/response/constraint.rs b/src/response/constraint.rs index 40ddf11..97dda74 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -102,7 +102,7 @@ impl TryFrom<&String> for ConstraintStatus { } /// A constraint applied on all 'properties' of the graph entity 'label' in this graph -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Constraint { /// Is this constraint applies the 'unique' modifier or the 'mandatory' modifier pub constraint_type: ConstraintType, diff --git a/src/response/execution_plan.rs b/src/response/execution_plan.rs index c60991b..a7889b4 100644 --- a/src/response/execution_plan.rs +++ b/src/response/execution_plan.rs @@ -6,7 +6,7 @@ use crate::{FalkorDBError, FalkorValue}; /// An execution plan, storing both the specific string details for each step, and also a formatted plaintext string for pretty display -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct ExecutionPlan { text: String, steps: Vec, diff --git a/src/response/mod.rs b/src/response/mod.rs index 5e9490a..c29ba80 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -15,7 +15,7 @@ pub(crate) mod slowlog_entry; /// A naive wrapper for the various possible responses from queries /// Its main usecase is for creating things like [`JoinSet`](tokio::task::JoinSet)s, which require all the tasks to return the same type -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum ResponseVariant { ExecutionPlan(ExecutionPlan), QueryResult(QueryResult), diff --git a/src/response/query_result.rs b/src/response/query_result.rs index 989b56a..a0948d2 100644 --- a/src/response/query_result.rs +++ b/src/response/query_result.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; use crate::value::utils_async::parse_type_async; /// A struct returned by the various queries, containing the result set, header, and stats -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct QueryResult { /// The statistics for this query, such as how long it took pub stats: Vec, diff --git a/src/response/slowlog_entry.rs b/src/response/slowlog_entry.rs index 76f2596..be826df 100644 --- a/src/response/slowlog_entry.rs +++ b/src/response/slowlog_entry.rs @@ -7,7 +7,7 @@ use crate::{FalkorDBError, FalkorValue}; use anyhow::Result; /// A slowlog entry, representing one of the N slowest queries in the current log -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct SlowlogEntry { /// At which time was this query received pub timestamp: i64, diff --git a/src/value/config.rs b/src/value/config.rs index 12a6004..dc6c35d 100644 --- a/src/value/config.rs +++ b/src/value/config.rs @@ -12,6 +12,15 @@ pub enum ConfigValue { Int64(i64), } +impl ConfigValue { + pub fn as_i64(&self) -> Option { + match self { + ConfigValue::String(_) => None, + ConfigValue::Int64(i64) => Some(*i64), + } + } +} + impl Display for ConfigValue { fn fmt( &self, diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index bfaeb1c..0674f9c 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -65,7 +65,7 @@ impl TryFrom<&String> for EntityType { } /// A node in the graph, containing a unique id, various labels describing it, and its own property. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Node { /// The internal entity ID pub entity_id: i64, @@ -149,7 +149,7 @@ impl crate::FalkorAsyncParseable for Node { } /// An edge in the graph, representing a relationship between two [`Node`]s. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Edge { /// The internal entity ID pub entity_id: i64, diff --git a/src/value/mod.rs b/src/value/mod.rs index 991cb47..be6742a 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -23,7 +23,7 @@ pub(crate) mod utils; pub(crate) mod utils_async; /// An enum of all the supported Falkor types -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub enum FalkorValue { FNode(Node), FEdge(Edge), diff --git a/src/value/path.rs b/src/value/path.rs index b1ea467..7c1a2ea 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -10,7 +10,7 @@ use crate::{ use anyhow::Result; /// TODO: not exactly sure what this represents -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Path { pub nodes: Vec, pub relationships: Vec, diff --git a/src/value/point.rs b/src/value/point.rs index 9ae16d6..da3d1ea 100644 --- a/src/value/point.rs +++ b/src/value/point.rs @@ -6,7 +6,7 @@ use crate::{FalkorDBError, FalkorValue}; /// A point in the world. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct Point { /// The latitude coordinate pub latitude: f64, From 36fa2610dedf0a1828570612e4d5ca26d88f014c Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Thu, 23 May 2024 17:23:39 +0300 Subject: [PATCH 13/62] Current tests --- Cargo.lock | 32 ++++++++++++ Cargo.toml | 5 +- src/graph/blocking.rs | 96 +++++++++++++++++++++++++++++++++-- src/response/constraint.rs | 24 +++++---- src/response/query_result.rs | 2 +- src/response/slowlog_entry.rs | 12 +++-- 6 files changed, 153 insertions(+), 18 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7898f44..73bb6c8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -162,6 +162,7 @@ dependencies = [ "redis", "thiserror", "tokio", + "tracing", "url-parse", ] @@ -866,6 +867,37 @@ dependencies = [ "tokio", ] +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", +] + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/Cargo.toml b/Cargo.toml index 64a9d22..c4174da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,13 +5,14 @@ edition = "2021" [dependencies] -async-recursion = { version = "1.1.1" } anyhow = { version = "1.0.83", default-features = false, features = ["std"] } +async-recursion = { version = "1.1.1" } log = { version = "0.4.21", default-features = false } parking_lot = { version = "0.12.2", default-features = false, features = ["deadlock_detection"] } redis = { version = "0.25.3", default-features = false, optional = true } -tokio = { version = "1.37.0", default-features = false, features = ["rt-multi-thread", "sync", "macros"], optional = true } thiserror = "1.0.60" +tokio = { version = "1.37.0", default-features = false, features = ["rt-multi-thread", "sync", "macros"], optional = true } +tracing = { version = "0.1.40", default-features = false, features = ["std", "attributes"] } url-parse = "1.0.8" [features] diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 4cc7d04..1fbda36 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -330,9 +330,13 @@ impl SyncGraph { /// A [`Vec`] of [`Constraint`]s pub fn list_constraints(&self) -> Result> { let mut conn = self.client.borrow_connection()?; - let query_res = self - .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, true, None)? - .into_vec()?; + let [_, query_res, _]: [FalkorValue; 3] = self + .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, false, None)? + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + let query_res = query_res.into_vec()?; let mut constraints_vec = Vec::with_capacity(query_res.len()); for item in query_res { @@ -483,4 +487,90 @@ mod tests { ) .expect("Could not drop constraint"); } + + #[test] + fn test_list_constraints() { + let graph = open_test_graph("test_list_constraint"); + + graph + .inner + .create_unique_constraint(EntityType::Node, "actor", &["first_name", "last_name"]) + .expect("Could not create constraint"); + + let constraints = graph + .inner + .list_constraints() + .expect("Could not list constraints"); + assert_eq!(constraints.len(), 1); + } + + #[test] + fn test_slowlog() { + let graph = open_test_graph("test_slowlog"); + + graph + .inner + .query("UNWIND range(0, 500) AS x RETURN x", None) + .expect("Could not generate the fast query"); + graph + .inner + .query("UNWIND range(0, 100000) AS x RETURN x", None) + .expect("Could not generate the slow query"); + + let slowlog = graph + .inner + .slowlog() + .expect("Could not get slowlog entries"); + + assert_eq!(slowlog.len(), 2); + assert_eq!( + slowlog[0].arguments, + "UNWIND range(0, 500) AS x RETURN x".to_string() + ); + assert_eq!( + slowlog[1].arguments, + "UNWIND range(0, 100000) AS x RETURN x".to_string() + ); + + graph + .inner + .slowlog_reset() + .expect("Could not reset slowlog memory"); + let slowlog_after_reset = graph + .inner + .slowlog() + .expect("Could not get slowlog entries after reset"); + assert!(slowlog_after_reset.is_empty()); + } + + #[test] + fn test_explain() { + let graph = open_test_graph("test_explain"); + + let execution_plan = graph.inner.explain("MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100").expect("Could not create execution plan"); + assert_eq!(execution_plan.steps().len(), 7); + assert_eq!( + execution_plan.text(), + "\nResults\n Limit\n Aggregate\n Filter\n Node By Index Scan | (b:actor)\n Project\n Node By Label Scan | (a:actor)" + ); + } + + #[test] + fn test_profile() { + let graph = open_test_graph("test_profile"); + + let execution_plan = graph + .inner + .profile("UNWIND range(0, 1000) AS x RETURN x") + .expect("Could not generate the query"); + + let steps = execution_plan.steps().to_vec(); + assert_eq!(steps.len(), 3); + + let expected = vec!["Results", "Project", "Unwind"]; + for (step, expected) in steps.into_iter().zip(expected) { + assert!(step.starts_with(expected)); + assert!(step.ends_with("ms")); + } + } } diff --git a/src/response/constraint.rs b/src/response/constraint.rs index 97dda74..b7ae3c8 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -4,8 +4,9 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, EntityType, FalkorDBError, FalkorParsable, - FalkorValue, SyncGraphSchema, + connection::blocking::BorrowedSyncConnection, + value::utils::{parse_type, type_val_from_value}, + EntityType, FalkorDBError, FalkorParsable, FalkorValue, SyncGraphSchema, }; use anyhow::Result; use std::fmt::{Display, Formatter}; @@ -119,14 +120,19 @@ pub struct Constraint { impl FalkorParsable for Constraint { fn from_falkor_value( value: FalkorValue, - _graph_schema: &SyncGraphSchema, - _conn: &mut BorrowedSyncConnection, + graph_schema: &SyncGraphSchema, + conn: &mut BorrowedSyncConnection, ) -> Result { - let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; - 5] = value - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + let value_vec = value.into_vec()?; + + let mut parsed_values = Vec::with_capacity(value_vec.len()); + for column_raw in value_vec { + parsed_values.push(type_val_from_value(column_raw).and_then( + |(type_marker, raw_val)| parse_type(type_marker, raw_val, graph_schema, conn), + )?); + } + + let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = parsed_values.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let constraint_type = constraint_type_raw.into_string()?.try_into()?; let label = label_raw.into_string()?; diff --git a/src/response/query_result.rs b/src/response/query_result.rs index a0948d2..9bea92d 100644 --- a/src/response/query_result.rs +++ b/src/response/query_result.rs @@ -75,7 +75,7 @@ fn query_parse_stats(stats: FalkorValue) -> Result> { Ok(stats_strings) } -fn parse_result_set( +pub(crate) fn parse_result_set( data_vec: Vec, graph_schema: &SyncGraphSchema, conn: &mut BorrowedSyncConnection, diff --git a/src/response/slowlog_entry.rs b/src/response/slowlog_entry.rs index be826df..90ca1d8 100644 --- a/src/response/slowlog_entry.rs +++ b/src/response/slowlog_entry.rs @@ -16,17 +16,23 @@ pub struct SlowlogEntry { /// The query itself pub arguments: String, /// How long did performing this query take. - pub time_taken: i64, + pub time_taken: f64, } impl SlowlogEntry { pub fn from_value_array(values: [FalkorValue; 4]) -> Result { let [timestamp, command, arguments, time_taken] = values; Ok(Self { - timestamp: timestamp.to_i64().ok_or(FalkorDBError::ParsingI64)?, + timestamp: timestamp + .into_string()? + .parse() + .map_err(|_| FalkorDBError::ParsingI64)?, command: command.into_string()?, arguments: arguments.into_string()?, - time_taken: time_taken.to_i64().ok_or(FalkorDBError::ParsingI64)?, + time_taken: time_taken + .into_string()? + .parse() + .map_err(|_| FalkorDBError::ParsingF64)?, }) } } From 297cf71d3374994bd8700b7035ff2df151d0b111 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Sun, 26 May 2024 16:16:25 +0300 Subject: [PATCH 14/62] Work towards indices --- src/client/blocking.rs | 31 +++- src/client/builder.rs | 47 ++++++- src/connection/blocking.rs | 6 +- src/connection_info/mod.rs | 51 +++++-- src/error/mod.rs | 6 + src/graph/blocking.rs | 198 +++++++++++++++++++++----- src/graph/utils.rs | 45 +++++- src/graph_schema/blocking.rs | 2 +- src/graph_schema/utils.rs | 121 +++++++++++++--- src/lib.rs | 1 + src/response/constraint.rs | 2 +- src/response/index.rs | 148 +++++++++++++++++++ src/response/mod.rs | 3 + src/value/graph_entities.rs | 2 +- src/value/map.rs | 27 +++- src/value/mod.rs | 210 +++++++++++++++------------ src/value/utils.rs | 266 ++++++++++++++++++++++++++++++++++- 17 files changed, 986 insertions(+), 180 deletions(-) create mode 100644 src/response/index.rs diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 3888f14..da6b5ac 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -10,6 +10,8 @@ use crate::{ }; use anyhow::Result; use parking_lot::Mutex; +use std::fmt::{Debug, Formatter}; +use std::time::Duration; use std::{ collections::HashMap, sync::{mpsc, Arc}, @@ -48,15 +50,28 @@ pub struct FalkorSyncClient { pub(crate) _connection_info: FalkorConnectionInfo, } +impl Debug for FalkorSyncClient { + fn fmt( + &self, + f: &mut Formatter<'_>, + ) -> std::fmt::Result { + f.debug_struct("FalkorSyncClient") + .field("inner", &"") + .field("connection_info", &self._connection_info) + .finish() + } +} + impl FalkorSyncClient { pub(crate) fn create( client: FalkorClientProvider, connection_info: FalkorConnectionInfo, num_connections: u8, + timeout: Option, ) -> Result { let (connection_pool_tx, connection_pool_rx) = mpsc::sync_channel(num_connections as usize); for _ in 0..num_connections { - connection_pool_tx.send(client.get_connection(None)?)?; + connection_pool_tx.send(client.get_connection(timeout)?)?; } Ok(Self { @@ -260,9 +275,9 @@ impl FalkorSyncClient { #[cfg(test)] mod tests { - use crate::test_utils::TestGraphHandle; + use super::*; use crate::{ - connection::blocking::BorrowedSyncConnection, test_utils::create_test_client, ConfigValue, + test_utils::{create_test_client, TestGraphHandle}, FalkorClientBuilder, }; use std::{mem, sync::mpsc::TryRecvError, thread}; @@ -293,6 +308,16 @@ mod tests { assert!(false); } + #[test] + fn test_list_graphs() { + let client = create_test_client(); + let res = client.list_graphs(); + assert!(res.is_ok()); + + let graphs = res.unwrap(); + assert_eq!(graphs[0], "imdb"); + } + #[test] fn test_open_graph_and_query() { let client = create_test_client(); diff --git a/src/client/builder.rs b/src/client/builder.rs index 70f0d20..28a2396 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -5,10 +5,12 @@ use crate::{client::FalkorClientProvider, FalkorConnectionInfo, FalkorDBError, FalkorSyncClient}; use anyhow::Result; +use std::time::Duration; /// A Builder-pattern implementation struct for creating a new Falkor client, sync or async. pub struct FalkorClientBuilder { connection_info: Option, + timeout: Option, num_connections: u8, } @@ -47,6 +49,23 @@ impl FalkorClientBuilder { ..self } } + + /// Specify a timeout duration for requests and connections. + /// + /// # Arguments + /// * `timeout`: a [`Duration`], after which a timeout error will be returned from the connection. + /// + /// # Returns + /// The consumed and modified self. + pub fn with_timeout( + self, + timeout: Duration, + ) -> Self { + Self { + timeout: Some(timeout), + ..self + } + } } fn get_client>(connection_info: T) -> Result @@ -71,6 +90,7 @@ impl FalkorClientBuilder<'S'> { pub fn new() -> Self { FalkorClientBuilder { connection_info: None, + timeout: None, num_connections: 4, } } @@ -92,6 +112,7 @@ impl FalkorClientBuilder<'S'> { get_client(connection_info.clone())?, connection_info, self.num_connections, + self.timeout, ) } } @@ -102,6 +123,7 @@ impl FalkorClientBuilder<'A'> { FalkorClientBuilder { connection_info: None, num_connections: 4, + timeout: None, } } @@ -118,7 +140,7 @@ impl FalkorClientBuilder<'A'> { #[cfg(test)] mod tests { - use crate::{FalkorClientBuilder, FalkorConnectionInfo}; + use super::*; #[test] fn test_sync_builder() { @@ -139,12 +161,8 @@ mod tests { let client = FalkorClientBuilder::new().build(); assert!(client.is_ok()); - if let FalkorConnectionInfo::Redis(redis_info) = client.unwrap()._connection_info { - assert_eq!(redis_info.addr.to_string().as_str(), "127.0.0.1:6379"); - return; - } - - assert!(false); + let FalkorConnectionInfo::Redis(redis_info) = client.unwrap()._connection_info; + assert_eq!(redis_info.addr.to_string().as_str(), "127.0.0.1:6379"); } #[test] @@ -165,4 +183,19 @@ mod tests { assert!(zero.is_err() && too_many.is_err()); } + + #[test] + fn test_timeout() { + { + let client = FalkorClientBuilder::new() + .with_timeout(Duration::from_millis(100)) + .build(); + assert!(client.is_ok()); + } + + let impossible_client = FalkorClientBuilder::new() + .with_timeout(Duration::from_nanos(10)) + .build(); + assert!(impossible_client.is_err()); + } } diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 5f98cff..a625110 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -22,10 +22,8 @@ pub struct BorrowedSyncConnection { } impl BorrowedSyncConnection { - pub(crate) fn as_inner(&mut self) -> Result<&mut FalkorSyncConnection> { - self.conn - .as_mut() - .ok_or(FalkorDBError::EmptyConnection.into()) + pub(crate) fn as_inner(&mut self) -> Result<&mut FalkorSyncConnection, FalkorDBError> { + self.conn.as_mut().ok_or(FalkorDBError::EmptyConnection) } pub(crate) fn send_command( diff --git a/src/connection_info/mod.rs b/src/connection_info/mod.rs index 98545da..eb28b21 100644 --- a/src/connection_info/mod.rs +++ b/src/connection_info/mod.rs @@ -7,7 +7,7 @@ use anyhow::Result; /// An agnostic container which allows maintaining of various connection details. /// The different enum variants are enabled based on compilation features -#[derive(Clone)] +#[derive(Clone, Debug)] pub enum FalkorConnectionInfo { #[cfg(feature = "redis")] Redis(redis::ConnectionInfo), @@ -20,6 +20,17 @@ impl FalkorConnectionInfo { redis::IntoConnectionInfo::into_connection_info(format!("redis://{full_url}"))?, )) } + + /// Retrieves the internally stored address for this connection info + /// + /// # Returns + /// 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(), + } + } } impl TryFrom<&str> for FalkorConnectionInfo { @@ -78,19 +89,39 @@ impl TryFrom<(T, u16)> for FalkorConnectionInfo { #[cfg(test)] mod tests { - use crate::FalkorConnectionInfo; + use super::*; + use std::{mem, str::FromStr}; #[test] #[cfg(feature = "redis")] - #[allow(irrefutable_let_patterns)] fn test_redis_fallback_provider() { - if let FalkorConnectionInfo::Redis(redis) = - FalkorConnectionInfo::fallback_provider("127.0.0.1:6379".to_string()).unwrap() - { - assert_eq!(redis.addr.to_string(), "127.0.0.1:6379".to_string()); - return; - } + let FalkorConnectionInfo::Redis(redis) = + FalkorConnectionInfo::fallback_provider("127.0.0.1:6379".to_string()).unwrap(); - assert!(false); + assert_eq!(redis.addr.to_string(), "127.0.0.1:6379".to_string()); + } + + #[test] + #[cfg(feature = "redis")] + fn test_try_from_redis() { + let res = FalkorConnectionInfo::try_from("redis://0.0.0.0:1234"); + assert!(res.is_ok()); + + let redis_conn = res.unwrap(); + let raw_redis_conn = redis::ConnectionInfo::from_str("redis://0.0.0.0:1234").unwrap(); + assert_eq!( + mem::discriminant(&redis_conn), + mem::discriminant(&FalkorConnectionInfo::Redis(raw_redis_conn.clone())) + ); + + let FalkorConnectionInfo::Redis(conn) = redis_conn; + assert_eq!(conn.addr, raw_redis_conn.addr); + } + + #[test] + fn test_from_addr_port() { + let res = FalkorConnectionInfo::try_from(("127.0.0.1", 1234)); + assert!(res.is_ok()); + assert_eq!(res.unwrap().address(), "127.0.0.1:1234".to_string()); } } diff --git a/src/error/mod.rs b/src/error/mod.rs index 725da8a..dd15a96 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -51,10 +51,16 @@ pub enum FalkorDBError { ParsingTypeMarkerTypeMismatch, #[error("Both key id and type marker were not of type i64")] ParsingKTVTypes, + #[error("Field missing or mismatched while parsing index")] + ParsingIndex, #[error("Attempting to parse an FArray into a struct, but the array doesn't have the expected element count")] ParsingArrayToStructElementCount, #[error("Invalid constraint type, expected 'UNIQUE' or 'MANDATORY'")] ConstraintType, #[error("Invalid constraint status, expected 'OPERATIONAL', 'UNDER CONSTRUCTION' or 'FAILED'")] ConstraintStatus, + #[error("Invalid Index status, expected 'OPERATIONAL' or 'UNDER CONSTRUCTION'")] + IndexStatus, + #[error("Invalid Index field type, expected 'RANGE', 'VECTOR' or 'FULLTEXT'")] + IndexFieldType, } diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 1fbda36..b727f09 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -4,16 +4,15 @@ */ use super::utils::{construct_query, generate_procedure_call}; -use crate::connection::blocking::FalkorSyncConnection; use crate::{ - client::blocking::FalkorSyncClientInner, Constraint, ConstraintType, EntityType, ExecutionPlan, - FalkorDBError, FalkorParsable, FalkorValue, QueryResult, SlowlogEntry, SyncGraphSchema, + client::blocking::FalkorSyncClientInner, connection::blocking::FalkorSyncConnection, + Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorDBError, FalkorIndex, + FalkorParsable, FalkorValue, IndexFieldType, QueryResult, SlowlogEntry, SyncGraphSchema, }; use anyhow::Result; -use std::collections::HashMap; -use std::fmt::Display; use std::{ - fmt::{Debug, Formatter}, + collections::HashMap, + fmt::{Debug, Display, Formatter}, sync::Arc, }; @@ -292,8 +291,8 @@ impl SyncGraph { pub fn call_procedure( &self, procedure: C, - args: Option<&[String]>, - yields: Option<&[String]>, + args: Option<&[&str]>, + yields: Option<&[&str]>, read_only: bool, timeout: Option, ) -> Result

{ @@ -311,17 +310,98 @@ impl SyncGraph { ) } - /// Calls the DB.INDICES procedure on the graph - /// TODO: - pub fn list_indices(&self) -> Result { - let query_res = self - .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, true, None)? - .into_vec()?; + /// Calls the DB.INDICES procedure on the graph, returning all the indexing methods currently used + /// + /// # Returns + /// A [`Vec`] of [`Index`] + pub fn list_indices(&self) -> Result> { + let mut conn = self.client.borrow_connection()?; + let [_, indices, _]: [FalkorValue; 3] = self + .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false, None)? + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - for item in query_res { - log::info!("{item:?}"); + let indices = indices.into_vec()?; + + let mut out_vec = Vec::with_capacity(indices.len()); + for index in indices { + out_vec.push( + FalkorIndex::from_falkor_value(index, &self.graph_schema, &mut conn) + .expect("Could not parse"), + ); + } + + Ok(out_vec) + } + + pub fn create_index( + &self, + index_field_type: IndexFieldType, + entity_type: EntityType, + label: L, + properties: &[P], + ) -> Result { + // Create index from these properties + let properties_string = properties + .iter() + .map(|element| format!("l.{}", element.to_string())) + .collect::>() + .join(", "); + + let pattern = match entity_type { + EntityType::Node => format!("(l:{})", label.to_string()), + EntityType::Edge => format!("()-[l:{}]->()", label.to_string()), + }; + + let idx_type = match index_field_type { + IndexFieldType::Range => "", + IndexFieldType::Vector => "VECTOR", + IndexFieldType::Fulltext => "FULLTEXT", + } + .to_string(); + + self.query( + format!( + "CREATE {idx_type} INDEX FOR {pattern} ON ({})", + properties_string + ), + None, + ) + } + + pub fn drop_index( + &self, + index_field_type: IndexFieldType, + entity_type: EntityType, + label: L, + properties: &[P], + ) -> Result { + let properties_string = properties + .iter() + .map(|element| format!("e.{}", element.to_string())) + .collect::>() + .join(", "); + + let pattern = match entity_type { + EntityType::Node => format!("(e:{})", label.to_string()), + EntityType::Edge => format!("()-[e:{}]->()", label.to_string()), + }; + + let idx_type = match index_field_type { + IndexFieldType::Range => "", + IndexFieldType::Vector => "VECTOR", + IndexFieldType::Fulltext => "FULLTEXT", } - Ok(FalkorValue::None) + .to_string(); + + self.query( + format!( + "DROP {idx_type} INDEX for {pattern} ON ({})", + properties_string + ), + None, + ) } /// Calls the DB.CONSTRAINTS procedure on the graph, returning an array of the graph's constraints @@ -381,26 +461,14 @@ 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. - pub fn create_unique_constraint( + pub fn create_unique_constraint( &self, + index_field_type: IndexFieldType, entity_type: EntityType, - label: L, + label: String, properties: &[P], ) -> Result { - // Create index from these properties - let properties_string = properties - .iter() - .map(|element| format!("l.{}", element.to_string())) - .collect::>() - .join(", "); - self.query( - format!( - "CREATE INDEX FOR (l:{}) ON ({})", - label.to_string(), - properties_string - ), - None, - )?; + self.create_index(index_field_type, entity_type, label.as_str(), properties)?; let mut params: Vec = Vec::with_capacity(5 + properties.len()); params.extend([ @@ -446,7 +514,55 @@ impl SyncGraph { #[cfg(test)] mod tests { - use crate::{test_utils::open_test_graph, ConstraintType, EntityType}; + use super::*; + use crate::test_utils::open_test_graph; + use crate::IndexFieldType; + + #[test] + fn test_create_drop_index() { + let graph = open_test_graph("test_create_drop_index"); + let res = graph.inner.create_index( + IndexFieldType::Fulltext, + EntityType::Node, + "actor".to_string(), + &["Hello"], + ); + assert!(res.is_ok()); + + let res = graph.inner.list_indices(); + assert!(res.is_ok()); + + let indices = res.unwrap(); + assert_eq!(indices.len(), 2); + + let res = graph.inner.drop_index( + IndexFieldType::Fulltext, + EntityType::Node, + "actor".to_string(), + &["Hello"], + ); + assert!(res.is_ok()); + } + + #[test] + fn test_list_indices() { + let graph = open_test_graph("test_list_indices"); + let res = graph.inner.list_indices(); + assert!(res.is_ok()); + + let indices = res.unwrap(); + assert_eq!(indices.len(), 1); + assert_eq!(indices[0].entity_type, EntityType::Node); + assert_eq!(indices[0].index_label, "actor".to_string()); + assert_eq!(indices[0].field_types.len(), 2); + assert_eq!( + indices[0].field_types, + HashMap::from([ + ("age".to_string(), vec![IndexFieldType::Range]), + ("name".to_string(), vec![IndexFieldType::Fulltext]) + ]) + ); + } #[test] fn test_create_drop_mandatory_constraint() { @@ -474,7 +590,12 @@ mod tests { graph .inner - .create_unique_constraint(EntityType::Node, "actor", &["first_name", "last_name"]) + .create_unique_constraint( + IndexFieldType::Fulltext, + EntityType::Node, + "actor".to_string(), + &["first_name", "last_name"], + ) .expect("Could not create constraint"); graph @@ -494,7 +615,12 @@ mod tests { graph .inner - .create_unique_constraint(EntityType::Node, "actor", &["first_name", "last_name"]) + .create_unique_constraint( + IndexFieldType::Fulltext, + EntityType::Node, + "actor".to_string(), + &["first_name", "last_name"], + ) .expect("Could not create constraint"); let constraints = graph diff --git a/src/graph/utils.rs b/src/graph/utils.rs index 4dc0d77..ebb654c 100644 --- a/src/graph/utils.rs +++ b/src/graph/utils.rs @@ -3,12 +3,12 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use std::{collections::HashMap, ops::Not}; +use std::{collections::HashMap, fmt::Display, ops::Not}; -pub(crate) fn generate_procedure_call( +pub(crate) fn generate_procedure_call( procedure: P, - args: Option<&[String]>, - yields: Option<&[String]>, + args: Option<&[T]>, + yields: Option<&[Z]>, ) -> (String, Option>) { let params = args.map(|args| { args.iter() @@ -32,7 +32,7 @@ pub(crate) fn generate_procedure_call( let yields = yields.unwrap_or_default(); if !yields.is_empty() { query_string += format!( - "YIELD {}", + " YIELD {}", yields .iter() .map(|element| element.to_string()) @@ -55,9 +55,42 @@ pub(crate) fn construct_query( .and_then(|params| params.is_empty().not().then(|| params .iter() .fold("CYPHER ".to_string(), |acc, (key, val)| { - format!("{} {}={}", acc, key.to_string(), val.to_string()) + format!("{}{}={} ", acc, key.to_string(), val.to_string()) }))) .unwrap_or_default(), query_str.to_string() ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_procedure_call() { + let (query, params) = generate_procedure_call( + "DB.CONSTRAINTS", + Some(&["Hello".to_string(), "World".to_string()]), + Some(&["Foo".to_string(), "Bar".to_string()]), + ); + + assert_eq!(query, "CALL DB.CONSTRAINTS($Hello,$World) YIELD Foo,Bar"); + assert!(params.is_some()); + + let params = params.unwrap(); + assert_eq!(params["param0"], "Hello"); + assert_eq!(params["param1"], "World"); + } + + #[test] + fn test_construct_query() { + let query = construct_query("MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100", + Some(&HashMap::from([("Foo", "Bar"), ("Bizz", "Bazz")]))); + assert!(query.starts_with("CYPHER ")); + assert!(query.ends_with(" MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100")); + + // Order not guaranteed + assert!(query.contains(" Foo=Bar ")); + assert!(query.contains(" Bizz=Bazz ")); + } +} diff --git a/src/graph_schema/blocking.rs b/src/graph_schema/blocking.rs index fefe9b4..306e4cb 100644 --- a/src/graph_schema/blocking.rs +++ b/src/graph_schema/blocking.rs @@ -110,6 +110,6 @@ impl SyncGraphSchema { .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - update_map(write_lock.deref_mut(), keys, id_hashset) + Ok(update_map(write_lock.deref_mut(), keys, id_hashset)?) } } diff --git a/src/graph_schema/utils.rs b/src/graph_schema/utils.rs index 18f75c5..29fdb69 100644 --- a/src/graph_schema/utils.rs +++ b/src/graph_schema/utils.rs @@ -19,7 +19,7 @@ pub(crate) fn update_map( map_to_update: &mut HashMap, keys: FalkorValue, id_hashset: Option<&HashSet>, -) -> Result>> { +) -> Result>, FalkorDBError> { let keys_vec = keys.into_vec()?; let mut new_keys = HashMap::with_capacity(keys_vec.len()); @@ -29,7 +29,7 @@ pub(crate) fn update_map( .into_iter() .next() .ok_or(FalkorDBError::ParsingError)? - .into_string()?; + .try_into()?; new_keys.insert(idx as i64, key); } @@ -37,20 +37,7 @@ pub(crate) fn update_map( match id_hashset { None => Ok(None), - Some(id_hashset) => { - let mut relevant_ids = HashMap::with_capacity(id_hashset.len()); - for id in id_hashset { - relevant_ids.insert( - *id, - map_to_update - .get(id) - .cloned() - .ok_or(FalkorDBError::ParsingError)?, - ); - } - - Ok(Some(relevant_ids)) - } + Some(id_hashset) => Ok(get_relevant_hashmap(id_hashset, map_to_update)), } } @@ -70,3 +57,105 @@ pub(crate) fn get_relevant_hashmap( Some(id_hashmap) } + +#[cfg(test)] +mod tests { + use super::*; + + fn get_test_keys() -> FalkorValue { + FalkorValue::FArray(vec![ + FalkorValue::FArray(vec![FalkorValue::FString("Hello".to_string())]), + FalkorValue::FArray(vec![FalkorValue::FString("Iterator".to_string())]), + FalkorValue::FArray(vec![FalkorValue::FString("My-".to_string())]), + FalkorValue::FArray(vec![FalkorValue::FString("Panic".to_string())]), + ]) + } + + #[test] + fn test_update_map() { + let mut map_to_update = HashMap::from([(5, "Ye Olde Value".to_string())]); + let res = update_map(&mut map_to_update, get_test_keys(), None); + assert!(res.is_ok()); + + let relevant_ids = res.unwrap(); + assert!(relevant_ids.is_none()); + + assert_eq!(map_to_update.get(&0), Some(&"Hello".to_string())); + assert_eq!(map_to_update.get(&1), Some(&"Iterator".to_string())); + assert_eq!(map_to_update.get(&2), Some(&"My-".to_string())); + assert_eq!(map_to_update.get(&3), Some(&"Panic".to_string())); + + assert_eq!(map_to_update.get(&5), None); + } + + #[test] + fn test_update_map_with_relevant_hashmap() { + let mut map_to_update = HashMap::new(); + let res = update_map( + &mut map_to_update, + get_test_keys(), + Some(&HashSet::from([2, 3, 0])), + ); + assert!(res.is_ok()); + + let relevant_hashmap = res.unwrap(); + assert!(relevant_hashmap.is_some()); + + let relevant_hashmap = relevant_hashmap.unwrap(); + assert_eq!(relevant_hashmap.get(&0), Some(&"Hello".to_string())); + assert_eq!(relevant_hashmap.get(&2), Some(&"My-".to_string())); + assert_eq!(relevant_hashmap.get(&3), Some(&"Panic".to_string())); + + assert_eq!(relevant_hashmap.get(&1), None); + } + + #[test] + fn test_update_no_relevant_ids_still_success() { + let mut map_to_update = HashMap::new(); + let res = update_map( + &mut map_to_update, + get_test_keys(), + Some(&HashSet::from([2, 5, 0])), + ); + assert!(res.is_ok()); + + let relevant_hashmap = res.unwrap(); + assert!(relevant_hashmap.is_none()); + } + + #[test] + fn test_get_relevant_hashmap() { + let hashset = HashSet::from([2, 1, 3, 0]); + let locked_map = HashMap::from([ + (0, "Hello".to_string()), + (1, "Darkness".to_string()), + (2, "My".to_string()), + (3, "Old".to_string()), + (4, "Friend".to_string()), + ]); + let res = get_relevant_hashmap(&hashset, &locked_map); + assert!(res.is_some()); + + let relevant_hashmap = res.unwrap(); + assert_eq!(relevant_hashmap.get(&0), Some(&"Hello".to_string())); + assert_eq!(relevant_hashmap.get(&1), Some(&"Darkness".to_string())); + assert_eq!(relevant_hashmap.get(&2), Some(&"My".to_string())); + assert_eq!(relevant_hashmap.get(&3), Some(&"Old".to_string())); + + // Was not in the requested hashset: + assert_eq!(relevant_hashmap.get(&4), None); + } + + #[test] + fn test_no_relevant_hashmap() { + let hashset = HashSet::from([2, 1, 5, 0]); + let locked_map = HashMap::from([ + (0, "Hello".to_string()), + (2, "My".to_string()), + (5, "Old".to_string()), + (4, "Friend".to_string()), + ]); + let res = get_relevant_hashmap(&hashset, &locked_map); + assert!(res.is_none()) + } +} diff --git a/src/lib.rs b/src/lib.rs index eec3d71..6bce612 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,6 +28,7 @@ pub use parser::FalkorParsable; pub use response::{ constraint::{Constraint, ConstraintStatus, ConstraintType}, execution_plan::ExecutionPlan, + index::{FalkorIndex, IndexFieldType, IndexStatus}, query_result::QueryResult, slowlog_entry::SlowlogEntry, ResponseVariant, diff --git a/src/response/constraint.rs b/src/response/constraint.rs index b7ae3c8..d41ac3a 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -135,7 +135,7 @@ impl FalkorParsable for Constraint { let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = parsed_values.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let constraint_type = constraint_type_raw.into_string()?.try_into()?; - let label = label_raw.into_string()?; + let label = label_raw.try_into()?; let entity_type = entity_type_raw.into_string()?.try_into()?; let status = status_raw.into_string()?.try_into()?; diff --git a/src/response/index.rs b/src/response/index.rs new file mode 100644 index 0000000..3d3e3e0 --- /dev/null +++ b/src/response/index.rs @@ -0,0 +1,148 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use crate::connection::blocking::BorrowedSyncConnection; +use crate::value::utils::{parse_type, type_val_from_value}; +use crate::{ + value::parse_vec, EntityType, FalkorDBError, FalkorParsable, FalkorValue, SyncGraphSchema, +}; +use anyhow::Result; +use std::collections::HashMap; + +/// The status of this index +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum IndexStatus { + /// This index is active. + Active, + /// This index is still being created. + Pending, +} + +impl TryFrom<&str> for IndexStatus { + type Error = FalkorDBError; + + fn try_from(value: &str) -> anyhow::Result { + Ok(match value.to_uppercase().as_str() { + "OPERATIONAL" => Self::Active, + "UNDER CONSTRUCTION" => Self::Pending, + _ => Err(FalkorDBError::IndexStatus)?, + }) + } +} + +impl TryFrom for IndexStatus { + type Error = FalkorDBError; + + fn try_from(value: String) -> anyhow::Result { + value.as_str().try_into() + } +} + +impl TryFrom<&String> for IndexStatus { + type Error = FalkorDBError; + + fn try_from(value: &String) -> anyhow::Result { + value.as_str().try_into() + } +} + +/// The type of this indexed field +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum IndexFieldType { + /// This index is a number + Range, + /// This index is raw vector data + Vector, + /// This field is a string + Fulltext, +} + +impl TryFrom<&str> for IndexFieldType { + type Error = FalkorDBError; + + fn try_from(value: &str) -> anyhow::Result { + Ok(match value.to_uppercase().as_str() { + "RANGE" => Self::Range, + "VECTOR" => Self::Vector, + "FULLTEXT" => Self::Fulltext, + _ => Err(FalkorDBError::IndexFieldType)?, + }) + } +} + +impl TryFrom for IndexFieldType { + type Error = FalkorDBError; + + fn try_from(value: String) -> Result { + value.as_str().try_into() + } +} + +impl TryFrom<&String> for IndexFieldType { + type Error = FalkorDBError; + + fn try_from(value: &String) -> Result { + value.as_str().try_into() + } +} + +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(IndexFieldType::try_from(field_type.into_string()?)?); + } + + out_map.insert(key, field_types); + } + + Ok(out_map) +} + +#[derive(Clone, Debug, PartialEq)] +pub struct FalkorIndex { + pub entity_type: EntityType, + pub status: IndexStatus, + pub index_label: String, + pub fields: Vec, + pub field_types: HashMap>, + pub language: String, + pub stopwords: Vec, + pub info: HashMap, +} + +impl FalkorParsable for FalkorIndex { + fn from_falkor_value( + value: FalkorValue, + graph_schema: &SyncGraphSchema, + conn: &mut BorrowedSyncConnection, + ) -> Result { + let value_vec = value.into_vec()?; + let mut semi_parsed_items = Vec::with_capacity(value_vec.len()); + for item in value_vec { + let (type_marker, val) = type_val_from_value(item)?; + semi_parsed_items.push(parse_type(type_marker, val, graph_schema, conn)?); + } + + let [label, fields, field_types, language, stopwords, entity_type, status, info]: [FalkorValue; 8] = semi_parsed_items.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + Ok(Self { + entity_type: EntityType::try_from(entity_type.into_string()?)?, + status: IndexStatus::try_from(status.into_string()?)?, + 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()?, + }) + } +} diff --git a/src/response/mod.rs b/src/response/mod.rs index c29ba80..c9cce3c 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -5,11 +5,13 @@ use constraint::Constraint; use execution_plan::ExecutionPlan; +use index::FalkorIndex; use query_result::QueryResult; use slowlog_entry::SlowlogEntry; pub(crate) mod constraint; pub(crate) mod execution_plan; +pub(crate) mod index; pub(crate) mod query_result; pub(crate) mod slowlog_entry; @@ -21,4 +23,5 @@ pub enum ResponseVariant { QueryResult(QueryResult), SlowlogEntry(SlowlogEntry), Constraints(Vec), + Indices(Vec), } diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index 0674f9c..f3775f5 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -149,7 +149,7 @@ impl crate::FalkorAsyncParseable for Node { } /// An edge in the graph, representing a relationship between two [`Node`]s. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Edge { /// The internal entity ID pub entity_id: i64, diff --git a/src/value/map.rs b/src/value/map.rs index d5e1d0e..48acad4 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -102,13 +102,30 @@ impl FalkorParsable for HashMap { conn: &mut BorrowedSyncConnection, ) -> Result { let val_vec = value.into_vec()?; + if val_vec.len() % 2 != 0 { + Err(FalkorDBError::ParsingFMap)?; + } + + let mut new_map = HashMap::with_capacity(val_vec.len() / 2); + for pair in val_vec.chunks_exact(2) { + let [key, val]: [FalkorValue; 2] = pair + .to_vec() + .try_into() + .map_err(|_| FalkorDBError::ParsingFMap)?; + + let [type_marker, val]: [FalkorValue; 2] = val + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingFMap)?; - let mut new_map = HashMap::with_capacity(val_vec.len()); - for val in val_vec { - let fktv = FKeyTypeVal::try_from(val)?; new_map.insert( - fktv.key.to_string(), - parse_type(fktv.type_marker, fktv.val, graph_schema, conn)?, + key.into_string()?, + parse_type( + type_marker.to_i64().ok_or(FalkorDBError::ParsingKTVTypes)?, + val, + graph_schema, + conn, + )?, ); } diff --git a/src/value/mod.rs b/src/value/mod.rs index be6742a..28866d3 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -34,7 +34,6 @@ pub enum FalkorValue { Int64(i64), F64(f64), FPoint(Point), - FVector(Vec), FPath(Path), None, } @@ -82,6 +81,95 @@ where } } +impl TryFrom for Vec { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::FArray(val) => Ok(val), + _ => Err(FalkorDBError::ParsingFArray), + } + } +} + +impl TryFrom for f64 { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::FString(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) -> Result { + match value { + FalkorValue::FString(val) => Ok(val), + _ => Err(FalkorDBError::ParsingFString), + } + } +} + +impl TryFrom for Edge { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::FEdge(edge) => Ok(edge), + _ => Err(FalkorDBError::ParsingFEdge), + } + } +} + +impl TryFrom for Node { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::FNode(node) => Ok(node), + _ => Err(FalkorDBError::ParsingFNode), + } + } +} + +impl TryFrom for Path { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::FPath(path) => Ok(path), + _ => Err(FalkorDBError::ParsingFPath), + } + } +} + +impl TryFrom for HashMap { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::FMap(map) => Ok(map), + _ => Err(FalkorDBError::ParsingFMap), + } + } +} + +impl TryFrom for Point { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::FPoint(point) => Ok(point), + _ => Err(FalkorDBError::ParsingFPoint), + } + } +} + impl FalkorParsable for FalkorValue { fn from_falkor_value( value: FalkorValue, @@ -103,7 +191,6 @@ impl crate::FalkorAsyncParseable for FalkorValue { } } -// By-reference conversions impl FalkorValue { /// Returns a reference to the internal [`Vec`] if this is an FArray variant. /// @@ -181,92 +268,7 @@ impl FalkorValue { _ => None, } } -} -// Consuming conversions -// TODO: convert these to TryFrom/TryInto traits? -impl FalkorValue { - /// Consumes this variant and returns the underlying [`Vec`] if this is an FArray variant - /// - /// # Returns - /// The inner [`Vec`] - pub fn into_vec(self) -> Result, FalkorDBError> { - match self { - FalkorValue::FArray(val) => Some(val), - _ => None, - } - .ok_or(FalkorDBError::ParsingFArray) - } - - /// Consumes this variant and returns the underlying [`String`] if this is an FString variant - /// - /// # Returns - /// The inner [`String`] - pub fn into_string(self) -> Result { - match self { - FalkorValue::FString(val) => Ok(val), - _ => Err(FalkorDBError::ParsingFString), - } - } - - /// Consumes this variant and returns the underlying [`Edge`] if this is an FEdge variant - /// - /// # Returns - /// The inner [`Edge`] - pub fn into_edge(self) -> Result { - match self { - Self::FEdge(edge) => Ok(edge), - _ => Err(FalkorDBError::ParsingFEdge), - } - } - - /// Consumes this variant and returns the underlying [`Node`] if this is an FNode variant - /// - /// # Returns - /// The inner [`Node`] - pub fn into_node(self) -> Result { - match self { - Self::FNode(node) => Ok(node), - _ => Err(FalkorDBError::ParsingFNode), - } - } - - /// Consumes this variant and returns the underlying [`Path`] if this is an FPath variant - /// - /// # Returns - /// The inner [`Path`] - pub fn into_path(self) -> Result { - match self { - Self::FPath(path) => Ok(path), - _ => Err(FalkorDBError::ParsingFPath), - } - } - - /// Consumes this variant and returns the underlying [`HashMap`] if this is an FMap variant - /// - /// # Returns - /// The inner [`HashMap`] - pub fn into_map(self) -> Result, FalkorDBError> { - match self { - FalkorValue::FMap(map) => Ok(map), - _ => Err(FalkorDBError::ParsingFMap), - } - } - - /// Consumes this variant and returns the underlying [`Point`] if this is an FPoint variant - /// - /// # Returns - /// The inner [`Point`] - pub fn into_point(self) -> Result { - match self { - Self::FPoint(point) => Ok(point), - _ => Err(FalkorDBError::ParsingFPoint), - } - } -} - -// For types implementing Copy -impl FalkorValue { /// Returns a Copy of the inner [`i64`] if this is an Int64 variant /// /// # Returns @@ -285,6 +287,11 @@ impl FalkorValue { pub fn to_bool(&self) -> Option { match self { FalkorValue::FBool(val) => Some(*val), + FalkorValue::FString(bool_str) => match bool_str.as_str() { + "true" => Some(true), + "false" => Some(false), + _ => None, + }, _ => None, } } @@ -299,4 +306,33 @@ impl FalkorValue { _ => None, } } + + /// Consumes itself and returns the inner [`Vec`] if this is an FArray variant + /// + /// # Returns + /// The inner [`Vec`] + pub fn into_vec(self) -> Result, FalkorDBError> { + self.try_into() + } + + /// Consumes itself and returns the inner [`String`] if this is an FString variant + /// + /// # Returns + /// The inner [`String`] + pub fn into_string(self) -> Result { + self.try_into() + } +} + +pub(crate) fn parse_vec>( + value: FalkorValue +) -> Result, FalkorDBError> { + let val_vec = value.into_vec()?; + + let mut out_vec = Vec::with_capacity(val_vec.len()); + for element in val_vec { + out_vec.push(element.try_into()?); + } + + Ok(out_vec) } diff --git a/src/value/utils.rs b/src/value/utils.rs index 0b2970f..3d4f3fd 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -3,8 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::BorrowedSyncConnection; -use crate::{FalkorDBError, FalkorParsable, FalkorValue, Point, SchemaType, SyncGraphSchema}; +use crate::{ + connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, FalkorValue, + Point, SchemaType, SyncGraphSchema, +}; use anyhow::Result; use std::collections::HashSet; @@ -65,7 +67,7 @@ pub(crate) fn parse_type( 2 => FalkorValue::FString(val.into_string()?), 3 => FalkorValue::Int64(val.to_i64().ok_or(FalkorDBError::ParsingI64)?), 4 => FalkorValue::FBool(val.to_bool().ok_or(FalkorDBError::ParsingBool)?), - 5 => FalkorValue::F64(val.to_f64().ok_or(FalkorDBError::ParsingF64)?), + 5 => FalkorValue::F64(val.try_into()?), 6 => FalkorValue::FArray({ let val = val.into_vec()?; let mut parsed_vec = Vec::with_capacity(val.len()); @@ -86,3 +88,261 @@ pub(crate) fn parse_type( Ok(res) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::create_test_client; + use std::collections::HashMap; + + fn open_readonly_graph() -> (SyncGraphSchema, BorrowedSyncConnection) { + let client = create_test_client(); + let schema = client.open_graph("imdb").graph_schema.clone(); + let conn = client + .borrow_connection() + .expect("Could not borrow_connection"); + + { + let write_lock = schema.properties(); + *write_lock.write() = HashMap::from([ + (0, "age".to_string()), + (1, "is_boring".to_string()), + (2, "something_else".to_string()), + (3, "secs_since_login".to_string()), + ]); + } + + { + let write_lock = schema.labels(); + *write_lock.write() = + HashMap::from([(0, "much".to_string()), (1, "actor".to_string())]); + } + + { + let write_lock = schema.relationships(); + *write_lock.write() = HashMap::from([(0, "very".to_string()), (1, "wow".to_string())]); + } + + (schema, conn) + } + #[test] + fn test_parse_edge() { + let (schema, mut conn) = open_readonly_graph(); + + let res = parse_type( + 7, + FalkorValue::FArray(vec![ + FalkorValue::Int64(100), // edge id + FalkorValue::Int64(0), // edge type + FalkorValue::Int64(51), // src node + FalkorValue::Int64(52), // dst node + FalkorValue::FArray(vec![ + FalkorValue::FArray(vec![ + FalkorValue::Int64(0), + FalkorValue::Int64(3), + FalkorValue::Int64(20), + ]), + FalkorValue::FArray(vec![ + FalkorValue::Int64(1), + FalkorValue::Int64(4), + FalkorValue::FBool(false), + ]), + ]), + ]), + &schema, + &mut conn, + ); + assert!(res.is_ok()); + + let falkor_edge = res.unwrap(); + + let FalkorValue::FEdge(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::Int64(20))); + assert_eq!( + edge.properties.get("is_boring"), + Some(&FalkorValue::FBool(false)) + ); + } + + #[test] + fn test_parse_node() { + let (schema, mut conn) = open_readonly_graph(); + + let res = parse_type( + 8, + FalkorValue::FArray(vec![ + FalkorValue::Int64(51), // node id + FalkorValue::FArray(vec![FalkorValue::Int64(0), FalkorValue::Int64(1)]), // node type + FalkorValue::FArray(vec![ + FalkorValue::FArray(vec![ + FalkorValue::Int64(0), + FalkorValue::Int64(3), + FalkorValue::Int64(15), + ]), + FalkorValue::FArray(vec![ + FalkorValue::Int64(2), + FalkorValue::Int64(2), + FalkorValue::FString("the something".to_string()), + ]), + FalkorValue::FArray(vec![ + FalkorValue::Int64(3), + FalkorValue::Int64(5), + FalkorValue::F64(105.5), + ]), + ]), + ]), + &schema, + &mut conn, + ); + assert!(res.is_ok()); + + let falkor_node = res.unwrap(); + let FalkorValue::FNode(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::Int64(15))); + assert_eq!( + node.properties.get("something_else"), + Some(&FalkorValue::FString("the something".to_string())) + ); + assert_eq!( + node.properties.get(&"secs_since_login".to_string()), + Some(&FalkorValue::F64(105.5)) + ); + } + + #[test] + fn test_parse_path() { + let (schema, mut conn) = open_readonly_graph(); + + let res = parse_type( + 9, + FalkorValue::FArray(vec![ + FalkorValue::FArray(vec![ + FalkorValue::FArray(vec![ + FalkorValue::Int64(51), + FalkorValue::FArray(vec![FalkorValue::Int64(0)]), + FalkorValue::FArray(vec![]), + ]), + FalkorValue::FArray(vec![ + FalkorValue::Int64(52), + FalkorValue::FArray(vec![FalkorValue::Int64(0)]), + FalkorValue::FArray(vec![]), + ]), + FalkorValue::FArray(vec![ + FalkorValue::Int64(53), + FalkorValue::FArray(vec![FalkorValue::Int64(0)]), + FalkorValue::FArray(vec![]), + ]), + ]), + FalkorValue::FArray(vec![ + FalkorValue::FArray(vec![ + FalkorValue::Int64(100), + FalkorValue::Int64(0), + FalkorValue::Int64(51), + FalkorValue::Int64(52), + FalkorValue::FArray(vec![]), + ]), + FalkorValue::FArray(vec![ + FalkorValue::Int64(101), + FalkorValue::Int64(1), + FalkorValue::Int64(52), + FalkorValue::Int64(53), + FalkorValue::FArray(vec![]), + ]), + ]), + ]), + &schema, + &mut conn, + ); + assert!(res.is_ok()); + + let falkor_path = res.unwrap(); + let FalkorValue::FPath(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 (schema, mut conn) = open_readonly_graph(); + + let res = parse_type( + 10, + FalkorValue::FArray(vec![ + FalkorValue::FString("key0".to_string()), + FalkorValue::FArray(vec![ + FalkorValue::Int64(2), + FalkorValue::FString("val0".to_string()), + ]), + FalkorValue::FString("key1".to_string()), + FalkorValue::FArray(vec![FalkorValue::Int64(3), FalkorValue::Int64(1)]), + FalkorValue::FString("key2".to_string()), + FalkorValue::FArray(vec![FalkorValue::Int64(4), FalkorValue::FBool(true)]), + ]), + &schema, + &mut conn, + ); + assert!(res.is_ok()); + + let falkor_map = res.unwrap(); + let FalkorValue::FMap(map) = falkor_map else { + panic!("Is not of type map") + }; + + assert_eq!(map.len(), 3); + assert_eq!( + map.get("key0"), + Some(&FalkorValue::FString("val0".to_string())) + ); + assert_eq!(map.get("key1"), Some(&FalkorValue::Int64(1))); + assert_eq!(map.get("key2"), Some(&FalkorValue::FBool(true))); + } + + #[test] + fn test_parse_point() { + let (schema, mut conn) = open_readonly_graph(); + + let res = parse_type( + 11, + FalkorValue::FArray(vec![FalkorValue::F64(102.0), FalkorValue::F64(15.2)]), + &schema, + &mut conn, + ); + assert!(res.is_ok()); + + let falkor_point = res.unwrap(); + let FalkorValue::FPoint(point) = falkor_point else { + panic!("Is not of type point") + }; + assert_eq!(point.latitude, 102.0); + assert_eq!(point.longitude, 15.2); + } +} From 43df2087e7bc8bf96b69925941bc01fd5ab47209 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Sun, 26 May 2024 16:41:20 +0300 Subject: [PATCH 15/62] Fixed tests --- src/graph/blocking.rs | 49 +++++++++++++++++++++++++++++-------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index b727f09..2acfe89 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -277,7 +277,7 @@ impl SyncGraph { /// Run a query which calls a procedure on the graph, read-only, or otherwise. /// Read-only queries are more limited with the operations they are allowed to perform. - /// This function allows adding extra parameters after the query, and adding a YIELD block afterwards + /// This function allows adding extra parameters after the query, and adding a YIELD block afterward /// /// # Arguments /// * `procedure`: The procedure to call @@ -341,6 +341,7 @@ impl SyncGraph { entity_type: EntityType, label: L, properties: &[P], + options: Option<&HashMap>, ) -> Result { // Create index from these properties let properties_string = properties @@ -356,18 +357,27 @@ impl SyncGraph { let idx_type = match index_field_type { IndexFieldType::Range => "", - IndexFieldType::Vector => "VECTOR", - IndexFieldType::Fulltext => "FULLTEXT", + IndexFieldType::Vector => "VECTOR ", + IndexFieldType::Fulltext => "FULLTEXT ", } .to_string(); - self.query( - format!( - "CREATE {idx_type} INDEX FOR {pattern} ON ({})", - properties_string - ), - None, - ) + let options_string = options + .map(|hashmap| { + hashmap + .into_iter() + .map(|(key, val)| format!("'{key}':'{val}'")) + .collect::>() + .join(",") + }) + .map(|options_string| format!(" OPTIONS {{ {} }}", options_string)) + .unwrap_or_default(); + + let full_query = format!( + "CREATE {idx_type}INDEX FOR {pattern} ON ({}){}", + properties_string, options_string + ); + self.query(full_query, None) } pub fn drop_index( @@ -463,12 +473,17 @@ impl SyncGraph { /// * `properties`: A slice of the names of properties this constraint will apply to. pub fn create_unique_constraint( &self, - index_field_type: IndexFieldType, entity_type: EntityType, label: String, properties: &[P], ) -> Result { - self.create_index(index_field_type, entity_type, label.as_str(), properties)?; + self.create_index( + IndexFieldType::Range, + entity_type, + label.as_str(), + properties, + None, + )?; let mut params: Vec = Vec::with_capacity(5 + properties.len()); params.extend([ @@ -515,8 +530,7 @@ impl SyncGraph { #[cfg(test)] mod tests { use super::*; - use crate::test_utils::open_test_graph; - use crate::IndexFieldType; + use crate::{test_utils::open_test_graph, IndexFieldType}; #[test] fn test_create_drop_index() { @@ -526,6 +540,7 @@ mod tests { EntityType::Node, "actor".to_string(), &["Hello"], + None, ); assert!(res.is_ok()); @@ -534,6 +549,10 @@ mod tests { let indices = res.unwrap(); assert_eq!(indices.len(), 2); + assert_eq!( + indices[0].field_types["Hello"], + vec![IndexFieldType::Fulltext] + ); let res = graph.inner.drop_index( IndexFieldType::Fulltext, @@ -591,7 +610,6 @@ mod tests { graph .inner .create_unique_constraint( - IndexFieldType::Fulltext, EntityType::Node, "actor".to_string(), &["first_name", "last_name"], @@ -616,7 +634,6 @@ mod tests { graph .inner .create_unique_constraint( - IndexFieldType::Fulltext, EntityType::Node, "actor".to_string(), &["first_name", "last_name"], From a972d41c3a946703cec621926ffe87860f88c3bd Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Sun, 26 May 2024 16:45:08 +0300 Subject: [PATCH 16/62] merge uses --- src/error/mod.rs | 2 +- src/graph/blocking.rs | 39 ++++++++++++++++++++------------------- src/lib.rs | 2 +- src/response/index.rs | 32 +++++++++++++++----------------- src/value/mod.rs | 13 ------------- src/value/utils.rs | 13 +++++++++++++ 6 files changed, 50 insertions(+), 51 deletions(-) diff --git a/src/error/mod.rs b/src/error/mod.rs index dd15a96..76c3ba5 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -62,5 +62,5 @@ pub enum FalkorDBError { #[error("Invalid Index status, expected 'OPERATIONAL' or 'UNDER CONSTRUCTION'")] IndexStatus, #[error("Invalid Index field type, expected 'RANGE', 'VECTOR' or 'FULLTEXT'")] - IndexFieldType, + IndexType, } diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 2acfe89..5b59db7 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -7,7 +7,7 @@ use super::utils::{construct_query, generate_procedure_call}; use crate::{ client::blocking::FalkorSyncClientInner, connection::blocking::FalkorSyncConnection, Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorDBError, FalkorIndex, - FalkorParsable, FalkorValue, IndexFieldType, QueryResult, SlowlogEntry, SyncGraphSchema, + FalkorParsable, FalkorValue, IndexType, QueryResult, SlowlogEntry, SyncGraphSchema, }; use anyhow::Result; use std::{ @@ -337,7 +337,7 @@ impl SyncGraph { pub fn create_index( &self, - index_field_type: IndexFieldType, + index_field_type: IndexType, entity_type: EntityType, label: L, properties: &[P], @@ -356,9 +356,9 @@ impl SyncGraph { }; let idx_type = match index_field_type { - IndexFieldType::Range => "", - IndexFieldType::Vector => "VECTOR ", - IndexFieldType::Fulltext => "FULLTEXT ", + IndexType::Range => "", + IndexType::Vector => "VECTOR ", + IndexType::Fulltext => "FULLTEXT ", } .to_string(); @@ -380,9 +380,13 @@ impl SyncGraph { self.query(full_query, None) } + /// Drop an existing index, by specifying its type, entity, label and specific properties + /// + /// # Arguments + /// * `index_field_type` pub fn drop_index( &self, - index_field_type: IndexFieldType, + index_field_type: IndexType, entity_type: EntityType, label: L, properties: &[P], @@ -399,9 +403,9 @@ impl SyncGraph { }; let idx_type = match index_field_type { - IndexFieldType::Range => "", - IndexFieldType::Vector => "VECTOR", - IndexFieldType::Fulltext => "FULLTEXT", + IndexType::Range => "", + IndexType::Vector => "VECTOR", + IndexType::Fulltext => "FULLTEXT", } .to_string(); @@ -478,7 +482,7 @@ impl SyncGraph { properties: &[P], ) -> Result { self.create_index( - IndexFieldType::Range, + IndexType::Range, entity_type, label.as_str(), properties, @@ -530,13 +534,13 @@ impl SyncGraph { #[cfg(test)] mod tests { use super::*; - use crate::{test_utils::open_test_graph, IndexFieldType}; + use crate::{test_utils::open_test_graph, IndexType}; #[test] fn test_create_drop_index() { let graph = open_test_graph("test_create_drop_index"); let res = graph.inner.create_index( - IndexFieldType::Fulltext, + IndexType::Fulltext, EntityType::Node, "actor".to_string(), &["Hello"], @@ -549,13 +553,10 @@ mod tests { let indices = res.unwrap(); assert_eq!(indices.len(), 2); - assert_eq!( - indices[0].field_types["Hello"], - vec![IndexFieldType::Fulltext] - ); + assert_eq!(indices[0].field_types["Hello"], vec![IndexType::Fulltext]); let res = graph.inner.drop_index( - IndexFieldType::Fulltext, + IndexType::Fulltext, EntityType::Node, "actor".to_string(), &["Hello"], @@ -577,8 +578,8 @@ mod tests { assert_eq!( indices[0].field_types, HashMap::from([ - ("age".to_string(), vec![IndexFieldType::Range]), - ("name".to_string(), vec![IndexFieldType::Fulltext]) + ("age".to_string(), vec![IndexType::Range]), + ("name".to_string(), vec![IndexType::Fulltext]) ]) ); } diff --git a/src/lib.rs b/src/lib.rs index 6bce612..94f3175 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -28,7 +28,7 @@ pub use parser::FalkorParsable; pub use response::{ constraint::{Constraint, ConstraintStatus, ConstraintType}, execution_plan::ExecutionPlan, - index::{FalkorIndex, IndexFieldType, IndexStatus}, + index::{FalkorIndex, IndexStatus, IndexType}, query_result::QueryResult, slowlog_entry::SlowlogEntry, ResponseVariant, diff --git a/src/response/index.rs b/src/response/index.rs index 3d3e3e0..e7c7803 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -3,10 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::BorrowedSyncConnection; -use crate::value::utils::{parse_type, type_val_from_value}; use crate::{ - value::parse_vec, EntityType, FalkorDBError, FalkorParsable, FalkorValue, SyncGraphSchema, + connection::blocking::BorrowedSyncConnection, + value::utils::{parse_type, parse_vec, type_val_from_value}, + EntityType, FalkorDBError, FalkorParsable, FalkorValue, SyncGraphSchema, }; use anyhow::Result; use std::collections::HashMap; @@ -23,7 +23,7 @@ pub enum IndexStatus { impl TryFrom<&str> for IndexStatus { type Error = FalkorDBError; - fn try_from(value: &str) -> anyhow::Result { + fn try_from(value: &str) -> Result { Ok(match value.to_uppercase().as_str() { "OPERATIONAL" => Self::Active, "UNDER CONSTRUCTION" => Self::Pending, @@ -35,7 +35,7 @@ impl TryFrom<&str> for IndexStatus { impl TryFrom for IndexStatus { type Error = FalkorDBError; - fn try_from(value: String) -> anyhow::Result { + fn try_from(value: String) -> Result { value.as_str().try_into() } } @@ -43,14 +43,14 @@ impl TryFrom for IndexStatus { impl TryFrom<&String> for IndexStatus { type Error = FalkorDBError; - fn try_from(value: &String) -> anyhow::Result { + fn try_from(value: &String) -> Result { value.as_str().try_into() } } /// The type of this indexed field #[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum IndexFieldType { +pub enum IndexType { /// This index is a number Range, /// This index is raw vector data @@ -59,20 +59,20 @@ pub enum IndexFieldType { Fulltext, } -impl TryFrom<&str> for IndexFieldType { +impl TryFrom<&str> for IndexType { type Error = FalkorDBError; - fn try_from(value: &str) -> anyhow::Result { + fn try_from(value: &str) -> Result { Ok(match value.to_uppercase().as_str() { "RANGE" => Self::Range, "VECTOR" => Self::Vector, "FULLTEXT" => Self::Fulltext, - _ => Err(FalkorDBError::IndexFieldType)?, + _ => Err(FalkorDBError::IndexType)?, }) } } -impl TryFrom for IndexFieldType { +impl TryFrom for IndexType { type Error = FalkorDBError; fn try_from(value: String) -> Result { @@ -80,7 +80,7 @@ impl TryFrom for IndexFieldType { } } -impl TryFrom<&String> for IndexFieldType { +impl TryFrom<&String> for IndexType { type Error = FalkorDBError; fn try_from(value: &String) -> Result { @@ -88,9 +88,7 @@ impl TryFrom<&String> for IndexFieldType { } } -fn parse_types_map( - value: FalkorValue -) -> Result>, FalkorDBError> { +fn parse_types_map(value: FalkorValue) -> Result>, FalkorDBError> { let value: HashMap = value.try_into()?; let mut out_map = HashMap::with_capacity(value.len()); @@ -98,7 +96,7 @@ fn parse_types_map( let val = val.into_vec()?; let mut field_types = Vec::with_capacity(val.len()); for field_type in val { - field_types.push(IndexFieldType::try_from(field_type.into_string()?)?); + field_types.push(IndexType::try_from(field_type.into_string()?)?); } out_map.insert(key, field_types); @@ -113,7 +111,7 @@ pub struct FalkorIndex { pub status: IndexStatus, pub index_label: String, pub fields: Vec, - pub field_types: HashMap>, + pub field_types: HashMap>, pub language: String, pub stopwords: Vec, pub info: HashMap, diff --git a/src/value/mod.rs b/src/value/mod.rs index 28866d3..de19a5e 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -323,16 +323,3 @@ impl FalkorValue { self.try_into() } } - -pub(crate) fn parse_vec>( - value: FalkorValue -) -> Result, FalkorDBError> { - let val_vec = value.into_vec()?; - - let mut out_vec = Vec::with_capacity(val_vec.len()); - for element in val_vec { - out_vec.push(element.try_into()?); - } - - Ok(out_vec) -} diff --git a/src/value/utils.rs b/src/value/utils.rs index 3d4f3fd..2245310 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -89,6 +89,19 @@ pub(crate) fn parse_type( Ok(res) } +pub(crate) fn parse_vec>( + value: FalkorValue +) -> Result, FalkorDBError> { + let val_vec = value.into_vec()?; + + let mut out_vec = Vec::with_capacity(val_vec.len()); + for element in val_vec { + out_vec.push(element.try_into()?); + } + + Ok(out_vec) +} + #[cfg(test)] mod tests { use super::*; From 2085f1002cf3fba636a2d97db7fbd21abe0a5657 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Sun, 26 May 2024 17:51:32 +0300 Subject: [PATCH 17/62] Start working on async --- src/client/asynchronous.rs | 133 +++++++-- src/client/builder.rs | 8 +- src/connection/asynchronous.rs | 19 +- src/graph/asynchronous.rs | 474 +++++++++++++++++++++++++++---- src/graph/blocking.rs | 5 +- src/graph_schema/asynchronous.rs | 5 +- src/parser/mod.rs | 2 +- src/response/constraint.rs | 47 +++ src/value/graph_entities.rs | 2 +- src/value/map.rs | 4 +- src/value/utils_async.rs | 2 +- 11 files changed, 594 insertions(+), 107 deletions(-) diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index 9722ebe..2d67d1e 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -5,40 +5,99 @@ use crate::{ client::FalkorClientProvider, AsyncGraph, AsyncGraphSchema, ConfigValue, FalkorAsyncConnection, - FalkorDBError, + FalkorConnectionInfo, FalkorDBError, }; use anyhow::Result; -use std::{collections::HashMap, sync::Arc}; +use std::fmt::{Debug, Formatter}; +use std::{ + collections::{HashMap, VecDeque}, + sync::Arc, + time::Duration, +}; use tokio::sync::Mutex; +pub(crate) struct FalkorAsyncClientInner { + _inner: Mutex, + graph_cache: Mutex>, + connection_pool_size: u8, + connection_pool: Mutex>, +} + +impl FalkorAsyncClientInner { + pub(crate) async fn get_connection(&self) -> Result { + let mut conn_pool = self.connection_pool.lock().await; + let connection = conn_pool + .pop_front() + .ok_or(FalkorDBError::EmptyConnection)?; + + let cloned = connection.clone(); + + conn_pool.push_back(connection); + + Ok(cloned) + } +} + +unsafe impl Sync for FalkorAsyncClientInner {} +unsafe impl Send for FalkorAsyncClientInner {} + pub struct FalkorAsyncClient { - _inner: FalkorClientProvider, - connection: FalkorAsyncConnection, - graph_cache: HashMap, + inner: Arc, + _connection_info: FalkorConnectionInfo, +} + +impl Debug for FalkorAsyncClient { + fn fmt( + &self, + f: &mut Formatter<'_>, + ) -> std::fmt::Result { + f.debug_struct("FalkorAsyncClient") + .field("inner", &"") + .field("connection_info", &self._connection_info) + .finish() + } } impl FalkorAsyncClient { - pub(crate) async fn create(client: FalkorClientProvider) -> Result>> { - Ok(Arc::new(Mutex::new(Self { - connection: client.get_async_connection(None).await?, - _inner: client, - graph_cache: Default::default(), - }))) + pub(crate) async fn create( + client: FalkorClientProvider, + connection_info: FalkorConnectionInfo, + num_connections: u8, + timeout: Option, + ) -> Result { + let connection_pool = Mutex::new(VecDeque::with_capacity(num_connections as usize)); + { + let mut connection_pool = connection_pool.lock().await; + for _ in 0..num_connections { + connection_pool.push_back(client.get_async_connection(timeout).await?); + } + } + + Ok(Self { + inner: Arc::new(FalkorAsyncClientInner { + _inner: client.into(), + graph_cache: Default::default(), + connection_pool_size: num_connections, + connection_pool, + }), + _connection_info: connection_info, + }) } - pub(crate) fn clone_connection(&self) -> FalkorAsyncConnection { - self.connection.clone() + pub(crate) async fn get_connection(&self) -> Result { + self.inner.get_connection().await } + /// Return a list of graphs currently residing in the database + /// + /// # Returns + /// A [`Vec`] of [`String`]s, containing the names of available graphs pub async fn list_graphs(&self) -> Result> { - let conn = self.clone_connection(); - let graph_list = match conn { + let graph_list = match self.get_connection().await? { #[cfg(feature = "redis")] FalkorAsyncConnection::Redis(mut redis_conn) => { - let res = match redis::cmd("GRAPH.LIST") - .query_async(&mut redis_conn) - .await? - { - redis::Value::Bulk(data) => data, + let cmd = redis::cmd("GRAPH.LIST"); + let res = match redis_conn.send_packed_command(&cmd).await { + Ok(redis::Value::Bulk(data)) => data, _ => Err(FalkorDBError::InvalidDataReceived)?, }; @@ -61,11 +120,19 @@ impl FalkorAsyncClient { Ok(graph_list) } + /// Return the current value of a configuration option in the database. + /// + /// # Arguments + /// * `config_Key`: A [`String`] representation of a configuration's key. + /// The config key can also be "*", which will return ALL the configuration options. + /// + /// # Returns + /// A [`HashMap`] comprised of [`String`] keys, and [`ConfigValue`] values. pub async fn config_get( &self, config_key: T, ) -> Result> { - let conn = self.clone_connection(); + let conn = self.get_connection().await?; Ok(match conn { #[cfg(feature = "redis")] FalkorAsyncConnection::Redis(mut redis_conn) => { @@ -117,12 +184,18 @@ impl FalkorAsyncClient { }) } + /// Return the current value of a configuration option in the database. + /// + /// # Arguments + /// * `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. pub async fn config_set, C: Into>( &self, config_key: T, value: C, ) -> Result<()> { - let conn = self.clone_connection(); + let conn = self.get_connection().await?; match conn { #[cfg(feature = "redis")] FalkorAsyncConnection::Redis(mut redis_conn) => { @@ -140,15 +213,25 @@ impl FalkorAsyncClient { Ok(()) } - pub fn open_graph( - &mut self, + /// Opens a graph context for queries and operations + /// + /// # Arguments + /// * `graph_name`: A string identifier of the graph to open. + /// + /// # Returns + /// a [`SyncGraph`] object, allowing various graph operations. + pub async fn open_graph( + &self, graph_name: T, ) -> AsyncGraph { AsyncGraph { - connection: self.connection.clone(), + client: self.inner.clone(), graph_name: graph_name.to_string(), graph_schema: self + .inner .graph_cache + .lock() + .await .entry(graph_name.to_string()) .or_insert(AsyncGraphSchema::new(graph_name.to_string())) .clone(), diff --git a/src/client/builder.rs b/src/client/builder.rs index 28a2396..dea56eb 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -134,7 +134,13 @@ impl FalkorClientBuilder<'A'> { .connection_info .unwrap_or("falkor://127.0.0.1:6379".try_into()?); - crate::FalkorAsyncClient::create(get_client(connection_info)?).await + crate::FalkorAsyncClient::create( + get_client(connection_info)?, + connection_info, + self.num_connections, + self.timeout, + ) + .await } } diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs index 7e83624..eeac27d 100644 --- a/src/connection/asynchronous.rs +++ b/src/connection/asynchronous.rs @@ -20,20 +20,21 @@ impl FalkorAsyncConnection { graph_name: Option, command: &str, subcommand: Option<&str>, - params: Option, + params: Option<&[String]>, ) -> Result { Ok(match self { #[cfg(feature = "redis")] FalkorAsyncConnection::Redis(redis_conn) => { + let mut cmd = redis::cmd(command); + cmd.arg(subcommand); + cmd.arg(graph_name); + if let Some(params) = params { + for param in params { + cmd.arg(param); + } + } redis::FromRedisValue::from_owned_redis_value( - redis_conn - .send_packed_command( - redis::cmd(command) - .arg(subcommand) - .arg(graph_name) - .arg(params), - ) - .await?, + redis_conn.send_packed_command(&cmd).await?, )? } }) diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index 0271b28..e2d04fe 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -4,17 +4,30 @@ */ use super::utils::{construct_query, generate_procedure_call}; +use crate::client::asynchronous::FalkorAsyncClientInner; +use crate::connection::blocking::FalkorSyncConnection; use crate::{ - AsyncGraphSchema, ExecutionPlan, FalkorAsyncConnection, FalkorAsyncParseable, FalkorDBError, - FalkorValue, QueryResult, SlowlogEntry, + AsyncGraphSchema, Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorAsyncClient, + FalkorAsyncConnection, FalkorAsyncParseable, FalkorDBError, FalkorIndex, FalkorParsable, + FalkorValue, IndexType, QueryResult, SlowlogEntry, }; use anyhow::Result; use std::collections::HashMap; +use std::fmt::Display; +use std::sync::Arc; +/// The main graph API, this allows the user to perform graph operations while exposing as little details as possible. +/// +/// # Thread Safety +/// This struct is fully thread safe, it can be cloned and passed within threads without constraints, +/// Its API uses only immutable references +#[derive(Clone)] pub struct AsyncGraph { + pub(crate) client: Arc, pub(crate) graph_name: String, - pub(crate) connection: FalkorAsyncConnection, - pub(crate) graph_schema: AsyncGraphSchema, + /// Provides user with access to the current graph schema, + /// which contains a safe cache of id to labels/properties/relationship maps + pub graph_schema: AsyncGraphSchema, } impl AsyncGraph { @@ -26,20 +39,29 @@ impl AsyncGraph { &self, command: &str, subcommand: Option<&str>, - params: Option, + params: Option<&[String]>, ) -> Result { - let mut conn = self.connection.clone(); + let mut conn = self.client.get_connection().await?; conn.send_command(Some(self.graph_name.clone()), command, subcommand, params) .await } + /// Deletes the graph stored in the database, and drop all the schema caches. + /// NOTE: This still maintains the graph API, operations are still viable. pub async fn delete(&self) -> Result<()> { - self.send_command("GRAPH.DELETE", None).await?; + self.send_command("GRAPH.DELETE", None, None).await?; Ok(()) } + /// Retrieves the slowlog data, which contains info about the N slowest queries. + /// + /// # Returns + /// A [`Vec`] of [`SlowlogEntry`], providing information about each query. pub async fn slowlog(&self) -> Result> { - let res = self.send_command("GRAPH.SLOWLOG", None).await?.into_vec()?; + let res = self + .send_command("GRAPH.SLOWLOG", None, None) + .await? + .into_vec()?; if res.is_empty() { return Ok(vec![]); @@ -58,12 +80,22 @@ impl AsyncGraph { Ok(slowlog_entries) } - pub async fn slowlog_reset(&self) -> Result<()> { - self.send_command("GRAPH.SLOWLOG", Some("RESET".to_string())) - .await?; - Ok(()) + /// Resets the slowlog, all query time data will be cleared. + pub async fn slowlog_reset(&self) -> Result { + self.send_command("GRAPH.SLOWLOG", Some("RESET"), None) + .await } + /// Returns an [`ExecutionPlan`] object for the selected query, + /// showing how long each step took to perform. + /// This function variant allows adding extra parameters after the query + /// + /// # Arguments + /// * `query_string`: The query to profile + /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// + /// # Returns + /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. pub async fn profile_with_params( &self, query_string: Q, @@ -71,10 +103,21 @@ impl AsyncGraph { ) -> Result { let query = construct_query(query_string, params); - ExecutionPlan::try_from(self.send_command("GRAPH.PROFILE", Some(query)).await?) - .map_err(Into::into) + ExecutionPlan::try_from( + self.send_command("GRAPH.PROFILE", None, Some(&[query])) + .await?, + ) + .map_err(Into::into) } + /// Returns an [`ExecutionPlan`] object for the selected query, + /// showing how long each step took to perform. + /// + /// # Arguments + /// * `query_string`: The query to profile + /// + /// # Returns + /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. pub async fn profile( &self, query_string: Q, @@ -83,16 +126,37 @@ impl AsyncGraph { .await } + /// Returns an [`ExecutionPlan`] object for the selected query, + /// showing the internals steps the database will go through to perform the query. + /// This function variant allows adding extra parameters after the query + /// + /// # Arguments + /// * `query_string`: The query to explain + /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// + /// # Returns + /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. pub async fn explain_with_params( &self, query_string: Q, params: Option<&HashMap>, ) -> Result { let query = construct_query(query_string, params); - ExecutionPlan::try_from(self.send_command("GRAPH.EXPLAIN", Some(query)).await?) - .map_err(Into::into) + ExecutionPlan::try_from( + self.send_command("GRAPH.EXPLAIN", None, Some(&[query])) + .await?, + ) + .map_err(Into::into) } + /// Returns an [`ExecutionPlan`] object for the selected query, + /// showing the internals steps the database will go through to perform the query. + /// + /// # Arguments + /// * `query_string`: The query to explain + /// + /// # Returns + /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. pub async fn explain( &self, query_string: Q, @@ -101,91 +165,377 @@ impl AsyncGraph { .await } - pub async fn query_with_params< - Q: ToString, - T: ToString, - Z: ToString, - P: FalkorAsyncParseable, - >( + async fn query_inner( &self, + command: &str, query_string: Q, params: Option<&HashMap>, - readonly: bool, - timeout: Option, + timeout: Option, ) -> Result

{ - self.query_with_parser( - if readonly { - "GRAPH.RO_QUERY" - } else { - "GRAPH.QUERY" - }, - query_string, - params, - timeout, - ) - .await + let query = construct_query(query_string, params); + + let conn = self.client.get_connection().await?; + let falkor_result = match conn.clone() { + #[cfg(feature = "redis")] + FalkorAsyncConnection::Redis(mut redis_conn) => { + use redis::FromRedisValue as _; + let redis_val = redis_conn + .send_packed_command( + redis::cmd(command) + .arg(self.graph_name.as_str()) + .arg(query) + .arg("--compact") + .arg(timeout.map(|timeout| format!("timeout {timeout}"))), + ) + .await?; + FalkorValue::from_owned_redis_value(redis_val)? + } + }; + + P::from_falkor_value_async(falkor_result, &self.graph_schema, conn).await } - pub async fn query( + /// Run a query on the graph + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub async fn query( &self, query_string: Q, - timeout: Option, + timeout: Option, ) -> Result { - self.query_with_params::(query_string, None, false, timeout) + self.query_inner::("GRAPH.QUERY", query_string, None, timeout) .await } - pub async fn query_readonly( + /// Run a query on the graph + /// This function variant allows adding extra parameters after the query + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub async fn query_with_params( &self, query_string: Q, - timeout: Option, + timeout: Option, + params: &HashMap, ) -> Result { - self.query_with_params::(query_string, None, true, timeout) + self.query_inner("GRAPH.QUERY", query_string, Some(params), timeout) .await } + /// Run a query on the graph + /// Read-only queries are more limited with the operations they are allowed to perform. + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub async fn query_readonly( + &self, + query_string: Q, + timeout: Option, + ) -> Result { + self.query_inner::( + "GRAPH.QUERY_RO", + query_string, + None, + timeout, + ) + .await + } + + /// Run a read-only query on the graph + /// Read-only queries are more limited with the operations they are allowed to perform. + /// This function variant allows adding extra parameters after the query + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub async fn query_readonly_with_params( + &self, + query_string: Q, + timeout: Option, + params: Option<&HashMap>, + ) -> Result { + self.query_inner("GRAPH.QUERY_RO", query_string, params, timeout) + .await + } + + /// Run a query which calls a procedure on the graph, read-only, or otherwise. + /// Read-only queries are more limited with the operations they are allowed to perform. + /// This function allows adding extra parameters after the query, and adding a YIELD block afterward + /// + /// # Arguments + /// * `procedure`: The procedure to call + /// * `args`: An optional slice of strings containing the parameters. + /// * `yields`: The optional yield block arguments. + /// * `read_only`: Whether this procedure is read-only. + /// * `timeout`: If provided, the query will abort if overruns the timeout. + /// + /// # Returns + /// A caller-provided type which implements [`FalkorAsyncParseable`] pub async fn call_procedure( &self, procedure: C, - args: Option<&[String]>, - yields: Option<&[String]>, - read_only: Option, - timeout: Option, + args: Option<&[&str]>, + yields: Option<&[&str]>, + read_only: bool, + timeout: Option, ) -> Result

{ let (query_string, params) = generate_procedure_call(procedure, args, yields); - self.query_with_params( + self.query_inner( + if read_only { + "GRAPH.QUERY_RO" + } else { + "GRAPH.QUERY" + }, query_string, params.as_ref(), - read_only.unwrap_or_default(), timeout, ) .await } - pub async fn list_indices(&self) -> Result { - let query_res = self - .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, None, None) + /// Calls the DB.INDICES procedure on the graph, returning all the indexing methods currently used + /// + /// # Returns + /// A [`Vec`] of [`Index`] + pub async fn list_indices(&self) -> Result> { + let mut conn = self.client.get_connection().await?; + let [_, indices, _]: [FalkorValue; 3] = self + .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false, None) .await? - .into_vec()?; + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - for item in query_res { - log::info!("{item:?}"); + let indices = indices.into_vec()?; + + let mut out_vec = Vec::with_capacity(indices.len()); + for index in indices { + out_vec.push( + FalkorIndex::from_falkor_value_async(index, &self.graph_schema, &mut conn) + .expect("Could not parse"), + ); } - Ok(FalkorValue::None) + + Ok(out_vec) } - pub async fn list_constraints(&self) -> Result { - let query_res = self - .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, None, None) + pub async fn create_index( + &self, + index_field_type: IndexType, + entity_type: EntityType, + label: L, + properties: &[P], + options: Option<&HashMap>, + ) -> Result { + // Create index from these properties + let properties_string = properties + .iter() + .map(|element| format!("l.{}", element.to_string())) + .collect::>() + .join(", "); + + let pattern = match entity_type { + EntityType::Node => format!("(l:{})", label.to_string()), + EntityType::Edge => format!("()-[l:{}]->()", label.to_string()), + }; + + let idx_type = match index_field_type { + IndexType::Range => "", + IndexType::Vector => "VECTOR ", + IndexType::Fulltext => "FULLTEXT ", + } + .to_string(); + + let options_string = options + .map(|hashmap| { + hashmap + .into_iter() + .map(|(key, val)| format!("'{key}':'{val}'")) + .collect::>() + .join(",") + }) + .map(|options_string| format!(" OPTIONS {{ {} }}", options_string)) + .unwrap_or_default(); + + let full_query = format!( + "CREATE {idx_type}INDEX FOR {pattern} ON ({}){}", + properties_string, options_string + ); + self.query(full_query, None).await + } + + /// Drop an existing index, by specifying its type, entity, label and specific properties + /// + /// # Arguments + /// * `index_field_type` + pub async fn drop_index( + &self, + index_field_type: IndexType, + entity_type: EntityType, + label: L, + properties: &[P], + ) -> Result { + let properties_string = properties + .iter() + .map(|element| format!("e.{}", element.to_string())) + .collect::>() + .join(", "); + + let pattern = match entity_type { + EntityType::Node => format!("(e:{})", label.to_string()), + EntityType::Edge => format!("()-[e:{}]->()", label.to_string()), + }; + + let idx_type = match index_field_type { + IndexType::Range => "", + IndexType::Vector => "VECTOR", + IndexType::Fulltext => "FULLTEXT", + } + .to_string(); + + self.query( + format!( + "DROP {idx_type} INDEX for {pattern} ON ({})", + properties_string + ), + None, + ) + .await + } + + /// Calls the DB.CONSTRAINTS procedure on the graph, returning an array of the graph's constraints + /// + /// # Returns + /// A [`Vec`] of [`Constraint`]s + pub async fn list_constraints(&self) -> Result> { + let mut conn = self.client.get_connection().await?; + let [_, query_res, _]: [FalkorValue; 3] = self + .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, false, None) .await? - .into_vec()?; + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + let query_res = query_res.into_vec()?; + let mut constraints_vec = Vec::with_capacity(query_res.len()); for item in query_res { - for sub_item in item.into_vec()? { - log::info!("{sub_item:?}"); - } + constraints_vec.push(Constraint::from_falkor_value_async( + item, + &self.graph_schema, + &mut conn, + )?); } - Ok(FalkorValue::None) + + Ok(constraints_vec) + } + + /// Creates a new constraint for this graph, making the provided properties mandatory + /// + /// # Arguments + /// * `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. + pub async fn create_mandatory_constraint( + &self, + entity_type: EntityType, + label: L, + properties: &[P], + ) -> Result { + let mut params = Vec::with_capacity(5 + properties.len()); + params.extend([ + "MANDATORY".to_string(), + entity_type.to_string(), + label.to_string(), + "PROPERTIES".to_string(), + properties.len().to_string(), + ]); + params.extend(properties.iter().map(|property| property.to_string())); + + self.send_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) + .await + } + + /// Creates a new constraint for this graph, making the provided properties unique + /// + /// # Arguments + /// * `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. + pub async fn create_unique_constraint( + &self, + entity_type: EntityType, + label: String, + properties: &[P], + ) -> Result { + self.create_index( + IndexType::Range, + entity_type, + label.as_str(), + properties, + None, + ) + .await?; + + let mut params: Vec = Vec::with_capacity(5 + properties.len()); + params.extend([ + "UNIQUE".to_string(), + entity_type.to_string(), + label.to_string(), + "PROPERTIES".to_string(), + properties.len().to_string(), + ]); + params.extend(properties.into_iter().map(|property| property.to_string())); + + // create constraint using index + self.send_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) + .await + } + + /// Drop an existing constraint from the graph + /// + /// # Arguments + /// * `constraint_type`: Which type of constraint to remove. + /// * `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. + pub async fn drop_constraint( + &self, + constraint_type: ConstraintType, + entity_type: EntityType, + label: L, + properties: &[P], + ) -> Result { + let mut params = Vec::with_capacity(5 + properties.len()); + params.extend([ + constraint_type.to_string(), + entity_type.to_string(), + label.to_string(), + "PROPERTIES".to_string(), + properties.len().to_string(), + ]); + params.extend(properties.iter().map(|property| property.to_string())); + + self.send_command("GRAPH.CONSTRAINT", Some("DROP"), Some(params.as_slice())) + .await } } diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 5b59db7..519899f 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -93,9 +93,8 @@ impl SyncGraph { } /// Resets the slowlog, all query time data will be cleared. - pub fn slowlog_reset(&self) -> Result<()> { - self.send_command("GRAPH.SLOWLOG", None, Some(&["RESET".to_string()]))?; - Ok(()) + pub fn slowlog_reset(&self) -> Result { + self.send_command("GRAPH.SLOWLOG", None, Some(&["RESET".to_string()])) } /// Returns an [`ExecutionPlan`] object for the selected query, diff --git a/src/graph_schema/asynchronous.rs b/src/graph_schema/asynchronous.rs index 8dc45f1..d8ed5cc 100644 --- a/src/graph_schema/asynchronous.rs +++ b/src/graph_schema/asynchronous.rs @@ -77,7 +77,7 @@ impl AsyncGraphSchema { pub(crate) async fn refresh( &self, schema_type: SchemaType, - conn: &mut FalkorAsyncConnection, + mut conn: FalkorAsyncConnection, id_hashset: Option<&HashSet>, ) -> Result>> { let command = get_refresh_command(schema_type); @@ -94,7 +94,8 @@ impl AsyncGraphSchema { .send_command( Some(self.graph_name.clone()), "GRAPH.QUERY", - Some(format!("CALL {command}()")), + None, + Some(&[format!("CALL {command}()")]), ) .await? .into_vec()? diff --git a/src/parser/mod.rs b/src/parser/mod.rs index a740cd3..6703cc3 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -20,6 +20,6 @@ pub trait FalkorAsyncParseable: Sized { fn from_falkor_value_async( value: FalkorValue, graph_schema: &crate::AsyncGraphSchema, - conn: &mut crate::FalkorAsyncConnection, + conn: crate::FalkorAsyncConnection, ) -> impl std::future::Future> + Send; } diff --git a/src/response/constraint.rs b/src/response/constraint.rs index d41ac3a..f0cb99e 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -154,3 +154,50 @@ impl FalkorParsable for Constraint { }) } } + +#[cfg(feature = "tokio")] +impl crate::FalkorAsyncParseable for Constraint { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &crate::AsyncGraphSchema, + conn: crate::FalkorAsyncConnection, + ) -> Result { + let value_vec = value.into_vec()?; + + let mut parsed_values = Vec::with_capacity(value_vec.len()); + for column_raw in value_vec { + parsed_values.push(type_val_from_value(column_raw).and_then( + |(type_marker, raw_val)| { + crate::value::utils_async::parse_type_async( + type_marker, + raw_val, + graph_schema, + conn.clone(), + ) + .await + }, + )?); + } + + let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = parsed_values.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + let constraint_type = constraint_type_raw.into_string()?.try_into()?; + let label = label_raw.try_into()?; + let entity_type = entity_type_raw.into_string()?.try_into()?; + let status = status_raw.into_string()?.try_into()?; + + let properties_vec = properties_raw.into_vec()?; + let mut properties = Vec::with_capacity(properties_vec.len()); + for property in properties_vec { + properties.push(property.into_string()?); + } + + Ok(Constraint { + constraint_type, + label, + properties, + entity_type, + status, + }) + } +} diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index f3775f5..c7731ce 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -220,7 +220,7 @@ impl crate::FalkorAsyncParseable for Edge { async fn from_falkor_value_async( value: FalkorValue, graph_schema: &crate::AsyncGraphSchema, - conn: &mut crate::FalkorAsyncConnection, + conn: crate::FalkorAsyncConnection, ) -> Result { let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value .into_vec()? diff --git a/src/value/map.rs b/src/value/map.rs index 48acad4..fde46a4 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -138,7 +138,7 @@ async fn ktv_vec_to_map_async( map_vec: Vec, relevant_ids_map: HashMap, graph_schema: &crate::AsyncGraphSchema, - conn: &mut crate::FalkorAsyncConnection, + conn: crate::FalkorAsyncConnection, ) -> Result> { let mut new_map = HashMap::with_capacity(map_vec.len()); for fktv in map_vec { @@ -158,7 +158,7 @@ async fn ktv_vec_to_map_async( pub(crate) async fn parse_map_with_schema_async( value: FalkorValue, graph_schema: &crate::AsyncGraphSchema, - conn: &mut crate::FalkorAsyncConnection, + conn: crate::FalkorAsyncConnection, schema_type: SchemaType, ) -> Result> { let val_vec = value.into_vec()?; diff --git a/src/value/utils_async.rs b/src/value/utils_async.rs index a04c0ab..a6a504b 100644 --- a/src/value/utils_async.rs +++ b/src/value/utils_async.rs @@ -57,7 +57,7 @@ pub(crate) async fn parse_type_async( type_marker: i64, val: FalkorValue, graph_schema: &AsyncGraphSchema, - conn: &mut FalkorAsyncConnection, + conn: FalkorAsyncConnection, ) -> Result { let res = match type_marker { 1 => FalkorValue::None, From 3fa48e61416722b76fcc4888022873ddc8a79354 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Mon, 27 May 2024 10:12:47 +0300 Subject: [PATCH 18/62] Matched the blocking API --- Cargo.toml | 2 +- src/client/asynchronous.rs | 30 +++++++++++++++++++------- src/client/builder.rs | 6 ++---- src/graph/asynchronous.rs | 32 +++++++++++++-------------- src/graph/blocking.rs | 9 ++++---- src/graph_schema/asynchronous.rs | 2 +- src/response/constraint.rs | 21 +++++++++--------- src/response/index.rs | 37 ++++++++++++++++++++++++++++++++ src/response/query_result.rs | 7 +++--- src/value/graph_entities.rs | 6 +++--- src/value/map.rs | 8 +++---- src/value/mod.rs | 2 +- src/value/path.rs | 8 ++++--- src/value/utils_async.rs | 5 +++-- 14 files changed, 113 insertions(+), 62 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c4174da..13ee1fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ tracing = { version = "0.1.40", default-features = false, features = ["std", "at url-parse = "1.0.8" [features] -default = ["redis"] +default = ["redis", "tokio"] native-tls = ["redis/tls-native-tls"] rustls = ["redis/tls-rustls"] tokio = ["dep:tokio", "redis/tokio-comp"] diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index 2d67d1e..c61ea51 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -8,9 +8,9 @@ use crate::{ FalkorConnectionInfo, FalkorDBError, }; use anyhow::Result; -use std::fmt::{Debug, Formatter}; use std::{ collections::{HashMap, VecDeque}, + fmt::{Debug, Formatter}, sync::Arc, time::Duration, }; @@ -238,14 +238,28 @@ impl FalkorAsyncClient { } } - pub async fn copy_graph( - &mut self, - cloned_graph_name: T, + /// Copies an entire graph and returns the [`SyncGraph`] for the new copied graph. + /// + /// # Arguments + /// * `graph_to_clone`: A string identifier of the graph to copy. + /// * `new_graph_name`: The name to give the new graph. + /// + /// # Returns + /// If successful, will return the new [`SyncGraph`] object. + pub async fn copy_graph( + &self, + graph_to_clone: T, + new_graph_name: Z, ) -> Result { - self.connection - .clone() - .send_command(Some(cloned_graph_name.to_string()), "GRAPH.COPY", None) + self.get_connection() + .await? + .send_command( + Some(graph_to_clone.to_string()), + "GRAPH.COPY", + None, + Some(&[new_graph_name.to_string()]), + ) .await?; - Ok(self.open_graph(cloned_graph_name)) + Ok(self.open_graph(new_graph_name.to_string()).await) } } diff --git a/src/client/builder.rs b/src/client/builder.rs index dea56eb..2858272 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -127,15 +127,13 @@ impl FalkorClientBuilder<'A'> { } } - pub async fn build( - self - ) -> Result>> { + pub async fn build(self) -> Result { let connection_info = self .connection_info .unwrap_or("falkor://127.0.0.1:6379".try_into()?); crate::FalkorAsyncClient::create( - get_client(connection_info)?, + get_client(connection_info.clone())?, connection_info, self.num_connections, self.timeout, diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index e2d04fe..9bdd05a 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -4,17 +4,13 @@ */ use super::utils::{construct_query, generate_procedure_call}; -use crate::client::asynchronous::FalkorAsyncClientInner; -use crate::connection::blocking::FalkorSyncConnection; use crate::{ - AsyncGraphSchema, Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorAsyncClient, - FalkorAsyncConnection, FalkorAsyncParseable, FalkorDBError, FalkorIndex, FalkorParsable, - FalkorValue, IndexType, QueryResult, SlowlogEntry, + client::asynchronous::FalkorAsyncClientInner, AsyncGraphSchema, Constraint, ConstraintType, + EntityType, ExecutionPlan, FalkorAsyncConnection, FalkorAsyncParseable, FalkorDBError, + FalkorIndex, FalkorValue, IndexType, QueryResult, SlowlogEntry, }; use anyhow::Result; -use std::collections::HashMap; -use std::fmt::Display; -use std::sync::Arc; +use std::{collections::HashMap, fmt::Display, sync::Arc}; /// The main graph API, this allows the user to perform graph operations while exposing as little details as possible. /// @@ -31,6 +27,10 @@ pub struct AsyncGraph { } impl AsyncGraph { + /// Returns the name of the graph for which this API performs operations. + /// + /// # Returns + /// The graph name as a string slice, without cloning. pub fn graph_name(&self) -> &str { self.graph_name.as_str() } @@ -317,7 +317,7 @@ impl AsyncGraph { /// # Returns /// A [`Vec`] of [`Index`] pub async fn list_indices(&self) -> Result> { - let mut conn = self.client.get_connection().await?; + let conn = self.client.get_connection().await?; let [_, indices, _]: [FalkorValue; 3] = self .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false, None) .await? @@ -330,8 +330,8 @@ impl AsyncGraph { let mut out_vec = Vec::with_capacity(indices.len()); for index in indices { out_vec.push( - FalkorIndex::from_falkor_value_async(index, &self.graph_schema, &mut conn) - .expect("Could not parse"), + FalkorIndex::from_falkor_value_async(index, &self.graph_schema, conn.clone()) + .await?, ); } @@ -427,7 +427,7 @@ impl AsyncGraph { /// # Returns /// A [`Vec`] of [`Constraint`]s pub async fn list_constraints(&self) -> Result> { - let mut conn = self.client.get_connection().await?; + let conn = self.client.get_connection().await?; let [_, query_res, _]: [FalkorValue; 3] = self .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, false, None) .await? @@ -439,11 +439,9 @@ impl AsyncGraph { let mut constraints_vec = Vec::with_capacity(query_res.len()); for item in query_res { - constraints_vec.push(Constraint::from_falkor_value_async( - item, - &self.graph_schema, - &mut conn, - )?); + constraints_vec.push( + Constraint::from_falkor_value_async(item, &self.graph_schema, conn.clone()).await?, + ); } Ok(constraints_vec) diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 519899f..c3e93b3 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -325,10 +325,11 @@ impl SyncGraph { let mut out_vec = Vec::with_capacity(indices.len()); for index in indices { - out_vec.push( - FalkorIndex::from_falkor_value(index, &self.graph_schema, &mut conn) - .expect("Could not parse"), - ); + out_vec.push(FalkorIndex::from_falkor_value( + index, + &self.graph_schema, + &mut conn, + )?); } Ok(out_vec) diff --git a/src/graph_schema/asynchronous.rs b/src/graph_schema/asynchronous.rs index d8ed5cc..132def9 100644 --- a/src/graph_schema/asynchronous.rs +++ b/src/graph_schema/asynchronous.rs @@ -102,6 +102,6 @@ impl AsyncGraphSchema { .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - update_map(write_lock.deref_mut(), keys, id_hashset) + Ok(update_map(write_lock.deref_mut(), keys, id_hashset)?) } } diff --git a/src/response/constraint.rs b/src/response/constraint.rs index f0cb99e..b5bfabe 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -166,17 +166,16 @@ impl crate::FalkorAsyncParseable for Constraint { let mut parsed_values = Vec::with_capacity(value_vec.len()); for column_raw in value_vec { - parsed_values.push(type_val_from_value(column_raw).and_then( - |(type_marker, raw_val)| { - crate::value::utils_async::parse_type_async( - type_marker, - raw_val, - graph_schema, - conn.clone(), - ) - .await - }, - )?); + let (type_marker, val) = type_val_from_value(column_raw)?; + parsed_values.push( + crate::value::utils_async::parse_type_async( + type_marker, + val, + graph_schema, + conn.clone(), + ) + .await?, + ); } let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = parsed_values.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; diff --git a/src/response/index.rs b/src/response/index.rs index e7c7803..9bbc108 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -144,3 +144,40 @@ impl FalkorParsable for FalkorIndex { }) } } + +#[cfg(feature = "tokio")] +impl crate::FalkorAsyncParseable for FalkorIndex { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &crate::AsyncGraphSchema, + conn: crate::FalkorAsyncConnection, + ) -> Result { + let value_vec = value.into_vec()?; + let mut semi_parsed_items = Vec::with_capacity(value_vec.len()); + for item in value_vec { + let (type_marker, val) = type_val_from_value(item)?; + semi_parsed_items.push( + crate::value::utils_async::parse_type_async( + type_marker, + val, + graph_schema, + conn.clone(), + ) + .await?, + ); + } + + let [label, fields, field_types, language, stopwords, entity_type, status, info]: [FalkorValue; 8] = semi_parsed_items.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + Ok(Self { + entity_type: EntityType::try_from(entity_type.into_string()?)?, + status: IndexStatus::try_from(status.into_string()?)?, + 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()?, + }) + } +} diff --git a/src/response/query_result.rs b/src/response/query_result.rs index 9bea92d..3387083 100644 --- a/src/response/query_result.rs +++ b/src/response/query_result.rs @@ -153,7 +153,7 @@ impl FalkorParsable for QueryResult { async fn parse_result_set_async( data_vec: Vec, graph_schema: &crate::AsyncGraphSchema, - conn: &mut crate::FalkorAsyncConnection, + conn: crate::FalkorAsyncConnection, header_keys: &[String], ) -> Result>> { let mut parsed_result_set = Vec::with_capacity(data_vec.len()); @@ -163,7 +163,8 @@ async fn parse_result_set_async( let mut parsed_column = Vec::with_capacity(column_vec.len()); for column_item in column_vec { let (type_marker, val) = type_val_from_value(column_item)?; - parsed_column.push(parse_type_async(type_marker, val, graph_schema, conn).await?); + parsed_column + .push(parse_type_async(type_marker, val, graph_schema, conn.clone()).await?); } parsed_result_set.push(header_keys.iter().cloned().zip(parsed_column).collect()) @@ -177,7 +178,7 @@ impl crate::FalkorAsyncParseable for QueryResult { async fn from_falkor_value_async( value: FalkorValue, graph_schema: &crate::AsyncGraphSchema, - conn: &mut crate::FalkorAsyncConnection, + conn: crate::FalkorAsyncConnection, ) -> Result { let value_vec = value.into_vec()?; diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index c7731ce..a08eb3f 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -115,7 +115,7 @@ impl crate::FalkorAsyncParseable for Node { async fn from_falkor_value_async( value: FalkorValue, graph_schema: &crate::AsyncGraphSchema, - conn: &mut crate::FalkorAsyncConnection, + conn: crate::FalkorAsyncConnection, ) -> Result { let [entity_id, labels, properties]: [FalkorValue; 3] = value .into_vec()? @@ -133,7 +133,7 @@ impl crate::FalkorAsyncParseable for Node { } let parsed_labels = - parse_labels_async(labels, graph_schema, conn, SchemaType::Labels).await?; + parse_labels_async(labels, graph_schema, conn.clone(), SchemaType::Labels).await?; Ok(Node { entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, labels: parsed_labels, @@ -253,7 +253,7 @@ impl crate::FalkorAsyncParseable for Edge { match graph_schema .refresh( SchemaType::Relationships, - conn, + conn.clone(), Some(&HashSet::from([relation])), ) .await? diff --git a/src/value/map.rs b/src/value/map.rs index fde46a4..e47867e 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -147,7 +147,7 @@ async fn ktv_vec_to_map_async( .get(&fktv.key) .cloned() .ok_or(FalkorDBError::ParsingError)?, - parse_type_async(fktv.type_marker, fktv.val, graph_schema, conn).await?, + parse_type_async(fktv.type_marker, fktv.val, graph_schema, conn.clone()).await?, ); } @@ -179,7 +179,7 @@ pub(crate) async fn parse_map_with_schema_async( // If we reached here, schema validation failed and we need to refresh our schema match graph_schema - .refresh(schema_type, conn, Some(&id_hashset)) + .refresh(schema_type, conn.clone(), Some(&id_hashset)) .await? { Some(relevant_ids_map) => { @@ -194,7 +194,7 @@ impl crate::FalkorAsyncParseable for HashMap { async fn from_falkor_value_async( value: FalkorValue, graph_schema: &crate::AsyncGraphSchema, - conn: &mut crate::FalkorAsyncConnection, + conn: crate::FalkorAsyncConnection, ) -> Result { let val_vec = value.into_vec()?; @@ -203,7 +203,7 @@ impl crate::FalkorAsyncParseable for HashMap { let fktv = FKeyTypeVal::try_from(val)?; new_map.insert( fktv.key.to_string(), - parse_type_async(fktv.type_marker, fktv.val, graph_schema, conn).await?, + parse_type_async(fktv.type_marker, fktv.val, graph_schema, conn.clone()).await?, ); } diff --git a/src/value/mod.rs b/src/value/mod.rs index de19a5e..5654fdf 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -185,7 +185,7 @@ impl crate::FalkorAsyncParseable for FalkorValue { async fn from_falkor_value_async( value: FalkorValue, _graph_schema: &crate::AsyncGraphSchema, - _conn: &mut crate::FalkorAsyncConnection, + _conn: crate::FalkorAsyncConnection, ) -> Result { Ok(value) } diff --git a/src/value/path.rs b/src/value/path.rs index 7c1a2ea..550b4ad 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -58,7 +58,7 @@ impl crate::FalkorAsyncParseable for Path { async fn from_falkor_value_async( value: FalkorValue, graph_schema: &crate::AsyncGraphSchema, - conn: &mut crate::FalkorAsyncConnection, + conn: crate::FalkorAsyncConnection, ) -> Result { let [nodes, relationships]: [FalkorValue; 2] = value .into_vec()? @@ -68,12 +68,14 @@ impl crate::FalkorAsyncParseable for Path { let mut parsed_nodes = Vec::with_capacity(nodes.len()); for node_raw in nodes { - parsed_nodes.push(Node::from_falkor_value_async(node_raw, graph_schema, conn).await?); + parsed_nodes + .push(Node::from_falkor_value_async(node_raw, graph_schema, conn.clone()).await?); } let mut parsed_edges = Vec::with_capacity(relationships.len()); for edge_raw in relationships { - parsed_edges.push(Edge::from_falkor_value_async(edge_raw, graph_schema, conn).await?); + parsed_edges + .push(Edge::from_falkor_value_async(edge_raw, graph_schema, conn.clone()).await?); } Ok(Path { diff --git a/src/value/utils_async.rs b/src/value/utils_async.rs index a6a504b..edc8745 100644 --- a/src/value/utils_async.rs +++ b/src/value/utils_async.rs @@ -15,7 +15,7 @@ use std::collections::{HashMap, HashSet}; pub(crate) async fn parse_labels_async( raw_ids: Vec, graph_schema: &AsyncGraphSchema, - conn: &mut FalkorAsyncConnection, + conn: FalkorAsyncConnection, schema_type: SchemaType, ) -> Result> { let mut ids_hashset = HashSet::with_capacity(raw_ids.len()); @@ -70,7 +70,8 @@ pub(crate) async fn parse_type_async( let mut parsed_vec = Vec::with_capacity(val.len()); for item in val { let (type_marker, val) = type_val_from_value(item)?; - parsed_vec.push(parse_type_async(type_marker, val, graph_schema, conn).await?); + parsed_vec + .push(parse_type_async(type_marker, val, graph_schema, conn.clone()).await?); } parsed_vec }), From 828cda1b9f71b6ea76a7feba25ef5f0e729f99d5 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Mon, 27 May 2024 11:13:37 +0300 Subject: [PATCH 19/62] Weird things in async --- src/client/asynchronous.rs | 209 ++++++++++++++++++++++++++----- src/client/blocking.rs | 4 +- src/client/builder.rs | 31 +++++ src/connection/asynchronous.rs | 118 ++++++++--------- src/graph/asynchronous.rs | 23 ++-- src/graph_schema/asynchronous.rs | 7 +- src/lib.rs | 22 +++- src/parser/mod.rs | 7 +- src/response/constraint.rs | 19 ++- src/response/index.rs | 20 +-- src/response/query_result.rs | 18 +-- src/value/graph_entities.rs | 22 ++-- src/value/map.rs | 30 ++--- src/value/mod.rs | 11 +- src/value/path.rs | 17 +-- src/value/utils_async.rs | 13 +- 16 files changed, 386 insertions(+), 185 deletions(-) diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index c61ea51..32b7f61 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -4,8 +4,8 @@ */ use crate::{ - client::FalkorClientProvider, AsyncGraph, AsyncGraphSchema, ConfigValue, FalkorAsyncConnection, - FalkorConnectionInfo, FalkorDBError, + client::FalkorClientProvider, connection::asynchronous::BorrowedAsyncConnection, AsyncGraph, + AsyncGraphSchema, ConfigValue, FalkorAsyncConnection, FalkorConnectionInfo, FalkorDBError, }; use anyhow::Result; use std::{ @@ -20,21 +20,20 @@ pub(crate) struct FalkorAsyncClientInner { _inner: Mutex, graph_cache: Mutex>, connection_pool_size: u8, - connection_pool: Mutex>, + connection_pool: Arc>>, } impl FalkorAsyncClientInner { - pub(crate) async fn get_connection(&self) -> Result { + pub(crate) async fn borrow_connection(&self) -> Result { let mut conn_pool = self.connection_pool.lock().await; let connection = conn_pool .pop_front() .ok_or(FalkorDBError::EmptyConnection)?; - let cloned = connection.clone(); - - conn_pool.push_back(connection); - - Ok(cloned) + Ok(BorrowedAsyncConnection { + conn: Some(connection), + conn_pool: self.connection_pool.clone(), + }) } } @@ -65,7 +64,9 @@ impl FalkorAsyncClient { num_connections: u8, timeout: Option, ) -> Result { - let connection_pool = Mutex::new(VecDeque::with_capacity(num_connections as usize)); + let connection_pool = Arc::new(Mutex::new(VecDeque::with_capacity( + num_connections as usize, + ))); { let mut connection_pool = connection_pool.lock().await; for _ in 0..num_connections { @@ -83,8 +84,14 @@ impl FalkorAsyncClient { _connection_info: connection_info, }) } - pub(crate) async fn get_connection(&self) -> Result { - self.inner.get_connection().await + + /// Get the max number of connections in the client's connection pool + pub fn connection_pool_size(&self) -> u8 { + self.inner.connection_pool_size + } + + pub(crate) async fn borrow_connection(&self) -> Result { + self.inner.borrow_connection().await } /// Return a list of graphs currently residing in the database @@ -92,9 +99,10 @@ impl FalkorAsyncClient { /// # Returns /// A [`Vec`] of [`String`]s, containing the names of available graphs pub async fn list_graphs(&self) -> Result> { - let graph_list = match self.get_connection().await? { + let mut conn = self.borrow_connection().await?; + let graph_list = match conn.as_inner()? { #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(mut redis_conn) => { + FalkorAsyncConnection::Redis(redis_conn) => { let cmd = redis::cmd("GRAPH.LIST"); let res = match redis_conn.send_packed_command(&cmd).await { Ok(redis::Value::Bulk(data)) => data, @@ -132,10 +140,10 @@ impl FalkorAsyncClient { &self, config_key: T, ) -> Result> { - let conn = self.get_connection().await?; - Ok(match conn { + let mut conn = self.borrow_connection().await?; + Ok(match conn.as_inner()? { #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(mut redis_conn) => { + FalkorAsyncConnection::Redis(redis_conn) => { let bulk_data = match redis_conn .send_packed_command( redis::cmd("GRAPH.CONFIG") @@ -145,19 +153,28 @@ impl FalkorAsyncClient { .await? { redis::Value::Bulk(bulk_data) => bulk_data, - _ => return Err(FalkorDBError::InvalidDataReceived.into()), + _ => { + return Err(FalkorDBError::InvalidDataReceived)?; + } }; if bulk_data.is_empty() { - return Err(FalkorDBError::InvalidDataReceived.into()); + Err(FalkorDBError::InvalidDataReceived)?; } else if bulk_data.len() == 2 { - return if let Some(redis::Value::Status(config_key)) = bulk_data.first() { + return if let Some(value) = bulk_data.first() { + let str_key = match value { + redis::Value::Data(bytes_data) => { + String::from_utf8_lossy(bytes_data.as_slice()).to_string() + } + redis::Value::Status(status_data) => status_data.to_owned(), + _ => Err(FalkorDBError::InvalidDataReceived)?, + }; Ok(HashMap::from([( - config_key.to_string(), + str_key.to_string(), ConfigValue::try_from(&bulk_data[1])?, )])) } else { - Err(FalkorDBError::InvalidDataReceived.into()) + Err(FalkorDBError::InvalidDataReceived)? }; } @@ -195,10 +212,10 @@ impl FalkorAsyncClient { config_key: T, value: C, ) -> Result<()> { - let conn = self.get_connection().await?; - match conn { + let mut conn = self.borrow_connection().await?; + match conn.as_inner()? { #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(mut redis_conn) => { + FalkorAsyncConnection::Redis(redis_conn) => { redis_conn .send_packed_command( redis::cmd("GRAPH.CONFIG") @@ -219,7 +236,7 @@ impl FalkorAsyncClient { /// * `graph_name`: A string identifier of the graph to open. /// /// # Returns - /// a [`SyncGraph`] object, allowing various graph operations. + /// a [`AsyncGraph`] object, allowing various graph operations. pub async fn open_graph( &self, graph_name: T, @@ -238,20 +255,20 @@ impl FalkorAsyncClient { } } - /// Copies an entire graph and returns the [`SyncGraph`] for the new copied graph. + /// Copies an entire graph and returns the [`AsyncGraph`] for the new copied graph. /// /// # Arguments /// * `graph_to_clone`: A string identifier of the graph to copy. /// * `new_graph_name`: The name to give the new graph. /// /// # Returns - /// If successful, will return the new [`SyncGraph`] object. + /// If successful, will return the new [`AsyncGraph`] object. pub async fn copy_graph( &self, graph_to_clone: T, new_graph_name: Z, ) -> Result { - self.get_connection() + self.borrow_connection() .await? .send_command( Some(graph_to_clone.to_string()), @@ -263,3 +280,137 @@ impl FalkorAsyncClient { Ok(self.open_graph(new_graph_name.to_string()).await) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::create_async_test_client; + use std::{mem, thread}; + + #[tokio::test] + async fn test_list_graphs() { + let client = create_async_test_client().await; + let res = client.list_graphs().await; + assert!(res.is_ok()); + + let graphs = res.unwrap(); + assert_eq!(graphs[0], "imdb"); + } + + #[tokio::test] + async fn test_open_graph_and_query() { + let client = create_async_test_client().await; + + let graph = client.open_graph("imdb").await; + assert_eq!(graph.graph_name(), "imdb".to_string()); + + let res = graph + .query("MATCH (a:actor) return a".to_string(), None) + .await + .expect("Could not get actors from unmodified graph"); + + assert_eq!(res.result_set.len(), 1317); + } + + #[tokio::test] + #[ignore] + async fn test_copy_graph() { + let client = create_async_test_client().await; + + client.open_graph("imdb_ro_copy").await.delete().await.ok(); + + let graph = client + .copy_graph("imdb", "imdb_ro_copy") + .await + .expect("Could not copy graph"); + + let original_graph = client.open_graph("imdb").await; + + assert_eq!( + graph + .query("MATCH (a:actor) RETURN a".to_string(), None) + .await + .expect("Could not get actors from unmodified graph") + .result_set, + original_graph + .query("MATCH (a:actor) RETURN a".to_string(), None) + .await + .expect("Could not get actors from unmodified graph") + .result_set + ) + } + + #[tokio::test] + async fn test_get_config() { + let client = create_async_test_client().await; + + let config = client + .config_get("QUERY_MEM_CAPACITY") + .await + .expect("Could not get configuration"); + + assert_eq!(config.len(), 1); + assert!(config.contains_key("QUERY_MEM_CAPACITY")); + assert_eq!( + mem::discriminant(config.get("QUERY_MEM_CAPACITY").unwrap()), + mem::discriminant(&ConfigValue::Int64(0)) + ); + } + + #[tokio::test] + async fn test_get_config_all() { + let client = create_async_test_client().await; + let configuration = client + .config_get("*") + .await + .expect("Could not get configuration"); + assert_eq!( + configuration.get("THREAD_COUNT").cloned().unwrap(), + ConfigValue::Int64(thread::available_parallelism().unwrap().get() as i64) + ); + } + + #[tokio::test] + async fn test_set_config() { + let client = create_async_test_client().await; + + let config = client + .config_get("DELTA_MAX_PENDING_CHANGES") + .await + .expect("Could not get configuration"); + + let current_val = config + .get("DELTA_MAX_PENDING_CHANGES") + .cloned() + .unwrap() + .as_i64() + .unwrap(); + + let desired_val = if current_val == 10000 { 50000 } else { 10000 }; + + client + .config_set("DELTA_MAX_PENDING_CHANGES", desired_val) + .await + .expect("Could not set config value"); + + let new_config = client + .config_get("DELTA_MAX_PENDING_CHANGES") + .await + .expect("Could not get configuration"); + + assert_eq!( + new_config + .get("DELTA_MAX_PENDING_CHANGES") + .cloned() + .unwrap() + .as_i64() + .unwrap(), + desired_val + ); + + client + .config_set("DELTA_MAX_PENDING_CHANGES", current_val) + .await + .ok(); + } +} diff --git a/src/client/blocking.rs b/src/client/blocking.rs index da6b5ac..a0bf509 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -277,7 +277,7 @@ impl FalkorSyncClient { mod tests { use super::*; use crate::{ - test_utils::{create_test_client, TestGraphHandle}, + test_utils::{create_test_client, TestSyncGraphHandle}, FalkorClientBuilder, }; use std::{mem, sync::mpsc::TryRecvError, thread}; @@ -341,7 +341,7 @@ mod tests { let graph = client.copy_graph("imdb", "imdb_ro_copy"); assert!(graph.is_ok()); - let graph = TestGraphHandle { + let graph = TestSyncGraphHandle { inner: graph.unwrap(), }; diff --git a/src/client/builder.rs b/src/client/builder.rs index 2858272..e0b2f29 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -202,4 +202,35 @@ mod tests { .build(); assert!(impossible_client.is_err()); } + + #[cfg(feature = "redis")] + #[tokio::test] + async fn test_async_builder() { + let conneciton_info = "redis://127.0.0.1:6379".try_into(); + assert!(conneciton_info.is_ok()); + + assert!(FalkorClientBuilder::new_async() + .with_num_connections(4) + .with_connection_info(conneciton_info.unwrap()) + .build() + .await + .is_ok()); + } + + #[tokio::test] + async fn test_async_timeout() { + { + let client = FalkorClientBuilder::new_async() + .with_timeout(Duration::from_millis(100)) + .build() + .await; + assert!(client.is_ok()); + } + + let impossible_client = FalkorClientBuilder::new_async() + .with_timeout(Duration::from_nanos(10)) + .build() + .await; + assert!(impossible_client.is_err()); + } } diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs index eeac27d..1b15d9d 100644 --- a/src/connection/asynchronous.rs +++ b/src/connection/asynchronous.rs @@ -3,10 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::graph::utils::construct_query; -use crate::FalkorValue; +use crate::{FalkorDBError, FalkorValue}; use anyhow::Result; -use std::collections::HashMap; +use std::{collections::VecDeque, sync::Arc}; +use tokio::sync::Mutex; #[derive(Clone)] pub enum FalkorAsyncConnection { @@ -14,7 +14,20 @@ pub enum FalkorAsyncConnection { Redis(redis::aio::MultiplexedConnection), } -impl FalkorAsyncConnection { +/// A container for a connection that is borrowed from the pool. +/// Upon going out of scope, it will return the connection to the pool. +/// +/// This is publicly exposed for user-implementations of [`FalkorParsable`](crate::FalkorParsable) +pub struct BorrowedAsyncConnection { + pub(crate) conn: Option, + pub(crate) conn_pool: Arc>>, +} + +impl BorrowedAsyncConnection { + pub(crate) fn as_inner(&mut self) -> Result<&mut FalkorAsyncConnection, FalkorDBError> { + self.conn.as_mut().ok_or(FalkorDBError::EmptyConnection) + } + pub(crate) async fn send_command( &mut self, graph_name: Option, @@ -22,72 +35,45 @@ impl FalkorAsyncConnection { subcommand: Option<&str>, params: Option<&[String]>, ) -> Result { - Ok(match self { - #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(redis_conn) => { - let mut cmd = redis::cmd(command); - cmd.arg(subcommand); - cmd.arg(graph_name); - if let Some(params) = params { - for param in params { - cmd.arg(param); + Ok( + match self.conn.as_mut().ok_or(FalkorDBError::EmptyConnection)? { + #[cfg(feature = "redis")] + FalkorAsyncConnection::Redis(redis_conn) => { + let mut cmd = redis::cmd(command); + cmd.arg(subcommand); + cmd.arg(graph_name); + if let Some(params) = params { + for param in params { + cmd.arg(param); + } } + + redis::FromRedisValue::from_owned_redis_value( + redis_conn.send_packed_command(&cmd).await?, + )? } - redis::FromRedisValue::from_owned_redis_value( - redis_conn.send_packed_command(&cmd).await?, - )? - } - }) + }, + ) } +} - async fn query_internal( - &mut self, - graph_name: String, - command: &str, - query_string: Q, - params: Option<&HashMap>, - timeout: Option, - ) -> Result { - let query = construct_query(query_string, params); - - Ok(match self { - #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(redis_conn) => { - use redis::FromRedisValue; - let redis_val = redis_conn - .send_packed_command( - redis::cmd(command) - .arg(graph_name.as_str()) - .arg(query) - .arg("--compact") - .arg(timeout.map(|timeout| format!("timeout {timeout}"))), - ) - .await?; - - FalkorValue::from_owned_redis_value(redis_val)? +impl Drop for BorrowedAsyncConnection { + fn drop(&mut self) { + if let Some(conn) = self.conn.take() { + let handle = tokio::runtime::Handle::try_current(); + match handle { + Ok(handle) => { + handle.spawn_blocking({ + let pool = self.conn_pool.clone(); + move || { + pool.blocking_lock().push_back(conn); + } + }); + } + Err(_) => { + self.conn_pool.blocking_lock().push_back(conn); + } } - }) - } - - pub(crate) async fn query( - &mut self, - graph_name: String, - query_string: Q, - params: Option<&HashMap>, - timeout: Option, - ) -> Result { - self.query_internal(graph_name, "GRAPH.QUERY", query_string, params, timeout) - .await - } - - pub(crate) async fn query_readonly( - &mut self, - graph_name: String, - query_string: Q, - params: Option<&HashMap>, - timeout: Option, - ) -> Result { - self.query_internal(graph_name, "GRAPH.QUERY_RO", query_string, params, timeout) - .await + } } } diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index 9bdd05a..fbdfa18 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -41,8 +41,10 @@ impl AsyncGraph { subcommand: Option<&str>, params: Option<&[String]>, ) -> Result { - let mut conn = self.client.get_connection().await?; - conn.send_command(Some(self.graph_name.clone()), command, subcommand, params) + self.client + .borrow_connection() + .await? + .send_command(Some(self.graph_name.clone()), command, subcommand, params) .await } @@ -174,10 +176,10 @@ impl AsyncGraph { ) -> Result

{ let query = construct_query(query_string, params); - let conn = self.client.get_connection().await?; - let falkor_result = match conn.clone() { + let mut conn = self.client.borrow_connection().await?; + let falkor_result = match conn.as_inner()? { #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(mut redis_conn) => { + FalkorAsyncConnection::Redis(redis_conn) => { use redis::FromRedisValue as _; let redis_val = redis_conn .send_packed_command( @@ -192,7 +194,7 @@ impl AsyncGraph { } }; - P::from_falkor_value_async(falkor_result, &self.graph_schema, conn).await + P::from_falkor_value_async(falkor_result, &self.graph_schema, &mut conn).await } /// Run a query on the graph @@ -317,7 +319,7 @@ impl AsyncGraph { /// # Returns /// A [`Vec`] of [`Index`] pub async fn list_indices(&self) -> Result> { - let conn = self.client.get_connection().await?; + let mut conn = self.client.borrow_connection().await?; let [_, indices, _]: [FalkorValue; 3] = self .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false, None) .await? @@ -330,8 +332,7 @@ impl AsyncGraph { let mut out_vec = Vec::with_capacity(indices.len()); for index in indices { out_vec.push( - FalkorIndex::from_falkor_value_async(index, &self.graph_schema, conn.clone()) - .await?, + FalkorIndex::from_falkor_value_async(index, &self.graph_schema, &mut conn).await?, ); } @@ -427,7 +428,7 @@ impl AsyncGraph { /// # Returns /// A [`Vec`] of [`Constraint`]s pub async fn list_constraints(&self) -> Result> { - let conn = self.client.get_connection().await?; + let mut conn = self.client.borrow_connection().await?; let [_, query_res, _]: [FalkorValue; 3] = self .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, false, None) .await? @@ -440,7 +441,7 @@ impl AsyncGraph { let mut constraints_vec = Vec::with_capacity(query_res.len()); for item in query_res { constraints_vec.push( - Constraint::from_falkor_value_async(item, &self.graph_schema, conn.clone()).await?, + Constraint::from_falkor_value_async(item, &self.graph_schema, &mut conn).await?, ); } diff --git a/src/graph_schema/asynchronous.rs b/src/graph_schema/asynchronous.rs index 132def9..daa731d 100644 --- a/src/graph_schema/asynchronous.rs +++ b/src/graph_schema/asynchronous.rs @@ -4,7 +4,10 @@ */ use super::utils::{get_refresh_command, get_relevant_hashmap, update_map}; -use crate::{value::FalkorValue, FalkorAsyncConnection, FalkorDBError, SchemaType}; +use crate::{ + connection::asynchronous::BorrowedAsyncConnection, value::FalkorValue, FalkorDBError, + SchemaType, +}; use anyhow::Result; use std::{ collections::{HashMap, HashSet}, @@ -77,7 +80,7 @@ impl AsyncGraphSchema { pub(crate) async fn refresh( &self, schema_type: SchemaType, - mut conn: FalkorAsyncConnection, + conn: &mut BorrowedAsyncConnection, id_hashset: Option<&HashSet>, ) -> Result>> { let command = get_refresh_command(schema_type); diff --git a/src/lib.rs b/src/lib.rs index 94f3175..bb906bd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -52,11 +52,11 @@ pub use { pub(crate) mod test_utils { use crate::{FalkorClientBuilder, FalkorSyncClient, SyncGraph}; - pub(crate) struct TestGraphHandle { + pub(crate) struct TestSyncGraphHandle { pub(crate) inner: SyncGraph, } - impl Drop for TestGraphHandle { + impl Drop for TestSyncGraphHandle { fn drop(&mut self) { self.inner.delete().ok(); } @@ -68,15 +68,29 @@ pub(crate) mod test_utils { .expect("Could not create client") } - pub(crate) fn open_test_graph(graph_name: &str) -> TestGraphHandle { + pub(crate) fn open_test_graph(graph_name: &str) -> TestSyncGraphHandle { let client = create_test_client(); client.open_graph(graph_name).delete().ok(); - TestGraphHandle { + TestSyncGraphHandle { inner: client .copy_graph("imdb", graph_name) .expect("Could not copy graph for test"), } } + + #[cfg(feature = "tokio")] + pub(crate) async fn create_async_test_client() -> crate::FalkorAsyncClient { + FalkorClientBuilder::new_async() + .build() + .await + .expect("Could not construct client") + } + + #[cfg(feature = "tokio")] + pub(crate) async fn open_test_graph_async(graph_name: &str) -> crate::AsyncGraph { + let client = create_async_test_client().await; + client.open_graph(graph_name).await + } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 6703cc3..0e77fbc 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -6,6 +6,9 @@ use crate::{connection::blocking::BorrowedSyncConnection, FalkorValue, SyncGraphSchema}; use anyhow::Result; +#[cfg(feature = "tokio")] +use crate::{connection::asynchronous::BorrowedAsyncConnection, AsyncGraphSchema}; + /// This trait allows implementing a parser from the table-style result sent by the database, to any other struct pub trait FalkorParsable: Sized { fn from_falkor_value( @@ -19,7 +22,7 @@ pub trait FalkorParsable: Sized { pub trait FalkorAsyncParseable: Sized { fn from_falkor_value_async( value: FalkorValue, - graph_schema: &crate::AsyncGraphSchema, - conn: crate::FalkorAsyncConnection, + graph_schema: &AsyncGraphSchema, + conn: &mut BorrowedAsyncConnection, ) -> impl std::future::Future> + Send; } diff --git a/src/response/constraint.rs b/src/response/constraint.rs index b5bfabe..c4b4d49 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -3,10 +3,12 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use crate::connection::asynchronous::BorrowedAsyncConnection; use crate::{ connection::blocking::BorrowedSyncConnection, value::utils::{parse_type, type_val_from_value}, - EntityType, FalkorDBError, FalkorParsable, FalkorValue, SyncGraphSchema, + AsyncGraphSchema, EntityType, FalkorAsyncParseable, FalkorDBError, FalkorParsable, FalkorValue, + SyncGraphSchema, }; use anyhow::Result; use std::fmt::{Display, Formatter}; @@ -156,11 +158,11 @@ impl FalkorParsable for Constraint { } #[cfg(feature = "tokio")] -impl crate::FalkorAsyncParseable for Constraint { +impl FalkorAsyncParseable for Constraint { async fn from_falkor_value_async( value: FalkorValue, - graph_schema: &crate::AsyncGraphSchema, - conn: crate::FalkorAsyncConnection, + graph_schema: &AsyncGraphSchema, + conn: &mut BorrowedAsyncConnection, ) -> Result { let value_vec = value.into_vec()?; @@ -168,13 +170,8 @@ impl crate::FalkorAsyncParseable for Constraint { for column_raw in value_vec { let (type_marker, val) = type_val_from_value(column_raw)?; parsed_values.push( - crate::value::utils_async::parse_type_async( - type_marker, - val, - graph_schema, - conn.clone(), - ) - .await?, + crate::value::utils_async::parse_type_async(type_marker, val, graph_schema, conn) + .await?, ); } diff --git a/src/response/index.rs b/src/response/index.rs index 9bbc108..93af43d 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -11,6 +11,11 @@ use crate::{ use anyhow::Result; use std::collections::HashMap; +#[cfg(feature = "tokio")] +use crate::{ + connection::asynchronous::BorrowedAsyncConnection, AsyncGraphSchema, FalkorAsyncParseable, +}; + /// The status of this index #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum IndexStatus { @@ -146,24 +151,19 @@ impl FalkorParsable for FalkorIndex { } #[cfg(feature = "tokio")] -impl crate::FalkorAsyncParseable for FalkorIndex { +impl FalkorAsyncParseable for FalkorIndex { async fn from_falkor_value_async( value: FalkorValue, - graph_schema: &crate::AsyncGraphSchema, - conn: crate::FalkorAsyncConnection, + graph_schema: &AsyncGraphSchema, + conn: &mut BorrowedAsyncConnection, ) -> Result { let value_vec = value.into_vec()?; let mut semi_parsed_items = Vec::with_capacity(value_vec.len()); for item in value_vec { let (type_marker, val) = type_val_from_value(item)?; semi_parsed_items.push( - crate::value::utils_async::parse_type_async( - type_marker, - val, - graph_schema, - conn.clone(), - ) - .await?, + crate::value::utils_async::parse_type_async(type_marker, val, graph_schema, conn) + .await?, ); } diff --git a/src/response/query_result.rs b/src/response/query_result.rs index 3387083..0771176 100644 --- a/src/response/query_result.rs +++ b/src/response/query_result.rs @@ -12,7 +12,10 @@ use anyhow::Result; use std::collections::HashMap; #[cfg(feature = "tokio")] -use crate::value::utils_async::parse_type_async; +use crate::{ + connection::asynchronous::BorrowedAsyncConnection, value::utils_async::parse_type_async, + AsyncGraphSchema, FalkorAsyncParseable, +}; /// A struct returned by the various queries, containing the result set, header, and stats #[derive(Clone, Debug, Default, PartialEq)] @@ -152,8 +155,8 @@ impl FalkorParsable for QueryResult { #[cfg(feature = "tokio")] async fn parse_result_set_async( data_vec: Vec, - graph_schema: &crate::AsyncGraphSchema, - conn: crate::FalkorAsyncConnection, + graph_schema: &AsyncGraphSchema, + conn: &mut BorrowedAsyncConnection, header_keys: &[String], ) -> Result>> { let mut parsed_result_set = Vec::with_capacity(data_vec.len()); @@ -163,8 +166,7 @@ async fn parse_result_set_async( let mut parsed_column = Vec::with_capacity(column_vec.len()); for column_item in column_vec { let (type_marker, val) = type_val_from_value(column_item)?; - parsed_column - .push(parse_type_async(type_marker, val, graph_schema, conn.clone()).await?); + parsed_column.push(parse_type_async(type_marker, val, graph_schema, conn).await?); } parsed_result_set.push(header_keys.iter().cloned().zip(parsed_column).collect()) @@ -174,11 +176,11 @@ async fn parse_result_set_async( } #[cfg(feature = "tokio")] -impl crate::FalkorAsyncParseable for QueryResult { +impl FalkorAsyncParseable for QueryResult { async fn from_falkor_value_async( value: FalkorValue, - graph_schema: &crate::AsyncGraphSchema, - conn: crate::FalkorAsyncConnection, + graph_schema: &AsyncGraphSchema, + conn: &mut BorrowedAsyncConnection, ) -> Result { let value_vec = value.into_vec()?; diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index a08eb3f..c9916df 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -15,7 +15,11 @@ use std::{ }; #[cfg(feature = "tokio")] -use crate::value::{map::parse_map_with_schema_async, utils_async::parse_labels_async}; +use crate::{ + connection::asynchronous::BorrowedAsyncConnection, + value::{map::parse_map_with_schema_async, utils_async::parse_labels_async}, + AsyncGraphSchema, FalkorAsyncParseable, +}; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum EntityType { @@ -111,11 +115,11 @@ impl FalkorParsable for Node { } #[cfg(feature = "tokio")] -impl crate::FalkorAsyncParseable for Node { +impl FalkorAsyncParseable for Node { async fn from_falkor_value_async( value: FalkorValue, - graph_schema: &crate::AsyncGraphSchema, - conn: crate::FalkorAsyncConnection, + graph_schema: &AsyncGraphSchema, + conn: &mut BorrowedAsyncConnection, ) -> Result { let [entity_id, labels, properties]: [FalkorValue; 3] = value .into_vec()? @@ -133,7 +137,7 @@ impl crate::FalkorAsyncParseable for Node { } let parsed_labels = - parse_labels_async(labels, graph_schema, conn.clone(), SchemaType::Labels).await?; + parse_labels_async(labels, graph_schema, conn, SchemaType::Labels).await?; Ok(Node { entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, labels: parsed_labels, @@ -216,11 +220,11 @@ impl FalkorParsable for Edge { } #[cfg(feature = "tokio")] -impl crate::FalkorAsyncParseable for Edge { +impl FalkorAsyncParseable for Edge { async fn from_falkor_value_async( value: FalkorValue, - graph_schema: &crate::AsyncGraphSchema, - conn: crate::FalkorAsyncConnection, + graph_schema: &AsyncGraphSchema, + conn: &mut BorrowedAsyncConnection, ) -> Result { let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value .into_vec()? @@ -253,7 +257,7 @@ impl crate::FalkorAsyncParseable for Edge { match graph_schema .refresh( SchemaType::Relationships, - conn.clone(), + conn, Some(&HashSet::from([relation])), ) .await? diff --git a/src/value/map.rs b/src/value/map.rs index e47867e..291bc3a 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -3,16 +3,18 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use super::utils::parse_type; use crate::{ - connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, FalkorValue, - SchemaType, SyncGraphSchema, + connection::blocking::BorrowedSyncConnection, value::utils::parse_type, FalkorDBError, + FalkorParsable, FalkorValue, SchemaType, SyncGraphSchema, }; use anyhow::Result; use std::collections::{HashMap, HashSet}; #[cfg(feature = "tokio")] -use super::utils_async::parse_type_async; +use crate::{ + connection::asynchronous::BorrowedAsyncConnection, value::utils_async::parse_type_async, + AsyncGraphSchema, FalkorAsyncParseable, +}; // Intermediate type for map parsing pub(crate) struct FKeyTypeVal { @@ -137,8 +139,8 @@ impl FalkorParsable for HashMap { async fn ktv_vec_to_map_async( map_vec: Vec, relevant_ids_map: HashMap, - graph_schema: &crate::AsyncGraphSchema, - conn: crate::FalkorAsyncConnection, + graph_schema: &AsyncGraphSchema, + conn: &mut BorrowedAsyncConnection, ) -> Result> { let mut new_map = HashMap::with_capacity(map_vec.len()); for fktv in map_vec { @@ -147,7 +149,7 @@ async fn ktv_vec_to_map_async( .get(&fktv.key) .cloned() .ok_or(FalkorDBError::ParsingError)?, - parse_type_async(fktv.type_marker, fktv.val, graph_schema, conn.clone()).await?, + parse_type_async(fktv.type_marker, fktv.val, graph_schema, conn).await?, ); } @@ -157,8 +159,8 @@ async fn ktv_vec_to_map_async( #[cfg(feature = "tokio")] pub(crate) async fn parse_map_with_schema_async( value: FalkorValue, - graph_schema: &crate::AsyncGraphSchema, - conn: crate::FalkorAsyncConnection, + graph_schema: &AsyncGraphSchema, + conn: &mut BorrowedAsyncConnection, schema_type: SchemaType, ) -> Result> { let val_vec = value.into_vec()?; @@ -179,7 +181,7 @@ pub(crate) async fn parse_map_with_schema_async( // If we reached here, schema validation failed and we need to refresh our schema match graph_schema - .refresh(schema_type, conn.clone(), Some(&id_hashset)) + .refresh(schema_type, conn, Some(&id_hashset)) .await? { Some(relevant_ids_map) => { @@ -190,11 +192,11 @@ pub(crate) async fn parse_map_with_schema_async( } #[cfg(feature = "tokio")] -impl crate::FalkorAsyncParseable for HashMap { +impl FalkorAsyncParseable for HashMap { async fn from_falkor_value_async( value: FalkorValue, - graph_schema: &crate::AsyncGraphSchema, - conn: crate::FalkorAsyncConnection, + graph_schema: &AsyncGraphSchema, + conn: &mut BorrowedAsyncConnection, ) -> Result { let val_vec = value.into_vec()?; @@ -203,7 +205,7 @@ impl crate::FalkorAsyncParseable for HashMap { let fktv = FKeyTypeVal::try_from(val)?; new_map.insert( fktv.key.to_string(), - parse_type_async(fktv.type_marker, fktv.val, graph_schema, conn.clone()).await?, + parse_type_async(fktv.type_marker, fktv.val, graph_schema, conn).await?, ); } diff --git a/src/value/mod.rs b/src/value/mod.rs index 5654fdf..15ae5a6 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -12,6 +12,11 @@ use path::Path; use point::Point; use std::{collections::HashMap, fmt::Debug}; +#[cfg(feature = "tokio")] +use crate::{ + connection::asynchronous::BorrowedAsyncConnection, AsyncGraphSchema, FalkorAsyncParseable, +}; + pub(crate) mod config; pub(crate) mod graph_entities; pub(crate) mod map; @@ -181,11 +186,11 @@ impl FalkorParsable for FalkorValue { } #[cfg(feature = "tokio")] -impl crate::FalkorAsyncParseable for FalkorValue { +impl FalkorAsyncParseable for FalkorValue { async fn from_falkor_value_async( value: FalkorValue, - _graph_schema: &crate::AsyncGraphSchema, - _conn: crate::FalkorAsyncConnection, + _graph_schema: &AsyncGraphSchema, + _conn: &mut BorrowedAsyncConnection, ) -> Result { Ok(value) } diff --git a/src/value/path.rs b/src/value/path.rs index 550b4ad..ebf2c64 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -9,6 +9,11 @@ use crate::{ }; use anyhow::Result; +#[cfg(feature = "tokio")] +use crate::{ + connection::asynchronous::BorrowedAsyncConnection, AsyncGraphSchema, FalkorAsyncParseable, +}; + /// TODO: not exactly sure what this represents #[derive(Clone, Debug, PartialEq)] pub struct Path { @@ -54,11 +59,11 @@ impl FalkorParsable for Path { } #[cfg(feature = "tokio")] -impl crate::FalkorAsyncParseable for Path { +impl FalkorAsyncParseable for Path { async fn from_falkor_value_async( value: FalkorValue, - graph_schema: &crate::AsyncGraphSchema, - conn: crate::FalkorAsyncConnection, + graph_schema: &AsyncGraphSchema, + conn: &mut BorrowedAsyncConnection, ) -> Result { let [nodes, relationships]: [FalkorValue; 2] = value .into_vec()? @@ -68,14 +73,12 @@ impl crate::FalkorAsyncParseable for Path { let mut parsed_nodes = Vec::with_capacity(nodes.len()); for node_raw in nodes { - parsed_nodes - .push(Node::from_falkor_value_async(node_raw, graph_schema, conn.clone()).await?); + parsed_nodes.push(Node::from_falkor_value_async(node_raw, graph_schema, conn).await?); } let mut parsed_edges = Vec::with_capacity(relationships.len()); for edge_raw in relationships { - parsed_edges - .push(Edge::from_falkor_value_async(edge_raw, graph_schema, conn.clone()).await?); + parsed_edges.push(Edge::from_falkor_value_async(edge_raw, graph_schema, conn).await?); } Ok(Path { diff --git a/src/value/utils_async.rs b/src/value/utils_async.rs index edc8745..3ad680c 100644 --- a/src/value/utils_async.rs +++ b/src/value/utils_async.rs @@ -3,10 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use super::utils::type_val_from_value; use crate::{ - AsyncGraphSchema, Edge, FalkorAsyncConnection, FalkorAsyncParseable, FalkorDBError, - FalkorValue, Node, Path, Point, SchemaType, + connection::asynchronous::BorrowedAsyncConnection, value::utils::type_val_from_value, + AsyncGraphSchema, Edge, FalkorAsyncParseable, FalkorDBError, FalkorValue, Node, Path, Point, + SchemaType, }; use anyhow::Result; use async_recursion::async_recursion; @@ -15,7 +15,7 @@ use std::collections::{HashMap, HashSet}; pub(crate) async fn parse_labels_async( raw_ids: Vec, graph_schema: &AsyncGraphSchema, - conn: FalkorAsyncConnection, + conn: &mut BorrowedAsyncConnection, schema_type: SchemaType, ) -> Result> { let mut ids_hashset = HashSet::with_capacity(raw_ids.len()); @@ -57,7 +57,7 @@ pub(crate) async fn parse_type_async( type_marker: i64, val: FalkorValue, graph_schema: &AsyncGraphSchema, - conn: FalkorAsyncConnection, + conn: &mut BorrowedAsyncConnection, ) -> Result { let res = match type_marker { 1 => FalkorValue::None, @@ -70,8 +70,7 @@ pub(crate) async fn parse_type_async( let mut parsed_vec = Vec::with_capacity(val.len()); for item in val { let (type_marker, val) = type_val_from_value(item)?; - parsed_vec - .push(parse_type_async(type_marker, val, graph_schema, conn.clone()).await?); + parsed_vec.push(parse_type_async(type_marker, val, graph_schema, conn).await?); } parsed_vec }), From 9265d718ea09fa19dbd0b4762a19c8fc0c385b51 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Mon, 27 May 2024 13:26:56 +0300 Subject: [PATCH 20/62] clippy and fixes --- src/client/asynchronous.rs | 37 ++++--- src/client/builder.rs | 8 +- src/client/mod.rs | 2 +- src/graph/asynchronous.rs | 216 ++++++++++++++++++++++++++++++++++++- src/graph/blocking.rs | 4 +- src/lib.rs | 22 +++- 6 files changed, 262 insertions(+), 27 deletions(-) diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index 32b7f61..fda10b2 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -288,7 +288,7 @@ mod tests { use std::{mem, thread}; #[tokio::test] - async fn test_list_graphs() { + async fn test_async_list_graphs() { let client = create_async_test_client().await; let res = client.list_graphs().await; assert!(res.is_ok()); @@ -298,7 +298,7 @@ mod tests { } #[tokio::test] - async fn test_open_graph_and_query() { + async fn test_async_open_graph_and_query() { let client = create_async_test_client().await; let graph = client.open_graph("imdb").await; @@ -313,14 +313,18 @@ mod tests { } #[tokio::test] - #[ignore] - async fn test_copy_graph() { + async fn test_async_copy_graph() { let client = create_async_test_client().await; - client.open_graph("imdb_ro_copy").await.delete().await.ok(); + client + .open_graph("imdb_async_ro_copy") + .await + .delete() + .await + .ok(); let graph = client - .copy_graph("imdb", "imdb_ro_copy") + .copy_graph("imdb", "imdb_async_ro_copy") .await .expect("Could not copy graph"); @@ -341,7 +345,7 @@ mod tests { } #[tokio::test] - async fn test_get_config() { + async fn test_async_get_config() { let client = create_async_test_client().await; let config = client @@ -358,12 +362,13 @@ mod tests { } #[tokio::test] - async fn test_get_config_all() { + async fn test_async_get_config_all() { let client = create_async_test_client().await; let configuration = client .config_get("*") .await .expect("Could not get configuration"); + assert_eq!( configuration.get("THREAD_COUNT").cloned().unwrap(), ConfigValue::Int64(thread::available_parallelism().unwrap().get() as i64) @@ -371,36 +376,36 @@ mod tests { } #[tokio::test] - async fn test_set_config() { + async fn test_async_set_config() { let client = create_async_test_client().await; let config = client - .config_get("DELTA_MAX_PENDING_CHANGES") + .config_get("EFFECTS_THRESHOLD") .await .expect("Could not get configuration"); let current_val = config - .get("DELTA_MAX_PENDING_CHANGES") + .get("EFFECTS_THRESHOLD") .cloned() .unwrap() .as_i64() .unwrap(); - let desired_val = if current_val == 10000 { 50000 } else { 10000 }; + let desired_val = if current_val == 300 { 250 } else { 300 }; client - .config_set("DELTA_MAX_PENDING_CHANGES", desired_val) + .config_set("EFFECTS_THRESHOLD", desired_val) .await .expect("Could not set config value"); let new_config = client - .config_get("DELTA_MAX_PENDING_CHANGES") + .config_get("EFFECTS_THRESHOLD") .await .expect("Could not get configuration"); assert_eq!( new_config - .get("DELTA_MAX_PENDING_CHANGES") + .get("EFFECTS_THRESHOLD") .cloned() .unwrap() .as_i64() @@ -409,7 +414,7 @@ mod tests { ); client - .config_set("DELTA_MAX_PENDING_CHANGES", current_val) + .config_set("EFFECTS_THRESHOLD", current_val) .await .ok(); } diff --git a/src/client/builder.rs b/src/client/builder.rs index e0b2f29..1c0f869 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -7,6 +7,9 @@ use crate::{client::FalkorClientProvider, FalkorConnectionInfo, FalkorDBError, F use anyhow::Result; use std::time::Duration; +#[cfg(feature = "tokio")] +use crate::FalkorAsyncClient; + /// A Builder-pattern implementation struct for creating a new Falkor client, sync or async. pub struct FalkorClientBuilder { connection_info: Option, @@ -127,12 +130,12 @@ impl FalkorClientBuilder<'A'> { } } - pub async fn build(self) -> Result { + pub async fn build(self) -> Result { let connection_info = self .connection_info .unwrap_or("falkor://127.0.0.1:6379".try_into()?); - crate::FalkorAsyncClient::create( + FalkorAsyncClient::create( get_client(connection_info.clone())?, connection_info, self.num_connections, @@ -218,6 +221,7 @@ mod tests { } #[tokio::test] + #[ignore] async fn test_async_timeout() { { let client = FalkorClientBuilder::new_async() diff --git a/src/client/mod.rs b/src/client/mod.rs index 0729614..fecbb78 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -41,7 +41,7 @@ impl FalkorClientProvider { FalkorClientProvider::Redis(redis_client) => match connection_timeout { Some(timeout) => { redis_client - .get_multiplexed_tokio_connection_with_response_timeouts(timeout, timeout) + .get_multiplexed_async_connection_with_timeouts(timeout, timeout) .await? } None => redis_client.get_multiplexed_tokio_connection().await?, diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index fbdfa18..c70d55b 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -369,7 +369,7 @@ impl AsyncGraph { let options_string = options .map(|hashmap| { hashmap - .into_iter() + .iter() .map(|(key, val)| format!("'{key}':'{val}'")) .collect::>() .join(",") @@ -503,7 +503,7 @@ impl AsyncGraph { "PROPERTIES".to_string(), properties.len().to_string(), ]); - params.extend(properties.into_iter().map(|property| property.to_string())); + params.extend(properties.iter().map(|property| property.to_string())); // create constraint using index self.send_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) @@ -538,3 +538,215 @@ impl AsyncGraph { .await } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::{test_utils::open_test_graph_async, IndexType}; + + #[tokio::test] + async fn test_create_drop_index_async() { + let graph = open_test_graph_async("test_create_drop_index_async").await; + graph + .inner + .create_index( + IndexType::Fulltext, + EntityType::Node, + "actor".to_string(), + &["Hello"], + None, + ) + .await + .expect("Could not create index"); + + let indices = graph + .inner + .list_indices() + .await + .expect("Could not list indices"); + + assert_eq!(indices.len(), 2); + assert_eq!(indices[0].field_types["Hello"], vec![IndexType::Fulltext]); + + graph + .inner + .drop_index( + IndexType::Fulltext, + EntityType::Node, + "actor".to_string(), + &["Hello"], + ) + .await + .expect("Could not drop index"); + } + + #[tokio::test] + async fn test_list_indices_async() { + let graph = open_test_graph_async("test_list_indices_async").await; + let indices = graph + .inner + .list_indices() + .await + .expect("Could not list indices"); + + assert_eq!(indices.len(), 1); + assert_eq!(indices[0].entity_type, EntityType::Node); + assert_eq!(indices[0].index_label, "actor".to_string()); + assert_eq!(indices[0].field_types.len(), 2); + assert_eq!( + indices[0].field_types, + HashMap::from([ + ("age".to_string(), vec![IndexType::Range]), + ("name".to_string(), vec![IndexType::Fulltext]) + ]) + ); + + graph.inner.delete().await.ok(); + } + + #[tokio::test] + async fn test_create_drop_mandatory_constraint_async() { + let graph = open_test_graph_async("test_mandatory_constraint_async").await; + + graph + .inner + .create_mandatory_constraint(EntityType::Edge, "act", &["hello", "goodbye"]) + .await + .expect("Could not create constraint"); + + graph + .inner + .drop_constraint( + ConstraintType::Mandatory, + EntityType::Edge, + "act", + &["hello", "goodbye"], + ) + .await + .expect("Could not drop constraint"); + } + + #[tokio::test] + async fn test_create_drop_unique_constraint_async() { + let graph = open_test_graph_async("test_unique_constraint_async").await; + + graph + .inner + .create_unique_constraint( + EntityType::Node, + "actor".to_string(), + &["first_name", "last_name"], + ) + .await + .expect("Could not create constraint"); + + graph + .inner + .drop_constraint( + ConstraintType::Unique, + EntityType::Node, + "actor", + &["first_name", "last_name"], + ) + .await + .expect("Could not drop constraint"); + } + + #[tokio::test] + async fn test_list_constraints_async() { + let graph = open_test_graph_async("test_list_constraint_async").await; + + graph + .inner + .create_unique_constraint( + EntityType::Node, + "actor".to_string(), + &["first_name", "last_name"], + ) + .await + .expect("Could not create constraint"); + + let constraints = graph + .inner + .list_constraints() + .await + .expect("Could not list constraints"); + assert_eq!(constraints.len(), 1); + } + + #[tokio::test] + async fn test_slowlog_async() { + let graph = open_test_graph_async("test_slowlog_async").await; + + graph + .inner + .query("UNWIND range(0, 500) AS x RETURN x", None) + .await + .expect("Could not generate the fast query"); + graph + .inner + .query("UNWIND range(0, 100000) AS x RETURN x", None) + .await + .expect("Could not generate the slow query"); + + let slowlog = graph + .inner + .slowlog() + .await + .expect("Could not get slowlog entries"); + + assert_eq!(slowlog.len(), 2); + assert_eq!( + slowlog[0].arguments, + "UNWIND range(0, 500) AS x RETURN x".to_string() + ); + assert_eq!( + slowlog[1].arguments, + "UNWIND range(0, 100000) AS x RETURN x".to_string() + ); + + graph + .inner + .slowlog_reset() + .await + .expect("Could not reset slowlog memory"); + let slowlog_after_reset = graph + .inner + .slowlog() + .await + .expect("Could not get slowlog entries after reset"); + assert!(slowlog_after_reset.is_empty()); + } + + #[tokio::test] + async fn test_explain_async() { + let graph = open_test_graph_async("test_explain_async").await; + + let execution_plan = graph.inner.explain("MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100").await.expect("Could not create execution plan"); + assert_eq!(execution_plan.steps().len(), 7); + assert_eq!( + execution_plan.text(), + "\nResults\n Limit\n Aggregate\n Filter\n Node By Index Scan | (b:actor)\n Project\n Node By Label Scan | (a:actor)" + ); + } + + #[tokio::test] + async fn test_profile_async() { + let graph = open_test_graph_async("test_profile_async").await; + + let execution_plan = graph + .inner + .profile("UNWIND range(0, 1000) AS x RETURN x") + .await + .expect("Could not generate the query"); + + let steps = execution_plan.steps().to_vec(); + assert_eq!(steps.len(), 3); + + let expected = vec!["Results", "Project", "Unwind"]; + for (step, expected) in steps.into_iter().zip(expected) { + assert!(step.starts_with(expected)); + assert!(step.ends_with("ms")); + } + } +} diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index c3e93b3..a414059 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -365,7 +365,7 @@ impl SyncGraph { let options_string = options .map(|hashmap| { hashmap - .into_iter() + .iter() .map(|(key, val)| format!("'{key}':'{val}'")) .collect::>() .join(",") @@ -497,7 +497,7 @@ impl SyncGraph { "PROPERTIES".to_string(), properties.len().to_string(), ]); - params.extend(properties.into_iter().map(|property| property.to_string())); + params.extend(properties.iter().map(|property| property.to_string())); // create constraint using index self.send_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) diff --git a/src/lib.rs b/src/lib.rs index bb906bd..7dd1042 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,7 +50,7 @@ pub use { #[cfg(test)] pub(crate) mod test_utils { - use crate::{FalkorClientBuilder, FalkorSyncClient, SyncGraph}; + use super::*; pub(crate) struct TestSyncGraphHandle { pub(crate) inner: SyncGraph, @@ -80,8 +80,20 @@ pub(crate) mod test_utils { } } + pub(crate) struct TestAsyncGraphHandle { + pub(crate) inner: AsyncGraph, + } + + impl Drop for TestAsyncGraphHandle { + fn drop(&mut self) { + tokio::runtime::Handle::current().block_on(async { + self.inner.delete().await.ok(); + }); + } + } + #[cfg(feature = "tokio")] - pub(crate) async fn create_async_test_client() -> crate::FalkorAsyncClient { + pub(crate) async fn create_async_test_client() -> FalkorAsyncClient { FalkorClientBuilder::new_async() .build() .await @@ -89,8 +101,10 @@ pub(crate) mod test_utils { } #[cfg(feature = "tokio")] - pub(crate) async fn open_test_graph_async(graph_name: &str) -> crate::AsyncGraph { + pub(crate) async fn open_test_graph_async(graph_name: &str) -> TestAsyncGraphHandle { let client = create_async_test_client().await; - client.open_graph(graph_name).await + TestAsyncGraphHandle { + inner: client.open_graph(graph_name).await, + } } } From 92052c80a38946fe79e2920dcb688a9fa58118a6 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Mon, 27 May 2024 13:30:52 +0300 Subject: [PATCH 21/62] separate tokio --- Cargo.toml | 2 +- src/client/builder.rs | 3 ++- src/lib.rs | 2 ++ src/response/constraint.rs | 9 ++++++--- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 13ee1fa..c4174da 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ tracing = { version = "0.1.40", default-features = false, features = ["std", "at url-parse = "1.0.8" [features] -default = ["redis", "tokio"] +default = ["redis"] native-tls = ["redis/tls-native-tls"] rustls = ["redis/tls-rustls"] tokio = ["dep:tokio", "redis/tokio-comp"] diff --git a/src/client/builder.rs b/src/client/builder.rs index 1c0f869..072c429 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -206,7 +206,7 @@ mod tests { assert!(impossible_client.is_err()); } - #[cfg(feature = "redis")] + #[cfg(all(feature = "tokio", feature = "redis"))] #[tokio::test] async fn test_async_builder() { let conneciton_info = "redis://127.0.0.1:6379".try_into(); @@ -220,6 +220,7 @@ mod tests { .is_ok()); } + #[cfg(feature = "tokio")] #[tokio::test] #[ignore] async fn test_async_timeout() { diff --git a/src/lib.rs b/src/lib.rs index 7dd1042..a6fcb38 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,10 +80,12 @@ pub(crate) mod test_utils { } } + #[cfg(feature = "tokio")] pub(crate) struct TestAsyncGraphHandle { pub(crate) inner: AsyncGraph, } + #[cfg(feature = "tokio")] impl Drop for TestAsyncGraphHandle { fn drop(&mut self) { tokio::runtime::Handle::current().block_on(async { diff --git a/src/response/constraint.rs b/src/response/constraint.rs index c4b4d49..680d1cc 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -3,16 +3,19 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::asynchronous::BorrowedAsyncConnection; use crate::{ connection::blocking::BorrowedSyncConnection, value::utils::{parse_type, type_val_from_value}, - AsyncGraphSchema, EntityType, FalkorAsyncParseable, FalkorDBError, FalkorParsable, FalkorValue, - SyncGraphSchema, + EntityType, FalkorDBError, FalkorParsable, FalkorValue, SyncGraphSchema, }; use anyhow::Result; use std::fmt::{Display, Formatter}; +#[cfg(feature = "tokio")] +use crate::{ + connection::asynchronous::BorrowedAsyncConnection, AsyncGraphSchema, FalkorAsyncParseable, +}; + /// The type of restriction to apply for the property #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ConstraintType { From 701386c11ef3437a69b2a545f2a1d52dc9e074a2 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Mon, 27 May 2024 21:22:56 +0300 Subject: [PATCH 22/62] Sync part is better and simpler now --- .github/dependabot.yml | 4 +- src/client/asynchronous.rs | 44 ++--- src/client/blocking.rs | 210 +++++++---------------- src/client/builder.rs | 38 ++-- src/connection/asynchronous.rs | 8 +- src/connection/blocking.rs | 49 +++--- src/error/mod.rs | 2 + src/graph/blocking.rs | 305 ++++++++++++++++++++++----------- src/graph/utils.rs | 4 +- src/graph_schema/blocking.rs | 102 ++++++----- src/graph_schema/utils.rs | 35 ++-- src/lib.rs | 4 +- src/parser/mod.rs | 2 +- src/response/constraint.rs | 2 +- src/response/index.rs | 2 +- src/response/query_result.rs | 4 +- src/value/config.rs | 13 ++ src/value/graph_entities.rs | 8 +- src/value/map.rs | 6 +- src/value/mod.rs | 2 +- src/value/path.rs | 2 +- src/value/utils.rs | 82 +++------ src/value/utils_async.rs | 8 +- 23 files changed, 473 insertions(+), 463 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e5b6219..059ba3d 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,12 +4,12 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "monthly" + interval: "weekly" target-branch: "main" # This is actually for cargo crates - package-ecosystem: cargo directory: "/" schedule: - interval: "monthly" + interval: "weekly" target-branch: "main" \ No newline at end of file diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index fda10b2..4edf0c5 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -100,32 +100,14 @@ impl FalkorAsyncClient { /// A [`Vec`] of [`String`]s, containing the names of available graphs pub async fn list_graphs(&self) -> Result> { let mut conn = self.borrow_connection().await?; - let graph_list = match conn.as_inner()? { - #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(redis_conn) => { - let cmd = redis::cmd("GRAPH.LIST"); - let res = match redis_conn.send_packed_command(&cmd).await { - Ok(redis::Value::Bulk(data)) => data, - _ => Err(FalkorDBError::InvalidDataReceived)?, - }; - let mut graph_list = Vec::with_capacity(res.len()); - for graph in res { - let graph = match graph { - redis::Value::Data(data) => { - Ok(String::from_utf8_lossy(data.as_slice()).to_string()) - } - redis::Value::Status(data) => Ok(data), - _ => Err(FalkorDBError::ParsingError), - }?; - - graph_list.push(graph); - } - graph_list - } - }; - - Ok(graph_list) + Ok(conn + .send_command(None, "GRAPH.LIST", None, None) + .await? + .into_vec()? + .into_iter() + .flat_map(|data| data.into_string()) + .collect::>()) } /// Return the current value of a configuration option in the database. @@ -237,7 +219,7 @@ impl FalkorAsyncClient { /// /// # Returns /// a [`AsyncGraph`] object, allowing various graph operations. - pub async fn open_graph( + pub async fn select_graph( &self, graph_name: T, ) -> AsyncGraph { @@ -277,7 +259,7 @@ impl FalkorAsyncClient { Some(&[new_graph_name.to_string()]), ) .await?; - Ok(self.open_graph(new_graph_name.to_string()).await) + Ok(self.select_graph(new_graph_name).await) } } @@ -298,10 +280,10 @@ mod tests { } #[tokio::test] - async fn test_async_open_graph_and_query() { + async fn test_async_select_graph_and_query() { let client = create_async_test_client().await; - let graph = client.open_graph("imdb").await; + let graph = client.select_graph("imdb").await; assert_eq!(graph.graph_name(), "imdb".to_string()); let res = graph @@ -317,7 +299,7 @@ mod tests { let client = create_async_test_client().await; client - .open_graph("imdb_async_ro_copy") + .select_graph("imdb_async_ro_copy") .await .delete() .await @@ -328,7 +310,7 @@ mod tests { .await .expect("Could not copy graph"); - let original_graph = client.open_graph("imdb").await; + let original_graph = client.select_graph("imdb").await; assert_eq!( graph diff --git a/src/client/blocking.rs b/src/client/blocking.rs index a0bf509..f1a7c2a 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -6,20 +6,19 @@ use crate::{ client::FalkorClientProvider, connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}, - ConfigValue, FalkorConnectionInfo, FalkorDBError, SyncGraph, SyncGraphSchema, + ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorValue, SyncGraph, SyncGraphSchema, }; use anyhow::Result; use parking_lot::Mutex; -use std::fmt::{Debug, Formatter}; -use std::time::Duration; use std::{ collections::HashMap, + fmt::Display, sync::{mpsc, Arc}, + time::Duration, }; pub(crate) struct FalkorSyncClientInner { _inner: Mutex, - graph_cache: Mutex>, connection_pool_size: u8, connection_pool_tx: mpsc::SyncSender, connection_pool_rx: mpsc::Receiver, @@ -27,10 +26,10 @@ pub(crate) struct FalkorSyncClientInner { impl FalkorSyncClientInner { pub(crate) fn borrow_connection(&self) -> Result { - Ok(BorrowedSyncConnection { - return_tx: self.connection_pool_tx.clone(), - conn: Some(self.connection_pool_rx.recv()?), - }) + Ok(BorrowedSyncConnection::new( + self.connection_pool_rx.recv()?, + self.connection_pool_tx.clone(), + )) } } @@ -42,7 +41,7 @@ unsafe impl Send for FalkorSyncClientInner {} /// and will select it based on enabled features and url connection /// /// # Thread Safety -/// This struct is fully thread safe, it can be cloned and passed within threads without constraints, +/// This struct is fully thread safe, it can be cloned and passed between threads without constraints, /// Its API uses only immutable references #[derive(Clone)] pub struct FalkorSyncClient { @@ -50,18 +49,6 @@ pub struct FalkorSyncClient { pub(crate) _connection_info: FalkorConnectionInfo, } -impl Debug for FalkorSyncClient { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { - f.debug_struct("FalkorSyncClient") - .field("inner", &"") - .field("connection_info", &self._connection_info) - .finish() - } -} - impl FalkorSyncClient { pub(crate) fn create( client: FalkorClientProvider, @@ -77,7 +64,6 @@ impl FalkorSyncClient { Ok(Self { inner: Arc::new(FalkorSyncClientInner { _inner: client.into(), - graph_cache: Default::default(), connection_pool_size: num_connections, connection_pool_tx, connection_pool_rx, @@ -102,32 +88,12 @@ impl FalkorSyncClient { pub fn list_graphs(&self) -> Result> { let mut conn = self.borrow_connection()?; - let graph_list = match conn.as_inner()? { - #[cfg(feature = "redis")] - FalkorSyncConnection::Redis(redis_conn) => { - use redis::ConnectionLike as _; - let res = match redis_conn.req_command(&redis::cmd("GRAPH.LIST"))? { - redis::Value::Bulk(data) => data, - _ => Err(FalkorDBError::InvalidDataReceived)?, - }; - - let mut graph_list = Vec::with_capacity(res.len()); - for graph in res { - let graph = match graph { - redis::Value::Data(data) => { - Ok(String::from_utf8_lossy(data.as_slice()).to_string()) - } - redis::Value::Status(data) => Ok(data), - _ => Err(FalkorDBError::ParsingError), - }?; - - graph_list.push(graph); - } - graph_list - } - }; - - Ok(graph_list) + let graph_list = conn.send_command::<&str>(None, "GRAPH.LIST", None, None)?; + Ok(graph_list + .into_vec()? + .into_iter() + .flat_map(|name| name.into_string()) + .collect()) } /// Return the current value of a configuration option in the database. @@ -138,64 +104,37 @@ impl FalkorSyncClient { /// /// # Returns /// A [`HashMap`] comprised of [`String`] keys, and [`ConfigValue`] values. - pub fn config_get( + pub fn config_get( &self, config_key: T, ) -> Result> { let mut conn = self.borrow_connection()?; + let config = conn + .send_command(None, "GRAPH.CONFIG", Some("GET"), Some(&[config_key]))? + .into_vec()?; + + if config.len() == 2 { + let [key, val]: [FalkorValue; 2] = config + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + return Ok(HashMap::from([( + key.into_string()?, + ConfigValue::try_from(val)?, + )])); + } - Ok(match conn.as_inner()? { - #[cfg(feature = "redis")] - FalkorSyncConnection::Redis(redis_conn) => { - use redis::ConnectionLike as _; - - let bulk_data = match redis_conn.req_command( - redis::cmd("GRAPH.CONFIG") - .arg("GET") - .arg(config_key.to_string()), - )? { - redis::Value::Bulk(bulk_data) => bulk_data, - _ => return Err(FalkorDBError::InvalidDataReceived.into()), - }; - - if bulk_data.is_empty() { - return Err(FalkorDBError::InvalidDataReceived.into()); - } else if bulk_data.len() == 2 { - return if let Some(ConfigValue::String(config_key)) = bulk_data - .first() - .map(ConfigValue::try_from) - .and_then(Result::ok) - { - Ok(HashMap::from([( - config_key.to_string(), - ConfigValue::try_from(&bulk_data[1])?, - )])) - } else { - Err(FalkorDBError::InvalidDataReceived.into()) - }; - } - - let mut config_map = HashMap::with_capacity(bulk_data.len()); - for raw_map in bulk_data { - for (key, val) in raw_map - .into_map_iter() - .map_err(|_| FalkorDBError::ParsingError)? - { - let key = match key { - redis::Value::Status(config_key) => Ok(config_key), - redis::Value::Data(config_key) => { - Ok(String::from_utf8_lossy(config_key.as_slice()).to_string()) - } - _ => Err(FalkorDBError::InvalidDataReceived), - }?; - - config_map.insert(key, ConfigValue::try_from(&val)?); - } - } - - config_map - } - }) + Ok(config + .into_iter() + .flat_map(|config| { + let [key, val]: [FalkorValue; 2] = config + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + Result::<_, FalkorDBError>::Ok((key.into_string()?, ConfigValue::try_from(val)?)) + }) + .collect::>()) } /// Return the current value of a configuration option in the database. @@ -208,22 +147,13 @@ impl FalkorSyncClient { &self, config_key: T, value: C, - ) -> Result<()> { - let mut conn = self.borrow_connection()?; - match conn.as_inner()? { - #[cfg(feature = "redis")] - FalkorSyncConnection::Redis(redis_conn) => { - use redis::ConnectionLike as _; - redis_conn.req_command( - redis::cmd("GRAPH.CONFIG") - .arg("SET") - .arg(config_key.into()) - .arg(value.into()), - )?; - } - } - - Ok(()) + ) -> Result { + self.borrow_connection()?.send_command( + None, + "GRAPH.CONFIG", + Some("SET"), + Some(&[config_key.into(), value.into()]), + ) } /// Opens a graph context for queries and operations @@ -233,20 +163,14 @@ impl FalkorSyncClient { /// /// # Returns /// a [`SyncGraph`] object, allowing various graph operations. - pub fn open_graph( + pub fn select_graph( &self, graph_name: T, ) -> SyncGraph { SyncGraph { client: self.inner.clone(), graph_name: graph_name.to_string(), - graph_schema: self - .inner - .graph_cache - .lock() - .entry(graph_name.to_string()) - .or_insert(SyncGraphSchema::new(graph_name.to_string())) - .clone(), + graph_schema: SyncGraphSchema::new(graph_name.to_string()), // Required for requesting refreshes } } @@ -258,18 +182,18 @@ impl FalkorSyncClient { /// /// # Returns /// If successful, will return the new [`SyncGraph`] object. - pub fn copy_graph( + pub fn copy_graph( &self, - graph_to_clone: T, - new_graph_name: Z, + graph_to_clone: &str, + new_graph_name: &str, ) -> Result { self.borrow_connection()?.send_command( - Some(graph_to_clone.to_string()), + Some(graph_to_clone), "GRAPH.COPY", None, - Some(&[new_graph_name.to_string()]), + Some(&[new_graph_name]), )?; - Ok(self.open_graph(new_graph_name.to_string())) + Ok(self.select_graph(new_graph_name)) } } @@ -291,7 +215,6 @@ mod tests { // Client was created with 6 connections let _conn_vec: Vec> = (0..6) - .into_iter() .map(|_| { let conn = client.borrow_connection(); assert!(conn.is_ok()); @@ -302,10 +225,9 @@ mod tests { let non_existing_conn = client.inner.connection_pool_rx.try_recv(); assert!(non_existing_conn.is_err()); - if let Err(TryRecvError::Empty) = non_existing_conn { - return; - } - assert!(false); + let Err(TryRecvError::Empty) = non_existing_conn else { + panic!("Got error, but not a TryRecvError::Empty, as expected"); + }; } #[test] @@ -319,14 +241,14 @@ mod tests { } #[test] - fn test_open_graph_and_query() { + fn test_select_graph_and_query() { let client = create_test_client(); - let graph = client.open_graph("imdb"); + let mut graph = client.select_graph("imdb"); assert_eq!(graph.graph_name(), "imdb".to_string()); let res = graph - .query("MATCH (a:actor) return a".to_string(), None) + .query("MATCH (a:actor) return a".to_string()) .expect("Could not get actors from unmodified graph"); assert_eq!(res.result_set.len(), 1317); @@ -336,25 +258,25 @@ mod tests { fn test_copy_graph() { let client = create_test_client(); - client.open_graph("imdb_ro_copy").delete().ok(); + client.select_graph("imdb_ro_copy").delete().ok(); let graph = client.copy_graph("imdb", "imdb_ro_copy"); assert!(graph.is_ok()); - let graph = TestSyncGraphHandle { + let mut graph = TestSyncGraphHandle { inner: graph.unwrap(), }; - let original_graph = client.open_graph("imdb"); + let mut original_graph = client.select_graph("imdb"); assert_eq!( graph .inner - .query("MATCH (a:actor) RETURN a".to_string(), None) + .query("MATCH (a:actor) RETURN a".to_string()) .expect("Could not get actors from unmodified graph") .result_set, original_graph - .query("MATCH (a:actor) RETURN a".to_string(), None) + .query("MATCH (a:actor) RETURN a".to_string()) .expect("Could not get actors from unmodified graph") .result_set ) diff --git a/src/client/builder.rs b/src/client/builder.rs index 072c429..56c14c0 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -69,19 +69,21 @@ impl FalkorClientBuilder { ..self } } -} -fn get_client>(connection_info: T) -> Result -where - anyhow::Error: From, -{ - let connection_info = connection_info.try_into()?; - Ok(match connection_info { - #[cfg(feature = "redis")] - FalkorConnectionInfo::Redis(connection_info) => { - FalkorClientProvider::Redis(redis::Client::open(connection_info.clone())?) - } - }) + fn get_client>( + connection_info: T + ) -> Result + where + anyhow::Error: From, + { + let connection_info = connection_info.try_into()?; + Ok(match connection_info { + #[cfg(feature = "redis")] + FalkorConnectionInfo::Redis(connection_info) => { + FalkorClientProvider::Redis(redis::Client::open(connection_info.clone())?) + } + }) + } } impl FalkorClientBuilder<'S'> { @@ -112,7 +114,7 @@ impl FalkorClientBuilder<'S'> { .unwrap_or("falkor://127.0.0.1:6379".try_into()?); FalkorSyncClient::create( - get_client(connection_info.clone())?, + Self::get_client(connection_info.clone())?, connection_info, self.num_connections, self.timeout, @@ -136,7 +138,7 @@ impl FalkorClientBuilder<'A'> { .unwrap_or("falkor://127.0.0.1:6379".try_into()?); FalkorAsyncClient::create( - get_client(connection_info.clone())?, + Self::get_client(connection_info.clone())?, connection_info, self.num_connections, self.timeout, @@ -151,7 +153,7 @@ mod tests { #[test] fn test_sync_builder() { - let conneciton_info = "redis://127.0.0.1:6379".try_into(); + let conneciton_info = "falkor://127.0.0.1:6379".try_into(); assert!(conneciton_info.is_ok()); assert!(FalkorClientBuilder::new() @@ -185,10 +187,10 @@ mod tests { // Connection pool size must be between 0 and 32 let zero = FalkorClientBuilder::new().with_num_connections(0).build(); + assert!(zero.is_err()); let too_many = FalkorClientBuilder::new().with_num_connections(36).build(); - - assert!(zero.is_err() && too_many.is_err()); + assert!(too_many.is_err()); } #[test] @@ -209,7 +211,7 @@ mod tests { #[cfg(all(feature = "tokio", feature = "redis"))] #[tokio::test] async fn test_async_builder() { - let conneciton_info = "redis://127.0.0.1:6379".try_into(); + let conneciton_info = "falkor://127.0.0.1:6379".try_into(); assert!(conneciton_info.is_ok()); assert!(FalkorClientBuilder::new_async() diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs index 1b15d9d..bf4a5ba 100644 --- a/src/connection/asynchronous.rs +++ b/src/connection/asynchronous.rs @@ -63,11 +63,9 @@ impl Drop for BorrowedAsyncConnection { let handle = tokio::runtime::Handle::try_current(); match handle { Ok(handle) => { - handle.spawn_blocking({ - let pool = self.conn_pool.clone(); - move || { - pool.blocking_lock().push_back(conn); - } + let pool = self.conn_pool.clone(); + handle.spawn_blocking(move || { + pool.blocking_lock().push_back(conn); }); } Err(_) => { diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index a625110..6d1ec16 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -5,6 +5,7 @@ use crate::{FalkorDBError, FalkorValue}; use anyhow::Result; +use std::fmt::Display; use std::sync::mpsc; pub(crate) enum FalkorSyncConnection { @@ -17,39 +18,47 @@ pub(crate) enum FalkorSyncConnection { /// /// This is publicly exposed for user-implementations of [`FalkorParsable`](crate::FalkorParsable) pub struct BorrowedSyncConnection { - pub(crate) conn: Option, - pub(crate) return_tx: mpsc::SyncSender, + conn: Option, + return_tx: mpsc::SyncSender, } impl BorrowedSyncConnection { + pub(crate) fn new( + conn: FalkorSyncConnection, + return_tx: mpsc::SyncSender, + ) -> Self { + Self { + conn: Some(conn), + return_tx, + } + } + pub(crate) fn as_inner(&mut self) -> Result<&mut FalkorSyncConnection, FalkorDBError> { self.conn.as_mut().ok_or(FalkorDBError::EmptyConnection) } - pub(crate) fn send_command( + pub(crate) fn send_command( &mut self, - graph_name: Option, + graph_name: Option<&str>, command: &str, subcommand: Option<&str>, - params: Option<&[String]>, + params: Option<&[P]>, ) -> Result { - Ok( - match self.conn.as_mut().ok_or(FalkorDBError::EmptyConnection)? { - #[cfg(feature = "redis")] - FalkorSyncConnection::Redis(redis_conn) => { - use redis::ConnectionLike as _; - let mut cmd = redis::cmd(command); - cmd.arg(subcommand); - cmd.arg(graph_name); - if let Some(params) = params { - for param in params { - cmd.arg(param); - } + Ok(match self.as_inner()? { + #[cfg(feature = "redis")] + FalkorSyncConnection::Redis(redis_conn) => { + use redis::ConnectionLike as _; + let mut cmd = redis::cmd(command); + cmd.arg(subcommand); + cmd.arg(graph_name); + if let Some(params) = params { + for param in params { + cmd.arg(param.to_string()); } - redis::FromRedisValue::from_owned_redis_value(redis_conn.req_command(&cmd)?)? } - }, - ) + redis::FromRedisValue::from_owned_redis_value(redis_conn.req_command(&cmd)?)? + } + }) } } diff --git a/src/error/mod.rs b/src/error/mod.rs index 76c3ba5..e36d66b 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -27,6 +27,8 @@ pub enum FalkorDBError { ParsingUnknownType, #[error("Element was not of type Bool")] ParsingBool, + #[error("Could not parse into config value, was not one of the supported types")] + ParsingConfigValue, #[error("Element was not of type I64")] ParsingI64, #[error("Element was not of type F64")] diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index a414059..c4c679c 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -3,44 +3,27 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use super::utils::{construct_query, generate_procedure_call}; use crate::{ - client::blocking::FalkorSyncClientInner, connection::blocking::FalkorSyncConnection, + client::blocking::FalkorSyncClientInner, + graph::utils::{construct_query, generate_procedure_call}, Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorDBError, FalkorIndex, FalkorParsable, FalkorValue, IndexType, QueryResult, SlowlogEntry, SyncGraphSchema, }; use anyhow::Result; -use std::{ - collections::HashMap, - fmt::{Debug, Display, Formatter}, - sync::Arc, -}; +use std::{collections::HashMap, fmt::Display, sync::Arc}; /// The main graph API, this allows the user to perform graph operations while exposing as little details as possible. -/// /// # Thread Safety -/// This struct is fully thread safe, it can be cloned and passed within threads without constraints, -/// Its API uses only immutable references +/// This struct is NOT thread safe, and synchronization is up to the user. +/// Also, graph schema is not shared between instances of SyncGraph, even with the same name #[derive(Clone)] pub struct SyncGraph { pub(crate) client: Arc, pub(crate) graph_name: String, /// Provides user with access to the current graph schema, - /// which contains a safe cache of id to labels/properties/relationship maps pub graph_schema: SyncGraphSchema, } -impl Debug for SyncGraph { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { - f.debug_struct("SyncGraph") - .field("client", &"") - .finish() - } -} - impl SyncGraph { /// Returns the name of the graph for which this API performs operations. /// @@ -57,12 +40,12 @@ impl SyncGraph { params: Option<&[String]>, ) -> Result { let mut conn = self.client.borrow_connection()?; - conn.send_command(Some(self.graph_name.clone()), command, subcommand, params) + conn.send_command(Some(self.graph_name.as_str()), command, subcommand, params) } /// Deletes the graph stored in the database, and drop all the schema caches. /// NOTE: This still maintains the graph API, operations are still viable. - pub fn delete(&self) -> Result<()> { + pub fn delete(&mut self) -> Result<()> { self.send_command("GRAPH.DELETE", None, None)?; self.graph_schema.clear(); Ok(()) @@ -168,49 +151,82 @@ impl SyncGraph { self.explain_with_params::(query_string, None) } - fn query_inner( - &self, + fn query_inner_with_timeout( + &mut self, command: &str, query_string: Q, params: Option<&HashMap>, - timeout: Option, + timeout: i64, ) -> Result

{ let query = construct_query(query_string, params); let mut conn = self.client.borrow_connection()?; - let falkor_result = match conn.as_inner()? { - #[cfg(feature = "redis")] - FalkorSyncConnection::Redis(redis_conn) => { - use redis::ConnectionLike as _; - use redis::FromRedisValue as _; - let redis_val = redis_conn.req_command( - redis::cmd(command) - .arg(self.graph_name.as_str()) - .arg(query) - .arg("--compact") - .arg(timeout.map(|timeout| format!("timeout {timeout}"))), - )?; - FalkorValue::from_owned_redis_value(redis_val)? - } - }; + P::from_falkor_value( + conn.send_command( + Some(self.graph_name.as_str()), + command, + None, + Some(&[query, "--compact".to_string(), format!("timeout {timeout}")]), + )?, + &mut self.graph_schema, + &mut conn, + ) + } + + fn query_inner( + &mut self, + command: &str, + query_string: Q, + params: Option<&HashMap>, + ) -> Result

{ + let query = construct_query(query_string, params); - P::from_falkor_value(falkor_result, &self.graph_schema, &mut conn) + let mut conn = self.client.borrow_connection()?; + P::from_falkor_value( + conn.send_command( + Some(self.graph_name.as_str()), + command, + None, + Some(&[query, "--compact".to_string()]), + )?, + &mut self.graph_schema, + &mut conn, + ) } /// Run a query on the graph /// /// # Arguments /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. /// /// # Returns /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query pub fn query( - &self, + &mut self, query_string: Q, - timeout: Option, ) -> Result { - self.query_inner::("GRAPH.QUERY", query_string, None, timeout) + self.query_inner::("GRAPH.QUERY", query_string, None) + } + + /// Run a query on the graph, but abort it if it exceeds the timeout + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub fn query_with_timeout( + &mut self, + query_string: Q, + timeout: i64, + ) -> Result { + self.query_inner_with_timeout::( + "GRAPH.QUERY", + query_string, + None, + timeout, + ) } /// Run a query on the graph @@ -218,18 +234,35 @@ impl SyncGraph { /// /// # Arguments /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. /// /// # Returns /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query pub fn query_with_params( - &self, + &mut self, + query_string: Q, + params: &HashMap, + ) -> Result { + self.query_inner("GRAPH.QUERY", query_string, Some(params)) + } + + /// Run a query on the graph but abort it if it exceeds the timeout + /// This function variant allows adding extra parameters after the query + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub fn query_with_params_and_timeout( + &mut self, query_string: Q, - timeout: Option, + timeout: i64, params: &HashMap, ) -> Result { - self.query_inner("GRAPH.QUERY", query_string, Some(params), timeout) + self.query_inner_with_timeout("GRAPH.QUERY", query_string, Some(params), timeout) } /// Run a query on the graph @@ -237,16 +270,31 @@ impl SyncGraph { /// /// # Arguments /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. /// /// # Returns /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query pub fn query_readonly( - &self, + &mut self, query_string: Q, - timeout: Option, ) -> Result { - self.query_inner::( + self.query_inner::("GRAPH.QUERY_RO", query_string, None) + } + + /// Run a query on the graph, but abort it if it exceeds the timeout + /// Read-only queries are more limited with the operations they are allowed to perform. + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub fn query_readonly_with_timeout( + &mut self, + query_string: Q, + timeout: i64, + ) -> Result { + self.query_inner_with_timeout::( "GRAPH.QUERY_RO", query_string, None, @@ -266,12 +314,31 @@ impl SyncGraph { /// # Returns /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query pub fn query_readonly_with_params( - &self, + &mut self, query_string: Q, - timeout: Option, - params: Option<&HashMap>, + params: &HashMap, + ) -> Result { + self.query_inner("GRAPH.QUERY_RO", query_string, Some(params)) + } + + /// Run a read-only query on the graph, but abort it if it exceeds the timeout + /// Read-only queries are more limited with the operations they are allowed to perform. + /// This function variant allows adding extra parameters after the query + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub fn query_readonly_with_params_and_timeout( + &mut self, + query_string: Q, + params: &HashMap, + timeout: i64, ) -> Result { - self.query_inner("GRAPH.QUERY_RO", query_string, params, timeout) + self.query_inner_with_timeout("GRAPH.QUERY_RO", query_string, Some(params), timeout) } /// Run a query which calls a procedure on the graph, read-only, or otherwise. @@ -288,12 +355,11 @@ impl SyncGraph { /// # Returns /// A caller-provided type which implements [`FalkorParsable`] pub fn call_procedure( - &self, + &mut self, procedure: C, args: Option<&[&str]>, yields: Option<&[&str]>, read_only: bool, - timeout: Option, ) -> Result

{ let (query_string, params) = generate_procedure_call(procedure, args, yields); @@ -305,6 +371,41 @@ impl SyncGraph { }, query_string, params.as_ref(), + ) + } + + /// Run a query which calls a procedure on the graph, read-only, or otherwise. + /// Read-only queries are more limited with the operations they are allowed to perform. + /// This function allows adding extra parameters after the query, and adding a YIELD block afterward + /// This function will cause the query to abort if it exceeds a certain timeout + /// + /// # Arguments + /// * `procedure`: The procedure to call + /// * `args`: An optional slice of strings containing the parameters. + /// * `yields`: The optional yield block arguments. + /// * `read_only`: Whether this procedure is read-only. + /// * `timeout`: If provided, the query will abort if overruns the timeout. + /// + /// # Returns + /// A caller-provided type which implements [`FalkorParsable`] + pub fn call_procedure_with_timeout( + &mut self, + procedure: C, + args: Option<&[&str]>, + yields: Option<&[&str]>, + read_only: bool, + timeout: i64, + ) -> Result

{ + let (query_string, params) = generate_procedure_call(procedure, args, yields); + + self.query_inner_with_timeout( + if read_only { + "GRAPH.QUERY_RO" + } else { + "GRAPH.QUERY" + }, + query_string, + params.as_ref(), timeout, ) } @@ -313,10 +414,10 @@ impl SyncGraph { /// /// # Returns /// A [`Vec`] of [`Index`] - pub fn list_indices(&self) -> Result> { + pub fn list_indices(&mut self) -> Result> { let mut conn = self.client.borrow_connection()?; let [_, indices, _]: [FalkorValue; 3] = self - .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false, None)? + .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false)? .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; @@ -327,7 +428,7 @@ impl SyncGraph { for index in indices { out_vec.push(FalkorIndex::from_falkor_value( index, - &self.graph_schema, + &mut self.graph_schema, &mut conn, )?); } @@ -336,7 +437,7 @@ impl SyncGraph { } pub fn create_index( - &self, + &mut self, index_field_type: IndexType, entity_type: EntityType, label: L, @@ -373,11 +474,10 @@ impl SyncGraph { .map(|options_string| format!(" OPTIONS {{ {} }}", options_string)) .unwrap_or_default(); - let full_query = format!( + self.query(format!( "CREATE {idx_type}INDEX FOR {pattern} ON ({}){}", properties_string, options_string - ); - self.query(full_query, None) + )) } /// Drop an existing index, by specifying its type, entity, label and specific properties @@ -385,7 +485,7 @@ impl SyncGraph { /// # Arguments /// * `index_field_type` pub fn drop_index( - &self, + &mut self, index_field_type: IndexType, entity_type: EntityType, label: L, @@ -409,39 +509,38 @@ impl SyncGraph { } .to_string(); - self.query( - format!( - "DROP {idx_type} INDEX for {pattern} ON ({})", - properties_string - ), - None, - ) + self.query(format!( + "DROP {idx_type} INDEX for {pattern} ON ({})", + properties_string + )) } /// Calls the DB.CONSTRAINTS procedure on the graph, returning an array of the graph's constraints /// /// # Returns - /// A [`Vec`] of [`Constraint`]s - pub fn list_constraints(&self) -> Result> { + /// A tuple where the first element is a [`Vec`] of [`Constraint`]s, and the second element is a [`Vec`] of stats as [`String`]s + pub fn list_constraints(&mut self) -> Result<(Vec, Vec)> { let mut conn = self.client.borrow_connection()?; - let [_, query_res, _]: [FalkorValue; 3] = self - .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, false, None)? + let [_, query_res, stats]: [FalkorValue; 3] = self + .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, false)? .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - let query_res = query_res.into_vec()?; - - let mut constraints_vec = Vec::with_capacity(query_res.len()); - for item in query_res { - constraints_vec.push(Constraint::from_falkor_value( - item, - &self.graph_schema, - &mut conn, - )?); - } - - Ok(constraints_vec) + Ok(( + query_res + .into_vec()? + .into_iter() + .flat_map(|item| { + Constraint::from_falkor_value(item, &mut self.graph_schema, &mut conn) + }) + .collect(), + stats + .into_vec()? + .into_iter() + .flat_map(|stat| stat.into_string()) + .collect(), + )) } /// Creates a new constraint for this graph, making the provided properties mandatory @@ -476,7 +575,7 @@ impl SyncGraph { /// * `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. pub fn create_unique_constraint( - &self, + &mut self, entity_type: EntityType, label: String, properties: &[P], @@ -538,7 +637,7 @@ mod tests { #[test] fn test_create_drop_index() { - let graph = open_test_graph("test_create_drop_index"); + let mut graph = open_test_graph("test_create_drop_index"); let res = graph.inner.create_index( IndexType::Fulltext, EntityType::Node, @@ -566,11 +665,9 @@ mod tests { #[test] fn test_list_indices() { - let graph = open_test_graph("test_list_indices"); - let res = graph.inner.list_indices(); - assert!(res.is_ok()); + let mut graph = open_test_graph("test_list_indices"); + let indices = graph.inner.list_indices().expect("Could not list indices"); - let indices = res.unwrap(); assert_eq!(indices.len(), 1); assert_eq!(indices[0].entity_type, EntityType::Node); assert_eq!(indices[0].index_label, "actor".to_string()); @@ -606,7 +703,7 @@ mod tests { #[test] fn test_create_drop_unique_constraint() { - let graph = open_test_graph("test_unique_constraint"); + let mut graph = open_test_graph("test_unique_constraint"); graph .inner @@ -630,7 +727,7 @@ mod tests { #[test] fn test_list_constraints() { - let graph = open_test_graph("test_list_constraint"); + let mut graph = open_test_graph("test_list_constraint"); graph .inner @@ -641,7 +738,7 @@ mod tests { ) .expect("Could not create constraint"); - let constraints = graph + let (constraints, _) = graph .inner .list_constraints() .expect("Could not list constraints"); @@ -650,15 +747,15 @@ mod tests { #[test] fn test_slowlog() { - let graph = open_test_graph("test_slowlog"); + let mut graph = open_test_graph("test_slowlog"); graph .inner - .query("UNWIND range(0, 500) AS x RETURN x", None) + .query("UNWIND range(0, 500) AS x RETURN x") .expect("Could not generate the fast query"); graph .inner - .query("UNWIND range(0, 100000) AS x RETURN x", None) + .query("UNWIND range(0, 100000) AS x RETURN x") .expect("Could not generate the slow query"); let slowlog = graph diff --git a/src/graph/utils.rs b/src/graph/utils.rs index ebb654c..5b57657 100644 --- a/src/graph/utils.rs +++ b/src/graph/utils.rs @@ -70,8 +70,8 @@ mod tests { fn test_generate_procedure_call() { let (query, params) = generate_procedure_call( "DB.CONSTRAINTS", - Some(&["Hello".to_string(), "World".to_string()]), - Some(&["Foo".to_string(), "Bar".to_string()]), + Some(&["Hello", "World"]), + Some(&["Foo", "Bar"]), ); assert_eq!(query, "CALL DB.CONSTRAINTS($Hello,$World) YIELD Foo,Bar"); diff --git a/src/graph_schema/blocking.rs b/src/graph_schema/blocking.rs index 306e4cb..f5aeabe 100644 --- a/src/graph_schema/blocking.rs +++ b/src/graph_schema/blocking.rs @@ -3,22 +3,16 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use super::utils::{get_refresh_command, get_relevant_hashmap, update_map}; use crate::{ - connection::blocking::BorrowedSyncConnection, value::FalkorValue, FalkorDBError, SchemaType, + connection::blocking::BorrowedSyncConnection, + graph_schema::utils::{get_refresh_command, get_relevant_hashmap, update_map}, + value::FalkorValue, + FalkorDBError, SchemaType, }; use anyhow::Result; -use parking_lot::RwLock; -use std::{ - collections::{HashMap, HashSet}, - ops::{Deref, DerefMut}, - sync::{ - atomic::{AtomicI64, Ordering::SeqCst}, - Arc, - }, -}; +use std::collections::{HashMap, HashSet}; -pub(crate) type LockableIdMap = Arc>>; +pub(crate) type IdMap = HashMap; /// A struct containing the various schema maps, allowing conversions between ids and their string representations. /// @@ -28,10 +22,10 @@ pub(crate) type LockableIdMap = Arc>>; #[derive(Clone, Debug, Default)] pub struct SyncGraphSchema { graph_name: String, - version: Arc, - labels: LockableIdMap, - properties: LockableIdMap, - relationships: LockableIdMap, + version: i64, + labels: IdMap, + properties: IdMap, + relationships: IdMap, } impl SyncGraphSchema { @@ -43,29 +37,29 @@ impl SyncGraphSchema { } /// Clears all cached schemas, this will cause a refresh when next attempting to parse a compact query. - pub fn clear(&self) { - self.version.store(0, SeqCst); - self.labels.write().clear(); - self.properties.write().clear(); - self.relationships.write().clear(); + pub fn clear(&mut self) { + self.version = 0; + self.labels.clear(); + self.properties.clear(); + self.relationships.clear(); } /// Returns a read-write-locked map, of the relationship ids to their respective string representations. /// Minimize locking these to avoid starvation. - pub fn relationships(&self) -> LockableIdMap { - self.relationships.clone() + pub fn relationships(&self) -> &IdMap { + &self.relationships } /// Returns a read-write-locked map, of the label ids to their respective string representations. /// Minimize locking these to avoid starvation. - pub fn labels(&self) -> LockableIdMap { - self.labels.clone() + pub fn labels(&self) -> &IdMap { + &self.labels } /// Returns a read-write-locked map, of the property ids to their respective string representations. /// Minimize locking these to avoid starvation. - pub fn properties(&self) -> LockableIdMap { - self.properties.clone() + pub fn properties(&self) -> &IdMap { + &self.properties } pub(crate) fn verify_id_set( @@ -73,35 +67,32 @@ impl SyncGraphSchema { id_set: &HashSet, schema_type: SchemaType, ) -> Option> { - let read_lock = match schema_type { + let id_map = match schema_type { SchemaType::Labels => &self.labels, SchemaType::Properties => &self.properties, SchemaType::Relationships => &self.relationships, - } - .read(); + }; - get_relevant_hashmap(id_set, read_lock.deref()) + get_relevant_hashmap(id_set, id_map) } pub(crate) fn refresh( - &self, + &mut self, schema_type: SchemaType, conn: &mut BorrowedSyncConnection, id_hashset: Option<&HashSet>, ) -> Result>> { let command = get_refresh_command(schema_type); - let map = match schema_type { - SchemaType::Labels => &self.labels, - SchemaType::Properties => &self.properties, - SchemaType::Relationships => &self.relationships, + let id_map = match schema_type { + SchemaType::Labels => &mut self.labels, + SchemaType::Properties => &mut self.properties, + SchemaType::Relationships => &mut self.relationships, }; - let mut write_lock = map.write(); - // 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] = conn .send_command( - Some(self.graph_name.clone()), + Some(self.graph_name.as_str()), "GRAPH.QUERY", None, Some(&[format!("CALL {command}()")]), @@ -110,6 +101,37 @@ impl SyncGraphSchema { .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - Ok(update_map(write_lock.deref_mut(), keys, id_hashset)?) + Ok(update_map(id_map, keys, id_hashset)?) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::{test_utils::create_test_client, SyncGraph}; + use std::collections::HashMap; + + pub(crate) fn open_readonly_graph_with_modified_schema() -> (SyncGraph, BorrowedSyncConnection) + { + let client = create_test_client(); + let mut graph = client.select_graph("imdb"); + let conn = client + .borrow_connection() + .expect("Could not borrow_connection"); + + graph.graph_schema.properties = HashMap::from([ + (0, "age".to_string()), + (1, "is_boring".to_string()), + (2, "something_else".to_string()), + (3, "secs_since_login".to_string()), + ]); + + graph.graph_schema.labels = + HashMap::from([(0, "much".to_string()), (1, "actor".to_string())]); + + graph.graph_schema.relationships = + HashMap::from([(0, "very".to_string()), (1, "wow".to_string())]); + + (graph, conn) } } diff --git a/src/graph_schema/utils.rs b/src/graph_schema/utils.rs index 29fdb69..2d87531 100644 --- a/src/graph_schema/utils.rs +++ b/src/graph_schema/utils.rs @@ -20,25 +20,28 @@ pub(crate) fn update_map( keys: FalkorValue, id_hashset: Option<&HashSet>, ) -> Result>, FalkorDBError> { - let keys_vec = keys.into_vec()?; - - let mut new_keys = HashMap::with_capacity(keys_vec.len()); - for (idx, item) in keys_vec.into_iter().enumerate() { - let key = item - .into_vec()? - .into_iter() - .next() - .ok_or(FalkorDBError::ParsingError)? - .try_into()?; - new_keys.insert(idx as i64, key); - } + let new_keys = keys + .into_vec()? + .into_iter() + .enumerate() + .flat_map(|(idx, item)| { + Result::<(i64, String), FalkorDBError>::Ok(( + idx as i64, + item.into_vec()? + .into_iter() + .next() + .ok_or(FalkorDBError::ParsingError) + .and_then(|item| item.into_string())?, + )) + }) + .collect::>(); *map_to_update = new_keys; - match id_hashset { - None => Ok(None), - Some(id_hashset) => Ok(get_relevant_hashmap(id_hashset, map_to_update)), - } + Ok(match id_hashset { + None => None, + Some(id_hashset) => get_relevant_hashmap(id_hashset, map_to_update), + }) } pub(crate) fn get_relevant_hashmap( diff --git a/src/lib.rs b/src/lib.rs index a6fcb38..bca3d99 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -71,7 +71,7 @@ pub(crate) mod test_utils { pub(crate) fn open_test_graph(graph_name: &str) -> TestSyncGraphHandle { let client = create_test_client(); - client.open_graph(graph_name).delete().ok(); + client.select_graph(graph_name).delete().ok(); TestSyncGraphHandle { inner: client @@ -106,7 +106,7 @@ pub(crate) mod test_utils { pub(crate) async fn open_test_graph_async(graph_name: &str) -> TestAsyncGraphHandle { let client = create_async_test_client().await; TestAsyncGraphHandle { - inner: client.open_graph(graph_name).await, + inner: client.select_graph(graph_name).await, } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 0e77fbc..600972c 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -13,7 +13,7 @@ use crate::{connection::asynchronous::BorrowedAsyncConnection, AsyncGraphSchema} pub trait FalkorParsable: Sized { fn from_falkor_value( value: FalkorValue, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result; } diff --git a/src/response/constraint.rs b/src/response/constraint.rs index 680d1cc..cd6eb61 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -125,7 +125,7 @@ pub struct Constraint { impl FalkorParsable for Constraint { fn from_falkor_value( value: FalkorValue, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let value_vec = value.into_vec()?; diff --git a/src/response/index.rs b/src/response/index.rs index 93af43d..6f890c5 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -125,7 +125,7 @@ pub struct FalkorIndex { impl FalkorParsable for FalkorIndex { fn from_falkor_value( value: FalkorValue, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let value_vec = value.into_vec()?; diff --git a/src/response/query_result.rs b/src/response/query_result.rs index 0771176..f883a99 100644 --- a/src/response/query_result.rs +++ b/src/response/query_result.rs @@ -80,7 +80,7 @@ fn query_parse_stats(stats: FalkorValue) -> Result> { pub(crate) fn parse_result_set( data_vec: Vec, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, header_keys: &[String], ) -> Result>> { @@ -103,7 +103,7 @@ pub(crate) fn parse_result_set( impl FalkorParsable for QueryResult { fn from_falkor_value( value: FalkorValue, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let value_vec = value.into_vec()?; diff --git a/src/value/config.rs b/src/value/config.rs index dc6c35d..1e13ec6 100644 --- a/src/value/config.rs +++ b/src/value/config.rs @@ -3,6 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use crate::{FalkorDBError, FalkorValue}; use std::fmt::{Display, Formatter}; /// An enum representing the two viable types for a config value @@ -50,3 +51,15 @@ impl From<&str> for ConfigValue { ConfigValue::String(value.to_string()) } } + +impl TryFrom for ConfigValue { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> Result { + match value { + FalkorValue::FString(str_val) => Ok(ConfigValue::String(str_val)), + FalkorValue::Int64(int_val) => Ok(ConfigValue::Int64(int_val)), + _ => Err(FalkorDBError::ParsingConfigValue), + } + } +} diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index c9916df..af7743c 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -82,7 +82,7 @@ pub struct Node { impl FalkorParsable for Node { fn from_falkor_value( value: FalkorValue, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let [entity_id, labels, properties]: [FalkorValue; 3] = value @@ -170,7 +170,7 @@ pub struct Edge { impl FalkorParsable for Edge { fn from_falkor_value( value: FalkorValue, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value @@ -179,10 +179,10 @@ impl FalkorParsable for Edge { .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let relation = relations.to_i64().ok_or(FalkorDBError::ParsingI64)?; - if let Some(relationship) = graph_schema.relationships().read().get(&relation).cloned() { + if let Some(relationship) = graph_schema.relationships().get(&relation) { return Ok(Edge { entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - relationship_type: relationship, + 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)?, properties: parse_map_with_schema( diff --git a/src/value/map.rs b/src/value/map.rs index 291bc3a..2c9cb42 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -51,7 +51,7 @@ impl TryFrom for FKeyTypeVal { fn ktv_vec_to_map( map_vec: Vec, relevant_ids_map: HashMap, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result> { let mut new_map = HashMap::with_capacity(map_vec.len()); @@ -70,7 +70,7 @@ fn ktv_vec_to_map( pub(crate) fn parse_map_with_schema( value: FalkorValue, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, schema_type: SchemaType, ) -> Result> { @@ -100,7 +100,7 @@ pub(crate) fn parse_map_with_schema( impl FalkorParsable for HashMap { fn from_falkor_value( value: FalkorValue, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let val_vec = value.into_vec()?; diff --git a/src/value/mod.rs b/src/value/mod.rs index 15ae5a6..afe2187 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -178,7 +178,7 @@ impl TryFrom for Point { impl FalkorParsable for FalkorValue { fn from_falkor_value( value: FalkorValue, - _graph_schema: &SyncGraphSchema, + _graph_schema: &mut SyncGraphSchema, _conn: &mut BorrowedSyncConnection, ) -> Result { Ok(value) diff --git a/src/value/path.rs b/src/value/path.rs index ebf2c64..83e8bc2 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -24,7 +24,7 @@ pub struct Path { impl FalkorParsable for Path { fn from_falkor_value( value: FalkorValue, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let [nodes, relationships]: [FalkorValue; 2] = value diff --git a/src/value/utils.rs b/src/value/utils.rs index 2245310..6d90d5a 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -12,33 +12,24 @@ use std::collections::HashSet; pub(crate) fn parse_labels( raw_ids: Vec, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, schema_type: SchemaType, ) -> Result> { - let mut ids_hashset = HashSet::with_capacity(raw_ids.len()); - for label in raw_ids.iter() { - ids_hashset.insert(label.to_i64().ok_or(FalkorDBError::ParsingI64)?); - } + let ids_hashset = raw_ids + .iter() + .filter_map(|label_id| label_id.to_i64()) + .collect::>(); match match graph_schema.verify_id_set(&ids_hashset, schema_type) { None => graph_schema.refresh(schema_type, conn, Some(&ids_hashset))?, relevant_ids => relevant_ids, } { Some(relevant_ids) => { - let mut parsed_ids = Vec::with_capacity(raw_ids.len()); - for id in raw_ids { - parsed_ids.push( - id.to_i64() - .ok_or(FalkorDBError::ParsingI64) - .and_then(|id| { - relevant_ids - .get(&id) - .cloned() - .ok_or(FalkorDBError::ParsingCompactIdUnknown) - })?, - ); - } + let parsed_ids = raw_ids + .into_iter() + .filter_map(|id| id.to_i64().and_then(|id| relevant_ids.get(&id).cloned())) + .collect(); Ok(parsed_ids) } @@ -59,7 +50,7 @@ pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValu pub(crate) fn parse_type( type_marker: i64, val: FalkorValue, - graph_schema: &SyncGraphSchema, + graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let res = match type_marker { @@ -105,42 +96,11 @@ pub(crate) fn parse_vec>( #[cfg(test)] mod tests { use super::*; - use crate::test_utils::create_test_client; - use std::collections::HashMap; - - fn open_readonly_graph() -> (SyncGraphSchema, BorrowedSyncConnection) { - let client = create_test_client(); - let schema = client.open_graph("imdb").graph_schema.clone(); - let conn = client - .borrow_connection() - .expect("Could not borrow_connection"); - - { - let write_lock = schema.properties(); - *write_lock.write() = HashMap::from([ - (0, "age".to_string()), - (1, "is_boring".to_string()), - (2, "something_else".to_string()), - (3, "secs_since_login".to_string()), - ]); - } - - { - let write_lock = schema.labels(); - *write_lock.write() = - HashMap::from([(0, "much".to_string()), (1, "actor".to_string())]); - } - - { - let write_lock = schema.relationships(); - *write_lock.write() = HashMap::from([(0, "very".to_string()), (1, "wow".to_string())]); - } + use crate::graph_schema::blocking::tests::open_readonly_graph_with_modified_schema; - (schema, conn) - } #[test] fn test_parse_edge() { - let (schema, mut conn) = open_readonly_graph(); + let (mut graph, mut conn) = open_readonly_graph_with_modified_schema(); let res = parse_type( 7, @@ -162,7 +122,7 @@ mod tests { ]), ]), ]), - &schema, + &mut graph.graph_schema, &mut conn, ); assert!(res.is_ok()); @@ -187,7 +147,7 @@ mod tests { #[test] fn test_parse_node() { - let (schema, mut conn) = open_readonly_graph(); + let (mut graph, mut conn) = open_readonly_graph_with_modified_schema(); let res = parse_type( 8, @@ -212,7 +172,7 @@ mod tests { ]), ]), ]), - &schema, + &mut graph.graph_schema, &mut conn, ); assert!(res.is_ok()); @@ -238,7 +198,7 @@ mod tests { #[test] fn test_parse_path() { - let (schema, mut conn) = open_readonly_graph(); + let (mut graph, mut conn) = open_readonly_graph_with_modified_schema(); let res = parse_type( 9, @@ -277,7 +237,7 @@ mod tests { ]), ]), ]), - &schema, + &mut graph.graph_schema, &mut conn, ); assert!(res.is_ok()); @@ -305,7 +265,7 @@ mod tests { #[test] fn test_parse_map() { - let (schema, mut conn) = open_readonly_graph(); + let (mut graph, mut conn) = open_readonly_graph_with_modified_schema(); let res = parse_type( 10, @@ -320,7 +280,7 @@ mod tests { FalkorValue::FString("key2".to_string()), FalkorValue::FArray(vec![FalkorValue::Int64(4), FalkorValue::FBool(true)]), ]), - &schema, + &mut graph.graph_schema, &mut conn, ); assert!(res.is_ok()); @@ -341,12 +301,12 @@ mod tests { #[test] fn test_parse_point() { - let (schema, mut conn) = open_readonly_graph(); + let (mut graph, mut conn) = open_readonly_graph_with_modified_schema(); let res = parse_type( 11, FalkorValue::FArray(vec![FalkorValue::F64(102.0), FalkorValue::F64(15.2)]), - &schema, + &mut graph.graph_schema, &mut conn, ); assert!(res.is_ok()); diff --git a/src/value/utils_async.rs b/src/value/utils_async.rs index 3ad680c..725fc14 100644 --- a/src/value/utils_async.rs +++ b/src/value/utils_async.rs @@ -18,10 +18,10 @@ pub(crate) async fn parse_labels_async( conn: &mut BorrowedAsyncConnection, schema_type: SchemaType, ) -> Result> { - let mut ids_hashset = HashSet::with_capacity(raw_ids.len()); - for label in raw_ids.iter() { - ids_hashset.insert(label.to_i64().ok_or(FalkorDBError::ParsingI64)?); - } + let ids_hashset = raw_ids + .iter() + .filter_map(|label_id| label_id.to_i64()) + .collect::>(); match match graph_schema.verify_id_set(&ids_hashset, schema_type).await { None => { From d77c7447d01ca6b041aa1e5a6e0cccd0268cf170 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Mon, 27 May 2024 21:26:24 +0300 Subject: [PATCH 23/62] Fixed docs --- src/connection/blocking.rs | 3 +-- src/graph/blocking.rs | 2 +- src/response/mod.rs | 2 +- src/value/path.rs | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 6d1ec16..b9f6413 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -5,8 +5,7 @@ use crate::{FalkorDBError, FalkorValue}; use anyhow::Result; -use std::fmt::Display; -use std::sync::mpsc; +use std::{fmt::Display, sync::mpsc}; pub(crate) enum FalkorSyncConnection { #[cfg(feature = "redis")] diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index c4c679c..eb0003a 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -413,7 +413,7 @@ impl SyncGraph { /// Calls the DB.INDICES procedure on the graph, returning all the indexing methods currently used /// /// # Returns - /// A [`Vec`] of [`Index`] + /// A [`Vec`] of [`FalkorIndex`] pub fn list_indices(&mut self) -> Result> { let mut conn = self.client.borrow_connection()?; let [_, indices, _]: [FalkorValue; 3] = self diff --git a/src/response/mod.rs b/src/response/mod.rs index c9cce3c..ed85f36 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -16,7 +16,7 @@ pub(crate) mod query_result; pub(crate) mod slowlog_entry; /// A naive wrapper for the various possible responses from queries -/// Its main usecase is for creating things like [`JoinSet`](tokio::task::JoinSet)s, which require all the tasks to return the same type +/// Its main usecase is for creating things like JoinSets, which require all the tasks to return the same type #[derive(Debug, Clone, PartialEq)] pub enum ResponseVariant { ExecutionPlan(ExecutionPlan), diff --git a/src/value/path.rs b/src/value/path.rs index 83e8bc2..64c0bb8 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -14,7 +14,7 @@ use crate::{ connection::asynchronous::BorrowedAsyncConnection, AsyncGraphSchema, FalkorAsyncParseable, }; -/// TODO: not exactly sure what this represents +/// Represents a path between two nodes, contains all the nodes, and the relationships between them along the path #[derive(Clone, Debug, PartialEq)] pub struct Path { pub nodes: Vec, From 1f8729097723ae248008ae8ec604262eb7f8e891 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 12:21:49 +0300 Subject: [PATCH 24/62] Fully sync safe --- src/client/blocking.rs | 11 ++++----- src/response/index.rs | 54 ++++++++++++++++++++---------------------- src/value/map.rs | 48 ++++++++++++++++++------------------- src/value/path.rs | 33 +++++++++----------------- src/value/utils.rs | 13 ++++------ 5 files changed, 70 insertions(+), 89 deletions(-) diff --git a/src/client/blocking.rs b/src/client/blocking.rs index f1a7c2a..18c6bc5 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -21,21 +21,18 @@ pub(crate) struct FalkorSyncClientInner { _inner: Mutex, connection_pool_size: u8, connection_pool_tx: mpsc::SyncSender, - connection_pool_rx: mpsc::Receiver, + connection_pool_rx: Mutex>, } impl FalkorSyncClientInner { pub(crate) fn borrow_connection(&self) -> Result { Ok(BorrowedSyncConnection::new( - self.connection_pool_rx.recv()?, + self.connection_pool_rx.lock().recv()?, self.connection_pool_tx.clone(), )) } } -unsafe impl Sync for FalkorSyncClientInner {} -unsafe impl Send for FalkorSyncClientInner {} - /// This is the publicly exposed API of the sync Falkor Client /// It makes no assumptions in regard to which database the Falkor module is running on, /// and will select it based on enabled features and url connection @@ -66,7 +63,7 @@ impl FalkorSyncClient { _inner: client.into(), connection_pool_size: num_connections, connection_pool_tx, - connection_pool_rx, + connection_pool_rx: Mutex::new(connection_pool_rx), }), _connection_info: connection_info, }) @@ -222,7 +219,7 @@ mod tests { }) .collect(); - let non_existing_conn = client.inner.connection_pool_rx.try_recv(); + let non_existing_conn = client.inner.connection_pool_rx.lock().try_recv(); assert!(non_existing_conn.is_err()); let Err(TryRecvError::Empty) = non_existing_conn else { diff --git a/src/response/index.rs b/src/response/index.rs index 6f890c5..7ce2c37 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -56,11 +56,11 @@ impl TryFrom<&String> for IndexStatus { /// The type of this indexed field #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum IndexType { - /// This index is a number + /// This index is a range Range, /// This index is raw vector data Vector, - /// This field is a string + /// This index is a string Fulltext, } @@ -110,6 +110,7 @@ fn parse_types_map(value: FalkorValue) -> Result> Ok(out_map) } +/// Contains all the info regarding an index on the database #[derive(Clone, Debug, PartialEq)] pub struct FalkorIndex { pub entity_type: EntityType, @@ -122,20 +123,9 @@ pub struct FalkorIndex { pub info: HashMap, } -impl FalkorParsable for FalkorIndex { - fn from_falkor_value( - value: FalkorValue, - graph_schema: &mut SyncGraphSchema, - conn: &mut BorrowedSyncConnection, - ) -> Result { - let value_vec = value.into_vec()?; - let mut semi_parsed_items = Vec::with_capacity(value_vec.len()); - for item in value_vec { - let (type_marker, val) = type_val_from_value(item)?; - semi_parsed_items.push(parse_type(type_marker, val, graph_schema, conn)?); - } - - let [label, fields, field_types, language, stopwords, entity_type, status, info]: [FalkorValue; 8] = semi_parsed_items.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; +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)?; Ok(Self { entity_type: EntityType::try_from(entity_type.into_string()?)?, @@ -150,6 +140,25 @@ impl FalkorParsable for FalkorIndex { } } +impl FalkorParsable for FalkorIndex { + fn from_falkor_value( + value: FalkorValue, + graph_schema: &mut SyncGraphSchema, + conn: &mut BorrowedSyncConnection, + ) -> 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, conn) + }) + .collect::>(); + + Self::from_raw_values(semi_parsed_items) + } +} + #[cfg(feature = "tokio")] impl FalkorAsyncParseable for FalkorIndex { async fn from_falkor_value_async( @@ -167,17 +176,6 @@ impl FalkorAsyncParseable for FalkorIndex { ); } - let [label, fields, field_types, language, stopwords, entity_type, status, info]: [FalkorValue; 8] = semi_parsed_items.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - Ok(Self { - entity_type: EntityType::try_from(entity_type.into_string()?)?, - status: IndexStatus::try_from(status.into_string()?)?, - 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()?, - }) + Self::from_raw_values(semi_parsed_items) } } diff --git a/src/value/map.rs b/src/value/map.rs index 2c9cb42..e231ddc 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -108,30 +108,30 @@ impl FalkorParsable for HashMap { Err(FalkorDBError::ParsingFMap)?; } - let mut new_map = HashMap::with_capacity(val_vec.len() / 2); - for pair in val_vec.chunks_exact(2) { - let [key, val]: [FalkorValue; 2] = pair - .to_vec() - .try_into() - .map_err(|_| FalkorDBError::ParsingFMap)?; - - let [type_marker, val]: [FalkorValue; 2] = val - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingFMap)?; - - new_map.insert( - key.into_string()?, - parse_type( - type_marker.to_i64().ok_or(FalkorDBError::ParsingKTVTypes)?, - val, - graph_schema, - conn, - )?, - ); - } - - Ok(new_map) + Ok(val_vec + .chunks_exact(2) + .flat_map(|pair| { + let [key, val]: [FalkorValue; 2] = pair + .to_vec() + .try_into() + .map_err(|_| FalkorDBError::ParsingFMap)?; + + let [type_marker, val]: [FalkorValue; 2] = val + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingFMap)?; + + Result::<_>::Ok(( + key.into_string()?, + parse_type( + type_marker.to_i64().ok_or(FalkorDBError::ParsingKTVTypes)?, + val, + graph_schema, + conn, + )?, + )) + }) + .collect()) } } diff --git a/src/value/path.rs b/src/value/path.rs index 64c0bb8..21db7eb 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -31,29 +31,18 @@ impl FalkorParsable for Path { .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - let (nodes, relationships) = (nodes.into_vec()?, relationships.into_vec()?); - - let mut parsed_nodes = Vec::with_capacity(nodes.len()); - for node_raw in nodes { - parsed_nodes.push(FalkorParsable::from_falkor_value( - node_raw, - graph_schema, - conn, - )?); - } - - let mut parsed_edges = Vec::with_capacity(relationships.len()); - for edge_raw in relationships { - parsed_edges.push(FalkorParsable::from_falkor_value( - edge_raw, - graph_schema, - conn, - )?); - } - Ok(Path { - nodes: parsed_nodes, - relationships: parsed_edges, + Ok(Self { + nodes: nodes + .into_vec()? + .into_iter() + .flat_map(|node| Node::from_falkor_value(node, graph_schema, conn)) + .collect(), + relationships: relationships + .into_vec()? + .into_iter() + .flat_map(|edge| Edge::from_falkor_value(edge, graph_schema, conn)) + .collect(), }) } } diff --git a/src/value/utils.rs b/src/value/utils.rs index 6d90d5a..ecfabb8 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -83,14 +83,11 @@ pub(crate) fn parse_type( pub(crate) fn parse_vec>( value: FalkorValue ) -> Result, FalkorDBError> { - let val_vec = value.into_vec()?; - - let mut out_vec = Vec::with_capacity(val_vec.len()); - for element in val_vec { - out_vec.push(element.try_into()?); - } - - Ok(out_vec) + Ok(value + .into_vec()? + .into_iter() + .flat_map(TryFrom::try_from) + .collect()) } #[cfg(test)] From bbb682fe58dd9f8fbed2e52cb5a2794a65df5687 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 14:37:27 +0300 Subject: [PATCH 25/62] Sync versions are much cleaner now --- src/client/asynchronous.rs | 16 +- src/client/blocking.rs | 16 +- src/graph/asynchronous.rs | 11 +- src/graph/blocking.rs | 300 +++++++++++++++++++-------------- src/graph_schema/utils.rs | 5 +- src/lib.rs | 3 +- src/parser/mod.rs | 2 + src/parser/utils.rs | 98 +++++++++++ src/redis_ext.rs | 12 +- src/response/constraint.rs | 98 ++++------- src/response/execution_plan.rs | 22 +-- src/response/mod.rs | 49 +++++- src/response/query_result.rs | 229 ------------------------- src/response/slowlog_entry.rs | 7 +- src/value/map.rs | 19 +-- src/value/path.rs | 1 + src/value/utils.rs | 33 ++-- 17 files changed, 416 insertions(+), 505 deletions(-) create mode 100644 src/parser/utils.rs delete mode 100644 src/response/query_result.rs diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index 4edf0c5..3473c46 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -4,8 +4,9 @@ */ use crate::{ - client::FalkorClientProvider, connection::asynchronous::BorrowedAsyncConnection, AsyncGraph, - AsyncGraphSchema, ConfigValue, FalkorAsyncConnection, FalkorConnectionInfo, FalkorDBError, + client::FalkorClientProvider, connection::asynchronous::BorrowedAsyncConnection, + parser::utils::string_vec_from_val, AsyncGraph, AsyncGraphSchema, ConfigValue, + FalkorAsyncConnection, FalkorConnectionInfo, FalkorDBError, }; use anyhow::Result; use std::{ @@ -100,14 +101,9 @@ impl FalkorAsyncClient { /// A [`Vec`] of [`String`]s, containing the names of available graphs pub async fn list_graphs(&self) -> Result> { let mut conn = self.borrow_connection().await?; - - Ok(conn - .send_command(None, "GRAPH.LIST", None, None) - .await? - .into_vec()? - .into_iter() - .flat_map(|data| data.into_string()) - .collect::>()) + conn.send_command(None, "GRAPH.LIST", None, None) + .await + .and_then(|res| string_vec_from_val(res).map_err(Into::into)) } /// Return the current value of a configuration option in the database. diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 18c6bc5..de05fd9 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -3,6 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use crate::parser::utils::string_vec_from_val; use crate::{ client::FalkorClientProvider, connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}, @@ -84,13 +85,8 @@ impl FalkorSyncClient { /// A [`Vec`] of [`String`]s, containing the names of available graphs pub fn list_graphs(&self) -> Result> { let mut conn = self.borrow_connection()?; - - let graph_list = conn.send_command::<&str>(None, "GRAPH.LIST", None, None)?; - Ok(graph_list - .into_vec()? - .into_iter() - .flat_map(|name| name.into_string()) - .collect()) + conn.send_command::<&str>(None, "GRAPH.LIST", None, None) + .and_then(|res| string_vec_from_val(res).map_err(Into::into)) } /// Return the current value of a configuration option in the database. @@ -248,7 +244,7 @@ mod tests { .query("MATCH (a:actor) return a".to_string()) .expect("Could not get actors from unmodified graph"); - assert_eq!(res.result_set.len(), 1317); + assert_eq!(res.data.len(), 1317); } #[test] @@ -271,11 +267,11 @@ mod tests { .inner .query("MATCH (a:actor) RETURN a".to_string()) .expect("Could not get actors from unmodified graph") - .result_set, + .data, original_graph .query("MATCH (a:actor) RETURN a".to_string()) .expect("Could not get actors from unmodified graph") - .result_set + .data ) } diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index c70d55b..a613765 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -71,12 +71,7 @@ impl AsyncGraph { let mut slowlog_entries = Vec::with_capacity(res.len()); for entry_raw in res { - slowlog_entries.push(SlowlogEntry::from_value_array( - entry_raw - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?, - )?); + slowlog_entries.push(SlowlogEntry::from_value_vec(entry_raw.into_vec()?)?); } Ok(slowlog_entries) @@ -320,7 +315,7 @@ impl AsyncGraph { /// A [`Vec`] of [`Index`] pub async fn list_indices(&self) -> Result> { let mut conn = self.client.borrow_connection().await?; - let [_, indices, _]: [FalkorValue; 3] = self + let [header, indices, stats]: [FalkorValue; 3] = self .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false, None) .await? .into_vec()? @@ -429,7 +424,7 @@ impl AsyncGraph { /// A [`Vec`] of [`Constraint`]s pub async fn list_constraints(&self) -> Result> { let mut conn = self.client.borrow_connection().await?; - let [_, query_res, _]: [FalkorValue; 3] = self + let [header, query_res, stats]: [FalkorValue; 3] = self .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, false, None) .await? .into_vec()? diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index eb0003a..7e24467 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -6,8 +6,10 @@ use crate::{ client::blocking::FalkorSyncClientInner, graph::utils::{construct_query, generate_procedure_call}, + parser::utils::{parse_header, parse_result_set}, Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorDBError, FalkorIndex, - FalkorParsable, FalkorValue, IndexType, QueryResult, SlowlogEntry, SyncGraphSchema, + FalkorParsable, FalkorResponse, FalkorValue, IndexType, ResultSet, SlowlogEntry, + SyncGraphSchema, }; use anyhow::Result; use std::{collections::HashMap, fmt::Display, sync::Arc}; @@ -62,17 +64,10 @@ impl SyncGraph { return Ok(vec![]); } - let mut slowlog_entries = Vec::with_capacity(res.len()); - for entry_raw in res { - slowlog_entries.push(SlowlogEntry::from_value_array( - entry_raw - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?, - )?); - } - - Ok(slowlog_entries) + Ok(res + .into_iter() + .flat_map(|entry_raw| SlowlogEntry::from_value_vec(entry_raw.into_vec()?)) + .collect()) } /// Resets the slowlog, all query time data will be cleared. @@ -151,47 +146,89 @@ impl SyncGraph { self.explain_with_params::(query_string, None) } - fn query_inner_with_timeout( + fn query_inner_with_timeout( &mut self, command: &str, query_string: Q, params: Option<&HashMap>, timeout: i64, - ) -> Result

{ + ) -> Result> { let query = construct_query(query_string, params); - let mut conn = self.client.borrow_connection()?; - P::from_falkor_value( - conn.send_command( + + let [header, data, stats]: [FalkorValue; 3] = conn + .send_command( Some(self.graph_name.as_str()), command, None, - Some(&[query, "--compact".to_string(), format!("timeout {timeout}")]), - )?, - &mut self.graph_schema, - &mut conn, + Some(&[ + query.as_str(), + "--compact", + format!("timeout {timeout}").as_str(), + ]), + )? + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + let header_keys = parse_header(header)?; + FalkorResponse::from_response_with_headers( + parse_result_set(data, &mut self.graph_schema, &mut conn, &header_keys)?, + header_keys, + stats, ) + .map_err(Into::into) } - fn query_inner( + fn query_inner( &mut self, command: &str, query_string: Q, params: Option<&HashMap>, - ) -> Result

{ + ) -> Result> { let query = construct_query(query_string, params); - let mut conn = self.client.borrow_connection()?; - P::from_falkor_value( - conn.send_command( + + let res = conn + .send_command( Some(self.graph_name.as_str()), command, None, Some(&[query, "--compact".to_string()]), - )?, - &mut self.graph_schema, - &mut conn, - ) + )? + .into_vec()?; + + match res.len() { + 1 => { + let stats = res + .into_iter() + .next() + .ok_or(FalkorDBError::ParsingArrayToStructElementCount)?; + + FalkorResponse::from_response(None, vec![], stats) + } + 2 => { + let [header, stats]: [FalkorValue; 2] = res + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + FalkorResponse::from_response(Some(header), vec![], stats) + } + 3 => { + let [header, data, stats]: [FalkorValue; 3] = res + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + let header_keys = parse_header(header)?; + FalkorResponse::from_response_with_headers( + parse_result_set(data, &mut self.graph_schema, &mut conn, &header_keys)?, + header_keys, + stats, + ) + } + _ => Err(FalkorDBError::ParsingArrayToStructElementCount), + } + .map_err(Into::into) } /// Run a query on the graph @@ -204,8 +241,8 @@ impl SyncGraph { pub fn query( &mut self, query_string: Q, - ) -> Result { - self.query_inner::("GRAPH.QUERY", query_string, None) + ) -> Result> { + self.query_inner::("GRAPH.QUERY", query_string, None) } /// Run a query on the graph, but abort it if it exceeds the timeout @@ -220,13 +257,8 @@ impl SyncGraph { &mut self, query_string: Q, timeout: i64, - ) -> Result { - self.query_inner_with_timeout::( - "GRAPH.QUERY", - query_string, - None, - timeout, - ) + ) -> Result> { + self.query_inner_with_timeout::("GRAPH.QUERY", query_string, None, timeout) } /// Run a query on the graph @@ -237,12 +269,12 @@ impl SyncGraph { /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. /// /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query pub fn query_with_params( &mut self, query_string: Q, params: &HashMap, - ) -> Result { + ) -> Result> { self.query_inner("GRAPH.QUERY", query_string, Some(params)) } @@ -261,7 +293,7 @@ impl SyncGraph { query_string: Q, timeout: i64, params: &HashMap, - ) -> Result { + ) -> Result> { self.query_inner_with_timeout("GRAPH.QUERY", query_string, Some(params), timeout) } @@ -272,12 +304,12 @@ impl SyncGraph { /// * `query_string`: The query to run /// /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query pub fn query_readonly( &mut self, query_string: Q, - ) -> Result { - self.query_inner::("GRAPH.QUERY_RO", query_string, None) + ) -> Result> { + self.query_inner::("GRAPH.QUERY_RO", query_string, None) } /// Run a query on the graph, but abort it if it exceeds the timeout @@ -288,13 +320,13 @@ impl SyncGraph { /// * `timeout`: Specify how long should the query run before aborting. /// /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query pub fn query_readonly_with_timeout( &mut self, query_string: Q, timeout: i64, - ) -> Result { - self.query_inner_with_timeout::( + ) -> Result> { + self.query_inner_with_timeout::( "GRAPH.QUERY_RO", query_string, None, @@ -312,12 +344,12 @@ impl SyncGraph { /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. /// /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query pub fn query_readonly_with_params( &mut self, query_string: Q, params: &HashMap, - ) -> Result { + ) -> Result> { self.query_inner("GRAPH.QUERY_RO", query_string, Some(params)) } @@ -331,13 +363,13 @@ impl SyncGraph { /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. /// /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query pub fn query_readonly_with_params_and_timeout( &mut self, query_string: Q, params: &HashMap, timeout: i64, - ) -> Result { + ) -> Result> { self.query_inner_with_timeout("GRAPH.QUERY_RO", query_string, Some(params), timeout) } @@ -362,15 +394,22 @@ impl SyncGraph { read_only: bool, ) -> Result

{ let (query_string, params) = generate_procedure_call(procedure, args, yields); + let query = construct_query(query_string, params.as_ref()); + let mut conn = self.client.borrow_connection()?; - self.query_inner( - if read_only { - "GRAPH.QUERY_RO" - } else { - "GRAPH.QUERY" - }, - query_string, - params.as_ref(), + P::from_falkor_value( + conn.send_command( + Some(self.graph_name.as_str()), + if read_only { + "GRAPH.QUERY_RO" + } else { + "GRAPH.QUERY" + }, + None, + Some(&[query, "--compact".to_string()]), + )?, + &mut self.graph_schema, + &mut conn, ) } @@ -397,16 +436,26 @@ impl SyncGraph { timeout: i64, ) -> Result

{ let (query_string, params) = generate_procedure_call(procedure, args, yields); + let query = construct_query(query_string, params.as_ref()); + let mut conn = self.client.borrow_connection()?; - self.query_inner_with_timeout( - if read_only { - "GRAPH.QUERY_RO" - } else { - "GRAPH.QUERY" - }, - query_string, - params.as_ref(), - timeout, + P::from_falkor_value( + conn.send_command( + Some(self.graph_name.as_str()), + if read_only { + "GRAPH.QUERY_RO" + } else { + "GRAPH.QUERY" + }, + None, + Some(&[ + query.as_str(), + "--compact", + format!("timeout {timeout}").as_str(), + ]), + )?, + &mut self.graph_schema, + &mut conn, ) } @@ -414,26 +463,26 @@ impl SyncGraph { /// /// # Returns /// A [`Vec`] of [`FalkorIndex`] - pub fn list_indices(&mut self) -> Result> { + pub fn list_indices(&mut self) -> Result>> { let mut conn = self.client.borrow_connection()?; - let [_, indices, _]: [FalkorValue; 3] = self + let [header, indices, stats]: [FalkorValue; 3] = self .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false)? .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - let indices = indices.into_vec()?; - - let mut out_vec = Vec::with_capacity(indices.len()); - for index in indices { - out_vec.push(FalkorIndex::from_falkor_value( - index, - &mut self.graph_schema, - &mut conn, - )?); - } - - Ok(out_vec) + FalkorResponse::from_response( + Some(header), + indices + .into_vec()? + .into_iter() + .flat_map(|index| { + FalkorIndex::from_falkor_value(index, &mut self.graph_schema, &mut conn) + }) + .collect(), + stats, + ) + .map_err(Into::into) } pub fn create_index( @@ -443,7 +492,7 @@ impl SyncGraph { label: L, properties: &[P], options: Option<&HashMap>, - ) -> Result { + ) -> Result> { // Create index from these properties let properties_string = properties .iter() @@ -490,7 +539,7 @@ impl SyncGraph { entity_type: EntityType, label: L, properties: &[P], - ) -> Result { + ) -> Result> { let properties_string = properties .iter() .map(|element| format!("e.{}", element.to_string())) @@ -519,15 +568,16 @@ 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 - pub fn list_constraints(&mut self) -> Result<(Vec, Vec)> { + pub fn list_constraints(&mut self) -> Result>> { let mut conn = self.client.borrow_connection()?; - let [_, query_res, stats]: [FalkorValue; 3] = self + let [header, query_res, stats]: [FalkorValue; 3] = self .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, false)? .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - Ok(( + FalkorResponse::from_response( + Some(header), query_res .into_vec()? .into_iter() @@ -535,12 +585,9 @@ impl SyncGraph { Constraint::from_falkor_value(item, &mut self.graph_schema, &mut conn) }) .collect(), - stats - .into_vec()? - .into_iter() - .flat_map(|stat| stat.into_string()) - .collect(), - )) + stats, + ) + .map_err(Into::into) } /// Creates a new constraint for this graph, making the provided properties mandatory @@ -549,11 +596,11 @@ 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. - pub fn create_mandatory_constraint( + pub fn create_mandatory_constraint( &self, entity_type: EntityType, - label: L, - properties: &[P], + label: &str, + properties: &[&str], ) -> Result { let mut params = Vec::with_capacity(5 + properties.len()); params.extend([ @@ -638,29 +685,34 @@ mod tests { #[test] fn test_create_drop_index() { let mut graph = open_test_graph("test_create_drop_index"); - let res = graph.inner.create_index( - IndexType::Fulltext, - EntityType::Node, - "actor".to_string(), - &["Hello"], - None, - ); - assert!(res.is_ok()); - - let res = graph.inner.list_indices(); - assert!(res.is_ok()); + graph + .inner + .create_index( + IndexType::Fulltext, + EntityType::Node, + "actor".to_string(), + &["Hello"], + None, + ) + .expect("Could not create index"); - let indices = res.unwrap(); - assert_eq!(indices.len(), 2); - assert_eq!(indices[0].field_types["Hello"], vec![IndexType::Fulltext]); + let indices = graph.inner.list_indices().expect("Could not list indices"); - let res = graph.inner.drop_index( - IndexType::Fulltext, - EntityType::Node, - "actor".to_string(), - &["Hello"], + assert_eq!(indices.data.len(), 2); + assert_eq!( + indices.data[0].field_types["Hello"], + vec![IndexType::Fulltext] ); - assert!(res.is_ok()); + + graph + .inner + .drop_index( + IndexType::Fulltext, + EntityType::Node, + "actor".to_string(), + &["Hello"], + ) + .expect("Could not drop index"); } #[test] @@ -668,12 +720,12 @@ mod tests { let mut graph = open_test_graph("test_list_indices"); let indices = graph.inner.list_indices().expect("Could not list indices"); - assert_eq!(indices.len(), 1); - assert_eq!(indices[0].entity_type, EntityType::Node); - assert_eq!(indices[0].index_label, "actor".to_string()); - assert_eq!(indices[0].field_types.len(), 2); + assert_eq!(indices.data.len(), 1); + assert_eq!(indices.data[0].entity_type, EntityType::Node); + assert_eq!(indices.data[0].index_label, "actor".to_string()); + assert_eq!(indices.data[0].field_types.len(), 2); assert_eq!( - indices[0].field_types, + indices.data[0].field_types, HashMap::from([ ("age".to_string(), vec![IndexType::Range]), ("name".to_string(), vec![IndexType::Fulltext]) @@ -738,11 +790,11 @@ mod tests { ) .expect("Could not create constraint"); - let (constraints, _) = graph + let res = graph .inner .list_constraints() .expect("Could not list constraints"); - assert_eq!(constraints.len(), 1); + assert_eq!(res.data.len(), 1); } #[test] diff --git a/src/graph_schema/utils.rs b/src/graph_schema/utils.rs index 2d87531..4ca6b92 100644 --- a/src/graph_schema/utils.rs +++ b/src/graph_schema/utils.rs @@ -77,10 +77,9 @@ mod tests { #[test] fn test_update_map() { let mut map_to_update = HashMap::from([(5, "Ye Olde Value".to_string())]); - let res = update_map(&mut map_to_update, get_test_keys(), None); - assert!(res.is_ok()); + let relevant_ids = + update_map(&mut map_to_update, get_test_keys(), None).expect("Could not update map"); - let relevant_ids = res.unwrap(); assert!(relevant_ids.is_none()); assert_eq!(map_to_update.get(&0), Some(&"Hello".to_string())); diff --git a/src/lib.rs b/src/lib.rs index bca3d99..b6f4b49 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -29,9 +29,8 @@ pub use response::{ constraint::{Constraint, ConstraintStatus, ConstraintType}, execution_plan::ExecutionPlan, index::{FalkorIndex, IndexStatus, IndexType}, - query_result::QueryResult, slowlog_entry::SlowlogEntry, - ResponseVariant, + FalkorResponse, ResponseEnum, ResultSet, }; pub use value::{ config::ConfigValue, diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 600972c..dc4f32d 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -3,6 +3,8 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +pub mod utils; + use crate::{connection::blocking::BorrowedSyncConnection, FalkorValue, SyncGraphSchema}; use anyhow::Result; diff --git a/src/parser/utils.rs b/src/parser/utils.rs new file mode 100644 index 0000000..472b13e --- /dev/null +++ b/src/parser/utils.rs @@ -0,0 +1,98 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use crate::{ + connection::blocking::BorrowedSyncConnection, + value::utils::{parse_type, type_val_from_value}, + FalkorDBError, FalkorValue, SyncGraphSchema, +}; +use anyhow::Result; +use std::collections::HashMap; + +#[cfg(feature = "tokio")] +use crate::{ + connection::asynchronous::BorrowedAsyncConnection, value::utils_async::parse_type_async, + AsyncGraphSchema, +}; + +pub fn string_vec_from_val(value: FalkorValue) -> Result, FalkorDBError> { + value.into_vec().map(|value_as_vec| { + value_as_vec + .into_iter() + .flat_map(FalkorValue::into_string) + .collect() + }) +} + +pub fn parse_header(header: FalkorValue) -> Result, FalkorDBError> { + header.into_vec().map(|header_as_vec| { + header_as_vec + .into_iter() + .flat_map(|item| { + let item_vec = item.into_vec()?; + if item_vec.len() == 2 { + let [_, key]: [FalkorValue; 2] = item_vec + .try_into() + .map_err(|_| FalkorDBError::ParsingHeader)?; + key + } else { + item_vec + .into_iter() + .next() + .ok_or(FalkorDBError::ParsingHeader)? + } + .into_string() + }) + .collect() + }) +} + +pub(crate) fn parse_result_set( + data: FalkorValue, + graph_schema: &mut SyncGraphSchema, + conn: &mut BorrowedSyncConnection, + header_keys: &[String], +) -> Result>> { + Ok(data + .into_vec()? + .into_iter() + .flat_map(|column| { + anyhow::Result::<_, FalkorDBError>::Ok( + header_keys + .iter() + .cloned() + .zip(column.into_vec()?.into_iter().flat_map(|column_subitem| { + type_val_from_value(column_subitem).and_then(|(type_marker, val)| { + parse_type(type_marker, val, graph_schema, conn) + }) + })) + .collect(), + ) + }) + .collect()) +} + +#[cfg(feature = "tokio")] +pub(crate) async fn parse_result_set_async( + data_vec: Vec, + graph_schema: &AsyncGraphSchema, + conn: &mut BorrowedAsyncConnection, + header_keys: &[String], +) -> Result>> { + let mut parsed_result_set = Vec::with_capacity(data_vec.len()); + for column in data_vec { + let column_vec = column.into_vec()?; + + let mut parsed_column = Vec::with_capacity(column_vec.len()); + for column_item in column_vec { + let (type_marker, val) = type_val_from_value(column_item)?; + parsed_column.push(parse_type_async(type_marker, val, graph_schema, conn).await?); + } + + parsed_result_set.push(header_keys.iter().cloned().zip(parsed_column).collect()) + } + + Ok(parsed_result_set) +} diff --git a/src/redis_ext.rs b/src/redis_ext.rs index 0938604..44c9cdb 100644 --- a/src/redis_ext.rs +++ b/src/redis_ext.rs @@ -70,13 +70,11 @@ impl FromRedisValue for FalkorValue { redis::Value::Data(str_val) => { FalkorValue::FString(String::from_utf8_lossy(str_val.as_slice()).to_string()) } - redis::Value::Bulk(bulk) => FalkorValue::FArray({ - let mut new_vec = Vec::with_capacity(bulk.len()); - for element in bulk { - new_vec.push(FalkorValue::from_redis_value(element)?); - } - new_vec - }), + redis::Value::Bulk(bulk) => FalkorValue::FArray( + bulk.iter() + .flat_map(FalkorValue::from_redis_value) + .collect(), + ), redis::Value::Status(status) => FalkorValue::FString(status.to_string()), redis::Value::Okay => FalkorValue::None, }) diff --git a/src/response/constraint.rs b/src/response/constraint.rs index cd6eb61..584493b 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -4,16 +4,16 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, - value::utils::{parse_type, type_val_from_value}, - EntityType, FalkorDBError, FalkorParsable, FalkorValue, SyncGraphSchema, + connection::blocking::BorrowedSyncConnection, value::utils::parse_type, EntityType, + FalkorDBError, FalkorParsable, FalkorValue, SyncGraphSchema, }; use anyhow::Result; use std::fmt::{Display, Formatter}; #[cfg(feature = "tokio")] use crate::{ - connection::asynchronous::BorrowedAsyncConnection, AsyncGraphSchema, FalkorAsyncParseable, + connection::asynchronous::BorrowedAsyncConnection, value::utils_async::parse_type_async, + AsyncGraphSchema, FalkorAsyncParseable, }; /// The type of restriction to apply for the property @@ -122,40 +122,35 @@ pub struct Constraint { pub status: ConstraintStatus, } +impl Constraint { + fn from_value_vec(vlaue_vec: Vec) -> Result { + let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = vlaue_vec.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + Ok(Constraint { + constraint_type: constraint_type_raw.into_string()?.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()?.try_into()?, + status: status_raw.into_string()?.try_into()?, + }) + } +} + impl FalkorParsable for Constraint { fn from_falkor_value( value: FalkorValue, graph_schema: &mut SyncGraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { - let value_vec = value.into_vec()?; - - let mut parsed_values = Vec::with_capacity(value_vec.len()); - for column_raw in value_vec { - parsed_values.push(type_val_from_value(column_raw).and_then( - |(type_marker, raw_val)| parse_type(type_marker, raw_val, graph_schema, conn), - )?); - } - - let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = parsed_values.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let constraint_type = constraint_type_raw.into_string()?.try_into()?; - let label = label_raw.try_into()?; - let entity_type = entity_type_raw.into_string()?.try_into()?; - let status = status_raw.into_string()?.try_into()?; - - let properties_vec = properties_raw.into_vec()?; - let mut properties = Vec::with_capacity(properties_vec.len()); - for property in properties_vec { - properties.push(property.into_string()?); - } - - Ok(Constraint { - constraint_type, - label, - properties, - entity_type, - status, + parse_type(6, value, graph_schema, conn).and_then(|parsed| { + parsed + .into_vec() + .map_err(Into::into) + .and_then(Constraint::from_value_vec) }) } } @@ -167,36 +162,13 @@ impl FalkorAsyncParseable for Constraint { graph_schema: &AsyncGraphSchema, conn: &mut BorrowedAsyncConnection, ) -> Result { - let value_vec = value.into_vec()?; - - let mut parsed_values = Vec::with_capacity(value_vec.len()); - for column_raw in value_vec { - let (type_marker, val) = type_val_from_value(column_raw)?; - parsed_values.push( - crate::value::utils_async::parse_type_async(type_marker, val, graph_schema, conn) - .await?, - ); - } - - let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = parsed_values.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let constraint_type = constraint_type_raw.into_string()?.try_into()?; - let label = label_raw.try_into()?; - let entity_type = entity_type_raw.into_string()?.try_into()?; - let status = status_raw.into_string()?.try_into()?; - - let properties_vec = properties_raw.into_vec()?; - let mut properties = Vec::with_capacity(properties_vec.len()); - for property in properties_vec { - properties.push(property.into_string()?); - } - - Ok(Constraint { - constraint_type, - label, - properties, - entity_type, - status, - }) + parse_type_async(6, value, graph_schema, conn) + .await + .and_then(|value_vec| { + value_vec + .into_vec() + .map_err(Into::into) + .and_then(Constraint::from_value_vec) + }) } } diff --git a/src/response/execution_plan.rs b/src/response/execution_plan.rs index a7889b4..15989c8 100644 --- a/src/response/execution_plan.rs +++ b/src/response/execution_plan.rs @@ -28,22 +28,18 @@ impl TryFrom for ExecutionPlan { type Error = FalkorDBError; fn try_from(value: FalkorValue) -> Result { - let string_vec = value.into_vec()?; - - let (mut execution_plan, mut execution_plan_text) = ( - Vec::with_capacity(string_vec.len()), - Vec::with_capacity(string_vec.len()), - ); - execution_plan_text.push(String::new()); - for item in string_vec { - let raw_text = item.into_string()?; - execution_plan.push(raw_text.trim().to_string()); - execution_plan_text.push(raw_text); - } + let (execution_plan, execution_plan_text): (Vec<_>, Vec<_>) = value + .into_vec()? + .into_iter() + .flat_map(|item| { + let item = item.into_string()?; + Result::<_, FalkorDBError>::Ok((item.trim().to_string(), item)) + }) + .unzip(); Ok(ExecutionPlan { steps: execution_plan, - text: execution_plan_text.join("\n"), + text: format!("\n{}", execution_plan_text.join("\n")), }) } } diff --git a/src/response/mod.rs b/src/response/mod.rs index ed85f36..c943d17 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -3,25 +3,66 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use crate::{ + parser::utils::{parse_header, string_vec_from_val}, + FalkorDBError, FalkorValue, +}; use constraint::Constraint; use execution_plan::ExecutionPlan; use index::FalkorIndex; -use query_result::QueryResult; use slowlog_entry::SlowlogEntry; +use std::collections::HashMap; pub(crate) mod constraint; pub(crate) mod execution_plan; pub(crate) mod index; -pub(crate) mod query_result; pub(crate) mod slowlog_entry; +pub type ResultSet = Vec>; + /// A naive wrapper for the various possible responses from queries /// Its main usecase is for creating things like JoinSets, which require all the tasks to return the same type #[derive(Debug, Clone, PartialEq)] -pub enum ResponseVariant { +pub enum ResponseEnum { ExecutionPlan(ExecutionPlan), - QueryResult(QueryResult), + QueryResult(ResultSet), SlowlogEntry(SlowlogEntry), Constraints(Vec), Indices(Vec), } + +#[derive(Clone, Debug, Default)] +pub struct FalkorResponse { + pub header: Vec, + pub data: T, + pub stats: Vec, +} + +impl FalkorResponse { + pub fn from_response( + headers: Option, + data: T, + stats: FalkorValue, + ) -> Result { + Ok(Self { + header: match headers { + Some(headers) => parse_header(headers)?, + None => vec![], + }, + data, + stats: string_vec_from_val(stats)?, + }) + } + + pub fn from_response_with_headers( + data: T, + header: Vec, + stats: FalkorValue, + ) -> Result { + Ok(Self { + header, + data, + stats: string_vec_from_val(stats)?, + }) + } +} diff --git a/src/response/query_result.rs b/src/response/query_result.rs deleted file mode 100644 index f883a99..0000000 --- a/src/response/query_result.rs +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{ - connection::blocking::BorrowedSyncConnection, - value::utils::{parse_type, type_val_from_value}, - FalkorDBError, FalkorParsable, FalkorValue, SyncGraphSchema, -}; -use anyhow::Result; -use std::collections::HashMap; - -#[cfg(feature = "tokio")] -use crate::{ - connection::asynchronous::BorrowedAsyncConnection, value::utils_async::parse_type_async, - AsyncGraphSchema, FalkorAsyncParseable, -}; - -/// A struct returned by the various queries, containing the result set, header, and stats -#[derive(Clone, Debug, Default, PartialEq)] -pub struct QueryResult { - /// The statistics for this query, such as how long it took - pub stats: Vec, - pub header: Vec, - pub result_set: Vec>, -} - -impl QueryResult { - /// Returns a slice of the statistics for this query, such as how long it took - pub fn stats(&self) -> &[String] { - self.stats.as_slice() - } - - /// Returns a slice of header for this query result, usually contains the verbose names of each column of the result set - pub fn header(&self) -> &[String] { - self.header.as_slice() - } - - /// Returns the result set as a slice which can be iterated without taking ownership - pub fn result_set(&self) -> &[HashMap] { - self.result_set.as_slice() - } -} - -fn query_parse_header(header: FalkorValue) -> Result> { - let header_vec = header.into_vec()?; - - let mut keys = Vec::with_capacity(header_vec.len()); - for item in header_vec { - let item_vec = item.into_vec()?; - let key = if item_vec.len() == 2 { - let [_, key]: [FalkorValue; 2] = item_vec - .try_into() - .map_err(|_| FalkorDBError::ParsingHeader)?; - key - } else { - item_vec - .into_iter() - .next() - .ok_or(FalkorDBError::ParsingHeader)? - } - .into_string()?; - keys.push(key); - } - - Ok(keys) -} - -fn query_parse_stats(stats: FalkorValue) -> Result> { - let stats_vec = stats.into_vec()?; - - let mut stats_strings = Vec::with_capacity(stats_vec.len()); - for element in stats_vec { - stats_strings.push(element.into_string()?); - } - - Ok(stats_strings) -} - -pub(crate) fn parse_result_set( - data_vec: Vec, - graph_schema: &mut SyncGraphSchema, - conn: &mut BorrowedSyncConnection, - header_keys: &[String], -) -> Result>> { - let mut parsed_result_set = Vec::with_capacity(data_vec.len()); - for column in data_vec { - let column_vec = column.into_vec()?; - - let mut parsed_column = Vec::with_capacity(column_vec.len()); - for column_item in column_vec { - let (type_marker, val) = type_val_from_value(column_item)?; - parsed_column.push(parse_type(type_marker, val, graph_schema, conn)?); - } - - parsed_result_set.push(header_keys.iter().cloned().zip(parsed_column).collect()) - } - - Ok(parsed_result_set) -} - -impl FalkorParsable for QueryResult { - fn from_falkor_value( - value: FalkorValue, - graph_schema: &mut SyncGraphSchema, - conn: &mut BorrowedSyncConnection, - ) -> Result { - let value_vec = value.into_vec()?; - - // Empty result - if value_vec.is_empty() { - return Ok(QueryResult::default()); - } - - // Result only contains stats - if value_vec.len() == 1 { - let first_element = value_vec.into_iter().nth(0).unwrap(); - return Ok(QueryResult { - stats: query_parse_stats(first_element)?, - ..Default::default() - }); - } - - // Invalid response - if value_vec.len() == 2 { - Err(FalkorDBError::InvalidDataReceived)?; - } - - // Full result - let [header, data, stats]: [FalkorValue; 3] = value_vec - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let header_keys = query_parse_header(header)?; - let stats_strings = query_parse_stats(stats)?; - - let data_vec = data.into_vec()?; - let data_len = data_vec.len(); - - let result_set = parse_result_set(data_vec, graph_schema, conn, &header_keys)?; - - if result_set.len() != data_len { - Err(FalkorDBError::ParsingError)?; - } - - Ok(Self { - stats: stats_strings, - header: header_keys, - result_set, - }) - } -} - -#[cfg(feature = "tokio")] -async fn parse_result_set_async( - data_vec: Vec, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, - header_keys: &[String], -) -> Result>> { - let mut parsed_result_set = Vec::with_capacity(data_vec.len()); - for column in data_vec { - let column_vec = column.into_vec()?; - - let mut parsed_column = Vec::with_capacity(column_vec.len()); - for column_item in column_vec { - let (type_marker, val) = type_val_from_value(column_item)?; - parsed_column.push(parse_type_async(type_marker, val, graph_schema, conn).await?); - } - - parsed_result_set.push(header_keys.iter().cloned().zip(parsed_column).collect()) - } - - Ok(parsed_result_set) -} - -#[cfg(feature = "tokio")] -impl FalkorAsyncParseable for QueryResult { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, - ) -> Result { - let value_vec = value.into_vec()?; - - // Empty result - if value_vec.is_empty() { - return Ok(QueryResult::default()); - } - - // Result only contains stats - if value_vec.len() == 1 { - let first_element = value_vec.into_iter().nth(0).unwrap(); - return Ok(QueryResult { - stats: query_parse_stats(first_element)?, - ..Default::default() - }); - } - - // Invalid response - if value_vec.len() == 2 { - Err(FalkorDBError::InvalidDataReceived)?; - } - - // Full result - let [header, data, stats]: [FalkorValue; 3] = value_vec - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let header_keys = query_parse_header(header)?; - let stats_strings = query_parse_stats(stats)?; - - let data_vec = data.into_vec()?; - let data_len = data_vec.len(); - - let result_set = parse_result_set_async(data_vec, graph_schema, conn, &header_keys).await?; - - if result_set.len() != data_len { - Err(FalkorDBError::ParsingError)?; - } - - Ok(Self { - stats: stats_strings, - header: header_keys, - result_set, - }) - } -} diff --git a/src/response/slowlog_entry.rs b/src/response/slowlog_entry.rs index 90ca1d8..fff0c0a 100644 --- a/src/response/slowlog_entry.rs +++ b/src/response/slowlog_entry.rs @@ -20,8 +20,11 @@ pub struct SlowlogEntry { } impl SlowlogEntry { - pub fn from_value_array(values: [FalkorValue; 4]) -> Result { - let [timestamp, command, arguments, time_taken] = values; + pub(crate) fn from_value_vec(values: Vec) -> Result { + let [timestamp, command, arguments, time_taken] = values + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + Ok(Self { timestamp: timestamp .into_string()? diff --git a/src/value/map.rs b/src/value/map.rs index e231ddc..69cd574 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -8,7 +8,7 @@ use crate::{ FalkorParsable, FalkorValue, SchemaType, SyncGraphSchema, }; use anyhow::Result; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; #[cfg(feature = "tokio")] use crate::{ @@ -74,17 +74,12 @@ pub(crate) fn parse_map_with_schema( conn: &mut BorrowedSyncConnection, schema_type: SchemaType, ) -> Result> { - let val_vec = value.into_vec()?; - let (mut id_hashset, mut map_vec) = ( - HashSet::with_capacity(val_vec.len()), - Vec::with_capacity(val_vec.len()), - ); - - for item in val_vec { - let fktv = FKeyTypeVal::try_from(item)?; - id_hashset.insert(fktv.key); - map_vec.push(fktv); - } + let (id_hashset, map_vec) = value + .into_vec()? + .into_iter() + .flat_map(FKeyTypeVal::try_from) + .map(|fktv| (fktv.key, fktv)) + .unzip(); if let Some(relevant_ids_map) = graph_schema.verify_id_set(&id_hashset, schema_type) { return ktv_vec_to_map(map_vec, relevant_ids_map, graph_schema, conn); diff --git a/src/value/path.rs b/src/value/path.rs index 21db7eb..443a491 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -58,6 +58,7 @@ impl FalkorAsyncParseable for Path { .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + let (nodes, relationships) = (nodes.into_vec()?, relationships.into_vec()?); let mut parsed_nodes = Vec::with_capacity(nodes.len()); diff --git a/src/value/utils.rs b/src/value/utils.rs index ecfabb8..686ebb8 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -21,20 +21,16 @@ pub(crate) fn parse_labels( .filter_map(|label_id| label_id.to_i64()) .collect::>(); - match match graph_schema.verify_id_set(&ids_hashset, schema_type) { + let relevant_ids = match graph_schema.verify_id_set(&ids_hashset, schema_type) { None => graph_schema.refresh(schema_type, conn, Some(&ids_hashset))?, relevant_ids => relevant_ids, - } { - Some(relevant_ids) => { - let parsed_ids = raw_ids - .into_iter() - .filter_map(|id| id.to_i64().and_then(|id| relevant_ids.get(&id).cloned())) - .collect(); - - Ok(parsed_ids) - } - _ => Err(FalkorDBError::ParsingError)?, } + .ok_or(FalkorDBError::ParsingError)?; + + Ok(raw_ids + .into_iter() + .filter_map(|id| id.to_i64().and_then(|id| relevant_ids.get(&id).cloned())) + .collect()) } pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValue)> { @@ -60,13 +56,14 @@ pub(crate) fn parse_type( 4 => FalkorValue::FBool(val.to_bool().ok_or(FalkorDBError::ParsingBool)?), 5 => FalkorValue::F64(val.try_into()?), 6 => FalkorValue::FArray({ - let val = val.into_vec()?; - let mut parsed_vec = Vec::with_capacity(val.len()); - for item in val { - let (type_marker, val) = type_val_from_value(item)?; - parsed_vec.push(parse_type(type_marker, val, graph_schema, conn)?); - } - parsed_vec + 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, conn) + }) + }) + .collect() }), // The following types are sent as an array and require specific parsing functions 7 => FalkorValue::FEdge(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), From cdbcc96e83e2922e0a36b2f1edc27119f7c25100 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 15:35:31 +0300 Subject: [PATCH 26/62] Work towards async --- Cargo.lock | 44 +++ Cargo.toml | 3 +- src/client/asynchronous.rs | 202 +++++--------- src/client/builder.rs | 1 - src/connection/asynchronous.rs | 38 ++- src/error/mod.rs | 2 + src/graph/asynchronous.rs | 443 ++++++++++++++++++++++--------- src/graph_schema/asynchronous.rs | 2 +- src/parser/utils.rs | 3 +- src/value/map.rs | 2 +- 10 files changed, 457 insertions(+), 283 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 73bb6c8..078753b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -157,6 +157,7 @@ version = "0.1.0" dependencies = [ "anyhow", "async-recursion", + "futures", "log", "parking_lot", "redis", @@ -202,12 +203,42 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + [[package]] name = "futures-sink" version = "0.3.30" @@ -226,11 +257,15 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ + "futures-channel", "futures-core", + "futures-io", "futures-sink", "futures-task", + "memchr", "pin-project-lite", "pin-utils", + "slab", ] [[package]] @@ -709,6 +744,15 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.13.2" diff --git a/Cargo.toml b/Cargo.toml index c4174da..3c571f8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" [dependencies] anyhow = { version = "1.0.83", default-features = false, features = ["std"] } async-recursion = { version = "1.1.1" } +futures = { version = "0.3.30", default-features = false, features = ["std"], optional = true } log = { version = "0.4.21", default-features = false } parking_lot = { version = "0.12.2", default-features = false, features = ["deadlock_detection"] } redis = { version = "0.25.3", default-features = false, optional = true } @@ -19,7 +20,7 @@ url-parse = "1.0.8" default = ["redis"] native-tls = ["redis/tls-native-tls"] rustls = ["redis/tls-rustls"] -tokio = ["dep:tokio", "redis/tokio-comp"] +tokio = ["dep:futures", "dep:tokio", "redis/tokio-comp"] tokio-native-tls = ["redis/tokio-native-tls-comp"] tokio-rustls = ["redis/tokio-rustls-comp"] redis = ["dep:redis"] diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index 3473c46..db3a0b0 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -6,20 +6,20 @@ use crate::{ client::FalkorClientProvider, connection::asynchronous::BorrowedAsyncConnection, parser::utils::string_vec_from_val, AsyncGraph, AsyncGraphSchema, ConfigValue, - FalkorAsyncConnection, FalkorConnectionInfo, FalkorDBError, + FalkorAsyncConnection, FalkorConnectionInfo, FalkorDBError, FalkorValue, }; use anyhow::Result; use std::{ collections::{HashMap, VecDeque}, - fmt::{Debug, Formatter}, + fmt::Display, sync::Arc, time::Duration, }; use tokio::sync::Mutex; pub(crate) struct FalkorAsyncClientInner { - _inner: Mutex, - graph_cache: Mutex>, + _inner: Arc, + graph_cache: Mutex>>>, connection_pool_size: u8, connection_pool: Arc>>, } @@ -38,26 +38,11 @@ impl FalkorAsyncClientInner { } } -unsafe impl Sync for FalkorAsyncClientInner {} -unsafe impl Send for FalkorAsyncClientInner {} - pub struct FalkorAsyncClient { inner: Arc, _connection_info: FalkorConnectionInfo, } -impl Debug for FalkorAsyncClient { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { - f.debug_struct("FalkorAsyncClient") - .field("inner", &"") - .field("connection_info", &self._connection_info) - .finish() - } -} - impl FalkorAsyncClient { pub(crate) async fn create( client: FalkorClientProvider, @@ -65,22 +50,26 @@ impl FalkorAsyncClient { num_connections: u8, timeout: Option, ) -> Result { - let connection_pool = Arc::new(Mutex::new(VecDeque::with_capacity( - num_connections as usize, - ))); - { - let mut connection_pool = connection_pool.lock().await; - for _ in 0..num_connections { - connection_pool.push_back(client.get_async_connection(timeout).await?); - } + let client = Arc::new(client); + let connection_pool: VecDeque<_> = futures::future::join_all( + (0..num_connections).map(|_| tokio::spawn(client.get_async_connection(timeout))), + ) + .await + .into_iter() + .flat_map(|x| x) + .flat_map(|x| x) + .collect(); + + if connection_pool.len() != num_connections as usize { + Err(FalkorDBError::NoConnection)?; } Ok(Self { inner: Arc::new(FalkorAsyncClientInner { - _inner: client.into(), + _inner: client, graph_cache: Default::default(), connection_pool_size: num_connections, - connection_pool, + connection_pool: Arc::new(Mutex::new(connection_pool)), }), _connection_info: connection_info, }) @@ -101,7 +90,7 @@ impl FalkorAsyncClient { /// A [`Vec`] of [`String`]s, containing the names of available graphs pub async fn list_graphs(&self) -> Result> { let mut conn = self.borrow_connection().await?; - conn.send_command(None, "GRAPH.LIST", None, None) + conn.send_command::<&str>(None, "GRAPH.LIST", None, None) .await .and_then(|res| string_vec_from_val(res).map_err(Into::into)) } @@ -114,69 +103,38 @@ impl FalkorAsyncClient { /// /// # Returns /// A [`HashMap`] comprised of [`String`] keys, and [`ConfigValue`] values. - pub async fn config_get( + pub async fn config_get( &self, config_key: T, ) -> Result> { let mut conn = self.borrow_connection().await?; - Ok(match conn.as_inner()? { - #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(redis_conn) => { - let bulk_data = match redis_conn - .send_packed_command( - redis::cmd("GRAPH.CONFIG") - .arg("GET") - .arg(config_key.to_string()), - ) - .await? - { - redis::Value::Bulk(bulk_data) => bulk_data, - _ => { - return Err(FalkorDBError::InvalidDataReceived)?; - } - }; - - if bulk_data.is_empty() { - Err(FalkorDBError::InvalidDataReceived)?; - } else if bulk_data.len() == 2 { - return if let Some(value) = bulk_data.first() { - let str_key = match value { - redis::Value::Data(bytes_data) => { - String::from_utf8_lossy(bytes_data.as_slice()).to_string() - } - redis::Value::Status(status_data) => status_data.to_owned(), - _ => Err(FalkorDBError::InvalidDataReceived)?, - }; - Ok(HashMap::from([( - str_key.to_string(), - ConfigValue::try_from(&bulk_data[1])?, - )])) - } else { - Err(FalkorDBError::InvalidDataReceived)? - }; - } - - let mut config_map = HashMap::with_capacity(bulk_data.len()); - for raw_map in bulk_data { - for (key, val) in raw_map - .into_map_iter() - .map_err(|_| FalkorDBError::ParsingError)? - { - let key = match key { - redis::Value::Status(config_key) => Ok(config_key), - redis::Value::Data(config_key) => { - Ok(String::from_utf8_lossy(config_key.as_slice()).to_string()) - } - _ => Err(FalkorDBError::InvalidDataReceived), - }?; - - config_map.insert(key, ConfigValue::try_from(&val)?); - } - } - - config_map - } - }) + let config = conn + .send_command(None, "GRAPH.CONFIG", Some("GET"), Some(&[config_key])) + .await? + .into_vec()?; + + if config.len() == 2 { + let [key, val]: [FalkorValue; 2] = config + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + return Ok(HashMap::from([( + key.into_string()?, + ConfigValue::try_from(val)?, + )])); + } + + Ok(config + .into_iter() + .flat_map(|config| { + let [key, val]: [FalkorValue; 2] = config + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + Result::<_, FalkorDBError>::Ok((key.into_string()?, ConfigValue::try_from(val)?)) + }) + .collect::>()) } /// Return the current value of a configuration option in the database. @@ -189,23 +147,16 @@ impl FalkorAsyncClient { &self, config_key: T, value: C, - ) -> Result<()> { - let mut conn = self.borrow_connection().await?; - match conn.as_inner()? { - #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(redis_conn) => { - redis_conn - .send_packed_command( - redis::cmd("GRAPH.CONFIG") - .arg("SET") - .arg(config_key.into()) - .arg(value.into()), - ) - .await?; - } - } - - Ok(()) + ) -> Result { + self.borrow_connection() + .await? + .send_command( + None, + "GRAPH.CONFIG", + Some("SET"), + Some(&[config_key.into(), value.into()]), + ) + .await } /// Opens a graph context for queries and operations @@ -214,7 +165,7 @@ impl FalkorAsyncClient { /// * `graph_name`: A string identifier of the graph to open. /// /// # Returns - /// a [`AsyncGraph`] object, allowing various graph operations. + /// a [`SyncGraph`] object, allowing various graph operations. pub async fn select_graph( &self, graph_name: T, @@ -222,14 +173,7 @@ impl FalkorAsyncClient { AsyncGraph { client: self.inner.clone(), graph_name: graph_name.to_string(), - graph_schema: self - .inner - .graph_cache - .lock() - .await - .entry(graph_name.to_string()) - .or_insert(AsyncGraphSchema::new(graph_name.to_string())) - .clone(), + graph_schema: AsyncGraphSchema::new(graph_name.to_string()), // Required for requesting refreshes } } @@ -241,18 +185,18 @@ impl FalkorAsyncClient { /// /// # Returns /// If successful, will return the new [`AsyncGraph`] object. - pub async fn copy_graph( + pub async fn copy_graph( &self, - graph_to_clone: T, - new_graph_name: Z, + graph_to_clone: &str, + new_graph_name: &str, ) -> Result { self.borrow_connection() .await? .send_command( - Some(graph_to_clone.to_string()), + Some(graph_to_clone), "GRAPH.COPY", None, - Some(&[new_graph_name.to_string()]), + Some(&[new_graph_name]), ) .await?; Ok(self.select_graph(new_graph_name).await) @@ -279,15 +223,15 @@ mod tests { async fn test_async_select_graph_and_query() { let client = create_async_test_client().await; - let graph = client.select_graph("imdb").await; + let mut graph = client.select_graph("imdb").await; assert_eq!(graph.graph_name(), "imdb".to_string()); let res = graph - .query("MATCH (a:actor) return a".to_string(), None) + .query("MATCH (a:actor) return a".to_string()) .await .expect("Could not get actors from unmodified graph"); - assert_eq!(res.result_set.len(), 1317); + assert_eq!(res.data.len(), 1317); } #[tokio::test] @@ -301,24 +245,24 @@ mod tests { .await .ok(); - let graph = client + let mut graph = client .copy_graph("imdb", "imdb_async_ro_copy") .await .expect("Could not copy graph"); - let original_graph = client.select_graph("imdb").await; + let mut original_graph = client.select_graph("imdb").await; assert_eq!( graph - .query("MATCH (a:actor) RETURN a".to_string(), None) + .query("MATCH (a:actor) RETURN a".to_string()) .await .expect("Could not get actors from unmodified graph") - .result_set, + .data, original_graph - .query("MATCH (a:actor) RETURN a".to_string(), None) + .query("MATCH (a:actor) RETURN a".to_string()) .await .expect("Could not get actors from unmodified graph") - .result_set + .data ) } diff --git a/src/client/builder.rs b/src/client/builder.rs index 56c14c0..b03947e 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -165,7 +165,6 @@ mod tests { #[test] #[cfg(feature = "redis")] - #[allow(irrefutable_let_patterns)] fn test_sync_builder_redis_fallback() { let client = FalkorClientBuilder::new().build(); assert!(client.is_ok()); diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs index bf4a5ba..bc2d188 100644 --- a/src/connection/asynchronous.rs +++ b/src/connection/asynchronous.rs @@ -5,6 +5,7 @@ use crate::{FalkorDBError, FalkorValue}; use anyhow::Result; +use std::fmt::Display; use std::{collections::VecDeque, sync::Arc}; use tokio::sync::Mutex; @@ -28,32 +29,29 @@ impl BorrowedAsyncConnection { self.conn.as_mut().ok_or(FalkorDBError::EmptyConnection) } - pub(crate) async fn send_command( + pub(crate) async fn send_command( &mut self, - graph_name: Option, + graph_name: Option<&str>, command: &str, subcommand: Option<&str>, - params: Option<&[String]>, + params: Option<&[P]>, ) -> Result { - Ok( - match self.conn.as_mut().ok_or(FalkorDBError::EmptyConnection)? { - #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(redis_conn) => { - let mut cmd = redis::cmd(command); - cmd.arg(subcommand); - cmd.arg(graph_name); - if let Some(params) = params { - for param in params { - cmd.arg(param); - } + Ok(match self.as_inner()? { + #[cfg(feature = "redis")] + FalkorAsyncConnection::Redis(redis_conn) => { + let mut cmd = redis::cmd(command); + cmd.arg(subcommand); + cmd.arg(graph_name); + if let Some(params) = params { + for param in params { + cmd.arg(param.to_string()); } - - redis::FromRedisValue::from_owned_redis_value( - redis_conn.send_packed_command(&cmd).await?, - )? } - }, - ) + redis::FromRedisValue::from_owned_redis_value( + redis_conn.send_packed_command(&cmd).await?, + )? + } + }) } } diff --git a/src/error/mod.rs b/src/error/mod.rs index e36d66b..3d70e28 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -15,6 +15,8 @@ pub enum FalkorDBError { UnavailableProvider, #[error("The number of connections for the client has to be between 1 and 32")] InvalidConnectionPoolSize, + #[error("Could not connect to the server with the provided address")] + NoConnection, #[error("Attempting to use an empty connection object")] EmptyConnection, #[error("General parsing error")] diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index a613765..3d9adbb 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -3,11 +3,12 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use super::utils::{construct_query, generate_procedure_call}; use crate::{ - client::asynchronous::FalkorAsyncClientInner, AsyncGraphSchema, Constraint, ConstraintType, - EntityType, ExecutionPlan, FalkorAsyncConnection, FalkorAsyncParseable, FalkorDBError, - FalkorIndex, FalkorValue, IndexType, QueryResult, SlowlogEntry, + client::asynchronous::FalkorAsyncClientInner, + graph::utils::{construct_query, generate_procedure_call}, + parser::utils::{parse_header, parse_result_set_async}, + AsyncGraphSchema, Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorAsyncParseable, + FalkorDBError, FalkorIndex, FalkorResponse, FalkorValue, IndexType, ResultSet, SlowlogEntry, }; use anyhow::Result; use std::{collections::HashMap, fmt::Display, sync::Arc}; @@ -41,17 +42,16 @@ impl AsyncGraph { subcommand: Option<&str>, params: Option<&[String]>, ) -> Result { - self.client - .borrow_connection() - .await? - .send_command(Some(self.graph_name.clone()), command, subcommand, params) + let mut conn = self.client.borrow_connection().await?; + conn.send_command(Some(self.graph_name.as_str()), command, subcommand, params) .await } /// Deletes the graph stored in the database, and drop all the schema caches. /// NOTE: This still maintains the graph API, operations are still viable. - pub async fn delete(&self) -> Result<()> { + pub async fn delete(&mut self) -> Result<()> { self.send_command("GRAPH.DELETE", None, None).await?; + self.graph_schema.clear().await; Ok(()) } @@ -69,17 +69,15 @@ impl AsyncGraph { return Ok(vec![]); } - let mut slowlog_entries = Vec::with_capacity(res.len()); - for entry_raw in res { - slowlog_entries.push(SlowlogEntry::from_value_vec(entry_raw.into_vec()?)?); - } - - Ok(slowlog_entries) + Ok(res + .into_iter() + .flat_map(|entry_raw| SlowlogEntry::from_value_vec(entry_raw.into_vec()?)) + .collect()) } /// Resets the slowlog, all query time data will be cleared. pub async fn slowlog_reset(&self) -> Result { - self.send_command("GRAPH.SLOWLOG", Some("RESET"), None) + self.send_command("GRAPH.SLOWLOG", None, Some(&["RESET".to_string()])) .await } @@ -162,50 +160,123 @@ impl AsyncGraph { .await } - async fn query_inner( - &self, + async fn query_inner_with_timeout( + &mut self, command: &str, query_string: Q, params: Option<&HashMap>, - timeout: Option, - ) -> Result

{ + timeout: i64, + ) -> Result> { let query = construct_query(query_string, params); + let mut conn = self.client.borrow_connection().await?; + let [header, data, stats]: [FalkorValue; 3] = conn + .send_command( + Some(self.graph_name.as_str()), + command, + None, + Some(&[ + query.as_str(), + "--compact", + format!("timeout {timeout}").as_str(), + ]), + ) + .await? + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + let header_keys = parse_header(header)?; + FalkorResponse::from_response_with_headers( + parse_result_set_async(data, &mut self.graph_schema, &mut conn, &header_keys).await?, + header_keys, + stats, + ) + .map_err(Into::into) + } + + async fn query_inner( + &mut self, + command: &str, + query_string: Q, + params: Option<&HashMap>, + ) -> Result> { + let query = construct_query(query_string, params); let mut conn = self.client.borrow_connection().await?; - let falkor_result = match conn.as_inner()? { - #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(redis_conn) => { - use redis::FromRedisValue as _; - let redis_val = redis_conn - .send_packed_command( - redis::cmd(command) - .arg(self.graph_name.as_str()) - .arg(query) - .arg("--compact") - .arg(timeout.map(|timeout| format!("timeout {timeout}"))), - ) - .await?; - FalkorValue::from_owned_redis_value(redis_val)? + + let res = conn + .send_command( + Some(self.graph_name.as_str()), + command, + None, + Some(&[query, "--compact".to_string()]), + ) + .await? + .into_vec()?; + + match res.len() { + 1 => { + let stats = res + .into_iter() + .next() + .ok_or(FalkorDBError::ParsingArrayToStructElementCount)?; + + FalkorResponse::from_response(None, vec![], stats) } - }; + 2 => { + let [header, stats]: [FalkorValue; 2] = res + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - P::from_falkor_value_async(falkor_result, &self.graph_schema, &mut conn).await + FalkorResponse::from_response(Some(header), vec![], stats) + } + 3 => { + let [header, data, stats]: [FalkorValue; 3] = res + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + let header_keys = parse_header(header)?; + FalkorResponse::from_response_with_headers( + parse_result_set_async(data, &mut self.graph_schema, &mut conn, &header_keys) + .await?, + header_keys, + stats, + ) + } + _ => Err(FalkorDBError::ParsingArrayToStructElementCount), + } + .map_err(Into::into) } /// Run a query on the graph /// /// # Arguments /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. /// /// # Returns /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query pub async fn query( - &self, + &mut self, + query_string: Q, + ) -> Result> { + self.query_inner::("GRAPH.QUERY", query_string, None) + .await + } + + /// Run a query on the graph, but abort it if it exceeds the timeout + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// + /// # Returns + /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + pub async fn query_with_timeout( + &mut self, query_string: Q, - timeout: Option, - ) -> Result { - self.query_inner::("GRAPH.QUERY", query_string, None, timeout) + timeout: i64, + ) -> Result> { + self.query_inner_with_timeout::("GRAPH.QUERY", query_string, None, timeout) .await } @@ -214,18 +285,36 @@ impl AsyncGraph { /// /// # Arguments /// * `query_string`: The query to run + /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// + /// # Returns + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query + pub async fn query_with_params( + &mut self, + query_string: Q, + params: &HashMap, + ) -> Result> { + self.query_inner("GRAPH.QUERY", query_string, Some(params)) + .await + } + + /// Run a query on the graph but abort it if it exceeds the timeout + /// This function variant allows adding extra parameters after the query + /// + /// # Arguments + /// * `query_string`: The query to run /// * `timeout`: Specify how long should the query run before aborting. /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. /// /// # Returns /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query - pub async fn query_with_params( - &self, + pub async fn query_with_params_and_timeout( + &mut self, query_string: Q, - timeout: Option, + timeout: i64, params: &HashMap, - ) -> Result { - self.query_inner("GRAPH.QUERY", query_string, Some(params), timeout) + ) -> Result> { + self.query_inner_with_timeout("GRAPH.QUERY", query_string, Some(params), timeout) .await } @@ -234,16 +323,32 @@ impl AsyncGraph { /// /// # Arguments /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. /// /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query pub async fn query_readonly( - &self, + &mut self, query_string: Q, - timeout: Option, - ) -> Result { - self.query_inner::( + ) -> Result> { + self.query_inner::("GRAPH.QUERY_RO", query_string, None) + .await + } + + /// Run a query on the graph, but abort it if it exceeds the timeout + /// Read-only queries are more limited with the operations they are allowed to perform. + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// + /// # Returns + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query + pub async fn query_readonly_with_timeout( + &mut self, + query_string: Q, + timeout: i64, + ) -> Result> { + self.query_inner_with_timeout::( "GRAPH.QUERY_RO", query_string, None, @@ -262,14 +367,34 @@ impl AsyncGraph { /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. /// /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query pub async fn query_readonly_with_params( - &self, + &mut self, query_string: Q, - timeout: Option, - params: Option<&HashMap>, - ) -> Result { - self.query_inner("GRAPH.QUERY_RO", query_string, params, timeout) + params: &HashMap, + ) -> Result> { + self.query_inner("GRAPH.QUERY_RO", query_string, Some(params)) + .await + } + + /// Run a read-only query on the graph, but abort it if it exceeds the timeout + /// Read-only queries are more limited with the operations they are allowed to perform. + /// This function variant allows adding extra parameters after the query + /// + /// # Arguments + /// * `query_string`: The query to run + /// * `timeout`: Specify how long should the query run before aborting. + /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// + /// # Returns + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query + pub async fn query_readonly_with_params_and_timeout( + &mut self, + query_string: Q, + params: &HashMap, + timeout: i64, + ) -> Result> { + self.query_inner_with_timeout("GRAPH.QUERY_RO", query_string, Some(params), timeout) .await } @@ -285,26 +410,80 @@ impl AsyncGraph { /// * `timeout`: If provided, the query will abort if overruns the timeout. /// /// # Returns - /// A caller-provided type which implements [`FalkorAsyncParseable`] + /// A caller-provided type which implements [`FalkorParsable`] pub async fn call_procedure( - &self, + &mut self, procedure: C, args: Option<&[&str]>, yields: Option<&[&str]>, read_only: bool, - timeout: Option, ) -> Result

{ let (query_string, params) = generate_procedure_call(procedure, args, yields); + let query = construct_query(query_string, params.as_ref()); + let mut conn = self.client.borrow_connection().await?; - self.query_inner( - if read_only { - "GRAPH.QUERY_RO" - } else { - "GRAPH.QUERY" - }, - query_string, - params.as_ref(), - timeout, + P::from_falkor_value_async( + conn.send_command( + Some(self.graph_name.as_str()), + if read_only { + "GRAPH.QUERY_RO" + } else { + "GRAPH.QUERY" + }, + None, + Some(&[query, "--compact".to_string()]), + ) + .await?, + &mut self.graph_schema, + &mut conn, + ) + .await + } + + /// Run a query which calls a procedure on the graph, read-only, or otherwise. + /// Read-only queries are more limited with the operations they are allowed to perform. + /// This function allows adding extra parameters after the query, and adding a YIELD block afterward + /// This function will cause the query to abort if it exceeds a certain timeout + /// + /// # Arguments + /// * `procedure`: The procedure to call + /// * `args`: An optional slice of strings containing the parameters. + /// * `yields`: The optional yield block arguments. + /// * `read_only`: Whether this procedure is read-only. + /// * `timeout`: If provided, the query will abort if overruns the timeout. + /// + /// # Returns + /// A caller-provided type which implements [`FalkorParsable`] + pub async fn call_procedure_with_timeout( + &mut self, + procedure: C, + args: Option<&[&str]>, + yields: Option<&[&str]>, + read_only: bool, + timeout: i64, + ) -> Result

{ + let (query_string, params) = generate_procedure_call(procedure, args, yields); + let query = construct_query(query_string, params.as_ref()); + let mut conn = self.client.borrow_connection().await?; + + P::from_falkor_value_async( + conn.send_command( + Some(self.graph_name.as_str()), + if read_only { + "GRAPH.QUERY_RO" + } else { + "GRAPH.QUERY" + }, + None, + Some(&[ + query.as_str(), + "--compact", + format!("timeout {timeout}").as_str(), + ]), + ) + .await?, + &mut self.graph_schema, + &mut conn, ) .await } @@ -312,36 +491,39 @@ impl AsyncGraph { /// Calls the DB.INDICES procedure on the graph, returning all the indexing methods currently used /// /// # Returns - /// A [`Vec`] of [`Index`] - pub async fn list_indices(&self) -> Result> { + /// A [`Vec`] of [`FalkorIndex`] + pub async fn list_indices(&mut self) -> Result>> { let mut conn = self.client.borrow_connection().await?; let [header, indices, stats]: [FalkorValue; 3] = self - .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false, None) + .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false) .await? .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - let indices = indices.into_vec()?; - - let mut out_vec = Vec::with_capacity(indices.len()); - for index in indices { - out_vec.push( - FalkorIndex::from_falkor_value_async(index, &self.graph_schema, &mut conn).await?, - ); - } - - Ok(out_vec) + FalkorResponse::from_response( + Some(header), + indices + .into_vec()? + .into_iter() + .flat_map(|index| { + FalkorIndex::from_falkor_value_async(index, &mut self.graph_schema, &mut conn) + .await + }) + .collect(), + stats, + ) + .map_err(Into::into) } pub async fn create_index( - &self, + &mut self, index_field_type: IndexType, entity_type: EntityType, label: L, properties: &[P], options: Option<&HashMap>, - ) -> Result { + ) -> Result> { // Create index from these properties let properties_string = properties .iter() @@ -372,11 +554,11 @@ impl AsyncGraph { .map(|options_string| format!(" OPTIONS {{ {} }}", options_string)) .unwrap_or_default(); - let full_query = format!( + self.query(format!( "CREATE {idx_type}INDEX FOR {pattern} ON ({}){}", properties_string, options_string - ); - self.query(full_query, None).await + )) + .await } /// Drop an existing index, by specifying its type, entity, label and specific properties @@ -384,12 +566,12 @@ impl AsyncGraph { /// # Arguments /// * `index_field_type` pub async fn drop_index( - &self, + &mut self, index_field_type: IndexType, entity_type: EntityType, label: L, properties: &[P], - ) -> Result { + ) -> Result> { let properties_string = properties .iter() .map(|element| format!("e.{}", element.to_string())) @@ -408,39 +590,39 @@ impl AsyncGraph { } .to_string(); - self.query( - format!( - "DROP {idx_type} INDEX for {pattern} ON ({})", - properties_string - ), - None, - ) + self.query(format!( + "DROP {idx_type} INDEX for {pattern} ON ({})", + properties_string + )) .await } /// Calls the DB.CONSTRAINTS procedure on the graph, returning an array of the graph's constraints /// /// # Returns - /// A [`Vec`] of [`Constraint`]s - pub async fn list_constraints(&self) -> Result> { + /// A tuple where the first element is a [`Vec`] of [`Constraint`]s, and the second element is a [`Vec`] of stats as [`String`]s + pub async fn list_constraints(&mut self) -> Result>> { let mut conn = self.client.borrow_connection().await?; let [header, query_res, stats]: [FalkorValue; 3] = self - .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, false, None) + .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, false) .await? .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - let query_res = query_res.into_vec()?; - - let mut constraints_vec = Vec::with_capacity(query_res.len()); - for item in query_res { - constraints_vec.push( - Constraint::from_falkor_value_async(item, &self.graph_schema, &mut conn).await?, - ); - } - - Ok(constraints_vec) + FalkorResponse::from_response( + Some(header), + query_res + .into_vec()? + .into_iter() + .flat_map(|item| { + Constraint::from_falkor_value_async(item, &mut self.graph_schema, &mut conn) + .await + }) + .collect(), + stats, + ) + .map_err(Into::into) } /// Creates a new constraint for this graph, making the provided properties mandatory @@ -449,11 +631,11 @@ impl AsyncGraph { /// * `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. - pub async fn create_mandatory_constraint( + pub async fn create_mandatory_constraint( &self, entity_type: EntityType, - label: L, - properties: &[P], + label: &str, + properties: &[&str], ) -> Result { let mut params = Vec::with_capacity(5 + properties.len()); params.extend([ @@ -476,7 +658,7 @@ impl AsyncGraph { /// * `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. pub async fn create_unique_constraint( - &self, + &mut self, entity_type: EntityType, label: String, properties: &[P], @@ -541,7 +723,7 @@ mod tests { #[tokio::test] async fn test_create_drop_index_async() { - let graph = open_test_graph_async("test_create_drop_index_async").await; + let mut graph = open_test_graph_async("test_create_drop_index_async").await; graph .inner .create_index( @@ -560,8 +742,11 @@ mod tests { .await .expect("Could not list indices"); - assert_eq!(indices.len(), 2); - assert_eq!(indices[0].field_types["Hello"], vec![IndexType::Fulltext]); + assert_eq!(indices.data.len(), 2); + assert_eq!( + indices.data[0].field_types["Hello"], + vec![IndexType::Fulltext] + ); graph .inner @@ -577,19 +762,19 @@ mod tests { #[tokio::test] async fn test_list_indices_async() { - let graph = open_test_graph_async("test_list_indices_async").await; + let mut graph = open_test_graph_async("test_list_indices_async").await; let indices = graph .inner .list_indices() .await .expect("Could not list indices"); - assert_eq!(indices.len(), 1); - assert_eq!(indices[0].entity_type, EntityType::Node); - assert_eq!(indices[0].index_label, "actor".to_string()); - assert_eq!(indices[0].field_types.len(), 2); + assert_eq!(indices.data.len(), 1); + assert_eq!(indices.data[0].entity_type, EntityType::Node); + assert_eq!(indices.data[0].index_label, "actor".to_string()); + assert_eq!(indices.data[0].field_types.len(), 2); assert_eq!( - indices[0].field_types, + indices.data[0].field_types, HashMap::from([ ("age".to_string(), vec![IndexType::Range]), ("name".to_string(), vec![IndexType::Fulltext]) @@ -623,7 +808,7 @@ mod tests { #[tokio::test] async fn test_create_drop_unique_constraint_async() { - let graph = open_test_graph_async("test_unique_constraint_async").await; + let mut graph = open_test_graph_async("test_unique_constraint_async").await; graph .inner @@ -649,7 +834,7 @@ mod tests { #[tokio::test] async fn test_list_constraints_async() { - let graph = open_test_graph_async("test_list_constraint_async").await; + let mut graph = open_test_graph_async("test_list_constraint_async").await; graph .inner @@ -666,21 +851,21 @@ mod tests { .list_constraints() .await .expect("Could not list constraints"); - assert_eq!(constraints.len(), 1); + assert_eq!(constraints.data.len(), 1); } #[tokio::test] async fn test_slowlog_async() { - let graph = open_test_graph_async("test_slowlog_async").await; + let mut graph = open_test_graph_async("test_slowlog_async").await; graph .inner - .query("UNWIND range(0, 500) AS x RETURN x", None) + .query("UNWIND range(0, 500) AS x RETURN x") .await .expect("Could not generate the fast query"); graph .inner - .query("UNWIND range(0, 100000) AS x RETURN x", None) + .query("UNWIND range(0, 100000) AS x RETURN x") .await .expect("Could not generate the slow query"); diff --git a/src/graph_schema/asynchronous.rs b/src/graph_schema/asynchronous.rs index daa731d..d65203a 100644 --- a/src/graph_schema/asynchronous.rs +++ b/src/graph_schema/asynchronous.rs @@ -95,7 +95,7 @@ impl AsyncGraphSchema { // 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] = conn .send_command( - Some(self.graph_name.clone()), + Some(self.graph_name.as_str()), "GRAPH.QUERY", None, Some(&[format!("CALL {command}()")]), diff --git a/src/parser/utils.rs b/src/parser/utils.rs index 472b13e..ec29bd5 100644 --- a/src/parser/utils.rs +++ b/src/parser/utils.rs @@ -76,11 +76,12 @@ pub(crate) fn parse_result_set( #[cfg(feature = "tokio")] pub(crate) async fn parse_result_set_async( - data_vec: Vec, + data: FalkorValue, graph_schema: &AsyncGraphSchema, conn: &mut BorrowedAsyncConnection, header_keys: &[String], ) -> Result>> { + let data_vec = data.into_vec()?; let mut parsed_result_set = Vec::with_capacity(data_vec.len()); for column in data_vec { let column_vec = column.into_vec()?; diff --git a/src/value/map.rs b/src/value/map.rs index 69cd574..888928c 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -160,7 +160,7 @@ pub(crate) async fn parse_map_with_schema_async( ) -> Result> { let val_vec = value.into_vec()?; let (mut id_hashset, mut map_vec) = ( - HashSet::with_capacity(val_vec.len()), + std::collections::HashSet::with_capacity(val_vec.len()), Vec::with_capacity(val_vec.len()), ); From 30e9d75a2d4dae2c7be8acb7afe9cb5595f48599 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 15:46:23 +0300 Subject: [PATCH 27/62] Ensure passing checks --- src/client/blocking.rs | 2 +- src/connection/asynchronous.rs | 3 +-- src/graph/blocking.rs | 6 +++--- src/graph_schema/asynchronous.rs | 7 ++++--- src/value/graph_entities.rs | 6 +++--- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/client/blocking.rs b/src/client/blocking.rs index de05fd9..ead0715 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -3,10 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::parser::utils::string_vec_from_val; use crate::{ client::FalkorClientProvider, connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}, + parser::utils::string_vec_from_val, ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorValue, SyncGraph, SyncGraphSchema, }; use anyhow::Result; diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs index bc2d188..1bb9ffd 100644 --- a/src/connection/asynchronous.rs +++ b/src/connection/asynchronous.rs @@ -5,8 +5,7 @@ use crate::{FalkorDBError, FalkorValue}; use anyhow::Result; -use std::fmt::Display; -use std::{collections::VecDeque, sync::Arc}; +use std::{collections::VecDeque, fmt::Display, sync::Arc}; use tokio::sync::Mutex; #[derive(Clone)] diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 7e24467..6906362 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -237,7 +237,7 @@ impl SyncGraph { /// * `query_string`: The query to run /// /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query pub fn query( &mut self, query_string: Q, @@ -252,7 +252,7 @@ impl SyncGraph { /// * `timeout`: Specify how long should the query run before aborting. /// /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query pub fn query_with_timeout( &mut self, query_string: Q, @@ -287,7 +287,7 @@ impl SyncGraph { /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. /// /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query + /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query pub fn query_with_params_and_timeout( &mut self, query_string: Q, diff --git a/src/graph_schema/asynchronous.rs b/src/graph_schema/asynchronous.rs index d65203a..85d8aa1 100644 --- a/src/graph_schema/asynchronous.rs +++ b/src/graph_schema/asynchronous.rs @@ -3,10 +3,11 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use super::utils::{get_refresh_command, get_relevant_hashmap, update_map}; use crate::{ - connection::asynchronous::BorrowedAsyncConnection, value::FalkorValue, FalkorDBError, - SchemaType, + connection::asynchronous::BorrowedAsyncConnection, + graph_schema::utils::{get_refresh_command, get_relevant_hashmap, update_map}, + value::FalkorValue, + FalkorDBError, SchemaType, }; use anyhow::Result; use std::{ diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index af7743c..ac116d0 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -3,10 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use super::utils::parse_labels; use crate::{ - connection::blocking::BorrowedSyncConnection, value::map::parse_map_with_schema, FalkorDBError, - FalkorParsable, FalkorValue, SchemaType, SyncGraphSchema, + connection::blocking::BorrowedSyncConnection, + value::{map::parse_map_with_schema, utils::parse_labels}, + FalkorDBError, FalkorParsable, FalkorValue, SchemaType, SyncGraphSchema, }; use anyhow::Result; use std::{ From 907993fd99f9740df603ee013a81d18f5d2a074f Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 15:50:30 +0300 Subject: [PATCH 28/62] Add readme --- READMD.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 READMD.md diff --git a/READMD.md b/READMD.md new file mode 100644 index 0000000..8b188b1 --- /dev/null +++ b/READMD.md @@ -0,0 +1,5 @@ +[![license](https://img.shields.io/github/license/falkordb/falkordb-client-rs.svg)](https://github.com/falkordb/falkordb-client-rs) +[![Release](https://img.shields.io/github/release/falkordb/falkordb-client-rs.svg)](https://github.com/falkordb/falkordb-client-rs/releases/latest) +[![Codecov](https://codecov.io/gh/falkordb/falkordb-client-rs/branch/main/graph/badge.svg)](https://codecov.io/gh/falkordb/falkordb-client-rs) +[![Forum](https://img.shields.io/badge/Forum-falkordb-blue)](https://github.com/orgs/FalkorDB/discussions) +[![Discord](https://img.shields.io/discord/1146782921294884966?style=flat-square)](https://discord.gg/ErBEqN9E) From b6fd292f3425d37f68420ad72aa3a0ae438af628 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 15:51:15 +0300 Subject: [PATCH 29/62] Add readme --- READMD.md => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename READMD.md => README.md (100%) diff --git a/READMD.md b/README.md similarity index 100% rename from READMD.md rename to README.md From f0ba829a45d24bc0d277f5a636afd26c736209aa Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 15:57:43 +0300 Subject: [PATCH 30/62] tiny improvement --- src/graph/asynchronous.rs | 5 +---- src/graph/blocking.rs | 5 +---- src/response/slowlog_entry.rs | 10 ++++++---- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index 3d9adbb..cba1f2f 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -69,10 +69,7 @@ impl AsyncGraph { return Ok(vec![]); } - Ok(res - .into_iter() - .flat_map(|entry_raw| SlowlogEntry::from_value_vec(entry_raw.into_vec()?)) - .collect()) + Ok(res.into_iter().flat_map(SlowlogEntry::try_from).collect()) } /// Resets the slowlog, all query time data will be cleared. diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 6906362..c1c2b9f 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -64,10 +64,7 @@ impl SyncGraph { return Ok(vec![]); } - Ok(res - .into_iter() - .flat_map(|entry_raw| SlowlogEntry::from_value_vec(entry_raw.into_vec()?)) - .collect()) + Ok(res.into_iter().flat_map(SlowlogEntry::try_from).collect()) } /// Resets the slowlog, all query time data will be cleared. diff --git a/src/response/slowlog_entry.rs b/src/response/slowlog_entry.rs index fff0c0a..aaaf853 100644 --- a/src/response/slowlog_entry.rs +++ b/src/response/slowlog_entry.rs @@ -4,7 +4,6 @@ */ use crate::{FalkorDBError, FalkorValue}; -use anyhow::Result; /// A slowlog entry, representing one of the N slowest queries in the current log #[derive(Clone, Debug, PartialEq)] @@ -19,9 +18,12 @@ pub struct SlowlogEntry { pub time_taken: f64, } -impl SlowlogEntry { - pub(crate) fn from_value_vec(values: Vec) -> Result { - let [timestamp, command, arguments, time_taken] = values +impl TryFrom for SlowlogEntry { + type Error = FalkorDBError; + + fn try_from(value: FalkorValue) -> std::result::Result { + let [timestamp, command, arguments, time_taken] = value + .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; From f10ea03fcd855f255337475f1a5b427497dd05f9 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 20:26:07 +0300 Subject: [PATCH 31/62] Cleaner sync, but with regression on async --- Cargo.lock | 13 --- Cargo.toml | 2 +- src/client/asynchronous.rs | 52 +++++---- src/client/blocking.rs | 4 +- src/client/builder.rs | 1 - src/graph/asynchronous.rs | 47 ++++---- src/graph/blocking.rs | 5 +- src/graph_schema/asynchronous.rs | 111 ------------------ src/graph_schema/blocking.rs | 137 ---------------------- src/graph_schema/mod.rs | 187 ++++++++++++++++++++++++++++++- src/lib.rs | 17 +-- src/parser/mod.rs | 16 +-- src/parser/utils.rs | 47 +------- src/response/constraint.rs | 28 +---- src/response/index.rs | 30 +---- src/response/slowlog_entry.rs | 2 +- src/value/graph_entities.rs | 117 +------------------ src/value/map.rs | 94 +--------------- src/value/mod.rs | 41 ++----- src/value/path.rs | 40 +------ src/value/utils.rs | 10 +- src/value/utils_async.rs | 87 -------------- 22 files changed, 290 insertions(+), 798 deletions(-) delete mode 100644 src/graph_schema/asynchronous.rs delete mode 100644 src/graph_schema/blocking.rs delete mode 100644 src/value/utils_async.rs diff --git a/Cargo.lock b/Cargo.lock index 078753b..6050686 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -257,15 +257,11 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ - "futures-channel", "futures-core", - "futures-io", "futures-sink", "futures-task", - "memchr", "pin-project-lite", "pin-utils", - "slab", ] [[package]] @@ -744,15 +740,6 @@ dependencies = [ "libc", ] -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - [[package]] name = "smallvec" version = "1.13.2" diff --git a/Cargo.toml b/Cargo.toml index 3c571f8..11063df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] anyhow = { version = "1.0.83", default-features = false, features = ["std"] } async-recursion = { version = "1.1.1" } -futures = { version = "0.3.30", default-features = false, features = ["std"], optional = true } +futures = { version = "0.3.30", default-features = false, optional = true } log = { version = "0.4.21", default-features = false } parking_lot = { version = "0.12.2", default-features = false, features = ["deadlock_detection"] } redis = { version = "0.25.3", default-features = false, optional = true } diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index db3a0b0..2dfe3d0 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -3,10 +3,11 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use crate::connection::blocking::FalkorSyncConnection; use crate::{ client::FalkorClientProvider, connection::asynchronous::BorrowedAsyncConnection, - parser::utils::string_vec_from_val, AsyncGraph, AsyncGraphSchema, ConfigValue, - FalkorAsyncConnection, FalkorConnectionInfo, FalkorDBError, FalkorValue, + parser::utils::string_vec_from_val, AsyncGraph, ConfigValue, FalkorAsyncConnection, + FalkorDBError, FalkorValue, GraphSchema, }; use anyhow::Result; use std::{ @@ -19,7 +20,6 @@ use tokio::sync::Mutex; pub(crate) struct FalkorAsyncClientInner { _inner: Arc, - graph_cache: Mutex>>>, connection_pool_size: u8, connection_pool: Arc>>, } @@ -36,29 +36,42 @@ impl FalkorAsyncClientInner { conn_pool: self.connection_pool.clone(), }) } + + pub(crate) fn create_sync_connection(&self) -> Result { + Ok(match self._inner.as_ref() { + #[cfg(feature = "redis")] + FalkorClientProvider::Redis(redis_client) => { + FalkorSyncConnection::Redis(redis_client.get_connection()?) + } + }) + } } pub struct FalkorAsyncClient { inner: Arc, - _connection_info: FalkorConnectionInfo, } impl FalkorAsyncClient { pub(crate) async fn create( client: FalkorClientProvider, - connection_info: FalkorConnectionInfo, num_connections: u8, timeout: Option, ) -> Result { let client = Arc::new(client); - let connection_pool: VecDeque<_> = futures::future::join_all( - (0..num_connections).map(|_| tokio::spawn(client.get_async_connection(timeout))), - ) - .await - .into_iter() - .flat_map(|x| x) - .flat_map(|x| x) - .collect(); + + // Wait for all tasks to complete and collect results + let connection_pool: VecDeque<_> = + futures::future::join_all((0..num_connections).map(|_| { + let client = Arc::clone(&client); + tokio::task::spawn(async move { + client.get_async_connection(timeout).await // Replace `timeout` with your actual timeout value + }) + })) + .await + .into_iter() + .flatten() + .flatten() + .collect(); if connection_pool.len() != num_connections as usize { Err(FalkorDBError::NoConnection)?; @@ -67,11 +80,9 @@ impl FalkorAsyncClient { Ok(Self { inner: Arc::new(FalkorAsyncClientInner { _inner: client, - graph_cache: Default::default(), connection_pool_size: num_connections, connection_pool: Arc::new(Mutex::new(connection_pool)), }), - _connection_info: connection_info, }) } @@ -166,14 +177,14 @@ impl FalkorAsyncClient { /// /// # Returns /// a [`SyncGraph`] object, allowing various graph operations. - pub async fn select_graph( + pub fn select_graph( &self, graph_name: T, ) -> AsyncGraph { AsyncGraph { client: self.inner.clone(), graph_name: graph_name.to_string(), - graph_schema: AsyncGraphSchema::new(graph_name.to_string()), // Required for requesting refreshes + graph_schema: GraphSchema::new(graph_name.to_string()), // Required for requesting refreshes } } @@ -199,7 +210,7 @@ impl FalkorAsyncClient { Some(&[new_graph_name]), ) .await?; - Ok(self.select_graph(new_graph_name).await) + Ok(self.select_graph(new_graph_name)) } } @@ -223,7 +234,7 @@ mod tests { async fn test_async_select_graph_and_query() { let client = create_async_test_client().await; - let mut graph = client.select_graph("imdb").await; + let mut graph = client.select_graph("imdb"); assert_eq!(graph.graph_name(), "imdb".to_string()); let res = graph @@ -240,7 +251,6 @@ mod tests { client .select_graph("imdb_async_ro_copy") - .await .delete() .await .ok(); @@ -250,7 +260,7 @@ mod tests { .await .expect("Could not copy graph"); - let mut original_graph = client.select_graph("imdb").await; + let mut original_graph = client.select_graph("imdb"); assert_eq!( graph diff --git a/src/client/blocking.rs b/src/client/blocking.rs index ead0715..e9e4774 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -7,7 +7,7 @@ use crate::{ client::FalkorClientProvider, connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}, parser::utils::string_vec_from_val, - ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorValue, SyncGraph, SyncGraphSchema, + ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorValue, GraphSchema, SyncGraph, }; use anyhow::Result; use parking_lot::Mutex; @@ -163,7 +163,7 @@ impl FalkorSyncClient { SyncGraph { client: self.inner.clone(), graph_name: graph_name.to_string(), - graph_schema: SyncGraphSchema::new(graph_name.to_string()), // Required for requesting refreshes + graph_schema: GraphSchema::new(graph_name.to_string()), // Required for requesting refreshes } } diff --git a/src/client/builder.rs b/src/client/builder.rs index b03947e..66f9e83 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -139,7 +139,6 @@ impl FalkorClientBuilder<'A'> { FalkorAsyncClient::create( Self::get_client(connection_info.clone())?, - connection_info, self.num_connections, self.timeout, ) diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index cba1f2f..d6536d2 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -6,9 +6,9 @@ use crate::{ client::asynchronous::FalkorAsyncClientInner, graph::utils::{construct_query, generate_procedure_call}, - parser::utils::{parse_header, parse_result_set_async}, - AsyncGraphSchema, Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorAsyncParseable, - FalkorDBError, FalkorIndex, FalkorResponse, FalkorValue, IndexType, ResultSet, SlowlogEntry, + parser::utils::{parse_header, parse_result_set}, + Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorDBError, FalkorIndex, + FalkorParsable, FalkorResponse, FalkorValue, GraphSchema, IndexType, ResultSet, SlowlogEntry, }; use anyhow::Result; use std::{collections::HashMap, fmt::Display, sync::Arc}; @@ -24,7 +24,7 @@ pub struct AsyncGraph { pub(crate) graph_name: String, /// Provides user with access to the current graph schema, /// which contains a safe cache of id to labels/properties/relationship maps - pub graph_schema: AsyncGraphSchema, + pub graph_schema: GraphSchema, } impl AsyncGraph { @@ -51,7 +51,7 @@ impl AsyncGraph { /// NOTE: This still maintains the graph API, operations are still viable. pub async fn delete(&mut self) -> Result<()> { self.send_command("GRAPH.DELETE", None, None).await?; - self.graph_schema.clear().await; + self.graph_schema.clear(); Ok(()) } @@ -185,7 +185,7 @@ impl AsyncGraph { let header_keys = parse_header(header)?; FalkorResponse::from_response_with_headers( - parse_result_set_async(data, &mut self.graph_schema, &mut conn, &header_keys).await?, + parse_result_set(data, &mut self.graph_schema, &mut conn, &header_keys)?, header_keys, stats, ) @@ -234,8 +234,7 @@ impl AsyncGraph { let header_keys = parse_header(header)?; FalkorResponse::from_response_with_headers( - parse_result_set_async(data, &mut self.graph_schema, &mut conn, &header_keys) - .await?, + parse_result_set(data, &mut self.graph_schema, &mut conn, &header_keys)?, header_keys, stats, ) @@ -408,7 +407,7 @@ impl AsyncGraph { /// /// # Returns /// A caller-provided type which implements [`FalkorParsable`] - pub async fn call_procedure( + pub async fn call_procedure( &mut self, procedure: C, args: Option<&[&str]>, @@ -419,7 +418,7 @@ impl AsyncGraph { let query = construct_query(query_string, params.as_ref()); let mut conn = self.client.borrow_connection().await?; - P::from_falkor_value_async( + P::from_falkor_value( conn.send_command( Some(self.graph_name.as_str()), if read_only { @@ -434,7 +433,6 @@ impl AsyncGraph { &mut self.graph_schema, &mut conn, ) - .await } /// Run a query which calls a procedure on the graph, read-only, or otherwise. @@ -451,7 +449,7 @@ impl AsyncGraph { /// /// # Returns /// A caller-provided type which implements [`FalkorParsable`] - pub async fn call_procedure_with_timeout( + pub async fn call_procedure_with_timeout( &mut self, procedure: C, args: Option<&[&str]>, @@ -463,7 +461,7 @@ impl AsyncGraph { let query = construct_query(query_string, params.as_ref()); let mut conn = self.client.borrow_connection().await?; - P::from_falkor_value_async( + P::from_falkor_value( conn.send_command( Some(self.graph_name.as_str()), if read_only { @@ -482,7 +480,6 @@ impl AsyncGraph { &mut self.graph_schema, &mut conn, ) - .await } /// Calls the DB.INDICES procedure on the graph, returning all the indexing methods currently used @@ -504,8 +501,7 @@ impl AsyncGraph { .into_vec()? .into_iter() .flat_map(|index| { - FalkorIndex::from_falkor_value_async(index, &mut self.graph_schema, &mut conn) - .await + FalkorIndex::from_falkor_value(index, &mut self.graph_schema, &mut conn) }) .collect(), stats, @@ -613,8 +609,7 @@ impl AsyncGraph { .into_vec()? .into_iter() .flat_map(|item| { - Constraint::from_falkor_value_async(item, &mut self.graph_schema, &mut conn) - .await + Constraint::from_falkor_value(item, &mut self.graph_schema, &mut conn) }) .collect(), stats, @@ -718,7 +713,7 @@ mod tests { use super::*; use crate::{test_utils::open_test_graph_async, IndexType}; - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn test_create_drop_index_async() { let mut graph = open_test_graph_async("test_create_drop_index_async").await; graph @@ -757,7 +752,7 @@ mod tests { .expect("Could not drop index"); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn test_list_indices_async() { let mut graph = open_test_graph_async("test_list_indices_async").await; let indices = graph @@ -781,7 +776,7 @@ mod tests { graph.inner.delete().await.ok(); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn test_create_drop_mandatory_constraint_async() { let graph = open_test_graph_async("test_mandatory_constraint_async").await; @@ -803,7 +798,7 @@ mod tests { .expect("Could not drop constraint"); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn test_create_drop_unique_constraint_async() { let mut graph = open_test_graph_async("test_unique_constraint_async").await; @@ -829,7 +824,7 @@ mod tests { .expect("Could not drop constraint"); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn test_list_constraints_async() { let mut graph = open_test_graph_async("test_list_constraint_async").await; @@ -851,7 +846,7 @@ mod tests { assert_eq!(constraints.data.len(), 1); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn test_slowlog_async() { let mut graph = open_test_graph_async("test_slowlog_async").await; @@ -895,7 +890,7 @@ mod tests { assert!(slowlog_after_reset.is_empty()); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn test_explain_async() { let graph = open_test_graph_async("test_explain_async").await; @@ -907,7 +902,7 @@ mod tests { ); } - #[tokio::test] + #[tokio::test(flavor = "multi_thread")] async fn test_profile_async() { let graph = open_test_graph_async("test_profile_async").await; diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index c1c2b9f..8391eff 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -8,8 +8,7 @@ use crate::{ graph::utils::{construct_query, generate_procedure_call}, parser::utils::{parse_header, parse_result_set}, Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorDBError, FalkorIndex, - FalkorParsable, FalkorResponse, FalkorValue, IndexType, ResultSet, SlowlogEntry, - SyncGraphSchema, + FalkorParsable, FalkorResponse, FalkorValue, GraphSchema, IndexType, ResultSet, SlowlogEntry, }; use anyhow::Result; use std::{collections::HashMap, fmt::Display, sync::Arc}; @@ -23,7 +22,7 @@ pub struct SyncGraph { pub(crate) client: Arc, pub(crate) graph_name: String, /// Provides user with access to the current graph schema, - pub graph_schema: SyncGraphSchema, + pub graph_schema: GraphSchema, } impl SyncGraph { diff --git a/src/graph_schema/asynchronous.rs b/src/graph_schema/asynchronous.rs deleted file mode 100644 index 85d8aa1..0000000 --- a/src/graph_schema/asynchronous.rs +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{ - connection::asynchronous::BorrowedAsyncConnection, - graph_schema::utils::{get_refresh_command, get_relevant_hashmap, update_map}, - value::FalkorValue, - FalkorDBError, SchemaType, -}; -use anyhow::Result; -use std::{ - collections::{HashMap, HashSet}, - ops::{Deref, DerefMut}, - sync::{ - atomic::{AtomicI64, Ordering::SeqCst}, - Arc, - }, -}; -use tokio::sync::RwLock; - -pub(crate) type LockableIdMap = Arc>>; - -#[derive(Clone, Debug, Default)] -pub struct AsyncGraphSchema { - graph_name: String, - version: Arc, - labels: LockableIdMap, - properties: LockableIdMap, - relationships: LockableIdMap, -} - -impl AsyncGraphSchema { - pub fn new(graph_name: String) -> Self { - Self { - graph_name, - ..Default::default() - } - } - - pub async fn clear(&mut self) { - self.version.store(0, SeqCst); - self.labels.write().await.clear(); - self.properties.write().await.clear(); - self.relationships.write().await.clear(); - } - - pub fn graph_name(&self) -> String { - self.graph_name.clone() - } - - pub fn relationships(&self) -> LockableIdMap { - self.relationships.clone() - } - - pub fn labels(&self) -> LockableIdMap { - self.labels.clone() - } - - pub fn properties(&self) -> LockableIdMap { - self.properties.clone() - } - - pub(crate) async fn verify_id_set( - &self, - id_set: &HashSet, - schema_type: SchemaType, - ) -> Option> { - let read_lock = match schema_type { - SchemaType::Labels => &self.labels, - SchemaType::Properties => &self.properties, - SchemaType::Relationships => &self.relationships, - } - .read() - .await; - - get_relevant_hashmap(id_set, read_lock.deref()) - } - - pub(crate) async fn refresh( - &self, - schema_type: SchemaType, - conn: &mut BorrowedAsyncConnection, - id_hashset: Option<&HashSet>, - ) -> Result>> { - let command = get_refresh_command(schema_type); - let map = match schema_type { - SchemaType::Labels => &self.labels, - SchemaType::Properties => &self.properties, - SchemaType::Relationships => &self.relationships, - }; - - let mut write_lock = map.write().await; - - // 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] = conn - .send_command( - Some(self.graph_name.as_str()), - "GRAPH.QUERY", - None, - Some(&[format!("CALL {command}()")]), - ) - .await? - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - Ok(update_map(write_lock.deref_mut(), keys, id_hashset)?) - } -} diff --git a/src/graph_schema/blocking.rs b/src/graph_schema/blocking.rs deleted file mode 100644 index f5aeabe..0000000 --- a/src/graph_schema/blocking.rs +++ /dev/null @@ -1,137 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{ - connection::blocking::BorrowedSyncConnection, - graph_schema::utils::{get_refresh_command, get_relevant_hashmap, update_map}, - value::FalkorValue, - FalkorDBError, SchemaType, -}; -use anyhow::Result; -use std::collections::{HashMap, HashSet}; - -pub(crate) type IdMap = HashMap; - -/// A struct containing the various schema maps, allowing conversions between ids and their string representations. -/// -/// # Thread Safety -/// This struct is fully thread safe, it can be cloned and passed within threads without constraints, -/// Its API uses only immutable references -#[derive(Clone, Debug, Default)] -pub struct SyncGraphSchema { - graph_name: String, - version: i64, - labels: IdMap, - properties: IdMap, - relationships: IdMap, -} - -impl SyncGraphSchema { - pub(crate) fn new(graph_name: String) -> Self { - Self { - graph_name, - ..Default::default() - } - } - - /// Clears all cached schemas, this will cause a refresh when next attempting to parse a compact query. - pub fn clear(&mut self) { - self.version = 0; - self.labels.clear(); - self.properties.clear(); - self.relationships.clear(); - } - - /// Returns a read-write-locked map, of the relationship ids to their respective string representations. - /// Minimize locking these to avoid starvation. - pub fn relationships(&self) -> &IdMap { - &self.relationships - } - - /// Returns a read-write-locked map, of the label ids to their respective string representations. - /// Minimize locking these to avoid starvation. - pub fn labels(&self) -> &IdMap { - &self.labels - } - - /// Returns a read-write-locked map, of the property ids to their respective string representations. - /// Minimize locking these to avoid starvation. - pub fn properties(&self) -> &IdMap { - &self.properties - } - - pub(crate) fn verify_id_set( - &self, - id_set: &HashSet, - schema_type: SchemaType, - ) -> Option> { - let id_map = match schema_type { - SchemaType::Labels => &self.labels, - SchemaType::Properties => &self.properties, - SchemaType::Relationships => &self.relationships, - }; - - get_relevant_hashmap(id_set, id_map) - } - - pub(crate) fn refresh( - &mut self, - schema_type: SchemaType, - conn: &mut BorrowedSyncConnection, - id_hashset: Option<&HashSet>, - ) -> Result>> { - let command = get_refresh_command(schema_type); - let id_map = match schema_type { - SchemaType::Labels => &mut self.labels, - SchemaType::Properties => &mut self.properties, - SchemaType::Relationships => &mut self.relationships, - }; - - // 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] = conn - .send_command( - Some(self.graph_name.as_str()), - "GRAPH.QUERY", - None, - Some(&[format!("CALL {command}()")]), - )? - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - Ok(update_map(id_map, keys, id_hashset)?) - } -} - -#[cfg(test)] -pub(crate) mod tests { - use super::*; - use crate::{test_utils::create_test_client, SyncGraph}; - use std::collections::HashMap; - - pub(crate) fn open_readonly_graph_with_modified_schema() -> (SyncGraph, BorrowedSyncConnection) - { - let client = create_test_client(); - let mut graph = client.select_graph("imdb"); - let conn = client - .borrow_connection() - .expect("Could not borrow_connection"); - - graph.graph_schema.properties = HashMap::from([ - (0, "age".to_string()), - (1, "is_boring".to_string()), - (2, "something_else".to_string()), - (3, "secs_since_login".to_string()), - ]); - - graph.graph_schema.labels = - HashMap::from([(0, "much".to_string()), (1, "actor".to_string())]); - - graph.graph_schema.relationships = - HashMap::from([(0, "very".to_string()), (1, "wow".to_string())]); - - (graph, conn) - } -} diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index ef4f26d..abfaeea 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -3,11 +3,15 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -pub(crate) mod blocking; -mod utils; +use crate::{connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorValue}; +use anyhow::Result; +use std::collections::{HashMap, HashSet}; +use utils::{get_refresh_command, get_relevant_hashmap, update_map}; #[cfg(feature = "tokio")] -pub(crate) mod asynchronous; +use crate::connection::asynchronous::BorrowedAsyncConnection; + +mod utils; /// An enum specifying which schema type we are addressing /// When querying using the compact parser, ids are returned for the various schema entities instead of strings @@ -18,3 +22,180 @@ pub enum SchemaType { Properties, Relationships, } + +pub trait RefreshSchemaKeys { + fn refresh_schema_keys( + &mut self, + schema_type: SchemaType, + graph_name: &str, + ) -> Result; +} + +impl RefreshSchemaKeys for BorrowedSyncConnection { + fn refresh_schema_keys( + &mut self, + schema_type: SchemaType, + graph_name: &str, + ) -> Result { + self.send_command( + Some(graph_name), + "GRAPH.QUERY", + None, + Some(&[format!("CALL {}()", get_refresh_command(schema_type))]), + ) + } +} +#[cfg(feature = "tokio")] +impl RefreshSchemaKeys for BorrowedAsyncConnection { + fn refresh_schema_keys( + &mut self, + schema_type: SchemaType, + graph_name: &str, + ) -> Result { + let (oneshot_tx, oneshot_rx) = std::sync::mpsc::sync_channel(1); + + tokio::task::spawn_blocking({ + let self_ptr = self as *mut BorrowedAsyncConnection as usize; + let graph_name = graph_name.to_string(); + move || unsafe { + let self_ref = &mut *(self_ptr as *mut BorrowedAsyncConnection); + let params = vec![format!("CALL {}()", get_refresh_command(schema_type))]; + + oneshot_tx + .send( + tokio::runtime::Handle::current().block_on(self_ref.send_command( + Some(graph_name.as_str()), + "GRAPH.QUERY", + None, + Some(params.as_slice()), + )), + ) + .ok(); + } + }); + + // Receive the result from the channel + oneshot_rx + .recv() + .map_err(Into::into) + .and_then(|res| res.map_err(Into::into)) + } +} + +pub(crate) type IdMap = HashMap; + +/// A struct containing the various schema maps, allowing conversions between ids and their string representations. +/// +/// # Thread Safety +/// This struct is fully thread safe, it can be cloned and passed within threads without constraints, +/// Its API uses only immutable references +#[derive(Clone, Debug, Default)] +pub struct GraphSchema { + graph_name: String, + version: i64, + labels: IdMap, + properties: IdMap, + relationships: IdMap, +} + +impl GraphSchema { + pub(crate) fn new(graph_name: String) -> Self { + Self { + graph_name, + ..Default::default() + } + } + + /// Clears all cached schemas, this will cause a refresh when next attempting to parse a compact query. + pub fn clear(&mut self) { + self.version = 0; + self.labels.clear(); + self.properties.clear(); + self.relationships.clear(); + } + + /// Returns a read-write-locked map, of the relationship ids to their respective string representations. + /// Minimize locking these to avoid starvation. + pub fn relationships(&self) -> &IdMap { + &self.relationships + } + + /// Returns a read-write-locked map, of the label ids to their respective string representations. + /// Minimize locking these to avoid starvation. + pub fn labels(&self) -> &IdMap { + &self.labels + } + + /// Returns a read-write-locked map, of the property ids to their respective string representations. + /// Minimize locking these to avoid starvation. + pub fn properties(&self) -> &IdMap { + &self.properties + } + + pub(crate) fn verify_id_set( + &self, + id_set: &HashSet, + schema_type: SchemaType, + ) -> Option> { + let id_map = match schema_type { + SchemaType::Labels => &self.labels, + SchemaType::Properties => &self.properties, + SchemaType::Relationships => &self.relationships, + }; + + get_relevant_hashmap(id_set, id_map) + } + + pub(crate) fn refresh( + &mut self, + conn: &mut BorrowedSyncConnection, + schema_type: SchemaType, + id_hashset: Option<&HashSet>, + ) -> Result>> { + let id_map = match schema_type { + SchemaType::Labels => &mut self.labels, + SchemaType::Properties => &mut self.properties, + SchemaType::Relationships => &mut self.relationships, + }; + + // 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] = conn + .refresh_schema_keys(schema_type, self.graph_name.as_str())? + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + Ok(update_map(id_map, keys, id_hashset)?) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use super::*; + use crate::{test_utils::create_test_client, SyncGraph}; + use std::collections::HashMap; + + pub(crate) fn open_readonly_graph_with_modified_schema() -> (SyncGraph, BorrowedSyncConnection) + { + let client = create_test_client(); + let mut graph = client.select_graph("imdb"); + let conn = client + .borrow_connection() + .expect("Could not borrow_connection"); + + graph.graph_schema.properties = HashMap::from([ + (0, "age".to_string()), + (1, "is_boring".to_string()), + (2, "something_else".to_string()), + (3, "secs_since_login".to_string()), + ]); + + graph.graph_schema.labels = + HashMap::from([(0, "much".to_string()), (1, "actor".to_string())]); + + graph.graph_schema.relationships = + HashMap::from([(0, "very".to_string()), (1, "wow".to_string())]); + + (graph, conn) + } +} diff --git a/src/lib.rs b/src/lib.rs index b6f4b49..99e3e8a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,7 +23,7 @@ pub use client::{blocking::FalkorSyncClient, builder::FalkorClientBuilder}; pub use connection_info::FalkorConnectionInfo; pub use error::FalkorDBError; pub use graph::blocking::SyncGraph; -pub use graph_schema::{blocking::SyncGraphSchema, SchemaType}; +pub use graph_schema::{GraphSchema, SchemaType}; pub use parser::FalkorParsable; pub use response::{ constraint::{Constraint, ConstraintStatus, ConstraintType}, @@ -43,8 +43,7 @@ pub use value::{ #[cfg(feature = "tokio")] pub use { client::asynchronous::FalkorAsyncClient, connection::asynchronous::FalkorAsyncConnection, - graph::asynchronous::AsyncGraph, graph_schema::asynchronous::AsyncGraphSchema, - parser::FalkorAsyncParseable, + graph::asynchronous::AsyncGraph, }; #[cfg(test)] @@ -87,9 +86,10 @@ pub(crate) mod test_utils { #[cfg(feature = "tokio")] impl Drop for TestAsyncGraphHandle { fn drop(&mut self) { - tokio::runtime::Handle::current().block_on(async { - self.inner.delete().await.ok(); - }); + tokio::task::block_in_place(|| { + tokio::runtime::Handle::current().block_on(self.inner.delete()) + }) + .ok(); } } @@ -105,7 +105,10 @@ pub(crate) mod test_utils { pub(crate) async fn open_test_graph_async(graph_name: &str) -> TestAsyncGraphHandle { let client = create_async_test_client().await; TestAsyncGraphHandle { - inner: client.select_graph(graph_name).await, + inner: client + .copy_graph("imdb", graph_name) + .await + .expect("Could not copy graph"), } } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index dc4f32d..40187a6 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5,26 +5,14 @@ pub mod utils; -use crate::{connection::blocking::BorrowedSyncConnection, FalkorValue, SyncGraphSchema}; +use crate::{connection::blocking::BorrowedSyncConnection, FalkorValue, GraphSchema}; use anyhow::Result; -#[cfg(feature = "tokio")] -use crate::{connection::asynchronous::BorrowedAsyncConnection, AsyncGraphSchema}; - /// This trait allows implementing a parser from the table-style result sent by the database, to any other struct pub trait FalkorParsable: Sized { fn from_falkor_value( value: FalkorValue, - graph_schema: &mut SyncGraphSchema, + graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result; } - -#[cfg(feature = "tokio")] -pub trait FalkorAsyncParseable: Sized { - fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, - ) -> impl std::future::Future> + Send; -} diff --git a/src/parser/utils.rs b/src/parser/utils.rs index ec29bd5..3061a63 100644 --- a/src/parser/utils.rs +++ b/src/parser/utils.rs @@ -3,20 +3,11 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{ - connection::blocking::BorrowedSyncConnection, - value::utils::{parse_type, type_val_from_value}, - FalkorDBError, FalkorValue, SyncGraphSchema, -}; +use crate::connection::blocking::BorrowedSyncConnection; +use crate::{value::utils::parse_type, FalkorDBError, FalkorValue, GraphSchema}; use anyhow::Result; use std::collections::HashMap; -#[cfg(feature = "tokio")] -use crate::{ - connection::asynchronous::BorrowedAsyncConnection, value::utils_async::parse_type_async, - AsyncGraphSchema, -}; - pub fn string_vec_from_val(value: FalkorValue) -> Result, FalkorDBError> { value.into_vec().map(|value_as_vec| { value_as_vec @@ -51,7 +42,7 @@ pub fn parse_header(header: FalkorValue) -> Result, FalkorDBError> { pub(crate) fn parse_result_set( data: FalkorValue, - graph_schema: &mut SyncGraphSchema, + graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, header_keys: &[String], ) -> Result>> { @@ -59,41 +50,13 @@ pub(crate) fn parse_result_set( .into_vec()? .into_iter() .flat_map(|column| { - anyhow::Result::<_, FalkorDBError>::Ok( + Result::<_, FalkorDBError>::Ok( header_keys .iter() .cloned() - .zip(column.into_vec()?.into_iter().flat_map(|column_subitem| { - type_val_from_value(column_subitem).and_then(|(type_marker, val)| { - parse_type(type_marker, val, graph_schema, conn) - }) - })) + .zip(parse_type(6, column, graph_schema, conn)) .collect(), ) }) .collect()) } - -#[cfg(feature = "tokio")] -pub(crate) async fn parse_result_set_async( - data: FalkorValue, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, - header_keys: &[String], -) -> Result>> { - let data_vec = data.into_vec()?; - let mut parsed_result_set = Vec::with_capacity(data_vec.len()); - for column in data_vec { - let column_vec = column.into_vec()?; - - let mut parsed_column = Vec::with_capacity(column_vec.len()); - for column_item in column_vec { - let (type_marker, val) = type_val_from_value(column_item)?; - parsed_column.push(parse_type_async(type_marker, val, graph_schema, conn).await?); - } - - parsed_result_set.push(header_keys.iter().cloned().zip(parsed_column).collect()) - } - - Ok(parsed_result_set) -} diff --git a/src/response/constraint.rs b/src/response/constraint.rs index 584493b..127a5cd 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -5,17 +5,11 @@ use crate::{ connection::blocking::BorrowedSyncConnection, value::utils::parse_type, EntityType, - FalkorDBError, FalkorParsable, FalkorValue, SyncGraphSchema, + FalkorDBError, FalkorParsable, FalkorValue, GraphSchema, }; use anyhow::Result; use std::fmt::{Display, Formatter}; -#[cfg(feature = "tokio")] -use crate::{ - connection::asynchronous::BorrowedAsyncConnection, value::utils_async::parse_type_async, - AsyncGraphSchema, FalkorAsyncParseable, -}; - /// The type of restriction to apply for the property #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ConstraintType { @@ -143,7 +137,7 @@ impl Constraint { impl FalkorParsable for Constraint { fn from_falkor_value( value: FalkorValue, - graph_schema: &mut SyncGraphSchema, + graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { parse_type(6, value, graph_schema, conn).and_then(|parsed| { @@ -154,21 +148,3 @@ impl FalkorParsable for Constraint { }) } } - -#[cfg(feature = "tokio")] -impl FalkorAsyncParseable for Constraint { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, - ) -> Result { - parse_type_async(6, value, graph_schema, conn) - .await - .and_then(|value_vec| { - value_vec - .into_vec() - .map_err(Into::into) - .and_then(Constraint::from_value_vec) - }) - } -} diff --git a/src/response/index.rs b/src/response/index.rs index 7ce2c37..2e1ca31 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -6,16 +6,11 @@ use crate::{ connection::blocking::BorrowedSyncConnection, value::utils::{parse_type, parse_vec, type_val_from_value}, - EntityType, FalkorDBError, FalkorParsable, FalkorValue, SyncGraphSchema, + EntityType, FalkorDBError, FalkorParsable, FalkorValue, GraphSchema, }; use anyhow::Result; use std::collections::HashMap; -#[cfg(feature = "tokio")] -use crate::{ - connection::asynchronous::BorrowedAsyncConnection, AsyncGraphSchema, FalkorAsyncParseable, -}; - /// The status of this index #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum IndexStatus { @@ -143,7 +138,7 @@ impl FalkorIndex { impl FalkorParsable for FalkorIndex { fn from_falkor_value( value: FalkorValue, - graph_schema: &mut SyncGraphSchema, + graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let semi_parsed_items = value @@ -158,24 +153,3 @@ impl FalkorParsable for FalkorIndex { Self::from_raw_values(semi_parsed_items) } } - -#[cfg(feature = "tokio")] -impl FalkorAsyncParseable for FalkorIndex { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, - ) -> Result { - let value_vec = value.into_vec()?; - let mut semi_parsed_items = Vec::with_capacity(value_vec.len()); - for item in value_vec { - let (type_marker, val) = type_val_from_value(item)?; - semi_parsed_items.push( - crate::value::utils_async::parse_type_async(type_marker, val, graph_schema, conn) - .await?, - ); - } - - Self::from_raw_values(semi_parsed_items) - } -} diff --git a/src/response/slowlog_entry.rs b/src/response/slowlog_entry.rs index aaaf853..fe25089 100644 --- a/src/response/slowlog_entry.rs +++ b/src/response/slowlog_entry.rs @@ -21,7 +21,7 @@ pub struct SlowlogEntry { impl TryFrom for SlowlogEntry { type Error = FalkorDBError; - fn try_from(value: FalkorValue) -> std::result::Result { + fn try_from(value: FalkorValue) -> Result { let [timestamp, command, arguments, time_taken] = value .into_vec()? .try_into() diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index ac116d0..82ebbf6 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -6,7 +6,7 @@ use crate::{ connection::blocking::BorrowedSyncConnection, value::{map::parse_map_with_schema, utils::parse_labels}, - FalkorDBError, FalkorParsable, FalkorValue, SchemaType, SyncGraphSchema, + FalkorDBError, FalkorParsable, FalkorValue, GraphSchema, SchemaType, }; use anyhow::Result; use std::{ @@ -14,13 +14,6 @@ use std::{ fmt::{Display, Formatter}, }; -#[cfg(feature = "tokio")] -use crate::{ - connection::asynchronous::BorrowedAsyncConnection, - value::{map::parse_map_with_schema_async, utils_async::parse_labels_async}, - AsyncGraphSchema, FalkorAsyncParseable, -}; - #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum EntityType { Node, @@ -82,7 +75,7 @@ pub struct Node { impl FalkorParsable for Node { fn from_falkor_value( value: FalkorValue, - graph_schema: &mut SyncGraphSchema, + graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let [entity_id, labels, properties]: [FalkorValue; 3] = value @@ -114,44 +107,6 @@ impl FalkorParsable for Node { } } -#[cfg(feature = "tokio")] -impl FalkorAsyncParseable for Node { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, - ) -> Result { - let [entity_id, labels, properties]: [FalkorValue; 3] = value - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - 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)?, - ); - } - - let parsed_labels = - parse_labels_async(labels, graph_schema, conn, SchemaType::Labels).await?; - Ok(Node { - entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - labels: parsed_labels, - properties: parse_map_with_schema_async( - properties, - graph_schema, - conn, - SchemaType::Properties, - ) - .await?, - }) - } -} - /// An edge in the graph, representing a relationship between two [`Node`]s. #[derive(Clone, Debug, Default, PartialEq)] pub struct Edge { @@ -170,7 +125,7 @@ pub struct Edge { impl FalkorParsable for Edge { fn from_falkor_value( value: FalkorValue, - graph_schema: &mut SyncGraphSchema, + graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value @@ -195,8 +150,8 @@ impl FalkorParsable for Edge { } match graph_schema.refresh( - SchemaType::Relationships, conn, + SchemaType::Relationships, Some(&HashSet::from([relation])), )? { None => Err(FalkorDBError::ParsingCompactIdUnknown)?, @@ -218,67 +173,3 @@ impl FalkorParsable for Edge { } } } - -#[cfg(feature = "tokio")] -impl FalkorAsyncParseable for Edge { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, - ) -> Result { - let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let relation = relations.to_i64().ok_or(FalkorDBError::ParsingI64)?; - if let Some(relationship) = graph_schema - .relationships() - .read() - .await - .get(&relation) - .cloned() - { - return Ok(Edge { - entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - relationship_type: relationship, - src_node_id: src_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - dst_node_id: dst_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - properties: parse_map_with_schema_async( - properties, - graph_schema, - conn, - SchemaType::Properties, - ) - .await?, - }); - } - - match graph_schema - .refresh( - SchemaType::Relationships, - conn, - Some(&HashSet::from([relation])), - ) - .await? - { - None => Err(FalkorDBError::ParsingCompactIdUnknown)?, - Some(id) => Ok(Edge { - entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - relationship_type: id - .get(&relation) - .cloned() - .ok_or(FalkorDBError::ParsingCompactIdUnknown)?, - src_node_id: src_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - dst_node_id: dst_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - properties: parse_map_with_schema_async( - properties, - graph_schema, - conn, - SchemaType::Properties, - ) - .await?, - }), - } - } -} diff --git a/src/value/map.rs b/src/value/map.rs index 888928c..fdca84b 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -5,17 +5,11 @@ use crate::{ connection::blocking::BorrowedSyncConnection, value::utils::parse_type, FalkorDBError, - FalkorParsable, FalkorValue, SchemaType, SyncGraphSchema, + FalkorParsable, FalkorValue, GraphSchema, SchemaType, }; use anyhow::Result; use std::collections::HashMap; -#[cfg(feature = "tokio")] -use crate::{ - connection::asynchronous::BorrowedAsyncConnection, value::utils_async::parse_type_async, - AsyncGraphSchema, FalkorAsyncParseable, -}; - // Intermediate type for map parsing pub(crate) struct FKeyTypeVal { key: i64, @@ -51,7 +45,7 @@ impl TryFrom for FKeyTypeVal { fn ktv_vec_to_map( map_vec: Vec, relevant_ids_map: HashMap, - graph_schema: &mut SyncGraphSchema, + graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result> { let mut new_map = HashMap::with_capacity(map_vec.len()); @@ -70,7 +64,7 @@ fn ktv_vec_to_map( pub(crate) fn parse_map_with_schema( value: FalkorValue, - graph_schema: &mut SyncGraphSchema, + graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, schema_type: SchemaType, ) -> Result> { @@ -86,7 +80,7 @@ pub(crate) fn parse_map_with_schema( } // If we reached here, schema validation failed and we need to refresh our schema - match graph_schema.refresh(schema_type, conn, Some(&id_hashset))? { + match graph_schema.refresh(conn, schema_type, Some(&id_hashset))? { Some(relevant_ids_map) => ktv_vec_to_map(map_vec, relevant_ids_map, graph_schema, conn), None => Err(FalkorDBError::ParsingError)?, } @@ -95,7 +89,7 @@ pub(crate) fn parse_map_with_schema( impl FalkorParsable for HashMap { fn from_falkor_value( value: FalkorValue, - graph_schema: &mut SyncGraphSchema, + graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let val_vec = value.into_vec()?; @@ -129,81 +123,3 @@ impl FalkorParsable for HashMap { .collect()) } } - -#[cfg(feature = "tokio")] -async fn ktv_vec_to_map_async( - map_vec: Vec, - relevant_ids_map: HashMap, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, -) -> Result> { - let mut new_map = HashMap::with_capacity(map_vec.len()); - for fktv in map_vec { - new_map.insert( - relevant_ids_map - .get(&fktv.key) - .cloned() - .ok_or(FalkorDBError::ParsingError)?, - parse_type_async(fktv.type_marker, fktv.val, graph_schema, conn).await?, - ); - } - - Ok(new_map) -} - -#[cfg(feature = "tokio")] -pub(crate) async fn parse_map_with_schema_async( - value: FalkorValue, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, - schema_type: SchemaType, -) -> Result> { - let val_vec = value.into_vec()?; - let (mut id_hashset, mut map_vec) = ( - std::collections::HashSet::with_capacity(val_vec.len()), - Vec::with_capacity(val_vec.len()), - ); - - for item in val_vec { - let fktv = FKeyTypeVal::try_from(item)?; - id_hashset.insert(fktv.key); - map_vec.push(fktv); - } - - if let Some(relevant_ids_map) = graph_schema.verify_id_set(&id_hashset, schema_type).await { - return ktv_vec_to_map_async(map_vec, relevant_ids_map, graph_schema, conn).await; - } - - // If we reached here, schema validation failed and we need to refresh our schema - match graph_schema - .refresh(schema_type, conn, Some(&id_hashset)) - .await? - { - Some(relevant_ids_map) => { - ktv_vec_to_map_async(map_vec, relevant_ids_map, graph_schema, conn).await - } - None => Err(FalkorDBError::ParsingError)?, - } -} - -#[cfg(feature = "tokio")] -impl FalkorAsyncParseable for HashMap { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, - ) -> Result { - let val_vec = value.into_vec()?; - - let mut new_map = HashMap::with_capacity(val_vec.len()); - for val in val_vec { - let fktv = FKeyTypeVal::try_from(val)?; - new_map.insert( - fktv.key.to_string(), - parse_type_async(fktv.type_marker, fktv.val, graph_schema, conn).await?, - ); - } - - Ok(new_map) - } -} diff --git a/src/value/mod.rs b/src/value/mod.rs index afe2187..0516845 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -4,7 +4,7 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, SyncGraphSchema, + connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, GraphSchema, }; use anyhow::Result; use graph_entities::{Edge, Node}; @@ -12,11 +12,6 @@ use path::Path; use point::Point; use std::{collections::HashMap, fmt::Debug}; -#[cfg(feature = "tokio")] -use crate::{ - connection::asynchronous::BorrowedAsyncConnection, AsyncGraphSchema, FalkorAsyncParseable, -}; - pub(crate) mod config; pub(crate) mod graph_entities; pub(crate) mod map; @@ -24,9 +19,6 @@ pub(crate) mod path; pub(crate) mod point; pub(crate) mod utils; -#[cfg(feature = "tokio")] -pub(crate) mod utils_async; - /// An enum of all the supported Falkor types #[derive(Clone, Debug, PartialEq)] pub enum FalkorValue { @@ -175,27 +167,6 @@ impl TryFrom for Point { } } -impl FalkorParsable for FalkorValue { - fn from_falkor_value( - value: FalkorValue, - _graph_schema: &mut SyncGraphSchema, - _conn: &mut BorrowedSyncConnection, - ) -> Result { - Ok(value) - } -} - -#[cfg(feature = "tokio")] -impl FalkorAsyncParseable for FalkorValue { - async fn from_falkor_value_async( - value: FalkorValue, - _graph_schema: &AsyncGraphSchema, - _conn: &mut BorrowedAsyncConnection, - ) -> Result { - Ok(value) - } -} - impl FalkorValue { /// Returns a reference to the internal [`Vec`] if this is an FArray variant. /// @@ -328,3 +299,13 @@ impl FalkorValue { self.try_into() } } + +impl FalkorParsable for FalkorValue { + fn from_falkor_value( + value: FalkorValue, + _graph_schema: &mut GraphSchema, + _conn: &mut BorrowedSyncConnection, + ) -> Result { + Ok(value) + } +} diff --git a/src/value/path.rs b/src/value/path.rs index 443a491..56ea80b 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -5,15 +5,10 @@ use crate::{ connection::blocking::BorrowedSyncConnection, Edge, FalkorDBError, FalkorParsable, FalkorValue, - Node, SyncGraphSchema, + GraphSchema, Node, }; use anyhow::Result; -#[cfg(feature = "tokio")] -use crate::{ - connection::asynchronous::BorrowedAsyncConnection, AsyncGraphSchema, FalkorAsyncParseable, -}; - /// Represents a path between two nodes, contains all the nodes, and the relationships between them along the path #[derive(Clone, Debug, PartialEq)] pub struct Path { @@ -24,7 +19,7 @@ pub struct Path { impl FalkorParsable for Path { fn from_falkor_value( value: FalkorValue, - graph_schema: &mut SyncGraphSchema, + graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let [nodes, relationships]: [FalkorValue; 2] = value @@ -46,34 +41,3 @@ impl FalkorParsable for Path { }) } } - -#[cfg(feature = "tokio")] -impl FalkorAsyncParseable for Path { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, - ) -> Result { - let [nodes, relationships]: [FalkorValue; 2] = value - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let (nodes, relationships) = (nodes.into_vec()?, relationships.into_vec()?); - - let mut parsed_nodes = Vec::with_capacity(nodes.len()); - for node_raw in nodes { - parsed_nodes.push(Node::from_falkor_value_async(node_raw, graph_schema, conn).await?); - } - - let mut parsed_edges = Vec::with_capacity(relationships.len()); - for edge_raw in relationships { - parsed_edges.push(Edge::from_falkor_value_async(edge_raw, graph_schema, conn).await?); - } - - Ok(Path { - nodes: parsed_nodes, - relationships: parsed_edges, - }) - } -} diff --git a/src/value/utils.rs b/src/value/utils.rs index 686ebb8..520b647 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -5,14 +5,14 @@ use crate::{ connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, FalkorValue, - Point, SchemaType, SyncGraphSchema, + GraphSchema, Point, SchemaType, }; use anyhow::Result; use std::collections::HashSet; pub(crate) fn parse_labels( raw_ids: Vec, - graph_schema: &mut SyncGraphSchema, + graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, schema_type: SchemaType, ) -> Result> { @@ -22,7 +22,7 @@ pub(crate) fn parse_labels( .collect::>(); let relevant_ids = match graph_schema.verify_id_set(&ids_hashset, schema_type) { - None => graph_schema.refresh(schema_type, conn, Some(&ids_hashset))?, + None => graph_schema.refresh(conn, schema_type, Some(&ids_hashset))?, relevant_ids => relevant_ids, } .ok_or(FalkorDBError::ParsingError)?; @@ -46,7 +46,7 @@ pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValu pub(crate) fn parse_type( type_marker: i64, val: FalkorValue, - graph_schema: &mut SyncGraphSchema, + graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, ) -> Result { let res = match type_marker { @@ -90,7 +90,7 @@ pub(crate) fn parse_vec>( #[cfg(test)] mod tests { use super::*; - use crate::graph_schema::blocking::tests::open_readonly_graph_with_modified_schema; + use crate::graph_schema::tests::open_readonly_graph_with_modified_schema; #[test] fn test_parse_edge() { diff --git a/src/value/utils_async.rs b/src/value/utils_async.rs deleted file mode 100644 index 725fc14..0000000 --- a/src/value/utils_async.rs +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{ - connection::asynchronous::BorrowedAsyncConnection, value::utils::type_val_from_value, - AsyncGraphSchema, Edge, FalkorAsyncParseable, FalkorDBError, FalkorValue, Node, Path, Point, - SchemaType, -}; -use anyhow::Result; -use async_recursion::async_recursion; -use std::collections::{HashMap, HashSet}; - -pub(crate) async fn parse_labels_async( - raw_ids: Vec, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, - schema_type: SchemaType, -) -> Result> { - let ids_hashset = raw_ids - .iter() - .filter_map(|label_id| label_id.to_i64()) - .collect::>(); - - match match graph_schema.verify_id_set(&ids_hashset, schema_type).await { - None => { - graph_schema - .refresh(schema_type, conn, Some(&ids_hashset)) - .await? - } - relevant_ids => relevant_ids, - } { - Some(relevant_ids) => { - let mut parsed_ids = Vec::with_capacity(raw_ids.len()); - for id in raw_ids { - parsed_ids.push( - id.to_i64() - .ok_or(FalkorDBError::ParsingI64) - .and_then(|id| { - relevant_ids - .get(&id) - .cloned() - .ok_or(FalkorDBError::ParsingCompactIdUnknown) - })?, - ); - } - - Ok(parsed_ids) - } - _ => Err(FalkorDBError::ParsingError)?, - } -} - -#[async_recursion] -pub(crate) async fn parse_type_async( - type_marker: i64, - val: FalkorValue, - graph_schema: &AsyncGraphSchema, - conn: &mut BorrowedAsyncConnection, -) -> Result { - let res = match type_marker { - 1 => FalkorValue::None, - 2 => FalkorValue::FString(val.into_string()?), - 3 => FalkorValue::Int64(val.to_i64().ok_or(FalkorDBError::ParsingI64)?), - 4 => FalkorValue::FBool(val.to_bool().ok_or(FalkorDBError::ParsingBool)?), - 5 => FalkorValue::F64(val.to_f64().ok_or(FalkorDBError::ParsingF64)?), - 6 => FalkorValue::FArray({ - let val = val.into_vec()?; - let mut parsed_vec = Vec::with_capacity(val.len()); - for item in val { - let (type_marker, val) = type_val_from_value(item)?; - parsed_vec.push(parse_type_async(type_marker, val, graph_schema, conn).await?); - } - parsed_vec - }), - // The following types are sent as an array and require specific parsing functions - 7 => FalkorValue::FEdge(Edge::from_falkor_value_async(val, graph_schema, conn).await?), - 8 => FalkorValue::FNode(Node::from_falkor_value_async(val, graph_schema, conn).await?), - 9 => FalkorValue::FPath(Path::from_falkor_value_async(val, graph_schema, conn).await?), - 10 => FalkorValue::FMap(HashMap::from_falkor_value_async(val, graph_schema, conn).await?), - 11 => FalkorValue::FPoint(Point::parse(val)?), - _ => Err(FalkorDBError::ParsingUnknownType)?, - }; - - Ok(res) -} From 49f83b55d53ad4075304fe56e4964206d7d7995f Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 21:48:33 +0300 Subject: [PATCH 32/62] Cleaner sync but async is in bad shape tbh --- src/client/asynchronous.rs | 10 ---- src/graph/asynchronous.rs | 53 ++++++++++-------- src/graph_schema/mod.rs | 33 +++++++++-- src/lib.rs | 2 +- src/parser/mod.rs | 14 +++++ src/parser/utils.rs | 35 +++++++++++- src/response/constraint.rs | 28 ++++++++++ src/response/index.rs | 30 ++++++++++ src/value/graph_entities.rs | 107 ++++++++++++++++++++++++++++++++++++ src/value/map.rs | 101 ++++++++++++++++++++++++++++++++++ src/value/mod.rs | 22 +++++++- src/value/path.rs | 34 ++++++++++++ src/value/utils.rs | 79 ++++++++++++++++++++++++++ 13 files changed, 505 insertions(+), 43 deletions(-) diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index 2dfe3d0..3afdc73 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -3,7 +3,6 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::FalkorSyncConnection; use crate::{ client::FalkorClientProvider, connection::asynchronous::BorrowedAsyncConnection, parser::utils::string_vec_from_val, AsyncGraph, ConfigValue, FalkorAsyncConnection, @@ -36,15 +35,6 @@ impl FalkorAsyncClientInner { conn_pool: self.connection_pool.clone(), }) } - - pub(crate) fn create_sync_connection(&self) -> Result { - Ok(match self._inner.as_ref() { - #[cfg(feature = "redis")] - FalkorClientProvider::Redis(redis_client) => { - FalkorSyncConnection::Redis(redis_client.get_connection()?) - } - }) - } } pub struct FalkorAsyncClient { diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index d6536d2..bdbb1bf 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -6,12 +6,14 @@ use crate::{ client::asynchronous::FalkorAsyncClientInner, graph::utils::{construct_query, generate_procedure_call}, - parser::utils::{parse_header, parse_result_set}, + parser::utils::{parse_header, parse_result_set_async}, Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorDBError, FalkorIndex, - FalkorParsable, FalkorResponse, FalkorValue, GraphSchema, IndexType, ResultSet, SlowlogEntry, + FalkorParsableAsync, FalkorResponse, FalkorValue, GraphSchema, IndexType, ResultSet, + SlowlogEntry, }; use anyhow::Result; use std::{collections::HashMap, fmt::Display, sync::Arc}; +use tokio::sync::Mutex; /// The main graph API, this allows the user to perform graph operations while exposing as little details as possible. /// @@ -184,8 +186,9 @@ impl AsyncGraph { .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let header_keys = parse_header(header)?; + let conn = Arc::new(Mutex::new(conn)); FalkorResponse::from_response_with_headers( - parse_result_set(data, &mut self.graph_schema, &mut conn, &header_keys)?, + parse_result_set_async(data, &mut self.graph_schema, conn, &header_keys).await?, header_keys, stats, ) @@ -233,8 +236,10 @@ impl AsyncGraph { .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let header_keys = parse_header(header)?; + let conn = Arc::new(Mutex::new(conn)); FalkorResponse::from_response_with_headers( - parse_result_set(data, &mut self.graph_schema, &mut conn, &header_keys)?, + parse_result_set_async(data, &mut self.graph_schema, conn, &header_keys) + .await?, header_keys, stats, ) @@ -406,8 +411,8 @@ impl AsyncGraph { /// * `timeout`: If provided, the query will abort if overruns the timeout. /// /// # Returns - /// A caller-provided type which implements [`FalkorParsable`] - pub async fn call_procedure( + /// A caller-provided type which implements [`FalkorParsableAsync`] + pub async fn call_procedure( &mut self, procedure: C, args: Option<&[&str]>, @@ -418,8 +423,8 @@ impl AsyncGraph { let query = construct_query(query_string, params.as_ref()); let mut conn = self.client.borrow_connection().await?; - P::from_falkor_value( - conn.send_command( + let res = conn + .send_command( Some(self.graph_name.as_str()), if read_only { "GRAPH.QUERY_RO" @@ -429,10 +434,10 @@ impl AsyncGraph { None, Some(&[query, "--compact".to_string()]), ) - .await?, - &mut self.graph_schema, - &mut conn, - ) + .await?; + + let conn = Arc::new(Mutex::new(conn)); + P::from_falkor_value_async(res, &mut self.graph_schema, conn).await } /// Run a query which calls a procedure on the graph, read-only, or otherwise. @@ -448,8 +453,8 @@ impl AsyncGraph { /// * `timeout`: If provided, the query will abort if overruns the timeout. /// /// # Returns - /// A caller-provided type which implements [`FalkorParsable`] - pub async fn call_procedure_with_timeout( + /// A caller-provided type which implements [`FalkorParsableAsync`] + pub async fn call_procedure_with_timeout( &mut self, procedure: C, args: Option<&[&str]>, @@ -461,8 +466,8 @@ impl AsyncGraph { let query = construct_query(query_string, params.as_ref()); let mut conn = self.client.borrow_connection().await?; - P::from_falkor_value( - conn.send_command( + let res = conn + .send_command( Some(self.graph_name.as_str()), if read_only { "GRAPH.QUERY_RO" @@ -476,10 +481,10 @@ impl AsyncGraph { format!("timeout {timeout}").as_str(), ]), ) - .await?, - &mut self.graph_schema, - &mut conn, - ) + .await?; + + let conn = Arc::new(Mutex::new(conn)); + P::from_falkor_value_async(res, &mut self.graph_schema, conn).await } /// Calls the DB.INDICES procedure on the graph, returning all the indexing methods currently used @@ -487,7 +492,6 @@ impl AsyncGraph { /// # Returns /// A [`Vec`] of [`FalkorIndex`] pub async fn list_indices(&mut self) -> Result>> { - let mut conn = self.client.borrow_connection().await?; let [header, indices, stats]: [FalkorValue; 3] = self .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false) .await? @@ -495,13 +499,15 @@ impl AsyncGraph { .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + let conn = Arc::new(Mutex::new(self.client.borrow_connection().await?)); FalkorResponse::from_response( Some(header), indices .into_vec()? .into_iter() .flat_map(|index| { - FalkorIndex::from_falkor_value(index, &mut self.graph_schema, &mut conn) + let conn = Arc::clone(&conn); + FalkorIndex::from_falkor_value_async(index, &mut self.graph_schema, conn).await }) .collect(), stats, @@ -603,13 +609,14 @@ impl AsyncGraph { .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + let conn = Arc::new(Mutex::new(conn)); FalkorResponse::from_response( Some(header), query_res .into_vec()? .into_iter() .flat_map(|item| { - Constraint::from_falkor_value(item, &mut self.graph_schema, &mut conn) + Constraint::from_falkor_value_async(item, &mut self.graph_schema, conn).await }) .collect(), stats, diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index abfaeea..131a308 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -9,7 +9,9 @@ use std::collections::{HashMap, HashSet}; use utils::{get_refresh_command, get_relevant_hashmap, update_map}; #[cfg(feature = "tokio")] -use crate::connection::asynchronous::BorrowedAsyncConnection; +use { + crate::connection::asynchronous::BorrowedAsyncConnection, std::sync::Arc, tokio::sync::Mutex, +}; mod utils; @@ -85,10 +87,6 @@ impl RefreshSchemaKeys for BorrowedAsyncConnection { pub(crate) type IdMap = HashMap; /// A struct containing the various schema maps, allowing conversions between ids and their string representations. -/// -/// # Thread Safety -/// This struct is fully thread safe, it can be cloned and passed within threads without constraints, -/// Its API uses only immutable references #[derive(Clone, Debug, Default)] pub struct GraphSchema { graph_name: String, @@ -167,6 +165,31 @@ impl GraphSchema { Ok(update_map(id_map, keys, id_hashset)?) } + + #[cfg(feature = "tokio")] + pub(crate) async fn refresh_async( + &mut self, + conn: &Arc>, + schema_type: SchemaType, + id_hashset: Option<&HashSet>, + ) -> Result>> { + let id_map = match schema_type { + SchemaType::Labels => &mut self.labels, + SchemaType::Properties => &mut self.properties, + SchemaType::Relationships => &mut self.relationships, + }; + + // 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] = conn + .lock() + .await + .refresh_schema_keys(schema_type, self.graph_name.as_str())? + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + Ok(update_map(id_map, keys, id_hashset)?) + } } #[cfg(test)] diff --git a/src/lib.rs b/src/lib.rs index 99e3e8a..7bd826e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,7 +43,7 @@ pub use value::{ #[cfg(feature = "tokio")] pub use { client::asynchronous::FalkorAsyncClient, connection::asynchronous::FalkorAsyncConnection, - graph::asynchronous::AsyncGraph, + graph::asynchronous::AsyncGraph, parser::FalkorParsableAsync, }; #[cfg(test)] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 40187a6..26f2d86 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -8,6 +8,11 @@ pub mod utils; use crate::{connection::blocking::BorrowedSyncConnection, FalkorValue, GraphSchema}; use anyhow::Result; +#[cfg(feature = "tokio")] +use { + crate::connection::asynchronous::BorrowedAsyncConnection, std::sync::Arc, tokio::sync::Mutex, +}; + /// This trait allows implementing a parser from the table-style result sent by the database, to any other struct pub trait FalkorParsable: Sized { fn from_falkor_value( @@ -16,3 +21,12 @@ pub trait FalkorParsable: Sized { conn: &mut BorrowedSyncConnection, ) -> Result; } + +#[cfg(feature = "tokio")] +pub trait FalkorParsableAsync: Sized { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &mut GraphSchema, + conn: Arc>, + ) -> Result; +} diff --git a/src/parser/utils.rs b/src/parser/utils.rs index 3061a63..86a9dc5 100644 --- a/src/parser/utils.rs +++ b/src/parser/utils.rs @@ -3,11 +3,20 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::BorrowedSyncConnection; -use crate::{value::utils::parse_type, FalkorDBError, FalkorValue, GraphSchema}; +use crate::{ + connection::blocking::BorrowedSyncConnection, value::utils::parse_type, FalkorDBError, + FalkorValue, GraphSchema, +}; use anyhow::Result; use std::collections::HashMap; +#[cfg(feature = "tokio")] +use { + crate::{connection::asynchronous::BorrowedAsyncConnection, value::utils::parse_type_async}, + std::sync::Arc, + tokio::sync::Mutex, +}; + pub fn string_vec_from_val(value: FalkorValue) -> Result, FalkorDBError> { value.into_vec().map(|value_as_vec| { value_as_vec @@ -60,3 +69,25 @@ pub(crate) fn parse_result_set( }) .collect()) } + +#[cfg(feature = "tokio")] +pub(crate) async fn parse_result_set_async( + data: FalkorValue, + graph_schema: &mut GraphSchema, + conn: Arc>, + header_keys: &[String], +) -> Result>> { + Ok(data + .into_vec()? + .into_iter() + .flat_map(|column| { + Result::<_, FalkorDBError>::Ok( + header_keys + .iter() + .cloned() + .zip(parse_type_async(6, column, graph_schema, conn).await) + .collect(), + ) + }) + .collect()) +} diff --git a/src/response/constraint.rs b/src/response/constraint.rs index 127a5cd..ed2d8a2 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -10,6 +10,16 @@ use crate::{ use anyhow::Result; use std::fmt::{Display, Formatter}; +#[cfg(feature = "tokio")] +use { + crate::{ + connection::asynchronous::BorrowedAsyncConnection, value::utils::parse_type_async, + FalkorParsableAsync, + }, + std::sync::Arc, + tokio::sync::Mutex, +}; + /// The type of restriction to apply for the property #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ConstraintType { @@ -148,3 +158,21 @@ impl FalkorParsable for Constraint { }) } } + +#[cfg(feature = "tokio")] +impl FalkorParsableAsync for Constraint { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &mut GraphSchema, + conn: Arc>, + ) -> Result { + parse_type_async(6, value, graph_schema, conn) + .await + .and_then(|parsed| { + parsed + .into_vec() + .map_err(Into::into) + .and_then(Constraint::from_value_vec) + }) + } +} diff --git a/src/response/index.rs b/src/response/index.rs index 2e1ca31..2a67af1 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -11,6 +11,16 @@ use crate::{ use anyhow::Result; use std::collections::HashMap; +#[cfg(feature = "tokio")] +use { + crate::{ + connection::asynchronous::BorrowedAsyncConnection, value::utils::parse_type_async, + FalkorParsableAsync, + }, + std::sync::Arc, + tokio::sync::Mutex, +}; + /// The status of this index #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum IndexStatus { @@ -153,3 +163,23 @@ impl FalkorParsable for FalkorIndex { Self::from_raw_values(semi_parsed_items) } } + +#[cfg(feature = "tokio")] +impl FalkorParsableAsync for FalkorIndex { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &mut GraphSchema, + conn: Arc>, + ) -> Result { + let semi_parsed_items = value + .into_vec()? + .into_iter() + .flat_map(|item| { + let (type_marker, val) = type_val_from_value(item)?; + parse_type_async(type_marker, val, graph_schema, conn).await + }) + .collect::>(); + + Self::from_raw_values(semi_parsed_items) + } +} diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index 82ebbf6..639d891 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -14,6 +14,17 @@ use std::{ fmt::{Display, Formatter}, }; +#[cfg(feature = "tokio")] +use { + crate::{ + connection::asynchronous::BorrowedAsyncConnection, + value::{map::parse_map_with_schema_async, utils::parse_labels_async}, + FalkorParsableAsync, + }, + std::sync::Arc, + tokio::sync::Mutex, +}; + #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum EntityType { Node, @@ -107,6 +118,44 @@ impl FalkorParsable for Node { } } +#[cfg(feature = "tokio")] +impl FalkorParsableAsync for Node { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &mut GraphSchema, + conn: Arc>, + ) -> Result { + let [entity_id, labels, properties]: [FalkorValue; 3] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + 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)?, + ); + } + + let parsed_labels = + parse_labels_async(labels, graph_schema, Arc::clone(&conn), SchemaType::Labels).await?; + Ok(Node { + entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + labels: parsed_labels, + properties: parse_map_with_schema_async( + properties, + graph_schema, + conn, + SchemaType::Properties, + ) + .await?, + }) + } +} + /// An edge in the graph, representing a relationship between two [`Node`]s. #[derive(Clone, Debug, Default, PartialEq)] pub struct Edge { @@ -173,3 +222,61 @@ impl FalkorParsable for Edge { } } } + +#[cfg(feature = "tokio")] +impl FalkorParsableAsync for Edge { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &mut GraphSchema, + conn: Arc>, + ) -> Result { + let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + let relation = relations.to_i64().ok_or(FalkorDBError::ParsingI64)?; + if let Some(relationship) = graph_schema.relationships().get(&relation) { + return Ok(Edge { + entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + 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)?, + properties: parse_map_with_schema_async( + properties, + graph_schema, + conn, + SchemaType::Properties, + ) + .await?, + }); + } + + match graph_schema + .refresh_async( + &conn, + SchemaType::Relationships, + Some(&HashSet::from([relation])), + ) + .await? + { + None => Err(FalkorDBError::ParsingCompactIdUnknown)?, + Some(id) => Ok(Edge { + entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + relationship_type: id + .get(&relation) + .cloned() + .ok_or(FalkorDBError::ParsingCompactIdUnknown)?, + src_node_id: src_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + dst_node_id: dst_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + properties: parse_map_with_schema_async( + properties, + graph_schema, + conn, + SchemaType::Properties, + ) + .await?, + }), + } + } +} diff --git a/src/value/map.rs b/src/value/map.rs index fdca84b..3912278 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -10,6 +10,16 @@ use crate::{ use anyhow::Result; use std::collections::HashMap; +#[cfg(feature = "tokio")] +use { + crate::{ + connection::asynchronous::BorrowedAsyncConnection, value::utils::parse_type_async, + FalkorParsableAsync, + }, + std::sync::Arc, + tokio::sync::Mutex, +}; + // Intermediate type for map parsing pub(crate) struct FKeyTypeVal { key: i64, @@ -62,6 +72,27 @@ fn ktv_vec_to_map( Ok(new_map) } +#[cfg(feature = "tokio")] +async fn ktv_vec_to_map_async( + map_vec: Vec, + relevant_ids_map: HashMap, + graph_schema: &mut GraphSchema, + conn: Arc>, +) -> Result> { + let mut new_map = HashMap::with_capacity(map_vec.len()); + for fktv in map_vec { + new_map.insert( + relevant_ids_map + .get(&fktv.key) + .cloned() + .ok_or(FalkorDBError::ParsingError)?, + parse_type_async(fktv.type_marker, fktv.val, graph_schema, Arc::clone(&conn)).await?, + ); + } + + Ok(new_map) +} + pub(crate) fn parse_map_with_schema( value: FalkorValue, graph_schema: &mut GraphSchema, @@ -86,6 +117,36 @@ pub(crate) fn parse_map_with_schema( } } +#[cfg(feature = "tokio")] +pub(crate) async fn parse_map_with_schema_async( + value: FalkorValue, + graph_schema: &mut GraphSchema, + conn: Arc>, + schema_type: SchemaType, +) -> Result> { + let (id_hashset, map_vec) = value + .into_vec()? + .into_iter() + .flat_map(FKeyTypeVal::try_from) + .map(|fktv| (fktv.key, fktv)) + .unzip(); + + if let Some(relevant_ids_map) = graph_schema.verify_id_set(&id_hashset, schema_type) { + return ktv_vec_to_map_async(map_vec, relevant_ids_map, graph_schema, conn).await; + } + + // If we reached here, schema validation failed and we need to refresh our schema + match graph_schema + .refresh_async(&conn, schema_type, Some(&id_hashset)) + .await? + { + Some(relevant_ids_map) => { + ktv_vec_to_map_async(map_vec, relevant_ids_map, graph_schema, conn).await + } + None => Err(FalkorDBError::ParsingError)?, + } +} + impl FalkorParsable for HashMap { fn from_falkor_value( value: FalkorValue, @@ -123,3 +184,43 @@ impl FalkorParsable for HashMap { .collect()) } } + +#[cfg(feature = "tokio")] +impl FalkorParsableAsync for HashMap { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &mut GraphSchema, + conn: Arc>, + ) -> Result { + let val_vec = value.into_vec()?; + if val_vec.len() % 2 != 0 { + Err(FalkorDBError::ParsingFMap)?; + } + + Ok(val_vec + .chunks_exact(2) + .flat_map(|pair| { + let [key, val]: [FalkorValue; 2] = pair + .to_vec() + .try_into() + .map_err(|_| FalkorDBError::ParsingFMap)?; + + let [type_marker, val]: [FalkorValue; 2] = val + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingFMap)?; + + Result::<_>::Ok(( + key.into_string()?, + parse_type_async( + type_marker.to_i64().ok_or(FalkorDBError::ParsingKTVTypes)?, + val, + graph_schema, + conn, + ) + .await?, + )) + }) + .collect()) + } +} diff --git a/src/value/mod.rs b/src/value/mod.rs index 0516845..04927b9 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -12,6 +12,13 @@ use path::Path; use point::Point; use std::{collections::HashMap, fmt::Debug}; +#[cfg(feature = "tokio")] +use { + crate::{connection::asynchronous::BorrowedAsyncConnection, FalkorParsableAsync}, + std::sync::Arc, + tokio::sync::Mutex, +}; + pub(crate) mod config; pub(crate) mod graph_entities; pub(crate) mod map; @@ -303,8 +310,19 @@ impl FalkorValue { impl FalkorParsable for FalkorValue { fn from_falkor_value( value: FalkorValue, - _graph_schema: &mut GraphSchema, - _conn: &mut BorrowedSyncConnection, + _: &mut GraphSchema, + _: &mut BorrowedSyncConnection, + ) -> Result { + Ok(value) + } +} + +#[cfg(feature = "tokio")] +impl FalkorParsableAsync for FalkorValue { + async fn from_falkor_value_async( + value: FalkorValue, + _: &mut GraphSchema, + _: Arc>, ) -> Result { Ok(value) } diff --git a/src/value/path.rs b/src/value/path.rs index 56ea80b..0df2d36 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -9,6 +9,13 @@ use crate::{ }; use anyhow::Result; +#[cfg(feature = "tokio")] +use { + crate::{connection::asynchronous::BorrowedAsyncConnection, FalkorParsableAsync}, + std::sync::Arc, + tokio::sync::Mutex, +}; + /// Represents a path between two nodes, contains all the nodes, and the relationships between them along the path #[derive(Clone, Debug, PartialEq)] pub struct Path { @@ -41,3 +48,30 @@ impl FalkorParsable for Path { }) } } + +#[cfg(feature = "tokio")] +impl FalkorParsableAsync for Path { + async fn from_falkor_value_async( + value: FalkorValue, + graph_schema: &mut GraphSchema, + conn: Arc>, + ) -> Result { + let [nodes, relationships]: [FalkorValue; 2] = value + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + Ok(Self { + nodes: nodes + .into_vec()? + .into_iter() + .flat_map(|node| Node::from_falkor_value_async(node, graph_schema, conn).await) + .collect(), + relationships: relationships + .into_vec()? + .into_iter() + .flat_map(|edge| Edge::from_falkor_value_async(edge, graph_schema, conn).await) + .collect(), + }) + } +} diff --git a/src/value/utils.rs b/src/value/utils.rs index 520b647..f677105 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -10,6 +10,13 @@ use crate::{ use anyhow::Result; use std::collections::HashSet; +#[cfg(feature = "tokio")] +use { + crate::{connection::asynchronous::BorrowedAsyncConnection, FalkorParsableAsync}, + std::sync::Arc, + tokio::sync::Mutex, +}; + pub(crate) fn parse_labels( raw_ids: Vec, graph_schema: &mut GraphSchema, @@ -33,6 +40,34 @@ pub(crate) fn parse_labels( .collect()) } +#[cfg(feature = "tokio")] +pub(crate) async fn parse_labels_async( + raw_ids: Vec, + graph_schema: &mut GraphSchema, + conn: Arc>, + schema_type: SchemaType, +) -> Result> { + let ids_hashset = raw_ids + .iter() + .filter_map(|label_id| label_id.to_i64()) + .collect::>(); + + let relevant_ids = match graph_schema.verify_id_set(&ids_hashset, schema_type) { + None => { + graph_schema + .refresh_async(&conn, schema_type, Some(&ids_hashset)) + .await? + } + relevant_ids => relevant_ids, + } + .ok_or(FalkorDBError::ParsingError)?; + + Ok(raw_ids + .into_iter() + .filter_map(|id| id.to_i64().and_then(|id| relevant_ids.get(&id).cloned())) + .collect()) +} + pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValue)> { let [type_marker, val]: [FalkorValue; 2] = value .into_vec()? @@ -77,6 +112,50 @@ pub(crate) fn parse_type( Ok(res) } +#[cfg(feature = "tokio")] +#[async_recursion::async_recursion] +pub(crate) async fn parse_type_async( + type_marker: i64, + val: FalkorValue, + graph_schema: &mut GraphSchema, + conn: Arc>, +) -> Result { + let res = match type_marker { + 1 => FalkorValue::None, + 2 => FalkorValue::FString(val.into_string()?), + 3 => FalkorValue::Int64(val.to_i64().ok_or(FalkorDBError::ParsingI64)?), + 4 => FalkorValue::FBool(val.to_bool().ok_or(FalkorDBError::ParsingBool)?), + 5 => FalkorValue::F64(val.try_into()?), + 6 => FalkorValue::FArray({ + val.into_vec()? + .into_iter() + .flat_map(|item| { + type_val_from_value(item).and_then(|(type_marker, val)| { + parse_type_async(type_marker, val, graph_schema, conn).await + }) + }) + .collect() + }), + // The following types are sent as an array and require specific parsing functions + 7 => FalkorValue::FEdge( + FalkorParsableAsync::from_falkor_value_async(val, graph_schema, conn).await?, + ), + 8 => FalkorValue::FNode( + FalkorParsableAsync::from_falkor_value_async(val, graph_schema, conn).await?, + ), + 9 => FalkorValue::FPath( + FalkorParsableAsync::from_falkor_value_async(val, graph_schema, conn).await?, + ), + 10 => FalkorValue::FMap( + FalkorParsableAsync::from_falkor_value_async(val, graph_schema, conn).await?, + ), + 11 => FalkorValue::FPoint(Point::parse(val)?), + _ => Err(FalkorDBError::ParsingUnknownType)?, + }; + + Ok(res) +} + pub(crate) fn parse_vec>( value: FalkorValue ) -> Result, FalkorDBError> { From 8a07cb7f7ec6b988ec85dc304408fc46b6039b73 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 22:00:47 +0300 Subject: [PATCH 33/62] Add falkor container for tests --- .github/workflows/codecov.yml | 8 ++++++++ .github/workflows/pr-checks.yml | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 5d1d6d3..e47aae6 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -10,6 +10,14 @@ env: jobs: coverage: runs-on: linux-latest + + services: + falkordb: + image: falkordb/falkordb:edge + volumes: + - resources/imdb.rdb:/FalkorDB/dump.rdb + ports: + - 6379:6379 steps: - uses: actions/checkout@v4 - uses: taiki-e/install-action@cargo-llvm-cov diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 6930161..5b16c55 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -40,6 +40,13 @@ jobs: build-linux: runs-on: ubuntu-latest + services: + falkordb: + image: falkordb/falkordb:edge + volumes: + - resources/imdb.rdb:/FalkorDB/dump.rdb + ports: + - 6379:6379 steps: - uses: actions/checkout@v4 - name: Build From fac3a4cae14cce01700f89b1b3ce934876639604 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 22:03:09 +0300 Subject: [PATCH 34/62] Try local path --- .github/workflows/codecov.yml | 2 +- .github/workflows/pr-checks.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index e47aae6..4807eea 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -15,7 +15,7 @@ jobs: falkordb: image: falkordb/falkordb:edge volumes: - - resources/imdb.rdb:/FalkorDB/dump.rdb + - ./resources/imdb.rdb:/FalkorDB/dump.rdb ports: - 6379:6379 steps: diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 5b16c55..ac3bc32 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -44,7 +44,7 @@ jobs: falkordb: image: falkordb/falkordb:edge volumes: - - resources/imdb.rdb:/FalkorDB/dump.rdb + - ./resources/imdb.rdb:/FalkorDB/dump.rdb ports: - 6379:6379 steps: From 9923657c791ccf7e24d01ac9bb70d07750a6069b Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 22:05:38 +0300 Subject: [PATCH 35/62] Use prebuilt deny --- .github/workflows/pr-checks.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index ac3bc32..44b43a0 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -19,8 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Install Deny - run: cargo install cargo-deny + - uses: EmbarkStudios/cargo-deny-action@v1 - name: Check Deny run: cargo deny --log-level info check From 621fff156e409b7955819482b7e6a6065bedbf81 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 22:28:26 +0300 Subject: [PATCH 36/62] Cargo deny --- deny.toml | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/deny.toml b/deny.toml index 008ef05..1b10793 100644 --- a/deny.toml +++ b/deny.toml @@ -2,13 +2,20 @@ [bans] multiple-versions = "deny" -skip = [] +skip = ["windows_x86_64_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", + "windows_i686_msvc", "windows_i686_gnu", "windows_i686_gnullvm", + "windows_aarch64_msvc", "windows_aarch64_gnu", "windows_aarch64_gnullvm", + "windows-targets", "windows-sys"] # Windows crates are all locked in `mio`, but they are very strictly BC so are ok [sources] unknown-registry = "deny" unknown-git = "deny" [licenses] +exceptions = [ + { name = "unicode-ident", allow = ["Unicode-DFS-2016"] }, # One of a kind license, no need to put it in the "allow" list + { name = "ring", allow = ["LicenseRef-ring"] } # ring uses a specific BoringSSL license that does not match the standard text so requires allowing the specific hash +] unused-allowed-license = "allow" confidence-threshold = 0.93 allow = [ @@ -18,8 +25,16 @@ allow = [ "BSD-2-Clause", "BSD-3-Clause", "MIT", - "Unicode-DFS-2016", "GPL-3.0", "LGPL-3.0", - "AGPL-3.0" + "AGPL-3.0", + "ISC" +] + + +[[licenses.clarify]] +name = "ring" +expression = "LicenseRef-ring" +license-files = [ + { path = "LICENSE", hash = 0xbd0eed23 }, ] From cb1ef4f890175e14b63867f7407f1fe5b15f2c6e Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 28 May 2024 22:31:59 +0300 Subject: [PATCH 37/62] Removed volumes TODO: add a ctor() function using the ctor crate --- .github/workflows/codecov.yml | 2 -- .github/workflows/pr-checks.yml | 2 -- 2 files changed, 4 deletions(-) diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 4807eea..b216681 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -14,8 +14,6 @@ jobs: services: falkordb: image: falkordb/falkordb:edge - volumes: - - ./resources/imdb.rdb:/FalkorDB/dump.rdb ports: - 6379:6379 steps: diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 44b43a0..5120016 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -42,8 +42,6 @@ jobs: services: falkordb: image: falkordb/falkordb:edge - volumes: - - ./resources/imdb.rdb:/FalkorDB/dump.rdb ports: - 6379:6379 steps: From 6a7f87e7f3704364eecbb7bb5df97764f15a8125 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 29 May 2024 16:32:06 +0300 Subject: [PATCH 38/62] Docs finished, reverted to original schema logic --- README.md | 36 ++ clippy.toml | 5 - src/client/asynchronous.rs | 157 +-------- src/client/blocking.rs | 49 +-- src/client/builder.rs | 28 +- src/connection/asynchronous.rs | 13 +- src/connection/blocking.rs | 13 +- src/connection_info/mod.rs | 29 +- src/error/mod.rs | 41 +++ src/graph/asynchronous.rs | 24 +- src/graph/blocking.rs | 603 +++++++++------------------------ src/graph/mod.rs | 3 +- src/graph/query_builder.rs | 364 ++++++++++++++++++++ src/graph/utils.rs | 96 ------ src/graph_schema/mod.rs | 115 +------ src/graph_schema/utils.rs | 10 +- src/lib.rs | 49 +-- src/parser/mod.rs | 21 +- src/parser/utils.rs | 58 +--- src/redis_ext.rs | 8 +- src/response/constraint.rs | 55 +-- src/response/index.rs | 44 +-- src/response/mod.rs | 41 +-- src/value/config.rs | 7 +- src/value/graph_entities.rs | 188 +++------- src/value/map.rs | 114 +------ src/value/mod.rs | 127 ++++--- src/value/path.rs | 43 +-- src/value/point.rs | 12 +- src/value/utils.rs | 296 ++++++---------- 30 files changed, 1017 insertions(+), 1632 deletions(-) delete mode 100644 clippy.toml create mode 100644 src/graph/query_builder.rs delete mode 100644 src/graph/utils.rs diff --git a/README.md b/README.md index 8b188b1..a3ce0e2 100644 --- a/README.md +++ b/README.md @@ -3,3 +3,39 @@ [![Codecov](https://codecov.io/gh/falkordb/falkordb-client-rs/branch/main/graph/badge.svg)](https://codecov.io/gh/falkordb/falkordb-client-rs) [![Forum](https://img.shields.io/badge/Forum-falkordb-blue)](https://github.com/orgs/FalkorDB/discussions) [![Discord](https://img.shields.io/discord/1146782921294884966?style=flat-square)](https://discord.gg/ErBEqN9E) + +# falkordb-client-rs + +[![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 + +## Usage + +### Run FalkorDB instance + +Docker: + +```sh +docker run --rm -p 6379:6379 falkordb/falkordb +``` + +Or use our [sandbox](https://cloud.falkordb.com/sandbox) + +### Example + +```rust +use falkordb_client_rs::FalkorClientBuilder; + +// Connect to FalkorDB +let client = FalkorClientBuilder::new().with_connection_info("falkor://127.0.0.1:6379".try_into().unwrap()).build().unwrap(); + +// Select the social graph +let mut graph = client.select_graph("social"); + +// Create 100 nodes and return a handful +let nodes = graph.query("UNWIND range(0, 100) AS i CREATE (n { v:1 }) RETURN n LIMIT 10").with_timeout(5000).perform().unwrap().data; +for n in nodes { +println!("{:?}", n[0]); +} +``` diff --git a/clippy.toml b/clippy.toml deleted file mode 100644 index d46845e..0000000 --- a/clippy.toml +++ /dev/null @@ -1,5 +0,0 @@ -# Environment -msrv = "1.72" - -# Documentation -missing-docs-in-crate-items = true \ No newline at end of file diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs index 3afdc73..f4ff4fa 100644 --- a/src/client/asynchronous.rs +++ b/src/client/asynchronous.rs @@ -6,9 +6,8 @@ use crate::{ client::FalkorClientProvider, connection::asynchronous::BorrowedAsyncConnection, parser::utils::string_vec_from_val, AsyncGraph, ConfigValue, FalkorAsyncConnection, - FalkorDBError, FalkorValue, GraphSchema, + FalkorDBError, FalkorResult, FalkorValue, GraphSchema, }; -use anyhow::Result; use std::{ collections::{HashMap, VecDeque}, fmt::Display, @@ -24,7 +23,7 @@ pub(crate) struct FalkorAsyncClientInner { } impl FalkorAsyncClientInner { - pub(crate) async fn borrow_connection(&self) -> Result { + pub(crate) async fn borrow_connection(&self) -> FalkorResult { let mut conn_pool = self.connection_pool.lock().await; let connection = conn_pool .pop_front() @@ -46,7 +45,7 @@ impl FalkorAsyncClient { client: FalkorClientProvider, num_connections: u8, timeout: Option, - ) -> Result { + ) -> FalkorResult { let client = Arc::new(client); // Wait for all tasks to complete and collect results @@ -81,7 +80,7 @@ impl FalkorAsyncClient { self.inner.connection_pool_size } - pub(crate) async fn borrow_connection(&self) -> Result { + pub(crate) async fn borrow_connection(&self) -> FalkorResult { self.inner.borrow_connection().await } @@ -89,11 +88,11 @@ impl FalkorAsyncClient { /// /// # Returns /// A [`Vec`] of [`String`]s, containing the names of available graphs - pub async fn list_graphs(&self) -> Result> { + pub async fn list_graphs(&self) -> FalkorResult> { let mut conn = self.borrow_connection().await?; conn.send_command::<&str>(None, "GRAPH.LIST", None, None) .await - .and_then(|res| string_vec_from_val(res).map_err(Into::into)) + .and_then(|res| string_vec_from_val(res)) } /// Return the current value of a configuration option in the database. @@ -107,7 +106,7 @@ impl FalkorAsyncClient { pub async fn config_get( &self, config_key: T, - ) -> Result> { + ) -> FalkorResult> { let mut conn = self.borrow_connection().await?; let config = conn .send_command(None, "GRAPH.CONFIG", Some("GET"), Some(&[config_key])) @@ -148,7 +147,7 @@ impl FalkorAsyncClient { &self, config_key: T, value: C, - ) -> Result { + ) -> FalkorResult { self.borrow_connection() .await? .send_command( @@ -190,7 +189,7 @@ impl FalkorAsyncClient { &self, graph_to_clone: &str, new_graph_name: &str, - ) -> Result { + ) -> FalkorResult { self.borrow_connection() .await? .send_command( @@ -203,141 +202,3 @@ impl FalkorAsyncClient { Ok(self.select_graph(new_graph_name)) } } - -#[cfg(test)] -mod tests { - use super::*; - use crate::test_utils::create_async_test_client; - use std::{mem, thread}; - - #[tokio::test] - async fn test_async_list_graphs() { - let client = create_async_test_client().await; - let res = client.list_graphs().await; - assert!(res.is_ok()); - - let graphs = res.unwrap(); - assert_eq!(graphs[0], "imdb"); - } - - #[tokio::test] - async fn test_async_select_graph_and_query() { - let client = create_async_test_client().await; - - let mut graph = client.select_graph("imdb"); - assert_eq!(graph.graph_name(), "imdb".to_string()); - - let res = graph - .query("MATCH (a:actor) return a".to_string()) - .await - .expect("Could not get actors from unmodified graph"); - - assert_eq!(res.data.len(), 1317); - } - - #[tokio::test] - async fn test_async_copy_graph() { - let client = create_async_test_client().await; - - client - .select_graph("imdb_async_ro_copy") - .delete() - .await - .ok(); - - let mut graph = client - .copy_graph("imdb", "imdb_async_ro_copy") - .await - .expect("Could not copy graph"); - - let mut original_graph = client.select_graph("imdb"); - - assert_eq!( - graph - .query("MATCH (a:actor) RETURN a".to_string()) - .await - .expect("Could not get actors from unmodified graph") - .data, - original_graph - .query("MATCH (a:actor) RETURN a".to_string()) - .await - .expect("Could not get actors from unmodified graph") - .data - ) - } - - #[tokio::test] - async fn test_async_get_config() { - let client = create_async_test_client().await; - - let config = client - .config_get("QUERY_MEM_CAPACITY") - .await - .expect("Could not get configuration"); - - assert_eq!(config.len(), 1); - assert!(config.contains_key("QUERY_MEM_CAPACITY")); - assert_eq!( - mem::discriminant(config.get("QUERY_MEM_CAPACITY").unwrap()), - mem::discriminant(&ConfigValue::Int64(0)) - ); - } - - #[tokio::test] - async fn test_async_get_config_all() { - let client = create_async_test_client().await; - let configuration = client - .config_get("*") - .await - .expect("Could not get configuration"); - - assert_eq!( - configuration.get("THREAD_COUNT").cloned().unwrap(), - ConfigValue::Int64(thread::available_parallelism().unwrap().get() as i64) - ); - } - - #[tokio::test] - async fn test_async_set_config() { - let client = create_async_test_client().await; - - let config = client - .config_get("EFFECTS_THRESHOLD") - .await - .expect("Could not get configuration"); - - let current_val = config - .get("EFFECTS_THRESHOLD") - .cloned() - .unwrap() - .as_i64() - .unwrap(); - - let desired_val = if current_val == 300 { 250 } else { 300 }; - - client - .config_set("EFFECTS_THRESHOLD", desired_val) - .await - .expect("Could not set config value"); - - let new_config = client - .config_get("EFFECTS_THRESHOLD") - .await - .expect("Could not get configuration"); - - assert_eq!( - new_config - .get("EFFECTS_THRESHOLD") - .cloned() - .unwrap() - .as_i64() - .unwrap(), - desired_val - ); - - client - .config_set("EFFECTS_THRESHOLD", current_val) - .await - .ok(); - } -} diff --git a/src/client/blocking.rs b/src/client/blocking.rs index e9e4774..331b56b 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -7,9 +7,8 @@ use crate::{ client::FalkorClientProvider, connection::blocking::{BorrowedSyncConnection, FalkorSyncConnection}, parser::utils::string_vec_from_val, - ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorValue, GraphSchema, SyncGraph, + ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorResult, FalkorValue, SyncGraph, }; -use anyhow::Result; use parking_lot::Mutex; use std::{ collections::HashMap, @@ -26,9 +25,12 @@ pub(crate) struct FalkorSyncClientInner { } impl FalkorSyncClientInner { - pub(crate) fn borrow_connection(&self) -> Result { + pub(crate) fn borrow_connection(&self) -> Result { Ok(BorrowedSyncConnection::new( - self.connection_pool_rx.lock().recv()?, + self.connection_pool_rx + .lock() + .recv() + .map_err(|_| FalkorDBError::EmptyConnection)?, self.connection_pool_tx.clone(), )) } @@ -53,10 +55,16 @@ impl FalkorSyncClient { connection_info: FalkorConnectionInfo, num_connections: u8, timeout: Option, - ) -> Result { + ) -> FalkorResult { let (connection_pool_tx, connection_pool_rx) = mpsc::sync_channel(num_connections as usize); for _ in 0..num_connections { - connection_pool_tx.send(client.get_connection(timeout)?)?; + connection_pool_tx + .send( + client + .get_connection(timeout) + .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, + ) + .map_err(|_| FalkorDBError::EmptyConnection)?; } Ok(Self { @@ -75,7 +83,7 @@ impl FalkorSyncClient { self.inner.connection_pool_size } - pub(crate) fn borrow_connection(&self) -> Result { + pub(crate) fn borrow_connection(&self) -> FalkorResult { self.inner.borrow_connection() } @@ -83,10 +91,10 @@ impl FalkorSyncClient { /// /// # Returns /// A [`Vec`] of [`String`]s, containing the names of available graphs - pub fn list_graphs(&self) -> Result> { + pub fn list_graphs(&self) -> FalkorResult> { let mut conn = self.borrow_connection()?; conn.send_command::<&str>(None, "GRAPH.LIST", None, None) - .and_then(|res| string_vec_from_val(res).map_err(Into::into)) + .and_then(string_vec_from_val) } /// Return the current value of a configuration option in the database. @@ -100,7 +108,7 @@ impl FalkorSyncClient { pub fn config_get( &self, config_key: T, - ) -> Result> { + ) -> FalkorResult> { let mut conn = self.borrow_connection()?; let config = conn .send_command(None, "GRAPH.CONFIG", Some("GET"), Some(&[config_key]))? @@ -140,7 +148,7 @@ impl FalkorSyncClient { &self, config_key: T, value: C, - ) -> Result { + ) -> FalkorResult { self.borrow_connection()?.send_command( None, "GRAPH.CONFIG", @@ -160,11 +168,7 @@ impl FalkorSyncClient { &self, graph_name: T, ) -> SyncGraph { - SyncGraph { - client: self.inner.clone(), - graph_name: graph_name.to_string(), - graph_schema: GraphSchema::new(graph_name.to_string()), // Required for requesting refreshes - } + SyncGraph::new(self.inner.clone(), graph_name) } /// Copies an entire graph and returns the [`SyncGraph`] for the new copied graph. @@ -179,7 +183,7 @@ impl FalkorSyncClient { &self, graph_to_clone: &str, new_graph_name: &str, - ) -> Result { + ) -> FalkorResult { self.borrow_connection()?.send_command( Some(graph_to_clone), "GRAPH.COPY", @@ -207,7 +211,7 @@ mod tests { .expect("Could not create client for this test"); // Client was created with 6 connections - let _conn_vec: Vec> = (0..6) + let _conn_vec: Vec> = (0..6) .map(|_| { let conn = client.borrow_connection(); assert!(conn.is_ok()); @@ -241,7 +245,8 @@ mod tests { assert_eq!(graph.graph_name(), "imdb".to_string()); let res = graph - .query("MATCH (a:actor) return a".to_string()) + .query("MATCH (a:actor) return a") + .perform() .expect("Could not get actors from unmodified graph"); assert_eq!(res.data.len(), 1317); @@ -265,11 +270,13 @@ mod tests { assert_eq!( graph .inner - .query("MATCH (a:actor) RETURN a".to_string()) + .query("MATCH (a:actor) RETURN a") + .perform() .expect("Could not get actors from unmodified graph") .data, original_graph - .query("MATCH (a:actor) RETURN a".to_string()) + .query("MATCH (a:actor) RETURN a") + .perform() .expect("Could not get actors from unmodified graph") .data ) diff --git a/src/client/builder.rs b/src/client/builder.rs index 66f9e83..9ad20f9 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -3,8 +3,10 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{client::FalkorClientProvider, FalkorConnectionInfo, FalkorDBError, FalkorSyncClient}; -use anyhow::Result; +use crate::{ + client::FalkorClientProvider, FalkorConnectionInfo, FalkorDBError, FalkorResult, + FalkorSyncClient, +}; use std::time::Duration; #[cfg(feature = "tokio")] @@ -72,16 +74,16 @@ impl FalkorClientBuilder { fn get_client>( connection_info: T - ) -> Result - where - anyhow::Error: From, - { - let connection_info = connection_info.try_into()?; + ) -> FalkorResult { + let connection_info = connection_info + .try_into() + .map_err(|_| FalkorDBError::InvalidConnectionInfo)?; Ok(match connection_info { #[cfg(feature = "redis")] - FalkorConnectionInfo::Redis(connection_info) => { - FalkorClientProvider::Redis(redis::Client::open(connection_info.clone())?) - } + FalkorConnectionInfo::Redis(connection_info) => FalkorClientProvider::Redis( + redis::Client::open(connection_info.clone()) + .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, + ), }) } } @@ -104,9 +106,9 @@ impl FalkorClientBuilder<'S'> { /// /// # Returns /// a new [`FalkorSyncClient`] - pub fn build(self) -> Result { + pub fn build(self) -> FalkorResult { if self.num_connections < 1 || self.num_connections > 32 { - return Err(FalkorDBError::InvalidConnectionPoolSize.into()); + return Err(FalkorDBError::InvalidConnectionPoolSize); } let connection_info = self @@ -132,7 +134,7 @@ impl FalkorClientBuilder<'A'> { } } - pub async fn build(self) -> Result { + pub async fn build(self) -> FalkorResult { let connection_info = self .connection_info .unwrap_or("falkor://127.0.0.1:6379".try_into()?); diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs index 1bb9ffd..b5a7e57 100644 --- a/src/connection/asynchronous.rs +++ b/src/connection/asynchronous.rs @@ -3,8 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{FalkorDBError, FalkorValue}; -use anyhow::Result; +use crate::{FalkorDBError, FalkorResult, FalkorValue}; use std::{collections::VecDeque, fmt::Display, sync::Arc}; use tokio::sync::Mutex; @@ -34,7 +33,7 @@ impl BorrowedAsyncConnection { command: &str, subcommand: Option<&str>, params: Option<&[P]>, - ) -> Result { + ) -> FalkorResult { Ok(match self.as_inner()? { #[cfg(feature = "redis")] FalkorAsyncConnection::Redis(redis_conn) => { @@ -47,8 +46,12 @@ impl BorrowedAsyncConnection { } } redis::FromRedisValue::from_owned_redis_value( - redis_conn.send_packed_command(&cmd).await?, - )? + redis_conn + .send_packed_command(&cmd) + .await + .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, + ) + .map_err(|err| FalkorDBError::RedisParsingError(err.to_string()))? } }) } diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index b9f6413..55f987e 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -42,8 +42,8 @@ impl BorrowedSyncConnection { command: &str, subcommand: Option<&str>, params: Option<&[P]>, - ) -> Result { - Ok(match self.as_inner()? { + ) -> Result { + match self.as_inner()? { #[cfg(feature = "redis")] FalkorSyncConnection::Redis(redis_conn) => { use redis::ConnectionLike as _; @@ -55,9 +55,14 @@ impl BorrowedSyncConnection { cmd.arg(param.to_string()); } } - redis::FromRedisValue::from_owned_redis_value(redis_conn.req_command(&cmd)?)? + redis::FromRedisValue::from_owned_redis_value( + redis_conn + .req_command(&cmd) + .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, + ) + .map_err(|err| FalkorDBError::RedisParsingError(err.to_string())) } - }) + } } } diff --git a/src/connection_info/mod.rs b/src/connection_info/mod.rs index eb28b21..eae0c50 100644 --- a/src/connection_info/mod.rs +++ b/src/connection_info/mod.rs @@ -3,21 +3,23 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use anyhow::Result; +use crate::{FalkorDBError, FalkorResult}; /// An agnostic container which allows maintaining of various connection details. /// The different enum variants are enabled based on compilation features #[derive(Clone, Debug)] pub enum FalkorConnectionInfo { + /// A Redis database connection #[cfg(feature = "redis")] Redis(redis::ConnectionInfo), } impl FalkorConnectionInfo { - fn fallback_provider(full_url: String) -> Result { + fn fallback_provider(full_url: String) -> FalkorResult { #[cfg(feature = "redis")] Ok(FalkorConnectionInfo::Redis( - redis::IntoConnectionInfo::into_connection_info(format!("redis://{full_url}"))?, + redis::IntoConnectionInfo::into_connection_info(format!("redis://{full_url}")) + .map_err(|_| FalkorDBError::InvalidConnectionInfo)?, )) } @@ -34,10 +36,12 @@ impl FalkorConnectionInfo { } impl TryFrom<&str> for FalkorConnectionInfo { - type Error = anyhow::Error; + type Error = FalkorDBError; - fn try_from(value: &str) -> Result { - let url = url_parse::core::Parser::new(None).parse(value)?; + fn try_from(value: &str) -> FalkorResult { + let url = url_parse::core::Parser::new(None) + .parse(value) + .map_err(|_| FalkorDBError::InvalidConnectionInfo)?; let scheme = url.scheme.unwrap_or("falkor".to_string()); let addr = url.domain.unwrap_or("127.0.0.1".to_string()); @@ -56,10 +60,11 @@ impl TryFrom<&str> for FalkorConnectionInfo { redis::IntoConnectionInfo::into_connection_info(format!( "{}://{}{}:{}", scheme, user_pass_string, addr, port - ))?, + )) + .map_err(|_| FalkorDBError::InvalidConnectionInfo)?, )); #[cfg(not(feature = "redis"))] - return Err(FalkorDBError::UnavailableProvider.into()); + return Err(FalkorDBError::UnavailableProvider); } _ => FalkorConnectionInfo::fallback_provider(format!( "{}{}:{}", @@ -70,19 +75,19 @@ impl TryFrom<&str> for FalkorConnectionInfo { } impl TryFrom for FalkorConnectionInfo { - type Error = anyhow::Error; + type Error = FalkorDBError; #[inline] - fn try_from(value: String) -> Result { + fn try_from(value: String) -> FalkorResult { Self::try_from(value.as_str()) } } impl TryFrom<(T, u16)> for FalkorConnectionInfo { - type Error = anyhow::Error; + type Error = FalkorDBError; #[inline] - fn try_from(value: (T, u16)) -> Result { + fn try_from(value: (T, u16)) -> FalkorResult { Self::try_from(format!("{}:{}", value.0.to_string(), value.1)) } } diff --git a/src/error/mod.rs b/src/error/mod.rs index 3d70e28..e5e18c8 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -3,68 +3,109 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use crate::SchemaType; + /// A verbose error enum used throughout the client, messages are static string slices. /// this allows easy [`anyhow`] integration using [`thiserror`] #[derive(thiserror::Error, Debug)] pub enum FalkorDBError { + /// A required Id for parsing was not found in the schema + #[error("A required Id for parsing was not found in the schema")] + MissingSchemaId(SchemaType), + /// An error occurred while sending the request to Redis + #[error("An error occurred while sending the request to Redis")] + RedisConnectionError(String), + /// An error occurred while parsing the Redis response" + #[error("An error occurred while parsing the Redis response")] + RedisParsingError(String), + /// The provided connection info is invalid #[error("The provided connection info is invalid")] InvalidConnectionInfo, + /// The connection returned invalid data for this command #[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")] UnavailableProvider, + /// The number of connections for the client has to be between 1 and 32 #[error("The number of connections for the client has to be between 1 and 32")] InvalidConnectionPoolSize, + /// Could not connect to the server with the provided address #[error("Could not connect to the server with the provided address")] NoConnection, + /// Attempting to use an empty connection object #[error("Attempting to use an empty connection object")] EmptyConnection, + /// General parsing error #[error("General parsing error")] ParsingError, + /// Received malformed header #[error("Received malformed header")] ParsingHeader, + /// The id received for this label/property/relationship was unknown #[error("The id received for this label/property/relationship was unknown")] ParsingCompactIdUnknown, + /// Unknown type #[error("Unknown type")] ParsingUnknownType, + /// Element was not of type Bool #[error("Element was not of type Bool")] ParsingBool, + /// Could not parse into config value, was not one of the supported types #[error("Could not parse into config value, was not one of the supported types")] ParsingConfigValue, + /// Element was not of type I64 #[error("Element was not of type I64")] ParsingI64, + /// 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 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")] ParsingFMap, + /// Element was not of type FPoint #[error("Element was not of type FPoint")] ParsingFPoint, + /// Key id was not of type i64 #[error("Key id was not of type i64")] ParsingKeyIdTypeMismatch, + /// Type marker was not of type i64 #[error("Type marker was not of type i64")] ParsingTypeMarkerTypeMismatch, + /// Both key id and type marker were not of type i64 #[error("Both key id and type marker were not of type i64")] ParsingKTVTypes, + /// Field missing or mismatched while parsing index #[error("Field missing or mismatched while parsing index")] ParsingIndex, + /// 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")] ParsingArrayToStructElementCount, + /// Invalid constraint type, expected 'UNIQUE' or 'MANDATORY' #[error("Invalid constraint type, expected 'UNIQUE' or 'MANDATORY'")] ConstraintType, + /// Invalid constraint status, expected 'OPERATIONAL', 'UNDER CONSTRUCTION' or 'FAILED' #[error("Invalid constraint status, expected 'OPERATIONAL', 'UNDER CONSTRUCTION' or 'FAILED'")] ConstraintStatus, + /// Invalid Index status, expected 'OPERATIONAL' or 'UNDER CONSTRUCTION' #[error("Invalid Index status, expected 'OPERATIONAL' or 'UNDER CONSTRUCTION'")] IndexStatus, + /// Invalid Index field type, expected 'RANGE', 'VECTOR' or 'FULLTEXT' #[error("Invalid Index field type, expected 'RANGE', 'VECTOR' or 'FULLTEXT'")] IndexType, } diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs index bdbb1bf..000e943 100644 --- a/src/graph/asynchronous.rs +++ b/src/graph/asynchronous.rs @@ -6,10 +6,9 @@ use crate::{ client::asynchronous::FalkorAsyncClientInner, graph::utils::{construct_query, generate_procedure_call}, - parser::utils::{parse_header, parse_result_set_async}, + parser::utils::{parse_header, parse_result_set}, Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorDBError, FalkorIndex, - FalkorParsableAsync, FalkorResponse, FalkorValue, GraphSchema, IndexType, ResultSet, - SlowlogEntry, + FalkorParsable, FalkorResponse, FalkorValue, GraphSchema, IndexType, ResultSet, SlowlogEntry, }; use anyhow::Result; use std::{collections::HashMap, fmt::Display, sync::Arc}; @@ -188,7 +187,7 @@ impl AsyncGraph { let header_keys = parse_header(header)?; let conn = Arc::new(Mutex::new(conn)); FalkorResponse::from_response_with_headers( - parse_result_set_async(data, &mut self.graph_schema, conn, &header_keys).await?, + parse_result_set(data, &mut self.graph_schema, conn)?, header_keys, stats, ) @@ -238,8 +237,7 @@ impl AsyncGraph { let header_keys = parse_header(header)?; let conn = Arc::new(Mutex::new(conn)); FalkorResponse::from_response_with_headers( - parse_result_set_async(data, &mut self.graph_schema, conn, &header_keys) - .await?, + parse_result_set(data, &mut self.graph_schema, conn, &header_keys)?, header_keys, stats, ) @@ -412,7 +410,7 @@ impl AsyncGraph { /// /// # Returns /// A caller-provided type which implements [`FalkorParsableAsync`] - pub async fn call_procedure( + pub async fn call_procedure( &mut self, procedure: C, args: Option<&[&str]>, @@ -437,7 +435,7 @@ impl AsyncGraph { .await?; let conn = Arc::new(Mutex::new(conn)); - P::from_falkor_value_async(res, &mut self.graph_schema, conn).await + P::from_falkor_value(res, &mut self.graph_schema, conn) } /// Run a query which calls a procedure on the graph, read-only, or otherwise. @@ -454,7 +452,7 @@ impl AsyncGraph { /// /// # Returns /// A caller-provided type which implements [`FalkorParsableAsync`] - pub async fn call_procedure_with_timeout( + pub async fn call_procedure_with_timeout( &mut self, procedure: C, args: Option<&[&str]>, @@ -484,7 +482,7 @@ impl AsyncGraph { .await?; let conn = Arc::new(Mutex::new(conn)); - P::from_falkor_value_async(res, &mut self.graph_schema, conn).await + P::from_falkor_value(res, &mut self.graph_schema, conn) } /// Calls the DB.INDICES procedure on the graph, returning all the indexing methods currently used @@ -507,7 +505,7 @@ impl AsyncGraph { .into_iter() .flat_map(|index| { let conn = Arc::clone(&conn); - FalkorIndex::from_falkor_value_async(index, &mut self.graph_schema, conn).await + FalkorIndex::from_falkor_value(index, &mut self.graph_schema, conn) }) .collect(), stats, @@ -615,9 +613,7 @@ impl AsyncGraph { query_res .into_vec()? .into_iter() - .flat_map(|item| { - Constraint::from_falkor_value_async(item, &mut self.graph_schema, conn).await - }) + .flat_map(|item| Constraint::from_falkor_value(item, &mut self.graph_schema, conn)) .collect(), stats, ) diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 8391eff..4d96fc4 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -4,14 +4,12 @@ */ use crate::{ - client::blocking::FalkorSyncClientInner, - graph::utils::{construct_query, generate_procedure_call}, - parser::utils::{parse_header, parse_result_set}, - Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorDBError, FalkorIndex, - FalkorParsable, FalkorResponse, FalkorValue, GraphSchema, IndexType, ResultSet, SlowlogEntry, + client::blocking::FalkorSyncClientInner, Constraint, ConstraintType, EntityType, ExecutionPlan, + FalkorIndex, FalkorResponse, FalkorResult, FalkorValue, GraphSchema, IndexType, + ProcedureBuilder, QueryBuilder, ResultSet, SlowlogEntry, }; -use anyhow::Result; -use std::{collections::HashMap, fmt::Display, sync::Arc}; +use std::fmt::Display; +use std::{collections::HashMap, sync::Arc}; /// The main graph API, this allows the user to perform graph operations while exposing as little details as possible. /// # Thread Safety @@ -20,12 +18,22 @@ use std::{collections::HashMap, fmt::Display, sync::Arc}; #[derive(Clone)] pub struct SyncGraph { pub(crate) client: Arc, - pub(crate) graph_name: String, - /// Provides user with access to the current graph schema, - pub graph_schema: GraphSchema, + graph_name: String, + pub(crate) graph_schema: GraphSchema, } impl SyncGraph { + pub(crate) fn new( + client: Arc, + graph_name: T, + ) -> Self { + Self { + client, + graph_name: graph_name.to_string(), + graph_schema: GraphSchema::new(graph_name), // Required for requesting refreshes + } + } + /// Returns the name of the graph for which this API performs operations. /// /// # Returns @@ -38,15 +46,15 @@ impl SyncGraph { &self, command: &str, subcommand: Option<&str>, - params: Option<&[String]>, - ) -> Result { + params: Option<&[&str]>, + ) -> FalkorResult { let mut conn = self.client.borrow_connection()?; conn.send_command(Some(self.graph_name.as_str()), command, subcommand, params) } /// Deletes the graph stored in the database, and drop all the schema caches. /// NOTE: This still maintains the graph API, operations are still viable. - pub fn delete(&mut self) -> Result<()> { + pub fn delete(&mut self) -> FalkorResult<()> { self.send_command("GRAPH.DELETE", None, None)?; self.graph_schema.clear(); Ok(()) @@ -56,439 +64,137 @@ impl SyncGraph { /// /// # Returns /// A [`Vec`] of [`SlowlogEntry`], providing information about each query. - pub fn slowlog(&self) -> Result> { + pub fn slowlog(&self) -> FalkorResult> { let res = self.send_command("GRAPH.SLOWLOG", None, None)?.into_vec()?; - if res.is_empty() { - return Ok(vec![]); - } - Ok(res.into_iter().flat_map(SlowlogEntry::try_from).collect()) } /// Resets the slowlog, all query time data will be cleared. - pub fn slowlog_reset(&self) -> Result { - self.send_command("GRAPH.SLOWLOG", None, Some(&["RESET".to_string()])) + pub fn slowlog_reset(&self) -> FalkorResult { + self.send_command("GRAPH.SLOWLOG", None, Some(&["RESET"])) } - /// Returns an [`ExecutionPlan`] object for the selected query, - /// showing how long each step took to perform. - /// This function variant allows adding extra parameters after the query + /// Creates a [`QueryBuilder`] for this graph, in an attempt to profile a specific query + /// This [`QueryBuilder`] has to be dropped or ran using [`QueryBuilder::perform`], before reusing the graph, as it takes a mutable reference to the graph for as long as it exists /// /// # Arguments /// * `query_string`: The query to profile - /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. /// /// # Returns - /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. - pub fn profile_with_params( - &self, - query_string: Q, - params: Option<&HashMap>, - ) -> Result { - let query = construct_query(query_string, params); - - ExecutionPlan::try_from(self.send_command("GRAPH.PROFILE", None, Some(&[query]))?) - .map_err(Into::into) + /// A [`QueryBuilder`] object, which when performed will return an [`ExecutionPlan`] + pub fn profile<'a>( + &'a mut self, + query_string: &'a str, + ) -> QueryBuilder { + QueryBuilder::<'a>::new(self, "GRAPH.PROFILE", query_string) } - /// Returns an [`ExecutionPlan`] object for the selected query, - /// showing how long each step took to perform. + /// Creates a [`QueryBuilder`] for this graph, in an attempt to explain a specific query + /// This [`QueryBuilder`] has to be dropped or ran using [`QueryBuilder::perform`], before reusing the graph, as it takes a mutable reference to the graph for as long as it exists /// /// # Arguments - /// * `query_string`: The query to profile + /// * `query_string`: The query to explain the process for /// /// # Returns - /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. - pub fn profile( - &self, - query_string: Q, - ) -> Result { - self.profile_with_params::(query_string, None) + /// A [`QueryBuilder`] object, which when performed will return an [`ExecutionPlan`] + pub fn explain<'a>( + &'a mut self, + query_string: &'a str, + ) -> QueryBuilder { + QueryBuilder::new(self, "GRAPH.EXPLAIN", query_string) } - /// Returns an [`ExecutionPlan`] object for the selected query, - /// showing the internals steps the database will go through to perform the query. - /// This function variant allows adding extra parameters after the query - /// - /// # Arguments - /// * `query_string`: The query to explain - /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. - /// - /// # Returns - /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. - pub fn explain_with_params( - &self, - query_string: Q, - params: Option<&HashMap>, - ) -> Result { - let query = construct_query(query_string, params); - ExecutionPlan::try_from(self.send_command("GRAPH.EXPLAIN", None, Some(&[query]))?) - .map_err(Into::into) - } - - /// Returns an [`ExecutionPlan`] object for the selected query, - /// showing the internals steps the database will go through to perform the query. - /// - /// # Arguments - /// * `query_string`: The query to explain - /// - /// # Returns - /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. - pub fn explain( - &self, - query_string: Q, - ) -> Result { - self.explain_with_params::(query_string, None) - } - - fn query_inner_with_timeout( - &mut self, - command: &str, - query_string: Q, - params: Option<&HashMap>, - timeout: i64, - ) -> Result> { - let query = construct_query(query_string, params); - let mut conn = self.client.borrow_connection()?; - - let [header, data, stats]: [FalkorValue; 3] = conn - .send_command( - Some(self.graph_name.as_str()), - command, - None, - Some(&[ - query.as_str(), - "--compact", - format!("timeout {timeout}").as_str(), - ]), - )? - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let header_keys = parse_header(header)?; - FalkorResponse::from_response_with_headers( - parse_result_set(data, &mut self.graph_schema, &mut conn, &header_keys)?, - header_keys, - stats, - ) - .map_err(Into::into) - } - - fn query_inner( - &mut self, - command: &str, - query_string: Q, - params: Option<&HashMap>, - ) -> Result> { - let query = construct_query(query_string, params); - let mut conn = self.client.borrow_connection()?; - - let res = conn - .send_command( - Some(self.graph_name.as_str()), - command, - None, - Some(&[query, "--compact".to_string()]), - )? - .into_vec()?; - - match res.len() { - 1 => { - let stats = res - .into_iter() - .next() - .ok_or(FalkorDBError::ParsingArrayToStructElementCount)?; - - FalkorResponse::from_response(None, vec![], stats) - } - 2 => { - let [header, stats]: [FalkorValue; 2] = res - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - FalkorResponse::from_response(Some(header), vec![], stats) - } - 3 => { - let [header, data, stats]: [FalkorValue; 3] = res - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let header_keys = parse_header(header)?; - FalkorResponse::from_response_with_headers( - parse_result_set(data, &mut self.graph_schema, &mut conn, &header_keys)?, - header_keys, - stats, - ) - } - _ => Err(FalkorDBError::ParsingArrayToStructElementCount), - } - .map_err(Into::into) - } - - /// Run a query on the graph - /// - /// # Arguments - /// * `query_string`: The query to run - /// - /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub fn query( - &mut self, - query_string: Q, - ) -> Result> { - self.query_inner::("GRAPH.QUERY", query_string, None) - } - - /// Run a query on the graph, but abort it if it exceeds the timeout - /// - /// # Arguments - /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. - /// - /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub fn query_with_timeout( - &mut self, - query_string: Q, - timeout: i64, - ) -> Result> { - self.query_inner_with_timeout::("GRAPH.QUERY", query_string, None, timeout) - } - - /// Run a query on the graph - /// This function variant allows adding extra parameters after the query - /// - /// # Arguments - /// * `query_string`: The query to run - /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. - /// - /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub fn query_with_params( - &mut self, - query_string: Q, - params: &HashMap, - ) -> Result> { - self.query_inner("GRAPH.QUERY", query_string, Some(params)) - } - - /// Run a query on the graph but abort it if it exceeds the timeout - /// This function variant allows adding extra parameters after the query + /// Creates a [`QueryBuilder`] for this graph + /// This [`QueryBuilder`] has to be dropped or ran using [`QueryBuilder::perform`], before reusing the graph, as it takes a mutable reference to the graph for as long as it exists /// /// # Arguments /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. - /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. /// /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub fn query_with_params_and_timeout( - &mut self, - query_string: Q, - timeout: i64, - params: &HashMap, - ) -> Result> { - self.query_inner_with_timeout("GRAPH.QUERY", query_string, Some(params), timeout) + /// A [`QueryBuilder`] object, which when performed will return a [`FalkorResponse`] + pub fn query<'a>( + &'a mut self, + query_string: &'a str, + ) -> QueryBuilder> { + QueryBuilder::new(self, "GRAPH.QUERY", query_string) } - /// Run a query on the graph + /// Creates a [`QueryBuilder`] for this graph, for a readonly query + /// This [`QueryBuilder`] has to be dropped or ran using [`QueryBuilder::perform`], before reusing the graph, as it takes a mutable reference to the graph for as long as it exists /// Read-only queries are more limited with the operations they are allowed to perform. /// /// # Arguments /// * `query_string`: The query to run /// /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub fn query_readonly( - &mut self, - query_string: Q, - ) -> Result> { - self.query_inner::("GRAPH.QUERY_RO", query_string, None) + /// A [`QueryBuilder`] object + pub fn ro_query<'a>( + &'a mut self, + query_string: &'a str, + ) -> QueryBuilder> { + QueryBuilder::new(self, "GRAPH.QUERY_RO", query_string) } - /// Run a query on the graph, but abort it if it exceeds the timeout + /// Creates a [`ProcedureBuilder`] for this graph + /// This [`ProcedureBuilder`] has to be dropped or ran using [`ProcedureBuilder::perform`], before reusing the graph, as it takes a mutable reference to the graph for as long as it exists /// Read-only queries are more limited with the operations they are allowed to perform. /// /// # Arguments - /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. - /// - /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub fn query_readonly_with_timeout( - &mut self, - query_string: Q, - timeout: i64, - ) -> Result> { - self.query_inner_with_timeout::( - "GRAPH.QUERY_RO", - query_string, - None, - timeout, - ) - } - - /// Run a read-only query on the graph - /// Read-only queries are more limited with the operations they are allowed to perform. - /// This function variant allows adding extra parameters after the query - /// - /// # Arguments - /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. - /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// * `procedure_name`: The name of the procedure to call /// /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub fn query_readonly_with_params( - &mut self, - query_string: Q, - params: &HashMap, - ) -> Result> { - self.query_inner("GRAPH.QUERY_RO", query_string, Some(params)) + /// A [`ProcedureBuilder`] object + pub fn call_procedure<'a, P>( + &'a mut self, + procedure_name: &'a str, + ) -> ProcedureBuilder

{ + ProcedureBuilder::new(self, procedure_name) } - /// Run a read-only query on the graph, but abort it if it exceeds the timeout - /// Read-only queries are more limited with the operations they are allowed to perform. - /// This function variant allows adding extra parameters after the query + /// Creates a [`ProcedureBuilder`] for this graph, for a readonly procedure + /// This [`ProcedureBuilder`] has to be dropped or ran using [`ProcedureBuilder::perform`], before reusing the graph, as it takes a mutable reference to the graph for as long as it exists + /// Read-only procedures are more limited with the operations they are allowed to perform. /// /// # Arguments - /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. - /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. + /// * `procedure_name`: The name of the procedure to call /// /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub fn query_readonly_with_params_and_timeout( - &mut self, - query_string: Q, - params: &HashMap, - timeout: i64, - ) -> Result> { - self.query_inner_with_timeout("GRAPH.QUERY_RO", query_string, Some(params), timeout) + /// A [`ProcedureBuilder`] object + pub fn call_proecdure_ro<'a, P>( + &'a mut self, + procedure_name: &'a str, + ) -> ProcedureBuilder

{ + ProcedureBuilder::new_readonly(self, procedure_name) } - /// Run a query which calls a procedure on the graph, read-only, or otherwise. - /// Read-only queries are more limited with the operations they are allowed to perform. - /// This function allows adding extra parameters after the query, and adding a YIELD block afterward - /// - /// # Arguments - /// * `procedure`: The procedure to call - /// * `args`: An optional slice of strings containing the parameters. - /// * `yields`: The optional yield block arguments. - /// * `read_only`: Whether this procedure is read-only. - /// * `timeout`: If provided, the query will abort if overruns the timeout. + /// Calls the DB.INDICES procedure on the graph, returning all the indexing methods currently used /// /// # Returns - /// A caller-provided type which implements [`FalkorParsable`] - pub fn call_procedure( - &mut self, - procedure: C, - args: Option<&[&str]>, - yields: Option<&[&str]>, - read_only: bool, - ) -> Result

{ - let (query_string, params) = generate_procedure_call(procedure, args, yields); - let query = construct_query(query_string, params.as_ref()); - let mut conn = self.client.borrow_connection()?; - - P::from_falkor_value( - conn.send_command( - Some(self.graph_name.as_str()), - if read_only { - "GRAPH.QUERY_RO" - } else { - "GRAPH.QUERY" - }, - None, - Some(&[query, "--compact".to_string()]), - )?, - &mut self.graph_schema, - &mut conn, - ) + /// A [`Vec`] of [`FalkorIndex`] + pub fn list_indices(&mut self) -> FalkorResult>> { + ProcedureBuilder::>>::new(self, "DB.INDEXES").perform() } - /// Run a query which calls a procedure on the graph, read-only, or otherwise. - /// Read-only queries are more limited with the operations they are allowed to perform. - /// This function allows adding extra parameters after the query, and adding a YIELD block afterward - /// This function will cause the query to abort if it exceeds a certain timeout + /// Creates a new index in the graph, for the selected entity type(Node/Edge), selected label, and properties /// /// # Arguments - /// * `procedure`: The procedure to call - /// * `args`: An optional slice of strings containing the parameters. - /// * `yields`: The optional yield block arguments. - /// * `read_only`: Whether this procedure is read-only. - /// * `timeout`: If provided, the query will abort if overruns the timeout. - /// - /// # Returns - /// A caller-provided type which implements [`FalkorParsable`] - pub fn call_procedure_with_timeout( - &mut self, - procedure: C, - args: Option<&[&str]>, - yields: Option<&[&str]>, - read_only: bool, - timeout: i64, - ) -> Result

{ - let (query_string, params) = generate_procedure_call(procedure, args, yields); - let query = construct_query(query_string, params.as_ref()); - let mut conn = self.client.borrow_connection()?; - - P::from_falkor_value( - conn.send_command( - Some(self.graph_name.as_str()), - if read_only { - "GRAPH.QUERY_RO" - } else { - "GRAPH.QUERY" - }, - None, - Some(&[ - query.as_str(), - "--compact", - format!("timeout {timeout}").as_str(), - ]), - )?, - &mut self.graph_schema, - &mut conn, - ) - } - - /// Calls the DB.INDICES procedure on the graph, returning all the indexing methods currently used + /// * `index_field_type`: + /// * `entity_type`: + /// * `label`: + /// * `properties`: + /// * `options`: /// /// # Returns - /// A [`Vec`] of [`FalkorIndex`] - pub fn list_indices(&mut self) -> Result>> { - let mut conn = self.client.borrow_connection()?; - let [header, indices, stats]: [FalkorValue; 3] = self - .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false)? - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - FalkorResponse::from_response( - Some(header), - indices - .into_vec()? - .into_iter() - .flat_map(|index| { - FalkorIndex::from_falkor_value(index, &mut self.graph_schema, &mut conn) - }) - .collect(), - stats, - ) - .map_err(Into::into) - } - - pub fn create_index( + /// A [`ResultSet`] containing information on the created index + pub fn create_index( &mut self, index_field_type: IndexType, entity_type: EntityType, - label: L, + label: &str, properties: &[P], options: Option<&HashMap>, - ) -> Result> { + ) -> FalkorResult> { // Create index from these properties let properties_string = properties .iter() @@ -497,16 +203,15 @@ impl SyncGraph { .join(", "); let pattern = match entity_type { - EntityType::Node => format!("(l:{})", label.to_string()), - EntityType::Edge => format!("()-[l:{}]->()", label.to_string()), + EntityType::Node => format!("(l:{})", label), + EntityType::Edge => format!("()-[l:{}]->()", label), }; let idx_type = match index_field_type { IndexType::Range => "", IndexType::Vector => "VECTOR ", IndexType::Fulltext => "FULLTEXT ", - } - .to_string(); + }; let options_string = options .map(|hashmap| { @@ -519,10 +224,14 @@ impl SyncGraph { .map(|options_string| format!(" OPTIONS {{ {} }}", options_string)) .unwrap_or_default(); - self.query(format!( - "CREATE {idx_type}INDEX FOR {pattern} ON ({}){}", - properties_string, options_string - )) + self.query( + format!( + "CREATE {idx_type}INDEX FOR {pattern} ON ({}){}", + properties_string, options_string + ) + .as_str(), + ) + .perform() } /// Drop an existing index, by specifying its type, entity, label and specific properties @@ -535,7 +244,7 @@ impl SyncGraph { entity_type: EntityType, label: L, properties: &[P], - ) -> Result> { + ) -> FalkorResult> { let properties_string = properties .iter() .map(|element| format!("e.{}", element.to_string())) @@ -554,36 +263,22 @@ impl SyncGraph { } .to_string(); - self.query(format!( - "DROP {idx_type} INDEX for {pattern} ON ({})", - properties_string - )) + self.query( + format!( + "DROP {idx_type} INDEX for {pattern} ON ({})", + properties_string + ) + .as_str(), + ) + .perform() } /// Calls the DB.CONSTRAINTS procedure on the graph, returning an array of the graph's constraints /// /// # 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 - pub fn list_constraints(&mut self) -> Result>> { - let mut conn = self.client.borrow_connection()?; - let [header, query_res, stats]: [FalkorValue; 3] = self - .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, false)? - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - FalkorResponse::from_response( - Some(header), - query_res - .into_vec()? - .into_iter() - .flat_map(|item| { - Constraint::from_falkor_value(item, &mut self.graph_schema, &mut conn) - }) - .collect(), - stats, - ) - .map_err(Into::into) + pub fn list_constraints(&mut self) -> FalkorResult>> { + ProcedureBuilder::>>::new(self, "DB.CONSTRAINTS").perform() } /// Creates a new constraint for this graph, making the provided properties mandatory @@ -597,16 +292,19 @@ impl SyncGraph { entity_type: EntityType, label: &str, properties: &[&str], - ) -> Result { + ) -> FalkorResult { + let entity_type = entity_type.to_string(); + let properties_count = properties.len().to_string(); + let mut params = Vec::with_capacity(5 + properties.len()); params.extend([ - "MANDATORY".to_string(), - entity_type.to_string(), - label.to_string(), - "PROPERTIES".to_string(), - properties.len().to_string(), + "MANDATORY", + entity_type.as_str(), + label, + "PROPERTIES", + properties_count.as_str(), ]); - params.extend(properties.iter().map(|property| property.to_string())); + params.extend(properties); self.send_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) } @@ -617,12 +315,12 @@ 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. - pub fn create_unique_constraint( + pub fn create_unique_constraint( &mut self, entity_type: EntityType, label: String, - properties: &[P], - ) -> Result { + properties: &[&str], + ) -> FalkorResult { self.create_index( IndexType::Range, entity_type, @@ -631,15 +329,17 @@ impl SyncGraph { None, )?; - let mut params: Vec = Vec::with_capacity(5 + properties.len()); + let entity_type = entity_type.to_string(); + let properties_count = properties.len().to_string(); + let mut params: Vec<&str> = Vec::with_capacity(5 + properties.len()); params.extend([ - "UNIQUE".to_string(), - entity_type.to_string(), - label.to_string(), - "PROPERTIES".to_string(), - properties.len().to_string(), + "UNIQUE", + entity_type.as_str(), + label.as_str(), + "PROPERTIES", + properties_count.as_str(), ]); - params.extend(properties.iter().map(|property| property.to_string())); + params.extend(properties); // create constraint using index self.send_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) @@ -652,22 +352,26 @@ 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. - pub fn drop_constraint( + pub fn drop_constraint( &self, constraint_type: ConstraintType, entity_type: EntityType, - label: L, - properties: &[P], - ) -> Result { + label: &str, + properties: &[&str], + ) -> FalkorResult { + let constraint_type = constraint_type.to_string(); + let entity_type = entity_type.to_string(); + let properties_count = properties.len().to_string(); + let mut params = Vec::with_capacity(5 + properties.len()); params.extend([ - constraint_type.to_string(), - entity_type.to_string(), - label.to_string(), - "PROPERTIES".to_string(), - properties.len().to_string(), + constraint_type.as_str(), + entity_type.as_str(), + label, + "PROPERTIES", + properties_count.as_str(), ]); - params.extend(properties.iter().map(|property| property.to_string())); + params.extend(properties); self.send_command("GRAPH.CONSTRAINT", Some("DROP"), Some(params.as_slice())) } @@ -686,7 +390,7 @@ mod tests { .create_index( IndexType::Fulltext, EntityType::Node, - "actor".to_string(), + "actor", &["Hello"], None, ) @@ -800,10 +504,12 @@ mod tests { graph .inner .query("UNWIND range(0, 500) AS x RETURN x") + .perform() .expect("Could not generate the fast query"); graph .inner .query("UNWIND range(0, 100000) AS x RETURN x") + .perform() .expect("Could not generate the slow query"); let slowlog = graph @@ -834,9 +540,9 @@ mod tests { #[test] fn test_explain() { - let graph = open_test_graph("test_explain"); + let mut graph = open_test_graph("test_explain"); - let execution_plan = graph.inner.explain("MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100").expect("Could not create execution plan"); + let execution_plan = graph.inner.explain("MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100").perform().expect("Could not create execution plan"); assert_eq!(execution_plan.steps().len(), 7); assert_eq!( execution_plan.text(), @@ -846,11 +552,12 @@ mod tests { #[test] fn test_profile() { - let graph = open_test_graph("test_profile"); + let mut graph = open_test_graph("test_profile"); let execution_plan = graph .inner .profile("UNWIND range(0, 1000) AS x RETURN x") + .perform() .expect("Could not generate the query"); let steps = execution_plan.steps().to_vec(); diff --git a/src/graph/mod.rs b/src/graph/mod.rs index eec005f..39712e2 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -4,7 +4,8 @@ */ pub(crate) mod blocking; -pub(crate) mod utils; #[cfg(feature = "tokio")] pub(crate) mod asynchronous; + +pub(crate) mod query_builder; diff --git a/src/graph/query_builder.rs b/src/graph/query_builder.rs new file mode 100644 index 0000000..afaae27 --- /dev/null +++ b/src/graph/query_builder.rs @@ -0,0 +1,364 @@ +/* + * Copyright FalkorDB Ltd. 2023 - present + * Licensed under the Server Side Public License v1 (SSPLv1). + */ + +use crate::connection::blocking::BorrowedSyncConnection; +use crate::{ + parser::utils::{parse_header, parse_result_set}, + Constraint, ExecutionPlan, FalkorDBError, FalkorIndex, FalkorParsable, FalkorResponse, + FalkorResult, FalkorValue, ResultSet, SyncGraph, +}; +use std::{collections::HashMap, fmt::Display, marker::PhantomData, ops::Not}; + +pub(crate) fn construct_query( + query_str: Q, + params: Option<&HashMap>, +) -> String { + format!( + "{}{}", + params + .and_then(|params| params.is_empty().not().then(|| params + .iter() + .fold("CYPHER ".to_string(), |acc, (key, val)| { + format!("{}{}={} ", acc, key.to_string(), val.to_string()) + }))) + .unwrap_or_default(), + query_str.to_string() + ) +} + +/// A Builder-pattern struct that allows creating and performing queries on a graph +pub struct QueryBuilder<'a, Output> { + _unused: PhantomData, + graph: &'a mut SyncGraph, + command: &'a str, + query_string: &'a str, + params: Option<&'a HashMap>, + timeout: Option, +} + +impl<'a, Output> QueryBuilder<'a, Output> { + pub(crate) fn new( + graph: &'a mut SyncGraph, + command: &'a str, + query_string: &'a str, + ) -> Self { + Self { + _unused: PhantomData, + graph, + command, + query_string, + params: None, + timeout: None, + } + } + + /// Pass the following params to the query (as "CYPHER {param_key}={param_val}" + /// + /// # Arguments + /// * `params`: A [`HashMap`] of params in key-val format + pub fn with_params( + self, + params: &'a HashMap, + ) -> Self { + Self { + params: Some(params), + ..self + } + } + + /// Specify a timeout after which to abort the query + /// + /// # Arguments + /// * `timeout`: the timeout after which to abort, in ms + pub fn with_timeout( + self, + timeout: i64, + ) -> Self { + Self { + timeout: Some(timeout), + ..self + } + } + + fn perform_common( + &mut self, + conn: &mut BorrowedSyncConnection, + ) -> FalkorResult { + let query = construct_query(self.query_string, self.params); + + let timeout = self.timeout.map(|timeout| format!("timeout {timeout}")); + let mut params = vec![query.as_str(), "--compact"]; + params.extend(timeout.as_deref()); + + conn.send_command( + Some(self.graph.graph_name()), + self.command, + None, + Some(params.as_slice()), + ) + } +} + +impl<'a> QueryBuilder<'a, FalkorResponse> { + /// Perform the query, retuning a [`FalkorResponse`], with a [`ResultSet`] as its `data` member + pub fn perform(mut self) -> FalkorResult> { + let mut conn = self.graph.client.borrow_connection()?; + let res = self.perform_common(&mut conn)?.into_vec()?; + + match res.len() { + 1 => { + let stats = res + .into_iter() + .next() + .ok_or(FalkorDBError::ParsingArrayToStructElementCount)?; + + FalkorResponse::from_response(None, vec![], stats) + } + 2 => { + let [header, stats]: [FalkorValue; 2] = res + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + FalkorResponse::from_response(Some(header), vec![], stats) + } + 3 => { + let [header, data, stats]: [FalkorValue; 3] = res + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + FalkorResponse::from_response_with_headers( + parse_result_set(data, &mut self.graph.graph_schema, &mut conn)?, + parse_header(header)?, + stats, + ) + } + _ => Err(FalkorDBError::ParsingArrayToStructElementCount)?, + } + } +} + +impl<'a> QueryBuilder<'a, ExecutionPlan> { + /// Perform the query, returning an [`ExecutionPlan`] from the data returned + pub fn perform(mut self) -> FalkorResult { + let mut conn = self.graph.client.borrow_connection()?; + let res = self.perform_common(&mut conn)?; + + ExecutionPlan::try_from(res) + } +} + +pub(crate) fn generate_procedure_call( + procedure: P, + args: Option<&[T]>, + yields: Option<&[Z]>, +) -> (String, Option>) { + let params = args.map(|args| { + args.iter() + .enumerate() + .fold(HashMap::new(), |mut acc, (idx, param)| { + acc.insert(format!("param{idx}"), param.to_string()); + acc + }) + }); + + let mut query_string = format!( + "CALL {}({})", + procedure.to_string(), + args.unwrap_or_default() + .iter() + .map(|element| format!("${}", element)) + .collect::>() + .join(",") + ); + + let yields = yields.unwrap_or_default(); + if !yields.is_empty() { + query_string += format!( + " YIELD {}", + yields + .iter() + .map(|element| element.to_string()) + .collect::>() + .join(",") + ) + .as_str(); + } + + (query_string, params) +} + +/// A Builder-pattern struct that allows creating and performing procedure call on a graph +pub struct ProcedureBuilder<'a, Output> { + _unused: PhantomData, + graph: &'a mut SyncGraph, + readonly: bool, + procedure_name: &'a str, + args: Option<&'a [&'a str]>, + yields: Option<&'a [&'a str]>, +} + +impl<'a, Output> ProcedureBuilder<'a, Output> { + pub(crate) fn new( + graph: &'a mut SyncGraph, + procedure_name: &'a str, + ) -> Self { + Self { + _unused: PhantomData, + graph, + readonly: false, + procedure_name, + args: None, + yields: None, + } + } + + pub(crate) fn new_readonly( + graph: &'a mut SyncGraph, + procedure_name: &'a str, + ) -> Self { + Self { + _unused: PhantomData, + graph, + readonly: true, + procedure_name, + args: None, + yields: None, + } + } + + /// Pass arguments to the procedure call + /// + /// # Arguments + /// * `args`: The arguments to pass + pub fn with_args( + self, + args: &'a [&str], + ) -> Self { + Self { + args: Some(args), + ..self + } + } + + /// Tell the procedure call it should yield the following results + /// + /// # Arguments + /// * `yields`: The values to yield + pub fn with_yields( + self, + yields: &'a [&str], + ) -> Self { + Self { + yields: Some(yields), + ..self + } + } + + fn perform_common( + &mut self, + conn: &mut BorrowedSyncConnection, + ) -> FalkorResult { + let command = match self.readonly { + true => "GRAPH.QUERY_RO", + false => "GRAPH.QUERY", + }; + + let (query_string, params) = + generate_procedure_call(self.procedure_name, self.args, self.yields); + let query = construct_query(query_string, params.as_ref()); + + conn.send_command( + Some(self.graph.graph_name()), + command, + None, + Some(&[query.as_str(), "--compact"]), + ) + } +} + +impl<'a> ProcedureBuilder<'a, FalkorResponse>> { + /// Performs the procedure call and return a [`FalkorResponse`] type containing a result set of [`FalkorIndex`]s + /// This functions consumes self + pub fn perform(mut self) -> FalkorResult>> { + let mut conn = self.graph.client.borrow_connection()?; + + let [header, indices, stats]: [FalkorValue; 3] = self + .perform_common(&mut conn)? + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + FalkorResponse::from_response( + Some(header), + indices + .into_vec()? + .into_iter() + .flat_map(|index| { + FalkorIndex::from_falkor_value(index, &mut self.graph.graph_schema, &mut conn) + }) + .collect(), + stats, + ) + } +} + +impl<'a> ProcedureBuilder<'a, FalkorResponse>> { + /// Performs the procedure call and return a [`FalkorResponse`] type containing a result set of [`Constraint`]s + /// This functions consumes self + pub fn perform(mut self) -> FalkorResult>> { + let mut conn = self.graph.client.borrow_connection()?; + + let [header, query_res, stats]: [FalkorValue; 3] = self + .perform_common(&mut conn)? + .into_vec()? + .try_into() + .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + + FalkorResponse::from_response( + Some(header), + query_res + .into_vec()? + .into_iter() + .flat_map(|item| { + Constraint::from_falkor_value(item, &mut self.graph.graph_schema, &mut conn) + }) + .collect(), + stats, + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_generate_procedure_call() { + let (query, params) = generate_procedure_call( + "DB.CONSTRAINTS", + Some(&["Hello", "World"]), + Some(&["Foo", "Bar"]), + ); + + assert_eq!(query, "CALL DB.CONSTRAINTS($Hello,$World) YIELD Foo,Bar"); + assert!(params.is_some()); + + let params = params.unwrap(); + assert_eq!(params["param0"], "Hello"); + assert_eq!(params["param1"], "World"); + } + + #[test] + fn test_construct_query() { + let query = construct_query("MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100", + Some(&HashMap::from([("Foo", "Bar"), ("Bizz", "Bazz")]))); + assert!(query.starts_with("CYPHER ")); + assert!(query.ends_with(" MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100")); + + // Order not guaranteed + assert!(query.contains(" Foo=Bar ")); + assert!(query.contains(" Bizz=Bazz ")); + } +} diff --git a/src/graph/utils.rs b/src/graph/utils.rs deleted file mode 100644 index 5b57657..0000000 --- a/src/graph/utils.rs +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use std::{collections::HashMap, fmt::Display, ops::Not}; - -pub(crate) fn generate_procedure_call( - procedure: P, - args: Option<&[T]>, - yields: Option<&[Z]>, -) -> (String, Option>) { - let params = args.map(|args| { - args.iter() - .enumerate() - .fold(HashMap::new(), |mut acc, (idx, param)| { - acc.insert(format!("param{idx}"), param.to_string()); - acc - }) - }); - - let mut query_string = format!( - "CALL {}({})", - procedure.to_string(), - args.unwrap_or_default() - .iter() - .map(|element| format!("${}", element)) - .collect::>() - .join(",") - ); - - let yields = yields.unwrap_or_default(); - if !yields.is_empty() { - query_string += format!( - " YIELD {}", - yields - .iter() - .map(|element| element.to_string()) - .collect::>() - .join(",") - ) - .as_str(); - } - - (query_string, params) -} - -pub(crate) fn construct_query( - query_str: Q, - params: Option<&HashMap>, -) -> String { - format!( - "{}{}", - params - .and_then(|params| params.is_empty().not().then(|| params - .iter() - .fold("CYPHER ".to_string(), |acc, (key, val)| { - format!("{}{}={} ", acc, key.to_string(), val.to_string()) - }))) - .unwrap_or_default(), - query_str.to_string() - ) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_generate_procedure_call() { - let (query, params) = generate_procedure_call( - "DB.CONSTRAINTS", - Some(&["Hello", "World"]), - Some(&["Foo", "Bar"]), - ); - - assert_eq!(query, "CALL DB.CONSTRAINTS($Hello,$World) YIELD Foo,Bar"); - assert!(params.is_some()); - - let params = params.unwrap(); - assert_eq!(params["param0"], "Hello"); - assert_eq!(params["param1"], "World"); - } - - #[test] - fn test_construct_query() { - let query = construct_query("MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100", - Some(&HashMap::from([("Foo", "Bar"), ("Bizz", "Bazz")]))); - assert!(query.starts_with("CYPHER ")); - assert!(query.ends_with(" MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100")); - - // Order not guaranteed - assert!(query.contains(" Foo=Bar ")); - assert!(query.contains(" Bizz=Bazz ")); - } -} diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index 131a308..928e2b1 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -3,15 +3,12 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorValue}; -use anyhow::Result; -use std::collections::{HashMap, HashSet}; -use utils::{get_refresh_command, get_relevant_hashmap, update_map}; - -#[cfg(feature = "tokio")] -use { - crate::connection::asynchronous::BorrowedAsyncConnection, std::sync::Arc, tokio::sync::Mutex, +use crate::{ + connection::blocking::BorrowedSyncConnection, graph_schema::utils::get_refresh_command, + FalkorDBError, FalkorResult, FalkorValue, }; +use std::collections::{HashMap, HashSet}; +use utils::{get_relevant_hashmap, update_map}; mod utils; @@ -20,70 +17,14 @@ mod utils; /// Using this enum we know which of the schema maps to access in order to convert these ids to strings #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum SchemaType { + /// The schema for [`Node`](crate::Node) labels Labels, + /// The schema for [`Node`](crate::Node) or [`Edge`](crate::Edge) properties (attribute keys) Properties, + /// The schema for [`Edge`](crate::Edge) labels Relationships, } -pub trait RefreshSchemaKeys { - fn refresh_schema_keys( - &mut self, - schema_type: SchemaType, - graph_name: &str, - ) -> Result; -} - -impl RefreshSchemaKeys for BorrowedSyncConnection { - fn refresh_schema_keys( - &mut self, - schema_type: SchemaType, - graph_name: &str, - ) -> Result { - self.send_command( - Some(graph_name), - "GRAPH.QUERY", - None, - Some(&[format!("CALL {}()", get_refresh_command(schema_type))]), - ) - } -} -#[cfg(feature = "tokio")] -impl RefreshSchemaKeys for BorrowedAsyncConnection { - fn refresh_schema_keys( - &mut self, - schema_type: SchemaType, - graph_name: &str, - ) -> Result { - let (oneshot_tx, oneshot_rx) = std::sync::mpsc::sync_channel(1); - - tokio::task::spawn_blocking({ - let self_ptr = self as *mut BorrowedAsyncConnection as usize; - let graph_name = graph_name.to_string(); - move || unsafe { - let self_ref = &mut *(self_ptr as *mut BorrowedAsyncConnection); - let params = vec![format!("CALL {}()", get_refresh_command(schema_type))]; - - oneshot_tx - .send( - tokio::runtime::Handle::current().block_on(self_ref.send_command( - Some(graph_name.as_str()), - "GRAPH.QUERY", - None, - Some(params.as_slice()), - )), - ) - .ok(); - } - }); - - // Receive the result from the channel - oneshot_rx - .recv() - .map_err(Into::into) - .and_then(|res| res.map_err(Into::into)) - } -} - pub(crate) type IdMap = HashMap; /// A struct containing the various schema maps, allowing conversions between ids and their string representations. @@ -97,9 +38,9 @@ pub struct GraphSchema { } impl GraphSchema { - pub(crate) fn new(graph_name: String) -> Self { + pub(crate) fn new(graph_name: T) -> Self { Self { - graph_name, + graph_name: graph_name.to_string(), ..Default::default() } } @@ -149,30 +90,7 @@ impl GraphSchema { conn: &mut BorrowedSyncConnection, schema_type: SchemaType, id_hashset: Option<&HashSet>, - ) -> Result>> { - let id_map = match schema_type { - SchemaType::Labels => &mut self.labels, - SchemaType::Properties => &mut self.properties, - SchemaType::Relationships => &mut self.relationships, - }; - - // 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] = conn - .refresh_schema_keys(schema_type, self.graph_name.as_str())? - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - Ok(update_map(id_map, keys, id_hashset)?) - } - - #[cfg(feature = "tokio")] - pub(crate) async fn refresh_async( - &mut self, - conn: &Arc>, - schema_type: SchemaType, - id_hashset: Option<&HashSet>, - ) -> Result>> { + ) -> FalkorResult>> { let id_map = match schema_type { SchemaType::Labels => &mut self.labels, SchemaType::Properties => &mut self.properties, @@ -181,14 +99,17 @@ 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] = conn - .lock() - .await - .refresh_schema_keys(schema_type, self.graph_name.as_str())? + .send_command( + Some(self.graph_name.as_str()), + "GRAPH.QUERY", + None, + Some(&[format!("CALL {}()", get_refresh_command(schema_type))]), + )? .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - Ok(update_map(id_map, keys, id_hashset)?) + update_map(id_map, keys, id_hashset) } } diff --git a/src/graph_schema/utils.rs b/src/graph_schema/utils.rs index 4ca6b92..40067d9 100644 --- a/src/graph_schema/utils.rs +++ b/src/graph_schema/utils.rs @@ -66,11 +66,11 @@ mod tests { use super::*; fn get_test_keys() -> FalkorValue { - FalkorValue::FArray(vec![ - FalkorValue::FArray(vec![FalkorValue::FString("Hello".to_string())]), - FalkorValue::FArray(vec![FalkorValue::FString("Iterator".to_string())]), - FalkorValue::FArray(vec![FalkorValue::FString("My-".to_string())]), - FalkorValue::FArray(vec![FalkorValue::FString("Panic".to_string())]), + FalkorValue::Array(vec![ + FalkorValue::Array(vec![FalkorValue::String("Hello".to_string())]), + FalkorValue::Array(vec![FalkorValue::String("Iterator".to_string())]), + FalkorValue::Array(vec![FalkorValue::String("My-".to_string())]), + FalkorValue::Array(vec![FalkorValue::String("Panic".to_string())]), ]) } diff --git a/src/lib.rs b/src/lib.rs index 7bd826e..14adc8c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,9 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +#![deny(missing_docs)] +#![doc = include_str!("../README.md")] + #[cfg(not(feature = "redis"))] compile_error!("The `redis` feature must be enabled."); @@ -19,10 +22,16 @@ mod value; #[cfg(feature = "redis")] mod redis_ext; +/// A [`Result`] which only returns [`FalkorDBError`] as its E type +pub type FalkorResult = Result; + pub use client::{blocking::FalkorSyncClient, builder::FalkorClientBuilder}; pub use connection_info::FalkorConnectionInfo; pub use error::FalkorDBError; -pub use graph::blocking::SyncGraph; +pub use graph::{ + blocking::SyncGraph, + query_builder::{ProcedureBuilder, QueryBuilder}, +}; pub use graph_schema::{GraphSchema, SchemaType}; pub use parser::FalkorParsable; pub use response::{ @@ -30,7 +39,7 @@ pub use response::{ execution_plan::ExecutionPlan, index::{FalkorIndex, IndexStatus, IndexType}, slowlog_entry::SlowlogEntry, - FalkorResponse, ResponseEnum, ResultSet, + FalkorResponse, ResultSet, }; pub use value::{ config::ConfigValue, @@ -43,7 +52,7 @@ pub use value::{ #[cfg(feature = "tokio")] pub use { client::asynchronous::FalkorAsyncClient, connection::asynchronous::FalkorAsyncConnection, - graph::asynchronous::AsyncGraph, parser::FalkorParsableAsync, + graph::asynchronous::AsyncGraph, }; #[cfg(test)] @@ -77,38 +86,4 @@ pub(crate) mod test_utils { .expect("Could not copy graph for test"), } } - - #[cfg(feature = "tokio")] - pub(crate) struct TestAsyncGraphHandle { - pub(crate) inner: AsyncGraph, - } - - #[cfg(feature = "tokio")] - impl Drop for TestAsyncGraphHandle { - fn drop(&mut self) { - tokio::task::block_in_place(|| { - tokio::runtime::Handle::current().block_on(self.inner.delete()) - }) - .ok(); - } - } - - #[cfg(feature = "tokio")] - pub(crate) async fn create_async_test_client() -> FalkorAsyncClient { - FalkorClientBuilder::new_async() - .build() - .await - .expect("Could not construct client") - } - - #[cfg(feature = "tokio")] - pub(crate) async fn open_test_graph_async(graph_name: &str) -> TestAsyncGraphHandle { - let client = create_async_test_client().await; - TestAsyncGraphHandle { - inner: client - .copy_graph("imdb", graph_name) - .await - .expect("Could not copy graph"), - } - } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 26f2d86..032aaf2 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5,28 +5,15 @@ pub mod utils; -use crate::{connection::blocking::BorrowedSyncConnection, FalkorValue, GraphSchema}; -use anyhow::Result; - -#[cfg(feature = "tokio")] -use { - crate::connection::asynchronous::BorrowedAsyncConnection, std::sync::Arc, tokio::sync::Mutex, -}; +use crate::connection::blocking::BorrowedSyncConnection; +use crate::{FalkorResult, FalkorValue, GraphSchema}; /// 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, conn: &mut BorrowedSyncConnection, - ) -> Result; -} - -#[cfg(feature = "tokio")] -pub trait FalkorParsableAsync: Sized { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &mut GraphSchema, - conn: Arc>, - ) -> Result; + ) -> FalkorResult; } diff --git a/src/parser/utils.rs b/src/parser/utils.rs index 86a9dc5..ca45bce 100644 --- a/src/parser/utils.rs +++ b/src/parser/utils.rs @@ -5,19 +5,11 @@ use crate::{ connection::blocking::BorrowedSyncConnection, value::utils::parse_type, FalkorDBError, - FalkorValue, GraphSchema, + FalkorValue, GraphSchema, ResultSet, }; use anyhow::Result; -use std::collections::HashMap; -#[cfg(feature = "tokio")] -use { - crate::{connection::asynchronous::BorrowedAsyncConnection, value::utils::parse_type_async}, - std::sync::Arc, - tokio::sync::Mutex, -}; - -pub fn string_vec_from_val(value: FalkorValue) -> Result, FalkorDBError> { +pub(crate) fn string_vec_from_val(value: FalkorValue) -> Result, FalkorDBError> { value.into_vec().map(|value_as_vec| { value_as_vec .into_iter() @@ -26,7 +18,7 @@ pub fn string_vec_from_val(value: FalkorValue) -> Result, FalkorDBEr }) } -pub fn parse_header(header: FalkorValue) -> Result, FalkorDBError> { +pub(crate) fn parse_header(header: FalkorValue) -> Result, FalkorDBError> { header.into_vec().map(|header_as_vec| { header_as_vec .into_iter() @@ -53,41 +45,13 @@ pub(crate) fn parse_result_set( data: FalkorValue, graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, - header_keys: &[String], -) -> Result>> { - Ok(data - .into_vec()? - .into_iter() - .flat_map(|column| { - Result::<_, FalkorDBError>::Ok( - header_keys - .iter() - .cloned() - .zip(parse_type(6, column, graph_schema, conn)) - .collect(), - ) - }) - .collect()) -} +) -> Result { + let data_vec = data.into_vec()?; -#[cfg(feature = "tokio")] -pub(crate) async fn parse_result_set_async( - data: FalkorValue, - graph_schema: &mut GraphSchema, - conn: Arc>, - header_keys: &[String], -) -> Result>> { - Ok(data - .into_vec()? - .into_iter() - .flat_map(|column| { - Result::<_, FalkorDBError>::Ok( - header_keys - .iter() - .cloned() - .zip(parse_type_async(6, column, graph_schema, conn).await) - .collect(), - ) - }) - .collect()) + let mut out_vec = Vec::with_capacity(data_vec.len()); + for column in data_vec { + out_vec.push(parse_type(6, column, graph_schema, conn)?.into_vec()?); + } + + Ok(out_vec) } diff --git a/src/redis_ext.rs b/src/redis_ext.rs index 44c9cdb..d639b70 100644 --- a/src/redis_ext.rs +++ b/src/redis_ext.rs @@ -66,16 +66,16 @@ 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::Int64(*int_val), + redis::Value::Int(int_val) => FalkorValue::I64(*int_val), redis::Value::Data(str_val) => { - FalkorValue::FString(String::from_utf8_lossy(str_val.as_slice()).to_string()) + FalkorValue::String(String::from_utf8_lossy(str_val.as_slice()).to_string()) } - redis::Value::Bulk(bulk) => FalkorValue::FArray( + redis::Value::Bulk(bulk) => FalkorValue::Array( bulk.iter() .flat_map(FalkorValue::from_redis_value) .collect(), ), - redis::Value::Status(status) => FalkorValue::FString(status.to_string()), + 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 ed2d8a2..5f8a26c 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -5,21 +5,10 @@ use crate::{ connection::blocking::BorrowedSyncConnection, value::utils::parse_type, EntityType, - FalkorDBError, FalkorParsable, FalkorValue, GraphSchema, + FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, }; -use anyhow::Result; use std::fmt::{Display, Formatter}; -#[cfg(feature = "tokio")] -use { - crate::{ - connection::asynchronous::BorrowedAsyncConnection, value::utils::parse_type_async, - FalkorParsableAsync, - }, - std::sync::Arc, - tokio::sync::Mutex, -}; - /// The type of restriction to apply for the property #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum ConstraintType { @@ -46,7 +35,7 @@ impl Display for ConstraintType { impl TryFrom<&str> for ConstraintType { type Error = FalkorDBError; - fn try_from(value: &str) -> Result { + fn try_from(value: &str) -> FalkorResult { Ok(match value.to_uppercase().as_str() { "MANDATORY" => Self::Mandatory, "UNIQUE" => Self::Unique, @@ -58,7 +47,7 @@ impl TryFrom<&str> for ConstraintType { impl TryFrom for ConstraintType { type Error = FalkorDBError; - fn try_from(value: String) -> Result { + fn try_from(value: String) -> FalkorResult { value.as_str().try_into() } } @@ -66,7 +55,7 @@ impl TryFrom for ConstraintType { impl TryFrom<&String> for ConstraintType { type Error = FalkorDBError; - fn try_from(value: &String) -> Result { + fn try_from(value: &String) -> FalkorResult { value.as_str().try_into() } } @@ -85,7 +74,7 @@ pub enum ConstraintStatus { impl TryFrom<&str> for ConstraintStatus { type Error = FalkorDBError; - fn try_from(value: &str) -> Result { + fn try_from(value: &str) -> FalkorResult { Ok(match value.to_uppercase().as_str() { "OPERATIONAL" => Self::Active, "UNDER CONSTRUCTION" => Self::Pending, @@ -98,7 +87,7 @@ impl TryFrom<&str> for ConstraintStatus { impl TryFrom for ConstraintStatus { type Error = FalkorDBError; - fn try_from(value: String) -> Result { + fn try_from(value: String) -> FalkorResult { value.as_str().try_into() } } @@ -106,7 +95,7 @@ impl TryFrom for ConstraintStatus { impl TryFrom<&String> for ConstraintStatus { type Error = FalkorDBError; - fn try_from(value: &String) -> Result { + fn try_from(value: &String) -> FalkorResult { value.as_str().try_into() } } @@ -127,7 +116,7 @@ pub struct Constraint { } impl Constraint { - fn from_value_vec(vlaue_vec: Vec) -> Result { + fn from_value_vec(vlaue_vec: Vec) -> FalkorResult { let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = vlaue_vec.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; Ok(Constraint { @@ -149,30 +138,8 @@ impl FalkorParsable for Constraint { value: FalkorValue, graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, - ) -> Result { - parse_type(6, value, graph_schema, conn).and_then(|parsed| { - parsed - .into_vec() - .map_err(Into::into) - .and_then(Constraint::from_value_vec) - }) - } -} - -#[cfg(feature = "tokio")] -impl FalkorParsableAsync for Constraint { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &mut GraphSchema, - conn: Arc>, - ) -> Result { - parse_type_async(6, value, graph_schema, conn) - .await - .and_then(|parsed| { - parsed - .into_vec() - .map_err(Into::into) - .and_then(Constraint::from_value_vec) - }) + ) -> FalkorResult { + parse_type(6, value, graph_schema, conn) + .and_then(|parsed| parsed.into_vec().and_then(Constraint::from_value_vec)) } } diff --git a/src/response/index.rs b/src/response/index.rs index 2a67af1..42f5de6 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -3,24 +3,14 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ +use crate::connection::blocking::BorrowedSyncConnection; use crate::{ - connection::blocking::BorrowedSyncConnection, value::utils::{parse_type, parse_vec, type_val_from_value}, EntityType, FalkorDBError, FalkorParsable, FalkorValue, GraphSchema, }; use anyhow::Result; use std::collections::HashMap; -#[cfg(feature = "tokio")] -use { - crate::{ - connection::asynchronous::BorrowedAsyncConnection, value::utils::parse_type_async, - FalkorParsableAsync, - }, - std::sync::Arc, - tokio::sync::Mutex, -}; - /// The status of this index #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum IndexStatus { @@ -118,18 +108,26 @@ fn parse_types_map(value: FalkorValue) -> Result> /// Contains all the info regarding an index on the database #[derive(Clone, Debug, PartialEq)] pub struct FalkorIndex { + /// Whether this index is for a Node or on an Edge pub entity_type: EntityType, + /// Whether this index is active or still under construction pub status: IndexStatus, + /// What is this index's label pub index_label: String, + /// What fields to index by pub fields: Vec, + /// Whether each field is a text field, range, etc. pub field_types: HashMap>, + /// Which language is the text used to index in pub language: String, + /// 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, } impl FalkorIndex { - fn from_raw_values(items: Vec) -> Result { + 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)?; Ok(Self { @@ -150,7 +148,7 @@ impl FalkorParsable for FalkorIndex { value: FalkorValue, graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, - ) -> Result { + ) -> Result { let semi_parsed_items = value .into_vec()? .into_iter() @@ -163,23 +161,3 @@ impl FalkorParsable for FalkorIndex { Self::from_raw_values(semi_parsed_items) } } - -#[cfg(feature = "tokio")] -impl FalkorParsableAsync for FalkorIndex { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &mut GraphSchema, - conn: Arc>, - ) -> Result { - let semi_parsed_items = value - .into_vec()? - .into_iter() - .flat_map(|item| { - let (type_marker, val) = type_val_from_value(item)?; - parse_type_async(type_marker, val, graph_schema, conn).await - }) - .collect::>(); - - Self::from_raw_values(semi_parsed_items) - } -} diff --git a/src/response/mod.rs b/src/response/mod.rs index c943d17..f94c0d6 100644 --- a/src/response/mod.rs +++ b/src/response/mod.rs @@ -5,45 +5,40 @@ use crate::{ parser::utils::{parse_header, string_vec_from_val}, - FalkorDBError, FalkorValue, + FalkorResult, FalkorValue, }; -use constraint::Constraint; -use execution_plan::ExecutionPlan; -use index::FalkorIndex; -use slowlog_entry::SlowlogEntry; -use std::collections::HashMap; pub(crate) mod constraint; pub(crate) mod execution_plan; pub(crate) mod index; pub(crate) mod slowlog_entry; -pub type ResultSet = Vec>; - -/// A naive wrapper for the various possible responses from queries -/// Its main usecase is for creating things like JoinSets, which require all the tasks to return the same type -#[derive(Debug, Clone, PartialEq)] -pub enum ResponseEnum { - ExecutionPlan(ExecutionPlan), - QueryResult(ResultSet), - SlowlogEntry(SlowlogEntry), - Constraints(Vec), - Indices(Vec), -} +/// A [`Vec`], representing a table of other [`Vec`]s, representing columns, containing [`FalkorValue`]s +pub type ResultSet = Vec>; +/// A response struct which also contains the returned header and stats data #[derive(Clone, Debug, Default)] pub struct FalkorResponse { + /// Header for the result data, usually contains the scalar aliases for the columns pub header: Vec, + /// The actual data returned from the database pub data: T, + /// Various statistics regarding the request, such as execution time and number of successful operations pub stats: Vec, } 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`] + /// * `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`] pub fn from_response( headers: Option, data: T, stats: FalkorValue, - ) -> Result { + ) -> FalkorResult { Ok(Self { header: match headers { Some(headers) => parse_header(headers)?, @@ -54,11 +49,17 @@ impl FalkorResponse { }) } + /// Creates a [`FalkorResponse`] from the specified data, and raw stats, where parsed headers are given and parsing them is not needed + /// + /// # Arguments + /// * `data`: The actual data + /// * `headers`: The already parsed headers, this is usually used to pass an empty header vec, or if we already parsed the headers for use in some other context and don't want to repeat + /// * `stats`: a [`FalkorValue`] that is expected to be of variant [`FalkorValue::Array`], where each element is expected to be of variant [`FalkorValue::String`] pub fn from_response_with_headers( data: T, header: Vec, stats: FalkorValue, - ) -> Result { + ) -> FalkorResult { Ok(Self { header, data, diff --git a/src/value/config.rs b/src/value/config.rs index 1e13ec6..80baeac 100644 --- a/src/value/config.rs +++ b/src/value/config.rs @@ -9,11 +9,14 @@ use std::fmt::{Display, Formatter}; /// An enum representing the two viable types for a config value #[derive(Clone, Debug, PartialEq)] pub enum ConfigValue { + /// A string value String(String), + /// An int value, also used to represent booleans Int64(i64), } impl ConfigValue { + /// Returns a copy of the contained int value, if there is one. pub fn as_i64(&self) -> Option { match self { ConfigValue::String(_) => None, @@ -57,8 +60,8 @@ impl TryFrom for ConfigValue { fn try_from(value: FalkorValue) -> Result { match value { - FalkorValue::FString(str_val) => Ok(ConfigValue::String(str_val)), - FalkorValue::Int64(int_val) => Ok(ConfigValue::Int64(int_val)), + FalkorValue::String(str_val) => Ok(ConfigValue::String(str_val)), + FalkorValue::I64(int_val) => Ok(ConfigValue::Int64(int_val)), _ => Err(FalkorDBError::ParsingConfigValue), } } diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index 639d891..df18ca7 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -4,9 +4,8 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, - value::{map::parse_map_with_schema, utils::parse_labels}, - FalkorDBError, FalkorParsable, FalkorValue, GraphSchema, SchemaType, + connection::blocking::BorrowedSyncConnection, value::map::parse_map_with_schema, FalkorDBError, + FalkorParsable, FalkorResult, FalkorValue, GraphSchema, SchemaType, }; use anyhow::Result; use std::{ @@ -14,20 +13,12 @@ use std::{ fmt::{Display, Formatter}, }; -#[cfg(feature = "tokio")] -use { - crate::{ - connection::asynchronous::BorrowedAsyncConnection, - value::{map::parse_map_with_schema_async, utils::parse_labels_async}, - FalkorParsableAsync, - }, - std::sync::Arc, - tokio::sync::Mutex, -}; - +/// Whether this element is a node or edge in the graph #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum EntityType { + /// A node in the graph Node, + /// An edge in the graph, meaning a relationship between two nodes Edge, } @@ -88,7 +79,7 @@ impl FalkorParsable for Node { value: FalkorValue, graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, - ) -> Result { + ) -> FalkorResult { let [entity_id, labels, properties]: [FalkorValue; 3] = value .into_vec()? .try_into() @@ -118,44 +109,6 @@ impl FalkorParsable for Node { } } -#[cfg(feature = "tokio")] -impl FalkorParsableAsync for Node { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &mut GraphSchema, - conn: Arc>, - ) -> Result { - let [entity_id, labels, properties]: [FalkorValue; 3] = value - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - 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)?, - ); - } - - let parsed_labels = - parse_labels_async(labels, graph_schema, Arc::clone(&conn), SchemaType::Labels).await?; - Ok(Node { - entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - labels: parsed_labels, - properties: parse_map_with_schema_async( - properties, - graph_schema, - conn, - SchemaType::Properties, - ) - .await?, - }) - } -} - /// An edge in the graph, representing a relationship between two [`Node`]s. #[derive(Clone, Debug, Default, PartialEq)] pub struct Edge { @@ -176,107 +129,52 @@ impl FalkorParsable for Edge { value: FalkorValue, graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, - ) -> Result { + ) -> FalkorResult { let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value .into_vec()? .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; let relation = relations.to_i64().ok_or(FalkorDBError::ParsingI64)?; - if let Some(relationship) = graph_schema.relationships().get(&relation) { - return Ok(Edge { - entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - 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)?, - properties: parse_map_with_schema( - properties, - graph_schema, - conn, - SchemaType::Properties, - )?, - }); - } + let relationship = graph_schema + .relationships() + .get(&relation) + .ok_or(FalkorDBError::MissingSchemaId(SchemaType::Relationships))?; - match graph_schema.refresh( - conn, - SchemaType::Relationships, - Some(&HashSet::from([relation])), - )? { - None => Err(FalkorDBError::ParsingCompactIdUnknown)?, - Some(id) => Ok(Edge { - entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - relationship_type: id - .get(&relation) - .cloned() - .ok_or(FalkorDBError::ParsingCompactIdUnknown)?, - src_node_id: src_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - dst_node_id: dst_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - properties: parse_map_with_schema( - properties, - graph_schema, - conn, - SchemaType::Properties, - )?, - }), - } + Ok(Edge { + entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, + 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)?, + properties: parse_map_with_schema( + properties, + graph_schema, + conn, + SchemaType::Properties, + )?, + }) } } -#[cfg(feature = "tokio")] -impl FalkorParsableAsync for Edge { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &mut GraphSchema, - conn: Arc>, - ) -> Result { - let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let relation = relations.to_i64().ok_or(FalkorDBError::ParsingI64)?; - if let Some(relationship) = graph_schema.relationships().get(&relation) { - return Ok(Edge { - entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - 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)?, - properties: parse_map_with_schema_async( - properties, - graph_schema, - conn, - SchemaType::Properties, - ) - .await?, - }); - } - - match graph_schema - .refresh_async( - &conn, - SchemaType::Relationships, - Some(&HashSet::from([relation])), - ) - .await? - { - None => Err(FalkorDBError::ParsingCompactIdUnknown)?, - Some(id) => Ok(Edge { - entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - relationship_type: id - .get(&relation) - .cloned() - .ok_or(FalkorDBError::ParsingCompactIdUnknown)?, - src_node_id: src_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - dst_node_id: dst_node_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - properties: parse_map_with_schema_async( - properties, - graph_schema, - conn, - SchemaType::Properties, - ) - .await?, - }), - } +pub(crate) fn parse_labels( + raw_ids: Vec, + graph_schema: &mut GraphSchema, + conn: &mut BorrowedSyncConnection, + schema_type: SchemaType, +) -> FalkorResult> { + let ids_hashset = raw_ids + .iter() + .filter_map(|label_id| label_id.to_i64()) + .collect::>(); + + let relevant_ids = match graph_schema.verify_id_set(&ids_hashset, schema_type) { + None => graph_schema.refresh(conn, schema_type, Some(&ids_hashset))?, + relevant_ids => relevant_ids, } + .ok_or(FalkorDBError::ParsingError)?; + + Ok(raw_ids + .into_iter() + .filter_map(|id| id.to_i64().and_then(|id| relevant_ids.get(&id).cloned())) + .collect()) } diff --git a/src/value/map.rs b/src/value/map.rs index 3912278..ee99bce 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -5,21 +5,10 @@ use crate::{ connection::blocking::BorrowedSyncConnection, value::utils::parse_type, FalkorDBError, - FalkorParsable, FalkorValue, GraphSchema, SchemaType, + FalkorParsable, FalkorResult, FalkorValue, GraphSchema, SchemaType, }; -use anyhow::Result; use std::collections::HashMap; -#[cfg(feature = "tokio")] -use { - crate::{ - connection::asynchronous::BorrowedAsyncConnection, value::utils::parse_type_async, - FalkorParsableAsync, - }, - std::sync::Arc, - tokio::sync::Mutex, -}; - // Intermediate type for map parsing pub(crate) struct FKeyTypeVal { key: i64, @@ -30,7 +19,7 @@ pub(crate) struct FKeyTypeVal { impl TryFrom for FKeyTypeVal { type Error = FalkorDBError; - fn try_from(value: FalkorValue) -> Result { + fn try_from(value: FalkorValue) -> FalkorResult { let [key_raw, type_raw, val]: [FalkorValue; 3] = value .into_vec()? .try_into() @@ -57,7 +46,7 @@ fn ktv_vec_to_map( relevant_ids_map: HashMap, graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, -) -> Result> { +) -> FalkorResult> { let mut new_map = HashMap::with_capacity(map_vec.len()); for fktv in map_vec { new_map.insert( @@ -72,33 +61,12 @@ fn ktv_vec_to_map( Ok(new_map) } -#[cfg(feature = "tokio")] -async fn ktv_vec_to_map_async( - map_vec: Vec, - relevant_ids_map: HashMap, - graph_schema: &mut GraphSchema, - conn: Arc>, -) -> Result> { - let mut new_map = HashMap::with_capacity(map_vec.len()); - for fktv in map_vec { - new_map.insert( - relevant_ids_map - .get(&fktv.key) - .cloned() - .ok_or(FalkorDBError::ParsingError)?, - parse_type_async(fktv.type_marker, fktv.val, graph_schema, Arc::clone(&conn)).await?, - ); - } - - Ok(new_map) -} - pub(crate) fn parse_map_with_schema( value: FalkorValue, graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, schema_type: SchemaType, -) -> Result> { +) -> FalkorResult> { let (id_hashset, map_vec) = value .into_vec()? .into_iter() @@ -117,42 +85,12 @@ pub(crate) fn parse_map_with_schema( } } -#[cfg(feature = "tokio")] -pub(crate) async fn parse_map_with_schema_async( - value: FalkorValue, - graph_schema: &mut GraphSchema, - conn: Arc>, - schema_type: SchemaType, -) -> Result> { - let (id_hashset, map_vec) = value - .into_vec()? - .into_iter() - .flat_map(FKeyTypeVal::try_from) - .map(|fktv| (fktv.key, fktv)) - .unzip(); - - if let Some(relevant_ids_map) = graph_schema.verify_id_set(&id_hashset, schema_type) { - return ktv_vec_to_map_async(map_vec, relevant_ids_map, graph_schema, conn).await; - } - - // If we reached here, schema validation failed and we need to refresh our schema - match graph_schema - .refresh_async(&conn, schema_type, Some(&id_hashset)) - .await? - { - Some(relevant_ids_map) => { - ktv_vec_to_map_async(map_vec, relevant_ids_map, graph_schema, conn).await - } - None => Err(FalkorDBError::ParsingError)?, - } -} - impl FalkorParsable for HashMap { fn from_falkor_value( value: FalkorValue, graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, - ) -> Result { + ) -> FalkorResult { let val_vec = value.into_vec()?; if val_vec.len() % 2 != 0 { Err(FalkorDBError::ParsingFMap)?; @@ -171,7 +109,7 @@ impl FalkorParsable for HashMap { .try_into() .map_err(|_| FalkorDBError::ParsingFMap)?; - Result::<_>::Ok(( + FalkorResult::<_>::Ok(( key.into_string()?, parse_type( type_marker.to_i64().ok_or(FalkorDBError::ParsingKTVTypes)?, @@ -184,43 +122,3 @@ impl FalkorParsable for HashMap { .collect()) } } - -#[cfg(feature = "tokio")] -impl FalkorParsableAsync for HashMap { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &mut GraphSchema, - conn: Arc>, - ) -> Result { - let val_vec = value.into_vec()?; - if val_vec.len() % 2 != 0 { - Err(FalkorDBError::ParsingFMap)?; - } - - Ok(val_vec - .chunks_exact(2) - .flat_map(|pair| { - let [key, val]: [FalkorValue; 2] = pair - .to_vec() - .try_into() - .map_err(|_| FalkorDBError::ParsingFMap)?; - - let [type_marker, val]: [FalkorValue; 2] = val - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingFMap)?; - - Result::<_>::Ok(( - key.into_string()?, - parse_type_async( - type_marker.to_i64().ok_or(FalkorDBError::ParsingKTVTypes)?, - val, - graph_schema, - conn, - ) - .await?, - )) - }) - .collect()) - } -} diff --git a/src/value/mod.rs b/src/value/mod.rs index 04927b9..b1694bc 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -4,21 +4,14 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, GraphSchema, + connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, FalkorResult, + GraphSchema, }; -use anyhow::Result; use graph_entities::{Edge, Node}; use path::Path; use point::Point; use std::{collections::HashMap, fmt::Debug}; -#[cfg(feature = "tokio")] -use { - crate::{connection::asynchronous::BorrowedAsyncConnection, FalkorParsableAsync}, - std::sync::Arc, - tokio::sync::Mutex, -}; - pub(crate) mod config; pub(crate) mod graph_entities; pub(crate) mod map; @@ -29,16 +22,27 @@ pub(crate) mod utils; /// An enum of all the supported Falkor types #[derive(Clone, Debug, PartialEq)] pub enum FalkorValue { - FNode(Node), - FEdge(Edge), - FArray(Vec), - FMap(HashMap), - FString(String), - FBool(bool), - Int64(i64), + /// See [`Node`] + Node(Node), + /// See [`Edge`] + Edge(Edge), + /// A [`Vec`] of other [`FalkorValue`] + Array(Vec), + /// A [`HashMap`] of [`String`] as keys, and other [`FalkorValue`] as values + Map(HashMap), + /// Plain old string + String(String), + /// A boolean value + Bool(bool), + /// An [`i64`] value, Falkor only supports signed integers + I64(i64), + /// An [`f64`] value, Falkor only supports double precisions when not in Vectors F64(f64), - FPoint(Point), - FPath(Path), + /// See [`Point`] + Point(Point), + /// See [`Path`] + Path(Path), + /// A NULL type None, } @@ -52,22 +56,22 @@ macro_rules! impl_to_falkordb_value { }; } -impl_to_falkordb_value!(i8, Self::Int64); -impl_to_falkordb_value!(i32, Self::Int64); -impl_to_falkordb_value!(i64, Self::Int64); +impl_to_falkordb_value!(i8, Self::I64); +impl_to_falkordb_value!(i32, Self::I64); +impl_to_falkordb_value!(i64, Self::I64); -impl_to_falkordb_value!(u8, Self::Int64); -impl_to_falkordb_value!(u32, Self::Int64); -impl_to_falkordb_value!(u64, Self::Int64); +impl_to_falkordb_value!(u8, Self::I64); +impl_to_falkordb_value!(u32, Self::I64); +impl_to_falkordb_value!(u64, Self::I64); impl_to_falkordb_value!(f32, Self::F64); impl_to_falkordb_value!(f64, Self::F64); -impl_to_falkordb_value!(String, Self::FString); +impl_to_falkordb_value!(String, Self::String); impl From<&str> for FalkorValue { fn from(value: &str) -> Self { - Self::FString(value.to_string()) + Self::String(value.to_string()) } } @@ -76,7 +80,7 @@ where FalkorValue: From, { fn from(value: Vec) -> Self { - Self::FArray( + Self::Array( value .into_iter() .map(|element| FalkorValue::from(element)) @@ -88,9 +92,9 @@ where impl TryFrom for Vec { type Error = FalkorDBError; - fn try_from(value: FalkorValue) -> Result { + fn try_from(value: FalkorValue) -> FalkorResult { match value { - FalkorValue::FArray(val) => Ok(val), + FalkorValue::Array(val) => Ok(val), _ => Err(FalkorDBError::ParsingFArray), } } @@ -99,9 +103,9 @@ impl TryFrom for Vec { impl TryFrom for f64 { type Error = FalkorDBError; - fn try_from(value: FalkorValue) -> Result { + fn try_from(value: FalkorValue) -> FalkorResult { match value { - FalkorValue::FString(f64_str) => f64_str.parse().map_err(|_| FalkorDBError::ParsingF64), + FalkorValue::String(f64_str) => f64_str.parse().map_err(|_| FalkorDBError::ParsingF64), FalkorValue::F64(f64_val) => Ok(f64_val), _ => Err(FalkorDBError::ParsingF64), } @@ -111,9 +115,9 @@ impl TryFrom for f64 { impl TryFrom for String { type Error = FalkorDBError; - fn try_from(value: FalkorValue) -> Result { + fn try_from(value: FalkorValue) -> FalkorResult { match value { - FalkorValue::FString(val) => Ok(val), + FalkorValue::String(val) => Ok(val), _ => Err(FalkorDBError::ParsingFString), } } @@ -122,9 +126,9 @@ impl TryFrom for String { impl TryFrom for Edge { type Error = FalkorDBError; - fn try_from(value: FalkorValue) -> Result { + fn try_from(value: FalkorValue) -> FalkorResult { match value { - FalkorValue::FEdge(edge) => Ok(edge), + FalkorValue::Edge(edge) => Ok(edge), _ => Err(FalkorDBError::ParsingFEdge), } } @@ -133,9 +137,9 @@ impl TryFrom for Edge { impl TryFrom for Node { type Error = FalkorDBError; - fn try_from(value: FalkorValue) -> Result { + fn try_from(value: FalkorValue) -> FalkorResult { match value { - FalkorValue::FNode(node) => Ok(node), + FalkorValue::Node(node) => Ok(node), _ => Err(FalkorDBError::ParsingFNode), } } @@ -144,9 +148,9 @@ impl TryFrom for Node { impl TryFrom for Path { type Error = FalkorDBError; - fn try_from(value: FalkorValue) -> Result { + fn try_from(value: FalkorValue) -> FalkorResult { match value { - FalkorValue::FPath(path) => Ok(path), + FalkorValue::Path(path) => Ok(path), _ => Err(FalkorDBError::ParsingFPath), } } @@ -155,9 +159,9 @@ impl TryFrom for Path { impl TryFrom for HashMap { type Error = FalkorDBError; - fn try_from(value: FalkorValue) -> Result { + fn try_from(value: FalkorValue) -> FalkorResult { match value { - FalkorValue::FMap(map) => Ok(map), + FalkorValue::Map(map) => Ok(map), _ => Err(FalkorDBError::ParsingFMap), } } @@ -166,9 +170,9 @@ impl TryFrom for HashMap { impl TryFrom for Point { type Error = FalkorDBError; - fn try_from(value: FalkorValue) -> Result { + fn try_from(value: FalkorValue) -> FalkorResult { match value { - FalkorValue::FPoint(point) => Ok(point), + FalkorValue::Point(point) => Ok(point), _ => Err(FalkorDBError::ParsingFPoint), } } @@ -181,7 +185,7 @@ impl FalkorValue { /// A reference to the internal [`Vec`] pub fn as_vec(&self) -> Option<&Vec> { match self { - FalkorValue::FArray(val) => Some(val), + FalkorValue::Array(val) => Some(val), _ => None, } } @@ -192,7 +196,7 @@ impl FalkorValue { /// A reference to the internal [`String`] pub fn as_string(&self) -> Option<&String> { match self { - FalkorValue::FString(val) => Some(val), + FalkorValue::String(val) => Some(val), _ => None, } } @@ -203,7 +207,7 @@ impl FalkorValue { /// A reference to the internal [`Edge`] pub fn as_edge(&self) -> Option<&Edge> { match self { - FalkorValue::FEdge(val) => Some(val), + FalkorValue::Edge(val) => Some(val), _ => None, } } @@ -214,7 +218,7 @@ impl FalkorValue { /// A reference to the internal [`Node`] pub fn as_node(&self) -> Option<&Node> { match self { - FalkorValue::FNode(val) => Some(val), + FalkorValue::Node(val) => Some(val), _ => None, } } @@ -225,7 +229,7 @@ impl FalkorValue { /// A reference to the internal [`Path`] pub fn as_path(&self) -> Option<&Path> { match self { - FalkorValue::FPath(val) => Some(val), + FalkorValue::Path(val) => Some(val), _ => None, } } @@ -236,7 +240,7 @@ impl FalkorValue { /// A reference to the internal [`HashMap`] pub fn as_map(&self) -> Option<&HashMap> { match self { - FalkorValue::FMap(val) => Some(val), + FalkorValue::Map(val) => Some(val), _ => None, } } @@ -247,7 +251,7 @@ impl FalkorValue { /// A reference to the internal [`Point`] pub fn as_point(&self) -> Option<&Point> { match self { - FalkorValue::FPoint(val) => Some(val), + FalkorValue::Point(val) => Some(val), _ => None, } } @@ -258,7 +262,7 @@ impl FalkorValue { /// A copy of the inner [`i64`] pub fn to_i64(&self) -> Option { match self { - FalkorValue::Int64(val) => Some(*val), + FalkorValue::I64(val) => Some(*val), _ => None, } } @@ -269,8 +273,8 @@ impl FalkorValue { /// A copy of the inner [`bool`] pub fn to_bool(&self) -> Option { match self { - FalkorValue::FBool(val) => Some(*val), - FalkorValue::FString(bool_str) => match bool_str.as_str() { + FalkorValue::Bool(val) => Some(*val), + FalkorValue::String(bool_str) => match bool_str.as_str() { "true" => Some(true), "false" => Some(false), _ => None, @@ -294,7 +298,7 @@ impl FalkorValue { /// /// # Returns /// The inner [`Vec`] - pub fn into_vec(self) -> Result, FalkorDBError> { + pub fn into_vec(self) -> FalkorResult> { self.try_into() } @@ -302,7 +306,7 @@ impl FalkorValue { /// /// # Returns /// The inner [`String`] - pub fn into_string(self) -> Result { + pub fn into_string(self) -> FalkorResult { self.try_into() } } @@ -312,18 +316,7 @@ impl FalkorParsable for FalkorValue { value: FalkorValue, _: &mut GraphSchema, _: &mut BorrowedSyncConnection, - ) -> Result { - Ok(value) - } -} - -#[cfg(feature = "tokio")] -impl FalkorParsableAsync for FalkorValue { - async fn from_falkor_value_async( - value: FalkorValue, - _: &mut GraphSchema, - _: Arc>, - ) -> Result { + ) -> FalkorResult { Ok(value) } } diff --git a/src/value/path.rs b/src/value/path.rs index 0df2d36..b268a6e 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -4,22 +4,16 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, Edge, FalkorDBError, FalkorParsable, FalkorValue, - GraphSchema, Node, -}; -use anyhow::Result; - -#[cfg(feature = "tokio")] -use { - crate::{connection::asynchronous::BorrowedAsyncConnection, FalkorParsableAsync}, - std::sync::Arc, - tokio::sync::Mutex, + connection::blocking::BorrowedSyncConnection, Edge, FalkorDBError, FalkorParsable, + FalkorResult, FalkorValue, GraphSchema, Node, }; /// Represents a path between two nodes, contains all the nodes, and the relationships between them along the path #[derive(Clone, Debug, PartialEq)] pub struct Path { + /// The nodes along the path, ordered pub nodes: Vec, + /// The relationships between the nodes in the path, ordered pub relationships: Vec, } @@ -28,7 +22,7 @@ impl FalkorParsable for Path { value: FalkorValue, graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, - ) -> Result { + ) -> FalkorResult { let [nodes, relationships]: [FalkorValue; 2] = value .into_vec()? .try_into() @@ -48,30 +42,3 @@ impl FalkorParsable for Path { }) } } - -#[cfg(feature = "tokio")] -impl FalkorParsableAsync for Path { - async fn from_falkor_value_async( - value: FalkorValue, - graph_schema: &mut GraphSchema, - conn: Arc>, - ) -> Result { - let [nodes, relationships]: [FalkorValue; 2] = value - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - Ok(Self { - nodes: nodes - .into_vec()? - .into_iter() - .flat_map(|node| Node::from_falkor_value_async(node, graph_schema, conn).await) - .collect(), - relationships: relationships - .into_vec()? - .into_iter() - .flat_map(|edge| Edge::from_falkor_value_async(edge, graph_schema, conn).await) - .collect(), - }) - } -} diff --git a/src/value/point.rs b/src/value/point.rs index da3d1ea..ea585dd 100644 --- a/src/value/point.rs +++ b/src/value/point.rs @@ -3,7 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{FalkorDBError, FalkorValue}; +use crate::{FalkorDBError, FalkorResult, FalkorValue}; /// A point in the world. #[derive(Clone, Debug, PartialEq)] @@ -15,7 +15,15 @@ pub struct Point { } impl Point { - pub fn parse(value: FalkorValue) -> anyhow::Result { + /// Parses a point from a FalkorValue::Array, + /// taking the first element as an f64 latitude, and second element as an f64 longitude + /// + /// # Arguments + /// * `value`: The value to parse + /// + /// # Returns + /// Self, if successful + pub fn parse(value: FalkorValue) -> FalkorResult { let [lat, long]: [FalkorValue; 2] = value .into_vec()? .try_into() diff --git a/src/value/utils.rs b/src/value/utils.rs index f677105..af0c2e7 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -5,70 +5,11 @@ use crate::{ connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, FalkorValue, - GraphSchema, Point, SchemaType, + GraphSchema, Point, }; use anyhow::Result; -use std::collections::HashSet; -#[cfg(feature = "tokio")] -use { - crate::{connection::asynchronous::BorrowedAsyncConnection, FalkorParsableAsync}, - std::sync::Arc, - tokio::sync::Mutex, -}; - -pub(crate) fn parse_labels( - raw_ids: Vec, - graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, - schema_type: SchemaType, -) -> Result> { - let ids_hashset = raw_ids - .iter() - .filter_map(|label_id| label_id.to_i64()) - .collect::>(); - - let relevant_ids = match graph_schema.verify_id_set(&ids_hashset, schema_type) { - None => graph_schema.refresh(conn, schema_type, Some(&ids_hashset))?, - relevant_ids => relevant_ids, - } - .ok_or(FalkorDBError::ParsingError)?; - - Ok(raw_ids - .into_iter() - .filter_map(|id| id.to_i64().and_then(|id| relevant_ids.get(&id).cloned())) - .collect()) -} - -#[cfg(feature = "tokio")] -pub(crate) async fn parse_labels_async( - raw_ids: Vec, - graph_schema: &mut GraphSchema, - conn: Arc>, - schema_type: SchemaType, -) -> Result> { - let ids_hashset = raw_ids - .iter() - .filter_map(|label_id| label_id.to_i64()) - .collect::>(); - - let relevant_ids = match graph_schema.verify_id_set(&ids_hashset, schema_type) { - None => { - graph_schema - .refresh_async(&conn, schema_type, Some(&ids_hashset)) - .await? - } - relevant_ids => relevant_ids, - } - .ok_or(FalkorDBError::ParsingError)?; - - Ok(raw_ids - .into_iter() - .filter_map(|id| id.to_i64().and_then(|id| relevant_ids.get(&id).cloned())) - .collect()) -} - -pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValue)> { +pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValue), FalkorDBError> { let [type_marker, val]: [FalkorValue; 2] = value .into_vec()? .try_into() @@ -83,73 +24,30 @@ pub(crate) fn parse_type( val: FalkorValue, graph_schema: &mut GraphSchema, conn: &mut BorrowedSyncConnection, -) -> Result { +) -> Result { let res = match type_marker { 1 => FalkorValue::None, - 2 => FalkorValue::FString(val.into_string()?), - 3 => FalkorValue::Int64(val.to_i64().ok_or(FalkorDBError::ParsingI64)?), - 4 => FalkorValue::FBool(val.to_bool().ok_or(FalkorDBError::ParsingBool)?), + 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::FArray({ - 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, conn) - }) - }) - .collect() - }), - // The following types are sent as an array and require specific parsing functions - 7 => FalkorValue::FEdge(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), - 8 => FalkorValue::FNode(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), - 9 => FalkorValue::FPath(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), - 10 => FalkorValue::FMap(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), - 11 => FalkorValue::FPoint(Point::parse(val)?), - _ => Err(FalkorDBError::ParsingUnknownType)?, - }; + 6 => FalkorValue::Array({ + let val_vec = val.into_vec()?; - Ok(res) -} + let mut out_vec = Vec::with_capacity(val_vec.len()); + for item in val_vec { + let (type_marker, val) = type_val_from_value(item)?; + out_vec.push(parse_type(type_marker, val, graph_schema, conn)?) + } -#[cfg(feature = "tokio")] -#[async_recursion::async_recursion] -pub(crate) async fn parse_type_async( - type_marker: i64, - val: FalkorValue, - graph_schema: &mut GraphSchema, - conn: Arc>, -) -> Result { - let res = match type_marker { - 1 => FalkorValue::None, - 2 => FalkorValue::FString(val.into_string()?), - 3 => FalkorValue::Int64(val.to_i64().ok_or(FalkorDBError::ParsingI64)?), - 4 => FalkorValue::FBool(val.to_bool().ok_or(FalkorDBError::ParsingBool)?), - 5 => FalkorValue::F64(val.try_into()?), - 6 => FalkorValue::FArray({ - val.into_vec()? - .into_iter() - .flat_map(|item| { - type_val_from_value(item).and_then(|(type_marker, val)| { - parse_type_async(type_marker, val, graph_schema, conn).await - }) - }) - .collect() + out_vec }), // The following types are sent as an array and require specific parsing functions - 7 => FalkorValue::FEdge( - FalkorParsableAsync::from_falkor_value_async(val, graph_schema, conn).await?, - ), - 8 => FalkorValue::FNode( - FalkorParsableAsync::from_falkor_value_async(val, graph_schema, conn).await?, - ), - 9 => FalkorValue::FPath( - FalkorParsableAsync::from_falkor_value_async(val, graph_schema, conn).await?, - ), - 10 => FalkorValue::FMap( - FalkorParsableAsync::from_falkor_value_async(val, graph_schema, conn).await?, - ), - 11 => FalkorValue::FPoint(Point::parse(val)?), + 7 => FalkorValue::Edge(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), + 8 => FalkorValue::Node(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), + 9 => FalkorValue::Path(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), + 10 => FalkorValue::Map(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), + 11 => FalkorValue::Point(Point::parse(val)?), _ => Err(FalkorDBError::ParsingUnknownType)?, }; @@ -177,21 +75,21 @@ mod tests { let res = parse_type( 7, - FalkorValue::FArray(vec![ - FalkorValue::Int64(100), // edge id - FalkorValue::Int64(0), // edge type - FalkorValue::Int64(51), // src node - FalkorValue::Int64(52), // dst node - FalkorValue::FArray(vec![ - FalkorValue::FArray(vec![ - FalkorValue::Int64(0), - FalkorValue::Int64(3), - FalkorValue::Int64(20), + 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::FArray(vec![ - FalkorValue::Int64(1), - FalkorValue::Int64(4), - FalkorValue::FBool(false), + FalkorValue::Array(vec![ + FalkorValue::I64(1), + FalkorValue::I64(4), + FalkorValue::Bool(false), ]), ]), ]), @@ -202,7 +100,7 @@ mod tests { let falkor_edge = res.unwrap(); - let FalkorValue::FEdge(edge) = falkor_edge else { + let FalkorValue::Edge(edge) = falkor_edge else { panic!("Was not of type edge") }; assert_eq!(edge.entity_id, 100); @@ -211,10 +109,10 @@ mod tests { assert_eq!(edge.dst_node_id, 52); assert_eq!(edge.properties.len(), 2); - assert_eq!(edge.properties.get("age"), Some(&FalkorValue::Int64(20))); + assert_eq!(edge.properties.get("age"), Some(&FalkorValue::I64(20))); assert_eq!( edge.properties.get("is_boring"), - Some(&FalkorValue::FBool(false)) + Some(&FalkorValue::Bool(false)) ); } @@ -224,23 +122,23 @@ mod tests { let res = parse_type( 8, - FalkorValue::FArray(vec![ - FalkorValue::Int64(51), // node id - FalkorValue::FArray(vec![FalkorValue::Int64(0), FalkorValue::Int64(1)]), // node type - FalkorValue::FArray(vec![ - FalkorValue::FArray(vec![ - FalkorValue::Int64(0), - FalkorValue::Int64(3), - FalkorValue::Int64(15), + 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::FArray(vec![ - FalkorValue::Int64(2), - FalkorValue::Int64(2), - FalkorValue::FString("the something".to_string()), + FalkorValue::Array(vec![ + FalkorValue::I64(2), + FalkorValue::I64(2), + FalkorValue::String("the something".to_string()), ]), - FalkorValue::FArray(vec![ - FalkorValue::Int64(3), - FalkorValue::Int64(5), + FalkorValue::Array(vec![ + FalkorValue::I64(3), + FalkorValue::I64(5), FalkorValue::F64(105.5), ]), ]), @@ -251,17 +149,17 @@ mod tests { assert!(res.is_ok()); let falkor_node = res.unwrap(); - let FalkorValue::FNode(node) = falkor_node else { + 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::Int64(15))); + assert_eq!(node.properties.get("age"), Some(&FalkorValue::I64(15))); assert_eq!( node.properties.get("something_else"), - Some(&FalkorValue::FString("the something".to_string())) + Some(&FalkorValue::String("the something".to_string())) ); assert_eq!( node.properties.get(&"secs_since_login".to_string()), @@ -275,38 +173,38 @@ mod tests { let res = parse_type( 9, - FalkorValue::FArray(vec![ - FalkorValue::FArray(vec![ - FalkorValue::FArray(vec![ - FalkorValue::Int64(51), - FalkorValue::FArray(vec![FalkorValue::Int64(0)]), - FalkorValue::FArray(vec![]), + FalkorValue::Array(vec![ + FalkorValue::Array(vec![ + FalkorValue::Array(vec![ + FalkorValue::I64(51), + FalkorValue::Array(vec![FalkorValue::I64(0)]), + FalkorValue::Array(vec![]), ]), - FalkorValue::FArray(vec![ - FalkorValue::Int64(52), - FalkorValue::FArray(vec![FalkorValue::Int64(0)]), - FalkorValue::FArray(vec![]), + FalkorValue::Array(vec![ + FalkorValue::I64(52), + FalkorValue::Array(vec![FalkorValue::I64(0)]), + FalkorValue::Array(vec![]), ]), - FalkorValue::FArray(vec![ - FalkorValue::Int64(53), - FalkorValue::FArray(vec![FalkorValue::Int64(0)]), - FalkorValue::FArray(vec![]), + FalkorValue::Array(vec![ + FalkorValue::I64(53), + FalkorValue::Array(vec![FalkorValue::I64(0)]), + FalkorValue::Array(vec![]), ]), ]), - FalkorValue::FArray(vec![ - FalkorValue::FArray(vec![ - FalkorValue::Int64(100), - FalkorValue::Int64(0), - FalkorValue::Int64(51), - FalkorValue::Int64(52), - FalkorValue::FArray(vec![]), + FalkorValue::Array(vec![ + FalkorValue::Array(vec![ + FalkorValue::I64(100), + FalkorValue::I64(0), + FalkorValue::I64(51), + FalkorValue::I64(52), + FalkorValue::Array(vec![]), ]), - FalkorValue::FArray(vec![ - FalkorValue::Int64(101), - FalkorValue::Int64(1), - FalkorValue::Int64(52), - FalkorValue::Int64(53), - FalkorValue::FArray(vec![]), + FalkorValue::Array(vec![ + FalkorValue::I64(101), + FalkorValue::I64(1), + FalkorValue::I64(52), + FalkorValue::I64(53), + FalkorValue::Array(vec![]), ]), ]), ]), @@ -316,7 +214,7 @@ mod tests { assert!(res.is_ok()); let falkor_path = res.unwrap(); - let FalkorValue::FPath(path) = falkor_path else { + let FalkorValue::Path(path) = falkor_path else { panic!("Is not of type path") }; @@ -342,16 +240,16 @@ mod tests { let res = parse_type( 10, - FalkorValue::FArray(vec![ - FalkorValue::FString("key0".to_string()), - FalkorValue::FArray(vec![ - FalkorValue::Int64(2), - FalkorValue::FString("val0".to_string()), + FalkorValue::Array(vec![ + FalkorValue::String("key0".to_string()), + FalkorValue::Array(vec![ + FalkorValue::I64(2), + FalkorValue::String("val0".to_string()), ]), - FalkorValue::FString("key1".to_string()), - FalkorValue::FArray(vec![FalkorValue::Int64(3), FalkorValue::Int64(1)]), - FalkorValue::FString("key2".to_string()), - FalkorValue::FArray(vec![FalkorValue::Int64(4), FalkorValue::FBool(true)]), + 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, &mut conn, @@ -359,17 +257,17 @@ mod tests { assert!(res.is_ok()); let falkor_map = res.unwrap(); - let FalkorValue::FMap(map) = falkor_map else { + 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::FString("val0".to_string())) + Some(&FalkorValue::String("val0".to_string())) ); - assert_eq!(map.get("key1"), Some(&FalkorValue::Int64(1))); - assert_eq!(map.get("key2"), Some(&FalkorValue::FBool(true))); + assert_eq!(map.get("key1"), Some(&FalkorValue::I64(1))); + assert_eq!(map.get("key2"), Some(&FalkorValue::Bool(true))); } #[test] @@ -378,14 +276,14 @@ mod tests { let res = parse_type( 11, - FalkorValue::FArray(vec![FalkorValue::F64(102.0), FalkorValue::F64(15.2)]), + FalkorValue::Array(vec![FalkorValue::F64(102.0), FalkorValue::F64(15.2)]), &mut graph.graph_schema, &mut conn, ); assert!(res.is_ok()); let falkor_point = res.unwrap(); - let FalkorValue::FPoint(point) = falkor_point else { + let FalkorValue::Point(point) = falkor_point else { panic!("Is not of type point") }; assert_eq!(point.latitude, 102.0); From b68f8461565b09a9c707df0155086fc067f3d7a5 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 29 May 2024 17:33:41 +0300 Subject: [PATCH 39/62] No async --- Cargo.lock | 317 +---------- Cargo.toml | 14 +- src/client/asynchronous.rs | 204 -------- src/client/builder.rs | 62 +-- src/client/mod.rs | 29 +- src/connection/asynchronous.rs | 77 --- src/connection/mod.rs | 3 - src/graph/asynchronous.rs | 927 --------------------------------- src/graph/mod.rs | 4 - src/lib.rs | 6 - src/redis_ext.rs | 7 - 11 files changed, 33 insertions(+), 1617 deletions(-) delete mode 100644 src/client/asynchronous.rs delete mode 100644 src/connection/asynchronous.rs delete mode 100644 src/graph/asynchronous.rs diff --git a/Cargo.lock b/Cargo.lock index 6050686..619ec7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -28,31 +28,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.83" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" - -[[package]] -name = "async-recursion" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-trait" -version = "0.1.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" [[package]] name = "autocfg" @@ -112,11 +90,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" dependencies = [ "bytes", - "futures-core", "memchr", - "pin-project-lite", - "tokio", - "tokio-util", ] [[package]] @@ -148,7 +122,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -156,13 +130,10 @@ name = "falkordb-client-rs" version = "0.1.0" dependencies = [ "anyhow", - "async-recursion", - "futures", "log", "parking_lot", "redis", "thiserror", - "tokio", "tracing", "url-parse", ] @@ -203,67 +174,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "futures" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" - -[[package]] -name = "futures-io" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" - -[[package]] -name = "futures-sink" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" - -[[package]] -name = "futures-task" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" - -[[package]] -name = "futures-util" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" -dependencies = [ - "futures-core", - "futures-sink", - "futures-task", - "pin-project-lite", - "pin-utils", -] - [[package]] name = "getrandom" version = "0.2.15" @@ -287,12 +197,6 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -[[package]] -name = "hermit-abi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" - [[package]] name = "idna" version = "0.5.0" @@ -368,17 +272,6 @@ dependencies = [ "adler", ] -[[package]] -name = "mio" -version = "0.8.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.48.0", -] - [[package]] name = "native-tls" version = "0.2.11" @@ -397,16 +290,6 @@ dependencies = [ "tempfile", ] -[[package]] -name = "num_cpus" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" -dependencies = [ - "hermit-abi", - "libc", -] - [[package]] name = "object" version = "0.32.2" @@ -468,9 +351,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e4af0ca4f6caed20e900d564c242b8e5d4903fdacf31d3daf527b66fe6f42fb" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" dependencies = [ "lock_api", "parking_lot_core", @@ -489,7 +372,7 @@ dependencies = [ "redox_syscall", "smallvec", "thread-id", - "windows-targets 0.52.5", + "windows-targets", ] [[package]] @@ -514,12 +397,6 @@ version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - [[package]] name = "pkg-config" version = "0.3.30" @@ -546,27 +423,19 @@ dependencies = [ [[package]] name = "redis" -version = "0.25.3" +version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6472825949c09872e8f2c50bde59fcefc17748b6be5c90fd67cd8b4daca73bfd" +checksum = "e0d7a6955c7511f60f3ba9e86c6d02b3c3f144f8c24b288d1f4e18074ab8bbec" dependencies = [ - "async-trait", - "bytes", "combine", - "futures-util", "itoa", "native-tls", "percent-encoding", - "pin-project-lite", "rustls", "rustls-native-certs", "rustls-pemfile", "rustls-pki-types", "ryu", - "tokio", - "tokio-native-tls", - "tokio-rustls", - "tokio-util", "url", ] @@ -620,7 +489,7 @@ dependencies = [ "libc", "spin", "untrusted", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -639,7 +508,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -708,7 +577,7 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys 0.52.0", + "windows-sys", ] [[package]] @@ -746,16 +615,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "socket2" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "spin" version = "0.9.8" @@ -788,23 +647,23 @@ dependencies = [ "cfg-if", "fastrand", "rustix", - "windows-sys 0.52.0", + "windows-sys", ] [[package]] name = "thiserror" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" +checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.60" +version = "1.0.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" +checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", @@ -836,68 +695,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "tokio" -version = "1.37.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" -dependencies = [ - "backtrace", - "bytes", - "libc", - "mio", - "num_cpus", - "pin-project-lite", - "socket2", - "tokio-macros", - "windows-sys 0.48.0", -] - -[[package]] -name = "tokio-macros" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] -name = "tokio-rustls" -version = "0.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" -dependencies = [ - "rustls", - "rustls-pki-types", - "tokio", -] - -[[package]] -name = "tokio-util" -version = "0.7.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - [[package]] name = "tracing" version = "0.1.40" @@ -1010,37 +807,13 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets 0.52.5", -] - -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", + "windows-targets", ] [[package]] @@ -1049,46 +822,28 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.5", - "windows_aarch64_msvc 0.52.5", - "windows_i686_gnu 0.52.5", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", "windows_i686_gnullvm", - "windows_i686_msvc 0.52.5", - "windows_x86_64_gnu 0.52.5", - "windows_x86_64_gnullvm 0.52.5", - "windows_x86_64_msvc 0.52.5", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - [[package]] name = "windows_aarch64_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - [[package]] name = "windows_i686_gnu" version = "0.52.5" @@ -1101,48 +856,24 @@ version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - [[package]] name = "windows_i686_msvc" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - [[package]] name = "windows_x86_64_gnu" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - [[package]] name = "windows_x86_64_msvc" version = "0.52.5" diff --git a/Cargo.toml b/Cargo.toml index 11063df..cfa684a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,14 +5,11 @@ edition = "2021" [dependencies] -anyhow = { version = "1.0.83", default-features = false, features = ["std"] } -async-recursion = { version = "1.1.1" } -futures = { version = "0.3.30", default-features = false, optional = true } +anyhow = { version = "1.0.86", default-features = false, features = ["std"] } log = { version = "0.4.21", default-features = false } -parking_lot = { version = "0.12.2", default-features = false, features = ["deadlock_detection"] } -redis = { version = "0.25.3", default-features = false, optional = true } -thiserror = "1.0.60" -tokio = { version = "1.37.0", default-features = false, features = ["rt-multi-thread", "sync", "macros"], optional = true } +parking_lot = { version = "0.12.3", default-features = false, features = ["deadlock_detection"] } +redis = { version = "0.25.4", default-features = false, optional = true } +thiserror = "1.0.61" tracing = { version = "0.1.40", default-features = false, features = ["std", "attributes"] } url-parse = "1.0.8" @@ -20,7 +17,4 @@ url-parse = "1.0.8" default = ["redis"] native-tls = ["redis/tls-native-tls"] rustls = ["redis/tls-rustls"] -tokio = ["dep:futures", "dep:tokio", "redis/tokio-comp"] -tokio-native-tls = ["redis/tokio-native-tls-comp"] -tokio-rustls = ["redis/tokio-rustls-comp"] redis = ["dep:redis"] diff --git a/src/client/asynchronous.rs b/src/client/asynchronous.rs deleted file mode 100644 index f4ff4fa..0000000 --- a/src/client/asynchronous.rs +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{ - client::FalkorClientProvider, connection::asynchronous::BorrowedAsyncConnection, - parser::utils::string_vec_from_val, AsyncGraph, ConfigValue, FalkorAsyncConnection, - FalkorDBError, FalkorResult, FalkorValue, GraphSchema, -}; -use std::{ - collections::{HashMap, VecDeque}, - fmt::Display, - sync::Arc, - time::Duration, -}; -use tokio::sync::Mutex; - -pub(crate) struct FalkorAsyncClientInner { - _inner: Arc, - connection_pool_size: u8, - connection_pool: Arc>>, -} - -impl FalkorAsyncClientInner { - pub(crate) async fn borrow_connection(&self) -> FalkorResult { - let mut conn_pool = self.connection_pool.lock().await; - let connection = conn_pool - .pop_front() - .ok_or(FalkorDBError::EmptyConnection)?; - - Ok(BorrowedAsyncConnection { - conn: Some(connection), - conn_pool: self.connection_pool.clone(), - }) - } -} - -pub struct FalkorAsyncClient { - inner: Arc, -} - -impl FalkorAsyncClient { - pub(crate) async fn create( - client: FalkorClientProvider, - num_connections: u8, - timeout: Option, - ) -> FalkorResult { - let client = Arc::new(client); - - // Wait for all tasks to complete and collect results - let connection_pool: VecDeque<_> = - futures::future::join_all((0..num_connections).map(|_| { - let client = Arc::clone(&client); - tokio::task::spawn(async move { - client.get_async_connection(timeout).await // Replace `timeout` with your actual timeout value - }) - })) - .await - .into_iter() - .flatten() - .flatten() - .collect(); - - if connection_pool.len() != num_connections as usize { - Err(FalkorDBError::NoConnection)?; - } - - Ok(Self { - inner: Arc::new(FalkorAsyncClientInner { - _inner: client, - connection_pool_size: num_connections, - connection_pool: Arc::new(Mutex::new(connection_pool)), - }), - }) - } - - /// Get the max number of connections in the client's connection pool - pub fn connection_pool_size(&self) -> u8 { - self.inner.connection_pool_size - } - - pub(crate) async fn borrow_connection(&self) -> FalkorResult { - self.inner.borrow_connection().await - } - - /// Return a list of graphs currently residing in the database - /// - /// # Returns - /// A [`Vec`] of [`String`]s, containing the names of available graphs - pub async fn list_graphs(&self) -> FalkorResult> { - let mut conn = self.borrow_connection().await?; - conn.send_command::<&str>(None, "GRAPH.LIST", None, None) - .await - .and_then(|res| string_vec_from_val(res)) - } - - /// Return the current value of a configuration option in the database. - /// - /// # Arguments - /// * `config_Key`: A [`String`] representation of a configuration's key. - /// The config key can also be "*", which will return ALL the configuration options. - /// - /// # Returns - /// A [`HashMap`] comprised of [`String`] keys, and [`ConfigValue`] values. - pub async fn config_get( - &self, - config_key: T, - ) -> FalkorResult> { - let mut conn = self.borrow_connection().await?; - let config = conn - .send_command(None, "GRAPH.CONFIG", Some("GET"), Some(&[config_key])) - .await? - .into_vec()?; - - if config.len() == 2 { - let [key, val]: [FalkorValue; 2] = config - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - return Ok(HashMap::from([( - key.into_string()?, - ConfigValue::try_from(val)?, - )])); - } - - Ok(config - .into_iter() - .flat_map(|config| { - let [key, val]: [FalkorValue; 2] = config - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - Result::<_, FalkorDBError>::Ok((key.into_string()?, ConfigValue::try_from(val)?)) - }) - .collect::>()) - } - - /// Return the current value of a configuration option in the database. - /// - /// # Arguments - /// * `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. - pub async fn config_set, C: Into>( - &self, - config_key: T, - value: C, - ) -> FalkorResult { - self.borrow_connection() - .await? - .send_command( - None, - "GRAPH.CONFIG", - Some("SET"), - Some(&[config_key.into(), value.into()]), - ) - .await - } - - /// Opens a graph context for queries and operations - /// - /// # Arguments - /// * `graph_name`: A string identifier of the graph to open. - /// - /// # Returns - /// a [`SyncGraph`] object, allowing various graph operations. - pub fn select_graph( - &self, - graph_name: T, - ) -> AsyncGraph { - AsyncGraph { - client: self.inner.clone(), - graph_name: graph_name.to_string(), - graph_schema: GraphSchema::new(graph_name.to_string()), // Required for requesting refreshes - } - } - - /// Copies an entire graph and returns the [`AsyncGraph`] for the new copied graph. - /// - /// # Arguments - /// * `graph_to_clone`: A string identifier of the graph to copy. - /// * `new_graph_name`: The name to give the new graph. - /// - /// # Returns - /// If successful, will return the new [`AsyncGraph`] object. - pub async fn copy_graph( - &self, - graph_to_clone: &str, - new_graph_name: &str, - ) -> FalkorResult { - self.borrow_connection() - .await? - .send_command( - Some(graph_to_clone), - "GRAPH.COPY", - None, - Some(&[new_graph_name]), - ) - .await?; - Ok(self.select_graph(new_graph_name)) - } -} diff --git a/src/client/builder.rs b/src/client/builder.rs index 9ad20f9..8930960 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -9,10 +9,7 @@ use crate::{ }; use std::time::Duration; -#[cfg(feature = "tokio")] -use crate::FalkorAsyncClient; - -/// A Builder-pattern implementation struct for creating a new Falkor client, sync or async. +/// A Builder-pattern implementation struct for creating a new Falkor client. pub struct FalkorClientBuilder { connection_info: Option, timeout: Option, @@ -124,30 +121,6 @@ impl FalkorClientBuilder<'S'> { } } -#[cfg(feature = "tokio")] -impl FalkorClientBuilder<'A'> { - pub fn new_async() -> Self { - FalkorClientBuilder { - connection_info: None, - num_connections: 4, - timeout: None, - } - } - - pub async fn build(self) -> FalkorResult { - let connection_info = self - .connection_info - .unwrap_or("falkor://127.0.0.1:6379".try_into()?); - - FalkorAsyncClient::create( - Self::get_client(connection_info.clone())?, - self.num_connections, - self.timeout, - ) - .await - } -} - #[cfg(test)] mod tests { use super::*; @@ -207,37 +180,4 @@ mod tests { .build(); assert!(impossible_client.is_err()); } - - #[cfg(all(feature = "tokio", feature = "redis"))] - #[tokio::test] - async fn test_async_builder() { - let conneciton_info = "falkor://127.0.0.1:6379".try_into(); - assert!(conneciton_info.is_ok()); - - assert!(FalkorClientBuilder::new_async() - .with_num_connections(4) - .with_connection_info(conneciton_info.unwrap()) - .build() - .await - .is_ok()); - } - - #[cfg(feature = "tokio")] - #[tokio::test] - #[ignore] - async fn test_async_timeout() { - { - let client = FalkorClientBuilder::new_async() - .with_timeout(Duration::from_millis(100)) - .build() - .await; - assert!(client.is_ok()); - } - - let impossible_client = FalkorClientBuilder::new_async() - .with_timeout(Duration::from_nanos(10)) - .build() - .await; - assert!(impossible_client.is_err()); - } } diff --git a/src/client/mod.rs b/src/client/mod.rs index fecbb78..1559f5f 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -3,16 +3,12 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::FalkorSyncConnection; -use anyhow::Result; +use crate::{connection::blocking::FalkorSyncConnection, FalkorDBError, FalkorResult}; use std::time::Duration; pub(crate) mod blocking; pub(crate) mod builder; -#[cfg(feature = "tokio")] -pub(crate) mod asynchronous; - pub(crate) enum FalkorClientProvider { #[cfg(feature = "redis")] Redis(redis::Client), @@ -22,31 +18,14 @@ impl FalkorClientProvider { pub(crate) fn get_connection( &self, connection_timeout: Option, - ) -> Result { + ) -> FalkorResult { Ok(match self { #[cfg(feature = "redis")] FalkorClientProvider::Redis(redis_client) => connection_timeout .map(|timeout| redis_client.get_connection_with_timeout(timeout)) - .unwrap_or_else(|| redis_client.get_connection())? + .unwrap_or_else(|| redis_client.get_connection()) + .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))? .into(), }) } - - #[cfg(feature = "tokio")] - pub(crate) async fn get_async_connection( - &self, - connection_timeout: Option, - ) -> Result { - Ok(match self { - FalkorClientProvider::Redis(redis_client) => match connection_timeout { - Some(timeout) => { - redis_client - .get_multiplexed_async_connection_with_timeouts(timeout, timeout) - .await? - } - None => redis_client.get_multiplexed_tokio_connection().await?, - } - .into(), - }) - } } diff --git a/src/connection/asynchronous.rs b/src/connection/asynchronous.rs deleted file mode 100644 index b5a7e57..0000000 --- a/src/connection/asynchronous.rs +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{FalkorDBError, FalkorResult, FalkorValue}; -use std::{collections::VecDeque, fmt::Display, sync::Arc}; -use tokio::sync::Mutex; - -#[derive(Clone)] -pub enum FalkorAsyncConnection { - #[cfg(feature = "redis")] - Redis(redis::aio::MultiplexedConnection), -} - -/// A container for a connection that is borrowed from the pool. -/// Upon going out of scope, it will return the connection to the pool. -/// -/// This is publicly exposed for user-implementations of [`FalkorParsable`](crate::FalkorParsable) -pub struct BorrowedAsyncConnection { - pub(crate) conn: Option, - pub(crate) conn_pool: Arc>>, -} - -impl BorrowedAsyncConnection { - pub(crate) fn as_inner(&mut self) -> Result<&mut FalkorAsyncConnection, FalkorDBError> { - self.conn.as_mut().ok_or(FalkorDBError::EmptyConnection) - } - - pub(crate) async fn send_command( - &mut self, - graph_name: Option<&str>, - command: &str, - subcommand: Option<&str>, - params: Option<&[P]>, - ) -> FalkorResult { - Ok(match self.as_inner()? { - #[cfg(feature = "redis")] - FalkorAsyncConnection::Redis(redis_conn) => { - let mut cmd = redis::cmd(command); - cmd.arg(subcommand); - cmd.arg(graph_name); - if let Some(params) = params { - for param in params { - cmd.arg(param.to_string()); - } - } - redis::FromRedisValue::from_owned_redis_value( - redis_conn - .send_packed_command(&cmd) - .await - .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, - ) - .map_err(|err| FalkorDBError::RedisParsingError(err.to_string()))? - } - }) - } -} - -impl Drop for BorrowedAsyncConnection { - fn drop(&mut self) { - if let Some(conn) = self.conn.take() { - let handle = tokio::runtime::Handle::try_current(); - match handle { - Ok(handle) => { - let pool = self.conn_pool.clone(); - handle.spawn_blocking(move || { - pool.blocking_lock().push_back(conn); - }); - } - Err(_) => { - self.conn_pool.blocking_lock().push_back(conn); - } - } - } - } -} diff --git a/src/connection/mod.rs b/src/connection/mod.rs index e4630fb..2d11cf3 100644 --- a/src/connection/mod.rs +++ b/src/connection/mod.rs @@ -4,6 +4,3 @@ */ pub(crate) mod blocking; - -#[cfg(feature = "tokio")] -pub(crate) mod asynchronous; diff --git a/src/graph/asynchronous.rs b/src/graph/asynchronous.rs deleted file mode 100644 index 000e943..0000000 --- a/src/graph/asynchronous.rs +++ /dev/null @@ -1,927 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{ - client::asynchronous::FalkorAsyncClientInner, - graph::utils::{construct_query, generate_procedure_call}, - parser::utils::{parse_header, parse_result_set}, - Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorDBError, FalkorIndex, - FalkorParsable, FalkorResponse, FalkorValue, GraphSchema, IndexType, ResultSet, SlowlogEntry, -}; -use anyhow::Result; -use std::{collections::HashMap, fmt::Display, sync::Arc}; -use tokio::sync::Mutex; - -/// The main graph API, this allows the user to perform graph operations while exposing as little details as possible. -/// -/// # Thread Safety -/// This struct is fully thread safe, it can be cloned and passed within threads without constraints, -/// Its API uses only immutable references -#[derive(Clone)] -pub struct AsyncGraph { - pub(crate) client: Arc, - pub(crate) graph_name: String, - /// Provides user with access to the current graph schema, - /// which contains a safe cache of id to labels/properties/relationship maps - pub graph_schema: GraphSchema, -} - -impl AsyncGraph { - /// Returns the name of the graph for which this API performs operations. - /// - /// # Returns - /// The graph name as a string slice, without cloning. - pub fn graph_name(&self) -> &str { - self.graph_name.as_str() - } - - async fn send_command( - &self, - command: &str, - subcommand: Option<&str>, - params: Option<&[String]>, - ) -> Result { - let mut conn = self.client.borrow_connection().await?; - conn.send_command(Some(self.graph_name.as_str()), command, subcommand, params) - .await - } - - /// Deletes the graph stored in the database, and drop all the schema caches. - /// NOTE: This still maintains the graph API, operations are still viable. - pub async fn delete(&mut self) -> Result<()> { - self.send_command("GRAPH.DELETE", None, None).await?; - self.graph_schema.clear(); - Ok(()) - } - - /// Retrieves the slowlog data, which contains info about the N slowest queries. - /// - /// # Returns - /// A [`Vec`] of [`SlowlogEntry`], providing information about each query. - pub async fn slowlog(&self) -> Result> { - let res = self - .send_command("GRAPH.SLOWLOG", None, None) - .await? - .into_vec()?; - - if res.is_empty() { - return Ok(vec![]); - } - - Ok(res.into_iter().flat_map(SlowlogEntry::try_from).collect()) - } - - /// Resets the slowlog, all query time data will be cleared. - pub async fn slowlog_reset(&self) -> Result { - self.send_command("GRAPH.SLOWLOG", None, Some(&["RESET".to_string()])) - .await - } - - /// Returns an [`ExecutionPlan`] object for the selected query, - /// showing how long each step took to perform. - /// This function variant allows adding extra parameters after the query - /// - /// # Arguments - /// * `query_string`: The query to profile - /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. - /// - /// # Returns - /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. - pub async fn profile_with_params( - &self, - query_string: Q, - params: Option<&HashMap>, - ) -> Result { - let query = construct_query(query_string, params); - - ExecutionPlan::try_from( - self.send_command("GRAPH.PROFILE", None, Some(&[query])) - .await?, - ) - .map_err(Into::into) - } - - /// Returns an [`ExecutionPlan`] object for the selected query, - /// showing how long each step took to perform. - /// - /// # Arguments - /// * `query_string`: The query to profile - /// - /// # Returns - /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. - pub async fn profile( - &self, - query_string: Q, - ) -> Result { - self.profile_with_params::(query_string, None) - .await - } - - /// Returns an [`ExecutionPlan`] object for the selected query, - /// showing the internals steps the database will go through to perform the query. - /// This function variant allows adding extra parameters after the query - /// - /// # Arguments - /// * `query_string`: The query to explain - /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. - /// - /// # Returns - /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. - pub async fn explain_with_params( - &self, - query_string: Q, - params: Option<&HashMap>, - ) -> Result { - let query = construct_query(query_string, params); - ExecutionPlan::try_from( - self.send_command("GRAPH.EXPLAIN", None, Some(&[query])) - .await?, - ) - .map_err(Into::into) - } - - /// Returns an [`ExecutionPlan`] object for the selected query, - /// showing the internals steps the database will go through to perform the query. - /// - /// # Arguments - /// * `query_string`: The query to explain - /// - /// # Returns - /// An [`ExecutionPlan`], which can provide info about each step, or a plaintext explanation of the whole thing for printing. - pub async fn explain( - &self, - query_string: Q, - ) -> Result { - self.explain_with_params::(query_string, None) - .await - } - - async fn query_inner_with_timeout( - &mut self, - command: &str, - query_string: Q, - params: Option<&HashMap>, - timeout: i64, - ) -> Result> { - let query = construct_query(query_string, params); - let mut conn = self.client.borrow_connection().await?; - - let [header, data, stats]: [FalkorValue; 3] = conn - .send_command( - Some(self.graph_name.as_str()), - command, - None, - Some(&[ - query.as_str(), - "--compact", - format!("timeout {timeout}").as_str(), - ]), - ) - .await? - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let header_keys = parse_header(header)?; - let conn = Arc::new(Mutex::new(conn)); - FalkorResponse::from_response_with_headers( - parse_result_set(data, &mut self.graph_schema, conn)?, - header_keys, - stats, - ) - .map_err(Into::into) - } - - async fn query_inner( - &mut self, - command: &str, - query_string: Q, - params: Option<&HashMap>, - ) -> Result> { - let query = construct_query(query_string, params); - let mut conn = self.client.borrow_connection().await?; - - let res = conn - .send_command( - Some(self.graph_name.as_str()), - command, - None, - Some(&[query, "--compact".to_string()]), - ) - .await? - .into_vec()?; - - match res.len() { - 1 => { - let stats = res - .into_iter() - .next() - .ok_or(FalkorDBError::ParsingArrayToStructElementCount)?; - - FalkorResponse::from_response(None, vec![], stats) - } - 2 => { - let [header, stats]: [FalkorValue; 2] = res - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - FalkorResponse::from_response(Some(header), vec![], stats) - } - 3 => { - let [header, data, stats]: [FalkorValue; 3] = res - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let header_keys = parse_header(header)?; - let conn = Arc::new(Mutex::new(conn)); - FalkorResponse::from_response_with_headers( - parse_result_set(data, &mut self.graph_schema, conn, &header_keys)?, - header_keys, - stats, - ) - } - _ => Err(FalkorDBError::ParsingArrayToStructElementCount), - } - .map_err(Into::into) - } - - /// Run a query on the graph - /// - /// # Arguments - /// * `query_string`: The query to run - /// - /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query - pub async fn query( - &mut self, - query_string: Q, - ) -> Result> { - self.query_inner::("GRAPH.QUERY", query_string, None) - .await - } - - /// Run a query on the graph, but abort it if it exceeds the timeout - /// - /// # Arguments - /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. - /// - /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query - pub async fn query_with_timeout( - &mut self, - query_string: Q, - timeout: i64, - ) -> Result> { - self.query_inner_with_timeout::("GRAPH.QUERY", query_string, None, timeout) - .await - } - - /// Run a query on the graph - /// This function variant allows adding extra parameters after the query - /// - /// # Arguments - /// * `query_string`: The query to run - /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. - /// - /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub async fn query_with_params( - &mut self, - query_string: Q, - params: &HashMap, - ) -> Result> { - self.query_inner("GRAPH.QUERY", query_string, Some(params)) - .await - } - - /// Run a query on the graph but abort it if it exceeds the timeout - /// This function variant allows adding extra parameters after the query - /// - /// # Arguments - /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. - /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. - /// - /// # Returns - /// A [`QueryResult`] object, containing the headers, statistics and the result set for the query - pub async fn query_with_params_and_timeout( - &mut self, - query_string: Q, - timeout: i64, - params: &HashMap, - ) -> Result> { - self.query_inner_with_timeout("GRAPH.QUERY", query_string, Some(params), timeout) - .await - } - - /// Run a query on the graph - /// Read-only queries are more limited with the operations they are allowed to perform. - /// - /// # Arguments - /// * `query_string`: The query to run - /// - /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub async fn query_readonly( - &mut self, - query_string: Q, - ) -> Result> { - self.query_inner::("GRAPH.QUERY_RO", query_string, None) - .await - } - - /// Run a query on the graph, but abort it if it exceeds the timeout - /// Read-only queries are more limited with the operations they are allowed to perform. - /// - /// # Arguments - /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. - /// - /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub async fn query_readonly_with_timeout( - &mut self, - query_string: Q, - timeout: i64, - ) -> Result> { - self.query_inner_with_timeout::( - "GRAPH.QUERY_RO", - query_string, - None, - timeout, - ) - .await - } - - /// Run a read-only query on the graph - /// Read-only queries are more limited with the operations they are allowed to perform. - /// This function variant allows adding extra parameters after the query - /// - /// # Arguments - /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. - /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. - /// - /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub async fn query_readonly_with_params( - &mut self, - query_string: Q, - params: &HashMap, - ) -> Result> { - self.query_inner("GRAPH.QUERY_RO", query_string, Some(params)) - .await - } - - /// Run a read-only query on the graph, but abort it if it exceeds the timeout - /// Read-only queries are more limited with the operations they are allowed to perform. - /// This function variant allows adding extra parameters after the query - /// - /// # Arguments - /// * `query_string`: The query to run - /// * `timeout`: Specify how long should the query run before aborting. - /// * `params`: a map of parameters and values, note that all keys should be of the same type, and all values should be of the same type. - /// - /// # Returns - /// A [`FalkorResponse`] object, containing the headers, statistics and the result set for the query - pub async fn query_readonly_with_params_and_timeout( - &mut self, - query_string: Q, - params: &HashMap, - timeout: i64, - ) -> Result> { - self.query_inner_with_timeout("GRAPH.QUERY_RO", query_string, Some(params), timeout) - .await - } - - /// Run a query which calls a procedure on the graph, read-only, or otherwise. - /// Read-only queries are more limited with the operations they are allowed to perform. - /// This function allows adding extra parameters after the query, and adding a YIELD block afterward - /// - /// # Arguments - /// * `procedure`: The procedure to call - /// * `args`: An optional slice of strings containing the parameters. - /// * `yields`: The optional yield block arguments. - /// * `read_only`: Whether this procedure is read-only. - /// * `timeout`: If provided, the query will abort if overruns the timeout. - /// - /// # Returns - /// A caller-provided type which implements [`FalkorParsableAsync`] - pub async fn call_procedure( - &mut self, - procedure: C, - args: Option<&[&str]>, - yields: Option<&[&str]>, - read_only: bool, - ) -> Result

{ - let (query_string, params) = generate_procedure_call(procedure, args, yields); - let query = construct_query(query_string, params.as_ref()); - let mut conn = self.client.borrow_connection().await?; - - let res = conn - .send_command( - Some(self.graph_name.as_str()), - if read_only { - "GRAPH.QUERY_RO" - } else { - "GRAPH.QUERY" - }, - None, - Some(&[query, "--compact".to_string()]), - ) - .await?; - - let conn = Arc::new(Mutex::new(conn)); - P::from_falkor_value(res, &mut self.graph_schema, conn) - } - - /// Run a query which calls a procedure on the graph, read-only, or otherwise. - /// Read-only queries are more limited with the operations they are allowed to perform. - /// This function allows adding extra parameters after the query, and adding a YIELD block afterward - /// This function will cause the query to abort if it exceeds a certain timeout - /// - /// # Arguments - /// * `procedure`: The procedure to call - /// * `args`: An optional slice of strings containing the parameters. - /// * `yields`: The optional yield block arguments. - /// * `read_only`: Whether this procedure is read-only. - /// * `timeout`: If provided, the query will abort if overruns the timeout. - /// - /// # Returns - /// A caller-provided type which implements [`FalkorParsableAsync`] - pub async fn call_procedure_with_timeout( - &mut self, - procedure: C, - args: Option<&[&str]>, - yields: Option<&[&str]>, - read_only: bool, - timeout: i64, - ) -> Result

{ - let (query_string, params) = generate_procedure_call(procedure, args, yields); - let query = construct_query(query_string, params.as_ref()); - let mut conn = self.client.borrow_connection().await?; - - let res = conn - .send_command( - Some(self.graph_name.as_str()), - if read_only { - "GRAPH.QUERY_RO" - } else { - "GRAPH.QUERY" - }, - None, - Some(&[ - query.as_str(), - "--compact", - format!("timeout {timeout}").as_str(), - ]), - ) - .await?; - - let conn = Arc::new(Mutex::new(conn)); - P::from_falkor_value(res, &mut self.graph_schema, conn) - } - - /// Calls the DB.INDICES procedure on the graph, returning all the indexing methods currently used - /// - /// # Returns - /// A [`Vec`] of [`FalkorIndex`] - pub async fn list_indices(&mut self) -> Result>> { - let [header, indices, stats]: [FalkorValue; 3] = self - .call_procedure::<&str, FalkorValue>("DB.INDEXES", None, None, false) - .await? - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let conn = Arc::new(Mutex::new(self.client.borrow_connection().await?)); - FalkorResponse::from_response( - Some(header), - indices - .into_vec()? - .into_iter() - .flat_map(|index| { - let conn = Arc::clone(&conn); - FalkorIndex::from_falkor_value(index, &mut self.graph_schema, conn) - }) - .collect(), - stats, - ) - .map_err(Into::into) - } - - pub async fn create_index( - &mut self, - index_field_type: IndexType, - entity_type: EntityType, - label: L, - properties: &[P], - options: Option<&HashMap>, - ) -> Result> { - // Create index from these properties - let properties_string = properties - .iter() - .map(|element| format!("l.{}", element.to_string())) - .collect::>() - .join(", "); - - let pattern = match entity_type { - EntityType::Node => format!("(l:{})", label.to_string()), - EntityType::Edge => format!("()-[l:{}]->()", label.to_string()), - }; - - let idx_type = match index_field_type { - IndexType::Range => "", - IndexType::Vector => "VECTOR ", - IndexType::Fulltext => "FULLTEXT ", - } - .to_string(); - - let options_string = options - .map(|hashmap| { - hashmap - .iter() - .map(|(key, val)| format!("'{key}':'{val}'")) - .collect::>() - .join(",") - }) - .map(|options_string| format!(" OPTIONS {{ {} }}", options_string)) - .unwrap_or_default(); - - self.query(format!( - "CREATE {idx_type}INDEX FOR {pattern} ON ({}){}", - properties_string, options_string - )) - .await - } - - /// Drop an existing index, by specifying its type, entity, label and specific properties - /// - /// # Arguments - /// * `index_field_type` - pub async fn drop_index( - &mut self, - index_field_type: IndexType, - entity_type: EntityType, - label: L, - properties: &[P], - ) -> Result> { - let properties_string = properties - .iter() - .map(|element| format!("e.{}", element.to_string())) - .collect::>() - .join(", "); - - let pattern = match entity_type { - EntityType::Node => format!("(e:{})", label.to_string()), - EntityType::Edge => format!("()-[e:{}]->()", label.to_string()), - }; - - let idx_type = match index_field_type { - IndexType::Range => "", - IndexType::Vector => "VECTOR", - IndexType::Fulltext => "FULLTEXT", - } - .to_string(); - - self.query(format!( - "DROP {idx_type} INDEX for {pattern} ON ({})", - properties_string - )) - .await - } - - /// Calls the DB.CONSTRAINTS procedure on the graph, returning an array of the graph's constraints - /// - /// # 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 - pub async fn list_constraints(&mut self) -> Result>> { - let mut conn = self.client.borrow_connection().await?; - let [header, query_res, stats]: [FalkorValue; 3] = self - .call_procedure::<&str, FalkorValue>("DB.CONSTRAINTS", None, None, false) - .await? - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - - let conn = Arc::new(Mutex::new(conn)); - FalkorResponse::from_response( - Some(header), - query_res - .into_vec()? - .into_iter() - .flat_map(|item| Constraint::from_falkor_value(item, &mut self.graph_schema, conn)) - .collect(), - stats, - ) - .map_err(Into::into) - } - - /// Creates a new constraint for this graph, making the provided properties mandatory - /// - /// # Arguments - /// * `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. - pub async fn create_mandatory_constraint( - &self, - entity_type: EntityType, - label: &str, - properties: &[&str], - ) -> Result { - let mut params = Vec::with_capacity(5 + properties.len()); - params.extend([ - "MANDATORY".to_string(), - entity_type.to_string(), - label.to_string(), - "PROPERTIES".to_string(), - properties.len().to_string(), - ]); - params.extend(properties.iter().map(|property| property.to_string())); - - self.send_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) - .await - } - - /// Creates a new constraint for this graph, making the provided properties unique - /// - /// # Arguments - /// * `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. - pub async fn create_unique_constraint( - &mut self, - entity_type: EntityType, - label: String, - properties: &[P], - ) -> Result { - self.create_index( - IndexType::Range, - entity_type, - label.as_str(), - properties, - None, - ) - .await?; - - let mut params: Vec = Vec::with_capacity(5 + properties.len()); - params.extend([ - "UNIQUE".to_string(), - entity_type.to_string(), - label.to_string(), - "PROPERTIES".to_string(), - properties.len().to_string(), - ]); - params.extend(properties.iter().map(|property| property.to_string())); - - // create constraint using index - self.send_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) - .await - } - - /// Drop an existing constraint from the graph - /// - /// # Arguments - /// * `constraint_type`: Which type of constraint to remove. - /// * `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. - pub async fn drop_constraint( - &self, - constraint_type: ConstraintType, - entity_type: EntityType, - label: L, - properties: &[P], - ) -> Result { - let mut params = Vec::with_capacity(5 + properties.len()); - params.extend([ - constraint_type.to_string(), - entity_type.to_string(), - label.to_string(), - "PROPERTIES".to_string(), - properties.len().to_string(), - ]); - params.extend(properties.iter().map(|property| property.to_string())); - - self.send_command("GRAPH.CONSTRAINT", Some("DROP"), Some(params.as_slice())) - .await - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{test_utils::open_test_graph_async, IndexType}; - - #[tokio::test(flavor = "multi_thread")] - async fn test_create_drop_index_async() { - let mut graph = open_test_graph_async("test_create_drop_index_async").await; - graph - .inner - .create_index( - IndexType::Fulltext, - EntityType::Node, - "actor".to_string(), - &["Hello"], - None, - ) - .await - .expect("Could not create index"); - - let indices = graph - .inner - .list_indices() - .await - .expect("Could not list indices"); - - assert_eq!(indices.data.len(), 2); - assert_eq!( - indices.data[0].field_types["Hello"], - vec![IndexType::Fulltext] - ); - - graph - .inner - .drop_index( - IndexType::Fulltext, - EntityType::Node, - "actor".to_string(), - &["Hello"], - ) - .await - .expect("Could not drop index"); - } - - #[tokio::test(flavor = "multi_thread")] - async fn test_list_indices_async() { - let mut graph = open_test_graph_async("test_list_indices_async").await; - let indices = graph - .inner - .list_indices() - .await - .expect("Could not list indices"); - - assert_eq!(indices.data.len(), 1); - assert_eq!(indices.data[0].entity_type, EntityType::Node); - assert_eq!(indices.data[0].index_label, "actor".to_string()); - assert_eq!(indices.data[0].field_types.len(), 2); - assert_eq!( - indices.data[0].field_types, - HashMap::from([ - ("age".to_string(), vec![IndexType::Range]), - ("name".to_string(), vec![IndexType::Fulltext]) - ]) - ); - - graph.inner.delete().await.ok(); - } - - #[tokio::test(flavor = "multi_thread")] - async fn test_create_drop_mandatory_constraint_async() { - let graph = open_test_graph_async("test_mandatory_constraint_async").await; - - graph - .inner - .create_mandatory_constraint(EntityType::Edge, "act", &["hello", "goodbye"]) - .await - .expect("Could not create constraint"); - - graph - .inner - .drop_constraint( - ConstraintType::Mandatory, - EntityType::Edge, - "act", - &["hello", "goodbye"], - ) - .await - .expect("Could not drop constraint"); - } - - #[tokio::test(flavor = "multi_thread")] - async fn test_create_drop_unique_constraint_async() { - let mut graph = open_test_graph_async("test_unique_constraint_async").await; - - graph - .inner - .create_unique_constraint( - EntityType::Node, - "actor".to_string(), - &["first_name", "last_name"], - ) - .await - .expect("Could not create constraint"); - - graph - .inner - .drop_constraint( - ConstraintType::Unique, - EntityType::Node, - "actor", - &["first_name", "last_name"], - ) - .await - .expect("Could not drop constraint"); - } - - #[tokio::test(flavor = "multi_thread")] - async fn test_list_constraints_async() { - let mut graph = open_test_graph_async("test_list_constraint_async").await; - - graph - .inner - .create_unique_constraint( - EntityType::Node, - "actor".to_string(), - &["first_name", "last_name"], - ) - .await - .expect("Could not create constraint"); - - let constraints = graph - .inner - .list_constraints() - .await - .expect("Could not list constraints"); - assert_eq!(constraints.data.len(), 1); - } - - #[tokio::test(flavor = "multi_thread")] - async fn test_slowlog_async() { - let mut graph = open_test_graph_async("test_slowlog_async").await; - - graph - .inner - .query("UNWIND range(0, 500) AS x RETURN x") - .await - .expect("Could not generate the fast query"); - graph - .inner - .query("UNWIND range(0, 100000) AS x RETURN x") - .await - .expect("Could not generate the slow query"); - - let slowlog = graph - .inner - .slowlog() - .await - .expect("Could not get slowlog entries"); - - assert_eq!(slowlog.len(), 2); - assert_eq!( - slowlog[0].arguments, - "UNWIND range(0, 500) AS x RETURN x".to_string() - ); - assert_eq!( - slowlog[1].arguments, - "UNWIND range(0, 100000) AS x RETURN x".to_string() - ); - - graph - .inner - .slowlog_reset() - .await - .expect("Could not reset slowlog memory"); - let slowlog_after_reset = graph - .inner - .slowlog() - .await - .expect("Could not get slowlog entries after reset"); - assert!(slowlog_after_reset.is_empty()); - } - - #[tokio::test(flavor = "multi_thread")] - async fn test_explain_async() { - let graph = open_test_graph_async("test_explain_async").await; - - let execution_plan = graph.inner.explain("MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100").await.expect("Could not create execution plan"); - assert_eq!(execution_plan.steps().len(), 7); - assert_eq!( - execution_plan.text(), - "\nResults\n Limit\n Aggregate\n Filter\n Node By Index Scan | (b:actor)\n Project\n Node By Label Scan | (a:actor)" - ); - } - - #[tokio::test(flavor = "multi_thread")] - async fn test_profile_async() { - let graph = open_test_graph_async("test_profile_async").await; - - let execution_plan = graph - .inner - .profile("UNWIND range(0, 1000) AS x RETURN x") - .await - .expect("Could not generate the query"); - - let steps = execution_plan.steps().to_vec(); - assert_eq!(steps.len(), 3); - - let expected = vec!["Results", "Project", "Unwind"]; - for (step, expected) in steps.into_iter().zip(expected) { - assert!(step.starts_with(expected)); - assert!(step.ends_with("ms")); - } - } -} diff --git a/src/graph/mod.rs b/src/graph/mod.rs index 39712e2..eb20df3 100644 --- a/src/graph/mod.rs +++ b/src/graph/mod.rs @@ -4,8 +4,4 @@ */ pub(crate) mod blocking; - -#[cfg(feature = "tokio")] -pub(crate) mod asynchronous; - pub(crate) mod query_builder; diff --git a/src/lib.rs b/src/lib.rs index 14adc8c..7765911 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,12 +49,6 @@ pub use value::{ FalkorValue, }; -#[cfg(feature = "tokio")] -pub use { - client::asynchronous::FalkorAsyncClient, connection::asynchronous::FalkorAsyncConnection, - graph::asynchronous::AsyncGraph, -}; - #[cfg(test)] pub(crate) mod test_utils { use super::*; diff --git a/src/redis_ext.rs b/src/redis_ext.rs index d639b70..13474a2 100644 --- a/src/redis_ext.rs +++ b/src/redis_ext.rs @@ -16,13 +16,6 @@ impl From for FalkorSyncConnection { } } -#[cfg(feature = "tokio")] -impl From for crate::FalkorAsyncConnection { - fn from(value: redis::aio::MultiplexedConnection) -> Self { - Self::Redis(value) - } -} - impl From for FalkorConnectionInfo { fn from(value: redis::ConnectionInfo) -> Self { Self::Redis(value) From ded8220eeae16bfd297968db09e1d397880c7d6a Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 29 May 2024 21:01:15 +0300 Subject: [PATCH 40/62] Execution plan --- Cargo.lock | 1 + Cargo.toml | 1 + src/error/mod.rs | 8 ++ src/graph/blocking.rs | 19 ++- src/response/execution_plan.rs | 238 ++++++++++++++++++++++++++++++--- 5 files changed, 242 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 619ec7e..a685d9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -133,6 +133,7 @@ dependencies = [ "log", "parking_lot", "redis", + "regex", "thiserror", "tracing", "url-parse", diff --git a/Cargo.toml b/Cargo.toml index cfa684a..5505d93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ anyhow = { version = "1.0.86", default-features = false, features = ["std"] } log = { version = "0.4.21", default-features = false } parking_lot = { version = "0.12.3", default-features = false, features = ["deadlock_detection"] } redis = { version = "0.25.4", default-features = false, optional = true } +regex = { version = "1.10.4", default-features = false, features = ["std", "perf", "unicode-bool", "unicode-perl"] } thiserror = "1.0.61" tracing = { version = "0.1.40", default-features = false, features = ["std", "attributes"] } url-parse = "1.0.8" diff --git a/src/error/mod.rs b/src/error/mod.rs index e5e18c8..8b9a338 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -27,6 +27,14 @@ pub enum FalkorDBError { /// 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( + "An error occurred when dealing with reference counts or RefCells, perhaps mutual borrows?" + )] + RefCountBooBoo, + /// The execution plan did not adhere to usual strucutre, and could not be parsed + #[error("The execution plan did not adhere to usual strucutre, and could not be parsed")] + CorruptExecutionPlan, /// The number of connections for the client has to be between 1 and 32 #[error("The number of connections for the client has to be between 1 and 32")] InvalidConnectionPoolSize, diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 4d96fc4..1f64d59 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -543,9 +543,12 @@ mod tests { let mut graph = open_test_graph("test_explain"); let execution_plan = graph.inner.explain("MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100").perform().expect("Could not create execution plan"); - assert_eq!(execution_plan.steps().len(), 7); + assert_eq!(execution_plan.plan().len(), 7); + assert!(execution_plan.operations().get("Aggregate").is_some()); + assert_eq!(execution_plan.operations()["Aggregate"].len(), 1); + assert_eq!( - execution_plan.text(), + execution_plan.string_representation(), "\nResults\n Limit\n Aggregate\n Filter\n Node By Index Scan | (b:actor)\n Project\n Node By Label Scan | (a:actor)" ); } @@ -560,13 +563,15 @@ mod tests { .perform() .expect("Could not generate the query"); - let steps = execution_plan.steps().to_vec(); - assert_eq!(steps.len(), 3); + assert_eq!(execution_plan.plan().len(), 3); let expected = vec!["Results", "Project", "Unwind"]; - for (step, expected) in steps.into_iter().zip(expected) { - assert!(step.starts_with(expected)); - assert!(step.ends_with("ms")); + let mut current_rc = execution_plan.operation_tree().clone(); + for step in expected { + assert_eq!(current_rc.name, step); + if step != "Unwind" { + current_rc = current_rc.children[0].clone(); + } } } } diff --git a/src/response/execution_plan.rs b/src/response/execution_plan.rs index 15989c8..f6ef314 100644 --- a/src/response/execution_plan.rs +++ b/src/response/execution_plan.rs @@ -3,24 +3,162 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{FalkorDBError, FalkorValue}; +use crate::{FalkorDBError, FalkorResult, FalkorValue}; +use regex::Regex; +use std::cell::RefCell; +use std::cmp::Ordering; +use std::collections::{HashMap, VecDeque}; +use std::ops::Not; +use std::rc::Rc; -/// An execution plan, storing both the specific string details for each step, and also a formatted plaintext string for pretty display +#[derive(Debug)] +struct IntermediateOperation { + name: String, + args: Option>, + records_produced: Option, + execution_time: Option, + depth: usize, + children: Vec>>, +} + +impl IntermediateOperation { + fn new( + depth: usize, + operation_string: &str, + ) -> FalkorResult { + let mut args = operation_string.split('|').collect::>(); + let name = args + .pop_front() + .ok_or(FalkorDBError::CorruptExecutionPlan)? + .trim(); + + let (records_produced, execution_time) = match args.pop_back() { + Some(last_arg) if last_arg.contains("Records produced") => ( + Regex::new(r"Records produced: (\d+)") + .map_err(|_| FalkorDBError::ParsingError)? + .captures(last_arg.trim()) + .and_then(|cap| cap.get(1)) + .and_then(|m| m.as_str().parse().ok()), + Regex::new(r"Execution time: (\d+\.\d+) ms") + .map_err(|_| FalkorDBError::ParsingError)? + .captures(last_arg.trim()) + .and_then(|cap| cap.get(1)) + .and_then(|m| m.as_str().parse().ok()), + ), + Some(last_arg) => { + args.push_back(last_arg); + (None, None) + } + None => (None, None), + }; + + Ok(Self { + name: name.to_string(), + args: args + .is_empty() + .not() + .then(|| args.into_iter().map(ToString::to_string).collect()), + records_produced, + execution_time, + depth, + children: vec![], + }) + } +} + +/// A graph operation, with its statistics if available, and pointers to its child operations +#[derive(Debug, Default, Clone, PartialEq)] +pub struct Operation { + /// The operation name, or string representation + pub name: String, + /// All arguments following + pub args: Option>, + /// The amount of records produced by this specific operation(regardless of later filtering), if any + pub records_produced: Option, + /// The time it took to execute this operation, if available + pub execution_time: Option, + /// all child operations performed on data retrieved, filtered or aggregated by this operation + pub children: Vec>, + depth: usize, +} + +/// An execution plan, allowing access both to the human-readable text representation, access to a per-operation map, or traversable operation tree #[derive(Debug, Clone, PartialEq)] pub struct ExecutionPlan { - text: String, - steps: Vec, + string_representation: String, + plan: Vec, + operations: HashMap>>, + operation_tree: Rc, } -impl ExecutionPlan { - /// Returns a pretty-print version of the execution plan - pub fn text(&self) -> &str { - self.text.as_str() +impl<'a> ExecutionPlan { + /// Returns the plan as a slice of human-readable strings + pub fn plan(&self) -> &[String] { + self.plan.as_slice() } /// Returns a slice of strings representing each step in the execution plan, which can be iterated. - pub fn steps(&self) -> &[String] { - self.steps.as_slice() + pub fn operations(&self) -> &HashMap>> { + &self.operations + } + + /// Returns a shared pointer to the operation tree, allowing easy immutable traversal + pub fn operation_tree(&self) -> &Rc { + &self.operation_tree + } + + /// Returns a string representation of the entire executino plan + pub fn string_representation(&self) -> &str { + self.string_representation.as_str() + } + + fn create_node( + depth: usize, + operation_string: &str, + traversal_stack: &mut Vec>>, + ) -> FalkorResult<()> { + let new_node = Rc::new(RefCell::new(IntermediateOperation::new( + depth, + operation_string, + )?)); + + traversal_stack.push(Rc::clone(&new_node)); + Ok(()) + } + + fn finalize_operation( + current_refcell: Rc> + ) -> FalkorResult> { + let current_op = Rc::try_unwrap(current_refcell) + .map_err(|_| FalkorDBError::RefCountBooBoo)? + .into_inner(); + + let mut out_vec = Vec::with_capacity(current_op.children.len()); + for child in current_op.children.into_iter() { + out_vec.push(Self::finalize_operation(child)?); + } + + Ok(Rc::new(Operation { + name: current_op.name, + args: current_op.args, + records_produced: current_op.records_produced, + execution_time: current_op.execution_time, + depth: current_op.depth, + children: out_vec, + })) + } + + fn operations_map_from_tree( + current_branch: &Rc, + map: &mut HashMap>>, + ) { + map.entry(current_branch.name.clone()) + .or_default() + .push(Rc::clone(current_branch)); + + for child in ¤t_branch.children { + Self::operations_map_from_tree(child, map); + } } } @@ -28,18 +166,82 @@ impl TryFrom for ExecutionPlan { type Error = FalkorDBError; fn try_from(value: FalkorValue) -> Result { - let (execution_plan, execution_plan_text): (Vec<_>, Vec<_>) = value + let execution_plan_operations: Vec<_> = value .into_vec()? .into_iter() - .flat_map(|item| { - let item = item.into_string()?; - Result::<_, FalkorDBError>::Ok((item.trim().to_string(), item)) - }) - .unzip(); + .flat_map(FalkorValue::into_string) + .collect(); + + let string_representation = ["".to_string()] + .into_iter() + .chain(execution_plan_operations.iter().cloned()) + .collect::>() + .join("\n"); + + 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(); + + let current_node = match current_traversal_stack.last().cloned() { + None => { + current_traversal_stack.push(Rc::new(RefCell::new( + IntermediateOperation::new(depth, node)?, + ))); + continue; + } + Some(current_node) => current_node, + }; + + let current_depth = current_node.borrow().depth; + match depth.cmp(¤t_depth) { + Ordering::Less => { + let times_to_pop = (current_depth - depth) + 1; + if times_to_pop > current_traversal_stack.len() { + return Err(FalkorDBError::CorruptExecutionPlan); + } + for _ in 0..times_to_pop { + current_traversal_stack.pop(); + } + + // Create this node as a child to the last node with one less depth than the new node + Self::create_node(depth, node, &mut current_traversal_stack)?; + } + Ordering::Equal => { + // Push new node to the parent node + current_traversal_stack.pop(); + Self::create_node(depth, node, &mut current_traversal_stack)?; + } + Ordering::Greater => { + if depth - current_depth > 1 { + // Too big a skip + return Err(FalkorDBError::CorruptExecutionPlan); + } + + let new_node = Rc::new(RefCell::new(IntermediateOperation::new(depth, node)?)); + current_traversal_stack.push(Rc::clone(&new_node)); + + // New node is a child of the current node, so we will push it as a child + current_node.borrow_mut().children.push(new_node); + } + } + } + + // Must drop traversal stack first + let root_node = current_traversal_stack + .into_iter() + .next() + .ok_or(FalkorDBError::CorruptExecutionPlan)?; + let operation_tree = Self::finalize_operation(root_node)?; + + let mut operations = HashMap::new(); + Self::operations_map_from_tree(&operation_tree, &mut operations); Ok(ExecutionPlan { - steps: execution_plan, - text: format!("\n{}", execution_plan_text.join("\n")), + string_representation, + plan: execution_plan_operations, + operations, + operation_tree, }) } } From e356b47f64686c4cb67759b61117fa58830794cf Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Thu, 30 May 2024 14:24:24 +0300 Subject: [PATCH 41/62] Add sentinel support, rename to execute command, fix url parsing bug --- Cargo.lock | 37 ++++++++++ Cargo.toml | 2 +- src/client/blocking.rs | 139 ++++++++++++++++++++++++++++++++----- src/client/builder.rs | 7 +- src/client/mod.rs | 34 +++++++-- src/connection/blocking.rs | 64 ++++++++++------- src/connection_info/mod.rs | 50 ++++++++----- src/error/mod.rs | 12 +++- src/graph/blocking.rs | 18 ++--- src/graph/query_builder.rs | 4 +- src/graph_schema/mod.rs | 4 +- src/redis_ext.rs | 5 +- 12 files changed, 289 insertions(+), 87 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a685d9e..89883b1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -404,6 +404,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + [[package]] name = "proc-macro2" version = "1.0.82" @@ -422,6 +428,36 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + [[package]] name = "redis" version = "0.25.4" @@ -432,6 +468,7 @@ dependencies = [ "itoa", "native-tls", "percent-encoding", + "rand", "rustls", "rustls-native-certs", "rustls-pemfile", diff --git a/Cargo.toml b/Cargo.toml index 5505d93..c65a89a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ edition = "2021" anyhow = { version = "1.0.86", default-features = false, features = ["std"] } log = { version = "0.4.21", default-features = false } parking_lot = { version = "0.12.3", default-features = false, features = ["deadlock_detection"] } -redis = { version = "0.25.4", default-features = false, optional = true } +redis = { version = "0.25.4", default-features = false, optional = true, features = ["sentinel"] } regex = { version = "1.10.4", default-features = false, features = ["std", "perf", "unicode-bool", "unicode-perl"] } thiserror = "1.0.61" tracing = { version = "0.1.40", default-features = false, features = ["std", "attributes"] } diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 331b56b..48fe78e 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -12,7 +12,6 @@ use crate::{ use parking_lot::Mutex; use std::{ collections::HashMap, - fmt::Display, sync::{mpsc, Arc}, time::Duration, }; @@ -36,6 +35,87 @@ impl FalkorSyncClientInner { } } +#[cfg(feature = "redis")] +fn get_redis_info( + conn: &mut FalkorSyncConnection, + section: Option<&str>, +) -> FalkorResult> { + Ok(conn + .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()) +} + +#[cfg(feature = "redis")] +fn is_sentinel(conn: &mut FalkorSyncConnection) -> FalkorResult { + let info_map = get_redis_info(conn, Some("server"))?; + Ok(info_map + .get("redis_mode") + .map(|redis_mode| redis_mode == "sentinel") + .unwrap_or_default()) +} + +fn get_sentinel_client( + client: &mut FalkorClientProvider, + connection_info: &redis::ConnectionInfo, +) -> FalkorResult> { + let mut conn = client.get_connection(None)?; + if !is_sentinel(&mut conn)? { + return Ok(None); + } + + // 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()?; + + if sentinel_masters.len() != 1 { + return Err(FalkorDBError::SentinelMastersCount); + } + + let sentinel_master: HashMap<_, _> = sentinel_masters + .into_iter() + .next() + .ok_or(FalkorDBError::SentinelMastersCount)? + .into_vec()? + .chunks_exact(2) + .flat_map(|chunk| TryInto::<[FalkorValue; 2]>::try_into(chunk.to_vec())) + .flat_map(|[key, val]| { + Result::<_, FalkorDBError>::Ok((key.into_string()?, val.into_string()?)) + }) + .collect(); + + let (_name, host, port) = match ( + sentinel_master.get("name"), + sentinel_master.get("host"), + sentinel_master.get("ip"), + sentinel_master.get("port"), + ) { + (Some(name), Some(host), _, Some(port)) => (name, host, port), + (Some(name), _, Some(ip), Some(port)) => (name, ip, port), + _ => return Err(FalkorDBError::SentinelMastersCount), + }; + + let user_pass_string = match ( + connection_info.redis.username.as_ref(), + connection_info.redis.password.as_ref(), + ) { + (None, Some(pass)) => format!("{}@", pass), // Password-only authentication is allowed in legacy auth + (Some(user), Some(pass)) => format!("{user}:{pass}@"), + _ => "".to_string(), + }; + let url = format!("{}://{}{host}:{port}", "redis", user_pass_string); + + Ok(Some(redis::Client::open(url).map_err(|err| { + FalkorDBError::SentinelConnection(err.to_string()) + })?)) +} + /// This is the publicly exposed API of the sync Falkor Client /// It makes no assumptions in regard to which database the Falkor module is running on, /// and will select it based on enabled features and url connection @@ -51,19 +131,28 @@ pub struct FalkorSyncClient { impl FalkorSyncClient { pub(crate) fn create( - client: FalkorClientProvider, + mut client: FalkorClientProvider, connection_info: FalkorConnectionInfo, num_connections: u8, timeout: Option, ) -> FalkorResult { let (connection_pool_tx, connection_pool_rx) = mpsc::sync_channel(num_connections as usize); + + #[cfg(feature = "redis")] + if let FalkorConnectionInfo::Redis(redis_conn_info) = &connection_info { + if let Some(sentinel) = get_sentinel_client(&mut client, redis_conn_info)? { + client.set_sentinel(sentinel); + } + } + + // One already exists for _ in 0..num_connections { + let new_conn = client + .get_connection(timeout) + .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?; + connection_pool_tx - .send( - client - .get_connection(timeout) - .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, - ) + .send(new_conn) .map_err(|_| FalkorDBError::EmptyConnection)?; } @@ -93,7 +182,7 @@ impl FalkorSyncClient { /// A [`Vec`] of [`String`]s, containing the names of available graphs pub fn list_graphs(&self) -> FalkorResult> { let mut conn = self.borrow_connection()?; - conn.send_command::<&str>(None, "GRAPH.LIST", None, None) + conn.execute_command(None, "GRAPH.LIST", None, None) .and_then(string_vec_from_val) } @@ -105,13 +194,13 @@ impl FalkorSyncClient { /// /// # Returns /// A [`HashMap`] comprised of [`String`] keys, and [`ConfigValue`] values. - pub fn config_get( + pub fn config_get( &self, - config_key: T, + config_key: &str, ) -> FalkorResult> { let mut conn = self.borrow_connection()?; let config = conn - .send_command(None, "GRAPH.CONFIG", Some("GET"), Some(&[config_key]))? + .execute_command(None, "GRAPH.CONFIG", Some("GET"), Some(&[config_key]))? .into_vec()?; if config.len() == 2 { @@ -144,16 +233,16 @@ 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. - pub fn config_set, C: Into>( + pub fn config_set>( &self, - config_key: T, + config_key: &str, value: C, ) -> FalkorResult { - self.borrow_connection()?.send_command( + self.borrow_connection()?.execute_command( None, "GRAPH.CONFIG", Some("SET"), - Some(&[config_key.into(), value.into()]), + Some(&[config_key, value.into().to_string().as_str()]), ) } @@ -184,7 +273,7 @@ impl FalkorSyncClient { graph_to_clone: &str, new_graph_name: &str, ) -> FalkorResult { - self.borrow_connection()?.send_command( + self.borrow_connection()?.execute_command( Some(graph_to_clone), "GRAPH.COPY", None, @@ -192,6 +281,16 @@ impl FalkorSyncClient { )?; Ok(self.select_graph(new_graph_name)) } + + #[cfg(feature = "redis")] + /// Retrieves redis information + pub fn redis_info( + &self, + section: Option<&str>, + ) -> FalkorResult> { + let mut conn = self.borrow_connection()?; + get_redis_info(conn.as_inner()?, section) + } } #[cfg(test)] @@ -347,4 +446,12 @@ mod tests { .config_set("DELTA_MAX_PENDING_CHANGES", current_val) .ok(); } + + #[cfg(feature = "redis")] + #[test] + fn test_redis_sentinel_build() { + FalkorClientBuilder::new().with_connection_info("falkor://falkordb:123456@singlezonesentinellblb.instance-e0srmv0mc.hc-jx5tis6bc.us-central1.gcp.f2e0a955bb84.cloud:26379".try_into().expect("Could not construct connectioninfo")) + .build() + .expect("Could not create sentinel client"); + } } diff --git a/src/client/builder.rs b/src/client/builder.rs index 8930960..8864800 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -77,10 +77,11 @@ impl FalkorClientBuilder { .map_err(|_| FalkorDBError::InvalidConnectionInfo)?; Ok(match connection_info { #[cfg(feature = "redis")] - FalkorConnectionInfo::Redis(connection_info) => FalkorClientProvider::Redis( - redis::Client::open(connection_info.clone()) + FalkorConnectionInfo::Redis(connection_info) => FalkorClientProvider::Redis { + client: redis::Client::open(connection_info.clone()) .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, - ), + sentinel: None, + }, }) } } diff --git a/src/client/mod.rs b/src/client/mod.rs index 1559f5f..1a94af1 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -11,21 +11,41 @@ pub(crate) mod builder; pub(crate) enum FalkorClientProvider { #[cfg(feature = "redis")] - Redis(redis::Client), + Redis { + client: redis::Client, + sentinel: Option, + }, } impl FalkorClientProvider { pub(crate) fn get_connection( - &self, + &mut self, connection_timeout: Option, ) -> FalkorResult { Ok(match self { #[cfg(feature = "redis")] - FalkorClientProvider::Redis(redis_client) => connection_timeout - .map(|timeout| redis_client.get_connection_with_timeout(timeout)) - .unwrap_or_else(|| redis_client.get_connection()) - .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))? - .into(), + FalkorClientProvider::Redis { + client: redis_client, + sentinel, + } => match ( + connection_timeout, + sentinel.as_ref().unwrap_or(redis_client), + ) { + (None, redis_client) => redis_client.get_connection(), + (Some(timeout), redis_client) => redis_client.get_connection_with_timeout(timeout), + } + .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))? + .into(), }) } + + #[cfg(feature = "redis")] + pub(crate) fn set_sentinel( + &mut self, + sentinel_client: redis::Client, + ) { + match self { + FalkorClientProvider::Redis { sentinel, .. } => *sentinel = Some(sentinel_client), + } + } } diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 55f987e..487fff9 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -3,15 +3,45 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{FalkorDBError, FalkorValue}; -use anyhow::Result; -use std::{fmt::Display, sync::mpsc}; +use crate::{FalkorDBError, FalkorResult, FalkorValue}; +use std::sync::mpsc; pub(crate) enum FalkorSyncConnection { #[cfg(feature = "redis")] Redis(redis::Connection), } +impl FalkorSyncConnection { + pub(crate) fn execute_command( + &mut self, + graph_name: Option<&str>, + command: &str, + subcommand: Option<&str>, + params: Option<&[&str]>, + ) -> FalkorResult { + match self { + #[cfg(feature = "redis")] + FalkorSyncConnection::Redis(redis_conn) => { + use redis::ConnectionLike as _; + let mut cmd = redis::cmd(command); + cmd.arg(subcommand); + cmd.arg(graph_name); + if let Some(params) = params { + for param in params { + cmd.arg(param.to_string()); + } + } + redis::FromRedisValue::from_owned_redis_value( + redis_conn + .req_command(&cmd) + .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, + ) + .map_err(|err| FalkorDBError::RedisParsingError(err.to_string())) + } + } + } +} + /// A container for a connection that is borrowed from the pool. /// Upon going out of scope, it will return the connection to the pool. /// @@ -32,37 +62,19 @@ impl BorrowedSyncConnection { } } - pub(crate) fn as_inner(&mut self) -> Result<&mut FalkorSyncConnection, FalkorDBError> { + pub(crate) fn as_inner(&mut self) -> FalkorResult<&mut FalkorSyncConnection> { self.conn.as_mut().ok_or(FalkorDBError::EmptyConnection) } - pub(crate) fn send_command( + pub(crate) fn execute_command( &mut self, graph_name: Option<&str>, command: &str, subcommand: Option<&str>, - params: Option<&[P]>, + params: Option<&[&str]>, ) -> Result { - match self.as_inner()? { - #[cfg(feature = "redis")] - FalkorSyncConnection::Redis(redis_conn) => { - use redis::ConnectionLike as _; - let mut cmd = redis::cmd(command); - cmd.arg(subcommand); - cmd.arg(graph_name); - if let Some(params) = params { - for param in params { - cmd.arg(param.to_string()); - } - } - redis::FromRedisValue::from_owned_redis_value( - redis_conn - .req_command(&cmd) - .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, - ) - .map_err(|err| FalkorDBError::RedisParsingError(err.to_string())) - } - } + 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 eae0c50..ebafbde 100644 --- a/src/connection_info/mod.rs +++ b/src/connection_info/mod.rs @@ -9,18 +9,23 @@ use crate::{FalkorDBError, FalkorResult}; /// The different enum variants are enabled based on compilation features #[derive(Clone, Debug)] pub enum FalkorConnectionInfo { - /// A Redis database connection #[cfg(feature = "redis")] + /// A Redis database connection Redis(redis::ConnectionInfo), } impl FalkorConnectionInfo { - fn fallback_provider(full_url: String) -> FalkorResult { + fn fallback_provider(mut full_url: String) -> FalkorResult { #[cfg(feature = "redis")] - Ok(FalkorConnectionInfo::Redis( - redis::IntoConnectionInfo::into_connection_info(format!("redis://{full_url}")) - .map_err(|_| FalkorDBError::InvalidConnectionInfo)?, - )) + Ok(FalkorConnectionInfo::Redis({ + if full_url.starts_with("falkor://") { + full_url = full_url.replace("falkor://", "redis://"); + } else if full_url.starts_with("falkors://") { + full_url = full_url.replace("falkors://", "rediss://"); + } + redis::IntoConnectionInfo::into_connection_info(full_url) + .map_err(|_| FalkorDBError::InvalidConnectionInfo)? + })) } /// Retrieves the internally stored address for this connection info @@ -43,33 +48,40 @@ impl TryFrom<&str> for FalkorConnectionInfo { .parse(value) .map_err(|_| FalkorDBError::InvalidConnectionInfo)?; + // The url_parse serializer seems ***ed up for some reason let scheme = url.scheme.unwrap_or("falkor".to_string()); - let addr = url.domain.unwrap_or("127.0.0.1".to_string()); - let port = url.port.unwrap_or(6379); // Might need to change in accordance with the default fallback - let user_pass_string = match url.user_pass { (Some(pass), None) => format!("{}@", pass), // Password-only authentication is allowed in legacy auth (Some(user), Some(pass)) => format!("{user}:{pass}@"), _ => "".to_string(), }; + let subdomain = url + .subdomain + .map(|subdomain| format!("{subdomain}.")) + .unwrap_or_default(); + + let domain = url.domain.unwrap_or("127.0.0.1".to_string()); + let top_level_domain = url + .top_level_domain + .map(|top_level_domain| format!(".{top_level_domain}")) + .unwrap_or_default(); + let port = url.port.unwrap_or(6379); // Might need to change in accordance with the default fallback + let serialized = format!( + "{}://{}{}{}{}:{}", + scheme, user_pass_string, subdomain, domain, top_level_domain, port + ); match scheme.as_str() { "redis" | "rediss" => { #[cfg(feature = "redis")] return Ok(FalkorConnectionInfo::Redis( - redis::IntoConnectionInfo::into_connection_info(format!( - "{}://{}{}:{}", - scheme, user_pass_string, addr, port - )) - .map_err(|_| FalkorDBError::InvalidConnectionInfo)?, + redis::IntoConnectionInfo::into_connection_info(serialized) + .map_err(|_| FalkorDBError::InvalidConnectionInfo)?, )); #[cfg(not(feature = "redis"))] return Err(FalkorDBError::UnavailableProvider); } - _ => FalkorConnectionInfo::fallback_provider(format!( - "{}{}:{}", - user_pass_string, addr, port - )), + _ => FalkorConnectionInfo::fallback_provider(serialized), } } } @@ -101,7 +113,7 @@ mod tests { #[cfg(feature = "redis")] fn test_redis_fallback_provider() { let FalkorConnectionInfo::Redis(redis) = - FalkorConnectionInfo::fallback_provider("127.0.0.1:6379".to_string()).unwrap(); + FalkorConnectionInfo::fallback_provider("redis://127.0.0.1:6379".to_string()).unwrap(); assert_eq!(redis.addr.to_string(), "127.0.0.1:6379".to_string()); } diff --git a/src/error/mod.rs b/src/error/mod.rs index 8b9a338..3ae46db 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -12,11 +12,19 @@ pub enum FalkorDBError { /// A required Id for parsing was not found in the schema #[error("A required Id for parsing was not found in the schema")] MissingSchemaId(SchemaType), + /// Could not connect to Redis Sentinel, or a critical Sentinel operation has failed + #[error( + "Could not connect to Redis Sentinel, or a critical Sentinel operation has failed: {0}" + )] + SentinelConnection(String), + /// Received unsupported number of sentinel masters in list, there can be only one + #[error("Received unsupported number of sentinel masters in list, there can be only one")] + SentinelMastersCount, /// An error occurred while sending the request to Redis - #[error("An error occurred while sending the request to Redis")] + #[error("An error occurred while sending the request to Redis: {0}")] RedisConnectionError(String), /// An error occurred while parsing the Redis response" - #[error("An error occurred while parsing the Redis response")] + #[error("An error occurred while parsing the Redis response: {0}")] RedisParsingError(String), /// The provided connection info is invalid #[error("The provided connection info is invalid")] diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 1f64d59..48bc156 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -42,20 +42,20 @@ impl SyncGraph { self.graph_name.as_str() } - fn send_command( + fn execute_command( &self, command: &str, subcommand: Option<&str>, params: Option<&[&str]>, ) -> FalkorResult { let mut conn = self.client.borrow_connection()?; - conn.send_command(Some(self.graph_name.as_str()), command, subcommand, params) + conn.execute_command(Some(self.graph_name.as_str()), command, subcommand, params) } /// Deletes the graph stored in the database, and drop all the schema caches. /// NOTE: This still maintains the graph API, operations are still viable. pub fn delete(&mut self) -> FalkorResult<()> { - self.send_command("GRAPH.DELETE", None, None)?; + self.execute_command("GRAPH.DELETE", None, None)?; self.graph_schema.clear(); Ok(()) } @@ -65,14 +65,16 @@ impl SyncGraph { /// # Returns /// A [`Vec`] of [`SlowlogEntry`], providing information about each query. pub fn slowlog(&self) -> FalkorResult> { - let res = self.send_command("GRAPH.SLOWLOG", None, None)?.into_vec()?; + let res = self + .execute_command("GRAPH.SLOWLOG", None, None)? + .into_vec()?; Ok(res.into_iter().flat_map(SlowlogEntry::try_from).collect()) } /// Resets the slowlog, all query time data will be cleared. pub fn slowlog_reset(&self) -> FalkorResult { - self.send_command("GRAPH.SLOWLOG", None, Some(&["RESET"])) + self.execute_command("GRAPH.SLOWLOG", None, Some(&["RESET"])) } /// Creates a [`QueryBuilder`] for this graph, in an attempt to profile a specific query @@ -306,7 +308,7 @@ impl SyncGraph { ]); params.extend(properties); - self.send_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) + self.execute_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) } /// Creates a new constraint for this graph, making the provided properties unique @@ -342,7 +344,7 @@ impl SyncGraph { params.extend(properties); // create constraint using index - self.send_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) + self.execute_command("GRAPH.CONSTRAINT", Some("CREATE"), Some(params.as_slice())) } /// Drop an existing constraint from the graph @@ -373,7 +375,7 @@ impl SyncGraph { ]); params.extend(properties); - self.send_command("GRAPH.CONSTRAINT", Some("DROP"), Some(params.as_slice())) + self.execute_command("GRAPH.CONSTRAINT", Some("DROP"), Some(params.as_slice())) } } diff --git a/src/graph/query_builder.rs b/src/graph/query_builder.rs index afaae27..db3850d 100644 --- a/src/graph/query_builder.rs +++ b/src/graph/query_builder.rs @@ -92,7 +92,7 @@ impl<'a, Output> QueryBuilder<'a, Output> { let mut params = vec![query.as_str(), "--compact"]; params.extend(timeout.as_deref()); - conn.send_command( + conn.execute_command( Some(self.graph.graph_name()), self.command, None, @@ -269,7 +269,7 @@ impl<'a, Output> ProcedureBuilder<'a, Output> { generate_procedure_call(self.procedure_name, self.args, self.yields); let query = construct_query(query_string, params.as_ref()); - conn.send_command( + conn.execute_command( Some(self.graph.graph_name()), command, None, diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index 928e2b1..8a5ac4e 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -99,11 +99,11 @@ 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] = conn - .send_command( + .execute_command( Some(self.graph_name.as_str()), "GRAPH.QUERY", None, - Some(&[format!("CALL {}()", get_refresh_command(schema_type))]), + Some(&[format!("CALL {}()", get_refresh_command(schema_type)).as_str()]), )? .into_vec()? .try_into() diff --git a/src/redis_ext.rs b/src/redis_ext.rs index 13474a2..4dfc922 100644 --- a/src/redis_ext.rs +++ b/src/redis_ext.rs @@ -24,7 +24,10 @@ impl From for FalkorConnectionInfo { impl From for FalkorClientProvider { fn from(value: redis::Client) -> Self { - Self::Redis(value) + Self::Redis { + client: value, + sentinel: None, + } } } From 3ceffffc6b42c9bbdbeec059997512467f454077 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 4 Jun 2024 10:23:26 +0300 Subject: [PATCH 42/62] Try setting up tests again --- .github/workflows/pr-checks.yml | 2 ++ deny.toml | 7 +++---- src/client/blocking.rs | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 5120016..5aae999 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -44,6 +44,8 @@ jobs: image: falkordb/falkordb:edge ports: - 6379:6379 + volumes: + - ./resources/imdb.rdb:/FalkorDB/dump.rdb steps: - uses: actions/checkout@v4 - name: Build diff --git a/deny.toml b/deny.toml index 1b10793..0f498af 100644 --- a/deny.toml +++ b/deny.toml @@ -4,8 +4,8 @@ multiple-versions = "deny" skip = ["windows_x86_64_msvc", "windows_x86_64_gnu", "windows_x86_64_gnullvm", "windows_i686_msvc", "windows_i686_gnu", "windows_i686_gnullvm", - "windows_aarch64_msvc", "windows_aarch64_gnu", "windows_aarch64_gnullvm", - "windows-targets", "windows-sys"] # Windows crates are all locked in `mio`, but they are very strictly BC so are ok + "windows_aarch64_msvc", "windows_aarch64_gnullvm", + "windows-targets"] # Windows crates are all locked in `mio`, but should be fine [sources] unknown-registry = "deny" @@ -13,8 +13,7 @@ unknown-git = "deny" [licenses] exceptions = [ - { name = "unicode-ident", allow = ["Unicode-DFS-2016"] }, # One of a kind license, no need to put it in the "allow" list - { name = "ring", allow = ["LicenseRef-ring"] } # ring uses a specific BoringSSL license that does not match the standard text so requires allowing the specific hash + { name = "unicode-ident", allow = ["Unicode-DFS-2016"] }, # unique license ] unused-allowed-license = "allow" confidence-threshold = 0.93 diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 48fe78e..f83fb2a 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -139,6 +139,7 @@ impl FalkorSyncClient { let (connection_pool_tx, connection_pool_rx) = mpsc::sync_channel(num_connections as usize); #[cfg(feature = "redis")] + #[allow(irrefutable_let_patterns)] if let FalkorConnectionInfo::Redis(redis_conn_info) = &connection_info { if let Some(sentinel) = get_sentinel_client(&mut client, redis_conn_info)? { client.set_sentinel(sentinel); From 5d23331fdd1eaacc3ca4f163f96dffcb741fc70c Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 4 Jun 2024 12:31:14 +0300 Subject: [PATCH 43/62] Much cleaner parsing, new test suite --- .github/workflows/pr-checks.yml | 18 +- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 2 +- deny.toml | 1 + resources/actors.csv | 1862 +++++++++++++++++++++++++++++++ resources/imdb.rdb | Bin 109007 -> 0 bytes resources/movies.csv | 285 +++++ resources/populate_graph.py | 59 + src/client/blocking.rs | 4 +- src/error/mod.rs | 8 +- src/graph/blocking.rs | 11 +- src/graph_schema/mod.rs | 180 ++- src/graph_schema/utils.rs | 163 --- src/response/execution_plan.rs | 10 +- src/value/graph_entities.rs | 45 +- src/value/map.rs | 78 +- src/value/point.rs | 4 +- 18 files changed, 2402 insertions(+), 332 deletions(-) create mode 100644 resources/actors.csv delete mode 100644 resources/imdb.rdb create mode 100644 resources/movies.csv create mode 100755 resources/populate_graph.py delete mode 100644 src/graph_schema/utils.rs diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 5aae999..de2cec5 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -18,8 +18,8 @@ jobs: check-deny: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 - uses: EmbarkStudios/cargo-deny-action@v1 + - uses: actions/checkout@v4 - name: Check Deny run: cargo deny --log-level info check @@ -39,17 +39,17 @@ jobs: build-linux: runs-on: ubuntu-latest - services: - falkordb: - image: falkordb/falkordb:edge - ports: - - 6379:6379 - volumes: - - ./resources/imdb.rdb:/FalkorDB/dump.rdb steps: - uses: actions/checkout@v4 - name: Build run: cargo build - uses: taiki-e/install-action@nextest + - name: Populate test graph + run: pip install falkordb && ./resources/populate_graph.py - name: Test - run: cargo nextest run --all \ No newline at end of file + run: cargo nextest run --all + services: + falkordb: + image: falkordb/falkordb:edge + ports: + - 6379:6379 \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 89883b1..cfef9c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,7 +126,7 @@ dependencies = [ ] [[package]] -name = "falkordb-client-rs" +name = "falkordb" version = "0.1.0" dependencies = [ "anyhow", diff --git a/Cargo.toml b/Cargo.toml index c65a89a..16e0e7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "falkordb-client-rs" +name = "falkordb" version = "0.1.0" edition = "2021" diff --git a/README.md b/README.md index a3ce0e2..3cfd874 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Or use our [sandbox](https://cloud.falkordb.com/sandbox) ### Example ```rust -use falkordb_client_rs::FalkorClientBuilder; +use falkordb::FalkorClientBuilder; // Connect to FalkorDB let client = FalkorClientBuilder::new().with_connection_info("falkor://127.0.0.1:6379".try_into().unwrap()).build().unwrap(); diff --git a/deny.toml b/deny.toml index 0f498af..2e966cc 100644 --- a/deny.toml +++ b/deny.toml @@ -14,6 +14,7 @@ unknown-git = "deny" [licenses] exceptions = [ { name = "unicode-ident", allow = ["Unicode-DFS-2016"] }, # unique license + { name = "ring", allow = ["LicenseRef-ring"] } # ring uses a specific BoringSSL license that does not match the standard text so requires allowing the specific hash ] unused-allowed-license = "allow" confidence-threshold = 0.93 diff --git a/resources/actors.csv b/resources/actors.csv new file mode 100644 index 0000000..56a42ef --- /dev/null +++ b/resources/actors.csv @@ -0,0 +1,1862 @@ +Chris Pratt,1979,Guardians of the Galaxy +Zoe Saldana,1978,Guardians of the Galaxy +Dave Bautista,1969,Guardians of the Galaxy +Vin Diesel,1967,Guardians of the Galaxy +Bradley Cooper,1975,Guardians of the Galaxy +Lee Pace,1979,Guardians of the Galaxy +Michael Rooker,1955,Guardians of the Galaxy +Karen Gillan,1987,Guardians of the Galaxy +Djimon Hounsou,1964,Guardians of the Galaxy +Ellen Burstyn,1932,Interstellar +Matthew McConaughey,1969,Interstellar +Mackenzie Foy,2000,Interstellar +John Lithgow,1945,Interstellar +David Oyelowo,1976,Interstellar +Collette Wolfe,1980,Interstellar +Francis X. McCarthy,1942,Interstellar +Bill Irwin,1950,Interstellar +Ralph Fiennes,1962,The Grand Budapest Hotel +F. Murray Abraham,1939,The Grand Budapest Hotel +Mathieu Amalric,1965,The Grand Budapest Hotel +Adrien Brody,1973,The Grand Budapest Hotel +Willem Dafoe,1955,The Grand Budapest Hotel +Jeff Goldblum,1952,The Grand Budapest Hotel +Harvey Keitel,1939,The Grand Budapest Hotel +Jude Law,1972,The Grand Budapest Hotel +Bill Murray,1950,The Grand Budapest Hotel +Ben Affleck,1972,Gone Girl +Rosamund Pike,1979,Gone Girl +Neil Patrick Harris,1973,Gone Girl +Tyler Perry,1969,Gone Girl +Carrie Coon,1981,Gone Girl +Kim Dickens,1965,Gone Girl +Patrick Fugit,1982,Gone Girl +David Clennon,1943,Gone Girl +Lisa Banes,1955,Gone Girl +Benedict Cumberbatch,1976,The Imitation Game +Keira Knightley,1985,The Imitation Game +Matthew Goode,1978,The Imitation Game +Rory Kinnear,1978,The Imitation Game +Allen Leech,1981,The Imitation Game +Matthew Beard,1989,The Imitation Game +Charles Dance,1946,The Imitation Game +Mark Strong,1963,The Imitation Game +Keanu Reeves,1964,John Wick +Michael Nyqvist,1960,John Wick +Alfie Allen,1986,John Wick +Willem Dafoe,1955,John Wick +Dean Winters,1964,John Wick +Adrianne Palicki,1983,John Wick +Omer Barnea,1980,John Wick +Daniel Bernhardt,1965,John Wick +Hugh Jackman,1968,X-Men: Days of Future Past +James McAvoy,1979,X-Men: Days of Future Past +Michael Fassbender,1977,X-Men: Days of Future Past +Jennifer Lawrence,1990,X-Men: Days of Future Past +Halle Berry,1966,X-Men: Days of Future Past +Nicholas Hoult,1989,X-Men: Days of Future Past +Anna Paquin,1982,X-Men: Days of Future Past +Ellen Page,1987,X-Men: Days of Future Past +Peter Dinklage,1969,X-Men: Days of Future Past +Elizabeth Reaser,1975,Ouija: Origin of Evil +Henry Thomas,1971,Ouija: Origin of Evil +Doug Jones,1960,Ouija: Origin of Evil +Kate Siegel,1982,Ouija: Origin of Evil +Denzel Washington,1954,The Equalizer +Marton Csokas,1966,The Equalizer +David Harbour,1975,The Equalizer +Haley Bennett,1988,The Equalizer +Bill Pullman,1953,The Equalizer +Melissa Leo,1960,The Equalizer +David Meunier,1973,The Equalizer +Angelina Jolie,1975,Maleficent +Elle Fanning,1998,Maleficent +Sharlto Copley,1973,Maleficent +Lesley Manville,1956,Maleficent +Imelda Staunton,1956,Maleficent +Juno Temple,1989,Maleficent +Sam Riley,1980,Maleficent +Brenton Thwaites,1989,Maleficent +Kenneth Cranham,1944,Maleficent +Miles Teller,1987,Whiplash +J.K. Simmons,1955,Whiplash +Paul Reiser,1957,Whiplash +Melissa Benoist,1988,Whiplash +Chris Mulkey,1948,Whiplash +Brad Pitt,1963,Fury +Shia LaBeouf,1986,Fury +Logan Lerman,1992,Fury +Jon Bernthal,1976,Fury +Jim Parrack,1981,Fury +Brad William Henke,1966,Fury +Xavier Samuel,1983,Fury +Colin Firth,1960,Kingsman: The Secret Service +Mark Strong,1963,Kingsman: The Secret Service +Jonno Davies,1992,Kingsman: The Secret Service +Jack Davenport,1973,Kingsman: The Secret Service +Samantha Womack,1972,Kingsman: The Secret Service +Mark Hamill,1951,Kingsman: The Secret Service +Velibor Topic,1970,Kingsman: The Secret Service +Keir Gilchrist,1992,It Follows +Maika Monroe,1993,It Follows +Lili Sepe,1997,It Follows +Olivia Luccardi,1989,It Follows +Tom Cruise,1962,Edge of Tomorrow +Emily Blunt,1983,Edge of Tomorrow +Brendan Gleeson,1955,Edge of Tomorrow +Bill Paxton,1955,Edge of Tomorrow +Jonas Armstrong,1981,Edge of Tomorrow +Tony Way,1978,Edge of Tomorrow +Kick Gurry,1978,Edge of Tomorrow +Franz Drameh,1993,Edge of Tomorrow +Eddie Redmayne,1982,The Theory of Everything +Felicity Jones,1983,The Theory of Everything +Harry Lloyd,1983,The Theory of Everything +Alice Orr-Ewing,1989,The Theory of Everything +David Thewlis,1963,The Theory of Everything +Thomas Morrison,1983,The Theory of Everything +Essie Davis,1970,The Babadook +Hayley McElhinney,1974,The Babadook +Daniel Henshall,1982,The Babadook +Shailene Woodley,1991,Divergent +Theo James,1984,Divergent +Ashley Judd,1968,Divergent +Jai Courtney,1986,Divergent +Ray Stevenson,1964,Divergent +Miles Teller,1987,Divergent +Tony Goldwyn,1960,Divergent +Ansel Elgort,1994,Divergent +Mickey Rourke,1952,Sin City: A Dame to Kill For +Jessica Alba,1981,Sin City: A Dame to Kill For +Josh Brolin,1968,Sin City: A Dame to Kill For +Joseph Gordon-Levitt,1981,Sin City: A Dame to Kill For +Rosario Dawson,1979,Sin City: A Dame to Kill For +Bruce Willis,1955,Sin City: A Dame to Kill For +Eva Green,1980,Sin City: A Dame to Kill For +Powers Boothe,1948,Sin City: A Dame to Kill For +Dennis Haysbert,1954,Sin City: A Dame to Kill For +Aaron Paul,1979,Need for Speed +Dominic Cooper,1978,Need for Speed +Imogen Poots,1989,Need for Speed +Scott Mescudi,1984,Need for Speed +Rami Malek,1981,Need for Speed +Ramon Rodriguez,1979,Need for Speed +Dakota Johnson,1989,Need for Speed +Stevie Ray Dallimore,1967,Need for Speed +Andrew Garfield,1983,The Amazing Spider-Man 2 +Emma Stone,1988,The Amazing Spider-Man 2 +Jamie Foxx,1967,The Amazing Spider-Man 2 +Dane DeHaan,1986,The Amazing Spider-Man 2 +Colm Feore,1958,The Amazing Spider-Man 2 +Felicity Jones,1983,The Amazing Spider-Man 2 +Paul Giamatti,1967,The Amazing Spider-Man 2 +Sally Field,1946,The Amazing Spider-Man 2 +Embeth Davidtz,1965,The Amazing Spider-Man 2 +Luke Evans,1979,Dracula Untold +Sarah Gadon,1987,Dracula Untold +Dominic Cooper,1978,Dracula Untold +Art Parkinson,2001,Dracula Untold +Charles Dance,1946,Dracula Untold +Paul Kaye,1965,Dracula Untold +William Houston,1968,Dracula Untold +Noah Huntley,1974,Dracula Untold +Diego Luna,1979,The Book of Life +Zoe Saldana,1978,The Book of Life +Channing Tatum,1980,The Book of Life +Ron Perlman,1950,The Book of Life +Christina Applegate,1971,The Book of Life +Ice Cube,1969,The Book of Life +Kate del Castillo,1972,The Book of Life +Hector Elizondo,1936,The Book of Life +Danny Trejo,1944,The Book of Life +Frank Grillo,1965,The Purge: Anarchy +Carmen Ejogo,1973,The Purge: Anarchy +Zach Gilford,1982,The Purge: Anarchy +Kiele Sanchez,1977,The Purge: Anarchy +Justina Machado,1972,The Purge: Anarchy +John Beasley,1943,The Purge: Anarchy +Noel Gugliemi,1970,The Purge: Anarchy +Chris Evans,1981,Captain America: The Winter Soldier +Samuel L. Jackson,1948,Captain America: The Winter Soldier +Scarlett Johansson,1984,Captain America: The Winter Soldier +Robert Redford,1936,Captain America: The Winter Soldier +Sebastian Stan,1982,Captain America: The Winter Soldier +Anthony Mackie,1978,Captain America: The Winter Soldier +Cobie Smulders,1982,Captain America: The Winter Soldier +Frank Grillo,1965,Captain America: The Winter Soldier +Mark Wahlberg,1971,Transformers: Age of Extinction +Stanley Tucci,1960,Transformers: Age of Extinction +Kelsey Grammer,1955,Transformers: Age of Extinction +Nicola Peltz,1995,Transformers: Age of Extinction +Jack Reynor,1992,Transformers: Age of Extinction +Titus Welliver,1961,Transformers: Age of Extinction +Sophia Myles,1980,Transformers: Age of Extinction +Bingbing Li,1973,Transformers: Age of Extinction +T.J. Miller,1981,Transformers: Age of Extinction +Megan Fox,1986,Teenage Mutant Ninja Turtles +Will Arnett,1970,Teenage Mutant Ninja Turtles +William Fichtner,1956,Teenage Mutant Ninja Turtles +Alan Ritchson,1984,Teenage Mutant Ninja Turtles +Noel Fisher,1984,Teenage Mutant Ninja Turtles +Pete Ploszek,1987,Teenage Mutant Ninja Turtles +Johnny Knoxville,1971,Teenage Mutant Ninja Turtles +Jeremy Howard,1981,Teenage Mutant Ninja Turtles +Danny Woodburn,1964,Teenage Mutant Ninja Turtles +Tom Ellis,1978,Lucifer +Lauren German,1978,Lucifer +Kevin Alejandro,1976,Lucifer +D.B. Woodside,1969,Lucifer +Rachael Harris,1968,Lucifer +Tricia Helfer,1974,Lucifer +Aimee Garcia,1978,Lucifer +Ian McKellen,1939,The Hobbit: The Battle of the Five Armies +Martin Freeman,1971,The Hobbit: The Battle of the Five Armies +Richard Armitage,1971,The Hobbit: The Battle of the Five Armies +Ken Stott,1955,The Hobbit: The Battle of the Five Armies +Graham McTavish,1961,The Hobbit: The Battle of the Five Armies +James Nesbitt,1965,The Hobbit: The Battle of the Five Armies +Dean O'Gorman,1976,The Hobbit: The Battle of the Five Armies +Laura Allen,1974,Clown +Peter Stormare,1953,Clown +Christian Distefano,2005,Clown +Elizabeth Whitmere,1981,Clown +Michael Keaton,1951,Birdman or (The Unexpected Virtue of Ignorance) +Emma Stone,1988,Birdman or (The Unexpected Virtue of Ignorance) +Zach Galifianakis,1969,Birdman or (The Unexpected Virtue of Ignorance) +Naomi Watts,1968,Birdman or (The Unexpected Virtue of Ignorance) +Andrea Riseborough,1981,Birdman or (The Unexpected Virtue of Ignorance) +Bruce Campbell,1958,The Evil Dead +Ellen Sandweiss,1958,The Evil Dead +Betsy Baker,1955,The Evil Dead +Theresa Tilly,1953,The Evil Dead +Jeff Bridges,1949,The Giver +Meryl Streep,1949,The Giver +Brenton Thwaites,1989,The Giver +Katie Holmes,1978,The Giver +Odeya Rush,1997,The Giver +Cameron Monaghan,1993,The Giver +Taylor Swift,1989,The Giver +Jemaine Clement,1974,What We Do in the Shadows +Taika Waititi,1975,What We Do in the Shadows +Hiroki Hasegawa,1977,Shin Gojira +Yutaka Takenouchi,1971,Shin Gojira +Satomi Ishihara,1986,Shin Gojira +Ren Ohsugi,1951,Shin Gojira +Akira Emoto,1948,Shin Gojira +Mikako Ichikawa,1978,Shin Gojira +Jun Kunimura,1955,Shin Gojira +Pierre Taki,1967,Shin Gojira +Dylan O'Brien,1991,The Maze Runner +Aml Ameen,1985,The Maze Runner +Thomas Brodie-Sangster,1990,The Maze Runner +Will Poulter,1993,The Maze Runner +Kaya Scodelario,1992,The Maze Runner +Chris Sheffield,1988,The Maze Runner +Jake Gyllenhaal,1980,Nightcrawler +Michael Papajohn,1964,Nightcrawler +Bill Paxton,1955,Nightcrawler +James Huang,1977,Nightcrawler +Kent Shocknek,1956,Nightcrawler +Sharon Tay,1966,Nightcrawler +Shailene Woodley,1991,The Fault in Our Stars +Ansel Elgort,1994,The Fault in Our Stars +Nat Wolff,1994,The Fault in Our Stars +Laura Dern,1967,The Fault in Our Stars +Sam Trammell,1969,The Fault in Our Stars +Willem Dafoe,1955,The Fault in Our Stars +Lotte Verbeek,1982,The Fault in Our Stars +Randy Kovitz,1955,The Fault in Our Stars +Dwayne Johnson,1972,Hercules +Ian McShane,1942,Hercules +John Hurt,1940,Hercules +Rufus Sewell,1967,Hercules +Aksel Hennie,1975,Hercules +Reece Ritchie,1986,Hercules +Joseph Fiennes,1970,Hercules +Tobias Santelmann,1980,Hercules +Seth Rogen,1982,Neighbors +Rose Byrne,1979,Neighbors +Brian Huskey,1968,Neighbors +Ike Barinholtz,1977,Neighbors +Carla Gallo,1975,Neighbors +Zac Efron,1987,Neighbors +Dave Franco,1985,Neighbors +Sylvester Stallone,1946,The Expendables 3 +Jason Statham,1967,The Expendables 3 +Harrison Ford,1942,The Expendables 3 +Arnold Schwarzenegger,1947,The Expendables 3 +Mel Gibson,1956,The Expendables 3 +Wesley Snipes,1962,The Expendables 3 +Dolph Lundgren,1957,The Expendables 3 +Randy Couture,1963,The Expendables 3 +Terry Crews,1968,The Expendables 3 +Will Arnett,1970,The Lego Movie +Elizabeth Banks,1974,The Lego Movie +Alison Brie,1982,The Lego Movie +Anthony Daniels,1946,The Lego Movie +Charlie Day,1976,The Lego Movie +Keith Ferguson,1972,The Lego Movie +Lily Collins,1989,"Love, Rosie" +Sam Claflin,1986,"Love, Rosie" +Christian Cooke,1987,"Love, Rosie" +Jaime Winstone,1985,"Love, Rosie" +Suki Waterhouse,1992,"Love, Rosie" +Tamsin Egerton,1988,"Love, Rosie" +Jamie Beamish,1976,"Love, Rosie" +Lorcan Cranitch,1959,"Love, Rosie" +Ellar Coltrane,1994,Boyhood +Patricia Arquette,1968,Boyhood +Lorelei Linklater,1994,Boyhood +Ethan Hawke,1970,Boyhood +Bradley Cooper,1975,American Sniper +Kyle Gallner,1986,American Sniper +Ben Reed,1965,American Sniper +Keir O'Donnell,1978,American Sniper +Scott Adsit,1965,Big Hero 6 +Daniel Henney,1979,Big Hero 6 +T.J. Miller,1981,Big Hero 6 +Jamie Chung,1983,Big Hero 6 +Damon Wayans Jr.,1982,Big Hero 6 +Genesis Rodriguez,1987,Big Hero 6 +James Cromwell,1940,Big Hero 6 +Alan Tudyk,1971,Big Hero 6 +Jennifer Lawrence,1990,The Hunger Games: Mockingjay - Part 1 +Josh Hutcherson,1992,The Hunger Games: Mockingjay - Part 1 +Liam Hemsworth,1990,The Hunger Games: Mockingjay - Part 1 +Woody Harrelson,1961,The Hunger Games: Mockingjay - Part 1 +Donald Sutherland,1935,The Hunger Games: Mockingjay - Part 1 +Philip Seymour Hoffman,1967,The Hunger Games: Mockingjay - Part 1 +Julianne Moore,1960,The Hunger Games: Mockingjay - Part 1 +Willow Shields,2000,The Hunger Games: Mockingjay - Part 1 +Sam Claflin,1986,The Hunger Games: Mockingjay - Part 1 +Russell Crowe,1964,Noah +Jennifer Connelly,1970,Noah +Ray Winstone,1957,Noah +Anthony Hopkins,1937,Noah +Emma Watson,1990,Noah +Logan Lerman,1992,Noah +Douglas Booth,1992,Noah +Nick Nolte,1941,Noah +Mark Margolis,1939,Noah +Ethan Hawke,1970,Predestination +Jack O'Connell,1990,Unbroken +Domhnall Gleeson,1983,Unbroken +Garrett Hedlund,1984,Unbroken +Finn Wittrock,1984,Unbroken +Jai Courtney,1986,Unbroken +Vincenzo Amato,1966,Unbroken +Amy Adams,1974,Big Eyes +Christoph Waltz,1956,Big Eyes +Danny Huston,1962,Big Eyes +Krysten Ritter,1981,Big Eyes +Jason Schwartzman,1980,Big Eyes +Terence Stamp,1938,Big Eyes +Jon Polito,1950,Big Eyes +Madeleine Arthur,1997,Big Eyes +Michael Pitt,1981,I Origins +Steven Yeun,1983,I Origins +Brit Marling,1982,I Origins +Charles Woods Gray,1949,I Origins +Joanna Newsom,1982,Inherent Vice +Katherine Waterston,1980,Inherent Vice +Joaquin Phoenix,1974,Inherent Vice +Jeannie Berlin,1949,Inherent Vice +Josh Brolin,1968,Inherent Vice +Eric Roberts,1956,Inherent Vice +Serena Scott Thomas,1961,Inherent Vice +Rita Cortese,1949,Relatos salvajes +Julieta Zylberberg,1983,Relatos salvajes +Leonardo Sbaraglia,1970,Relatos salvajes +Jill Larson,1947,The Taking +Anne Ramsay,1960,The Taking +Michelle Ang,1983,The Taking +Brett Gentile,1973,The Taking +Ryan Cutrona,1949,The Taking +Anne Bedian,1972,The Taking +Sullivan Stapleton,1977,300: Rise of an Empire +Eva Green,1980,300: Rise of an Empire +Lena Headey,1973,300: Rise of an Empire +Hans Matheson,1975,300: Rise of an Empire +Callan Mulvey,1975,300: Rise of an Empire +David Wenham,1965,300: Rise of an Empire +Rodrigo Santoro,1975,300: Rise of an Empire +Jack O'Connell,1990,300: Rise of an Empire +Andrew Tiernan,1965,300: Rise of an Empire +Jonah Hill,1983,22 Jump Street +Channing Tatum,1980,22 Jump Street +Peter Stormare,1953,22 Jump Street +Wyatt Russell,1986,22 Jump Street +Amber Stevens West,1986,22 Jump Street +Ice Cube,1969,22 Jump Street +Michelle Monaghan,1976,The Best of Me +James Marsden,1973,The Best of Me +Liana Liberato,1995,The Best of Me +Gerald McRaney,1947,The Best of Me +Caroline Goodall,1959,The Best of Me +Clarke Peters,1952,The Best of Me +Sebastian Arcelus,1976,The Best of Me +Jon Tenney,1961,The Best of Me +Zoey Deutch,1994,Vampire Academy +Danila Kozlovsky,1985,Vampire Academy +Gabriel Byrne,1950,Vampire Academy +Olga Kurylenko,1979,Vampire Academy +Sarah Hyland,1990,Vampire Academy +Cameron Monaghan,1993,Vampire Academy +Sami Gayle,1996,Vampire Academy +Jeff Bridges,1949,Seventh Son +Ben Barnes,1981,Seventh Son +Julianne Moore,1960,Seventh Son +Alicia Vikander,1988,Seventh Son +Antje Traue,1981,Seventh Son +Olivia Williams,1968,Seventh Son +Kit Harington,1986,Seventh Son +Djimon Hounsou,1964,Seventh Son +Eric Bana,1968,Deliver Us from Evil +Olivia Munn,1980,Deliver Us from Evil +Chris Coy,1986,Deliver Us from Evil +Dorian Missick,1976,Deliver Us from Evil +Sean Harris,1966,Deliver Us from Evil +Joel McHale,1971,Deliver Us from Evil +Mike Houston,1976,Deliver Us from Evil +Andy Serkis,1964,Dawn of the Planet of the Apes +Jason Clarke,1969,Dawn of the Planet of the Apes +Gary Oldman,1958,Dawn of the Planet of the Apes +Keri Russell,1976,Dawn of the Planet of the Apes +Toby Kebbell,1982,Dawn of the Planet of the Apes +Kodi Smit-McPhee,1996,Dawn of the Planet of the Apes +Kirk Acevedo,1971,Dawn of the Planet of the Apes +Christian Bale,1974,Exodus: Gods and Kings +Joel Edgerton,1974,Exodus: Gods and Kings +John Turturro,1957,Exodus: Gods and Kings +Aaron Paul,1979,Exodus: Gods and Kings +Ben Mendelsohn,1969,Exodus: Gods and Kings +Sigourney Weaver,1949,Exodus: Gods and Kings +Ben Kingsley,1943,Exodus: Gods and Kings +Annabelle Wallis,1984,Annabelle +Ward Horton,1976,Annabelle +Tony Amendola,1951,Annabelle +Alfre Woodard,1952,Annabelle +Eric Ladin,1978,Annabelle +Ivar Brogger,1947,Annabelle +Karl Urban,1972,The Loft +James Marsden,1973,The Loft +Wentworth Miller,1972,The Loft +Eric Stonestreet,1971,The Loft +Matthias Schoenaerts,1977,The Loft +Isabel Lucas,1985,The Loft +Rachael Taylor,1984,The Loft +Rhona Mitra,1976,The Loft +Valerie Cruz,1976,The Loft +Michael Parks,1940,Tusk +Justin Long,1978,Tusk +Genesis Rodriguez,1987,Tusk +Haley Joel Osment,1988,Tusk +Johnny Depp,1963,Tusk +Ralph Garman,1964,Tusk +Jennifer Schwalbach Smith,1971,Tusk +Harley Quinn Smith,1999,Tusk +James Franco,1978,The Interview +Seth Rogen,1982,The Interview +Lizzy Caplan,1982,The Interview +Timothy Simons,1978,The Interview +Ryan Reynolds,1976,The Voices +Gemma Arterton,1986,The Voices +Anna Kendrick,1985,The Voices +Jacki Weaver,1947,The Voices +Ella Smith,1983,The Voices +Sam Spruell,1977,The Voices +Aaron Eckhart,1968,"I, Frankenstein" +Yvonne Strahovski,1982,"I, Frankenstein" +Miranda Otto,1967,"I, Frankenstein" +Bill Nighy,1949,"I, Frankenstein" +Jai Courtney,1986,"I, Frankenstein" +Aden Young,1972,"I, Frankenstein" +Caitlin Stasey,1990,"I, Frankenstein" +Mahesh Jadu,1982,"I, Frankenstein" +Robert Downey Jr.,1965,The Judge +Robert Duvall,1931,The Judge +Vera Farmiga,1973,The Judge +Billy Bob Thornton,1955,The Judge +Vincent D'Onofrio,1959,The Judge +Dax Shepard,1975,The Judge +Leighton Meester,1986,The Judge +Ken Howard,1944,The Judge +Bradley Cooper,1975,Burnt +Sienna Miller,1981,Burnt +Riccardo Scamarcio,1979,Burnt +Omar Sy,1978,Burnt +Henry Goodman,1950,Burnt +Matthew Rhys,1974,Burnt +Stephen Campbell Moore,1979,Burnt +Mireille Enos,1975,If I Stay +Jamie Blackley,1991,If I Stay +Joshua Leonard,1975,If I Stay +Liana Liberato,1995,If I Stay +Stacy Keach,1941,If I Stay +Gabrielle Rose,1954,If I Stay +Jakob Davies,2003,If I Stay +Perdita Weeks,1985,"As Above, So Below" +Ben Feldman,1980,"As Above, So Below" +Edwin Hodge,1985,"As Above, So Below" +Kevin Costner,1955,3 Days to Kill +Amber Heard,1986,3 Days to Kill +Hailee Steinfeld,1996,3 Days to Kill +Connie Nielsen,1965,3 Days to Kill +Richard Sammel,1960,3 Days to Kill +Chris Evans,1981,Before We Go +Alice Eve,1982,Before We Go +Emma Fitzpatrick,1985,Before We Go +John Cullum,1930,Before We Go +Mark Kassen,1971,Before We Go +Daniel Spink,1979,Before We Go +Liam Neeson,1952,Non-Stop +Julianne Moore,1960,Non-Stop +Scoot McNairy,1977,Non-Stop +Michelle Dockery,1981,Non-Stop +Nate Parker,1979,Non-Stop +Corey Stoll,1976,Non-Stop +Lupita Nyong'o,1983,Non-Stop +Omar Metwally,1974,Non-Stop +Jason Butler Harner,1970,Non-Stop +Zac Efron,1987,That Awkward Moment +Miles Teller,1987,That Awkward Moment +Michael B. Jordan,1987,That Awkward Moment +Imogen Poots,1989,That Awkward Moment +Mackenzie Davis,1987,That Awkward Moment +Jessica Lucas,1985,That Awkward Moment +Addison Timlin,1991,That Awkward Moment +Josh Pais,1964,That Awkward Moment +Vincent Price,1911,Vincent +Adam Sandler,1966,Blended +Drew Barrymore,1975,Blended +Kevin Nealon,1953,Blended +Terry Crews,1968,Blended +Wendi McLendon-Covey,1969,Blended +Bella Thorne,1997,Blended +Dan Stevens,1982,The Guest +Maika Monroe,1993,The Guest +Brendan Meyer,1994,The Guest +Sheila Kelley,1961,The Guest +Leland Orser,1960,The Guest +Lance Reddick,1962,The Guest +Chase Williamson,1988,The Guest +Joel David Moore,1977,The Guest +Cameron Diaz,1972,The Other Woman +Leslie Mann,1972,The Other Woman +Nikolaj Coster-Waldau,1970,The Other Woman +Don Johnson,1949,The Other Woman +Kate Upton,1992,The Other Woman +Taylor Kinney,1981,The Other Woman +Nicki Minaj,1982,The Other Woman +Nicolas Cage,1964,Left Behind +Chad Michael Murray,1981,Left Behind +Cassi Thomson,1993,Left Behind +Jordin Sparks,1989,Left Behind +Lea Thompson,1961,Left Behind +Gary Grubbs,1949,Left Behind +Quinton Aaron,1984,Left Behind +Martin Klebba,1969,Left Behind +Jamie Foxx,1967,Annie +Rose Byrne,1979,Annie +Bobby Cannavale,1970,Annie +Adewale Akinnuoye-Agbaje,1967,Annie +David Zayas,1962,Annie +Cameron Diaz,1972,Annie +Nicolette Pierini,2003,Annie +Heather Sossaman,1987,Cybernatural +Courtney Halverson,1989,Cybernatural +Shelley Hennig,1987,Cybernatural +Will Peltz,1986,Cybernatural +Renee Olstead,1989,Cybernatural +Mark Wahlberg,1971,The Gambler +George Kennedy,1925,The Gambler +Jessica Lange,1949,The Gambler +Brie Larson,1989,The Gambler +Michael Kenneth Williams,1966,The Gambler +Mads Mikkelsen,1965,The Salvation +Eva Green,1980,The Salvation +Jeffrey Dean Morgan,1966,The Salvation +Eric Cantona,1966,The Salvation +Mikael Persbrandt,1963,The Salvation +Douglas Henshall,1965,The Salvation +Michael Raymond-James,1977,The Salvation +Jonathan Pryce,1947,The Salvation +Seth MacFarlane,1973,A Million Ways to Die in the West +Charlize Theron,1975,A Million Ways to Die in the West +Amanda Seyfried,1985,A Million Ways to Die in the West +Liam Neeson,1952,A Million Ways to Die in the West +Giovanni Ribisi,1974,A Million Ways to Die in the West +Neil Patrick Harris,1973,A Million Ways to Die in the West +Sarah Silverman,1970,A Million Ways to Die in the West +Wes Studi,1947,A Million Ways to Die in the West +Alicia Vikander,1988,Testament of Youth +Taron Egerton,1989,Testament of Youth +Colin Morgan,1986,Testament of Youth +Dominic West,1969,Testament of Youth +Emily Watson,1967,Testament of Youth +Kit Harington,1986,Testament of Youth +Joanna Scanlan,1961,Testament of Youth +Miranda Richardson,1958,Testament of Youth +Sam Neill,1947,Hunt for the Wilderpeople +Rima Te Wiata,1963,Hunt for the Wilderpeople +Johnny Depp,1963,Transcendence +Rebecca Hall,1982,Transcendence +Paul Bettany,1971,Transcendence +Cillian Murphy,1976,Transcendence +Kate Mara,1983,Transcendence +Cole Hauser,1975,Transcendence +Morgan Freeman,1937,Transcendence +Clifton Collins Jr.,1970,Transcendence +Cory Hardrict,1979,Transcendence +Cameron Diaz,1972,Sex Tape +Jason Segel,1980,Sex Tape +Rob Corddry,1971,Sex Tape +Ellie Kemper,1980,Sex Tape +Rob Lowe,1964,Sex Tape +Nancy Lenehan,1953,Sex Tape +Jessica Sula,1994,Honeytrap +Lucien Laviscount,1992,Honeytrap +Naomi Ryan,1977,Honeytrap +Asa Butterfield,1997,X+Y +Rafe Spall,1983,X+Y +Sally Hawkins,1976,X+Y +Eddie Marsan,1968,X+Y +Martin McCann,1983,X+Y +Jake Davies,1993,X+Y +Mark Duplass,1976,Creep +Tom Hardy,1977,The Drop +Noomi Rapace,1979,The Drop +James Gandolfini,1961,The Drop +Matthias Schoenaerts,1977,The Drop +John Ortiz,1968,The Drop +Elizabeth Rodriguez,1980,The Drop +Michael Esper,1976,The Drop +Susanne Wuest,1979,Ich seh ich seh +Kate Beckinsale,1973,Eliza Graves +Jim Sturgess,1978,Eliza Graves +David Thewlis,1963,Eliza Graves +Brendan Gleeson,1955,Eliza Graves +Ben Kingsley,1943,Eliza Graves +Michael Caine,1933,Eliza Graves +Jason Flemyng,1966,Eliza Graves +Jason Bateman,1969,Horrible Bosses 2 +Jason Sudeikis,1975,Horrible Bosses 2 +Charlie Day,1976,Horrible Bosses 2 +Jennifer Aniston,1969,Horrible Bosses 2 +Kevin Spacey,1959,Horrible Bosses 2 +Jamie Foxx,1967,Horrible Bosses 2 +Chris Pine,1980,Horrible Bosses 2 +Christoph Waltz,1956,Horrible Bosses 2 +Jonathan Banks,1947,Horrible Bosses 2 +Chris Pine,1980,Jack Ryan: Shadow Recruit +Keira Knightley,1985,Jack Ryan: Shadow Recruit +Kevin Costner,1955,Jack Ryan: Shadow Recruit +Kenneth Branagh,1960,Jack Ryan: Shadow Recruit +Lenn Kudrjawizki,1975,Jack Ryan: Shadow Recruit +Peter Andersson,1953,Jack Ryan: Shadow Recruit +Elena Velikanova,1984,Jack Ryan: Shadow Recruit +Nonso Anozie,1979,Jack Ryan: Shadow Recruit +Kate Mara,1983,Man Down +Shia LaBeouf,1986,Man Down +Jai Courtney,1986,Man Down +Clifton Collins Jr.,1970,Man Down +Gary Oldman,1958,Man Down +Jose Pablo Cantillo,1979,Man Down +Harrison Ford,1942,Star Wars: Episode VII - The Force Awakens +Mark Hamill,1951,Star Wars: Episode VII - The Force Awakens +Carrie Fisher,1956,Star Wars: Episode VII - The Force Awakens +Adam Driver,1983,Star Wars: Episode VII - The Force Awakens +Daisy Ridley,1992,Star Wars: Episode VII - The Force Awakens +John Boyega,1992,Star Wars: Episode VII - The Force Awakens +Oscar Isaac,1979,Star Wars: Episode VII - The Force Awakens +Lupita Nyong'o,1983,Star Wars: Episode VII - The Force Awakens +Andy Serkis,1964,Star Wars: Episode VII - The Force Awakens +Leonardo DiCaprio,1974,The Revenant +Tom Hardy,1977,The Revenant +Domhnall Gleeson,1983,The Revenant +Will Poulter,1993,The Revenant +Forrest Goodluck,1998,The Revenant +Kristoffer Joner,1972,The Revenant +Jennifer Lawrence,1990,Joy +Robert De Niro,1943,Joy +Bradley Cooper,1975,Joy +Diane Ladd,1935,Joy +Virginia Madsen,1961,Joy +Isabella Rossellini,1952,Joy +Daniel Craig,1968,Spectre +Christoph Waltz,1956,Spectre +Ralph Fiennes,1962,Spectre +Monica Bellucci,1964,Spectre +Ben Whishaw,1980,Spectre +Naomie Harris,1976,Spectre +Dave Bautista,1969,Spectre +Andrew Scott,1976,Spectre +Anya Taylor-Joy,1996,The VVitch: A New-England Folktale +Ralph Ineson,1969,The VVitch: A New-England Folktale +Kate Dickie,1971,The VVitch: A New-England Folktale +Julian Richings,1955,The VVitch: A New-England Folktale +Sarah Stephens,1990,The VVitch: A New-England Folktale +Ryan Gosling,1980,The Big Short +Charlie Talbert,1978,The Big Short +Christian Bale,1974,The Big Short +Chris Pratt,1979,Jurassic World +Bryce Dallas Howard,1981,Jurassic World +Irrfan Khan,1967,Jurassic World +Vincent D'Onofrio,1959,Jurassic World +Nick Robinson,1995,Jurassic World +Jake Johnson,1978,Jurassic World +Omar Sy,1978,Jurassic World +BD Wong,1960,Jurassic World +Tom Hardy,1977,Mad Max: Fury Road +Charlize Theron,1975,Mad Max: Fury Road +Nicholas Hoult,1989,Mad Max: Fury Road +Hugh Keays-Byrne,1947,Mad Max: Fury Road +Josh Helman,1986,Mad Max: Fury Road +Nathan Jones,1969,Mad Max: Fury Road +Rosie Huntington-Whiteley,1987,Mad Max: Fury Road +Riley Keough,1989,Mad Max: Fury Road +Matt Damon,1970,The Martian +Jessica Chastain,1977,The Martian +Kristen Wiig,1973,The Martian +Jeff Daniels,1955,The Martian +Sean Bean,1959,The Martian +Kate Mara,1983,The Martian +Sebastian Stan,1982,The Martian +Aksel Hennie,1975,The Martian +Samuel L. Jackson,1948,The Hateful Eight +Kurt Russell,1951,The Hateful Eight +Jennifer Jason Leigh,1962,The Hateful Eight +Walton Goggins,1971,The Hateful Eight +Tim Roth,1961,The Hateful Eight +Michael Madsen,1958,The Hateful Eight +Bruce Dern,1936,The Hateful Eight +James Parks,1968,The Hateful Eight +Alicia Vikander,1988,The Danish Girl +Eddie Redmayne,1982,The Danish Girl +Amber Heard,1986,The Danish Girl +Emerald Fennell,1985,The Danish Girl +Claus Bue,1947,The Danish Girl +Emily Blunt,1983,Sicario +Benicio Del Toro,1967,Sicario +Josh Brolin,1968,Sicario +Victor Garber,1949,Sicario +Jon Bernthal,1976,Sicario +Daniel Kaluuya,1989,Sicario +Jeffrey Donovan,1968,Sicario +Raoul Max Trujillo,1955,Sicario +Robert Downey Jr.,1965,Avengers: Age of Ultron +Chris Hemsworth,1983,Avengers: Age of Ultron +Mark Ruffalo,1967,Avengers: Age of Ultron +Chris Evans,1981,Avengers: Age of Ultron +Scarlett Johansson,1984,Avengers: Age of Ultron +Jeremy Renner,1971,Avengers: Age of Ultron +James Spader,1960,Avengers: Age of Ultron +Samuel L. Jackson,1948,Avengers: Age of Ultron +Don Cheadle,1964,Avengers: Age of Ultron +Mark Ruffalo,1967,Spotlight +Michael Keaton,1951,Spotlight +Rachel McAdams,1978,Spotlight +Liev Schreiber,1967,Spotlight +John Slattery,1962,Spotlight +Brian d'Arcy James,1968,Spotlight +Stanley Tucci,1960,Spotlight +Elena Wohl,1963,Spotlight +Brie Larson,1989,Room +Jacob Tremblay,2006,Room +Sean Bridgers,1968,Room +Wendy Crewson,1956,Room +Amanda Brugel,1978,Room +Joan Allen,1956,Room +Dakota Johnson,1989,Fifty Shades of Grey +Jamie Dornan,1982,Fifty Shades of Grey +Jennifer Ehle,1969,Fifty Shades of Grey +Eloise Mumford,1986,Fifty Shades of Grey +Victor Rasuk,1984,Fifty Shades of Grey +Luke Grimes,1984,Fifty Shades of Grey +Marcia Gay Harden,1959,Fifty Shades of Grey +Rita Ora,1990,Fifty Shades of Grey +Max Martini,1969,Fifty Shades of Grey +Domhnall Gleeson,1983,Ex Machina +Corey Johnson,1961,Ex Machina +Oscar Isaac,1979,Ex Machina +Alicia Vikander,1988,Ex Machina +Roger Ashton-Griffiths,1957,The Lobster +Jessica Barden,1992,The Lobster +Olivia Colman,1974,The Lobster +Colin Farrell,1976,The Lobster +Sam Rockwell,1968,Mr. Right +Anna Kendrick,1985,Mr. Right +Tim Roth,1961,Mr. Right +James Ransone,1979,Mr. Right +Anson Mount,1973,Mr. Right +RZA,1969,Mr. Right +Tom Cruise,1962,Mission: Impossible - Rogue Nation +Jeremy Renner,1971,Mission: Impossible - Rogue Nation +Simon Pegg,1970,Mission: Impossible - Rogue Nation +Rebecca Ferguson,1983,Mission: Impossible - Rogue Nation +Ving Rhames,1959,Mission: Impossible - Rogue Nation +Sean Harris,1966,Mission: Impossible - Rogue Nation +Simon McBurney,1957,Mission: Impossible - Rogue Nation +Jingchu Zhang,1980,Mission: Impossible - Rogue Nation +Tom Hollander,1967,Mission: Impossible - Rogue Nation +Jennifer Lawrence,1990,The Hunger Games: Mockingjay - Part 2 +Josh Hutcherson,1992,The Hunger Games: Mockingjay - Part 2 +Liam Hemsworth,1990,The Hunger Games: Mockingjay - Part 2 +Woody Harrelson,1961,The Hunger Games: Mockingjay - Part 2 +Donald Sutherland,1935,The Hunger Games: Mockingjay - Part 2 +Philip Seymour Hoffman,1967,The Hunger Games: Mockingjay - Part 2 +Julianne Moore,1960,The Hunger Games: Mockingjay - Part 2 +Willow Shields,2000,The Hunger Games: Mockingjay - Part 2 +Sam Claflin,1986,The Hunger Games: Mockingjay - Part 2 +Hilary Swank,1974,P.S. I Love You +Gerard Butler,1969,P.S. I Love You +Lisa Kudrow,1963,P.S. I Love You +Gina Gershon,1962,P.S. I Love You +James Marsters,1962,P.S. I Love You +Kathy Bates,1948,P.S. I Love You +Harry Connick Jr.,1967,P.S. I Love You +Nellie McKay,1982,P.S. I Love You +Jeffrey Dean Morgan,1966,P.S. I Love You +Johnny Depp,1963,Black Mass +Joel Edgerton,1974,Black Mass +Benedict Cumberbatch,1976,Black Mass +Dakota Johnson,1989,Black Mass +Kevin Bacon,1958,Black Mass +Peter Sarsgaard,1971,Black Mass +Jesse Plemons,1988,Black Mass +Rory Cochrane,1972,Black Mass +David Harbour,1975,Black Mass +Chris Hemsworth,1983,In the Heart of the Sea +Benjamin Walker,1982,In the Heart of the Sea +Cillian Murphy,1976,In the Heart of the Sea +Brendan Gleeson,1955,In the Heart of the Sea +Ben Whishaw,1980,In the Heart of the Sea +Tom Holland,1996,In the Heart of the Sea +Frank Dillane,1991,In the Heart of the Sea +Brad Pitt,1963,By the Sea +Angelina Jolie,1975,By the Sea +Melvil Poupaud,1973,By the Sea +Niels Arestrup,1949,By the Sea +Richard Bohringer,1942,By the Sea +Marika Green,1943,By the Sea +Keanu Reeves,1964,Knock Knock +Lorenza Izzo,1989,Knock Knock +Ana de Armas,1988,Knock Knock +Aaron Burns,1985,Knock Knock +Ignacia Allamand,1981,Knock Knock +Colleen Camp,1953,Knock Knock +Amy Poehler,1971,Inside Out +Phyllis Smith,1951,Inside Out +Richard Kind,1956,Inside Out +Bill Hader,1978,Inside Out +Lewis Black,1948,Inside Out +Mindy Kaling,1979,Inside Out +Diane Lane,1965,Inside Out +Kyle MacLachlan,1959,Inside Out +O'Shea Jackson Jr.,1991,Straight Outta Compton +Corey Hawkins,1988,Straight Outta Compton +Neil Brown Jr.,1980,Straight Outta Compton +Aldis Hodge,1986,Straight Outta Compton +Paul Rudd,1969,Ant-Man +Michael Douglas,1944,Ant-Man +Evangeline Lilly,1979,Ant-Man +Corey Stoll,1976,Ant-Man +Bobby Cannavale,1970,Ant-Man +Anthony Mackie,1978,Ant-Man +Judy Greer,1975,Ant-Man +Logan Marshall-Green,1976,The Invitation +Emayatzy Corinealdi,1980,The Invitation +Michelle Krusiec,1974,The Invitation +Jordi Vilasuso,1981,The Invitation +Marieh Delfino,1977,The Invitation +Tammy Blanchard,1976,The Invitation +Saoirse Ronan,1994,Brooklyn +Brid Brennan,1955,Brooklyn +Fiona Glascott,1982,Brooklyn +Mark Rylance,1960,Bridge of Spies +Domenick Lombardozzi,1976,Bridge of Spies +Tom Hanks,1956,Bridge of Spies +Joshua Harto,1979,Bridge of Spies +Cate Blanchett,1969,Cinderella +Lily James,1989,Cinderella +Richard Madden,1986,Cinderella +Helena Bonham Carter,1966,Cinderella +Nonso Anozie,1979,Cinderella +Sophie McShera,1985,Cinderella +Holliday Grainger,1988,Cinderella +Derek Jacobi,1938,Cinderella +Anton Yelchin,1989,Green Room +Joe Cole,1988,Green Room +Alia Shawkat,1989,Green Room +Mark Webber,1980,Green Room +Eric Edelstein,1977,Green Room +Michael Fassbender,1977,Steve Jobs +Kate Winslet,1975,Steve Jobs +Seth Rogen,1982,Steve Jobs +Jeff Daniels,1955,Steve Jobs +Michael Stuhlbarg,1968,Steve Jobs +Katherine Waterston,1980,Steve Jobs +Perla Haney-Jardine,1997,Steve Jobs +Emjay Anthony,2003,Krampus +Adam Scott,1973,Krampus +Toni Collette,1972,Krampus +Stefania LaVie Owen,1997,Krampus +Krista Stadler,1942,Krampus +Conchata Ferrell,1943,Krampus +Allison Tolman,1981,Krampus +David Koechner,1962,Krampus +Vin Diesel,1967,Furious Seven +Paul Walker,1973,Furious Seven +Jason Statham,1967,Furious Seven +Michelle Rodriguez,1978,Furious Seven +Jordana Brewster,1980,Furious Seven +Tyrese Gibson,1978,Furious Seven +Ludacris,1977,Furious Seven +Dwayne Johnson,1972,Furious Seven +Lucas Black,1982,Furious Seven +Mia Wasikowska,1989,Crimson Peak +Jessica Chastain,1977,Crimson Peak +Tom Hiddleston,1981,Crimson Peak +Charlie Hunnam,1980,Crimson Peak +Jim Beaver,1950,Crimson Peak +Burn Gorman,1974,Crimson Peak +Leslie Hope,1965,Crimson Peak +Doug Jones,1960,Crimson Peak +Jonathan Hyde,1948,Crimson Peak +Russell Crowe,1964,Fathers & Daughters +Amanda Seyfried,1985,Fathers & Daughters +Aaron Paul,1979,Fathers & Daughters +Diane Kruger,1976,Fathers & Daughters +Bruce Greenwood,1956,Fathers & Daughters +Janet McTeer,1961,Fathers & Daughters +Kylie Rogers,2004,Fathers & Daughters +Jane Fonda,1937,Fathers & Daughters +Arnold Schwarzenegger,1947,Terminator Genisys +Jason Clarke,1969,Terminator Genisys +Emilia Clarke,1986,Terminator Genisys +Jai Courtney,1986,Terminator Genisys +J.K. Simmons,1955,Terminator Genisys +Matt Smith,1982,Terminator Genisys +Courtney B. Vance,1960,Terminator Genisys +Byung-hun Lee,1970,Terminator Genisys +Victor Garber,1949,Legends of Tomorrow +Brandon Routh,1979,Legends of Tomorrow +Caity Lotz,1986,Legends of Tomorrow +Franz Drameh,1993,Legends of Tomorrow +Dominic Purcell,1970,Legends of Tomorrow +Arthur Darvill,1982,Legends of Tomorrow +Wentworth Miller,1972,Legends of Tomorrow +Jude Law,1972,Spy +Melissa McCarthy,1970,Spy +Miranda Hart,1972,Spy +Helen Mirren,1945,Eye in the Sky +Aaron Paul,1979,Eye in the Sky +Jason Clarke,1969,Everest +Thomas M. Wright,1983,Everest +Martin Henderson,1974,Everest +Tom Goodman-Hill,1968,Everest +Jeremy Irons,1948,The Man Who Knew Infinity +Dev Patel,1990,The Man Who Knew Infinity +Malcolm Sinclair,1950,The Man Who Knew Infinity +Dhritiman Chatterjee,1945,The Man Who Knew Infinity +Stephen Fry,1957,The Man Who Knew Infinity +Olivia DeJonge,1998,The Visit +Ed Oxenbould,2001,The Visit +Deanna Dunagan,1940,The Visit +Peter McRobbie,1943,The Visit +Kathryn Hahn,1973,The Visit +Cate Blanchett,1969,Carol +Rooney Mara,1985,Carol +Kyle Chandler,1965,Carol +Sarah Paulson,1974,Carol +Kevin Crowley,1958,Carol +Tia Mowry-Hardrict,1978,"Sister, Sister" +Tamera Mowry-Housley,1978,"Sister, Sister" +Tim Reid,1944,"Sister, Sister" +Marques Houston,1981,"Sister, Sister" +Robert De Niro,1943,The Intern +Anne Hathaway,1982,The Intern +Rene Russo,1954,The Intern +Andrew Rannells,1978,The Intern +Adam Devine,1983,The Intern +Jake Gyllenhaal,1980,Demolition +Naomi Watts,1968,Demolition +Chris Cooper,1951,Demolition +Polly Draper,1955,Demolition +Debra Monk,1949,Demolition +Heather Lind,1983,Demolition +Jack Black,1969,Goosebumps +Dylan Minnette,1996,Goosebumps +Odeya Rush,1997,Goosebumps +Ryan Lee,1996,Goosebumps +Amy Ryan,1968,Goosebumps +Halston Sage,1993,Goosebumps +Steven Krueger,1989,Goosebumps +Sharlto Copley,1973,Hardcore Henry +Danila Kozlovsky,1985,Hardcore Henry +Haley Bennett,1988,Hardcore Henry +Tim Roth,1961,Hardcore Henry +Svetlana Ustinova,1982,Hardcore Henry +Darya Charusha,1980,Hardcore Henry +Jeffrey Dean Morgan,1966,Desierto +Yuri Zackoqv,1993,Desierto +Jodelle Ferland,1994,The Unspoken +Pascale Hutton,1979,The Unspoken +Jonathan Whitesell,1991,The Unspoken +Chanelle Peloso,1994,The Unspoken +Rukiya Bernard,1983,The Unspoken +Lochlyn Munro,1966,The Unspoken +Colin Quinn,1959,Trainwreck +Amy Schumer,1981,Trainwreck +Josh Segarra,1986,Trainwreck +Ryan Farrell,1981,Trainwreck +Jim Florentine,1964,Trainwreck +Robert Kelly,1970,Trainwreck +Michael B. Jordan,1987,Creed +Sylvester Stallone,1946,Creed +Tessa Thompson,1983,Creed +Phylicia Rashad,1948,Creed +Andre Ward,1984,Creed +Tony Bellew,1982,Creed +Ritchie Coster,1967,Creed +Graham McTavish,1961,Creed +Jake Gyllenhaal,1980,Southpaw +Rachel McAdams,1978,Southpaw +Forest Whitaker,1961,Southpaw +Oona Laurence,2002,Southpaw +50 Cent,1975,Southpaw +Naomie Harris,1976,Southpaw +Daniel Radcliffe,1989,Victor Frankenstein +Jessica Brown Findlay,1989,Victor Frankenstein +Bronson Webb,1983,Victor Frankenstein +James McAvoy,1979,Victor Frankenstein +Daniel Mays,1978,Victor Frankenstein +Spencer Wilding,1972,Victor Frankenstein +Andrew Scott,1976,Victor Frankenstein +Anna Kendrick,1985,Pitch Perfect 2 +Rebel Wilson,1980,Pitch Perfect 2 +Hailee Steinfeld,1996,Pitch Perfect 2 +Brittany Snow,1986,Pitch Perfect 2 +Skylar Astin,1987,Pitch Perfect 2 +Adam Devine,1983,Pitch Perfect 2 +Katey Sagal,1954,Pitch Perfect 2 +Anna Camp,1982,Pitch Perfect 2 +Ben Platt,1993,Pitch Perfect 2 +Emma Watson,1990,Colonia +Michael Nyqvist,1960,Colonia +Richenda Carey,1948,Colonia +Vicky Krieps,1983,Colonia +Julian Ovenden,1976,Colonia +August Zirner,1956,Colonia +Martin Wuttke,1962,Colonia +Dwayne Johnson,1972,San Andreas +Carla Gugino,1971,San Andreas +Alexandra Daddario,1986,San Andreas +Ioan Gruffudd,1973,San Andreas +Archie Panjabi,1972,San Andreas +Paul Giamatti,1967,San Andreas +Art Parkinson,2001,San Andreas +Will Yun Lee,1971,San Andreas +Blake Lively,1987,The Age of Adaline +Michiel Huisman,1981,The Age of Adaline +Harrison Ford,1942,The Age of Adaline +Ellen Burstyn,1932,The Age of Adaline +Kathy Baker,1950,The Age of Adaline +Amanda Crew,1986,The Age of Adaline +Lynda Boyd,1965,The Age of Adaline +Richard Harmon,1991,The Age of Adaline +Ray Winstone,1957,Point Break +Teresa Palmer,1986,Point Break +Matias Varela,1980,Point Break +Clemens Schick,1972,Point Break +Tobias Santelmann,1980,Point Break +Max Thieriot,1988,Point Break +Delroy Lindo,1952,Point Break +Ellen Page,1987,Into the Forest +Evan Rachel Wood,1987,Into the Forest +Max Minghella,1985,Into the Forest +Callum Keith Rennie,1960,Into the Forest +Wendy Crewson,1956,Into the Forest +Adam Sandler,1966,Hotel Transylvania 2 +Andy Samberg,1978,Hotel Transylvania 2 +Selena Gomez,1992,Hotel Transylvania 2 +Kevin James,1965,Hotel Transylvania 2 +Steve Buscemi,1957,Hotel Transylvania 2 +David Spade,1964,Hotel Transylvania 2 +Keegan-Michael Key,1971,Hotel Transylvania 2 +Fran Drescher,1957,Hotel Transylvania 2 +Hugh Jackman,1968,Pan +Levi Miller,2002,Pan +Garrett Hedlund,1984,Pan +Rooney Mara,1985,Pan +Nonso Anozie,1979,Pan +Amanda Seyfried,1985,Pan +Kathy Burke,1964,Pan +Kate Winslet,1975,The Dressmaker +Liam Hemsworth,1990,The Dressmaker +Hugo Weaving,1960,The Dressmaker +Judy Davis,1955,The Dressmaker +Caroline Goodall,1959,The Dressmaker +Kerry Fox,1966,The Dressmaker +Rebecca Gibney,1964,The Dressmaker +Dylan O'Brien,1991,Maze Runner: The Scorch Trials +Kaya Scodelario,1992,Maze Runner: The Scorch Trials +Thomas Brodie-Sangster,1990,Maze Runner: The Scorch Trials +Alexander Flores,1990,Maze Runner: The Scorch Trials +Jacob Lofland,1996,Maze Runner: The Scorch Trials +Rosa Salazar,1985,Maze Runner: The Scorch Trials +Giancarlo Esposito,1958,Maze Runner: The Scorch Trials +Michael Caine,1933,Youth +Harvey Keitel,1939,Youth +Rachel Weisz,1970,Youth +Paul Dano,1984,Youth +Jane Fonda,1937,Youth +Ed Stoppard,1974,Youth +Alex Macqueen,1974,Youth +Madalina Diana Ghenea,1988,Youth +Ed Helms,1974,Vacation +Christina Applegate,1971,Vacation +Skyler Gisondo,1996,Vacation +Chris Hemsworth,1983,Vacation +Leslie Mann,1972,Vacation +Chevy Chase,1943,Vacation +Beverly D'Angelo,1951,Vacation +Charlie Day,1976,Vacation +Kurt Russell,1951,Bone Tomahawk +Patrick Wilson,1973,Bone Tomahawk +Matthew Fox,1966,Bone Tomahawk +Richard Jenkins,1947,Bone Tomahawk +Lili Simmons,1993,Bone Tomahawk +David Arquette,1971,Bone Tomahawk +Fred Melamed,1956,Bone Tomahawk +Sid Haig,1939,Bone Tomahawk +Tye Sheridan,1996,Scouts Guide to the Zombie Apocalypse +Logan Miller,1992,Scouts Guide to the Zombie Apocalypse +David Koechner,1962,Scouts Guide to the Zombie Apocalypse +Halston Sage,1993,Scouts Guide to the Zombie Apocalypse +Cloris Leachman,1926,Scouts Guide to the Zombie Apocalypse +Niki Koss,1994,Scouts Guide to the Zombie Apocalypse +Hiram A. Murray,1981,Scouts Guide to the Zombie Apocalypse +George Clooney,1961,Tomorrowland +Hugh Laurie,1959,Tomorrowland +Britt Robertson,1990,Tomorrowland +Tim McGraw,1967,Tomorrowland +Kathryn Hahn,1973,Tomorrowland +Keegan-Michael Key,1971,Tomorrowland +Chris Bauer,1966,Tomorrowland +Jason Bateman,1969,The Gift +Rebecca Hall,1982,The Gift +Joel Edgerton,1974,The Gift +Allison Tolman,1981,The Gift +Tim Griffin,1969,The Gift +Busy Philipps,1979,The Gift +Adam Lazarre-White,1969,The Gift +Wendell Pierce,1963,The Gift +Vin Diesel,1967,The Last Witch Hunter +Rose Leslie,1987,The Last Witch Hunter +Elijah Wood,1981,The Last Witch Hunter +Rena Owen,1962,The Last Witch Hunter +Julie Engelbrecht,1984,The Last Witch Hunter +Michael Caine,1933,The Last Witch Hunter +Joseph Gilgun,1984,The Last Witch Hunter +Kate Winslet,1975,Insurgent +Jai Courtney,1986,Insurgent +Mekhi Phifer,1974,Insurgent +Shailene Woodley,1991,Insurgent +Theo James,1984,Insurgent +Ansel Elgort,1994,Insurgent +Miles Teller,1987,Insurgent +Justice Leak,1979,Insurgent +Mae Whitman,1988,The DUFF +Robbie Amell,1988,The DUFF +Bella Thorne,1997,The DUFF +Skyler Samuels,1994,The DUFF +Romany Malco,1968,The DUFF +Nick Eversman,1986,The DUFF +Chris Wylde,1976,The DUFF +Ken Jeong,1969,The DUFF +Jeffrey Wright,1965,The Good Dinosaur +Frances McDormand,1957,The Good Dinosaur +Raymond Ochoa,2001,The Good Dinosaur +Will Smith,1968,Focus +Margot Robbie,1990,Focus +Gerald McRaney,1947,Focus +Rodrigo Santoro,1975,Focus +BD Wong,1960,Focus +Brennan Brown,1968,Focus +Robert Taylor,1963,Focus +Mila Kunis,1983,Jupiter Ascending +Channing Tatum,1980,Jupiter Ascending +Sean Bean,1959,Jupiter Ascending +Eddie Redmayne,1982,Jupiter Ascending +Douglas Booth,1992,Jupiter Ascending +Tuppence Middleton,1987,Jupiter Ascending +Nikki Amuka-Bird,1976,Jupiter Ascending +Christina Cole,1982,Jupiter Ascending +Miles Teller,1987,Fantastic Four +Michael B. Jordan,1987,Fantastic Four +Kate Mara,1983,Fantastic Four +Jamie Bell,1986,Fantastic Four +Toby Kebbell,1982,Fantastic Four +Reg E. Cathey,1958,Fantastic Four +Tim Blake Nelson,1964,Fantastic Four +Dan Castellaneta,1957,Fantastic Four +Will Smith,1968,Concussion +Alec Baldwin,1958,Concussion +Albert Brooks,1947,Concussion +Gugu Mbatha-Raw,1983,Concussion +David Morse,1953,Concussion +Arliss Howard,1954,Concussion +Mike O'Malley,1966,Concussion +Eddie Marsan,1968,Concussion +Hill Harper,1966,Concussion +Sareh Bayat,1979,Muhammad: The Messenger of God +Siamak Adib,1974,Muhammad: The Messenger of God +Nicholas Hoult,1989,Equals +Scott Lawrence,1963,Equals +Kristen Stewart,1990,Equals +Bel Powley,1992,Equals +David Selby,1941,Equals +Channing Tatum,1980,Magic Mike XXL +Juan Piedrahita,1985,Magic Mike XXL +Joe Manganiello,1976,Magic Mike XXL +Kevin Nash,1959,Magic Mike XXL +Gabriel Iglesias,1976,Magic Mike XXL +Matt Bomer,1977,Magic Mike XXL +Britt Robertson,1990,The Longest Ride +Scott Eastwood,1986,The Longest Ride +Alan Alda,1936,The Longest Ride +Jack Huston,1982,The Longest Ride +Oona Chaplin,1986,The Longest Ride +Melissa Benoist,1988,The Longest Ride +Lolita Davidovich,1961,The Longest Ride +Will Ferrell,1967,Daddy's Home +Mark Wahlberg,1971,Daddy's Home +Linda Cardellini,1975,Daddy's Home +Thomas Haden Church,1960,Daddy's Home +Bobby Cannavale,1970,Daddy's Home +Hannibal Buress,1983,Daddy's Home +Bill Burr,1968,Daddy's Home +Christian Bale,1974,Knight of Cups +Cate Blanchett,1969,Knight of Cups +Natalie Portman,1981,Knight of Cups +Brian Dennehy,1938,Knight of Cups +Antonio Banderas,1960,Knight of Cups +Freida Pinto,1984,Knight of Cups +Wes Bentley,1978,Knight of Cups +Isabel Lucas,1985,Knight of Cups +Teresa Palmer,1986,Knight of Cups +Adam Sandler,1966,The Ridiculous 6 +Terry Crews,1968,The Ridiculous 6 +Jorge Garcia,1973,The Ridiculous 6 +Taylor Lautner,1992,The Ridiculous 6 +Rob Schneider,1963,The Ridiculous 6 +Luke Wilson,1971,The Ridiculous 6 +Will Forte,1970,The Ridiculous 6 +Steve Zahn,1967,The Ridiculous 6 +Harvey Keitel,1939,The Ridiculous 6 +Mark Wahlberg,1971,Ted 2 +Seth MacFarlane,1973,Ted 2 +Amanda Seyfried,1985,Ted 2 +Giovanni Ribisi,1974,Ted 2 +Morgan Freeman,1937,Ted 2 +Sam J. Jones,1954,Ted 2 +Patrick Warburton,1964,Ted 2 +Michael Dorn,1952,Ted 2 +Sandra Bullock,1964,Minions +Jon Hamm,1971,Minions +Michael Keaton,1951,Minions +Allison Janney,1959,Minions +Steve Coogan,1965,Minions +Jennifer Saunders,1958,Minions +Geoffrey Rush,1951,Minions +Steve Carell,1962,Minions +Michael Fassbender,1977,Macbeth +Marion Cotillard,1975,Macbeth +Paddy Considine,1974,Macbeth +Taissa Farmiga,1994,The Final Girls +Malin Akerman,1978,The Final Girls +Alexander Ludwig,1992,The Final Girls +Nina Dobrev,1989,The Final Girls +Alia Shawkat,1989,The Final Girls +Thomas Middleditch,1982,The Final Girls +Adam Devine,1983,The Final Girls +Angela Trimbur,1981,The Final Girls +Chloe Bridges,1991,The Final Girls +Tilda Swinton,1960,A Bigger Splash +Matthias Schoenaerts,1977,A Bigger Splash +Ralph Fiennes,1962,A Bigger Splash +Dakota Johnson,1989,A Bigger Splash +Corrado Guzzanti,1965,A Bigger Splash +Sharlto Copley,1973,Chappie +Dev Patel,1990,Chappie +Ninja,1974,Chappie +Yo-Landi Visser,1984,Chappie +Jose Pablo Cantillo,1979,Chappie +Hugh Jackman,1968,Chappie +Sigourney Weaver,1949,Chappie +Bahar Pars,1979,En man som heter Ove +Filip Berg,1986,En man som heter Ove +Anna-Lena Brundin,1959,En man som heter Ove +Karin de Frumerie,1978,En man som heter Ove +Benedict Cumberbatch,1976,Doctor Strange +Chiwetel Ejiofor,1977,Doctor Strange +Rachel McAdams,1978,Doctor Strange +Benedict Wong,1970,Doctor Strange +Mads Mikkelsen,1965,Doctor Strange +Tilda Swinton,1960,Doctor Strange +Michael Stuhlbarg,1968,Doctor Strange +Benjamin Bratt,1963,Doctor Strange +Scott Adkins,1976,Doctor Strange +Tom Cruise,1962,Jack Reacher: Never Go Back +Cobie Smulders,1982,Jack Reacher: Never Go Back +Aldis Hodge,1986,Jack Reacher: Never Go Back +Danika Yarosh,1998,Jack Reacher: Never Go Back +Holt McCallany,1963,Jack Reacher: Never Go Back +Tom Hanks,1956,Inferno +Felicity Jones,1983,Inferno +Omar Sy,1978,Inferno +Irrfan Khan,1967,Inferno +Sidse Babett Knudsen,1968,Inferno +Ben Foster,1980,Inferno +Ana Ularu,1985,Inferno +Sugar Lyn Beard,1981,Sausage Party +Michael Cera,1988,Sausage Party +Ian James Corlett,1962,Sausage Party +Michael Daingerfield,1970,Sausage Party +Brian Dobson,1973,Sausage Party +Michael Dobson,1966,Sausage Party +James Franco,1978,Sausage Party +Ben Affleck,1972,The Accountant +Anna Kendrick,1985,The Accountant +J.K. Simmons,1955,The Accountant +Jon Bernthal,1976,The Accountant +Jeffrey Tambor,1944,The Accountant +Cynthia Addai-Robinson,1985,The Accountant +John Lithgow,1945,The Accountant +Jean Smart,1951,The Accountant +Amy Adams,1974,Nocturnal Animals +Jake Gyllenhaal,1980,Nocturnal Animals +Michael Shannon,1974,Nocturnal Animals +Aaron Taylor-Johnson,1990,Nocturnal Animals +Isla Fisher,1976,Nocturnal Animals +Ellie Bamber,1997,Nocturnal Animals +Armie Hammer,1986,Nocturnal Animals +Karl Glusman,1988,Nocturnal Animals +Emily Blunt,1983,The Girl on the Train +Haley Bennett,1988,The Girl on the Train +Rebecca Ferguson,1983,The Girl on the Train +Justin Theroux,1971,The Girl on the Train +Luke Evans,1979,The Girl on the Train +Laura Prepon,1980,The Girl on the Train +Allison Janney,1959,The Girl on the Train +Darren Goldstein,1974,The Girl on the Train +Will Smith,1968,Suicide Squad +Ike Barinholtz,1977,Suicide Squad +Margot Robbie,1990,Suicide Squad +Christopher Dyson,1978,Suicide Squad +Bambadjan Bamba,1982,Suicide Squad +Viola Davis,1965,Suicide Squad +David Harbour,1975,Suicide Squad +Tyler Perry,1969,Boo! A Madea Halloween +Cassi Davis,1964,Boo! A Madea Halloween +Bella Thorne,1997,Boo! A Madea Halloween +Yousef Erakat,1990,Boo! A Madea Halloween +James McAvoy,1979,Split +Haley Lu Richardson,1995,Split +Anya Taylor-Joy,1996,Split +Kim Director,1974,Split +Maria Breyman,1987,Split +Jessica Sula,1994,Split +Betty Buckley,1947,Split +Brad William Henke,1966,Split +Lyne Renee,1979,Split +Eva Green,1980,Miss Peregrine's Home for Peculiar Children +Asa Butterfield,1997,Miss Peregrine's Home for Peculiar Children +Samuel L. Jackson,1948,Miss Peregrine's Home for Peculiar Children +Judi Dench,1934,Miss Peregrine's Home for Peculiar Children +Rupert Everett,1959,Miss Peregrine's Home for Peculiar Children +Allison Janney,1959,Miss Peregrine's Home for Peculiar Children +Chris O'Dowd,1979,Miss Peregrine's Home for Peculiar Children +Terence Stamp,1938,Miss Peregrine's Home for Peculiar Children +Ella Purnell,1996,Miss Peregrine's Home for Peculiar Children +Zach Galifianakis,1969,Keeping Up with the Joneses +Isla Fisher,1976,Keeping Up with the Joneses +Jon Hamm,1971,Keeping Up with the Joneses +Gal Gadot,1985,Keeping Up with the Joneses +Patton Oswalt,1969,Keeping Up with the Joneses +Matt Walsh,1964,Keeping Up with the Joneses +Andrew Garfield,1983,Hacksaw Ridge +Hugo Weaving,1960,Hacksaw Ridge +Anna Kendrick,1985,Trolls +Justin Timberlake,1981,Trolls +Zooey Deschanel,1980,Trolls +Christopher Mintz-Plasse,1989,Trolls +Christine Baranski,1952,Trolls +Russell Brand,1975,Trolls +Gwen Stefani,1969,Trolls +John Cleese,1939,Trolls +James Corden,1978,Trolls +Viggo Mortensen,1958,Captain Fantastic +George MacKay,1992,Captain Fantastic +Samantha Isler,1998,Captain Fantastic +Nicholas Hamilton,2000,Captain Fantastic +Kathryn Hahn,1973,Captain Fantastic +Chris Pine,1980,Star Trek Beyond +Zachary Quinto,1977,Star Trek Beyond +Karl Urban,1972,Star Trek Beyond +Zoe Saldana,1978,Star Trek Beyond +Simon Pegg,1970,Star Trek Beyond +John Cho,1972,Star Trek Beyond +Anton Yelchin,1989,Star Trek Beyond +Idris Elba,1972,Star Trek Beyond +Sofia Boutella,1982,Star Trek Beyond +Mila Kunis,1983,Bad Moms +Kathryn Hahn,1973,Bad Moms +Kristen Bell,1980,Bad Moms +Christina Applegate,1971,Bad Moms +Jada Pinkett Smith,1971,Bad Moms +Annie Mumolo,1973,Bad Moms +Oona Laurence,2002,Bad Moms +Emjay Anthony,2003,Bad Moms +David Walton,1978,Bad Moms +Zach Woods,1984,Ghostbusters +Kristen Wiig,1973,Ghostbusters +Ed Begley Jr.,1949,Ghostbusters +Charles Dance,1946,Ghostbusters +Melissa McCarthy,1970,Ghostbusters +Kate McKinnon,1984,Ghostbusters +Amy Adams,1974,Arrival +Jeremy Renner,1971,Arrival +Michael Stuhlbarg,1968,Arrival +Forest Whitaker,1961,Arrival +Mark O'Brien,1984,Arrival +Stephen Lang,1952,Don't Breathe +Jane Levy,1989,Don't Breathe +Dylan Minnette,1996,Don't Breathe +Katia Bokor,1975,Don't Breathe +Sergej Onopko,1985,Don't Breathe +Emma Roberts,1991,Nerve +Dave Franco,1985,Nerve +Emily Meade,1989,Nerve +Miles Heizer,1994,Nerve +Kimiko Glenn,1989,Nerve +Marc John Jefferies,1990,Nerve +Machine Gun Kelly,1990,Nerve +Ellen DeGeneres,1958,Finding Dory +Albert Brooks,1947,Finding Dory +Ed O'Neill,1946,Finding Dory +Kaitlin Olson,1975,Finding Dory +Ty Burrell,1967,Finding Dory +Diane Keaton,1946,Finding Dory +Eugene Levy,1946,Finding Dory +Brenton Thwaites,1989,Gods of Egypt +Courtney Eaton,1996,Gods of Egypt +Nikolaj Coster-Waldau,1970,Gods of Egypt +Emily Wheaton,1989,Gods of Egypt +Elodie Yung,1981,Gods of Egypt +Rachael Blake,1971,Gods of Egypt +James McAvoy,1979,X-Men: Apocalypse +Michael Fassbender,1977,X-Men: Apocalypse +Jennifer Lawrence,1990,X-Men: Apocalypse +Nicholas Hoult,1989,X-Men: Apocalypse +Oscar Isaac,1979,X-Men: Apocalypse +Rose Byrne,1979,X-Men: Apocalypse +Evan Peters,1987,X-Men: Apocalypse +Josh Helman,1986,X-Men: Apocalypse +Sophie Turner,1996,X-Men: Apocalypse +Ryan Reynolds,1976,Deadpool +Ed Skrein,1983,Deadpool +Michael Benyaer,1970,Deadpool +Stefan Kapicic,1978,Deadpool +Brianna Hildebrand,1996,Deadpool +Kyle Cassie,1976,Deadpool +Taylor Hickson,1997,Deadpool +Liam Hemsworth,1990,Independence Day: Resurgence +Jeff Goldblum,1952,Independence Day: Resurgence +Jessie T. Usher,1992,Independence Day: Resurgence +Bill Pullman,1953,Independence Day: Resurgence +Maika Monroe,1993,Independence Day: Resurgence +Sela Ward,1956,Independence Day: Resurgence +William Fichtner,1956,Independence Day: Resurgence +Judd Hirsch,1935,Independence Day: Resurgence +Brent Spiner,1949,Independence Day: Resurgence +Mark Rylance,1960,The BFG +Ruby Barnhill,2004,The BFG +Penelope Wilton,1946,The BFG +Jemaine Clement,1974,The BFG +Rebecca Hall,1982,The BFG +Rafe Spall,1983,The BFG +Bill Hader,1978,The BFG +Adam Godley,1964,The BFG +Jamie Dornan,1982,Anthropoid +Cillian Murphy,1976,Anthropoid +Charlotte Le Bon,1986,Anthropoid +Harry Lloyd,1983,Anthropoid +Marcin Dorocinski,1973,Anthropoid +Denzel Washington,1954,The Magnificent Seven +Chris Pratt,1979,The Magnificent Seven +Ethan Hawke,1970,The Magnificent Seven +Vincent D'Onofrio,1959,The Magnificent Seven +Byung-hun Lee,1970,The Magnificent Seven +Manuel Garcia-Rulfo,1981,The Magnificent Seven +Haley Bennett,1988,The Magnificent Seven +Peter Sarsgaard,1971,The Magnificent Seven +Mark Wahlberg,1971,Deepwater Horizon +Kurt Russell,1951,Deepwater Horizon +Douglas M. Griffin,1966,Deepwater Horizon +James DuMont,1965,Deepwater Horizon +Gina Rodriguez,1984,Deepwater Horizon +Brad Leland,1954,Deepwater Horizon +John Malkovich,1953,Deepwater Horizon +Bryan Cranston,1956,The Infiltrator +Daniel Mays,1978,The Infiltrator +Tom Vaughan-Lawlor,1977,The Infiltrator +Juliet Aubrey,1966,The Infiltrator +Olympia Dukakis,1931,The Infiltrator +Amy Ryan,1968,The Infiltrator +Teresa Palmer,1986,Lights Out +Billy Burke,1966,Lights Out +Maria Bello,1967,Lights Out +Alicia Vela-Bailey,1982,Lights Out +Andi Osho,1973,Lights Out +Ethan Hawke,1970,In a Valley of Violence +John Travolta,1954,In a Valley of Violence +Taissa Farmiga,1994,In a Valley of Violence +James Ransone,1979,In a Valley of Violence +Karen Gillan,1987,In a Valley of Violence +Toby Huss,1966,In a Valley of Violence +Larry Fessenden,1963,In a Valley of Violence +Ginnifer Goodwin,1978,Zootopia +Jason Bateman,1969,Zootopia +Idris Elba,1972,Zootopia +Jenny Slate,1982,Zootopia +Nate Torrence,1977,Zootopia +Bonnie Hunt,1961,Zootopia +Don Lake,1956,Zootopia +Tommy Chong,1938,Zootopia +J.K. Simmons,1955,Zootopia +Christoph Waltz,1956,The Legend of Tarzan +Samuel L. Jackson,1948,The Legend of Tarzan +Margot Robbie,1990,The Legend of Tarzan +Travis Fimmel,1979,Warcraft +Paula Patton,1975,Warcraft +Ben Foster,1980,Warcraft +Dominic Cooper,1978,Warcraft +Toby Kebbell,1982,Warcraft +Robert Kazinsky,1983,Warcraft +Clancy Brown,1959,Warcraft +Daniel Wu,1974,Warcraft +Chris Evans,1981,Captain America: Civil War +Robert Downey Jr.,1965,Captain America: Civil War +Scarlett Johansson,1984,Captain America: Civil War +Sebastian Stan,1982,Captain America: Civil War +Anthony Mackie,1978,Captain America: Civil War +Don Cheadle,1964,Captain America: Civil War +Jeremy Renner,1971,Captain America: Civil War +Chadwick Boseman,1976,Captain America: Civil War +Paul Bettany,1971,Captain America: Civil War +Kristen Stewart,1990,Billy Lynn's Long Halftime Walk +Vin Diesel,1967,Billy Lynn's Long Halftime Walk +Garrett Hedlund,1984,Billy Lynn's Long Halftime Walk +Steve Martin,1945,Billy Lynn's Long Halftime Walk +Chris Tucker,1972,Billy Lynn's Long Halftime Walk +Deirdre Lovejoy,1962,Billy Lynn's Long Halftime Walk +Joe Alwyn,1991,Billy Lynn's Long Halftime Walk +Mahershala Ali,1974,Moonlight +Naomie Harris,1976,Moonlight +Ewan McGregor,1971,American Pastoral +Jennifer Connelly,1970,American Pastoral +Dakota Fanning,1994,American Pastoral +Peter Riegert,1947,American Pastoral +Rupert Evans,1976,American Pastoral +Uzo Aduba,1981,American Pastoral +Molly Parker,1972,American Pastoral +Valorie Curry,1986,American Pastoral +Sam Claflin,1986,Me Before You +Emilia Clarke,1986,Me Before You +Samantha Spiro,1968,Me Before You +Brendan Coyle,1963,Me Before You +Ben Affleck,1972,Batman v Superman: Dawn of Justice +Henry Cavill,1983,Batman v Superman: Dawn of Justice +Amy Adams,1974,Batman v Superman: Dawn of Justice +Jesse Eisenberg,1983,Batman v Superman: Dawn of Justice +Diane Lane,1965,Batman v Superman: Dawn of Justice +Laurence Fishburne,1961,Batman v Superman: Dawn of Justice +Jeremy Irons,1948,Batman v Superman: Dawn of Justice +Holly Hunter,1958,Batman v Superman: Dawn of Justice +Gal Gadot,1985,Batman v Superman: Dawn of Justice +Johnny Depp,1963,Alice Through the Looking Glass +Mia Wasikowska,1989,Alice Through the Looking Glass +Helena Bonham Carter,1966,Alice Through the Looking Glass +Anne Hathaway,1982,Alice Through the Looking Glass +Sacha Baron Cohen,1971,Alice Through the Looking Glass +Rhys Ifans,1967,Alice Through the Looking Glass +Matt Lucas,1974,Alice Through the Looking Glass +Lindsay Duncan,1950,Alice Through the Looking Glass +Leo Bill,1980,Alice Through the Looking Glass +Ajay Devgn,1969,Shivaay +Girish Karnad,1938,Shivaay +Saurabh Shukla,1963,Shivaay +James Franco,1978,King Cobra +Alicia Silverstone,1976,King Cobra +Molly Ringwald,1968,King Cobra +Christian Slater,1969,King Cobra +Keegan Allen,1989,King Cobra +Sean Grandillo,1992,King Cobra +Patrick Wilson,1973,The Conjuring 2 +Vera Farmiga,1973,The Conjuring 2 +Madison Wolfe,2002,The Conjuring 2 +Frances O'Connor,1967,The Conjuring 2 +Simon McBurney,1957,The Conjuring 2 +Maria Doyle Kennedy,1964,The Conjuring 2 +Tom Hanks,1956,Sully +Aaron Eckhart,1968,Sully +Valerie Mahaffey,1953,Sully +Delphi Harrington,1937,Sully +Mike O'Malley,1966,Sully +Jamey Sheridan,1951,Sully +Anna Gunn,1968,Sully +Holt McCallany,1963,Sully +Zac Efron,1987,Mike and Dave Need Wedding Dates +Adam Devine,1983,Mike and Dave Need Wedding Dates +Anna Kendrick,1985,Mike and Dave Need Wedding Dates +Aubrey Plaza,1984,Mike and Dave Need Wedding Dates +Stephen Root,1951,Mike and Dave Need Wedding Dates +Stephanie Faracy,1952,Mike and Dave Need Wedding Dates +Sugar Lyn Beard,1981,Mike and Dave Need Wedding Dates +Keanu Reeves,1964,The Whole Truth +Gugu Mbatha-Raw,1983,The Whole Truth +Jim Belushi,1954,The Whole Truth +Lara Grice,1971,The Whole Truth +Daniel Radcliffe,1989,Imperium +Toni Collette,1972,Imperium +Tracy Letts,1965,Imperium +Sam Trammell,1969,Imperium +Nestor Carbonell,1967,Imperium +Pawel Szajda,1982,Imperium +Luis Gnecco,1962,Neruda +Alfredo Castro,1955,Neruda +Pablo Derqui,1976,Neruda +Alejandro Goic,1969,Neruda +Kate McKinnon,1984,Masterminds +Kristen Wiig,1973,Masterminds +Zach Galifianakis,1969,Masterminds +Jason Sudeikis,1975,Masterminds +Leslie Jones,1967,Masterminds +Owen Wilson,1968,Masterminds +Mary Elizabeth Ellis,1979,Masterminds +Ken Marino,1968,Masterminds +Devin Ratray,1977,Masterminds +Kevin Spacey,1959,Nine Lives +Jennifer Garner,1972,Nine Lives +Robbie Amell,1988,Nine Lives +Cheryl Hines,1965,Nine Lives +Mark Consuelos,1971,Nine Lives +Christopher Walken,1943,Nine Lives +Teddy Sears,1977,Nine Lives +Russell Crowe,1964,The Nice Guys +Ryan Gosling,1980,The Nice Guys +Matt Bomer,1977,The Nice Guys +Yaya DaCosta,1982,The Nice Guys +Keith David,1956,The Nice Guys +Lois Smith,1930,The Nice Guys +Paul Dano,1984,Swiss Army Man +Daniel Radcliffe,1989,Swiss Army Man +Mary Elizabeth Winstead,1984,Swiss Army Man +Dwayne Johnson,1972,Central Intelligence +Kevin Hart,1979,Central Intelligence +Amy Ryan,1968,Central Intelligence +Danielle Nicolet,1973,Central Intelligence +Jason Bateman,1969,Central Intelligence +Aaron Paul,1979,Central Intelligence +Ryan Hansen,1981,Central Intelligence +Tim Griffin,1969,Central Intelligence +Jamie Dornan,1982,The Siege of Jadotville +Mark Strong,1963,The Siege of Jadotville +Jason O'Mara,1972,The Siege of Jadotville +Emmanuelle Seigner,1966,The Siege of Jadotville +Mikael Persbrandt,1963,The Siege of Jadotville +Guillaume Canet,1973,The Siege of Jadotville +Fiona Glascott,1982,The Siege of Jadotville +Michael McElhatton,1963,The Siege of Jadotville +Blake Lively,1987,The Shallows +Brett Cullen,1956,The Shallows +Neel Sethi,2003,The Jungle Book +Bill Murray,1950,The Jungle Book +Ben Kingsley,1943,The Jungle Book +Idris Elba,1972,The Jungle Book +Lupita Nyong'o,1983,The Jungle Book +Scarlett Johansson,1984,The Jungle Book +Giancarlo Esposito,1958,The Jungle Book +Christopher Walken,1943,The Jungle Book +Garry Shandling,1949,The Jungle Book +Mel Gibson,1956,Blood Father +Erin Moriarty,1994,Blood Father +Diego Luna,1979,Blood Father +Michael Parks,1940,Blood Father +William H. Macy,1950,Blood Father +Miguel Sandoval,1951,Blood Father +Dale Dickey,1961,Blood Father +Louis C.K.,1967,The Secret Life of Pets +Eric Stonestreet,1971,The Secret Life of Pets +Kevin Hart,1979,The Secret Life of Pets +Jenny Slate,1982,The Secret Life of Pets +Ellie Kemper,1980,The Secret Life of Pets +Albert Brooks,1947,The Secret Life of Pets +Lake Bell,1979,The Secret Life of Pets +Dana Carvey,1955,The Secret Life of Pets +Hannibal Buress,1983,The Secret Life of Pets +Andy Samberg,1978,Storks +Kelsey Grammer,1955,Storks +Jennifer Aniston,1969,Storks +Ty Burrell,1967,Storks +Keegan-Michael Key,1971,Storks +Jordan Peele,1979,Storks +Danny Trejo,1944,Storks +Elle Fanning,1998,The Neon Demon +Karl Glusman,1988,The Neon Demon +Jena Malone,1984,The Neon Demon +Bella Heathcote,1987,The Neon Demon +Abbey Lee,1987,The Neon Demon +Desmond Harrington,1976,The Neon Demon +Christina Hendricks,1975,The Neon Demon +Keanu Reeves,1964,The Neon Demon +Charles Baker,1971,The Neon Demon +Kelly Chen,1972,Tokyo Zance +Melissa Leo,1960,Snowden +Zachary Quinto,1977,Snowden +Joseph Gordon-Levitt,1981,Snowden +Jaymes Butler,1959,Snowden +Rhys Ifans,1967,Snowden +Frank Grillo,1965,The Purge: Election Year +Elizabeth Mitchell,1970,The Purge: Election Year +Mykelti Williamson,1957,The Purge: Election Year +Edwin Hodge,1985,The Purge: Election Year +Kyle Secor,1957,The Purge: Election Year +Barry Nolan,1947,The Purge: Election Year +Miles Teller,1987,Bleed for This +Katey Sagal,1954,Bleed for This +Aaron Eckhart,1968,Bleed for This +Ted Levine,1957,Bleed for This +Tina Casciani,1982,Bleed for This +Jesse Eisenberg,1983,Now You See Me 2 +Mark Ruffalo,1967,Now You See Me 2 +Woody Harrelson,1961,Now You See Me 2 +Dave Franco,1985,Now You See Me 2 +Daniel Radcliffe,1989,Now You See Me 2 +Lizzy Caplan,1982,Now You See Me 2 +Jay Chou,1979,Now You See Me 2 +Sanaa Lathan,1971,Now You See Me 2 +Michael Caine,1933,Now You See Me 2 +Matthew McConaughey,1969,Free State of Jones +Gugu Mbatha-Raw,1983,Free State of Jones +Mahershala Ali,1974,Free State of Jones +Keri Russell,1976,Free State of Jones +Sean Bridgers,1968,Free State of Jones +Jacob Lofland,1996,Free State of Jones +Gemma Jones,1942,Bridget Jones's Baby +Jim Broadbent,1949,Bridget Jones's Baby +Sally Phillips,1970,Bridget Jones's Baby +Julian Rhind-Tutt,1968,Bridget Jones's Baby +Shirley Henderson,1965,Bridget Jones's Baby +Ben Willbond,1973,Bridget Jones's Baby +Paul Bentall,1948,Bridget Jones's Baby +Colin Firth,1960,Bridget Jones's Baby +Josh Brener,1984,Max Steel +Maria Bello,1967,Max Steel +Andy Garcia,1956,Max Steel +Phillip DeVona,1970,Max Steel +Billy Slaughter,1980,Max Steel +Megan Fox,1986,Teenage Mutant Ninja Turtles: Out of the Shadows +Will Arnett,1970,Teenage Mutant Ninja Turtles: Out of the Shadows +Laura Linney,1964,Teenage Mutant Ninja Turtles: Out of the Shadows +Stephen Amell,1981,Teenage Mutant Ninja Turtles: Out of the Shadows +Noel Fisher,1984,Teenage Mutant Ninja Turtles: Out of the Shadows +Jeremy Howard,1981,Teenage Mutant Ninja Turtles: Out of the Shadows +Pete Ploszek,1987,Teenage Mutant Ninja Turtles: Out of the Shadows +Alan Ritchson,1984,Teenage Mutant Ninja Turtles: Out of the Shadows +Tyler Perry,1969,Teenage Mutant Ninja Turtles: Out of the Shadows +Zach Woods,1984,Mascots +Wayne Wilderson,1966,Mascots +Michael Hitchcock,1958,Mascots +Parker Posey,1968,Mascots +Chris O'Dowd,1979,Mascots +Min-hee Kim,1982,Ah-ga-ssi +Kim Tae-ri,1990,Ah-ga-ssi +Jung-woo Ha,1978,Ah-ga-ssi +Jin-woong Jo,1976,Ah-ga-ssi +So-ri Moon,1974,Ah-ga-ssi +Hae-suk Kim,1955,Ah-ga-ssi +Neil deGrasse Tyson,1958,Ice Age: Collision Course +Adam Devine,1983,Ice Age: Collision Course +Jesse Tyler Ferguson,1975,Ice Age: Collision Course +Max Greenfield,1980,Ice Age: Collision Course +Jessie J,1988,Ice Age: Collision Course +Queen Latifah,1970,Ice Age: Collision Course +Denis Leary,1957,Ice Age: Collision Course +John Krasinski,1979,13 Hours +James Badge Dale,1978,13 Hours +Pablo Schreiber,1978,13 Hours +David Denman,1973,13 Hours +Dominic Fumusa,1969,13 Hours +Max Martini,1969,13 Hours +Peyman Moaadi,1970,13 Hours +Ryan Gosling,1980,La La Land +Emma Stone,1988,La La Land +Cameron McKendry,1996,I'm Not Ashamed +Victoria Staley,1996,I'm Not Ashamed +Taylor Kalupa,1990,I'm Not Ashamed +Hailee Steinfeld,1996,The Edge of Seventeen +Haley Lu Richardson,1995,The Edge of Seventeen +Blake Jenner,1992,The Edge of Seventeen +Kyra Sedgwick,1965,The Edge of Seventeen +Woody Harrelson,1961,The Edge of Seventeen +Hayden Szeto,1985,The Edge of Seventeen +Eric Keenleyside,1957,The Edge of Seventeen +Matt Damon,1970,Jason Bourne +Tommy Lee Jones,1946,Jason Bourne +Alicia Vikander,1988,Jason Bourne +Vincent Cassel,1966,Jason Bourne +Julia Stiles,1981,Jason Bourne +Riz Ahmed,1982,Jason Bourne +Ato Essandoh,1972,Jason Bourne +Sasha Lane,1995,American Honey +Shia LaBeouf,1986,American Honey +Riley Keough,1989,American Honey +Jason Statham,1967,Mechanic: Resurrection +Jessica Alba,1981,Mechanic: Resurrection +Tommy Lee Jones,1946,Mechanic: Resurrection +Michelle Yeoh,1962,Mechanic: Resurrection +Sam Hazeldine,1972,Mechanic: Resurrection +John Cenatiempo,1963,Mechanic: Resurrection +Yoo Gong,1979,Busanhaeng +Yu-mi Jeong,1983,Busanhaeng +Dong-seok Ma,1971,Busanhaeng +Woo-sik Choi,1990,Busanhaeng +Sohee,1992,Busanhaeng +Sacha Baron Cohen,1971,Grimsby +Rebel Wilson,1980,Grimsby +Mark Strong,1963,Grimsby +Lex Shrapnel,1979,Grimsby +Isla Fisher,1976,Grimsby +Dale Dickey,1961,Hell or High Water +Ben Foster,1980,Hell or High Water +Chris Pine,1980,Hell or High Water +Buck Taylor,1938,Hell or High Water +Jeff Bridges,1949,Hell or High Water +Gil Birmingham,1953,Hell or High Water +John Goodman,1952,10 Cloverfield Lane +Mary Elizabeth Winstead,1984,10 Cloverfield Lane +John Gallagher Jr.,1984,10 Cloverfield Lane +Douglas M. Griffin,1966,10 Cloverfield Lane +Suzanne Cryer,1967,10 Cloverfield Lane +Bradley Cooper,1975,10 Cloverfield Lane +Chris Pine,1980,The Finest Hours +Casey Affleck,1975,The Finest Hours +Ben Foster,1980,The Finest Hours +Eric Bana,1968,The Finest Hours +Holliday Grainger,1988,The Finest Hours +John Ortiz,1968,The Finest Hours +Kyle Gallner,1986,The Finest Hours +Graham McTavish,1961,The Finest Hours diff --git a/resources/imdb.rdb b/resources/imdb.rdb deleted file mode 100644 index d8421d042c55fe2ff9a9b28ee4c943e67a462f1f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 109007 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 z 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 .join(", "); @@ -425,13 +425,10 @@ mod tests { assert_eq!(indices.data.len(), 1); assert_eq!(indices.data[0].entity_type, EntityType::Node); assert_eq!(indices.data[0].index_label, "actor".to_string()); - assert_eq!(indices.data[0].field_types.len(), 2); + assert_eq!(indices.data[0].field_types.len(), 1); assert_eq!( indices.data[0].field_types, - HashMap::from([ - ("age".to_string(), vec![IndexType::Range]), - ("name".to_string(), vec![IndexType::Fulltext]) - ]) + HashMap::from([("name".to_string(), vec![IndexType::Fulltext])]) ); } @@ -551,7 +548,7 @@ mod tests { assert_eq!( execution_plan.string_representation(), - "\nResults\n Limit\n Aggregate\n Filter\n Node By Index Scan | (b:actor)\n Project\n Node By Label Scan | (a:actor)" + "\nResults\n Limit\n Aggregate\n Filter\n Node By Label Scan | (b:actor)\n Project\n Node By Label Scan | (a:actor)" ); } diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index 8a5ac4e..0d8db09 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -4,13 +4,50 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, graph_schema::utils::get_refresh_command, - FalkorDBError, FalkorResult, FalkorValue, + connection::blocking::BorrowedSyncConnection, value::utils::parse_type, FalkorDBError, + FalkorResult, FalkorValue, }; -use std::collections::{HashMap, HashSet}; -use utils::{get_relevant_hashmap, update_map}; +use std::collections::HashMap; -mod utils; +pub(crate) fn get_refresh_command(schema_type: SchemaType) -> &'static str { + match schema_type { + SchemaType::Labels => "DB.LABELS", + SchemaType::Properties => "DB.PROPERTYKEYS", + SchemaType::Relationships => "DB.RELATIONSHIPTYPES", + } +} + +// Intermediate type for map parsing +pub(crate) struct FKeyTypeVal { + pub(crate) key: i64, + pub(crate) type_marker: i64, + pub(crate) val: FalkorValue, +} + +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)?; + + let key = key_raw.to_i64(); + let type_marker = type_raw.to_i64(); + + match (key, type_marker) { + (Some(key), Some(type_marker)) => Ok(FKeyTypeVal { + key, + type_marker, + val, + }), + (Some(_), None) => Err(FalkorDBError::ParsingTypeMarkerTypeMismatch)?, + (None, Some(_)) => Err(FalkorDBError::ParsingKeyIdTypeMismatch)?, + _ => Err(FalkorDBError::ParsingKTVTypes)?, + } + } +} /// An enum specifying which schema type we are addressing /// When querying using the compact parser, ids are returned for the various schema entities instead of strings @@ -71,26 +108,11 @@ impl GraphSchema { &self.properties } - pub(crate) fn verify_id_set( - &self, - id_set: &HashSet, - schema_type: SchemaType, - ) -> Option> { - let id_map = match schema_type { - SchemaType::Labels => &self.labels, - SchemaType::Properties => &self.properties, - SchemaType::Relationships => &self.relationships, - }; - - get_relevant_hashmap(id_set, id_map) - } - - pub(crate) fn refresh( + fn refresh( &mut self, conn: &mut BorrowedSyncConnection, schema_type: SchemaType, - id_hashset: Option<&HashSet>, - ) -> FalkorResult>> { + ) -> FalkorResult<()> { let id_map = match schema_type { SchemaType::Labels => &mut self.labels, SchemaType::Properties => &mut self.properties, @@ -109,7 +131,119 @@ impl GraphSchema { .try_into() .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; - update_map(id_map, keys, id_hashset) + 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( + "Expected new label/property to be the first element in an array" + .to_string(), + )) + .and_then(|item| item.into_string())?, + )) + }) + .collect::>(); + + *id_map = new_keys; + Ok(()) + } + + pub(crate) fn parse_labels_relationships( + &mut self, + raw_ids: Vec, + conn: &mut BorrowedSyncConnection, + schema_type: SchemaType, + ) -> FalkorResult> { + let ids_count = raw_ids.len(); + + let mut refs_vec = Vec::with_capacity(ids_count); + let mut success = true; + for raw_id in &raw_ids { + let id = raw_id.to_i64().ok_or(FalkorDBError::ParsingI64)?; + match self.labels.get(&id) { + None => { + success = false; + break; + } + Some(label) => refs_vec.push(label), + } + } + + if success { + // Clone the strings themselves and return the parsed labels + return Ok(refs_vec.into_iter().cloned().collect()); + } + + // Refresh and try again + self.refresh(conn, schema_type)?; + + let mut out_vec = Vec::with_capacity(ids_count); + for raw_id in raw_ids { + out_vec.push( + self.labels + .get(&raw_id.to_i64().ok_or(FalkorDBError::ParsingI64)?) + .cloned() + .ok_or(FalkorDBError::MissingSchemaId(SchemaType::Labels))?, + ); + } + + Ok(out_vec) + } + + pub(crate) fn parse_properties_map( + &mut self, + value: FalkorValue, + conn: &mut BorrowedSyncConnection, + ) -> FalkorResult> { + let raw_properties: Vec<_> = value + .into_vec()? + .into_iter() + .flat_map(FKeyTypeVal::try_from) + .collect(); + let properties_count = raw_properties.len(); + + let mut out_vec = Vec::with_capacity(properties_count); + let mut success = true; + for fktv in &raw_properties { + match self.properties().get(&fktv.key).cloned() { + None => { + success = false; + break; + } + Some(property) => out_vec.push(property), + } + } + + if success { + let mut new_map = HashMap::with_capacity(properties_count); + for (property, ktv) in out_vec.into_iter().zip(raw_properties) { + new_map.insert(property, parse_type(ktv.type_marker, ktv.val, self, conn)?); + } + + return Ok(new_map); + } + + // Refresh and try again + self.refresh(conn, SchemaType::Properties)?; + + let mut new_map = HashMap::with_capacity(properties_count); + for ktv in raw_properties { + new_map.insert( + self.properties + .get(&ktv.key) + .cloned() + .ok_or(FalkorDBError::MissingSchemaId(SchemaType::Properties))?, + parse_type(ktv.type_marker, ktv.val, self, conn)?, + ); + } + + Ok(new_map) } } diff --git a/src/graph_schema/utils.rs b/src/graph_schema/utils.rs deleted file mode 100644 index 40067d9..0000000 --- a/src/graph_schema/utils.rs +++ /dev/null @@ -1,163 +0,0 @@ -/* - * Copyright FalkorDB Ltd. 2023 - present - * Licensed under the Server Side Public License v1 (SSPLv1). - */ - -use crate::{FalkorDBError, FalkorValue, SchemaType}; -use anyhow::Result; -use std::collections::{HashMap, HashSet}; - -pub(crate) fn get_refresh_command(schema_type: SchemaType) -> &'static str { - match schema_type { - SchemaType::Labels => "DB.LABELS", - SchemaType::Properties => "DB.PROPERTYKEYS", - SchemaType::Relationships => "DB.RELATIONSHIPTYPES", - } -} - -pub(crate) fn update_map( - map_to_update: &mut HashMap, - keys: FalkorValue, - id_hashset: Option<&HashSet>, -) -> Result>, FalkorDBError> { - let new_keys = keys - .into_vec()? - .into_iter() - .enumerate() - .flat_map(|(idx, item)| { - Result::<(i64, String), FalkorDBError>::Ok(( - idx as i64, - item.into_vec()? - .into_iter() - .next() - .ok_or(FalkorDBError::ParsingError) - .and_then(|item| item.into_string())?, - )) - }) - .collect::>(); - - *map_to_update = new_keys; - - Ok(match id_hashset { - None => None, - Some(id_hashset) => get_relevant_hashmap(id_hashset, map_to_update), - }) -} - -pub(crate) fn get_relevant_hashmap( - id_set: &HashSet, - locked_map: &HashMap, -) -> Option> { - let mut id_hashmap = HashMap::new(); - for id in id_set { - if let Some(id_val) = locked_map.get(id).cloned() { - id_hashmap.insert(*id, id_val); - continue; - } - - return None; - } - - Some(id_hashmap) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn get_test_keys() -> FalkorValue { - FalkorValue::Array(vec![ - FalkorValue::Array(vec![FalkorValue::String("Hello".to_string())]), - FalkorValue::Array(vec![FalkorValue::String("Iterator".to_string())]), - FalkorValue::Array(vec![FalkorValue::String("My-".to_string())]), - FalkorValue::Array(vec![FalkorValue::String("Panic".to_string())]), - ]) - } - - #[test] - fn test_update_map() { - let mut map_to_update = HashMap::from([(5, "Ye Olde Value".to_string())]); - let relevant_ids = - update_map(&mut map_to_update, get_test_keys(), None).expect("Could not update map"); - - assert!(relevant_ids.is_none()); - - assert_eq!(map_to_update.get(&0), Some(&"Hello".to_string())); - assert_eq!(map_to_update.get(&1), Some(&"Iterator".to_string())); - assert_eq!(map_to_update.get(&2), Some(&"My-".to_string())); - assert_eq!(map_to_update.get(&3), Some(&"Panic".to_string())); - - assert_eq!(map_to_update.get(&5), None); - } - - #[test] - fn test_update_map_with_relevant_hashmap() { - let mut map_to_update = HashMap::new(); - let res = update_map( - &mut map_to_update, - get_test_keys(), - Some(&HashSet::from([2, 3, 0])), - ); - assert!(res.is_ok()); - - let relevant_hashmap = res.unwrap(); - assert!(relevant_hashmap.is_some()); - - let relevant_hashmap = relevant_hashmap.unwrap(); - assert_eq!(relevant_hashmap.get(&0), Some(&"Hello".to_string())); - assert_eq!(relevant_hashmap.get(&2), Some(&"My-".to_string())); - assert_eq!(relevant_hashmap.get(&3), Some(&"Panic".to_string())); - - assert_eq!(relevant_hashmap.get(&1), None); - } - - #[test] - fn test_update_no_relevant_ids_still_success() { - let mut map_to_update = HashMap::new(); - let res = update_map( - &mut map_to_update, - get_test_keys(), - Some(&HashSet::from([2, 5, 0])), - ); - assert!(res.is_ok()); - - let relevant_hashmap = res.unwrap(); - assert!(relevant_hashmap.is_none()); - } - - #[test] - fn test_get_relevant_hashmap() { - let hashset = HashSet::from([2, 1, 3, 0]); - let locked_map = HashMap::from([ - (0, "Hello".to_string()), - (1, "Darkness".to_string()), - (2, "My".to_string()), - (3, "Old".to_string()), - (4, "Friend".to_string()), - ]); - let res = get_relevant_hashmap(&hashset, &locked_map); - assert!(res.is_some()); - - let relevant_hashmap = res.unwrap(); - assert_eq!(relevant_hashmap.get(&0), Some(&"Hello".to_string())); - assert_eq!(relevant_hashmap.get(&1), Some(&"Darkness".to_string())); - assert_eq!(relevant_hashmap.get(&2), Some(&"My".to_string())); - assert_eq!(relevant_hashmap.get(&3), Some(&"Old".to_string())); - - // Was not in the requested hashset: - assert_eq!(relevant_hashmap.get(&4), None); - } - - #[test] - fn test_no_relevant_hashmap() { - let hashset = HashSet::from([2, 1, 5, 0]); - let locked_map = HashMap::from([ - (0, "Hello".to_string()), - (2, "My".to_string()), - (5, "Old".to_string()), - (4, "Friend".to_string()), - ]); - let res = get_relevant_hashmap(&hashset, &locked_map); - assert!(res.is_none()) - } -} diff --git a/src/response/execution_plan.rs b/src/response/execution_plan.rs index f6ef314..a833704 100644 --- a/src/response/execution_plan.rs +++ b/src/response/execution_plan.rs @@ -35,12 +35,16 @@ impl IntermediateOperation { let (records_produced, execution_time) = match args.pop_back() { Some(last_arg) if last_arg.contains("Records produced") => ( Regex::new(r"Records produced: (\d+)") - .map_err(|_| FalkorDBError::ParsingError)? + .map_err(|err| { + FalkorDBError::ParsingError(format!("Error constructing regex: {err}")) + })? .captures(last_arg.trim()) .and_then(|cap| cap.get(1)) .and_then(|m| m.as_str().parse().ok()), Regex::new(r"Execution time: (\d+\.\d+) ms") - .map_err(|_| FalkorDBError::ParsingError)? + .map_err(|err| { + FalkorDBError::ParsingError(format!("Error constructing regex: {err}")) + })? .captures(last_arg.trim()) .and_then(|cap| cap.get(1)) .and_then(|m| m.as_str().parse().ok()), @@ -91,7 +95,7 @@ pub struct ExecutionPlan { operation_tree: Rc, } -impl<'a> ExecutionPlan { +impl ExecutionPlan { /// Returns the plan as a slice of human-readable strings pub fn plan(&self) -> &[String] { self.plan.as_slice() diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index df18ca7..57e3604 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -4,8 +4,8 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, value::map::parse_map_with_schema, FalkorDBError, - FalkorParsable, FalkorResult, FalkorValue, GraphSchema, SchemaType, + connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, FalkorResult, + FalkorValue, GraphSchema, SchemaType, }; use anyhow::Result; use std::{ @@ -94,17 +94,10 @@ impl FalkorParsable for Node { .ok_or(FalkorDBError::ParsingCompactIdUnknown)?, ); } - - let parsed_labels = parse_labels(labels, graph_schema, conn, SchemaType::Labels)?; Ok(Node { entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - labels: parsed_labels, - properties: parse_map_with_schema( - properties, - graph_schema, - conn, - SchemaType::Properties, - )?, + labels: graph_schema.parse_labels_relationships(labels, conn, SchemaType::Labels)?, + properties: graph_schema.parse_properties_map(properties, conn)?, }) } } @@ -146,35 +139,7 @@ impl FalkorParsable for Edge { 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)?, - properties: parse_map_with_schema( - properties, - graph_schema, - conn, - SchemaType::Properties, - )?, + properties: graph_schema.parse_properties_map(properties, conn)?, }) } } - -pub(crate) fn parse_labels( - raw_ids: Vec, - graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, - schema_type: SchemaType, -) -> FalkorResult> { - let ids_hashset = raw_ids - .iter() - .filter_map(|label_id| label_id.to_i64()) - .collect::>(); - - let relevant_ids = match graph_schema.verify_id_set(&ids_hashset, schema_type) { - None => graph_schema.refresh(conn, schema_type, Some(&ids_hashset))?, - relevant_ids => relevant_ids, - } - .ok_or(FalkorDBError::ParsingError)?; - - Ok(raw_ids - .into_iter() - .filter_map(|id| id.to_i64().and_then(|id| relevant_ids.get(&id).cloned())) - .collect()) -} diff --git a/src/value/map.rs b/src/value/map.rs index ee99bce..e105984 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -5,86 +5,10 @@ use crate::{ connection::blocking::BorrowedSyncConnection, value::utils::parse_type, FalkorDBError, - FalkorParsable, FalkorResult, FalkorValue, GraphSchema, SchemaType, + FalkorParsable, FalkorResult, FalkorValue, GraphSchema, }; use std::collections::HashMap; -// Intermediate type for map parsing -pub(crate) struct FKeyTypeVal { - key: i64, - type_marker: i64, - val: FalkorValue, -} - -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)?; - - let key = key_raw.to_i64(); - let type_marker = type_raw.to_i64(); - - match (key, type_marker) { - (Some(key), Some(type_marker)) => Ok(FKeyTypeVal { - key, - type_marker, - val, - }), - (Some(_), None) => Err(FalkorDBError::ParsingTypeMarkerTypeMismatch)?, - (None, Some(_)) => Err(FalkorDBError::ParsingKeyIdTypeMismatch)?, - _ => Err(FalkorDBError::ParsingKTVTypes)?, - } - } -} - -fn ktv_vec_to_map( - map_vec: Vec, - relevant_ids_map: HashMap, - graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, -) -> FalkorResult> { - let mut new_map = HashMap::with_capacity(map_vec.len()); - for fktv in map_vec { - new_map.insert( - relevant_ids_map - .get(&fktv.key) - .cloned() - .ok_or(FalkorDBError::ParsingError)?, - parse_type(fktv.type_marker, fktv.val, graph_schema, conn)?, - ); - } - - Ok(new_map) -} - -pub(crate) fn parse_map_with_schema( - value: FalkorValue, - graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, - schema_type: SchemaType, -) -> FalkorResult> { - let (id_hashset, map_vec) = value - .into_vec()? - .into_iter() - .flat_map(FKeyTypeVal::try_from) - .map(|fktv| (fktv.key, fktv)) - .unzip(); - - if let Some(relevant_ids_map) = graph_schema.verify_id_set(&id_hashset, schema_type) { - return ktv_vec_to_map(map_vec, relevant_ids_map, graph_schema, conn); - } - - // If we reached here, schema validation failed and we need to refresh our schema - match graph_schema.refresh(conn, schema_type, Some(&id_hashset))? { - Some(relevant_ids_map) => ktv_vec_to_map(map_vec, relevant_ids_map, graph_schema, conn), - None => Err(FalkorDBError::ParsingError)?, - } -} - impl FalkorParsable for HashMap { fn from_falkor_value( value: FalkorValue, diff --git a/src/value/point.rs b/src/value/point.rs index ea585dd..5e18ecb 100644 --- a/src/value/point.rs +++ b/src/value/point.rs @@ -30,8 +30,8 @@ impl Point { .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; Ok(Point { - latitude: lat.to_f64().ok_or(FalkorDBError::ParsingError)?, - longitude: long.to_f64().ok_or(FalkorDBError::ParsingError)?, + latitude: lat.to_f64().ok_or(FalkorDBError::ParsingF64)?, + longitude: long.to_f64().ok_or(FalkorDBError::ParsingF64)?, }) } } From e7cdc422f96590447b0170ca7ce6010d4d11cfa4 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 4 Jun 2024 12:35:22 +0300 Subject: [PATCH 44/62] Test, deny should work now --- .github/workflows/pr-checks.yml | 4 +--- src/client/blocking.rs | 10 ---------- 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index de2cec5..8520c82 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -18,10 +18,8 @@ jobs: check-deny: runs-on: ubuntu-latest steps: - - uses: EmbarkStudios/cargo-deny-action@v1 - uses: actions/checkout@v4 - - name: Check Deny - run: cargo deny --log-level info check + - uses: EmbarkStudios/cargo-deny-action@v1 check-doc: runs-on: ubuntu-latest diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 3397ef1..bc79afd 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -447,14 +447,4 @@ mod tests { .config_set("DELTA_MAX_PENDING_CHANGES", current_val) .ok(); } - - #[cfg(feature = "redis")] - #[test] - fn test_redis_sentinel_build() { - FalkorClientBuilder::new() - .with_connection_info("falkor://falkordb:123456@singlezonesentinellblb.instance-e0srmv0mc.hc-jx5tis6bc.us-central1.gcp.f2e0a955bb84.cloud:26379" - .try_into().expect("Could not construct connectioninfo")) - .build() - .expect("Could not create sentinel client"); - } } From 218d520832650d1065e34df69626ff859019190f Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 4 Jun 2024 13:24:24 +0300 Subject: [PATCH 45/62] Crates io stages --- .github/workflows/build.yml | 9 +++- .github/workflows/codecov.yml | 15 ++++--- .github/workflows/release.yml | 80 +++++++++++++++++++++++++++++++++++ Cargo.toml | 2 + README.md | 17 ++++++-- 5 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a78136a..211e97c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,5 +15,12 @@ jobs: - name: Build run: cargo build - uses: taiki-e/install-action@nextest + - name: Populate test graph + run: pip install falkordb && ./resources/populate_graph.py - name: Test - run: cargo nextest run --all \ No newline at end of file + run: cargo nextest run --all + services: + falkordb: + image: falkordb/falkordb:edge + ports: + - 6379:6379 \ No newline at end of file diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index b216681..6c89c39 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -10,14 +10,10 @@ env: jobs: coverage: runs-on: linux-latest - - services: - falkordb: - image: falkordb/falkordb:edge - ports: - - 6379:6379 steps: - uses: actions/checkout@v4 + - name: Populate test graph + run: pip install falkordb && ./resources/populate_graph.py - uses: taiki-e/install-action@cargo-llvm-cov - uses: taiki-e/install-action@nextest - name: Generate Code Coverage @@ -27,4 +23,9 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} files: codecov.json - fail_ci_if_error: true \ No newline at end of file + fail_ci_if_error: true + services: + falkordb: + image: falkordb/falkordb:edge + ports: + - 6379:6379 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dbb9956 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,80 @@ +name: Publish on crates.io + +on: + push: + tags: [ '*' ] + +env: + CARGO_TERM_COLOR: always + +jobs: + # Ensure formatting + check-fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check Rustfmt + run: cargo fmt --all --check + + # Make sure no unwanted licenses or yanked crates have slipped in + deny: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v1 + + # Make sure the release's build works + build-linux: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build + run: cargo build + - uses: taiki-e/install-action@nextest + - name: Populate test graph + run: pip install falkordb && ./resources/populate_graph.py + - name: Test + run: cargo nextest run --all + services: + falkordb: + image: falkordb/falkordb:edge + ports: + - 6379:6379 + + # Ensure no clippy warnings + check-clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check Clippy + run: cargo clippy --all + + # Make sure the release's docs are full + doc: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Doc + run: cargo doc --all + + # Actually publish to crates.io + crates-io: + needs: + - build-linux + - doc + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Cargo Release + run: cargo install cargo-release + - name: Login + run: cargo login ${{ secrets.CRATES_IO_API_TOKEN }} + - name: Publish + run: |- + cargo release \ + publish \ + --all-features \ + --allow-branch HEAD \ + --no-confirm \ + --no-verify \ + --execute \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 16e0e7c..ca291fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,8 @@ name = "falkordb" version = "0.1.0" edition = "2021" +description = "A FalkorDB Rust client" +license = "SSPL-1.0" [dependencies] diff --git a/README.md b/README.md index 3cfd874..45093ba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ -[![license](https://img.shields.io/github/license/falkordb/falkordb-client-rs.svg)](https://github.com/falkordb/falkordb-client-rs) -[![Release](https://img.shields.io/github/release/falkordb/falkordb-client-rs.svg)](https://github.com/falkordb/falkordb-client-rs/releases/latest) -[![Codecov](https://codecov.io/gh/falkordb/falkordb-client-rs/branch/main/graph/badge.svg)](https://codecov.io/gh/falkordb/falkordb-client-rs) +[![license](https://img.shields.io/github/license/falkordb/falkordb-rs.svg)](https://github.com/falkordb/falkordb-rs) +[![Release](https://img.shields.io/github/release/falkordb/falkordb-rs.svg)](https://github.com/falkordb/falkordb-rs/releases/latest) +[![Codecov](https://codecov.io/gh/falkordb/falkordb-rs/branch/main/graph/badge.svg)](https://codecov.io/gh/falkordb/falkordb-rs) +[![Docs](https://img.shields.io/docsrs/:crate)](https://docs.rs/crate/falkordb/0.1.0)\ [![Forum](https://img.shields.io/badge/Forum-falkordb-blue)](https://github.com/orgs/FalkorDB/discussions) [![Discord](https://img.shields.io/discord/1146782921294884966?style=flat-square)](https://discord.gg/ErBEqN9E) @@ -12,6 +13,14 @@ FalkorDB Rust client ## Usage +### Installation + +Just add it to your `Cargo.toml`, like so + +```toml +falkordb = { version = "0.1.0" } +``` + ### Run FalkorDB instance Docker: @@ -22,7 +31,7 @@ docker run --rm -p 6379:6379 falkordb/falkordb Or use our [sandbox](https://cloud.falkordb.com/sandbox) -### Example +### Code Example ```rust use falkordb::FalkorClientBuilder; From cddc83d2da9d415526ea94291458a904d86ca2fc Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 4 Jun 2024 15:43:10 +0300 Subject: [PATCH 46/62] Move conn into graph schema as well, much nicer now --- src/graph/blocking.rs | 4 ++-- src/graph/query_builder.rs | 10 ++++----- src/graph_schema/mod.rs | 43 ++++++++++++++++++++----------------- src/parser/mod.rs | 2 -- src/parser/utils.rs | 8 ++----- src/response/constraint.rs | 7 +++--- src/response/index.rs | 4 +--- src/value/graph_entities.rs | 13 ++++------- src/value/map.rs | 5 +---- src/value/mod.rs | 6 +----- src/value/path.rs | 10 +++------ src/value/utils.rs | 31 ++++++++++---------------- 12 files changed, 55 insertions(+), 88 deletions(-) diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 95b8e2c..5e16300 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -28,9 +28,9 @@ impl SyncGraph { graph_name: T, ) -> Self { Self { - client, graph_name: graph_name.to_string(), - graph_schema: GraphSchema::new(graph_name), // Required for requesting refreshes + graph_schema: GraphSchema::new(graph_name, client.clone()), // Required for requesting refreshes + client, } } diff --git a/src/graph/query_builder.rs b/src/graph/query_builder.rs index db3850d..28afc51 100644 --- a/src/graph/query_builder.rs +++ b/src/graph/query_builder.rs @@ -3,8 +3,8 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::BorrowedSyncConnection; use crate::{ + connection::blocking::BorrowedSyncConnection, parser::utils::{parse_header, parse_result_set}, Constraint, ExecutionPlan, FalkorDBError, FalkorIndex, FalkorParsable, FalkorResponse, FalkorResult, FalkorValue, ResultSet, SyncGraph, @@ -129,7 +129,7 @@ impl<'a> QueryBuilder<'a, FalkorResponse> { .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; FalkorResponse::from_response_with_headers( - parse_result_set(data, &mut self.graph.graph_schema, &mut conn)?, + parse_result_set(data, &mut self.graph.graph_schema)?, parse_header(header)?, stats, ) @@ -296,7 +296,7 @@ impl<'a> ProcedureBuilder<'a, FalkorResponse>> { .into_vec()? .into_iter() .flat_map(|index| { - FalkorIndex::from_falkor_value(index, &mut self.graph.graph_schema, &mut conn) + FalkorIndex::from_falkor_value(index, &mut self.graph.graph_schema) }) .collect(), stats, @@ -321,9 +321,7 @@ impl<'a> ProcedureBuilder<'a, FalkorResponse>> { query_res .into_vec()? .into_iter() - .flat_map(|item| { - Constraint::from_falkor_value(item, &mut self.graph.graph_schema, &mut conn) - }) + .flat_map(|item| Constraint::from_falkor_value(item, &mut self.graph.graph_schema)) .collect(), stats, ) diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index 0d8db09..1effc55 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -4,10 +4,11 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, value::utils::parse_type, FalkorDBError, - FalkorResult, FalkorValue, + client::blocking::FalkorSyncClientInner, value::utils::parse_type, FalkorDBError, FalkorResult, + FalkorValue, }; use std::collections::HashMap; +use std::sync::Arc; pub(crate) fn get_refresh_command(schema_type: SchemaType) -> &'static str { match schema_type { @@ -65,8 +66,9 @@ pub enum SchemaType { pub(crate) type IdMap = HashMap; /// A struct containing the various schema maps, allowing conversions between ids and their string representations. -#[derive(Clone, Debug, Default)] +#[derive(Clone)] pub struct GraphSchema { + client: Arc, graph_name: String, version: i64, labels: IdMap, @@ -75,10 +77,17 @@ pub struct GraphSchema { } impl GraphSchema { - pub(crate) fn new(graph_name: T) -> Self { + pub(crate) fn new( + graph_name: T, + client: Arc, + ) -> Self { Self { + client, graph_name: graph_name.to_string(), - ..Default::default() + version: 0, + labels: IdMap::new(), + properties: IdMap::new(), + relationships: IdMap::new(), } } @@ -110,7 +119,6 @@ impl GraphSchema { fn refresh( &mut self, - conn: &mut BorrowedSyncConnection, schema_type: SchemaType, ) -> FalkorResult<()> { let id_map = match schema_type { @@ -120,7 +128,9 @@ 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] = conn + let [_, keys, _]: [FalkorValue; 3] = self + .client + .borrow_connection()? .execute_command( Some(self.graph_name.as_str()), "GRAPH.QUERY", @@ -157,7 +167,6 @@ impl GraphSchema { pub(crate) fn parse_labels_relationships( &mut self, raw_ids: Vec, - conn: &mut BorrowedSyncConnection, schema_type: SchemaType, ) -> FalkorResult> { let ids_count = raw_ids.len(); @@ -181,7 +190,7 @@ impl GraphSchema { } // Refresh and try again - self.refresh(conn, schema_type)?; + self.refresh(schema_type)?; let mut out_vec = Vec::with_capacity(ids_count); for raw_id in raw_ids { @@ -199,7 +208,6 @@ impl GraphSchema { pub(crate) fn parse_properties_map( &mut self, value: FalkorValue, - conn: &mut BorrowedSyncConnection, ) -> FalkorResult> { let raw_properties: Vec<_> = value .into_vec()? @@ -223,14 +231,14 @@ impl GraphSchema { if success { let mut new_map = HashMap::with_capacity(properties_count); for (property, ktv) in out_vec.into_iter().zip(raw_properties) { - new_map.insert(property, parse_type(ktv.type_marker, ktv.val, self, conn)?); + new_map.insert(property, parse_type(ktv.type_marker, ktv.val, self)?); } return Ok(new_map); } // Refresh and try again - self.refresh(conn, SchemaType::Properties)?; + self.refresh(SchemaType::Properties)?; let mut new_map = HashMap::with_capacity(properties_count); for ktv in raw_properties { @@ -239,7 +247,7 @@ impl GraphSchema { .get(&ktv.key) .cloned() .ok_or(FalkorDBError::MissingSchemaId(SchemaType::Properties))?, - parse_type(ktv.type_marker, ktv.val, self, conn)?, + parse_type(ktv.type_marker, ktv.val, self)?, ); } @@ -249,17 +257,12 @@ impl GraphSchema { #[cfg(test)] pub(crate) mod tests { - use super::*; use crate::{test_utils::create_test_client, SyncGraph}; use std::collections::HashMap; - pub(crate) fn open_readonly_graph_with_modified_schema() -> (SyncGraph, BorrowedSyncConnection) - { + pub(crate) fn open_readonly_graph_with_modified_schema() -> SyncGraph { let client = create_test_client(); let mut graph = client.select_graph("imdb"); - let conn = client - .borrow_connection() - .expect("Could not borrow_connection"); graph.graph_schema.properties = HashMap::from([ (0, "age".to_string()), @@ -274,6 +277,6 @@ pub(crate) mod tests { graph.graph_schema.relationships = HashMap::from([(0, "very".to_string()), (1, "wow".to_string())]); - (graph, conn) + graph } } diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 032aaf2..2c1edb1 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -5,7 +5,6 @@ pub mod utils; -use crate::connection::blocking::BorrowedSyncConnection; use crate::{FalkorResult, FalkorValue, GraphSchema}; /// This trait allows implementing a parser from the table-style result sent by the database, to any other struct @@ -14,6 +13,5 @@ pub trait FalkorParsable: Sized { fn from_falkor_value( value: FalkorValue, graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, ) -> FalkorResult; } diff --git a/src/parser/utils.rs b/src/parser/utils.rs index ca45bce..82f8621 100644 --- a/src/parser/utils.rs +++ b/src/parser/utils.rs @@ -3,10 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{ - connection::blocking::BorrowedSyncConnection, value::utils::parse_type, FalkorDBError, - FalkorValue, GraphSchema, ResultSet, -}; +use crate::{value::utils::parse_type, FalkorDBError, FalkorValue, GraphSchema, ResultSet}; use anyhow::Result; pub(crate) fn string_vec_from_val(value: FalkorValue) -> Result, FalkorDBError> { @@ -44,13 +41,12 @@ pub(crate) fn parse_header(header: FalkorValue) -> Result, FalkorDBE pub(crate) fn parse_result_set( data: FalkorValue, graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, ) -> Result { let data_vec = data.into_vec()?; let mut out_vec = Vec::with_capacity(data_vec.len()); for column in data_vec { - out_vec.push(parse_type(6, column, graph_schema, conn)?.into_vec()?); + out_vec.push(parse_type(6, column, graph_schema)?.into_vec()?); } Ok(out_vec) diff --git a/src/response/constraint.rs b/src/response/constraint.rs index 5f8a26c..3854fcb 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -4,8 +4,8 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, value::utils::parse_type, EntityType, - FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, + value::utils::parse_type, EntityType, FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, + GraphSchema, }; use std::fmt::{Display, Formatter}; @@ -137,9 +137,8 @@ impl FalkorParsable for Constraint { fn from_falkor_value( value: FalkorValue, graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, ) -> FalkorResult { - parse_type(6, value, graph_schema, conn) + parse_type(6, value, graph_schema) .and_then(|parsed| parsed.into_vec().and_then(Constraint::from_value_vec)) } } diff --git a/src/response/index.rs b/src/response/index.rs index 42f5de6..dc1006e 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -3,7 +3,6 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::connection::blocking::BorrowedSyncConnection; use crate::{ value::utils::{parse_type, parse_vec, type_val_from_value}, EntityType, FalkorDBError, FalkorParsable, FalkorValue, GraphSchema, @@ -147,14 +146,13 @@ impl FalkorParsable for FalkorIndex { fn from_falkor_value( value: FalkorValue, graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, ) -> 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, conn) + parse_type(type_marker, val, graph_schema) }) .collect::>(); diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index 57e3604..69ce348 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -3,10 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{ - connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, FalkorResult, - FalkorValue, GraphSchema, SchemaType, -}; +use crate::{FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, SchemaType}; use anyhow::Result; use std::{ collections::{HashMap, HashSet}, @@ -78,7 +75,6 @@ impl FalkorParsable for Node { fn from_falkor_value( value: FalkorValue, graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, ) -> FalkorResult { let [entity_id, labels, properties]: [FalkorValue; 3] = value .into_vec()? @@ -96,8 +92,8 @@ impl FalkorParsable for Node { } Ok(Node { entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - labels: graph_schema.parse_labels_relationships(labels, conn, SchemaType::Labels)?, - properties: graph_schema.parse_properties_map(properties, conn)?, + labels: graph_schema.parse_labels_relationships(labels, SchemaType::Labels)?, + properties: graph_schema.parse_properties_map(properties)?, }) } } @@ -121,7 +117,6 @@ impl FalkorParsable for Edge { fn from_falkor_value( value: FalkorValue, graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, ) -> FalkorResult { let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = value .into_vec()? @@ -139,7 +134,7 @@ impl FalkorParsable for Edge { 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)?, - properties: graph_schema.parse_properties_map(properties, conn)?, + properties: graph_schema.parse_properties_map(properties)?, }) } } diff --git a/src/value/map.rs b/src/value/map.rs index e105984..958ec54 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -4,8 +4,7 @@ */ use crate::{ - connection::blocking::BorrowedSyncConnection, value::utils::parse_type, FalkorDBError, - FalkorParsable, FalkorResult, FalkorValue, GraphSchema, + value::utils::parse_type, FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, }; use std::collections::HashMap; @@ -13,7 +12,6 @@ impl FalkorParsable for HashMap { fn from_falkor_value( value: FalkorValue, graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, ) -> FalkorResult { let val_vec = value.into_vec()?; if val_vec.len() % 2 != 0 { @@ -39,7 +37,6 @@ impl FalkorParsable for HashMap { type_marker.to_i64().ok_or(FalkorDBError::ParsingKTVTypes)?, val, graph_schema, - conn, )?, )) }) diff --git a/src/value/mod.rs b/src/value/mod.rs index b1694bc..d9c2e74 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -3,10 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{ - connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, FalkorResult, - GraphSchema, -}; +use crate::{FalkorDBError, FalkorParsable, FalkorResult, GraphSchema}; use graph_entities::{Edge, Node}; use path::Path; use point::Point; @@ -315,7 +312,6 @@ impl FalkorParsable for FalkorValue { fn from_falkor_value( value: FalkorValue, _: &mut GraphSchema, - _: &mut BorrowedSyncConnection, ) -> FalkorResult { Ok(value) } diff --git a/src/value/path.rs b/src/value/path.rs index b268a6e..d41a5af 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -3,10 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{ - connection::blocking::BorrowedSyncConnection, Edge, FalkorDBError, FalkorParsable, - FalkorResult, FalkorValue, GraphSchema, Node, -}; +use crate::{Edge, FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, Node}; /// Represents a path between two nodes, contains all the nodes, and the relationships between them along the path #[derive(Clone, Debug, PartialEq)] @@ -21,7 +18,6 @@ impl FalkorParsable for Path { fn from_falkor_value( value: FalkorValue, graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, ) -> FalkorResult { let [nodes, relationships]: [FalkorValue; 2] = value .into_vec()? @@ -32,12 +28,12 @@ impl FalkorParsable for Path { nodes: nodes .into_vec()? .into_iter() - .flat_map(|node| Node::from_falkor_value(node, graph_schema, conn)) + .flat_map(|node| Node::from_falkor_value(node, graph_schema)) .collect(), relationships: relationships .into_vec()? .into_iter() - .flat_map(|edge| Edge::from_falkor_value(edge, graph_schema, conn)) + .flat_map(|edge| Edge::from_falkor_value(edge, graph_schema)) .collect(), }) } diff --git a/src/value/utils.rs b/src/value/utils.rs index af0c2e7..bbda959 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -3,10 +3,7 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{ - connection::blocking::BorrowedSyncConnection, FalkorDBError, FalkorParsable, FalkorValue, - GraphSchema, Point, -}; +use crate::{FalkorDBError, FalkorParsable, FalkorValue, GraphSchema, Point}; use anyhow::Result; pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValue), FalkorDBError> { @@ -23,7 +20,6 @@ pub(crate) fn parse_type( type_marker: i64, val: FalkorValue, graph_schema: &mut GraphSchema, - conn: &mut BorrowedSyncConnection, ) -> Result { let res = match type_marker { 1 => FalkorValue::None, @@ -37,16 +33,16 @@ pub(crate) fn parse_type( let mut out_vec = Vec::with_capacity(val_vec.len()); for item in val_vec { let (type_marker, val) = type_val_from_value(item)?; - out_vec.push(parse_type(type_marker, val, graph_schema, conn)?) + out_vec.push(parse_type(type_marker, val, graph_schema)?) } out_vec }), // The following types are sent as an array and require specific parsing functions - 7 => FalkorValue::Edge(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), - 8 => FalkorValue::Node(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), - 9 => FalkorValue::Path(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), - 10 => FalkorValue::Map(FalkorParsable::from_falkor_value(val, graph_schema, conn)?), + 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)?, }; @@ -71,7 +67,7 @@ mod tests { #[test] fn test_parse_edge() { - let (mut graph, mut conn) = open_readonly_graph_with_modified_schema(); + let mut graph = open_readonly_graph_with_modified_schema(); let res = parse_type( 7, @@ -94,7 +90,6 @@ mod tests { ]), ]), &mut graph.graph_schema, - &mut conn, ); assert!(res.is_ok()); @@ -118,7 +113,7 @@ mod tests { #[test] fn test_parse_node() { - let (mut graph, mut conn) = open_readonly_graph_with_modified_schema(); + let mut graph = open_readonly_graph_with_modified_schema(); let res = parse_type( 8, @@ -144,7 +139,6 @@ mod tests { ]), ]), &mut graph.graph_schema, - &mut conn, ); assert!(res.is_ok()); @@ -169,7 +163,7 @@ mod tests { #[test] fn test_parse_path() { - let (mut graph, mut conn) = open_readonly_graph_with_modified_schema(); + let mut graph = open_readonly_graph_with_modified_schema(); let res = parse_type( 9, @@ -209,7 +203,6 @@ mod tests { ]), ]), &mut graph.graph_schema, - &mut conn, ); assert!(res.is_ok()); @@ -236,7 +229,7 @@ mod tests { #[test] fn test_parse_map() { - let (mut graph, mut conn) = open_readonly_graph_with_modified_schema(); + let mut graph = open_readonly_graph_with_modified_schema(); let res = parse_type( 10, @@ -252,7 +245,6 @@ mod tests { FalkorValue::Array(vec![FalkorValue::I64(4), FalkorValue::Bool(true)]), ]), &mut graph.graph_schema, - &mut conn, ); assert!(res.is_ok()); @@ -272,13 +264,12 @@ mod tests { #[test] fn test_parse_point() { - let (mut graph, mut conn) = open_readonly_graph_with_modified_schema(); + 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, - &mut conn, ); assert!(res.is_ok()); From 8e40bd2ec66d9e2bc1c4bb43ce90aaf2a7b43c56 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 4 Jun 2024 16:42:38 +0300 Subject: [PATCH 47/62] Remove anyhow --- Cargo.lock | 7 ------- Cargo.toml | 3 +-- src/error/mod.rs | 2 +- src/parser/utils.rs | 1 - src/redis_ext.rs | 1 - src/response/index.rs | 1 - src/value/graph_entities.rs | 1 - src/value/utils.rs | 1 - 8 files changed, 2 insertions(+), 15 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cfef9c1..5404307 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -26,12 +26,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "anyhow" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" - [[package]] name = "autocfg" version = "1.3.0" @@ -129,7 +123,6 @@ dependencies = [ name = "falkordb" version = "0.1.0" dependencies = [ - "anyhow", "log", "parking_lot", "redis", diff --git a/Cargo.toml b/Cargo.toml index ca291fd..fd08fee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,7 @@ license = "SSPL-1.0" [dependencies] -anyhow = { version = "1.0.86", default-features = false, features = ["std"] } -log = { version = "0.4.21", default-features = false } +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"] } regex = { version = "1.10.4", default-features = false, features = ["std", "perf", "unicode-bool", "unicode-perl"] } diff --git a/src/error/mod.rs b/src/error/mod.rs index d9af547..29dd94a 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -6,7 +6,7 @@ use crate::SchemaType; /// A verbose error enum used throughout the client, messages are static string slices. -/// this allows easy [`anyhow`] integration using [`thiserror`] +/// this allows easy error integration using [`thiserror`] #[derive(thiserror::Error, Debug)] pub enum FalkorDBError { /// A required Id for parsing was not found in the schema diff --git a/src/parser/utils.rs b/src/parser/utils.rs index 82f8621..9d06cc9 100644 --- a/src/parser/utils.rs +++ b/src/parser/utils.rs @@ -4,7 +4,6 @@ */ use crate::{value::utils::parse_type, FalkorDBError, FalkorValue, GraphSchema, ResultSet}; -use anyhow::Result; pub(crate) fn string_vec_from_val(value: FalkorValue) -> Result, FalkorDBError> { value.into_vec().map(|value_as_vec| { diff --git a/src/redis_ext.rs b/src/redis_ext.rs index 4dfc922..43bb8cc 100644 --- a/src/redis_ext.rs +++ b/src/redis_ext.rs @@ -7,7 +7,6 @@ use crate::{ client::FalkorClientProvider, connection::blocking::FalkorSyncConnection, ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorValue, }; -use anyhow::Result; use redis::{FromRedisValue, RedisResult, RedisWrite, ToRedisArgs}; impl From for FalkorSyncConnection { diff --git a/src/response/index.rs b/src/response/index.rs index dc1006e..1eb7df5 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -7,7 +7,6 @@ use crate::{ value::utils::{parse_type, parse_vec, type_val_from_value}, EntityType, FalkorDBError, FalkorParsable, FalkorValue, GraphSchema, }; -use anyhow::Result; use std::collections::HashMap; /// The status of this index diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index 69ce348..0dbf471 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -4,7 +4,6 @@ */ use crate::{FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, SchemaType}; -use anyhow::Result; use std::{ collections::{HashMap, HashSet}, fmt::{Display, Formatter}, diff --git a/src/value/utils.rs b/src/value/utils.rs index bbda959..dd81753 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -4,7 +4,6 @@ */ use crate::{FalkorDBError, FalkorParsable, FalkorValue, GraphSchema, Point}; -use anyhow::Result; pub(crate) fn type_val_from_value(value: FalkorValue) -> Result<(i64, FalkorValue), FalkorDBError> { let [type_marker, val]: [FalkorValue; 2] = value From ac97fd4b2ab8720973851c08b80b8b3483af3677 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 4 Jun 2024 16:45:51 +0300 Subject: [PATCH 48/62] Remove unused exts --- src/client/mod.rs | 21 ++++++++++++--------- src/redis_ext.rs | 26 +------------------------- 2 files changed, 13 insertions(+), 34 deletions(-) diff --git a/src/client/mod.rs b/src/client/mod.rs index 1a94af1..cb13c4b 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -27,15 +27,18 @@ impl FalkorClientProvider { FalkorClientProvider::Redis { client: redis_client, sentinel, - } => match ( - connection_timeout, - sentinel.as_ref().unwrap_or(redis_client), - ) { - (None, redis_client) => redis_client.get_connection(), - (Some(timeout), redis_client) => redis_client.get_connection_with_timeout(timeout), - } - .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))? - .into(), + } => FalkorSyncConnection::Redis( + match ( + connection_timeout, + sentinel.as_ref().unwrap_or(redis_client), + ) { + (None, redis_client) => redis_client.get_connection(), + (Some(timeout), redis_client) => { + redis_client.get_connection_with_timeout(timeout) + } + } + .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, + ), }) } diff --git a/src/redis_ext.rs b/src/redis_ext.rs index 43bb8cc..88feba0 100644 --- a/src/redis_ext.rs +++ b/src/redis_ext.rs @@ -3,33 +3,9 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{ - client::FalkorClientProvider, connection::blocking::FalkorSyncConnection, ConfigValue, - FalkorConnectionInfo, FalkorDBError, FalkorValue, -}; +use crate::{ConfigValue, FalkorDBError, FalkorValue}; use redis::{FromRedisValue, RedisResult, RedisWrite, ToRedisArgs}; -impl From for FalkorSyncConnection { - fn from(value: redis::Connection) -> Self { - Self::Redis(value) - } -} - -impl From for FalkorConnectionInfo { - fn from(value: redis::ConnectionInfo) -> Self { - Self::Redis(value) - } -} - -impl From for FalkorClientProvider { - fn from(value: redis::Client) -> Self { - Self::Redis { - client: value, - sentinel: None, - } - } -} - impl ToRedisArgs for ConfigValue { fn write_redis_args( &self, From cffbea07a685d3d8d56ebb4bd9338ae4737cb914 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Tue, 4 Jun 2024 19:27:16 +0300 Subject: [PATCH 49/62] Support refreshing connection pool on connection errors --- src/client/blocking.rs | 126 +++++++++++++++++++------------------ src/client/builder.rs | 55 ++++------------ src/client/mod.rs | 33 +++++----- src/connection/blocking.rs | 43 +++++++++++-- src/error/mod.rs | 77 ++++++++++++----------- src/graph/blocking.rs | 35 ++++++----- src/graph/query_builder.rs | 28 ++++++--- src/graph_schema/mod.rs | 2 +- src/lib.rs | 2 +- 9 files changed, 211 insertions(+), 190 deletions(-) diff --git a/src/client/blocking.rs b/src/client/blocking.rs index bc79afd..ee4ba6b 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -13,57 +13,71 @@ use parking_lot::Mutex; use std::{ collections::HashMap, sync::{mpsc, Arc}, - time::Duration, }; pub(crate) struct FalkorSyncClientInner { _inner: Mutex, connection_pool_size: u8, - connection_pool_tx: mpsc::SyncSender, + connection_pool_tx: Mutex>, connection_pool_rx: Mutex>, } impl FalkorSyncClientInner { - pub(crate) fn borrow_connection(&self) -> Result { + pub(crate) fn borrow_connection( + &self, + pool_owner: Arc, + ) -> FalkorResult { Ok(BorrowedSyncConnection::new( self.connection_pool_rx .lock() .recv() .map_err(|_| FalkorDBError::EmptyConnection)?, - self.connection_pool_tx.clone(), + self.connection_pool_tx.lock().clone(), + pool_owner, )) } -} -#[cfg(feature = "redis")] -fn get_redis_info( - conn: &mut FalkorSyncConnection, - section: Option<&str>, -) -> FalkorResult> { - Ok(conn - .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()) + pub(crate) fn get_connection(&self) -> FalkorResult { + self._inner.lock().get_connection() + } + + pub(crate) fn refresh_connection_pool(&self) -> FalkorResult<()> { + let conn_pool_size = self.connection_pool_size as usize; + let (connection_pool_tx, connection_pool_rx) = mpsc::sync_channel(conn_pool_size); + + // One already exists + for _ in 0..conn_pool_size { + let new_conn = self + .get_connection() + .map_err(|err| FalkorDBError::RedisError(err.to_string()))?; + + connection_pool_tx + .send(new_conn) + .map_err(|_| FalkorDBError::EmptyConnection)?; + } + + *(self.connection_pool_tx.lock()) = connection_pool_tx; + *(self.connection_pool_rx.lock()) = connection_pool_rx; + + Ok(()) + } } #[cfg(feature = "redis")] fn is_sentinel(conn: &mut FalkorSyncConnection) -> FalkorResult { - let info_map = get_redis_info(conn, Some("server"))?; + let info_map = conn.get_redis_info(Some("server"))?; Ok(info_map .get("redis_mode") .map(|redis_mode| redis_mode == "sentinel") .unwrap_or_default()) } -fn get_sentinel_client( +#[cfg(feature = "redis")] +pub(crate) fn get_sentinel_client( client: &mut FalkorClientProvider, connection_info: &redis::ConnectionInfo, -) -> FalkorResult> { - let mut conn = client.get_connection(None)?; +) -> FalkorResult> { + let mut conn = client.get_connection()?; if !is_sentinel(&mut conn)? { return Ok(None); } @@ -90,30 +104,22 @@ fn get_sentinel_client( }) .collect(); - let (_name, host, port) = match ( - sentinel_master.get("name"), - sentinel_master.get("host"), - sentinel_master.get("ip"), - sentinel_master.get("port"), - ) { - (Some(name), Some(host), _, Some(port)) => (name, host, port), - (Some(name), _, Some(ip), Some(port)) => (name, ip, port), - _ => return Err(FalkorDBError::SentinelMastersCount), - }; - - let user_pass_string = match ( - connection_info.redis.username.as_ref(), - connection_info.redis.password.as_ref(), - ) { - (None, Some(pass)) => format!("{}@", pass), // Password-only authentication is allowed in legacy auth - (Some(user), Some(pass)) => format!("{user}:{pass}@"), - _ => "".to_string(), - }; - let url = format!("{}://{}{host}:{port}", "redis", user_pass_string); - - Ok(Some(redis::Client::open(url).map_err(|err| { - FalkorDBError::SentinelConnection(err.to_string()) - })?)) + let name = sentinel_master + .get("name") + .ok_or(FalkorDBError::SentinelMastersCount)?; + + Ok(Some( + redis::sentinel::SentinelClient::build( + vec![connection_info.to_owned()], + name.to_string(), + Some(redis::sentinel::SentinelNodeConnectionInfo { + tls_mode: None, + redis_connection_info: Some(connection_info.redis.clone()), + }), + redis::sentinel::SentinelServerType::Master, + ) + .map_err(|err| FalkorDBError::SentinelConnection(err.to_string()))?, + )) } /// This is the publicly exposed API of the sync Falkor Client @@ -134,23 +140,14 @@ impl FalkorSyncClient { mut client: FalkorClientProvider, connection_info: FalkorConnectionInfo, num_connections: u8, - timeout: Option, ) -> FalkorResult { let (connection_pool_tx, connection_pool_rx) = mpsc::sync_channel(num_connections as usize); - #[cfg(feature = "redis")] - #[allow(irrefutable_let_patterns)] - if let FalkorConnectionInfo::Redis(redis_conn_info) = &connection_info { - if let Some(sentinel) = get_sentinel_client(&mut client, redis_conn_info)? { - client.set_sentinel(sentinel); - } - } - // One already exists for _ in 0..num_connections { let new_conn = client - .get_connection(timeout) - .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?; + .get_connection() + .map_err(|err| FalkorDBError::RedisError(err.to_string()))?; connection_pool_tx .send(new_conn) @@ -161,7 +158,7 @@ impl FalkorSyncClient { inner: Arc::new(FalkorSyncClientInner { _inner: client.into(), connection_pool_size: num_connections, - connection_pool_tx, + connection_pool_tx: Mutex::new(connection_pool_tx), connection_pool_rx: Mutex::new(connection_pool_rx), }), _connection_info: connection_info, @@ -174,7 +171,13 @@ impl FalkorSyncClient { } pub(crate) fn borrow_connection(&self) -> FalkorResult { - self.inner.borrow_connection() + self.inner.borrow_connection(self.inner.clone()) + } + + /// Attempts to create a new connection pool, if successful, drops all connections in the pool and set the new ones. + /// Any borrowed connections may or may not be viable, as this does not assume the reason for the refresh, but they are considered stale, and will not be returned to the pool. + pub fn refresh_connection_pool(&self) -> FalkorResult<()> { + self.inner.refresh_connection_pool() } /// Return a list of graphs currently residing in the database @@ -289,8 +292,9 @@ impl FalkorSyncClient { &self, section: Option<&str>, ) -> FalkorResult> { - let mut conn = self.borrow_connection()?; - get_redis_info(conn.as_inner()?, section) + self.borrow_connection()? + .as_inner()? + .get_redis_info(section) } } diff --git a/src/client/builder.rs b/src/client/builder.rs index 8864800..60b5fa7 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -7,12 +7,10 @@ use crate::{ client::FalkorClientProvider, FalkorConnectionInfo, FalkorDBError, FalkorResult, FalkorSyncClient, }; -use std::time::Duration; /// A Builder-pattern implementation struct for creating a new Falkor client. pub struct FalkorClientBuilder { connection_info: Option, - timeout: Option, num_connections: u8, } @@ -52,23 +50,6 @@ impl FalkorClientBuilder { } } - /// Specify a timeout duration for requests and connections. - /// - /// # Arguments - /// * `timeout`: a [`Duration`], after which a timeout error will be returned from the connection. - /// - /// # Returns - /// The consumed and modified self. - pub fn with_timeout( - self, - timeout: Duration, - ) -> Self { - Self { - timeout: Some(timeout), - ..self - } - } - fn get_client>( connection_info: T ) -> FalkorResult { @@ -79,7 +60,7 @@ impl FalkorClientBuilder { #[cfg(feature = "redis")] FalkorConnectionInfo::Redis(connection_info) => FalkorClientProvider::Redis { client: redis::Client::open(connection_info.clone()) - .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, + .map_err(|err| FalkorDBError::RedisError(err.to_string()))?, sentinel: None, }, }) @@ -95,7 +76,6 @@ impl FalkorClientBuilder<'S'> { pub fn new() -> Self { FalkorClientBuilder { connection_info: None, - timeout: None, num_connections: 4, } } @@ -113,12 +93,18 @@ impl FalkorClientBuilder<'S'> { .connection_info .unwrap_or("falkor://127.0.0.1:6379".try_into()?); - FalkorSyncClient::create( - Self::get_client(connection_info.clone())?, - connection_info, - self.num_connections, - self.timeout, - ) + 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) = + super::blocking::get_sentinel_client(&mut client, redis_conn_info)? + { + client.set_sentinel(sentinel); + } + } + FalkorSyncClient::create(client, connection_info, self.num_connections) } } @@ -166,19 +152,4 @@ mod tests { let too_many = FalkorClientBuilder::new().with_num_connections(36).build(); assert!(too_many.is_err()); } - - #[test] - fn test_timeout() { - { - let client = FalkorClientBuilder::new() - .with_timeout(Duration::from_millis(100)) - .build(); - assert!(client.is_ok()); - } - - let impossible_client = FalkorClientBuilder::new() - .with_timeout(Duration::from_nanos(10)) - .build(); - assert!(impossible_client.is_err()); - } } diff --git a/src/client/mod.rs b/src/client/mod.rs index cb13c4b..d1ea22e 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -4,7 +4,6 @@ */ use crate::{connection::blocking::FalkorSyncConnection, FalkorDBError, FalkorResult}; -use std::time::Duration; pub(crate) mod blocking; pub(crate) mod builder; @@ -13,31 +12,27 @@ pub(crate) enum FalkorClientProvider { #[cfg(feature = "redis")] Redis { client: redis::Client, - sentinel: Option, + sentinel: Option, }, } impl FalkorClientProvider { - pub(crate) fn get_connection( - &mut self, - connection_timeout: Option, - ) -> FalkorResult { + pub(crate) fn get_connection(&mut self) -> FalkorResult { Ok(match self { #[cfg(feature = "redis")] FalkorClientProvider::Redis { - client: redis_client, - sentinel, + sentinel: Some(sentinel), + .. } => FalkorSyncConnection::Redis( - match ( - connection_timeout, - sentinel.as_ref().unwrap_or(redis_client), - ) { - (None, redis_client) => redis_client.get_connection(), - (Some(timeout), redis_client) => { - redis_client.get_connection_with_timeout(timeout) - } - } - .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, + sentinel + .get_connection() + .map_err(|err| FalkorDBError::RedisError(err.to_string()))?, + ), + #[cfg(feature = "redis")] + FalkorClientProvider::Redis { client, .. } => FalkorSyncConnection::Redis( + client + .get_connection() + .map_err(|err| FalkorDBError::RedisError(err.to_string()))?, ), }) } @@ -45,7 +40,7 @@ impl FalkorClientProvider { #[cfg(feature = "redis")] pub(crate) fn set_sentinel( &mut self, - sentinel_client: redis::Client, + sentinel_client: redis::sentinel::SentinelClient, ) { match self { FalkorClientProvider::Redis { sentinel, .. } => *sentinel = Some(sentinel_client), diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 487fff9..88192ec 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -3,8 +3,11 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{FalkorDBError, FalkorResult, FalkorValue}; -use std::sync::mpsc; +use crate::{client::blocking::FalkorSyncClientInner, FalkorDBError, FalkorResult, FalkorValue}; +use std::{ + collections::HashMap, + sync::{mpsc, Arc}, +}; pub(crate) enum FalkorSyncConnection { #[cfg(feature = "redis")] @@ -34,12 +37,33 @@ impl FalkorSyncConnection { redis::FromRedisValue::from_owned_redis_value( redis_conn .req_command(&cmd) - .map_err(|err| FalkorDBError::RedisConnectionError(err.to_string()))?, + .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())) } } } + + #[cfg(feature = "redis")] + 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()) + } } /// A container for a connection that is borrowed from the pool. @@ -49,16 +73,19 @@ impl FalkorSyncConnection { pub struct BorrowedSyncConnection { conn: Option, return_tx: mpsc::SyncSender, + client: Arc, } impl BorrowedSyncConnection { pub(crate) fn new( conn: FalkorSyncConnection, return_tx: mpsc::SyncSender, + client: Arc, ) -> Self { Self { conn: Some(conn), return_tx, + client, } } @@ -73,8 +100,16 @@ impl BorrowedSyncConnection { subcommand: Option<&str>, params: Option<&[&str]>, ) -> Result { - self.as_inner()? + match self + .as_inner()? .execute_command(graph_name, command, subcommand, params) + { + Err(FalkorDBError::ConnectionDown) => { + self.client.refresh_connection_pool().ok(); + Err(FalkorDBError::ConnectionDown) + } + res => res, + } } } diff --git a/src/error/mod.rs b/src/error/mod.rs index 29dd94a..d6169de 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -9,30 +9,33 @@ use crate::SchemaType; /// this allows easy error integration using [`thiserror`] #[derive(thiserror::Error, Debug)] pub enum FalkorDBError { - /// A required Id for parsing was not found in the schema + /// A required Id for parsing was not found in the schema. #[error("A required Id for parsing was not found in the schema")] MissingSchemaId(SchemaType), - /// Could not connect to Redis Sentinel, or a critical Sentinel operation has failed + /// Could not connect to Redis Sentinel, or a critical Sentinel operation has failed. #[error( "Could not connect to Redis Sentinel, or a critical Sentinel operation has failed: {0}" )] SentinelConnection(String), - /// Received unsupported number of sentinel masters in list, there can be only one + /// Received unsupported number of sentinel masters in list, there can be only one. #[error("Received unsupported number of sentinel masters in list, there can be only one")] SentinelMastersCount, - /// An error occurred while sending the request to Redis + ///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}")] - RedisConnectionError(String), - /// An error occurred while parsing the Redis response" + RedisError(String), + /// An error occurred while parsing the Redis response. #[error("An error occurred while parsing the Redis response: {0}")] RedisParsingError(String), - /// The provided connection info is invalid + /// The provided connection info is invalid. #[error("The provided connection info is invalid")] InvalidConnectionInfo, - /// The connection returned invalid data for this command + /// The connection returned invalid data for this command. #[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 + /// 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? @@ -40,88 +43,88 @@ pub enum FalkorDBError { "An error occurred when dealing with reference counts or RefCells, perhaps mutual borrows?" )] RefCountBooBoo, - /// The execution plan did not adhere to usual structure, and could not be parsed + /// The execution plan did not adhere to usual structure, and could not be parsed. #[error("The execution plan did not adhere to usual structure, and could not be parsed")] CorruptExecutionPlan, - /// The number of connections for the client has to be between 1 and 32 + /// The number of connections for the client has to be between 1 and 32. #[error("The number of connections for the client has to be between 1 and 32")] InvalidConnectionPoolSize, - /// Could not connect to the server with the provided address + /// Could not connect to the server with the provided address. #[error("Could not connect to the server with the provided address")] NoConnection, - /// Attempting to use an empty connection object + /// Attempting to use an empty connection object. #[error("Attempting to use an empty connection object")] EmptyConnection, - /// General parsing error + /// General parsing error. #[error("General parsing error: {0}")] ParsingError(String), - /// Received malformed header + /// Received malformed header. #[error("Received malformed header")] ParsingHeader, - /// The id received for this label/property/relationship was unknown + /// The id received for this label/property/relationship was unknown. #[error("The id received for this label/property/relationship was unknown")] ParsingCompactIdUnknown, - /// Unknown type + /// Unknown type. #[error("Unknown type")] ParsingUnknownType, - /// Element was not of type Bool + /// Element was not of type Bool. #[error("Element was not of type Bool")] ParsingBool, - /// Could not parse into config value, was not one of the supported types + /// Could not parse into config value, was not one of the supported types. #[error("Could not parse into config value, was not one of the supported types")] ParsingConfigValue, - /// Element was not of type I64 + /// Element was not of type I64. #[error("Element was not of type I64")] ParsingI64, - /// Element was not of type F64 + /// Element was not of type F64. #[error("Element was not of type F64")] ParsingF64, - /// Element was not of type FArray + /// Element was not of type FArray. #[error("Element was not of type FArray")] ParsingFArray, - /// Element was not of type FString + /// Element was not of type FString. #[error("Element was not of type FString")] ParsingFString, - /// Element was not of type FEdge + /// Element was not of type FEdge. #[error("Element was not of type FEdge")] ParsingFEdge, - /// Element was not of type FNode + /// Element was not of type FNode. #[error("Element was not of type FNode")] ParsingFNode, - /// Element was not of type FPath + /// Element was not of type FPath. #[error("Element was not of type FPath")] ParsingFPath, - /// Element was not of type FMap + /// Element was not of type FMap. #[error("Element was not of type FMap")] ParsingFMap, - /// Element was not of type FPoint + /// Element was not of type FPoint. #[error("Element was not of type FPoint")] ParsingFPoint, - /// Key id was not of type i64 + /// Key id was not of type i64. #[error("Key id was not of type i64")] ParsingKeyIdTypeMismatch, - /// Type marker was not of type i64 + /// Type marker was not of type i64. #[error("Type marker was not of type i64")] ParsingTypeMarkerTypeMismatch, - /// Both key id and type marker were not of type i64 + /// Both key id and type marker were not of type i64. #[error("Both key id and type marker were not of type i64")] ParsingKTVTypes, - /// Field missing or mismatched while parsing index + /// Field missing or mismatched while parsing index. #[error("Field missing or mismatched while parsing index")] ParsingIndex, - /// Attempting to parse an FArray into a struct, but the array doesn't have the expected element count + /// 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")] ParsingArrayToStructElementCount, - /// Invalid constraint type, expected 'UNIQUE' or 'MANDATORY' + /// Invalid constraint type, expected 'UNIQUE' or 'MANDATORY'. #[error("Invalid constraint type, expected 'UNIQUE' or 'MANDATORY'")] ConstraintType, - /// Invalid constraint status, expected 'OPERATIONAL', 'UNDER CONSTRUCTION' or 'FAILED' + /// Invalid constraint status, expected 'OPERATIONAL', 'UNDER CONSTRUCTION' or 'FAILED'. #[error("Invalid constraint status, expected 'OPERATIONAL', 'UNDER CONSTRUCTION' or 'FAILED'")] ConstraintStatus, - /// Invalid Index status, expected 'OPERATIONAL' or 'UNDER CONSTRUCTION' + /// Invalid Index status, expected 'OPERATIONAL' or 'UNDER CONSTRUCTION'. #[error("Invalid Index status, expected 'OPERATIONAL' or 'UNDER CONSTRUCTION'")] IndexStatus, - /// Invalid Index field type, expected 'RANGE', 'VECTOR' or 'FULLTEXT' + /// Invalid Index field type, expected 'RANGE', 'VECTOR' or 'FULLTEXT'. #[error("Invalid Index field type, expected 'RANGE', 'VECTOR' or 'FULLTEXT'")] IndexType, } diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index 5e16300..54838b5 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -6,10 +6,9 @@ use crate::{ client::blocking::FalkorSyncClientInner, Constraint, ConstraintType, EntityType, ExecutionPlan, FalkorIndex, FalkorResponse, FalkorResult, FalkorValue, GraphSchema, IndexType, - ProcedureBuilder, QueryBuilder, ResultSet, SlowlogEntry, + ProcedureQueryBuilder, QueryBuilder, ResultSet, SlowlogEntry, }; -use std::fmt::Display; -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, fmt::Display, sync::Arc}; /// The main graph API, this allows the user to perform graph operations while exposing as little details as possible. /// # Thread Safety @@ -48,8 +47,9 @@ impl SyncGraph { subcommand: Option<&str>, params: Option<&[&str]>, ) -> FalkorResult { - let mut conn = self.client.borrow_connection()?; - conn.execute_command(Some(self.graph_name.as_str()), command, subcommand, params) + self.client + .borrow_connection(self.client.clone())? + .execute_command(Some(self.graph_name.as_str()), command, subcommand, params) } /// Deletes the graph stored in the database, and drop all the schema caches. @@ -138,36 +138,36 @@ impl SyncGraph { QueryBuilder::new(self, "GRAPH.QUERY_RO", query_string) } - /// Creates a [`ProcedureBuilder`] for this graph - /// This [`ProcedureBuilder`] has to be dropped or ran using [`ProcedureBuilder::perform`], before reusing the graph, as it takes a mutable reference to the graph for as long as it exists + /// Creates a [`ProcedureQueryBuilder`] for this graph + /// This [`ProcedureQueryBuilder`] has to be dropped or ran using [`ProcedureQueryBuilder::perform`], before reusing the graph, as it takes a mutable reference to the graph for as long as it exists /// Read-only queries are more limited with the operations they are allowed to perform. /// /// # Arguments /// * `procedure_name`: The name of the procedure to call /// /// # Returns - /// A [`ProcedureBuilder`] object + /// A [`ProcedureQueryBuilder`] object pub fn call_procedure<'a, P>( &'a mut self, procedure_name: &'a str, - ) -> ProcedureBuilder

{ - ProcedureBuilder::new(self, procedure_name) + ) -> ProcedureQueryBuilder

{ + ProcedureQueryBuilder::new(self, procedure_name) } - /// Creates a [`ProcedureBuilder`] for this graph, for a readonly procedure - /// This [`ProcedureBuilder`] has to be dropped or ran using [`ProcedureBuilder::perform`], before reusing the graph, as it takes a mutable reference to the graph for as long as it exists + /// Creates a [`ProcedureQueryBuilder`] for this graph, for a readonly procedure + /// This [`ProcedureQueryBuilder`] has to be dropped or ran using [`ProcedureQueryBuilder::perform`], before reusing the graph, as it takes a mutable reference to the graph for as long as it exists /// Read-only procedures are more limited with the operations they are allowed to perform. /// /// # Arguments /// * `procedure_name`: The name of the procedure to call /// /// # Returns - /// A [`ProcedureBuilder`] object + /// A [`ProcedureQueryBuilder`] object pub fn call_proecdure_ro<'a, P>( &'a mut self, procedure_name: &'a str, - ) -> ProcedureBuilder

{ - ProcedureBuilder::new_readonly(self, procedure_name) + ) -> ProcedureQueryBuilder

{ + ProcedureQueryBuilder::new_readonly(self, procedure_name) } /// Calls the DB.INDICES procedure on the graph, returning all the indexing methods currently used @@ -175,7 +175,7 @@ impl SyncGraph { /// # Returns /// A [`Vec`] of [`FalkorIndex`] pub fn list_indices(&mut self) -> FalkorResult>> { - ProcedureBuilder::>>::new(self, "DB.INDEXES").perform() + ProcedureQueryBuilder::>>::new(self, "DB.INDEXES").perform() } /// Creates a new index in the graph, for the selected entity type(Node/Edge), selected label, and properties @@ -280,7 +280,8 @@ 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 pub fn list_constraints(&mut self) -> FalkorResult>> { - ProcedureBuilder::>>::new(self, "DB.CONSTRAINTS").perform() + ProcedureQueryBuilder::>>::new(self, "DB.CONSTRAINTS") + .perform() } /// Creates a new constraint for this graph, making the provided properties mandatory diff --git a/src/graph/query_builder.rs b/src/graph/query_builder.rs index 28afc51..904340e 100644 --- a/src/graph/query_builder.rs +++ b/src/graph/query_builder.rs @@ -104,7 +104,10 @@ impl<'a, Output> QueryBuilder<'a, Output> { impl<'a> QueryBuilder<'a, FalkorResponse> { /// Perform the query, retuning a [`FalkorResponse`], with a [`ResultSet`] as its `data` member pub fn perform(mut self) -> FalkorResult> { - let mut conn = self.graph.client.borrow_connection()?; + let mut conn = self + .graph + .client + .borrow_connection(self.graph.client.clone())?; let res = self.perform_common(&mut conn)?.into_vec()?; match res.len() { @@ -142,7 +145,10 @@ impl<'a> QueryBuilder<'a, FalkorResponse> { impl<'a> QueryBuilder<'a, ExecutionPlan> { /// Perform the query, returning an [`ExecutionPlan`] from the data returned pub fn perform(mut self) -> FalkorResult { - let mut conn = self.graph.client.borrow_connection()?; + let mut conn = self + .graph + .client + .borrow_connection(self.graph.client.clone())?; let res = self.perform_common(&mut conn)?; ExecutionPlan::try_from(res) @@ -190,7 +196,7 @@ pub(crate) fn generate_procedure_call( } /// A Builder-pattern struct that allows creating and performing procedure call on a graph -pub struct ProcedureBuilder<'a, Output> { +pub struct ProcedureQueryBuilder<'a, Output> { _unused: PhantomData, graph: &'a mut SyncGraph, readonly: bool, @@ -199,7 +205,7 @@ pub struct ProcedureBuilder<'a, Output> { yields: Option<&'a [&'a str]>, } -impl<'a, Output> ProcedureBuilder<'a, Output> { +impl<'a, Output> ProcedureQueryBuilder<'a, Output> { pub(crate) fn new( graph: &'a mut SyncGraph, procedure_name: &'a str, @@ -278,11 +284,14 @@ impl<'a, Output> ProcedureBuilder<'a, Output> { } } -impl<'a> ProcedureBuilder<'a, FalkorResponse>> { +impl<'a> ProcedureQueryBuilder<'a, FalkorResponse>> { /// Performs the procedure call and return a [`FalkorResponse`] type containing a result set of [`FalkorIndex`]s /// This functions consumes self pub fn perform(mut self) -> FalkorResult>> { - let mut conn = self.graph.client.borrow_connection()?; + let mut conn = self + .graph + .client + .borrow_connection(self.graph.client.clone())?; let [header, indices, stats]: [FalkorValue; 3] = self .perform_common(&mut conn)? @@ -304,11 +313,14 @@ impl<'a> ProcedureBuilder<'a, FalkorResponse>> { } } -impl<'a> ProcedureBuilder<'a, FalkorResponse>> { +impl<'a> ProcedureQueryBuilder<'a, FalkorResponse>> { /// Performs the procedure call and return a [`FalkorResponse`] type containing a result set of [`Constraint`]s /// This functions consumes self pub fn perform(mut self) -> FalkorResult>> { - let mut conn = self.graph.client.borrow_connection()?; + let mut conn = self + .graph + .client + .borrow_connection(self.graph.client.clone())?; let [header, query_res, stats]: [FalkorValue; 3] = self .perform_common(&mut conn)? diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index 1effc55..56f8005 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -130,7 +130,7 @@ 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 .client - .borrow_connection()? + .borrow_connection(self.client.clone())? .execute_command( Some(self.graph_name.as_str()), "GRAPH.QUERY", diff --git a/src/lib.rs b/src/lib.rs index 7765911..736d3f7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,7 @@ pub use connection_info::FalkorConnectionInfo; pub use error::FalkorDBError; pub use graph::{ blocking::SyncGraph, - query_builder::{ProcedureBuilder, QueryBuilder}, + query_builder::{ProcedureQueryBuilder, QueryBuilder}, }; pub use graph_schema::{GraphSchema, SchemaType}; pub use parser::FalkorParsable; From 23b888d32c8b46d08ccd4ce7781d33faab7796c8 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 13:13:12 +0300 Subject: [PATCH 50/62] Working nicely with strum --- .gitignore | 3 +- Cargo.lock | 35 ++++++++++++++++ Cargo.toml | 1 + src/client/blocking.rs | 5 ++- src/client/builder.rs | 29 ++++--------- src/connection/blocking.rs | 7 +++- src/error/mod.rs | 27 ++++-------- src/response/constraint.rs | 84 ++++--------------------------------- src/response/index.rs | 71 ++++--------------------------- src/value/graph_entities.rs | 50 ++-------------------- 10 files changed, 84 insertions(+), 228 deletions(-) diff --git a/.gitignore b/.gitignore index 304e18b..75b6df3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ .idea/ .vscode/ .vs/ -codecov.json \ No newline at end of file +/codecov.json +/dump.rdb \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 5404307..4fead3a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,6 +127,7 @@ dependencies = [ "parking_lot", "redis", "regex", + "strum", "thiserror", "tracing", "url-parse", @@ -191,6 +192,12 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "idna" version = "0.5.0" @@ -596,6 +603,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.18" @@ -652,6 +665,28 @@ version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index fd08fee..42eff90 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ 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"] } regex = { version = "1.10.4", 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"] } url-parse = "1.0.8" diff --git a/src/client/blocking.rs b/src/client/blocking.rs index ee4ba6b..f7feb81 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -17,6 +17,7 @@ use std::{ pub(crate) struct FalkorSyncClientInner { _inner: Mutex, + connection_pool_size: u8, connection_pool_tx: Mutex>, connection_pool_rx: Mutex>, @@ -305,12 +306,12 @@ mod tests { test_utils::{create_test_client, TestSyncGraphHandle}, FalkorClientBuilder, }; - use std::{mem, sync::mpsc::TryRecvError, thread}; + use std::{mem, num::NonZeroU8, sync::mpsc::TryRecvError, thread}; #[test] fn test_borrow_connection() { let client = FalkorClientBuilder::new() - .with_num_connections(6) + .with_num_connections(NonZeroU8::new(6).expect("Could not create a perfectly valid u8")) .build() .expect("Could not create client for this test"); diff --git a/src/client/builder.rs b/src/client/builder.rs index 60b5fa7..990ea6b 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -7,11 +7,12 @@ use crate::{ client::FalkorClientProvider, FalkorConnectionInfo, FalkorDBError, FalkorResult, FalkorSyncClient, }; +use std::num::NonZeroU8; /// A Builder-pattern implementation struct for creating a new Falkor client. pub struct FalkorClientBuilder { connection_info: Option, - num_connections: u8, + num_connections: NonZeroU8, } impl FalkorClientBuilder { @@ -42,7 +43,7 @@ impl FalkorClientBuilder { /// The consumed and modified self. pub fn with_num_connections( self, - num_connections: u8, + num_connections: NonZeroU8, ) -> Self { Self { num_connections, @@ -76,7 +77,7 @@ impl FalkorClientBuilder<'S'> { pub fn new() -> Self { FalkorClientBuilder { connection_info: None, - num_connections: 4, + num_connections: NonZeroU8::new(8).expect("Error creating perfectly valid u8"), } } @@ -85,10 +86,6 @@ impl FalkorClientBuilder<'S'> { /// # Returns /// a new [`FalkorSyncClient`] pub fn build(self) -> FalkorResult { - if self.num_connections < 1 || self.num_connections > 32 { - return Err(FalkorDBError::InvalidConnectionPoolSize); - } - let connection_info = self .connection_info .unwrap_or("falkor://127.0.0.1:6379".try_into()?); @@ -104,7 +101,7 @@ impl FalkorClientBuilder<'S'> { client.set_sentinel(sentinel); } } - FalkorSyncClient::create(client, connection_info, self.num_connections) + FalkorSyncClient::create(client, connection_info, self.num_connections.get()) } } @@ -118,7 +115,6 @@ mod tests { assert!(conneciton_info.is_ok()); assert!(FalkorClientBuilder::new() - .with_num_connections(4) .with_connection_info(conneciton_info.unwrap()) .build() .is_ok()); @@ -136,20 +132,11 @@ mod tests { #[test] fn test_connection_pool_size() { - let client = FalkorClientBuilder::new().with_num_connections(16).build(); + let client = FalkorClientBuilder::new() + .with_num_connections(NonZeroU8::new(16).expect("Could not create a perfectly fine u8")) + .build(); assert!(client.is_ok()); assert_eq!(client.unwrap().connection_pool_size(), 16); } - - #[test] - fn test_invalid_connection_pool_size() { - // Connection pool size must be between 0 and 32 - - let zero = FalkorClientBuilder::new().with_num_connections(0).build(); - assert!(zero.is_err()); - - let too_many = FalkorClientBuilder::new().with_num_connections(36).build(); - assert!(too_many.is_err()); - } } diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 88192ec..9633fb9 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -105,8 +105,11 @@ impl BorrowedSyncConnection { .execute_command(graph_name, command, subcommand, params) { Err(FalkorDBError::ConnectionDown) => { - self.client.refresh_connection_pool().ok(); - Err(FalkorDBError::ConnectionDown) + if let Ok(new_conn) = self.client.get_connection() { + self.conn = Some(new_conn); + return Err(FalkorDBError::ConnectionDown); + } + Err(FalkorDBError::NoConnection) } res => res, } diff --git a/src/error/mod.rs b/src/error/mod.rs index d6169de..1733561 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -46,9 +46,6 @@ pub enum FalkorDBError { /// The execution plan did not adhere to usual structure, and could not be parsed. #[error("The execution plan did not adhere to usual structure, and could not be parsed")] CorruptExecutionPlan, - /// The number of connections for the client has to be between 1 and 32. - #[error("The number of connections for the client has to be between 1 and 32")] - InvalidConnectionPoolSize, /// Could not connect to the server with the provided address. #[error("Could not connect to the server with the provided address")] NoConnection, @@ -109,22 +106,16 @@ 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, - /// Field missing or mismatched while parsing index. - #[error("Field missing or mismatched while parsing index")] - ParsingIndex, /// 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")] ParsingArrayToStructElementCount, - /// Invalid constraint type, expected 'UNIQUE' or 'MANDATORY'. - #[error("Invalid constraint type, expected 'UNIQUE' or 'MANDATORY'")] - ConstraintType, - /// Invalid constraint status, expected 'OPERATIONAL', 'UNDER CONSTRUCTION' or 'FAILED'. - #[error("Invalid constraint status, expected 'OPERATIONAL', 'UNDER CONSTRUCTION' or 'FAILED'")] - ConstraintStatus, - /// Invalid Index status, expected 'OPERATIONAL' or 'UNDER CONSTRUCTION'. - #[error("Invalid Index status, expected 'OPERATIONAL' or 'UNDER CONSTRUCTION'")] - IndexStatus, - /// Invalid Index field type, expected 'RANGE', 'VECTOR' or 'FULLTEXT'. - #[error("Invalid Index field type, expected 'RANGE', 'VECTOR' or 'FULLTEXT'")] - IndexType, + /// Invalid enum string variant was encountered when parsing + #[error("Invalid enum string variant was encountered when parsing: {0}")] + InvalidEnumType(String), +} + +impl From for FalkorDBError { + fn from(value: strum::ParseError) -> Self { + FalkorDBError::InvalidEnumType(value.to_string()) + } } diff --git a/src/response/constraint.rs b/src/response/constraint.rs index 3854fcb..e5ff766 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -7,10 +7,10 @@ use crate::{ value::utils::parse_type, EntityType, FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, }; -use std::fmt::{Display, Formatter}; /// The type of restriction to apply for the property -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, strum::EnumString, strum::Display)] +#[strum(serialize_all = "UPPERCASE")] pub enum ConstraintType { /// This property may only appear once on entities of this type and label. Unique, @@ -19,87 +19,19 @@ pub enum ConstraintType { Mandatory, } -impl Display for ConstraintType { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { - let str = match self { - ConstraintType::Unique => "UNIQUE", - ConstraintType::Mandatory => "MANDATORY", - }; - f.write_str(str) - } -} - -impl TryFrom<&str> for ConstraintType { - type Error = FalkorDBError; - - fn try_from(value: &str) -> FalkorResult { - Ok(match value.to_uppercase().as_str() { - "MANDATORY" => Self::Mandatory, - "UNIQUE" => Self::Unique, - _ => Err(FalkorDBError::ConstraintType)?, - }) - } -} - -impl TryFrom for ConstraintType { - type Error = FalkorDBError; - - fn try_from(value: String) -> FalkorResult { - value.as_str().try_into() - } -} - -impl TryFrom<&String> for ConstraintType { - type Error = FalkorDBError; - - fn try_from(value: &String) -> FalkorResult { - value.as_str().try_into() - } -} - /// The status of this constraint -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, strum::EnumString, strum::Display)] pub enum ConstraintStatus { /// This constraint is active on all entities of this type and label. + #[strum(serialize = "OPERATIONAL")] Active, /// This constraint is still being applied and verified. + #[strum(serialize = "UNDER CONSTRUCTION")] Pending, /// This constraint could not be applied, not all entities of this type and label are compliant. Failed, } -impl TryFrom<&str> for ConstraintStatus { - type Error = FalkorDBError; - - fn try_from(value: &str) -> FalkorResult { - Ok(match value.to_uppercase().as_str() { - "OPERATIONAL" => Self::Active, - "UNDER CONSTRUCTION" => Self::Pending, - "FAILED" => Self::Failed, - _ => Err(FalkorDBError::ConstraintStatus)?, - }) - } -} - -impl TryFrom for ConstraintStatus { - type Error = FalkorDBError; - - fn try_from(value: String) -> FalkorResult { - value.as_str().try_into() - } -} - -impl TryFrom<&String> for ConstraintStatus { - type Error = FalkorDBError; - - fn try_from(value: &String) -> FalkorResult { - value.as_str().try_into() - } -} - /// A constraint applied on all 'properties' of the graph entity 'label' in this graph #[derive(Clone, Debug, PartialEq)] pub struct Constraint { @@ -120,15 +52,15 @@ impl Constraint { let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = vlaue_vec.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; Ok(Constraint { - constraint_type: constraint_type_raw.into_string()?.try_into()?, + 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()?.try_into()?, - status: status_raw.into_string()?.try_into()?, + entity_type: entity_type_raw.into_string()?.as_str().try_into()?, + status: status_raw.into_string()?.as_str().try_into()?, }) } } diff --git a/src/response/index.rs b/src/response/index.rs index 1eb7df5..a706c18 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -10,44 +10,20 @@ use crate::{ use std::collections::HashMap; /// The status of this index -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, strum::EnumString, strum::Display)] +#[strum(serialize_all = "UPPERCASE")] pub enum IndexStatus { /// This index is active. + #[strum(serialize = "OPERATIONAL")] Active, /// This index is still being created. + #[strum(serialize = "UNDER CONSTRUCTION")] Pending, } -impl TryFrom<&str> for IndexStatus { - type Error = FalkorDBError; - - fn try_from(value: &str) -> Result { - Ok(match value.to_uppercase().as_str() { - "OPERATIONAL" => Self::Active, - "UNDER CONSTRUCTION" => Self::Pending, - _ => Err(FalkorDBError::IndexStatus)?, - }) - } -} - -impl TryFrom for IndexStatus { - type Error = FalkorDBError; - - fn try_from(value: String) -> Result { - value.as_str().try_into() - } -} - -impl TryFrom<&String> for IndexStatus { - type Error = FalkorDBError; - - fn try_from(value: &String) -> Result { - value.as_str().try_into() - } -} - /// The type of this indexed field -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, strum::EnumString, strum::Display)] +#[strum(serialize_all = "UPPERCASE")] pub enum IndexType { /// This index is a range Range, @@ -57,35 +33,6 @@ pub enum IndexType { Fulltext, } -impl TryFrom<&str> for IndexType { - type Error = FalkorDBError; - - fn try_from(value: &str) -> Result { - Ok(match value.to_uppercase().as_str() { - "RANGE" => Self::Range, - "VECTOR" => Self::Vector, - "FULLTEXT" => Self::Fulltext, - _ => Err(FalkorDBError::IndexType)?, - }) - } -} - -impl TryFrom for IndexType { - type Error = FalkorDBError; - - fn try_from(value: String) -> Result { - value.as_str().try_into() - } -} - -impl TryFrom<&String> for IndexType { - type Error = FalkorDBError; - - fn try_from(value: &String) -> Result { - value.as_str().try_into() - } -} - fn parse_types_map(value: FalkorValue) -> Result>, FalkorDBError> { let value: HashMap = value.try_into()?; @@ -94,7 +41,7 @@ fn parse_types_map(value: FalkorValue) -> Result> let val = val.into_vec()?; let mut field_types = Vec::with_capacity(val.len()); for field_type in val { - field_types.push(IndexType::try_from(field_type.into_string()?)?); + field_types.push(field_type.into_string()?.as_str().try_into()?); } out_map.insert(key, field_types); @@ -129,8 +76,8 @@ impl FalkorIndex { let [label, fields, field_types, language, stopwords, entity_type, status, info]: [FalkorValue; 8] = items.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; Ok(Self { - entity_type: EntityType::try_from(entity_type.into_string()?)?, - status: IndexStatus::try_from(status.into_string()?)?, + 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)?, diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index 0dbf471..0051479 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -4,61 +4,19 @@ */ use crate::{FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, SchemaType}; -use std::{ - collections::{HashMap, HashSet}, - fmt::{Display, Formatter}, -}; +use std::collections::{HashMap, HashSet}; /// Whether this element is a node or edge in the graph -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, strum::EnumString, strum::Display)] +#[strum(serialize_all = "UPPERCASE")] pub enum EntityType { /// A node in the graph Node, /// An edge in the graph, meaning a relationship between two nodes + #[strum(serialize = "RELATIONSHIP")] Edge, } -impl Display for EntityType { - fn fmt( - &self, - f: &mut Formatter<'_>, - ) -> std::fmt::Result { - let str = match self { - EntityType::Node => "NODE", - EntityType::Edge => "RELATIONSHIP", - }; - f.write_str(str) - } -} - -impl TryFrom<&str> for EntityType { - type Error = FalkorDBError; - - fn try_from(value: &str) -> Result { - Ok(match value.to_uppercase().as_str() { - "NODE" => Self::Node, - "RELATIONSHIP" => Self::Edge, - _ => Err(FalkorDBError::ConstraintType)?, - }) - } -} - -impl TryFrom for EntityType { - type Error = FalkorDBError; - - fn try_from(value: String) -> Result { - value.as_str().try_into() - } -} - -impl TryFrom<&String> for EntityType { - type Error = FalkorDBError; - - fn try_from(value: &String) -> Result { - value.as_str().try_into() - } -} - /// A node in the graph, containing a unique id, various labels describing it, and its own property. #[derive(Clone, Debug, PartialEq)] pub struct Node { From 0edc1669be1b31c40393d561d45d90fa56868c4a Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 13:29:57 +0300 Subject: [PATCH 51/62] Allow sentinel tls --- src/client/blocking.rs | 10 +++++++++- src/connection_info/mod.rs | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/client/blocking.rs b/src/client/blocking.rs index f7feb81..16c3650 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -114,7 +114,15 @@ pub(crate) fn get_sentinel_client( vec![connection_info.to_owned()], name.to_string(), Some(redis::sentinel::SentinelNodeConnectionInfo { - tls_mode: None, + tls_mode: match connection_info.addr { + redis::ConnectionAddr::TcpTls { insecure: true, .. } => { + Some(redis::TlsMode::Insecure) + } + redis::ConnectionAddr::TcpTls { + insecure: false, .. + } => Some(redis::TlsMode::Secure), + _ => None, + }, redis_connection_info: Some(connection_info.redis.clone()), }), redis::sentinel::SentinelServerType::Master, diff --git a/src/connection_info/mod.rs b/src/connection_info/mod.rs index ebafbde..a54e42b 100644 --- a/src/connection_info/mod.rs +++ b/src/connection_info/mod.rs @@ -72,7 +72,7 @@ impl TryFrom<&str> for FalkorConnectionInfo { ); match scheme.as_str() { - "redis" | "rediss" => { + "redis" | "rediss" | "redis+unix" => { #[cfg(feature = "redis")] return Ok(FalkorConnectionInfo::Redis( redis::IntoConnectionInfo::into_connection_info(serialized) From fbb431d688915b23ceea60f85d15386c2fa9c5f1 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 14:18:39 +0300 Subject: [PATCH 52/62] Convert tx to RwLock for better reponse --- src/client/blocking.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 16c3650..c513160 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -9,7 +9,7 @@ use crate::{ parser::utils::string_vec_from_val, ConfigValue, FalkorConnectionInfo, FalkorDBError, FalkorResult, FalkorValue, SyncGraph, }; -use parking_lot::Mutex; +use parking_lot::{Mutex, RwLock}; use std::{ collections::HashMap, sync::{mpsc, Arc}, @@ -19,7 +19,7 @@ pub(crate) struct FalkorSyncClientInner { _inner: Mutex, connection_pool_size: u8, - connection_pool_tx: Mutex>, + connection_pool_tx: RwLock>, connection_pool_rx: Mutex>, } @@ -33,7 +33,7 @@ impl FalkorSyncClientInner { .lock() .recv() .map_err(|_| FalkorDBError::EmptyConnection)?, - self.connection_pool_tx.lock().clone(), + self.connection_pool_tx.read().clone(), pool_owner, )) } @@ -57,7 +57,7 @@ impl FalkorSyncClientInner { .map_err(|_| FalkorDBError::EmptyConnection)?; } - *(self.connection_pool_tx.lock()) = connection_pool_tx; + *(self.connection_pool_tx.write()) = connection_pool_tx; *(self.connection_pool_rx.lock()) = connection_pool_rx; Ok(()) @@ -167,7 +167,7 @@ impl FalkorSyncClient { inner: Arc::new(FalkorSyncClientInner { _inner: client.into(), connection_pool_size: num_connections, - connection_pool_tx: Mutex::new(connection_pool_tx), + connection_pool_tx: RwLock::new(connection_pool_tx), connection_pool_rx: Mutex::new(connection_pool_rx), }), _connection_info: connection_info, From ede07c934a5ffee45e22cb096aa7a240ee4616af Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 16:40:32 +0300 Subject: [PATCH 53/62] Various clippy, coderabbit suggestions --- .github/workflows/release.yml | 11 ++-- Cargo.lock | 10 --- Cargo.toml | 1 - README.md | 6 +- src/client/blocking.rs | 50 ++++---------- src/client/builder.rs | 4 +- src/connection_info/mod.rs | 46 ++++--------- src/error/mod.rs | 16 ++--- src/graph/blocking.rs | 16 ++--- src/graph/query_builder.rs | 121 ++++++++++++++++++---------------- src/graph_schema/mod.rs | 116 ++++++++++++++++---------------- src/parser/utils.rs | 12 ++-- src/response/constraint.rs | 2 +- src/response/index.rs | 2 +- src/response/slowlog_entry.rs | 10 +-- src/value/graph_entities.rs | 22 ++++--- src/value/map.rs | 16 +++-- src/value/mod.rs | 4 +- src/value/path.rs | 10 +-- src/value/point.rs | 9 +-- src/value/utils.rs | 9 +-- 21 files changed, 235 insertions(+), 258 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dbb9956..03cdc36 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,7 +9,7 @@ env: jobs: # Ensure formatting - check-fmt: + fmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -24,7 +24,7 @@ jobs: - uses: EmbarkStudios/cargo-deny-action@v1 # Make sure the release's build works - build-linux: + build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -42,7 +42,7 @@ jobs: - 6379:6379 # Ensure no clippy warnings - check-clippy: + clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -60,7 +60,10 @@ jobs: # Actually publish to crates.io crates-io: needs: - - build-linux + - fmt + - deny + - build + - clippy - doc runs-on: ubuntu-latest steps: diff --git a/Cargo.lock b/Cargo.lock index 4fead3a..f764631 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -130,7 +130,6 @@ dependencies = [ "strum", "thiserror", "tracing", - "url-parse", ] [[package]] @@ -830,15 +829,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "url-parse" -version = "1.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "865ece61c15cae30f180636ae551daa25c318c181938da07f3ab3ed06750bdd2" -dependencies = [ - "regex", -] - [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 42eff90..5ec4d84 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ regex = { version = "1.10.4", default-features = false, features = ["std", "perf 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"] } -url-parse = "1.0.8" [features] default = ["redis"] diff --git a/README.md b/README.md index 45093ba..e8e5973 100644 --- a/README.md +++ b/README.md @@ -29,21 +29,19 @@ Docker: docker run --rm -p 6379:6379 falkordb/falkordb ``` -Or use our [sandbox](https://cloud.falkordb.com/sandbox) - ### Code Example ```rust use falkordb::FalkorClientBuilder; // Connect to FalkorDB -let client = FalkorClientBuilder::new().with_connection_info("falkor://127.0.0.1:6379".try_into().unwrap()).build().unwrap(); +let client = FalkorClientBuilder::new().with_connection_info("falkor://127.0.0.1:6379".try_into().expect("Failed constructing connection info")).build().expect("Failed to build client"); // Select the social graph let mut graph = client.select_graph("social"); // Create 100 nodes and return a handful -let nodes = graph.query("UNWIND range(0, 100) AS i CREATE (n { v:1 }) RETURN n LIMIT 10").with_timeout(5000).perform().unwrap().data; +let nodes = graph.query("UNWIND range(0, 100) AS i CREATE (n { v:1 }) RETURN n LIMIT 10").with_timeout(5000).execute().expect("Failed performing query").data; for n in nodes { println!("{:?}", n[0]); } diff --git a/src/client/blocking.rs b/src/client/blocking.rs index c513160..7bc4b10 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -41,27 +41,6 @@ impl FalkorSyncClientInner { pub(crate) fn get_connection(&self) -> FalkorResult { self._inner.lock().get_connection() } - - pub(crate) fn refresh_connection_pool(&self) -> FalkorResult<()> { - let conn_pool_size = self.connection_pool_size as usize; - let (connection_pool_tx, connection_pool_rx) = mpsc::sync_channel(conn_pool_size); - - // One already exists - for _ in 0..conn_pool_size { - let new_conn = self - .get_connection() - .map_err(|err| FalkorDBError::RedisError(err.to_string()))?; - - connection_pool_tx - .send(new_conn) - .map_err(|_| FalkorDBError::EmptyConnection)?; - } - - *(self.connection_pool_tx.write()) = connection_pool_tx; - *(self.connection_pool_rx.lock()) = connection_pool_rx; - - Ok(()) - } } #[cfg(feature = "redis")] @@ -183,12 +162,6 @@ impl FalkorSyncClient { self.inner.borrow_connection(self.inner.clone()) } - /// Attempts to create a new connection pool, if successful, drops all connections in the pool and set the new ones. - /// Any borrowed connections may or may not be viable, as this does not assume the reason for the refresh, but they are considered stale, and will not be returned to the pool. - pub fn refresh_connection_pool(&self) -> FalkorResult<()> { - self.inner.refresh_connection_pool() - } - /// Return a list of graphs currently residing in the database /// /// # Returns @@ -217,9 +190,11 @@ impl FalkorSyncClient { .into_vec()?; if config.len() == 2 { - let [key, val]: [FalkorValue; 2] = config - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + let [key, val]: [FalkorValue; 2] = config.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 2 elements for configuration option".to_string(), + ) + })?; return Ok(HashMap::from([( key.into_string()?, @@ -230,10 +205,11 @@ impl FalkorSyncClient { Ok(config .into_iter() .flat_map(|config| { - let [key, val]: [FalkorValue; 2] = config - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + 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)?)) }) @@ -359,7 +335,7 @@ mod tests { let res = graph .query("MATCH (a:actor) return a") - .perform() + .execute() .expect("Could not get actors from unmodified graph"); assert_eq!(res.data.len(), 1317); @@ -384,12 +360,12 @@ mod tests { graph .inner .query("MATCH (a:actor) RETURN a") - .perform() + .execute() .expect("Could not get actors from unmodified graph") .data, original_graph .query("MATCH (a:actor) RETURN a") - .perform() + .execute() .expect("Could not get actors from unmodified graph") .data ) diff --git a/src/client/builder.rs b/src/client/builder.rs index 990ea6b..1e72447 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -51,12 +51,12 @@ impl FalkorClientBuilder { } } - fn get_client>( + fn get_client>( connection_info: T ) -> FalkorResult { let connection_info = connection_info .try_into() - .map_err(|_| FalkorDBError::InvalidConnectionInfo)?; + .map_err(|err| FalkorDBError::InvalidConnectionInfo(err.to_string()))?; Ok(match connection_info { #[cfg(feature = "redis")] FalkorConnectionInfo::Redis(connection_info) => FalkorClientProvider::Redis { diff --git a/src/connection_info/mod.rs b/src/connection_info/mod.rs index a54e42b..6459913 100644 --- a/src/connection_info/mod.rs +++ b/src/connection_info/mod.rs @@ -24,7 +24,7 @@ impl FalkorConnectionInfo { full_url = full_url.replace("falkors://", "rediss://"); } redis::IntoConnectionInfo::into_connection_info(full_url) - .map_err(|_| FalkorDBError::InvalidConnectionInfo)? + .map_err(|err| FalkorDBError::InvalidConnectionInfo(err.to_string()))? })) } @@ -44,44 +44,24 @@ impl TryFrom<&str> for FalkorConnectionInfo { type Error = FalkorDBError; fn try_from(value: &str) -> FalkorResult { - let url = url_parse::core::Parser::new(None) - .parse(value) - .map_err(|_| FalkorDBError::InvalidConnectionInfo)?; - - // The url_parse serializer seems ***ed up for some reason - let scheme = url.scheme.unwrap_or("falkor".to_string()); - let user_pass_string = match url.user_pass { - (Some(pass), None) => format!("{}@", pass), // Password-only authentication is allowed in legacy auth - (Some(user), Some(pass)) => format!("{user}:{pass}@"), - _ => "".to_string(), - }; - let subdomain = url - .subdomain - .map(|subdomain| format!("{subdomain}.")) - .unwrap_or_default(); - - let domain = url.domain.unwrap_or("127.0.0.1".to_string()); - let top_level_domain = url - .top_level_domain - .map(|top_level_domain| format!(".{top_level_domain}")) - .unwrap_or_default(); - let port = url.port.unwrap_or(6379); // Might need to change in accordance with the default fallback - let serialized = format!( - "{}://{}{}{}{}:{}", - scheme, user_pass_string, subdomain, domain, top_level_domain, port - ); - - match scheme.as_str() { - "redis" | "rediss" | "redis+unix" => { + let (url, url_schema) = regex::Regex::new(r"^(?P[a-zA-Z][a-zA-Z0-9+\-.]*):") + .map_err(|err| FalkorDBError::ParsingError(format!("Error constructing regex: {err}")))? + .captures(value) + .and_then(|cap| cap.get(1)) + .map(|m| (value.to_string(), m.as_str())) + .unwrap_or((format!("falkor://{value}"), "falkor")); + + match url_schema { + "redis" | "rediss" => { #[cfg(feature = "redis")] return Ok(FalkorConnectionInfo::Redis( - redis::IntoConnectionInfo::into_connection_info(serialized) - .map_err(|_| FalkorDBError::InvalidConnectionInfo)?, + redis::IntoConnectionInfo::into_connection_info(value) + .map_err(|err| FalkorDBError::InvalidConnectionInfo(err.to_string()))?, )); #[cfg(not(feature = "redis"))] return Err(FalkorDBError::UnavailableProvider); } - _ => FalkorConnectionInfo::fallback_provider(serialized), + _ => FalkorConnectionInfo::fallback_provider(url), } } } diff --git a/src/error/mod.rs b/src/error/mod.rs index 1733561..63f8095 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -30,8 +30,8 @@ pub enum FalkorDBError { #[error("An error occurred while parsing the Redis response: {0}")] RedisParsingError(String), /// The provided connection info is invalid. - #[error("The provided connection info is invalid")] - InvalidConnectionInfo, + #[error("Could not parse the provided connection info: {0}")] + InvalidConnectionInfo(String), /// The connection returned invalid data for this command. #[error("The connection returned invalid data for this command")] InvalidDataReceived, @@ -56,8 +56,8 @@ pub enum FalkorDBError { #[error("General parsing error: {0}")] ParsingError(String), /// Received malformed header. - #[error("Received malformed header")] - ParsingHeader, + #[error("Could not parse header: {0}")] + ParsingHeader(String), /// The id received for this label/property/relationship was unknown. #[error("The id received for this label/property/relationship was unknown")] ParsingCompactIdUnknown, @@ -92,8 +92,8 @@ pub enum FalkorDBError { #[error("Element was not of type FPath")] ParsingFPath, /// Element was not of type FMap. - #[error("Element was not of type FMap")] - ParsingFMap, + #[error("Element was not of type FMap: {0}")] + ParsingFMap(String), /// Element was not of type FPoint. #[error("Element was not of type FPoint")] ParsingFPoint, @@ -107,8 +107,8 @@ pub enum FalkorDBError { #[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")] - ParsingArrayToStructElementCount, + #[error("Attempting to parse an FArray into a struct, but the array doesn't have the expected element count: {0}")] + ParsingArrayToStructElementCount(String), /// 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 54838b5..ba2a648 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -175,7 +175,7 @@ impl SyncGraph { /// # Returns /// A [`Vec`] of [`FalkorIndex`] pub fn list_indices(&mut self) -> FalkorResult>> { - ProcedureQueryBuilder::>>::new(self, "DB.INDEXES").perform() + ProcedureQueryBuilder::>>::new(self, "DB.INDEXES").execute() } /// Creates a new index in the graph, for the selected entity type(Node/Edge), selected label, and properties @@ -233,7 +233,7 @@ impl SyncGraph { ) .as_str(), ) - .perform() + .execute() } /// Drop an existing index, by specifying its type, entity, label and specific properties @@ -272,7 +272,7 @@ impl SyncGraph { ) .as_str(), ) - .perform() + .execute() } /// Calls the DB.CONSTRAINTS procedure on the graph, returning an array of the graph's constraints @@ -281,7 +281,7 @@ impl SyncGraph { /// A tuple where the first element is a [`Vec`] of [`Constraint`]s, and the second element is a [`Vec`] of stats as [`String`]s pub fn list_constraints(&mut self) -> FalkorResult>> { ProcedureQueryBuilder::>>::new(self, "DB.CONSTRAINTS") - .perform() + .execute() } /// Creates a new constraint for this graph, making the provided properties mandatory @@ -504,12 +504,12 @@ mod tests { graph .inner .query("UNWIND range(0, 500) AS x RETURN x") - .perform() + .execute() .expect("Could not generate the fast query"); graph .inner .query("UNWIND range(0, 100000) AS x RETURN x") - .perform() + .execute() .expect("Could not generate the slow query"); let slowlog = graph @@ -542,7 +542,7 @@ mod tests { fn test_explain() { let mut graph = open_test_graph("test_explain"); - let execution_plan = graph.inner.explain("MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100").perform().expect("Could not create execution plan"); + let execution_plan = graph.inner.explain("MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100").execute().expect("Could not create execution plan"); assert_eq!(execution_plan.plan().len(), 7); assert!(execution_plan.operations().get("Aggregate").is_some()); assert_eq!(execution_plan.operations()["Aggregate"].len(), 1); @@ -560,7 +560,7 @@ mod tests { let execution_plan = graph .inner .profile("UNWIND range(0, 1000) AS x RETURN x") - .perform() + .execute() .expect("Could not generate the query"); assert_eq!(execution_plan.plan().len(), 3); diff --git a/src/graph/query_builder.rs b/src/graph/query_builder.rs index 904340e..a441c08 100644 --- a/src/graph/query_builder.rs +++ b/src/graph/query_builder.rs @@ -9,26 +9,25 @@ use crate::{ Constraint, ExecutionPlan, FalkorDBError, FalkorIndex, FalkorParsable, FalkorResponse, FalkorResult, FalkorValue, ResultSet, SyncGraph, }; -use std::{collections::HashMap, fmt::Display, marker::PhantomData, ops::Not}; +use std::{collections::HashMap, fmt::Display, marker::PhantomData}; -pub(crate) fn construct_query( +pub(crate) fn construct_query( query_str: Q, params: Option<&HashMap>, ) -> String { - format!( - "{}{}", - params - .and_then(|params| params.is_empty().not().then(|| params - .iter() - .fold("CYPHER ".to_string(), |acc, (key, val)| { - format!("{}{}={} ", acc, key.to_string(), val.to_string()) - }))) - .unwrap_or_default(), - query_str.to_string() - ) + let params_str = params + .map(|p| { + p.iter() + .map(|(k, v)| format!("{k}={v}")) + .collect::>() + .join(" ") + }) + .map(|params_str| format!("CYPHER {params_str} ")) + .unwrap_or_default(); + format!("{params_str}{query_str}") } -/// A Builder-pattern struct that allows creating and performing queries on a graph +/// A Builder-pattern struct that allows creating and executing queries on a graph pub struct QueryBuilder<'a, Output> { _unused: PhantomData, graph: &'a mut SyncGraph, @@ -82,7 +81,7 @@ impl<'a, Output> QueryBuilder<'a, Output> { } } - fn perform_common( + fn common_execute_steps( &mut self, conn: &mut BorrowedSyncConnection, ) -> FalkorResult { @@ -102,34 +101,39 @@ impl<'a, Output> QueryBuilder<'a, Output> { } impl<'a> QueryBuilder<'a, FalkorResponse> { - /// Perform the query, retuning a [`FalkorResponse`], with a [`ResultSet`] as its `data` member - pub fn perform(mut self) -> FalkorResult> { + /// Executes the query, retuning a [`FalkorResponse`], with a [`ResultSet`] as its `data` member + pub fn execute(mut self) -> FalkorResult> { let mut conn = self .graph .client .borrow_connection(self.graph.client.clone())?; - let res = self.perform_common(&mut conn)?.into_vec()?; + let res = self.common_execute_steps(&mut conn)?.into_vec()?; match res.len() { 1 => { - let stats = res - .into_iter() - .next() - .ok_or(FalkorDBError::ParsingArrayToStructElementCount)?; + let stats = res.into_iter().next().ok_or( + FalkorDBError::ParsingArrayToStructElementCount( + "One element exist but using next() failed".to_string(), + ), + )?; FalkorResponse::from_response(None, vec![], stats) } 2 => { - let [header, stats]: [FalkorValue; 2] = res - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + let [header, stats]: [FalkorValue; 2] = res.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Two elements exist but couldn't be parsed to an array".to_string(), + ) + })?; FalkorResponse::from_response(Some(header), vec![], stats) } 3 => { - let [header, data, stats]: [FalkorValue; 3] = res - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + let [header, data, stats]: [FalkorValue; 3] = res.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "3 elements exist but couldn't be parsed to an array".to_string(), + ) + })?; FalkorResponse::from_response_with_headers( parse_result_set(data, &mut self.graph.graph_schema)?, @@ -137,29 +141,39 @@ impl<'a> QueryBuilder<'a, FalkorResponse> { stats, ) } - _ => Err(FalkorDBError::ParsingArrayToStructElementCount)?, + _ => Err(FalkorDBError::ParsingArrayToStructElementCount( + "Invalid number of elements returned from query".to_string(), + ))?, } } } impl<'a> QueryBuilder<'a, ExecutionPlan> { - /// Perform the query, returning an [`ExecutionPlan`] from the data returned - pub fn perform(mut self) -> FalkorResult { + /// Executes the query, returning an [`ExecutionPlan`] from the data returned + pub fn execute(mut self) -> FalkorResult { let mut conn = self .graph .client .borrow_connection(self.graph.client.clone())?; - let res = self.perform_common(&mut conn)?; + let res = self.common_execute_steps(&mut conn)?; ExecutionPlan::try_from(res) } } -pub(crate) fn generate_procedure_call( +pub(crate) fn generate_procedure_call( procedure: P, args: Option<&[T]>, yields: Option<&[Z]>, ) -> (String, Option>) { + let args_str = args + .unwrap_or_default() + .iter() + .map(|e| format!("${}", e)) + .collect::>() + .join(","); + let mut query_string = format!("CALL {}({})", procedure, args_str); + let params = args.map(|args| { args.iter() .enumerate() @@ -169,18 +183,7 @@ pub(crate) fn generate_procedure_call( }) }); - let mut query_string = format!( - "CALL {}({})", - procedure.to_string(), - args.unwrap_or_default() - .iter() - .map(|element| format!("${}", element)) - .collect::>() - .join(",") - ); - - let yields = yields.unwrap_or_default(); - if !yields.is_empty() { + if let Some(yields) = yields { query_string += format!( " YIELD {}", yields @@ -195,7 +198,7 @@ pub(crate) fn generate_procedure_call( (query_string, params) } -/// A Builder-pattern struct that allows creating and performing procedure call on a graph +/// A Builder-pattern struct that allows creating and executing procedure call on a graph pub struct ProcedureQueryBuilder<'a, Output> { _unused: PhantomData, graph: &'a mut SyncGraph, @@ -262,7 +265,7 @@ impl<'a, Output> ProcedureQueryBuilder<'a, Output> { } } - fn perform_common( + fn common_execute_steps( &mut self, conn: &mut BorrowedSyncConnection, ) -> FalkorResult { @@ -285,19 +288,23 @@ impl<'a, Output> ProcedureQueryBuilder<'a, Output> { } impl<'a> ProcedureQueryBuilder<'a, FalkorResponse>> { - /// Performs the procedure call and return a [`FalkorResponse`] type containing a result set of [`FalkorIndex`]s + /// Executes the procedure call and return a [`FalkorResponse`] type containing a result set of [`FalkorIndex`]s /// This functions consumes self - pub fn perform(mut self) -> FalkorResult>> { + pub fn execute(mut self) -> FalkorResult>> { let mut conn = self .graph .client .borrow_connection(self.graph.client.clone())?; let [header, indices, stats]: [FalkorValue; 3] = self - .perform_common(&mut conn)? + .common_execute_steps(&mut conn)? .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + .map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 3 elements in query response".to_string(), + ) + })?; FalkorResponse::from_response( Some(header), @@ -314,19 +321,23 @@ impl<'a> ProcedureQueryBuilder<'a, FalkorResponse>> { } impl<'a> ProcedureQueryBuilder<'a, FalkorResponse>> { - /// Performs the procedure call and return a [`FalkorResponse`] type containing a result set of [`Constraint`]s + /// Executes the procedure call and return a [`FalkorResponse`] type containing a result set of [`Constraint`]s /// This functions consumes self - pub fn perform(mut self) -> FalkorResult>> { + pub fn execute(mut self) -> FalkorResult>> { let mut conn = self .graph .client .borrow_connection(self.graph.client.clone())?; let [header, query_res, stats]: [FalkorValue; 3] = self - .perform_common(&mut conn)? + .common_execute_steps(&mut conn)? .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + .map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 3 elements in query response".to_string(), + ) + })?; FalkorResponse::from_response( Some(header), diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index 56f8005..0500231 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -29,10 +29,12 @@ 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)?; + 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(), + ) + })?; let key = key_raw.to_i64(); let type_marker = type_raw.to_i64(); @@ -139,7 +141,12 @@ impl GraphSchema { )? .into_vec()? .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + .map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 3 types for header-resultset-stats response from refresh query" + .to_string(), + ) + })?; let new_keys = keys .into_vec()? @@ -164,38 +171,52 @@ impl GraphSchema { Ok(()) } - pub(crate) fn parse_labels_relationships( + pub(crate) fn parse_id_vec( &mut self, raw_ids: Vec, schema_type: SchemaType, ) -> FalkorResult> { let ids_count = raw_ids.len(); - let mut refs_vec = Vec::with_capacity(ids_count); - let mut success = true; - for raw_id in &raw_ids { - let id = raw_id.to_i64().ok_or(FalkorDBError::ParsingI64)?; - match self.labels.get(&id) { - None => { - success = false; - break; + { + let ids_map = match schema_type { + SchemaType::Labels => &self.labels, + SchemaType::Properties => &self.properties, + SchemaType::Relationships => &self.relationships, + }; + + let mut refs_vec = Vec::with_capacity(ids_count); + let mut success = true; + for raw_id in &raw_ids { + let id = raw_id.to_i64().ok_or(FalkorDBError::ParsingI64)?; + match ids_map.get(&id) { + None => { + success = false; + break; + } + Some(label) => refs_vec.push(label), } - Some(label) => refs_vec.push(label), } - } - if success { - // Clone the strings themselves and return the parsed labels - return Ok(refs_vec.into_iter().cloned().collect()); + if success { + // Clone the strings themselves and return the parsed labels + return Ok(refs_vec.into_iter().cloned().collect()); + } } // Refresh and try again self.refresh(schema_type)?; + let ids_map = match schema_type { + SchemaType::Labels => &self.labels, + SchemaType::Properties => &self.properties, + SchemaType::Relationships => &self.relationships, + }; + let mut out_vec = Vec::with_capacity(ids_count); for raw_id in raw_ids { out_vec.push( - self.labels + ids_map .get(&raw_id.to_i64().ok_or(FalkorDBError::ParsingI64)?) .cloned() .ok_or(FalkorDBError::MissingSchemaId(SchemaType::Labels))?, @@ -209,49 +230,28 @@ impl GraphSchema { &mut self, value: FalkorValue, ) -> FalkorResult> { - let raw_properties: Vec<_> = value - .into_vec()? - .into_iter() - .flat_map(FKeyTypeVal::try_from) - .collect(); - let properties_count = raw_properties.len(); - - let mut out_vec = Vec::with_capacity(properties_count); - let mut success = true; - for fktv in &raw_properties { - match self.properties().get(&fktv.key).cloned() { - None => { - success = false; - break; - } - Some(property) => out_vec.push(property), - } - } + let raw_properties_vec = value.into_vec()?; + let mut out_map = HashMap::with_capacity(raw_properties_vec.len()); - if success { - let mut new_map = HashMap::with_capacity(properties_count); - for (property, ktv) in out_vec.into_iter().zip(raw_properties) { - new_map.insert(property, parse_type(ktv.type_marker, ktv.val, self)?); - } - - return Ok(new_map); - } + for item in raw_properties_vec { + let ktv = FKeyTypeVal::try_from(item)?; - // Refresh and try again - self.refresh(SchemaType::Properties)?; + let key = match self.properties.get(&ktv.key).cloned() { + None => { + // Refresh, but this time when we try again, throw an error on failure + self.refresh(SchemaType::Properties)?; + self.properties + .get(&ktv.key) + .cloned() + .ok_or(FalkorDBError::MissingSchemaId(SchemaType::Properties))? + } + Some(key) => key, + }; - let mut new_map = HashMap::with_capacity(properties_count); - for ktv in raw_properties { - new_map.insert( - self.properties - .get(&ktv.key) - .cloned() - .ok_or(FalkorDBError::MissingSchemaId(SchemaType::Properties))?, - parse_type(ktv.type_marker, ktv.val, self)?, - ); + out_map.insert(key, parse_type(ktv.type_marker, ktv.val, self)?); } - Ok(new_map) + Ok(out_map) } } diff --git a/src/parser/utils.rs b/src/parser/utils.rs index 9d06cc9..7aa5171 100644 --- a/src/parser/utils.rs +++ b/src/parser/utils.rs @@ -21,15 +21,19 @@ pub(crate) fn parse_header(header: FalkorValue) -> Result, FalkorDBE .flat_map(|item| { let item_vec = item.into_vec()?; if item_vec.len() == 2 { - let [_, key]: [FalkorValue; 2] = item_vec - .try_into() - .map_err(|_| FalkorDBError::ParsingHeader)?; + let [_, key]: [FalkorValue; 2] = item_vec.try_into().map_err(|_| { + FalkorDBError::ParsingHeader( + "Header was not in (type: header) form".to_string(), + ) + })?; key } else { item_vec .into_iter() .next() - .ok_or(FalkorDBError::ParsingHeader)? + .ok_or(FalkorDBError::ParsingHeader( + "Expected at least one item in header vector".to_string(), + ))? } .into_string() }) diff --git a/src/response/constraint.rs b/src/response/constraint.rs index e5ff766..68bda07 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -49,7 +49,7 @@ pub struct Constraint { impl Constraint { fn from_value_vec(vlaue_vec: Vec) -> FalkorResult { - let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = vlaue_vec.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = vlaue_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()?, diff --git a/src/response/index.rs b/src/response/index.rs index a706c18..ea391dd 100644 --- a/src/response/index.rs +++ b/src/response/index.rs @@ -73,7 +73,7 @@ pub struct FalkorIndex { 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)?; + 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()?, diff --git a/src/response/slowlog_entry.rs b/src/response/slowlog_entry.rs index fe25089..f425e2f 100644 --- a/src/response/slowlog_entry.rs +++ b/src/response/slowlog_entry.rs @@ -22,10 +22,12 @@ 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)?; + let [timestamp, command, arguments, time_taken] = + value.into_vec()?.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 4 elements of slowlog entry".to_string(), + ) + })?; Ok(Self { timestamp: timestamp diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index 0051479..e5eaa38 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -33,10 +33,12 @@ impl FalkorParsable for Node { value: FalkorValue, graph_schema: &mut GraphSchema, ) -> FalkorResult { - let [entity_id, labels, properties]: [FalkorValue; 3] = value - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + 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 labels = labels.into_vec()?; let mut ids_hashset = HashSet::with_capacity(labels.len()); @@ -49,7 +51,7 @@ impl FalkorParsable for Node { } Ok(Node { entity_id: entity_id.to_i64().ok_or(FalkorDBError::ParsingI64)?, - labels: graph_schema.parse_labels_relationships(labels, SchemaType::Labels)?, + labels: graph_schema.parse_id_vec(labels, SchemaType::Labels)?, properties: graph_schema.parse_properties_map(properties)?, }) } @@ -75,10 +77,12 @@ impl FalkorParsable for Edge { value: FalkorValue, 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(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + let [entity_id, relations, src_node_id, dst_node_id, properties]: [FalkorValue; 5] = + value.into_vec()?.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 5 elements in edge object".to_string(), + ) + })?; let relation = relations.to_i64().ok_or(FalkorDBError::ParsingI64)?; let relationship = graph_schema diff --git a/src/value/map.rs b/src/value/map.rs index 958ec54..5e6c751 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -15,21 +15,25 @@ impl FalkorParsable for HashMap { ) -> FalkorResult { let val_vec = value.into_vec()?; if val_vec.len() % 2 != 0 { - Err(FalkorDBError::ParsingFMap)?; + Err(FalkorDBError::ParsingFMap( + "Map should have an even amount of elements".to_string(), + ))?; } Ok(val_vec .chunks_exact(2) .flat_map(|pair| { - let [key, val]: [FalkorValue; 2] = pair - .to_vec() - .try_into() - .map_err(|_| FalkorDBError::ParsingFMap)?; + 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)?; + .map_err(|_| FalkorDBError::ParsingFMap("The value in a map should be comprised of a type marker and value".to_string()))?; FalkorResult::<_>::Ok(( key.into_string()?, diff --git a/src/value/mod.rs b/src/value/mod.rs index d9c2e74..a414470 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -159,7 +159,9 @@ impl TryFrom for HashMap { fn try_from(value: FalkorValue) -> FalkorResult { match value { FalkorValue::Map(map) => Ok(map), - _ => Err(FalkorDBError::ParsingFMap), + _ => Err(FalkorDBError::ParsingFMap( + "Attempting to get a non-map element as a map".to_string(), + )), } } } diff --git a/src/value/path.rs b/src/value/path.rs index d41a5af..8b00755 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -19,10 +19,12 @@ impl FalkorParsable for Path { value: FalkorValue, graph_schema: &mut GraphSchema, ) -> FalkorResult { - let [nodes, relationships]: [FalkorValue; 2] = value - .into_vec()? - .try_into() - .map_err(|_| FalkorDBError::ParsingArrayToStructElementCount)?; + let [nodes, relationships]: [FalkorValue; 2] = + value.into_vec()?.try_into().map_err(|_| { + FalkorDBError::ParsingArrayToStructElementCount( + "Expected exactly 2 elements for path".to_string(), + ) + })?; Ok(Self { nodes: nodes diff --git a/src/value/point.rs b/src/value/point.rs index 5e18ecb..2fdc341 100644 --- a/src/value/point.rs +++ b/src/value/point.rs @@ -24,10 +24,11 @@ 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)?; + 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(), + ) + })?; Ok(Point { latitude: lat.to_f64().ok_or(FalkorDBError::ParsingF64)?, diff --git a/src/value/utils.rs b/src/value/utils.rs index dd81753..dc0096d 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -6,10 +6,11 @@ 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)?; + 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)) From c17c3da6989fbf6335d061bec7913a4775fc95a3 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 16:46:09 +0300 Subject: [PATCH 54/62] typo --- src/graph/blocking.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/graph/blocking.rs b/src/graph/blocking.rs index ba2a648..48b8548 100644 --- a/src/graph/blocking.rs +++ b/src/graph/blocking.rs @@ -163,7 +163,7 @@ impl SyncGraph { /// /// # Returns /// A [`ProcedureQueryBuilder`] object - pub fn call_proecdure_ro<'a, P>( + pub fn call_procedure_ro<'a, P>( &'a mut self, procedure_name: &'a str, ) -> ProcedureQueryBuilder

{ From 18500440620a1151db9c40f17806d13433a7f743 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 16:51:14 +0300 Subject: [PATCH 55/62] More various fixes --- src/client/builder.rs | 6 +++--- src/error/mod.rs | 2 +- src/graph/query_builder.rs | 2 +- src/response/constraint.rs | 4 ++-- src/response/execution_plan.rs | 2 +- src/value/utils.rs | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/client/builder.rs b/src/client/builder.rs index 1e72447..279f274 100644 --- a/src/client/builder.rs +++ b/src/client/builder.rs @@ -111,11 +111,11 @@ mod tests { #[test] fn test_sync_builder() { - let conneciton_info = "falkor://127.0.0.1:6379".try_into(); - assert!(conneciton_info.is_ok()); + let connection_info = "falkor://127.0.0.1:6379".try_into(); + assert!(connection_info.is_ok()); assert!(FalkorClientBuilder::new() - .with_connection_info(conneciton_info.unwrap()) + .with_connection_info(connection_info.unwrap()) .build() .is_ok()); } diff --git a/src/error/mod.rs b/src/error/mod.rs index 63f8095..57d9906 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -9,7 +9,7 @@ use crate::SchemaType; /// this allows easy error integration using [`thiserror`] #[derive(thiserror::Error, Debug)] pub enum FalkorDBError { - /// A required Id for parsing was not found in the schema. + /// A required ID for parsing was not found in the schema. #[error("A required Id for parsing was not found in the schema")] MissingSchemaId(SchemaType), /// Could not connect to Redis Sentinel, or a critical Sentinel operation has failed. diff --git a/src/graph/query_builder.rs b/src/graph/query_builder.rs index a441c08..3251fd5 100644 --- a/src/graph/query_builder.rs +++ b/src/graph/query_builder.rs @@ -53,7 +53,7 @@ impl<'a, Output> QueryBuilder<'a, Output> { } } - /// Pass the following params to the query (as "CYPHER {param_key}={param_val}" + /// Pass the following params to the query as "CYPHER {param_key}={param_val}" /// /// # Arguments /// * `params`: A [`HashMap`] of params in key-val format diff --git a/src/response/constraint.rs b/src/response/constraint.rs index 68bda07..b662da4 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -48,8 +48,8 @@ pub struct Constraint { } impl Constraint { - fn from_value_vec(vlaue_vec: Vec) -> FalkorResult { - let [constraint_type_raw, label_raw, properties_raw, entity_type_raw, status_raw]: [FalkorValue; 5] = vlaue_vec.try_into().map_err(|_| FalkorDBError::ParsingArrayToStructElementCount("Expected exactly 5 elements in constraint object".to_string()))?; + fn from_value_vec(value_vec: Vec) -> FalkorResult { + 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()?, diff --git a/src/response/execution_plan.rs b/src/response/execution_plan.rs index a833704..4d3f646 100644 --- a/src/response/execution_plan.rs +++ b/src/response/execution_plan.rs @@ -111,7 +111,7 @@ impl ExecutionPlan { &self.operation_tree } - /// Returns a string representation of the entire executino plan + /// Returns a string representation of the entire execution plan pub fn string_representation(&self) -> &str { self.string_representation.as_str() } diff --git a/src/value/utils.rs b/src/value/utils.rs index dc0096d..2a8e68b 100644 --- a/src/value/utils.rs +++ b/src/value/utils.rs @@ -156,7 +156,7 @@ mod tests { Some(&FalkorValue::String("the something".to_string())) ); assert_eq!( - node.properties.get(&"secs_since_login".to_string()), + node.properties.get("secs_since_login"), Some(&FalkorValue::F64(105.5)) ); } From 1d63b5531aa9f68ea915bc5cc48c72f96045e907 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 17:03:12 +0300 Subject: [PATCH 56/62] Couple more tests --- src/connection_info/mod.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/connection_info/mod.rs b/src/connection_info/mod.rs index 6459913..417cd6b 100644 --- a/src/connection_info/mod.rs +++ b/src/connection_info/mod.rs @@ -121,4 +121,34 @@ mod tests { assert!(res.is_ok()); assert_eq!(res.unwrap().address(), "127.0.0.1:1234".to_string()); } + + #[test] + fn test_invalid_scheme() { + let result = FalkorConnectionInfo::try_from("http://127.0.0.1:6379"); + assert!(result.is_err()); + } + + #[test] + fn test_missing_host() { + let result = FalkorConnectionInfo::try_from("redis://:6379"); + assert!(result.is_err()); + } + + #[test] + fn test_invalid_port() { + let result = FalkorConnectionInfo::try_from("redis://127.0.0.1:abc"); + assert!(result.is_err()); + } + + #[test] + fn test_unsupported_feature() { + let result = FalkorConnectionInfo::try_from("custom://127.0.0.1:6379"); + assert!(result.is_err()); + } + + #[test] + fn test_missing_scheme() { + let result = FalkorConnectionInfo::try_from("127.0.0.1:6379"); + assert!(result.is_ok()); + } } From a60b6c2f33144977c5d8ca0eca3e0c3c3a2646ea Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 17:40:25 +0300 Subject: [PATCH 57/62] More tests --- src/client/blocking.rs | 11 +++ src/client/mod.rs | 3 + src/graph/query_builder.rs | 139 +++++++++++++++++++++++++++------ src/value/map.rs | 152 ++++++++++++++++++++++++++++++++----- src/value/point.rs | 73 ++++++++++++++++++ 5 files changed, 335 insertions(+), 43 deletions(-) diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 7bc4b10..27d09d8 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -283,6 +283,17 @@ impl FalkorSyncClient { } } +#[cfg(test)] +pub(crate) fn create_empty_client() -> Arc { + let (tx, rx) = mpsc::sync_channel(1); + Arc::new(FalkorSyncClientInner { + _inner: Mutex::new(FalkorClientProvider::None), + connection_pool_size: 0, + connection_pool_tx: RwLock::new(tx), + connection_pool_rx: Mutex::new(rx), + }) +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/client/mod.rs b/src/client/mod.rs index d1ea22e..02bbff2 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -9,6 +9,7 @@ pub(crate) mod blocking; pub(crate) mod builder; pub(crate) enum FalkorClientProvider { + None, #[cfg(feature = "redis")] Redis { client: redis::Client, @@ -34,6 +35,7 @@ impl FalkorClientProvider { .get_connection() .map_err(|err| FalkorDBError::RedisError(err.to_string()))?, ), + FalkorClientProvider::None => Err(FalkorDBError::UnavailableProvider)?, }) } @@ -44,6 +46,7 @@ impl FalkorClientProvider { ) { match self { FalkorClientProvider::Redis { sentinel, .. } => *sentinel = Some(sentinel_client), + FalkorClientProvider::None => {} } } } diff --git a/src/graph/query_builder.rs b/src/graph/query_builder.rs index 3251fd5..c0d1da0 100644 --- a/src/graph/query_builder.rs +++ b/src/graph/query_builder.rs @@ -9,6 +9,7 @@ use crate::{ Constraint, ExecutionPlan, FalkorDBError, FalkorIndex, FalkorParsable, FalkorResponse, FalkorResult, FalkorValue, ResultSet, SyncGraph, }; +use std::ops::Not; use std::{collections::HashMap, fmt::Display, marker::PhantomData}; pub(crate) fn construct_query( @@ -22,7 +23,12 @@ pub(crate) fn construct_query( .collect::>() .join(" ") }) - .map(|params_str| format!("CYPHER {params_str} ")) + .and_then(|params_str| { + params_str + .is_empty() + .not() + .then_some(format!("CYPHER {params_str} ")) + }) .unwrap_or_default(); format!("{params_str}{query_str}") } @@ -356,30 +362,117 @@ mod tests { use super::*; #[test] - fn test_generate_procedure_call() { - let (query, params) = generate_procedure_call( - "DB.CONSTRAINTS", - Some(&["Hello", "World"]), - Some(&["Foo", "Bar"]), - ); - - assert_eq!(query, "CALL DB.CONSTRAINTS($Hello,$World) YIELD Foo,Bar"); - assert!(params.is_some()); - - let params = params.unwrap(); - assert_eq!(params["param0"], "Hello"); - assert_eq!(params["param1"], "World"); + fn test_generate_procedure_call_no_args_no_yields() { + let procedure = "my_procedure"; + let args: Option<&[String]> = None; + let yields: Option<&[String]> = None; + + let expected_query = "CALL my_procedure()".to_string(); + let expected_params: Option> = None; + + let result = generate_procedure_call(procedure, args, yields); + + assert_eq!(result, (expected_query, expected_params)); + } + + #[test] + fn test_generate_procedure_call_with_args_no_yields() { + let procedure = "my_procedure"; + let args = &["arg1".to_string(), "arg2".to_string()]; + let yields: Option<&[String]> = None; + + let expected_query = "CALL my_procedure($arg1,$arg2)".to_string(); + let mut expected_params = HashMap::new(); + expected_params.insert("param0".to_string(), "arg1".to_string()); + expected_params.insert("param1".to_string(), "arg2".to_string()); + + let result = generate_procedure_call(procedure, Some(args), yields); + + assert_eq!(result, (expected_query, Some(expected_params))); + } + + #[test] + fn test_generate_procedure_call_no_args_with_yields() { + let procedure = "my_procedure"; + let args: Option<&[String]> = None; + let yields = &["yield1".to_string(), "yield2".to_string()]; + + let expected_query = "CALL my_procedure() YIELD yield1,yield2".to_string(); + let expected_params: Option> = None; + + let result = generate_procedure_call(procedure, args, Some(yields)); + + assert_eq!(result, (expected_query, expected_params)); + } + + #[test] + fn test_generate_procedure_call_with_args_and_yields() { + let procedure = "my_procedure"; + let args = &["arg1".to_string(), "arg2".to_string()]; + let yields = &["yield1".to_string(), "yield2".to_string()]; + + let expected_query = "CALL my_procedure($arg1,$arg2) YIELD yield1,yield2".to_string(); + let mut expected_params = HashMap::new(); + expected_params.insert("param0".to_string(), "arg1".to_string()); + expected_params.insert("param1".to_string(), "arg2".to_string()); + + let result = generate_procedure_call(procedure, Some(args), Some(yields)); + + assert_eq!(result, (expected_query, Some(expected_params))); + } + + #[test] + fn test_construct_query_with_params() { + let query_str = "MATCH (n) RETURN n"; + let mut params = HashMap::new(); + params.insert("name", "Alice"); + params.insert("age", "30"); + + let result = construct_query(query_str, Some(¶ms)); + assert!(result.starts_with("CYPHER ")); + assert!(result.ends_with(" RETURN n")); + assert!(result.contains(" name=Alice ")); + assert!(result.contains(" age=30 ")); + } + + #[test] + fn test_construct_query_without_params() { + let query_str = "MATCH (n) RETURN n"; + let result = construct_query::<&str, &str, &str>(query_str, None); + assert_eq!(result, "MATCH (n) RETURN n"); + } + + #[test] + fn test_construct_query_empty_params() { + let query_str = "MATCH (n) RETURN n"; + let params: HashMap<&str, &str> = HashMap::new(); + let result = construct_query(query_str, Some(¶ms)); + assert_eq!(result, "MATCH (n) RETURN n"); + } + + #[test] + fn test_construct_query_single_param() { + let query_str = "MATCH (n) RETURN n"; + let mut params = HashMap::new(); + params.insert("name", "Alice"); + + let result = construct_query(query_str, Some(¶ms)); + assert_eq!(result, "CYPHER name=Alice MATCH (n) RETURN n"); } #[test] - fn test_construct_query() { - let query = construct_query("MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100", - Some(&HashMap::from([("Foo", "Bar"), ("Bizz", "Bazz")]))); - assert!(query.starts_with("CYPHER ")); - assert!(query.ends_with(" MATCH (a:actor) WITH a MATCH (b:actor) WHERE a.age = b.age AND a <> b RETURN a, collect(b) LIMIT 100")); - - // Order not guaranteed - assert!(query.contains(" Foo=Bar ")); - assert!(query.contains(" Bizz=Bazz ")); + fn test_construct_query_multiple_params() { + let query_str = "MATCH (n) RETURN n"; + let mut params = HashMap::new(); + params.insert("name", "Alice"); + params.insert("age", "30"); + params.insert("city", "Wonderland"); + + let result = construct_query(query_str, Some(¶ms)); + assert!(result.starts_with("CYPHER ")); + assert!(result.contains(" name=Alice ")); + assert!(result.contains(" age=30 ")); + assert!(result.contains(" city=Wonderland ")); + assert!(result.ends_with("MATCH (n) RETURN n")); } } diff --git a/src/value/map.rs b/src/value/map.rs index 5e6c751..1356b53 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -20,30 +20,142 @@ impl FalkorParsable for HashMap { ))?; } - Ok(val_vec - .chunks_exact(2) - .flat_map(|pair| { - let [key, val]: [FalkorValue; 2] = pair.to_vec().try_into().map_err(|_| { + 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 vec returned from using chunks_exact(2) should be comprised of 2 elements" + "The value in a map should be comprised of a type marker and value" .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()))?; - - FalkorResult::<_>::Ok(( - key.into_string()?, - parse_type( - type_marker.to_i64().ok_or(FalkorDBError::ParsingKTVTypes)?, - val, - graph_schema, - )?, - )) - }) - .collect()) + 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_client, GraphSchema}; + + #[test] + fn test_not_a_vec() { + let mut graph_schema = GraphSchema::new("test_graph", create_empty_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_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_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_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_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_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_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/point.rs b/src/value/point.rs index 2fdc341..2f30162 100644 --- a/src/value/point.rs +++ b/src/value/point.rs @@ -36,3 +36,76 @@ impl Point { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_parse_valid_point() { + let value = FalkorValue::Array(vec![FalkorValue::F64(45.0), FalkorValue::F64(90.0)]); + let result = Point::parse(value); + assert!(result.is_ok()); + let point = result.unwrap(); + assert_eq!(point.latitude, 45.0); + assert_eq!(point.longitude, 90.0); + } + + #[test] + fn test_parse_invalid_point_missing_elements() { + let value = FalkorValue::Array(vec![FalkorValue::F64(45.0)]); + let result = Point::parse(value); + assert!(result.is_err()); + match result { + Err(FalkorDBError::ParsingArrayToStructElementCount(msg)) => { + assert_eq!( + msg, + "Expected exactly 2 element in point - latitude and longitude".to_string() + ); + } + _ => panic!("Expected ParsingArrayToStructElementCount error"), + } + } + + #[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 result = Point::parse(value); + assert!(result.is_err()); + match result { + Err(FalkorDBError::ParsingArrayToStructElementCount(msg)) => { + assert_eq!( + msg, + "Expected exactly 2 element in point - latitude and longitude".to_string() + ); + } + _ => panic!("Expected ParsingArrayToStructElementCount error"), + } + } + + #[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 result = Point::parse(value); + assert!(result.is_err()); + // Check for the specific error type if needed + } +} From 8e8a903ec8e7258c773efe3ae65fc8acb6672e06 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 17:43:05 +0300 Subject: [PATCH 58/62] Ignore warnings for None variant --- src/client/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/client/mod.rs b/src/client/mod.rs index 02bbff2..e5201b3 100644 --- a/src/client/mod.rs +++ b/src/client/mod.rs @@ -8,7 +8,9 @@ use crate::{connection::blocking::FalkorSyncConnection, FalkorDBError, FalkorRes pub(crate) mod blocking; pub(crate) mod builder; +#[allow(clippy::large_enum_variant)] pub(crate) enum FalkorClientProvider { + #[allow(unused)] None, #[cfg(feature = "redis")] Redis { From d9959a1cc89aff89f16c42273fd770e15ab51655 Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 17:57:13 +0300 Subject: [PATCH 59/62] More tests --- src/error/mod.rs | 2 +- src/parser/utils.rs | 125 ++++++++++++++++++++++++++++++++++---------- 2 files changed, 98 insertions(+), 29 deletions(-) diff --git a/src/error/mod.rs b/src/error/mod.rs index 57d9906..c0f82ab 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -7,7 +7,7 @@ use crate::SchemaType; /// A verbose error enum used throughout the client, messages are static string slices. /// this allows easy error integration using [`thiserror`] -#[derive(thiserror::Error, Debug)] +#[derive(thiserror::Error, Debug, PartialEq)] pub enum FalkorDBError { /// A required ID for parsing was not found in the schema. #[error("A required Id for parsing was not found in the schema")] diff --git a/src/parser/utils.rs b/src/parser/utils.rs index 7aa5171..0ba5610 100644 --- a/src/parser/utils.rs +++ b/src/parser/utils.rs @@ -3,9 +3,11 @@ * Licensed under the Server Side Public License v1 (SSPLv1). */ -use crate::{value::utils::parse_type, FalkorDBError, FalkorValue, GraphSchema, ResultSet}; +use crate::{ + value::utils::parse_type, FalkorDBError, FalkorResult, FalkorValue, GraphSchema, ResultSet, +}; -pub(crate) fn string_vec_from_val(value: FalkorValue) -> Result, FalkorDBError> { +pub(crate) fn string_vec_from_val(value: FalkorValue) -> FalkorResult> { value.into_vec().map(|value_as_vec| { value_as_vec .into_iter() @@ -14,37 +16,40 @@ pub(crate) fn string_vec_from_val(value: FalkorValue) -> Result, Fal }) } -pub(crate) fn parse_header(header: FalkorValue) -> Result, FalkorDBError> { - header.into_vec().map(|header_as_vec| { - header_as_vec - .into_iter() - .flat_map(|item| { - let item_vec = item.into_vec()?; - if item_vec.len() == 2 { - let [_, key]: [FalkorValue; 2] = item_vec.try_into().map_err(|_| { - FalkorDBError::ParsingHeader( - "Header was not in (type: header) form".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() - }) - .collect() - }) +pub(crate) fn parse_header(header: FalkorValue) -> FalkorResult> { + let in_vec = header.into_vec()?; + + let mut out_vec = Vec::with_capacity(in_vec.len()); + for item in in_vec { + let item_vec = item.into_vec()?; + + out_vec.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(out_vec) } pub(crate) fn parse_result_set( data: FalkorValue, graph_schema: &mut GraphSchema, -) -> Result { +) -> FalkorResult { let data_vec = data.into_vec()?; let mut out_vec = Vec::with_capacity(data_vec.len()); @@ -54,3 +59,67 @@ pub(crate) fn parse_result_set( Ok(out_vec) } + +#[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"); + } +} From 07d9b80d55eaa7b47935cea5908194621312f3dc Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 18:16:37 +0300 Subject: [PATCH 60/62] More tests --- src/value/graph_entities.rs | 2 +- src/value/mod.rs | 127 ++++++++++++++++++++++++++++++++++++ src/value/path.rs | 2 +- src/value/point.rs | 2 +- 4 files changed, 130 insertions(+), 3 deletions(-) diff --git a/src/value/graph_entities.rs b/src/value/graph_entities.rs index e5eaa38..ddd742f 100644 --- a/src/value/graph_entities.rs +++ b/src/value/graph_entities.rs @@ -18,7 +18,7 @@ pub enum EntityType { } /// A node in the graph, containing a unique id, various labels describing it, and its own property. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Node { /// The internal entity ID pub entity_id: i64, diff --git a/src/value/mod.rs b/src/value/mod.rs index a414470..5e9c5e0 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -318,3 +318,130 @@ impl FalkorParsable for FalkorValue { Ok(value) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use std::f64::consts::PI; + + #[test] + fn test_as_vec() { + let vec_val = FalkorValue::Array(vec![FalkorValue::I64(1), FalkorValue::I64(2)]); + assert_eq!(vec_val.as_vec().unwrap().len(), 2); + + let non_vec_val = FalkorValue::I64(42); + assert!(non_vec_val.as_vec().is_none()); + } + + #[test] + fn test_as_string() { + let string_val = FalkorValue::String(String::from("hello")); + assert_eq!(string_val.as_string().unwrap(), "hello"); + + let non_string_val = FalkorValue::I64(42); + assert!(non_string_val.as_string().is_none()); + } + + #[test] + fn test_as_edge() { + let edge = Edge::default(); // Assuming Edge::new() is a valid constructor + let edge_val = FalkorValue::Edge(edge); + assert!(edge_val.as_edge().is_some()); + + let non_edge_val = FalkorValue::I64(42); + assert!(non_edge_val.as_edge().is_none()); + } + + #[test] + fn test_as_node() { + let node = Node::default(); // Assuming Node::new() is a valid constructor + let node_val = FalkorValue::Node(node); + assert!(node_val.as_node().is_some()); + + let non_node_val = FalkorValue::I64(42); + assert!(non_node_val.as_node().is_none()); + } + + #[test] + fn test_as_path() { + let path = Path::default(); // Assuming Path::new() is a valid constructor + let path_val = FalkorValue::Path(path); + assert!(path_val.as_path().is_some()); + + let non_path_val = FalkorValue::I64(42); + assert!(non_path_val.as_path().is_none()); + } + + #[test] + fn test_as_map() { + let mut map = HashMap::new(); + map.insert(String::from("key"), FalkorValue::I64(42)); + let map_val = FalkorValue::Map(map); + assert!(map_val.as_map().is_some()); + + let non_map_val = FalkorValue::I64(42); + assert!(non_map_val.as_map().is_none()); + } + + #[test] + fn test_as_point() { + let point = Point::default(); // Assuming Point::new() is a valid constructor + let point_val = FalkorValue::Point(point); + assert!(point_val.as_point().is_some()); + + let non_point_val = FalkorValue::I64(42); + assert!(non_point_val.as_point().is_none()); + } + + #[test] + fn test_to_i64() { + let int_val = FalkorValue::I64(42); + assert_eq!(int_val.to_i64().unwrap(), 42); + + let non_int_val = FalkorValue::String(String::from("hello")); + assert!(non_int_val.to_i64().is_none()); + } + + #[test] + fn test_to_bool() { + let bool_val = FalkorValue::Bool(true); + assert!(bool_val.to_bool().unwrap()); + + let bool_str_val = FalkorValue::String(String::from("false")); + assert!(!bool_str_val.to_bool().unwrap()); + + let invalid_bool_str_val = FalkorValue::String(String::from("notabool")); + assert!(invalid_bool_str_val.to_bool().is_none()); + + let non_bool_val = FalkorValue::I64(42); + assert!(non_bool_val.to_bool().is_none()); + } + + #[test] + fn test_to_f64() { + let float_val = FalkorValue::F64(PI); + assert_eq!(float_val.to_f64().unwrap(), PI); + + let non_float_val = FalkorValue::String(String::from("hello")); + assert!(non_float_val.to_f64().is_none()); + } + + #[test] + fn test_into_vec() { + let vec_val = FalkorValue::Array(vec![FalkorValue::I64(1), FalkorValue::I64(2)]); + assert_eq!(vec_val.into_vec().unwrap().len(), 2); + + let non_vec_val = FalkorValue::I64(42); + assert!(non_vec_val.into_vec().is_err()); + } + + #[test] + fn test_into_string() { + let string_val = FalkorValue::String(String::from("hello")); + assert_eq!(string_val.into_string().unwrap(), "hello"); + + let non_string_val = FalkorValue::I64(42); + assert!(non_string_val.into_string().is_err()); + } +} diff --git a/src/value/path.rs b/src/value/path.rs index 8b00755..6f499fe 100644 --- a/src/value/path.rs +++ b/src/value/path.rs @@ -6,7 +6,7 @@ use crate::{Edge, FalkorDBError, FalkorParsable, FalkorResult, FalkorValue, GraphSchema, Node}; /// Represents a path between two nodes, contains all the nodes, and the relationships between them along the path -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Path { /// The nodes along the path, ordered pub nodes: Vec, diff --git a/src/value/point.rs b/src/value/point.rs index 2f30162..afa1a54 100644 --- a/src/value/point.rs +++ b/src/value/point.rs @@ -6,7 +6,7 @@ use crate::{FalkorDBError, FalkorResult, FalkorValue}; /// A point in the world. -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Default, PartialEq)] pub struct Point { /// The latitude coordinate pub latitude: f64, From 3ccc462bfc7c7668f3d2fdf77a3099fcc9d4c29b Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 18:52:12 +0300 Subject: [PATCH 61/62] More tests, better parse function --- src/client/blocking.rs | 3 +- src/connection/blocking.rs | 3 + src/graph_schema/mod.rs | 129 +++++++++++++++++++++++++------------ src/response/constraint.rs | 22 +++---- src/value/map.rs | 16 ++--- 5 files changed, 109 insertions(+), 64 deletions(-) diff --git a/src/client/blocking.rs b/src/client/blocking.rs index 27d09d8..70c0ad2 100644 --- a/src/client/blocking.rs +++ b/src/client/blocking.rs @@ -284,8 +284,9 @@ impl FalkorSyncClient { } #[cfg(test)] -pub(crate) fn create_empty_client() -> Arc { +pub(crate) fn create_empty_inner_client() -> Arc { let (tx, rx) = mpsc::sync_channel(1); + tx.send(FalkorSyncConnection::None).ok(); Arc::new(FalkorSyncClientInner { _inner: Mutex::new(FalkorClientProvider::None), connection_pool_size: 0, diff --git a/src/connection/blocking.rs b/src/connection/blocking.rs index 9633fb9..1f65536 100644 --- a/src/connection/blocking.rs +++ b/src/connection/blocking.rs @@ -10,6 +10,8 @@ use std::{ }; pub(crate) enum FalkorSyncConnection { + #[allow(unused)] + None, #[cfg(feature = "redis")] Redis(redis::Connection), } @@ -47,6 +49,7 @@ impl FalkorSyncConnection { ) .map_err(|err| FalkorDBError::RedisParsingError(err.to_string())) } + FalkorSyncConnection::None => Ok(FalkorValue::None), } } diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index 0500231..5c8b1e2 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -19,6 +19,7 @@ pub(crate) fn get_refresh_command(schema_type: SchemaType) -> &'static str { } // Intermediate type for map parsing +#[derive(Debug)] pub(crate) struct FKeyTypeVal { pub(crate) key: i64, pub(crate) type_marker: i64, @@ -119,6 +120,18 @@ impl GraphSchema { &self.properties } + #[inline] + fn get_id_map_by_schema_type( + &self, + schema_type: SchemaType, + ) -> &IdMap { + match schema_type { + SchemaType::Labels => &self.labels, + SchemaType::Properties => &self.properties, + SchemaType::Relationships => &self.relationships, + } + } + fn refresh( &mut self, schema_type: SchemaType, @@ -176,50 +189,24 @@ impl GraphSchema { raw_ids: Vec, schema_type: SchemaType, ) -> FalkorResult> { - let ids_count = raw_ids.len(); - - { - let ids_map = match schema_type { - SchemaType::Labels => &self.labels, - SchemaType::Properties => &self.properties, - SchemaType::Relationships => &self.relationships, - }; - - let mut refs_vec = Vec::with_capacity(ids_count); - let mut success = true; - for raw_id in &raw_ids { - let id = raw_id.to_i64().ok_or(FalkorDBError::ParsingI64)?; - match ids_map.get(&id) { - None => { - success = false; - break; - } - Some(label) => refs_vec.push(label), - } - } - - if success { - // Clone the strings themselves and return the parsed labels - return Ok(refs_vec.into_iter().cloned().collect()); - } - } - - // Refresh and try again - self.refresh(schema_type)?; - - let ids_map = match schema_type { - SchemaType::Labels => &self.labels, - SchemaType::Properties => &self.properties, - SchemaType::Relationships => &self.relationships, - }; - - let mut out_vec = Vec::with_capacity(ids_count); + let mut out_vec = Vec::with_capacity(raw_ids.len()); for raw_id in raw_ids { + let id = raw_id.to_i64().ok_or(FalkorDBError::ParsingI64)?; out_vec.push( - ids_map - .get(&raw_id.to_i64().ok_or(FalkorDBError::ParsingI64)?) + match self + .get_id_map_by_schema_type(schema_type) + .get(&id) .cloned() - .ok_or(FalkorDBError::MissingSchemaId(SchemaType::Labels))?, + { + None => { + self.refresh(schema_type)?; + self.get_id_map_by_schema_type(schema_type) + .get(&id) + .cloned() + .ok_or(FalkorDBError::MissingSchemaId(schema_type))? + } + Some(exists) => exists, + }, ); } @@ -235,7 +222,6 @@ impl GraphSchema { 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 @@ -257,6 +243,8 @@ impl GraphSchema { #[cfg(test)] pub(crate) mod tests { + use super::*; + use crate::client::blocking::create_empty_inner_client; use crate::{test_utils::create_test_client, SyncGraph}; use std::collections::HashMap; @@ -279,4 +267,61 @@ pub(crate) mod tests { graph } + + #[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 result = parser.parse_properties_map(input_value); + assert!(result.is_err()); + } + + #[test] + fn test_parse_properties_map() { + let mut parser = GraphSchema::new("graph_name".to_string(), create_empty_inner_client()); + parser.properties = HashMap::from([ + (1, "property1".to_string()), + (2, "property2".to_string()), + (3, "property3".to_string()), + ]); + + // 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()), + ]), + FalkorValue::Array(vec![ + FalkorValue::I64(2), + FalkorValue::I64(3), + FalkorValue::I64(42), + ]), + FalkorValue::Array(vec![ + FalkorValue::I64(3), + FalkorValue::I64(4), + FalkorValue::Bool(true), + ]), + ]); + + // Expected output + let mut expected_map = HashMap::new(); + expected_map.insert( + "property1".to_string(), + FalkorValue::String("test".to_string()), + ); + expected_map.insert("property2".to_string(), FalkorValue::I64(42)); + expected_map.insert("property3".to_string(), FalkorValue::Bool(true)); + + // Parse the properties map + let result = parser.parse_properties_map(input_value); + + // Check if the result matches the expected output + assert_eq!(result.unwrap(), expected_map); + } } diff --git a/src/response/constraint.rs b/src/response/constraint.rs index b662da4..8d1aadf 100644 --- a/src/response/constraint.rs +++ b/src/response/constraint.rs @@ -47,9 +47,15 @@ pub struct Constraint { pub status: ConstraintStatus, } -impl Constraint { - fn from_value_vec(value_vec: Vec) -> FalkorResult { - 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()))?; +impl FalkorParsable for Constraint { + fn from_falkor_value( + value: FalkorValue, + 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]: [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()?, @@ -64,13 +70,3 @@ impl Constraint { }) } } - -impl FalkorParsable for Constraint { - fn from_falkor_value( - value: FalkorValue, - graph_schema: &mut GraphSchema, - ) -> FalkorResult { - parse_type(6, value, graph_schema) - .and_then(|parsed| parsed.into_vec().and_then(Constraint::from_value_vec)) - } -} diff --git a/src/value/map.rs b/src/value/map.rs index 1356b53..e6fd140 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -54,11 +54,11 @@ impl FalkorParsable for HashMap { #[cfg(test)] mod tests { use super::*; - use crate::{client::blocking::create_empty_client, GraphSchema}; + 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_client()); + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); let res: FalkorResult> = FalkorParsable::from_falkor_value( FalkorValue::String("Hello".to_string()), @@ -70,7 +70,7 @@ mod tests { #[test] fn test_vec_odd_element_count() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_client()); + 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]), @@ -82,7 +82,7 @@ mod tests { #[test] fn test_val_element_is_not_array() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_client()); + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); let res: FalkorResult> = FalkorParsable::from_falkor_value( FalkorValue::Array(vec![ @@ -97,7 +97,7 @@ mod tests { #[test] fn test_val_element_has_only_1_element() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_client()); + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); let res: FalkorResult> = FalkorParsable::from_falkor_value( FalkorValue::Array(vec![ @@ -112,7 +112,7 @@ mod tests { #[test] fn test_val_element_has_ge_2_elements() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_client()); + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); let res: FalkorResult> = FalkorParsable::from_falkor_value( FalkorValue::Array(vec![ @@ -127,7 +127,7 @@ mod tests { #[test] fn test_val_element_mismatch_type_marker() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_client()); + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); let res: FalkorResult> = FalkorParsable::from_falkor_value( FalkorValue::Array(vec![ @@ -142,7 +142,7 @@ mod tests { #[test] fn test_ok_values() { - let mut graph_schema = GraphSchema::new("test_graph", create_empty_client()); + let mut graph_schema = GraphSchema::new("test_graph", create_empty_inner_client()); let res: HashMap = FalkorParsable::from_falkor_value( FalkorValue::Array(vec![ From cbc796a4c45811aaa69de81a72e6f62aa6cb617f Mon Sep 17 00:00:00 2001 From: Emily Matheys Date: Wed, 5 Jun 2024 19:03:18 +0300 Subject: [PATCH 62/62] more tests --- src/graph_schema/mod.rs | 72 +++++++++++++++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 13 deletions(-) diff --git a/src/graph_schema/mod.rs b/src/graph_schema/mod.rs index 5c8b1e2..82950cf 100644 --- a/src/graph_schema/mod.rs +++ b/src/graph_schema/mod.rs @@ -244,8 +244,9 @@ impl GraphSchema { #[cfg(test)] pub(crate) mod tests { use super::*; - use crate::client::blocking::create_empty_inner_client; - use crate::{test_utils::create_test_client, SyncGraph}; + use crate::{ + client::blocking::create_empty_inner_client, test_utils::create_test_client, SyncGraph, + }; use std::collections::HashMap; pub(crate) fn open_readonly_graph_with_modified_schema() -> SyncGraph { @@ -309,19 +310,64 @@ pub(crate) mod tests { ]), ]); - // Expected output - let mut expected_map = HashMap::new(); - expected_map.insert( - "property1".to_string(), - FalkorValue::String("test".to_string()), - ); - expected_map.insert("property2".to_string(), FalkorValue::I64(42)); - expected_map.insert("property3".to_string(), FalkorValue::Bool(true)); - - // Parse the properties map let result = parser.parse_properties_map(input_value); - // Check if the result matches the expected output + let expected_map = HashMap::from([ + ( + "property1".to_string(), + FalkorValue::String("test".to_string()), + ), + ("property2".to_string(), FalkorValue::I64(42)), + ("property3".to_string(), FalkorValue::Bool(true)), + ]); assert_eq!(result.unwrap(), expected_map); } + + #[test] + fn test_parse_id_vec() { + let mut parser = GraphSchema::new("graph_name".to_string(), create_empty_inner_client()); + + parser.labels = HashMap::from([ + (1, "property1".to_string()), + (2, "property2".to_string()), + (3, "property3".to_string()), + ]); + + let labels_ok_res = + parser.parse_id_vec(vec![3.into(), 1.into(), 2.into()], SchemaType::Labels); + assert!(labels_ok_res.is_ok()); + assert_eq!( + labels_ok_res.unwrap(), + vec!["property3", "property1", "property2"] + ); + + // Should fail, these are not relationships + let labels_not_ok_res = parser.parse_id_vec( + vec![3.into(), 1.into(), 2.into()], + SchemaType::Relationships, + ); + assert!(labels_not_ok_res.is_err()); + + parser.clear(); + + parser.relationships = HashMap::from([ + (1, "property4".to_string()), + (2, "property5".to_string()), + (3, "property6".to_string()), + ]); + + let rels_ok_res = parser.parse_id_vec( + vec![3.into(), 1.into(), 2.into()], + SchemaType::Relationships, + ); + assert!(rels_ok_res.is_ok()); + assert_eq!( + rels_ok_res.unwrap(), + vec![ + "property6".to_string(), + "property4".to_string(), + "property5".to_string() + ] + ) + } }