Skip to main content

glide/commands/
options.rs

1// Copyright Valkey GLIDE Project Contributors - SPDX Identifier: Apache-2.0
2//! Shared option types used by multiple command families.
3//!
4//! Mirrors the Python `glide_shared.commands.core_options` and
5//! `command_args` modules.
6
7use crate::write::ToSingleValkeyArg;
8use crate::write::ToValkeyArgs;
9use crate::write::ValkeyWrite;
10
11/// Condition for the hash-field expire commands.
12///
13/// Mirrors redis-rs's `ExpireOption` type.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ExpireOption {
16    /// Set the expiry regardless of the field's current expiry (no argument).
17    NONE,
18    /// Set the expiry only when the field has no expiry (`NX`).
19    NX,
20    /// Set the expiry only when the field has an existing expiry (`XX`).
21    XX,
22    /// Set the expiry only when the new expiry is greater than the current one (`GT`).
23    GT,
24    /// Set the expiry only when the new expiry is less than the current one (`LT`).
25    LT,
26}
27
28impl ToValkeyArgs for ExpireOption {
29    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
30        match self {
31            ExpireOption::NONE => {}
32            ExpireOption::NX => out.write_arg(b"NX"),
33            ExpireOption::XX => out.write_arg(b"XX"),
34            ExpireOption::GT => out.write_arg(b"GT"),
35            ExpireOption::LT => out.write_arg(b"LT"),
36        }
37    }
38}
39
40/// Expiry to apply when setting a value.
41///
42/// Mirrors redis-rs's `SetExpiry` type.
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
44pub enum SetExpiry {
45    /// Expire after the given number of seconds (`EX`).
46    EX(u64),
47    /// Expire after the given number of milliseconds (`PX`).
48    PX(u64),
49    /// Expire at the given Unix time in seconds (`EXAT`).
50    EXAT(u64),
51    /// Expire at the given Unix time in milliseconds (`PXAT`).
52    PXAT(u64),
53    /// Retain the key's existing TTL (`KEEPTTL`).
54    KEEPTTL,
55}
56
57impl ToValkeyArgs for SetExpiry {
58    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
59        let mut kw = |k: &[u8], v: u64| {
60            out.write_arg(k);
61            out.write_arg_fmt(v);
62        };
63        match self {
64            SetExpiry::EX(secs) => kw(b"EX", *secs),
65            SetExpiry::PX(millis) => kw(b"PX", *millis),
66            SetExpiry::EXAT(ts) => kw(b"EXAT", *ts),
67            SetExpiry::PXAT(ts) => kw(b"PXAT", *ts),
68            SetExpiry::KEEPTTL => out.write_arg(b"KEEPTTL"),
69        }
70    }
71}
72
73/// Existence check for `SET` and `GEOADD`.
74///
75/// Mirrors redis-rs's `ExistenceCheck` type.
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum ExistenceCheck {
78    /// Only set the key if it does not already exist (`NX`).
79    NX,
80    /// Only set the key if it already exists (`XX`).
81    XX,
82}
83
84impl ToValkeyArgs for ExistenceCheck {
85    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
86        out.write_arg(match self {
87            ExistenceCheck::NX => b"NX".as_slice(),
88            ExistenceCheck::XX => b"XX".as_slice(),
89        });
90    }
91}
92
93/// Field existence check for `HSETEX`.
94///
95/// Mirrors redis-rs's `FieldExistenceCheck` type.
96#[derive(Debug, Clone, Copy, PartialEq, Eq)]
97pub enum FieldExistenceCheck {
98    /// Only set the fields if none of them already exist (`FNX`).
99    FNX,
100    /// Only set the fields if all of them already exist (`FXX`).
101    FXX,
102}
103
104impl ToValkeyArgs for FieldExistenceCheck {
105    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
106        out.write_arg(match self {
107            FieldExistenceCheck::FNX => b"FNX".as_slice(),
108            FieldExistenceCheck::FXX => b"FXX".as_slice(),
109        });
110    }
111}
112
113/// Options for the `HSETEX` command.
114///
115/// Mirrors redis-rs's `HashFieldExpirationOptions` type.
116#[derive(Debug, Clone, Copy, Default)]
117pub struct HashFieldExpirationOptions {
118    existence_check: Option<FieldExistenceCheck>,
119    expiration: Option<SetExpiry>,
120}
121
122impl HashFieldExpirationOptions {
123    /// Set the field existence check (`FNX`/`FXX`).
124    pub fn set_existence_check(mut self, field_existence_check: FieldExistenceCheck) -> Self {
125        self.existence_check = Some(field_existence_check);
126        self
127    }
128
129    /// Set the fields' expiry.
130    pub fn set_expiration(mut self, expiration: SetExpiry) -> Self {
131        self.expiration = Some(expiration);
132        self
133    }
134}
135
136impl ToValkeyArgs for HashFieldExpirationOptions {
137    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
138        if let Some(ref existence_check) = self.existence_check {
139            existence_check.write_valkey_args(out);
140        }
141        if let Some(ref expiration) = self.expiration {
142            expiration.write_valkey_args(out);
143        }
144    }
145}
146
147/// Options for the `COPY` command.
148///
149/// Mirrors redis-rs's `CopyOptions` type.
150#[derive(Debug, Clone, Copy)]
151pub struct CopyOptions<Db: ToString> {
152    db: Option<Db>,
153    replace: bool,
154}
155
156impl Default for CopyOptions<&'static str> {
157    fn default() -> Self {
158        CopyOptions {
159            db: None,
160            replace: false,
161        }
162    }
163}
164
165impl<Db: ToString> CopyOptions<Db> {
166    /// Copy into the given logical database (`DB`).
167    pub fn db<Db2: ToString>(self, db: Db2) -> CopyOptions<Db2> {
168        CopyOptions {
169            db: Some(db),
170            replace: self.replace,
171        }
172    }
173
174    /// Overwrite the destination key if it exists (`REPLACE`).
175    pub fn replace(mut self, replace: bool) -> Self {
176        self.replace = replace;
177        self
178    }
179}
180
181impl<Db: ToString> ToValkeyArgs for CopyOptions<Db> {
182    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
183        if let Some(ref db) = self.db {
184            out.write_arg(b"DB");
185            out.write_arg(db.to_string().as_bytes());
186        }
187        if self.replace {
188            out.write_arg(b"REPLACE");
189        }
190    }
191}
192
193/// Options for the `FLUSHALL` command.
194///
195/// Mirrors redis-rs's `FlushAllOptions` type.
196#[derive(Debug, Clone, Copy, Default)]
197pub struct FlushAllOptions {
198    /// Flush synchronously (`SYNC`) if `true`, asynchronously (`ASYNC`) otherwise.
199    pub blocking: bool,
200}
201
202impl FlushAllOptions {
203    /// Set whether to flush synchronously (`SYNC`) or asynchronously (`ASYNC`).
204    pub fn blocking(mut self, blocking: bool) -> Self {
205        self.blocking = blocking;
206        self
207    }
208}
209
210impl ToValkeyArgs for FlushAllOptions {
211    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
212        out.write_arg(if self.blocking { b"SYNC" } else { b"ASYNC" });
213    }
214}
215
216/// Options for the `FLUSHDB` command.
217///
218/// Mirrors redis-rs's `FlushDbOptions` type.
219pub type FlushDbOptions = FlushAllOptions;
220
221/// Options for the `FUNCTION FLUSH` command.
222pub type FunctionFlushOptions = FlushAllOptions;
223
224/// Value comparison for `SET` — whether the key's current value must equal
225/// the given one.
226///
227/// Mirrors redis-rs's `ValueComparison` type.
228/// Only `IFEQ` is supported.
229//
230// GLIDE's `ValueComparison` diverges from redis-rs by holding a `Vec<u8>`
231// instead of a `String`: redis-rs builds that `String` lossily, silently
232// breaking comparisons of binary (non-UTF-8) values. See #5046 for a related
233// binary `IFEQ` issue in the Java client.
234//
235// TODO #7237: Add `IFNE` (and `ValueComparison::ifne`) for Valkey 9.2.
236// TODO #7238: Use for `del_ex` (`DELEX`, Valkey 9.2).
237#[derive(Debug, Clone, PartialEq, Eq)]
238#[non_exhaustive]
239pub enum ValueComparison {
240    /// Only set if the current value equals the given value (`IFEQ`).
241    IFEQ(Vec<u8>),
242}
243
244impl ValueComparison {
245    /// Compare for equality with the given value (`IFEQ`).
246    pub fn ifeq(value: impl ToSingleValkeyArg) -> Self {
247        Self::IFEQ(Self::arg_to_bytes(value))
248    }
249
250    fn arg_to_bytes(value: impl ToSingleValkeyArg) -> Vec<u8> {
251        value.to_valkey_args().swap_remove(0)
252    }
253}
254
255impl ToValkeyArgs for ValueComparison {
256    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
257        match self {
258            ValueComparison::IFEQ(value) => {
259                out.write_arg(b"IFEQ");
260                out.write_arg(value);
261            }
262        }
263    }
264}
265
266/// Options for the `SET` command.
267///
268/// Mirrors redis-rs's `SetOptions` type.
269#[derive(Clone, Default)]
270pub struct SetOptions {
271    conditional_set: Option<ExistenceCheck>,
272    value_comparison: Option<ValueComparison>,
273    get: bool,
274    expiration: Option<SetExpiry>,
275}
276
277impl SetOptions {
278    /// Set the existence check (`NX`/`XX`).
279    pub fn conditional_set(mut self, existence_check: ExistenceCheck) -> Self {
280        self.conditional_set = Some(existence_check);
281        self
282    }
283
284    /// Set the value comparison (`IFEQ`).
285    // TODO #7237: Document `IFNE` once it is supported.
286    pub fn value_comparison(mut self, value_comparison: ValueComparison) -> Self {
287        self.value_comparison = Some(value_comparison);
288        self
289    }
290
291    /// Return the key's old value (`GET`).
292    pub fn get(mut self, get: bool) -> Self {
293        self.get = get;
294        self
295    }
296
297    /// Set the expiry.
298    pub fn with_expiration(mut self, expiration: SetExpiry) -> Self {
299        self.expiration = Some(expiration);
300        self
301    }
302}
303
304impl ToValkeyArgs for SetOptions {
305    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
306        if let Some(ref existence_check) = self.conditional_set {
307            existence_check.write_valkey_args(out);
308        }
309        if let Some(ref value_comparison) = self.value_comparison {
310            value_comparison.write_valkey_args(out);
311        }
312        if self.get {
313            out.write_arg(b"GET");
314        }
315        if let Some(ref expiration) = self.expiration {
316            expiration.write_valkey_args(out);
317        }
318    }
319}
320
321/// The `LEFT`/`RIGHT` argument used by list commands (`LMOVE`, `LMPOP`, ...).
322///
323/// Mirrors redis-rs's `Direction` type.
324#[derive(Debug, Clone, Copy, PartialEq, Eq)]
325pub enum Direction {
326    /// The head of the list (`LEFT`).
327    Left,
328    /// The tail of the list (`RIGHT`).
329    Right,
330}
331
332impl ToValkeyArgs for Direction {
333    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
334        out.write_arg(match self {
335            Direction::Left => b"LEFT".as_slice(),
336            Direction::Right => b"RIGHT".as_slice(),
337        });
338    }
339}
340
341/// Expiry argument for `GETEX`/`HGETEX`.
342///
343/// Mirrors redis-rs's `Expiry` type.
344#[derive(Debug, Clone, Copy, PartialEq, Eq)]
345pub enum Expiry {
346    /// Set expiry, in seconds (`EX`).
347    EX(u64),
348    /// Set expiry, in milliseconds (`PX`).
349    PX(u64),
350    /// Set expiry at a Unix time, in seconds (`EXAT`).
351    EXAT(u64),
352    /// Set expiry at a Unix time, in milliseconds (`PXAT`).
353    PXAT(u64),
354    /// Remove the time to live (`PERSIST`).
355    PERSIST,
356}
357
358impl ToValkeyArgs for Expiry {
359    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
360        let mut kw = |k: &[u8], v: u64| {
361            out.write_arg(k);
362            out.write_arg_fmt(v);
363        };
364        match self {
365            Expiry::EX(sec) => kw(b"EX", *sec),
366            Expiry::PX(ms) => kw(b"PX", *ms),
367            Expiry::EXAT(ts) => kw(b"EXAT", *ts),
368            Expiry::PXAT(ts) => kw(b"PXAT", *ts),
369            Expiry::PERSIST => out.write_arg(b"PERSIST"),
370        }
371    }
372}
373
374/// Options for the `LPOS` command.
375///
376/// Mirrors redis-rs's `LposOptions` type.
377#[derive(Debug, Clone, Copy, Default)]
378pub struct LposOptions {
379    count: Option<usize>,
380    maxlen: Option<usize>,
381    rank: Option<isize>,
382}
383
384impl LposOptions {
385    /// Limit the results to the first `n` matches (`COUNT`).
386    pub fn count(mut self, n: usize) -> Self {
387        self.count = Some(n);
388        self
389    }
390
391    /// Return the `n`-th match (`RANK`).
392    pub fn rank(mut self, n: isize) -> Self {
393        self.rank = Some(n);
394        self
395    }
396
397    /// Limit the search to the first `n` list entries (`MAXLEN`).
398    pub fn maxlen(mut self, n: usize) -> Self {
399        self.maxlen = Some(n);
400        self
401    }
402}
403
404impl ToValkeyArgs for LposOptions {
405    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
406        if let Some(n) = self.count {
407            out.write_arg(b"COUNT");
408            out.write_arg_fmt(n);
409        }
410        if let Some(n) = self.rank {
411            out.write_arg(b"RANK");
412            out.write_arg_fmt(n);
413        }
414        if let Some(n) = self.maxlen {
415            out.write_arg(b"MAXLEN");
416            out.write_arg_fmt(n);
417        }
418    }
419}
420
421/// Policy for the `FUNCTION RESTORE` command.
422///
423/// Mirrors Python `FunctionRestorePolicy`.
424#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
425pub enum FunctionRestorePolicy {
426    /// Append to existing libraries, aborting on name collision (`APPEND`).
427    #[default]
428    Append,
429    /// Delete all existing libraries before restoring (`FLUSH`).
430    Flush,
431    /// Append, replacing existing libraries on name collision (`REPLACE`).
432    Replace,
433}
434
435impl FunctionRestorePolicy {
436    pub(crate) fn as_arg(&self) -> &'static str {
437        match self {
438            FunctionRestorePolicy::Append => "APPEND",
439            FunctionRestorePolicy::Flush => "FLUSH",
440            FunctionRestorePolicy::Replace => "REPLACE",
441        }
442    }
443}
444
445/// Mode for the `CLIENT PAUSE` command.
446///
447/// Mirrors Python `ClientPauseMode`.
448#[derive(Debug, Clone, Copy, PartialEq, Eq)]
449pub enum ClientPauseMode {
450    /// Pause all client commands (`ALL`).
451    All,
452    /// Pause only client write commands (`WRITE`).
453    Write,
454}
455
456impl ClientPauseMode {
457    pub(crate) fn as_arg(&self) -> &'static str {
458        match self {
459            ClientPauseMode::All => "ALL",
460            ClientPauseMode::Write => "WRITE",
461        }
462    }
463}
464
465/// Options for the `MIGRATE` command.
466///
467/// Mirrors Python `MigrateOptions`.
468#[derive(Clone, Default)]
469pub struct MigrateOptions {
470    /// Do not remove the key from the source instance (`COPY`).
471    pub copy: bool,
472    /// Replace an existing key on the destination (`REPLACE`).
473    pub replace: bool,
474    /// Password for `AUTH`, or with `username` for `AUTH2`.
475    pub password: Option<String>,
476    /// Username for `AUTH2` (requires `password`).
477    pub username: Option<String>,
478}
479
480impl std::fmt::Debug for MigrateOptions {
481    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
482        f.debug_struct("MigrateOptions")
483            .field("copy", &self.copy)
484            .field("replace", &self.replace)
485            .field("password", &self.password.as_ref().map(|_| "<redacted>"))
486            .field("username", &self.username)
487            .finish()
488    }
489}
490
491impl ToValkeyArgs for MigrateOptions {
492    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
493        if self.copy {
494            out.write_arg(b"COPY");
495        }
496        if self.replace {
497            out.write_arg(b"REPLACE");
498        }
499        match (&self.username, &self.password) {
500            (Some(username), Some(password)) => {
501                out.write_arg(b"AUTH2");
502                out.write_arg(username.as_bytes());
503                out.write_arg(password.as_bytes());
504            }
505            (None, Some(password)) => {
506                out.write_arg(b"AUTH");
507                out.write_arg(password.as_bytes());
508            }
509            _ => {}
510        }
511    }
512}
513
514/// Options for the `RESTORE` command.
515///
516/// Mirrors the option surface of Python's `restore(...)`.
517#[derive(Debug, Clone, Copy, Default)]
518pub struct RestoreOptions {
519    /// Replace the key if it already exists (`REPLACE`).
520    pub replace: bool,
521    /// Treat `ttl` as an absolute Unix timestamp in milliseconds (`ABSTTL`).
522    pub absttl: bool,
523    /// Set the key's idle time, in seconds (`IDLETIME`).
524    pub idletime: Option<i64>,
525    /// Set the key's access frequency (`FREQ`), for `LFU` eviction policies.
526    pub frequency: Option<i64>,
527}
528
529impl ToValkeyArgs for RestoreOptions {
530    fn write_valkey_args<W: ?Sized + ValkeyWrite>(&self, out: &mut W) {
531        if self.replace {
532            out.write_arg(b"REPLACE");
533        }
534        if self.absttl {
535            out.write_arg(b"ABSTTL");
536        }
537        if let Some(idletime) = self.idletime {
538            out.write_arg(b"IDLETIME");
539            out.write_arg_fmt(idletime);
540        }
541        if let Some(frequency) = self.frequency {
542            out.write_arg(b"FREQ");
543            out.write_arg_fmt(frequency);
544        }
545    }
546}
547
548/// Sort/scan ordering.
549///
550/// Mirrors Python `OrderBy`.
551#[derive(Debug, Clone, Copy, PartialEq, Eq)]
552pub enum OrderBy {
553    /// Ascending order (`ASC`).
554    Asc,
555    /// Descending order (`DESC`).
556    Desc,
557}
558
559impl OrderBy {
560    pub(crate) fn as_arg(&self) -> &'static str {
561        match self {
562            OrderBy::Asc => "ASC",
563            OrderBy::Desc => "DESC",
564        }
565    }
566}
567
568/// A `LIMIT offset count` clause.
569///
570/// Mirrors Python `Limit`.
571#[derive(Debug, Clone, Copy, PartialEq, Eq)]
572pub struct Limit {
573    /// The offset from the start of the result set.
574    pub offset: i64,
575    /// The maximum number of elements to include.
576    pub count: i64,
577}
578
579/// The type of a key, used by `OBJECT`/`TYPE`/`SCAN`.
580#[derive(Debug, Clone, Copy, PartialEq, Eq)]
581#[non_exhaustive]
582pub enum ObjectType {
583    /// String.
584    String,
585    /// List.
586    List,
587    /// Set.
588    Set,
589    /// Sorted set.
590    ZSet,
591    /// Hash.
592    Hash,
593    /// Stream.
594    Stream,
595}
596
597impl ObjectType {
598    /// Map to the vendored `redis` crate's `ObjectType` (used by cluster scan).
599    pub(crate) fn to_redis(self) -> redis::ObjectType {
600        match self {
601            ObjectType::String => redis::ObjectType::String,
602            ObjectType::List => redis::ObjectType::List,
603            ObjectType::Set => redis::ObjectType::Set,
604            ObjectType::ZSet => redis::ObjectType::ZSet,
605            ObjectType::Hash => redis::ObjectType::Hash,
606            ObjectType::Stream => redis::ObjectType::Stream,
607        }
608    }
609}
610
611#[cfg(test)]
612mod tests {
613    use super::*;
614    use crate::test_utils::assert_args;
615    use crate::test_utils::assert_args_empty;
616
617    #[test]
618    fn existence_check_args() {
619        assert_args(ExistenceCheck::NX, &["NX"]);
620        assert_args(ExistenceCheck::XX, &["XX"]);
621    }
622
623    #[test]
624    fn set_expiry_args() {
625        assert_args(SetExpiry::EX(60), &["EX", "60"]);
626        assert_args(SetExpiry::PX(1500), &["PX", "1500"]);
627        assert_args(SetExpiry::EXAT(100), &["EXAT", "100"]);
628        assert_args(SetExpiry::PXAT(200), &["PXAT", "200"]);
629        assert_args(SetExpiry::KEEPTTL, &["KEEPTTL"]);
630        assert_args(SetExpiry::PXAT(u64::MAX), &["PXAT", "18446744073709551615"]);
631    }
632
633    #[test]
634    fn direction_args() {
635        assert_args(Direction::Left, &["LEFT"]);
636        assert_args(Direction::Right, &["RIGHT"]);
637    }
638
639    #[test]
640    fn expiry_args() {
641        assert_args(Expiry::EX(60), &["EX", "60"]);
642        assert_args(Expiry::PX(1500), &["PX", "1500"]);
643        assert_args(Expiry::EXAT(100), &["EXAT", "100"]);
644        assert_args(Expiry::PXAT(200), &["PXAT", "200"]);
645        assert_args(Expiry::PERSIST, &["PERSIST"]);
646        assert_args(Expiry::EXAT(u64::MAX), &["EXAT", "18446744073709551615"]);
647    }
648
649    #[test]
650    fn lpos_options_args() {
651        assert_args_empty(LposOptions::default());
652        assert_args(
653            LposOptions::default().count(2).rank(-1).maxlen(100),
654            &["COUNT", "2", "RANK", "-1", "MAXLEN", "100"],
655        );
656    }
657
658    #[test]
659    fn set_options_args() {
660        assert_args_empty(SetOptions::default());
661        assert_args(
662            SetOptions::default()
663                .conditional_set(ExistenceCheck::NX)
664                .get(true)
665                .with_expiration(SetExpiry::EX(60)),
666            &["NX", "GET", "EX", "60"],
667        );
668        assert_args(
669            SetOptions::default()
670                .conditional_set(ExistenceCheck::XX)
671                .value_comparison(ValueComparison::ifeq("old"))
672                .get(true)
673                .with_expiration(SetExpiry::PX(1500)),
674            &["XX", "IFEQ", "old", "GET", "PX", "1500"],
675        );
676    }
677
678    #[test]
679    fn value_comparison_args() {
680        assert_args(ValueComparison::ifeq("v"), &["IFEQ", "v"]);
681        assert_args(
682            ValueComparison::IFEQ([0u8].to_vec()),
683            &[b"IFEQ".as_slice(), &[0u8]],
684        );
685
686        // TODO #7237: Add `IFNE` tests.
687    }
688
689    #[test]
690    fn expire_option_args() {
691        assert_args_empty(ExpireOption::NONE);
692        assert_args(ExpireOption::NX, &["NX"]);
693        assert_args(ExpireOption::XX, &["XX"]);
694        assert_args(ExpireOption::GT, &["GT"]);
695        assert_args(ExpireOption::LT, &["LT"]);
696    }
697
698    #[test]
699    fn hash_field_expiration_options_args() {
700        assert_args_empty(HashFieldExpirationOptions::default());
701        assert_args(
702            HashFieldExpirationOptions::default()
703                .set_existence_check(FieldExistenceCheck::FNX)
704                .set_expiration(SetExpiry::PX(1500)),
705            &["FNX", "PX", "1500"],
706        );
707        assert_args(
708            HashFieldExpirationOptions::default().set_existence_check(FieldExistenceCheck::FXX),
709            &["FXX"],
710        );
711    }
712
713    #[test]
714    fn copy_options_args() {
715        assert_args_empty(CopyOptions::default());
716        assert_args(CopyOptions::default().replace(true), &["REPLACE"]);
717        assert_args(
718            CopyOptions::default().db(2).replace(true),
719            &["DB", "2", "REPLACE"],
720        );
721    }
722
723    #[test]
724    fn flush_all_options_args() {
725        assert_args(FlushAllOptions::default(), &["ASYNC"]);
726        assert_args(FlushDbOptions::default().blocking(true), &["SYNC"]);
727        assert_args(FunctionFlushOptions::default().blocking(true), &["SYNC"]);
728    }
729
730    #[test]
731    fn order_by_args() {
732        assert_eq!(OrderBy::Asc.as_arg(), "ASC");
733        assert_eq!(OrderBy::Desc.as_arg(), "DESC");
734    }
735
736    #[test]
737    fn restore_options_args() {
738        assert_args_empty(RestoreOptions::default());
739        assert_args(
740            RestoreOptions {
741                replace: true,
742                absttl: true,
743                idletime: Some(100),
744                frequency: Some(5),
745            },
746            &["REPLACE", "ABSTTL", "IDLETIME", "100", "FREQ", "5"],
747        );
748    }
749
750    #[test]
751    fn migrate_options_args() {
752        assert_args_empty(MigrateOptions::default());
753        assert_args(
754            MigrateOptions {
755                copy: true,
756                replace: true,
757                password: Some("pw".into()),
758                username: None,
759            },
760            &["COPY", "REPLACE", "AUTH", "pw"],
761        );
762        assert_args(
763            MigrateOptions {
764                copy: false,
765                replace: false,
766                password: Some("pw".into()),
767                username: Some("user".into()),
768            },
769            &["AUTH2", "user", "pw"],
770        );
771    }
772
773    #[test]
774    fn migrate_options_debug_redacts_password() {
775        let opts = MigrateOptions {
776            copy: true,
777            replace: false,
778            password: Some("super-secret".into()),
779            username: Some("alice".into()),
780        };
781
782        let redacted_str = format!("{opts:?}");
783        assert!(!redacted_str.contains("super-secret"));
784        assert!(redacted_str.contains("<redacted>"));
785
786        let unredacted_str = format!("{:?}", MigrateOptions::default());
787        assert!(!unredacted_str.contains("<redacted>"));
788    }
789
790    #[test]
791    fn client_pause_mode_args() {
792        assert_eq!(ClientPauseMode::All.as_arg(), "ALL");
793        assert_eq!(ClientPauseMode::Write.as_arg(), "WRITE");
794    }
795
796    #[test]
797    fn function_restore_policy_args() {
798        assert_eq!(FunctionRestorePolicy::Append.as_arg(), "APPEND");
799        assert_eq!(FunctionRestorePolicy::Flush.as_arg(), "FLUSH");
800        assert_eq!(FunctionRestorePolicy::Replace.as_arg(), "REPLACE");
801    }
802}