1mod scripts;
28pub use scripts::{MarkerAdmission, MarkerReply};
29
30use std::collections::{HashMap, HashSet};
31use std::time::Duration;
32
33use serde::Serialize;
34use serde::de::DeserializeOwned;
35use siderite_orm::{
36 BackendCapabilities, BackendCapabilityError, BackendError, Feature, OrmError, QueryError,
37};
38use thiserror::Error;
39
40const MAX_TTL_MS: u64 = (i64::MAX / 2) as u64;
46
47#[derive(Debug, Error)]
54#[non_exhaustive]
55pub enum RedisError {
56 #[error("redis connection error: {0}")]
58 Connection(String),
59 #[error("redis error: {0}")]
61 Command(String),
62 #[error("cannot decode redis value `{key}`: {reason}")]
64 Decode {
65 key: String,
67 reason: String,
69 },
70 #[error("{0}")]
72 Invalid(String),
73 #[error(transparent)]
75 Capability(#[from] BackendCapabilityError),
76}
77
78impl From<redis::RedisError> for RedisError {
79 fn from(err: redis::RedisError) -> Self {
80 match err.kind() {
81 redis::ErrorKind::Io
82 | redis::ErrorKind::Client
83 | redis::ErrorKind::AuthenticationFailed
84 | redis::ErrorKind::InvalidClientConfig
85 | redis::ErrorKind::ClusterConnectionNotFound
86 | redis::ErrorKind::MasterNameNotFoundBySentinel
87 | redis::ErrorKind::NoValidReplicasFoundBySentinel
88 | redis::ErrorKind::EmptySentinelList => Self::Connection(err.to_string()),
89 _ => Self::Command(err.to_string()),
90 }
91 }
92}
93
94impl From<RedisError> for OrmError {
95 fn from(err: RedisError) -> Self {
96 match err {
97 RedisError::Connection(message) => Self::Backend(BackendError::Connection(message)),
98 RedisError::Command(message) => Self::Backend(BackendError::Database(message)),
99 RedisError::Decode { key, reason } => Self::Query(QueryError::Decode {
100 column: key,
101 reason,
102 }),
103 RedisError::Invalid(message) => Self::Query(QueryError::InvalidPlan(message)),
104 RedisError::Capability(err) => Self::Capability(err),
105 }
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
111pub enum Ttl {
112 Missing,
114 Persistent,
116 ExpiresIn(Duration),
118}
119
120#[derive(Clone)]
126pub struct RedisStore {
127 manager: redis::aio::ConnectionManager,
128 prefix: String,
129}
130
131impl std::fmt::Debug for RedisStore {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 f.debug_struct("RedisStore")
134 .field("prefix", &self.prefix)
135 .field("database", &self.database())
136 .finish()
137 }
138}
139
140impl RedisStore {
141 pub fn capabilities() -> BackendCapabilities {
145 BackendCapabilities::redis()
146 }
147
148 pub fn require(feature: Feature) -> Result<(), RedisError> {
156 Self::capabilities().require(feature).map_err(Into::into)
157 }
158
159 pub async fn connect(url: &str) -> Result<Self, RedisError> {
166 let client = redis::Client::open(url)?;
167 Self::connect_with(client, redis::aio::ConnectionManagerConfig::new()).await
168 }
169
170 pub async fn connect_with(
182 client: redis::Client,
183 config: redis::aio::ConnectionManagerConfig,
184 ) -> Result<Self, RedisError> {
185 let manager = redis::aio::ConnectionManager::new_with_config(client, config).await?;
186 Ok(Self {
187 manager,
188 prefix: String::new(),
189 })
190 }
191
192 pub fn with_prefix(&self, prefix: impl Into<String>) -> Self {
198 Self {
199 manager: self.manager.clone(),
200 prefix: prefix.into(),
201 }
202 }
203
204 pub fn prefix(&self) -> &str {
206 &self.prefix
207 }
208
209 pub fn key(&self, key: &str) -> String {
211 prefixed_key(&self.prefix, key)
212 }
213
214 pub fn database(&self) -> i64 {
218 redis::aio::ConnectionLike::get_db(&self.manager)
219 }
220
221 pub async fn get(&self, key: &str) -> Result<Option<String>, RedisError> {
226 let raw: Option<Vec<u8>> = self.query(redis::cmd("GET").arg(self.key(key))).await?;
227 optional_utf8(key, raw)
228 }
229
230 pub async fn set(
236 &self,
237 key: &str,
238 value: &str,
239 ttl: Option<Duration>,
240 ) -> Result<(), RedisError> {
241 let px = optional_ttl(ttl)?;
242 let wrote = self.set_raw(key, value, px, false).await?;
243 if wrote {
244 Ok(())
245 } else {
246 Err(RedisError::Command(format!(
247 "SET `{key}` returned a nil reply"
248 )))
249 }
250 }
251
252 pub async fn set_nx(&self, key: &str, value: &str) -> Result<bool, RedisError> {
262 self.set_raw(key, value, None, true).await
263 }
264
265 pub async fn set_nx_px(
277 &self,
278 key: &str,
279 value: &str,
280 ttl: Duration,
281 ) -> Result<bool, RedisError> {
282 let px = required_ttl(ttl)?;
283 self.set_raw(key, value, Some(px), true).await
284 }
285
286 pub async fn del(&self, key: &str) -> Result<u64, RedisError> {
291 let removed: i64 = self.query(redis::cmd("DEL").arg(self.key(key))).await?;
292 as_count(removed)
293 }
294
295 pub async fn exists(&self, key: &str) -> Result<bool, RedisError> {
300 let n: i64 = self.query(redis::cmd("EXISTS").arg(self.key(key))).await?;
301 Ok(n != 0)
302 }
303
304 pub async fn expire(&self, key: &str, ttl: Duration) -> Result<bool, RedisError> {
310 let ms = required_ttl(ttl)?;
311 let n: i64 = self
312 .query(redis::cmd("PEXPIRE").arg(self.key(key)).arg(ms))
313 .await?;
314 Ok(n != 0)
315 }
316
317 pub async fn ttl(&self, key: &str) -> Result<Ttl, RedisError> {
322 let ms: i64 = self.query(redis::cmd("PTTL").arg(self.key(key))).await?;
323 match ms {
324 -2 => Ok(Ttl::Missing),
325 -1 => Ok(Ttl::Persistent),
326 0.. => Ok(Ttl::ExpiresIn(Duration::from_millis(
327 u64::try_from(ms)
328 .map_err(|_| RedisError::Command(format!("PTTL `{key}` overflowed: {ms}")))?,
329 ))),
330 other => Err(RedisError::Command(format!(
331 "unexpected PTTL reply for `{key}`: {other}"
332 ))),
333 }
334 }
335
336 pub async fn incr_by(&self, key: &str, by: i64) -> Result<i64, RedisError> {
341 self.query(redis::cmd("INCRBY").arg(self.key(key)).arg(by))
342 .await
343 }
344
345 pub async fn hget(&self, key: &str, field: &str) -> Result<Option<String>, RedisError> {
350 let raw: Option<Vec<u8>> = self
351 .query(redis::cmd("HGET").arg(self.key(key)).arg(field))
352 .await?;
353 optional_utf8(key, raw)
354 }
355
356 pub async fn hset(&self, key: &str, field: &str, value: &str) -> Result<u64, RedisError> {
361 let n: i64 = self
362 .query(redis::cmd("HSET").arg(self.key(key)).arg(field).arg(value))
363 .await?;
364 as_count(n)
365 }
366
367 pub async fn hset_many<I, F, V>(&self, key: &str, fields: I) -> Result<u64, RedisError>
373 where
374 I: IntoIterator<Item = (F, V)>,
375 F: AsRef<str>,
376 V: AsRef<str>,
377 {
378 let fields = collect_pairs(fields, "hset_many requires at least one field")?;
379 let mut cmd = redis::cmd("HSET");
380 cmd.arg(self.key(key));
381 for (field, value) in &fields {
382 cmd.arg(field).arg(value);
383 }
384 as_count(self.query(&cmd).await?)
385 }
386
387 pub async fn hgetall(&self, key: &str) -> Result<HashMap<String, String>, RedisError> {
394 let raw: Vec<(Vec<u8>, Vec<u8>)> =
395 self.query(redis::cmd("HGETALL").arg(self.key(key))).await?;
396 let mut out = HashMap::with_capacity(raw.len());
397 for (field, value) in raw {
398 out.insert(decode_utf8(key, field)?, decode_utf8(key, value)?);
399 }
400 Ok(out)
401 }
402
403 pub async fn hdel<I, F>(&self, key: &str, fields: I) -> Result<u64, RedisError>
409 where
410 I: IntoIterator<Item = F>,
411 F: AsRef<str>,
412 {
413 let fields = collect_strings(fields, "hdel requires at least one field")?;
414 let mut cmd = redis::cmd("HDEL");
415 cmd.arg(self.key(key));
416 for field in &fields {
417 cmd.arg(field);
418 }
419 as_count(self.query(&cmd).await?)
420 }
421
422 pub async fn sadd<I, M>(&self, key: &str, members: I) -> Result<u64, RedisError>
428 where
429 I: IntoIterator<Item = M>,
430 M: AsRef<str>,
431 {
432 self.set_op("SADD", "sadd requires at least one member", key, members)
433 .await
434 }
435
436 pub async fn srem<I, M>(&self, key: &str, members: I) -> Result<u64, RedisError>
442 where
443 I: IntoIterator<Item = M>,
444 M: AsRef<str>,
445 {
446 self.set_op("SREM", "srem requires at least one member", key, members)
447 .await
448 }
449
450 pub async fn smembers(&self, key: &str) -> Result<HashSet<String>, RedisError> {
455 let raw: Vec<Vec<u8>> = self
456 .query(redis::cmd("SMEMBERS").arg(self.key(key)))
457 .await?;
458 raw.into_iter()
459 .map(|bytes| decode_utf8(key, bytes))
460 .collect()
461 }
462
463 pub async fn sismember(&self, key: &str, member: &str) -> Result<bool, RedisError> {
468 let n: i64 = self
469 .query(redis::cmd("SISMEMBER").arg(self.key(key)).arg(member))
470 .await?;
471 Ok(n != 0)
472 }
473
474 pub async fn get_json<T>(&self, key: &str) -> Result<Option<T>, RedisError>
483 where
484 T: DeserializeOwned,
485 {
486 let Some(raw) = self.get(key).await? else {
487 return Ok(None);
488 };
489 serde_json::from_str(&raw)
490 .map(Some)
491 .map_err(|err| RedisError::Decode {
492 key: key.to_owned(),
493 reason: format!("invalid JSON: {err}"),
494 })
495 }
496
497 pub async fn set_json<T>(
503 &self,
504 key: &str,
505 value: &T,
506 ttl: Option<Duration>,
507 ) -> Result<(), RedisError>
508 where
509 T: Serialize,
510 {
511 let px = optional_ttl(ttl)?;
512 let json = serde_json::to_string(value).map_err(|err| {
513 RedisError::Invalid(format!("cannot serialize JSON for `{key}`: {err}"))
514 })?;
515 let wrote = self.set_raw(key, &json, px, false).await?;
516 if wrote {
517 Ok(())
518 } else {
519 Err(RedisError::Command(format!(
520 "SET `{key}` returned a nil reply"
521 )))
522 }
523 }
524
525 pub async fn pipeline<T, F>(&self, build: F) -> Result<T, RedisError>
542 where
543 T: redis::FromRedisValue,
544 F: FnOnce(&mut redis::Pipeline),
545 {
546 let mut pipe = redis::pipe();
547 build(pipe.atomic());
548 let mut conn = self.manager.clone();
549 pipe.query_async(&mut conn).await.map_err(RedisError::from)
550 }
551
552 pub async fn delete_namespace(&self) -> Result<u64, RedisError> {
562 if self.prefix.is_empty() {
563 return Err(RedisError::Invalid(
564 "refusing to delete keys when the prefix is empty; this would match every key"
565 .into(),
566 ));
567 }
568 let pattern = format!("{}*", glob_literal(&self.prefix));
569 let mut cursor = "0".to_owned();
570 let mut deleted = 0u64;
571 loop {
572 let (next, keys): (String, Vec<Vec<u8>>) = self
573 .query(
574 redis::cmd("SCAN")
575 .arg(&cursor)
576 .arg("MATCH")
577 .arg(&pattern)
578 .arg("COUNT")
579 .arg(200u32),
580 )
581 .await?;
582 if !keys.is_empty() {
583 let mut cmd = redis::cmd("DEL");
584 for key in &keys {
585 cmd.arg(key);
586 }
587 deleted += as_count(self.query(&cmd).await?)?;
588 }
589 if next == "0" {
590 break;
591 }
592 cursor = next;
593 }
594 Ok(deleted)
595 }
596
597 async fn set_raw(
598 &self,
599 key: &str,
600 value: &str,
601 px: Option<u64>,
602 nx: bool,
603 ) -> Result<bool, RedisError> {
604 let mut cmd = redis::cmd("SET");
605 cmd.arg(self.key(key)).arg(value);
606 if let Some(ms) = px {
607 cmd.arg("PX").arg(ms);
608 }
609 if nx {
610 cmd.arg("NX");
611 }
612 let reply: redis::Value = self.query(&cmd).await?;
613 match reply {
614 redis::Value::Nil => Ok(false),
615 redis::Value::SimpleString(ref status) if status == "OK" => Ok(true),
616 redis::Value::Okay => Ok(true),
617 other => Err(RedisError::Command(format!(
618 "unexpected SET reply for `{key}`: {other:?}"
619 ))),
620 }
621 }
622
623 async fn set_op<I, M>(
624 &self,
625 command: &str,
626 empty: &str,
627 key: &str,
628 members: I,
629 ) -> Result<u64, RedisError>
630 where
631 I: IntoIterator<Item = M>,
632 M: AsRef<str>,
633 {
634 let members = collect_strings(members, empty)?;
635 let mut cmd = redis::cmd(command);
636 cmd.arg(self.key(key));
637 for member in &members {
638 cmd.arg(member);
639 }
640 as_count(self.query(&cmd).await?)
641 }
642
643 async fn query<T>(&self, cmd: &redis::Cmd) -> Result<T, RedisError>
644 where
645 T: redis::FromRedisValue,
646 {
647 let mut conn = self.manager.clone();
648 cmd.query_async(&mut conn).await.map_err(RedisError::from)
649 }
650}
651
652fn prefixed_key(prefix: &str, key: &str) -> String {
653 let mut full = String::with_capacity(prefix.len() + key.len());
654 full.push_str(prefix);
655 full.push_str(key);
656 full
657}
658
659fn glob_literal(prefix: &str) -> String {
661 let mut out = String::with_capacity(prefix.len());
662 for ch in prefix.chars() {
663 if matches!(ch, '*' | '?' | '[' | '\\') {
664 out.push('\\');
665 }
666 out.push(ch);
667 }
668 out
669}
670
671fn optional_ttl(ttl: Option<Duration>) -> Result<Option<u64>, RedisError> {
672 ttl.map(required_ttl).transpose()
673}
674
675fn required_ttl(ttl: Duration) -> Result<u64, RedisError> {
676 let ms = ttl.as_millis();
677 let Ok(ms) = u64::try_from(ms) else {
678 return Err(ttl_rejected());
679 };
680 if ms == 0 || ms > MAX_TTL_MS {
681 return Err(ttl_rejected());
682 }
683 Ok(ms)
684}
685
686fn ttl_rejected() -> RedisError {
687 RedisError::Invalid(
688 "redis TTL must be between 1 millisecond and i64::MAX / 2 milliseconds".into(),
689 )
690}
691
692fn as_count(n: i64) -> Result<u64, RedisError> {
693 u64::try_from(n).map_err(|_| RedisError::Command(format!("negative count from redis: {n}")))
694}
695
696fn decode_utf8(key: &str, bytes: Vec<u8>) -> Result<String, RedisError> {
697 String::from_utf8(bytes).map_err(|err| RedisError::Decode {
698 key: key.to_owned(),
699 reason: format!("value is not UTF-8: {err}"),
700 })
701}
702
703fn optional_utf8(key: &str, bytes: Option<Vec<u8>>) -> Result<Option<String>, RedisError> {
704 bytes.map(|value| decode_utf8(key, value)).transpose()
705}
706
707fn collect_strings<I, S>(items: I, empty: &str) -> Result<Vec<String>, RedisError>
708where
709 I: IntoIterator<Item = S>,
710 S: AsRef<str>,
711{
712 let items: Vec<String> = items
713 .into_iter()
714 .map(|item| item.as_ref().to_owned())
715 .collect();
716 if items.is_empty() {
717 Err(RedisError::Invalid(empty.to_owned()))
718 } else {
719 Ok(items)
720 }
721}
722
723fn collect_pairs<I, F, V>(pairs: I, empty: &str) -> Result<Vec<(String, String)>, RedisError>
724where
725 I: IntoIterator<Item = (F, V)>,
726 F: AsRef<str>,
727 V: AsRef<str>,
728{
729 let pairs: Vec<(String, String)> = pairs
730 .into_iter()
731 .map(|(field, value)| (field.as_ref().to_owned(), value.as_ref().to_owned()))
732 .collect();
733 if pairs.is_empty() {
734 Err(RedisError::Invalid(empty.to_owned()))
735 } else {
736 Ok(pairs)
737 }
738}
739
740#[cfg(test)]
741mod tests {
742 use super::*;
743 use siderite_orm::{BackendKind, IsolationLevel};
744
745 #[test]
746 fn key_prefix_is_prepended_and_empty_prefix_is_identity() {
747 assert_eq!(prefixed_key("", "user:1"), "user:1");
748 assert_eq!(prefixed_key("app:", "user:1"), "app:user:1");
749 assert_eq!(prefixed_key("app:", ""), "app:");
750 assert_eq!(prefixed_key("ns", "user"), "nsuser");
751 }
752
753 #[test]
754 fn glob_metacharacters_in_a_prefix_are_escaped() {
755 assert_eq!(glob_literal("a*b?c[d]\\e"), "a\\*b\\?c\\[d]\\\\e");
756 assert_eq!(glob_literal("plain:"), "plain:");
757 }
758
759 #[test]
760 fn ttl_bounds_are_rejected_before_any_connection() {
761 assert!(matches!(
762 required_ttl(Duration::ZERO),
763 Err(RedisError::Invalid(_))
764 ));
765 assert!(matches!(
766 optional_ttl(Some(Duration::from_nanos(1))),
767 Err(RedisError::Invalid(_))
768 ));
769 assert_eq!(
770 optional_ttl(Some(Duration::from_millis(1500))).unwrap(),
771 Some(1500)
772 );
773 assert_eq!(optional_ttl(None).unwrap(), None);
774 let too_long = Duration::from_millis(MAX_TTL_MS).saturating_add(Duration::from_millis(1));
775 assert!(matches!(
776 required_ttl(too_long),
777 Err(RedisError::Invalid(_))
778 ));
779 let orm = OrmError::from(required_ttl(Duration::ZERO).unwrap_err());
780 assert!(matches!(orm, OrmError::Query(QueryError::InvalidPlan(_))));
781 }
782
783 #[test]
784 fn relational_features_fail_before_any_connection() {
785 assert!(matches!(
786 RedisStore::require(Feature::RowLocking),
787 Err(RedisError::Capability(
788 BackendCapabilityError::RowLockingUnsupported {
789 backend: BackendKind::Redis
790 }
791 ))
792 ));
793 let err = RedisStore::require(Feature::Joins).unwrap_err();
794 assert!(matches!(
795 OrmError::from(err),
796 OrmError::Capability(BackendCapabilityError::Unsupported {
797 backend: BackendKind::Redis,
798 feature: Feature::Joins,
799 })
800 ));
801 assert!(RedisStore::require(Feature::Savepoints).is_err());
802 assert!(RedisStore::require(Feature::Isolation(IsolationLevel::Serializable)).is_err());
803 assert!(!RedisStore::capabilities().supports(Feature::CaseInsensitiveLike));
804 }
805
806 #[test]
807 fn driver_and_decode_errors_map_into_orm_error() {
808 let connection = RedisError::Connection("refused".into());
809 assert!(matches!(
810 OrmError::from(connection),
811 OrmError::Backend(BackendError::Connection(message)) if message == "refused"
812 ));
813 let command = RedisError::Command("WRONGTYPE".into());
814 assert!(matches!(
815 OrmError::from(command),
816 OrmError::Backend(BackendError::Database(message)) if message == "WRONGTYPE"
817 ));
818 let decode = RedisError::Decode {
819 key: "widget".into(),
820 reason: "invalid JSON".into(),
821 };
822 assert!(matches!(
823 OrmError::from(decode),
824 OrmError::Query(QueryError::Decode { column, reason })
825 if column == "widget" && reason == "invalid JSON"
826 ));
827 }
828
829 #[test]
830 fn empty_argument_lists_are_rejected_without_a_command() {
831 let err = collect_strings(Vec::<&str>::new(), "sadd requires at least one member");
832 assert!(matches!(err, Err(RedisError::Invalid(_))));
833 let err = collect_pairs(
834 Vec::<(&str, &str)>::new(),
835 "hset_many requires at least one field",
836 );
837 assert!(matches!(err, Err(RedisError::Invalid(_))));
838 }
839
840 #[test]
841 fn store_type_is_send_and_sync() {
842 fn assert_send_sync<T: Send + Sync>() {}
843 assert_send_sync::<RedisStore>();
844 }
845
846 #[tokio::test]
847 async fn a_malformed_url_is_a_connection_error() {
848 let err = RedisStore::connect("not a url").await.unwrap_err();
849 assert!(matches!(err, RedisError::Connection(_)));
850 assert!(matches!(
851 OrmError::from(err),
852 OrmError::Backend(BackendError::Connection(_))
853 ));
854 }
855}