diff --git a/Cargo.toml b/Cargo.toml index 7e79b7b..8e2b600 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "redis_ts" -version = "0.3.0" +version = "0.4.0" authors = ["protom "] keywords = ["redis", "database"] description = "API for Redis time series types." @@ -13,7 +13,7 @@ edition = "2018" exclude = ["docker"] [dependencies] -redis = { version = "0.19.0", optional = true } +redis = { version = "^0.20.0", optional = true } [features] default = ['redis'] @@ -32,3 +32,6 @@ required-features = ['async-std-comp'] [[test]] name = "test_async_tokio_commands" required-features = ['tokio-comp'] + +[package.metadata.docs.rs] +all-features = true diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 0000000..6c8150a --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,41 @@ +[tasks.publish] +description = "Publish to crates.io" +dependencies = ["gh_checks"] +command = "cargo" +args = ["publish", "--all-features"] + +[tasks.gh_checks] +dependencies = [ + "cargo_check", + "test", + "check_fmt", + "clippy" +] + +[tasks.cargo_check] +description = "Runs cargo check" +command = "cargo" +args = ["check"] + +[tasks.check_fmt] +description = "Runs fmt in check mode" +install_crate = "rustfmt" +command = "cargo" +args = ["fmt", "--all", "--", "--check"] + +[tasks.test] +description = "Runs tests with all features" +command = "cargo" +args = ["test", "--all-features"] + +[tasks.doc] +description = "Generates docs with all features" +command = "cargo" +args = ["doc", "--all-features"] + +[tasks.clippy] +description = "Runs clippy" +install_crate = "clippy" +command = "cargo" +args = ["clippy", "--", "-D warnings"] + diff --git a/README.md b/README.md index 1ab3000..58affb8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # redis_ts -[![crates.io](https://img.shields.io/badge/crates.io-v0.3.0-orange)](https://crates.io/crates/redis_ts) +[![crates.io](https://img.shields.io/badge/crates.io-v0.4.0-orange)](https://crates.io/crates/redis_ts) ![Continuous integration](https://github.com/tompro/redis_ts/workflows/Continuous%20integration/badge.svg) redis_ts provides a small trait with extension functions for the @@ -10,12 +10,13 @@ a [redis module](https://oss.redislabs.com/redistimeseries). Time series commands are available as synchronous and asynchronous versions. The crate is called `redis_ts` and you can depend on it via cargo. You will -also need redis in your dependencies. +also need redis in your dependencies. It has been tested agains redis 0.20.0 +but should work with versions higher than that. ```ini [dependencies] - redis = "0.19.0" - redis_ts = "0.3.0" + redis = "0.20.0" + redis_ts = "0.4.0" ``` Or via git: @@ -28,8 +29,8 @@ also need redis in your dependencies. With async feature inherited from the [redis](https://docs.rs/redis) crate (either: 'async-std-comp' or 'tokio-comp): ```ini [dependencies] - redis = "0.19.0" - redis_ts = { version = "0.3.0", features = ['tokio-comp'] } + redis = "0.20.0" + redis_ts = { version = "0.4.0", features = ['tokio-comp'] } ``` ## Synchronous usage diff --git a/src/async_commands.rs b/src/async_commands.rs index 81ed03f..7bed956 100644 --- a/src/async_commands.rs +++ b/src/async_commands.rs @@ -306,8 +306,8 @@ pub trait AsyncTsCommands: ConnectionLike + Send + Sized { Box::pin(async move { cmd("TS.MGET").arg(filter_options).query_async(self).await }) } - /// Executes a redis time series range query. - fn ts_range< + #[doc(hidden)] + fn range< 'a, K: ToRedisArgs + Send + Sync + 'a, FTS: ToRedisArgs + Send + Sync + 'a, @@ -317,13 +317,14 @@ pub trait AsyncTsCommands: ConnectionLike + Send + Sized { V: std::marker::Copy + FromRedisValue, >( &'a mut self, + command: &str, key: K, from_timestamp: FTS, to_timestamp: TTS, count: Option, aggregation_type: Option, ) -> RedisFuture> { - let mut c = cmd("TS.RANGE"); + let mut c = cmd(command); c.arg(key).arg(from_timestamp).arg(to_timestamp); if let Some(ct) = count { c.arg("COUNT").arg(ct); @@ -331,8 +332,62 @@ pub trait AsyncTsCommands: ConnectionLike + Send + Sized { Box::pin(async move { c.arg(aggregation_type).query_async(self).await }) } - /// Executes multiple redis time series range queries. - fn ts_mrange< + /// Executes a redis time series range query. + fn ts_range< + 'a, + K: ToRedisArgs + Send + Sync + 'a, + FTS: ToRedisArgs + Send + Sync + 'a, + TTS: ToRedisArgs + Send + Sync + 'a, + C: ToRedisArgs + Send + Sync + 'a, + TS: std::marker::Copy + FromRedisValue, + V: std::marker::Copy + FromRedisValue, + >( + &'a mut self, + key: K, + from_timestamp: FTS, + to_timestamp: TTS, + count: Option, + aggregation_type: Option, + ) -> RedisFuture> { + self.range( + "TS.RANGE", + key, + from_timestamp, + to_timestamp, + count, + aggregation_type, + ) + } + + /// Executes a redis time series revrange query. + fn ts_revrange< + 'a, + K: ToRedisArgs + Send + Sync + 'a, + FTS: ToRedisArgs + Send + Sync + 'a, + TTS: ToRedisArgs + Send + Sync + 'a, + C: ToRedisArgs + Send + Sync + 'a, + TS: std::marker::Copy + FromRedisValue, + V: std::marker::Copy + FromRedisValue, + >( + &'a mut self, + key: K, + from_timestamp: FTS, + to_timestamp: TTS, + count: Option, + aggregation_type: Option, + ) -> RedisFuture> { + self.range( + "TS.REVRANGE", + key, + from_timestamp, + to_timestamp, + count, + aggregation_type, + ) + } + + #[doc(hidden)] + fn mrange< 'a, FTS: ToRedisArgs + Send + Sync + 'a, TTS: ToRedisArgs + Send + Sync + 'a, @@ -341,13 +396,14 @@ pub trait AsyncTsCommands: ConnectionLike + Send + Sized { V: std::default::Default + FromRedisValue + Copy, >( &mut self, + command: &str, from_timestamp: FTS, to_timestamp: TTS, count: Option, aggregation_type: Option, filter_options: TsFilterOptions, ) -> RedisFuture> { - let mut c = cmd("TS.MRANGE"); + let mut c = cmd(command); c.arg(from_timestamp).arg(to_timestamp); if let Some(ct) = count { c.arg("COUNT").arg(ct); @@ -356,6 +412,58 @@ pub trait AsyncTsCommands: ConnectionLike + Send + Sized { Box::pin(async move { c.query_async(self).await }) } + /// Executes multiple redis time series range queries. + fn ts_mrange< + 'a, + FTS: ToRedisArgs + Send + Sync + 'a, + TTS: ToRedisArgs + Send + Sync + 'a, + C: ToRedisArgs + Send + Sync + 'a, + TS: std::default::Default + FromRedisValue + Copy, + V: std::default::Default + FromRedisValue + Copy, + >( + &mut self, + from_timestamp: FTS, + to_timestamp: TTS, + count: Option, + aggregation_type: Option, + filter_options: TsFilterOptions, + ) -> RedisFuture> { + self.mrange( + "TS.MRANGE", + from_timestamp, + to_timestamp, + count, + aggregation_type, + filter_options, + ) + } + + /// Executes multiple redis time series revrange queries. + fn ts_mrevrange< + 'a, + FTS: ToRedisArgs + Send + Sync + 'a, + TTS: ToRedisArgs + Send + Sync + 'a, + C: ToRedisArgs + Send + Sync + 'a, + TS: std::default::Default + FromRedisValue + Copy, + V: std::default::Default + FromRedisValue + Copy, + >( + &mut self, + from_timestamp: FTS, + to_timestamp: TTS, + count: Option, + aggregation_type: Option, + filter_options: TsFilterOptions, + ) -> RedisFuture> { + self.mrange( + "TS.MREVRANGE", + from_timestamp, + to_timestamp, + count, + aggregation_type, + filter_options, + ) + } + /// Returns a filtered list of redis time series keys. fn ts_queryindex(&mut self, filter_options: TsFilterOptions) -> RedisFuture> { Box::pin(async move { diff --git a/src/commands.rs b/src/commands.rs index b0ce972..ad3de5a 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -195,8 +195,8 @@ pub trait TsCommands: ConnectionLike + Sized { .query(self) } - /// Executes a redis time series range query. - fn ts_range< + #[doc(hidden)] + fn range< K: ToRedisArgs, FTS: ToRedisArgs, TTS: ToRedisArgs, @@ -205,13 +205,14 @@ pub trait TsCommands: ConnectionLike + Sized { V: std::marker::Copy + FromRedisValue, >( &mut self, + command: &str, key: K, from_timestamp: FTS, to_timestamp: TTS, count: Option, aggregation_type: Option, ) -> RedisResult> { - let mut c = cmd("TS.RANGE"); + let mut c = cmd(command); c.arg(key).arg(from_timestamp).arg(to_timestamp); if let Some(ct) = count { c.arg("COUNT").arg(ct); @@ -219,8 +220,60 @@ pub trait TsCommands: ConnectionLike + Sized { c.arg(aggregation_type).query(self) } - /// Executes multiple redis time series range queries. - fn ts_mrange< + /// Executes a redis time series range query. + fn ts_range< + K: ToRedisArgs, + FTS: ToRedisArgs, + TTS: ToRedisArgs, + C: ToRedisArgs, + TS: std::marker::Copy + FromRedisValue, + V: std::marker::Copy + FromRedisValue, + >( + &mut self, + key: K, + from_timestamp: FTS, + to_timestamp: TTS, + count: Option, + aggregation_type: Option, + ) -> RedisResult> { + self.range( + "TS.RANGE", + key, + from_timestamp, + to_timestamp, + count, + aggregation_type, + ) + } + + /// Executes a redis time series revrange query. + fn ts_revrange< + K: ToRedisArgs, + FTS: ToRedisArgs, + TTS: ToRedisArgs, + C: ToRedisArgs, + TS: std::marker::Copy + FromRedisValue, + V: std::marker::Copy + FromRedisValue, + >( + &mut self, + key: K, + from_timestamp: FTS, + to_timestamp: TTS, + count: Option, + aggregation_type: Option, + ) -> RedisResult> { + self.range( + "TS.REVRANGE", + key, + from_timestamp, + to_timestamp, + count, + aggregation_type, + ) + } + + #[doc(hidden)] + fn mrange< FTS: ToRedisArgs, TTS: ToRedisArgs, C: ToRedisArgs, @@ -228,13 +281,14 @@ pub trait TsCommands: ConnectionLike + Sized { V: std::default::Default + FromRedisValue + Copy, >( &mut self, + command: &str, from_timestamp: FTS, to_timestamp: TTS, count: Option, aggregation_type: Option, filter_options: TsFilterOptions, ) -> RedisResult> { - let mut c = cmd("TS.MRANGE"); + let mut c = cmd(command); c.arg(from_timestamp).arg(to_timestamp); if let Some(ct) = count { c.arg("COUNT").arg(ct); @@ -243,6 +297,56 @@ pub trait TsCommands: ConnectionLike + Sized { c.query(self) } + /// Executes multiple redis time series range queries. + fn ts_mrange< + FTS: ToRedisArgs, + TTS: ToRedisArgs, + C: ToRedisArgs, + TS: std::default::Default + FromRedisValue + Copy, + V: std::default::Default + FromRedisValue + Copy, + >( + &mut self, + from_timestamp: FTS, + to_timestamp: TTS, + count: Option, + aggregation_type: Option, + filter_options: TsFilterOptions, + ) -> RedisResult> { + self.mrange( + "TS.MRANGE", + from_timestamp, + to_timestamp, + count, + aggregation_type, + filter_options, + ) + } + + /// Executes multiple redis time series revrange queries. + fn ts_mrevrange< + FTS: ToRedisArgs, + TTS: ToRedisArgs, + C: ToRedisArgs, + TS: std::default::Default + FromRedisValue + Copy, + V: std::default::Default + FromRedisValue + Copy, + >( + &mut self, + from_timestamp: FTS, + to_timestamp: TTS, + count: Option, + aggregation_type: Option, + filter_options: TsFilterOptions, + ) -> RedisResult> { + self.mrange( + "TS.MREVRANGE", + from_timestamp, + to_timestamp, + count, + aggregation_type, + filter_options, + ) + } + /// Returns the latest (current) value in a redis time series. fn ts_get( &mut self, diff --git a/src/lib.rs b/src/lib.rs index 77c7640..d21289d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,12 +5,13 @@ //! series commands are available as synchronous and asynchronous versions. //! //! The crate is called `redis_ts` and you can depend on it via cargo. You will -//! also need redis in your dependencies. +//! also need redis in your dependencies. It has been tested agains redis 0.20.0 +//! but should work with versions higher than that. //! //! ```ini //! [dependencies] -//! redis = "0.19.0" -//! redis_ts = "*" +//! redis = "0.20.0" +//! redis_ts = "0.4.0" //! ``` //! //! Or via git: @@ -24,8 +25,8 @@ //! crate (either: 'async-std-comp' or 'tokio-comp): //! ```ini //! [dependencies] -//! redis = "0.19.0" -//! redis_ts = { version = "0.3.0", features = ['tokio-comp'] } +//! redis = "0.20.0" +//! redis_ts = { version = "0.4.0", features = ['tokio-comp'] } //! ``` //! //! # Synchronous usage @@ -232,7 +233,7 @@ //! # Ok(()) } //! ``` //! -//! ## TS.RANGE +//! ## TS.RANGE/TS.REVRANGE //! Query for a range of time series data. //! //! ```rust,no_run @@ -248,10 +249,14 @@ //! let range_raw:TsRange = con.ts_range( //! "my_engine", 1234, 5678, None::, None //! )?; +//! +//! let rev_range_raw:TsRange = con.ts_revrange( +//! "my_engine", 1234, 5678, None::, None +//! )?; //! # Ok(()) } //! ``` //! -//! ## TS.MRANGE +//! ## TS.MRANGE/TS.MREVRANGE //! Batch query multiple ranges of time series data. //! //! ```rust,no_run @@ -269,6 +274,11 @@ //! 1234, 5678, None::, None, //! TsFilterOptions::default().equals("sensor", "temperature") //! )?; +//! +//! let rev_range_raw:TsMrange = con.ts_mrevrange( +//! 1234, 5678, None::, None, +//! TsFilterOptions::default().equals("sensor", "temperature") +//! )?; //! # Ok(()) } //! ``` //! diff --git a/tests/async_command_tests/mod.rs b/tests/async_command_tests/mod.rs index 24bea80..7157dda 100644 --- a/tests/async_command_tests/mod.rs +++ b/tests/async_command_tests/mod.rs @@ -352,6 +352,52 @@ pub async fn ts_range(name: &str) { assert_eq!(res.values, vec![]); } +pub async fn ts_revrange(name: &str) { + let name2 = &format!("{:}2", name); + let mut con = prepare_ts(name).await; + let _ = prepare_ts(name2).await; + let _: () = con + .ts_madd(&[(name, 12, 1.0), (name, 123, 2.0), (name, 1234, 3.0)]) + .await + .unwrap(); + + let res: TsRange = con + .ts_revrange(name, "-", "+", None::, None) + .await + .unwrap(); + assert_eq!(res.values, vec![(1234, 3.0), (123, 2.0), (12, 1.0)]); + + let one_res: TsRange = con + .ts_revrange(name, "-", "+", Some(1), None) + .await + .unwrap(); + assert_eq!(one_res.values, vec![(1234, 3.0)]); + + let range_res: TsRange = con + .ts_revrange(name, 12, 123, None::, None) + .await + .unwrap(); + assert_eq!(range_res.values, vec![(123, 2.0), (12, 1.0)]); + + let sum: TsRange = con + .ts_revrange( + name, + 12, + 123, + None::, + Some(TsAggregationType::Sum(10000)), + ) + .await + .unwrap(); + assert_eq!(sum.values, vec![(0, 3.0)]); + + let res: TsRange = con + .ts_revrange(name2, "-", "+", None::, None) + .await + .unwrap(); + assert_eq!(res.values, vec![]); +} + pub async fn ts_mrange(name: &str) { let name2: &str = &format!("{:}2", name); let label = &format!("{:}label", name); @@ -413,6 +459,67 @@ pub async fn ts_mrange(name: &str) { assert!(res2.values.is_empty()); } +pub async fn ts_mrevrange(name: &str) { + let name2: &str = &format!("{:}2", name); + let label = &format!("{:}label", name); + + let mut con = get_con().await; + let _: () = con.del(name).await.unwrap(); + let _: () = con.del(name2).await.unwrap(); + let opts: TsOptions = TsOptions::default().label("l", label); + let _: () = con.ts_create(name, opts.clone()).await.unwrap(); + let _: () = con.ts_create(name2, opts.clone()).await.unwrap(); + let _: () = con + .ts_madd(&[ + (name, 12, 1.0), + (name, 123, 2.0), + (name, 1234, 3.0), + (name2, 21, 1.0), + (name2, 321, 2.0), + (name2, 4321, 3.0), + ]) + .await + .unwrap(); + + let res: TsMrange = con + .ts_mrevrange( + "-", + "+", + None::, + None, + TsFilterOptions::default() + .equals("l", label) + .with_labels(true), + ) + .await + .unwrap(); + assert_eq!(res.values.len(), 2); + assert_eq!( + res.values[1].values, + vec![(4321, 3.0), (321, 2.0), (21, 1.0)] + ); + assert_eq!(res.values[0].key, name); + assert_eq!(res.values[1].key, name2); + assert_eq!( + res.values[0].labels, + vec![("l".to_string(), label.to_string())] + ); + + let res2: TsMrange = con + .ts_mrevrange( + "-", + "+", + None::, + None, + TsFilterOptions::default() + .equals("none", "existing") + .with_labels(true), + ) + .await + .unwrap(); + assert!(res2.values.is_empty()); +} + pub async fn ts_queryindex(name: &str) { let mut con = get_con().await; let _: () = con.del(name).await.unwrap(); diff --git a/tests/test_async_std_commands.rs b/tests/test_async_std_commands.rs index 92b0050..0731e93 100644 --- a/tests/test_async_std_commands.rs +++ b/tests/test_async_std_commands.rs @@ -101,11 +101,21 @@ fn test_ts_range() { let _: () = block_on(ts_range("async_test_ts_range_std")); } +#[test] +fn test_ts_revrange() { + let _: () = block_on(ts_revrange("async_test_ts_revrange_std")); +} + #[test] fn test_ts_mrange() { let _: () = block_on(ts_mrange("async_test_ts_mrange_std")); } +#[test] +fn test_ts_mrevrange() { + let _: () = block_on(ts_mrevrange("async_test_ts_mrevrange_std")); +} + #[test] fn test_ts_queryindex() { let _: () = block_on(ts_queryindex("async_test_ts_queryindex_std")); diff --git a/tests/test_async_tokio_commands.rs b/tests/test_async_tokio_commands.rs index 5232af5..bd3fffc 100644 --- a/tests/test_async_tokio_commands.rs +++ b/tests/test_async_tokio_commands.rs @@ -102,11 +102,21 @@ fn test_ts_range() { let _: () = block_on(ts_range("async_test_ts_range_tokio")); } +#[test] +fn test_ts_revrange() { + let _: () = block_on(ts_revrange("async_test_ts_revrange_tokio")); +} + #[test] fn test_ts_mrange() { let _: () = block_on(ts_mrange("async_test_ts_mrange_tokio")); } +#[test] +fn test_ts_mrevrange() { + let _: () = block_on(ts_mrevrange("async_test_ts_mrevrange_tokio")); +} + #[test] fn test_ts_queryindex() { let _: () = block_on(ts_queryindex("async_test_ts_queryindex_tokio")); diff --git a/tests/test_commands.rs b/tests/test_commands.rs index 419d8a9..80fd200 100644 --- a/tests/test_commands.rs +++ b/tests/test_commands.rs @@ -413,6 +413,56 @@ fn test_ts_range() { assert_eq!(res.values, vec![]); } +#[test] +fn test_ts_revrange() { + let _: () = get_con().del("test_ts_revrange").unwrap(); + let _: () = get_con().del("test_ts_revrange2").unwrap(); + let _: () = get_con() + .ts_create("test_ts_revrange", default_settings()) + .unwrap(); + let _: () = get_con() + .ts_create("test_ts_revrange2", default_settings()) + .unwrap(); + let _: () = get_con() + .ts_madd(&[ + ("test_ts_revrange", 12, 1.0), + ("test_ts_revrange", 123, 2.0), + ("test_ts_revrange", 1234, 3.0), + ]) + .unwrap(); + + let res: TsRange = get_con() + .ts_revrange("test_ts_revrange", "-", "+", None::, None) + .unwrap(); + assert_eq!(res.values, vec![(1234, 3.0), (123, 2.0), (12, 1.0)]); + + let one_res: TsRange = get_con() + .ts_revrange("test_ts_revrange", "-", "+", Some(1), None) + .unwrap(); + assert_eq!(one_res.values, vec![(1234, 3.0)]); + + let range_res: TsRange = get_con() + .ts_revrange("test_ts_revrange", 12, 123, None::, None) + .unwrap(); + assert_eq!(range_res.values, vec![(123, 2.0), (12, 1.0)]); + + let sum: TsRange = get_con() + .ts_revrange( + "test_ts_revrange", + 12, + 123, + None::, + Some(TsAggregationType::Sum(10000)), + ) + .unwrap(); + assert_eq!(sum.values, vec![(0, 3.0)]); + + let res: TsRange = get_con() + .ts_revrange("test_ts_revrange2", "-", "+", None::, None) + .unwrap(); + assert_eq!(res.values, vec![]); +} + #[test] fn test_ts_mrange() { let _: () = get_con().del("test_ts_mrange").unwrap(); @@ -470,6 +520,65 @@ fn test_ts_mrange() { assert!(res2.values.is_empty()); } +#[test] +fn test_ts_mrevrange() { + let _: () = get_con().del("test_ts_mrevrange").unwrap(); + let _: () = get_con().del("test_ts_mrevrange2").unwrap(); + let opts: TsOptions = TsOptions::default().label("l", "mrevrange"); + let _: () = get_con() + .ts_create("test_ts_mrevrange", opts.clone()) + .unwrap(); + let _: () = get_con() + .ts_create("test_ts_mrevrange2", opts.clone()) + .unwrap(); + let _: () = get_con() + .ts_madd(&[ + ("test_ts_mrevrange", 12, 1.0), + ("test_ts_mrevrange", 123, 2.0), + ("test_ts_mrevrange", 1234, 3.0), + ("test_ts_mrevrange2", 21, 1.0), + ("test_ts_mrevrange2", 321, 2.0), + ("test_ts_mrevrange2", 4321, 3.0), + ]) + .unwrap(); + + let res: TsMrange = get_con() + .ts_mrevrange( + "-", + "+", + None::, + None, + TsFilterOptions::default() + .equals("l", "mrevrange") + .with_labels(true), + ) + .unwrap(); + assert_eq!(res.values.len(), 2); + assert_eq!( + res.values[1].values, + vec![(4321, 3.0), (321, 2.0), (21, 1.0)] + ); + assert_eq!(res.values[0].key, "test_ts_mrevrange"); + assert_eq!(res.values[1].key, "test_ts_mrevrange2"); + assert_eq!( + res.values[0].labels, + vec![("l".to_string(), "mrevrange".to_string())] + ); + + let res2: TsMrange = get_con() + .ts_mrange( + "-", + "+", + None::, + None, + TsFilterOptions::default() + .equals("none", "existing") + .with_labels(true), + ) + .unwrap(); + assert!(res2.values.is_empty()); +} + #[test] fn test_ts_queryindex() { let _: () = get_con().del("test_ts_queryindex").unwrap();