Skip to main content

dynomite/proto/redis/
commands.rs

1//! Redis command catalog and classification helpers.
2//!
3//! This module centralises the command lookup the parser uses
4//! after it has read the keyword token, plus the predicate set the
5//! state machine consults to decide how many arguments a command
6//! takes. The classification mirrors the reference engine's
7//! `redis_arg{0,1,2,3,n,x,kvx,upto1,eval,argz,error}` helpers.
8//!
9//! All lookups are case-insensitive ASCII.
10
11// Both [`classify`] and [`lookup`] dispatch on dozens of arms; the
12// parity walk requires that the variants stay in this single
13// function. The match-same-arm and match-wildcard-arm warnings
14// arise from the long `match` blocks; suppressing them keeps the
15// per-command rows aligned with the reference engine's switch
16// statements.
17#![allow(clippy::too_many_lines)]
18#![allow(clippy::match_same_arms)]
19
20use crate::msg::MsgType;
21
22/// Argument-shape classification for a Redis request command.
23#[derive(Copy, Clone, Debug, Eq, PartialEq)]
24pub enum CommandClass {
25    /// Command takes no key (`PING`, `QUIT`, `SCRIPT FLUSH`, ...).
26    Argz,
27    /// Command takes exactly one key and zero arguments.
28    Arg0,
29    /// Command takes one key and exactly one argument.
30    Arg1,
31    /// Command takes one key and exactly two arguments.
32    Arg2,
33    /// Command takes one key and exactly three arguments.
34    Arg3,
35    /// Command takes one key and zero-or-more arguments (variadic).
36    ArgN,
37    /// Command takes one or more keys (`MGET`, `DEL`, `EXISTS`).
38    ArgX,
39    /// Command takes one or more key/value pairs (`MSET`).
40    ArgKvX,
41    /// Command takes one key and zero or one argument (`INFO`).
42    ArgUpto1,
43    /// Command is `EVAL` or `EVALSHA` (special two-arg + key list +
44    /// args layout).
45    ArgEval,
46}
47
48impl CommandClass {
49    /// True when the command takes no key.
50    #[must_use]
51    pub fn is_argz(self) -> bool {
52        matches!(self, Self::Argz)
53    }
54
55    /// True when the command is variadic over keys.
56    #[must_use]
57    pub fn is_argx(self) -> bool {
58        matches!(self, Self::ArgX)
59    }
60
61    /// True when the command is variadic over key/value pairs.
62    #[must_use]
63    pub fn is_argkvx(self) -> bool {
64        matches!(self, Self::ArgKvX)
65    }
66
67    /// True when the command is `EVAL` or `EVALSHA`.
68    #[must_use]
69    pub fn is_argeval(self) -> bool {
70        matches!(self, Self::ArgEval)
71    }
72}
73
74/// Classify a Redis request type into its argument-shape category.
75///
76/// # Examples
77///
78/// ```
79/// use dynomite::msg::MsgType;
80/// use dynomite::proto::redis::commands::{classify, CommandClass};
81///
82/// assert_eq!(classify(MsgType::ReqRedisGet), CommandClass::Arg0);
83/// assert_eq!(classify(MsgType::ReqRedisSet), CommandClass::ArgN);
84/// assert_eq!(classify(MsgType::ReqRedisMget), CommandClass::ArgX);
85/// assert_eq!(classify(MsgType::ReqRedisMset), CommandClass::ArgKvX);
86/// assert_eq!(classify(MsgType::ReqRedisEval), CommandClass::ArgEval);
87/// assert_eq!(classify(MsgType::ReqRedisPing), CommandClass::Argz);
88/// ```
89#[must_use]
90pub fn classify(ty: MsgType) -> CommandClass {
91    use MsgType as M;
92    match ty {
93        // argz
94        M::ReqRedisPing | M::ReqRedisQuit | M::ReqRedisScriptFlush | M::ReqRedisScriptKill => {
95            CommandClass::Argz
96        }
97
98        // arg0
99        M::ReqRedisPersist
100        | M::ReqRedisPttl
101        | M::ReqRedisTtl
102        | M::ReqRedisType
103        | M::ReqRedisDump
104        | M::ReqRedisDecr
105        | M::ReqRedisGet
106        | M::ReqRedisIncr
107        | M::ReqRedisStrlen
108        | M::ReqRedisHgetall
109        | M::ReqRedisHkeys
110        | M::ReqRedisHlen
111        | M::ReqRedisHvals
112        | M::ReqRedisLlen
113        | M::ReqRedisLpop
114        | M::ReqRedisRpop
115        | M::ReqRedisScard
116        | M::ReqRedisSmembers
117        | M::ReqRedisSrandmember
118        | M::ReqRedisZcard
119        | M::ReqRedisKeys
120        | M::ReqRedisPfcount => CommandClass::Arg0,
121
122        // arg1
123        M::ReqRedisExpire
124        | M::ReqRedisExpireat
125        | M::ReqRedisPexpire
126        | M::ReqRedisPexpireat
127        | M::ReqRedisAppend
128        | M::ReqRedisDecrby
129        | M::ReqRedisGetbit
130        | M::ReqRedisGetset
131        | M::ReqRedisIncrby
132        | M::ReqRedisIncrbyfloat
133        | M::ReqRedisSetnx
134        | M::ReqRedisHexists
135        | M::ReqRedisHget
136        | M::ReqRedisHstrlen
137        | M::ReqRedisLindex
138        | M::ReqRedisLpushx
139        | M::ReqRedisRpoplpush
140        | M::ReqRedisRpushx
141        | M::ReqRedisSismember
142        | M::ReqRedisZrank
143        | M::ReqRedisZrevrank
144        | M::ReqRedisZscore
145        | M::ReqRedisSlaveof
146        | M::ReqRedisConfig
147        | M::ReqRedisScriptLoad
148        | M::ReqRedisScriptExists => CommandClass::Arg1,
149
150        // arg2
151        M::ReqRedisGetrange
152        | M::ReqRedisPsetex
153        | M::ReqRedisSetbit
154        | M::ReqRedisSetex
155        | M::ReqRedisSetrange
156        | M::ReqRedisHincrby
157        | M::ReqRedisHincrbyfloat
158        | M::ReqRedisHsetnx
159        | M::ReqRedisLrange
160        | M::ReqRedisLrem
161        | M::ReqRedisLset
162        | M::ReqRedisLtrim
163        | M::ReqRedisSmove
164        | M::ReqRedisZcount
165        | M::ReqRedisZincrby
166        | M::ReqRedisZlexcount
167        | M::ReqRedisZremrangebylex
168        | M::ReqRedisZremrangebyrank
169        | M::ReqRedisZremrangebyscore
170        | M::ReqRedisRestore => CommandClass::Arg2,
171
172        // arg3
173        M::ReqRedisLinsert => CommandClass::Arg3,
174
175        // argn
176        M::ReqRedisSort
177        | M::ReqRedisBitcount
178        | M::ReqRedisBitpos
179        | M::ReqRedisSet
180        | M::ReqRedisScan
181        | M::ReqRedisHdel
182        | M::ReqRedisHmget
183        | M::ReqRedisHmset
184        | M::ReqRedisHscan
185        | M::ReqRedisHset
186        | M::ReqRedisLpush
187        | M::ReqRedisRpush
188        | M::ReqRedisSadd
189        | M::ReqRedisSdiff
190        | M::ReqRedisSdiffstore
191        | M::ReqRedisSinter
192        | M::ReqRedisSinterstore
193        | M::ReqRedisSrem
194        | M::ReqRedisSunion
195        | M::ReqRedisSunionstore
196        | M::ReqRedisSscan
197        | M::ReqRedisSpop
198        | M::ReqRedisZadd
199        | M::ReqRedisZinterstore
200        | M::ReqRedisZrange
201        | M::ReqRedisZrangebyscore
202        | M::ReqRedisZrem
203        | M::ReqRedisZrevrange
204        | M::ReqRedisZrangebylex
205        | M::ReqRedisZrevrangebylex
206        | M::ReqRedisZrevrangebyscore
207        | M::ReqRedisZunionstore
208        | M::ReqRedisZscan
209        | M::ReqRedisPfadd
210        | M::ReqRedisGeoadd
211        | M::ReqRedisGeoradius
212        | M::ReqRedisGeodist
213        | M::ReqRedisGeohash
214        | M::ReqRedisGeopos
215        | M::ReqRedisGeoradiusbymember
216        | M::ReqRedisJsonset
217        | M::ReqRedisJsonget
218        | M::ReqRedisJsondel
219        | M::ReqRedisJsontype
220        | M::ReqRedisJsonmget
221        | M::ReqRedisJsonarrappend
222        | M::ReqRedisJsonarrinsert
223        | M::ReqRedisJsonarrlen
224        | M::ReqRedisJsonobjkeys
225        | M::ReqRedisJsonobjlen
226        | M::ReqRedisUnlink => CommandClass::ArgN,
227
228        // argx (multi-key)
229        M::ReqRedisMget | M::ReqRedisDel | M::ReqRedisExists => CommandClass::ArgX,
230
231        // argkvx (multi key/value)
232        M::ReqRedisMset => CommandClass::ArgKvX,
233
234        // argupto1
235        M::ReqRedisInfo => CommandClass::ArgUpto1,
236
237        // argeval
238        M::ReqRedisEval | M::ReqRedisEvalsha => CommandClass::ArgEval,
239
240        // RediSearch FT.* commands. CREATE / SEARCH / DROPINDEX
241        // take the index name as the lone key and a variadic
242        // payload; INFO takes only the index name; LIST takes
243        // no key. FT.REGEX (Dynomite extension) takes the
244        // index name as the lone key and a variadic payload.
245        M::ReqRedisFtCreate | M::ReqRedisFtSearch | M::ReqRedisFtDropindex | M::ReqRedisFtRegex => {
246            CommandClass::ArgN
247        }
248        M::ReqRedisFtInfo => CommandClass::Arg0,
249        M::ReqRedisFtList => CommandClass::Argz,
250        // Unknown FT.* keywords surface as a generic variadic
251        // command so the parser collects every token into the
252        // arg vector for the dispatcher's intercept; the
253        // intercept then synthesises a `-ERR not supported`
254        // reply.
255        M::ReqRedisFtUnknown => CommandClass::ArgN,
256
257        _ => CommandClass::Arg0,
258    }
259}
260
261/// True when `ty` is a Redis error response variant.
262///
263/// # Examples
264///
265/// ```
266/// use dynomite::msg::MsgType;
267/// use dynomite::proto::redis::commands::is_redis_error;
268///
269/// assert!(is_redis_error(MsgType::RspRedisErrorErr));
270/// assert!(!is_redis_error(MsgType::RspRedisStatus));
271/// ```
272#[must_use]
273pub fn is_redis_error(ty: MsgType) -> bool {
274    use MsgType as M;
275    matches!(
276        ty,
277        M::RspRedisError
278            | M::RspRedisErrorErr
279            | M::RspRedisErrorOom
280            | M::RspRedisErrorBusy
281            | M::RspRedisErrorNoauth
282            | M::RspRedisErrorLoading
283            | M::RspRedisErrorBusykey
284            | M::RspRedisErrorMisconf
285            | M::RspRedisErrorNoscript
286            | M::RspRedisErrorReadonly
287            | M::RspRedisErrorWrongtype
288            | M::RspRedisErrorExecabort
289            | M::RspRedisErrorMasterdown
290            | M::RspRedisErrorNoreplicas
291    )
292}
293
294/// Routing override the parser stamps on a request based on its
295/// command type. `None` means the default `Normal` routing.
296#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
297pub struct CommandTraits {
298    /// True when the command is a read.
299    pub is_read: bool,
300    /// True when the command sets the `quit` flag.
301    pub quit: bool,
302    /// Routing override class, if any.
303    pub routing: RoutingOverride,
304}
305
306/// Routing override stamped on a parsed request.
307#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
308pub enum RoutingOverride {
309    /// No override; use the configured per-pool routing.
310    #[default]
311    None,
312    /// Send to the local node only.
313    LocalNodeOnly,
314    /// Apply key hashing but stay within the local rack.
315    TokenOwnerLocalRackOnly,
316    /// Send to all nodes / racks / DCs.
317    AllNodesAllRacksAllDcs,
318}
319
320/// Lookup a Redis command keyword (case-insensitive ASCII) and
321/// return the message type plus its parser traits.
322///
323/// Returns `None` when the keyword is not a known command.
324///
325/// # Examples
326///
327/// ```
328/// use dynomite::msg::MsgType;
329/// use dynomite::proto::redis::commands::lookup;
330///
331/// let (ty, traits) = lookup(b"GET").unwrap();
332/// assert_eq!(ty, MsgType::ReqRedisGet);
333/// assert!(traits.is_read);
334/// ```
335#[must_use]
336pub fn lookup(keyword: &[u8]) -> Option<(MsgType, CommandTraits)> {
337    if keyword.is_empty() || keyword.len() > 32 {
338        return None;
339    }
340    let mut buf = [0u8; 32];
341    for (i, &b) in keyword.iter().enumerate() {
342        buf[i] = b.to_ascii_lowercase();
343    }
344    let key = &buf[..keyword.len()];
345    // RediSearch FT.* surface. Recognised keywords map to
346    // their typed `ReqRedisFt*` variants below; any other FT.*
347    // keyword is stamped as `ReqRedisFtUnknown` so it lands in
348    // the dispatcher's vector-registry intercept and surfaces
349    // a structured `-ERR` rather than being reflected at the
350    // backend (which would return its own opaque `unknown
351    // command` reply).
352    if key.starts_with(b"ft.")
353        && !matches!(
354            key,
355            b"ft.create"
356                | b"ft.search"
357                | b"ft.info"
358                | b"ft.list"
359                | b"ft._list"
360                | b"ft.dropindex"
361                | b"ft.regex"
362        )
363    {
364        return Some((
365            MsgType::ReqRedisFtUnknown,
366            CommandTraits {
367                is_read: true,
368                quit: false,
369                routing: RoutingOverride::LocalNodeOnly,
370            },
371        ));
372    }
373    let (ty, is_read, quit, routing) = match key {
374        // length 3
375        b"get" => (MsgType::ReqRedisGet, true, false, RoutingOverride::None),
376        b"set" => (MsgType::ReqRedisSet, false, false, RoutingOverride::None),
377        b"ttl" => (MsgType::ReqRedisTtl, false, false, RoutingOverride::None),
378        b"del" => (MsgType::ReqRedisDel, false, false, RoutingOverride::None),
379        // length 4
380        b"pttl" => (MsgType::ReqRedisPttl, true, false, RoutingOverride::None),
381        b"decr" => (MsgType::ReqRedisDecr, false, false, RoutingOverride::None),
382        b"dump" => (MsgType::ReqRedisDump, true, false, RoutingOverride::None),
383        b"hdel" => (MsgType::ReqRedisHdel, false, false, RoutingOverride::None),
384        b"hget" => (MsgType::ReqRedisHget, true, false, RoutingOverride::None),
385        b"hlen" => (MsgType::ReqRedisHlen, true, false, RoutingOverride::None),
386        b"hset" => (MsgType::ReqRedisHset, false, false, RoutingOverride::None),
387        b"incr" => (MsgType::ReqRedisIncr, false, false, RoutingOverride::None),
388        b"keys" => (
389            MsgType::ReqRedisKeys,
390            true,
391            false,
392            RoutingOverride::LocalNodeOnly,
393        ),
394        b"info" => (
395            MsgType::ReqRedisInfo,
396            true,
397            false,
398            RoutingOverride::LocalNodeOnly,
399        ),
400        b"llen" => (MsgType::ReqRedisLlen, true, false, RoutingOverride::None),
401        b"lpop" => (MsgType::ReqRedisLpop, false, false, RoutingOverride::None),
402        b"lrem" => (MsgType::ReqRedisLrem, false, false, RoutingOverride::None),
403        b"lset" => (MsgType::ReqRedisLset, false, false, RoutingOverride::None),
404        b"mget" => (MsgType::ReqRedisMget, true, false, RoutingOverride::None),
405        b"mset" => (MsgType::ReqRedisMset, false, false, RoutingOverride::None),
406        b"ping" => (
407            MsgType::ReqRedisPing,
408            true,
409            false,
410            RoutingOverride::LocalNodeOnly,
411        ),
412        b"rpop" => (MsgType::ReqRedisRpop, false, false, RoutingOverride::None),
413        b"sadd" => (MsgType::ReqRedisSadd, false, false, RoutingOverride::None),
414        b"scan" => (
415            MsgType::ReqRedisScan,
416            true,
417            false,
418            RoutingOverride::LocalNodeOnly,
419        ),
420        b"spop" => (MsgType::ReqRedisSpop, false, false, RoutingOverride::None),
421        b"srem" => (MsgType::ReqRedisSrem, false, false, RoutingOverride::None),
422        b"type" => (MsgType::ReqRedisType, true, false, RoutingOverride::None),
423        b"zadd" => (MsgType::ReqRedisZadd, false, false, RoutingOverride::None),
424        b"zrem" => (MsgType::ReqRedisZrem, false, false, RoutingOverride::None),
425        b"eval" => (MsgType::ReqRedisEval, false, false, RoutingOverride::None),
426        b"sort" => (MsgType::ReqRedisSort, true, false, RoutingOverride::None),
427        b"quit" => (MsgType::ReqRedisQuit, false, true, RoutingOverride::None),
428        b"load" => (
429            MsgType::ReqRedisScriptLoad,
430            false,
431            false,
432            RoutingOverride::AllNodesAllRacksAllDcs,
433        ),
434        b"kill" => (
435            MsgType::ReqRedisScriptKill,
436            false,
437            false,
438            RoutingOverride::AllNodesAllRacksAllDcs,
439        ),
440        // length 5
441        b"hkeys" => (
442            MsgType::ReqRedisHkeys,
443            true,
444            false,
445            RoutingOverride::TokenOwnerLocalRackOnly,
446        ),
447        b"hmget" => (MsgType::ReqRedisHmget, true, false, RoutingOverride::None),
448        b"hmset" => (MsgType::ReqRedisHmset, false, false, RoutingOverride::None),
449        b"hvals" => (
450            MsgType::ReqRedisHvals,
451            true,
452            false,
453            RoutingOverride::TokenOwnerLocalRackOnly,
454        ),
455        b"hscan" => (
456            MsgType::ReqRedisHscan,
457            true,
458            false,
459            RoutingOverride::TokenOwnerLocalRackOnly,
460        ),
461        b"lpush" => (MsgType::ReqRedisLpush, false, false, RoutingOverride::None),
462        b"ltrim" => (MsgType::ReqRedisLtrim, false, false, RoutingOverride::None),
463        b"rpush" => (MsgType::ReqRedisRpush, false, false, RoutingOverride::None),
464        b"scard" => (MsgType::ReqRedisScard, true, false, RoutingOverride::None),
465        b"sdiff" => (MsgType::ReqRedisSdiff, true, false, RoutingOverride::None),
466        b"setex" => (MsgType::ReqRedisSetex, false, false, RoutingOverride::None),
467        b"setnx" => (MsgType::ReqRedisSetnx, false, false, RoutingOverride::None),
468        b"smove" => (MsgType::ReqRedisSmove, false, false, RoutingOverride::None),
469        b"sscan" => (
470            MsgType::ReqRedisSscan,
471            true,
472            false,
473            RoutingOverride::TokenOwnerLocalRackOnly,
474        ),
475        b"zcard" => (MsgType::ReqRedisZcard, true, false, RoutingOverride::None),
476        b"zrank" => (MsgType::ReqRedisZrank, true, false, RoutingOverride::None),
477        b"zscan" => (
478            MsgType::ReqRedisZscan,
479            true,
480            false,
481            RoutingOverride::TokenOwnerLocalRackOnly,
482        ),
483        b"pfadd" => (MsgType::ReqRedisPfadd, false, false, RoutingOverride::None),
484        b"flush" => (
485            MsgType::ReqRedisScriptFlush,
486            false,
487            false,
488            RoutingOverride::AllNodesAllRacksAllDcs,
489        ),
490        // length 6
491        b"append" => (MsgType::ReqRedisAppend, false, false, RoutingOverride::None),
492        b"decrby" => (MsgType::ReqRedisDecrby, false, false, RoutingOverride::None),
493        b"exists" => (MsgType::ReqRedisExists, true, false, RoutingOverride::None),
494        b"expire" => (MsgType::ReqRedisExpire, false, false, RoutingOverride::None),
495        b"getbit" => (MsgType::ReqRedisGetbit, true, false, RoutingOverride::None),
496        b"getset" => (MsgType::ReqRedisGetset, false, false, RoutingOverride::None),
497        b"psetex" => (MsgType::ReqRedisPsetex, false, false, RoutingOverride::None),
498        b"hsetnx" => (MsgType::ReqRedisHsetnx, false, false, RoutingOverride::None),
499        b"incrby" => (MsgType::ReqRedisIncrby, false, false, RoutingOverride::None),
500        b"lindex" => (MsgType::ReqRedisLindex, true, false, RoutingOverride::None),
501        b"lpushx" => (MsgType::ReqRedisLpushx, false, false, RoutingOverride::None),
502        b"lrange" => (MsgType::ReqRedisLrange, true, false, RoutingOverride::None),
503        b"rpushx" => (MsgType::ReqRedisRpushx, false, false, RoutingOverride::None),
504        b"setbit" => (MsgType::ReqRedisSetbit, false, false, RoutingOverride::None),
505        b"sinter" => (MsgType::ReqRedisSinter, true, false, RoutingOverride::None),
506        b"strlen" => (MsgType::ReqRedisStrlen, true, false, RoutingOverride::None),
507        b"sunion" => (MsgType::ReqRedisSunion, true, false, RoutingOverride::None),
508        b"zcount" => (MsgType::ReqRedisZcount, true, false, RoutingOverride::None),
509        b"zrange" => (MsgType::ReqRedisZrange, true, false, RoutingOverride::None),
510        b"zscore" => (MsgType::ReqRedisZscore, true, false, RoutingOverride::None),
511        b"config" => (
512            MsgType::ReqRedisConfig,
513            true,
514            false,
515            RoutingOverride::LocalNodeOnly,
516        ),
517        b"geoadd" => (MsgType::ReqRedisGeoadd, false, false, RoutingOverride::None),
518        b"geopos" => (MsgType::ReqRedisGeopos, true, false, RoutingOverride::None),
519        b"unlink" => (MsgType::ReqRedisUnlink, false, false, RoutingOverride::None),
520        b"script" => (MsgType::ReqRedisScript, false, false, RoutingOverride::None),
521        b"bitpos" => (MsgType::ReqRedisBitpos, true, false, RoutingOverride::None),
522        // length 7
523        b"persist" => (
524            MsgType::ReqRedisPersist,
525            false,
526            false,
527            RoutingOverride::None,
528        ),
529        b"pexpire" => (
530            MsgType::ReqRedisPexpire,
531            false,
532            false,
533            RoutingOverride::None,
534        ),
535        b"hexists" => (MsgType::ReqRedisHexists, true, false, RoutingOverride::None),
536        b"hgetall" => (
537            MsgType::ReqRedisHgetall,
538            true,
539            false,
540            RoutingOverride::TokenOwnerLocalRackOnly,
541        ),
542        b"hincrby" => (
543            MsgType::ReqRedisHincrby,
544            false,
545            false,
546            RoutingOverride::None,
547        ),
548        b"linsert" => (
549            MsgType::ReqRedisLinsert,
550            false,
551            false,
552            RoutingOverride::None,
553        ),
554        b"zincrby" => (
555            MsgType::ReqRedisZincrby,
556            false,
557            false,
558            RoutingOverride::None,
559        ),
560        b"evalsha" => (
561            MsgType::ReqRedisEvalsha,
562            false,
563            false,
564            RoutingOverride::None,
565        ),
566        b"restore" => (
567            MsgType::ReqRedisRestore,
568            false,
569            false,
570            RoutingOverride::None,
571        ),
572        b"slaveof" => (
573            MsgType::ReqRedisSlaveof,
574            false,
575            false,
576            RoutingOverride::LocalNodeOnly,
577        ),
578        b"pfcount" => (
579            MsgType::ReqRedisPfcount,
580            false,
581            false,
582            RoutingOverride::None,
583        ),
584        b"geohash" => (MsgType::ReqRedisGeohash, true, false, RoutingOverride::None),
585        b"geodist" => (MsgType::ReqRedisGeodist, true, false, RoutingOverride::None),
586        b"hstrlen" => (MsgType::ReqRedisHstrlen, true, false, RoutingOverride::None),
587        // length 8
588        b"expireat" => (
589            MsgType::ReqRedisExpireat,
590            false,
591            false,
592            RoutingOverride::None,
593        ),
594        b"bitcount" => (
595            MsgType::ReqRedisBitcount,
596            true,
597            false,
598            RoutingOverride::None,
599        ),
600        b"getrange" => (
601            MsgType::ReqRedisGetrange,
602            true,
603            false,
604            RoutingOverride::None,
605        ),
606        b"setrange" => (
607            MsgType::ReqRedisSetrange,
608            false,
609            false,
610            RoutingOverride::None,
611        ),
612        b"smembers" => (
613            MsgType::ReqRedisSmembers,
614            true,
615            false,
616            RoutingOverride::None,
617        ),
618        b"zrevrank" => (
619            MsgType::ReqRedisZrevrank,
620            true,
621            false,
622            RoutingOverride::None,
623        ),
624        b"json.set" => (
625            MsgType::ReqRedisJsonset,
626            false,
627            false,
628            RoutingOverride::None,
629        ),
630        b"json.get" => (MsgType::ReqRedisJsonget, true, false, RoutingOverride::None),
631        b"json.del" => (
632            MsgType::ReqRedisJsondel,
633            false,
634            false,
635            RoutingOverride::None,
636        ),
637        // length 9
638        b"pexpireat" => (
639            MsgType::ReqRedisPexpireat,
640            false,
641            false,
642            RoutingOverride::None,
643        ),
644        b"rpoplpush" => (
645            MsgType::ReqRedisRpoplpush,
646            false,
647            false,
648            RoutingOverride::None,
649        ),
650        b"sismember" => (
651            MsgType::ReqRedisSismember,
652            true,
653            false,
654            RoutingOverride::None,
655        ),
656        b"zlexcount" => (
657            MsgType::ReqRedisZlexcount,
658            true,
659            false,
660            RoutingOverride::None,
661        ),
662        b"zrevrange" => (
663            MsgType::ReqRedisZrevrange,
664            true,
665            false,
666            RoutingOverride::None,
667        ),
668        b"georadius" => (
669            MsgType::ReqRedisGeoradius,
670            true,
671            false,
672            RoutingOverride::None,
673        ),
674        b"json.type" => (
675            MsgType::ReqRedisJsontype,
676            true,
677            false,
678            RoutingOverride::None,
679        ),
680        b"json.mget" => (
681            MsgType::ReqRedisJsonmget,
682            true,
683            false,
684            RoutingOverride::None,
685        ),
686        // length 10
687        b"sdiffstore" => (
688            MsgType::ReqRedisSdiffstore,
689            false,
690            false,
691            RoutingOverride::None,
692        ),
693        // length 11
694        b"incrbyfloat" => (
695            MsgType::ReqRedisIncrbyfloat,
696            false,
697            false,
698            RoutingOverride::None,
699        ),
700        b"sinterstore" => (
701            MsgType::ReqRedisSinterstore,
702            false,
703            false,
704            RoutingOverride::None,
705        ),
706        b"srandmember" => (
707            MsgType::ReqRedisSrandmember,
708            true,
709            false,
710            RoutingOverride::None,
711        ),
712        b"sunionstore" => (
713            MsgType::ReqRedisSunionstore,
714            true,
715            false,
716            RoutingOverride::None,
717        ),
718        b"zinterstore" => (
719            MsgType::ReqRedisZinterstore,
720            true,
721            false,
722            RoutingOverride::None,
723        ),
724        b"zrangebylex" => (
725            MsgType::ReqRedisZrangebylex,
726            true,
727            false,
728            RoutingOverride::None,
729        ),
730        b"zunionstore" => (
731            MsgType::ReqRedisZunionstore,
732            true,
733            false,
734            RoutingOverride::None,
735        ),
736        b"json.arrlen" => (
737            MsgType::ReqRedisJsonarrlen,
738            true,
739            false,
740            RoutingOverride::None,
741        ),
742        b"json.objlen" => (
743            MsgType::ReqRedisJsonobjlen,
744            true,
745            false,
746            RoutingOverride::None,
747        ),
748        // length 12
749        b"hincrbyfloat" => (
750            MsgType::ReqRedisHincrbyfloat,
751            false,
752            false,
753            RoutingOverride::None,
754        ),
755        b"json.objkeys" => (
756            MsgType::ReqRedisJsonobjkeys,
757            true,
758            false,
759            RoutingOverride::None,
760        ),
761        // length 13
762        b"zrangebyscore" => (
763            MsgType::ReqRedisZrangebyscore,
764            true,
765            false,
766            RoutingOverride::None,
767        ),
768        // length 14
769        b"zremrangebylex" => (
770            MsgType::ReqRedisZremrangebylex,
771            false,
772            false,
773            RoutingOverride::None,
774        ),
775        b"zrevrangebylex" => (
776            MsgType::ReqRedisZrevrangebylex,
777            true,
778            false,
779            RoutingOverride::None,
780        ),
781        b"json.arrappend" => (
782            MsgType::ReqRedisJsonarrappend,
783            false,
784            false,
785            RoutingOverride::None,
786        ),
787        b"json.arrinsert" => (
788            MsgType::ReqRedisJsonarrinsert,
789            false,
790            false,
791            RoutingOverride::None,
792        ),
793        // length 15
794        b"zremrangebyrank" => (
795            MsgType::ReqRedisZremrangebyrank,
796            false,
797            false,
798            RoutingOverride::None,
799        ),
800        // length 16
801        b"zremrangebyscore" => (
802            MsgType::ReqRedisZremrangebyscore,
803            false,
804            false,
805            RoutingOverride::None,
806        ),
807        b"zrevrangebyscore" => (
808            MsgType::ReqRedisZrevrangebyscore,
809            true,
810            false,
811            RoutingOverride::None,
812        ),
813        // length 17
814        b"georadiusbymember" => (
815            MsgType::ReqRedisGeoradiusbymember,
816            true,
817            false,
818            RoutingOverride::None,
819        ),
820        // RediSearch FT.* surface. The index name is carried as
821        // the first key (or there is no key for FT.LIST), so the
822        // standard parser key/arg machinery picks them up
823        // directly.
824        b"ft.create" => (
825            MsgType::ReqRedisFtCreate,
826            false,
827            false,
828            RoutingOverride::LocalNodeOnly,
829        ),
830        b"ft.search" => (
831            MsgType::ReqRedisFtSearch,
832            true,
833            false,
834            RoutingOverride::LocalNodeOnly,
835        ),
836        b"ft.info" => (
837            MsgType::ReqRedisFtInfo,
838            true,
839            false,
840            RoutingOverride::LocalNodeOnly,
841        ),
842        b"ft.list" | b"ft._list" => (
843            MsgType::ReqRedisFtList,
844            true,
845            false,
846            RoutingOverride::LocalNodeOnly,
847        ),
848        b"ft.dropindex" => (
849            MsgType::ReqRedisFtDropindex,
850            false,
851            false,
852            RoutingOverride::LocalNodeOnly,
853        ),
854        // FT.REGEX is a Dynomite extension (not standard
855        // RediSearch) for exact and approximate-regex search
856        // over `TEXT`-typed schema fields. Routed exactly like
857        // FT.SEARCH: index name carried as the lone key, the
858        // rest goes through the variadic arg vector.
859        b"ft.regex" => (
860            MsgType::ReqRedisFtRegex,
861            true,
862            false,
863            RoutingOverride::LocalNodeOnly,
864        ),
865        // length 28: dynomite config
866        b"dyno_config:conn_consistency" => (
867            MsgType::HackSettingConnConsistency,
868            false,
869            false,
870            RoutingOverride::None,
871        ),
872        _ => return None,
873    };
874    Some((
875        ty,
876        CommandTraits {
877            is_read,
878            quit,
879            routing,
880        },
881    ))
882}
883
884/// Look up a Redis error response keyword (case-sensitive: error
885/// keywords are uppercase on the wire) and return the
886/// corresponding [`MsgType`].
887///
888/// # Examples
889///
890/// ```
891/// use dynomite::msg::MsgType;
892/// use dynomite::proto::redis::commands::error_lookup;
893///
894/// assert_eq!(error_lookup(b"-ERR"), Some(MsgType::RspRedisErrorErr));
895/// assert_eq!(error_lookup(b"-WRONGTYPE"), Some(MsgType::RspRedisErrorWrongtype));
896/// ```
897#[must_use]
898pub fn error_lookup(token: &[u8]) -> Option<MsgType> {
899    match token {
900        b"-ERR" => Some(MsgType::RspRedisErrorErr),
901        b"-OOM" => Some(MsgType::RspRedisErrorOom),
902        b"-BUSY" => Some(MsgType::RspRedisErrorBusy),
903        b"-NOAUTH" => Some(MsgType::RspRedisErrorNoauth),
904        b"-LOADING" => Some(MsgType::RspRedisErrorLoading),
905        b"-BUSYKEY" => Some(MsgType::RspRedisErrorBusykey),
906        b"-MISCONF" => Some(MsgType::RspRedisErrorMisconf),
907        b"-NOSCRIPT" => Some(MsgType::RspRedisErrorNoscript),
908        b"-READONLY" => Some(MsgType::RspRedisErrorReadonly),
909        b"-WRONGTYPE" => Some(MsgType::RspRedisErrorWrongtype),
910        b"-EXECABORT" => Some(MsgType::RspRedisErrorExecabort),
911        b"-MASTERDOWN" => Some(MsgType::RspRedisErrorMasterdown),
912        b"-NOREPLICAS" => Some(MsgType::RspRedisErrorNoreplicas),
913        _ => None,
914    }
915}
916
917#[cfg(test)]
918mod tests {
919    use super::*;
920
921    #[test]
922    fn lookup_get_set() {
923        let (ty, t) = lookup(b"GET").unwrap();
924        assert_eq!(ty, MsgType::ReqRedisGet);
925        assert!(t.is_read);
926        let (ty, t) = lookup(b"set").unwrap();
927        assert_eq!(ty, MsgType::ReqRedisSet);
928        assert!(!t.is_read);
929    }
930
931    #[test]
932    fn lookup_unknown() {
933        assert!(lookup(b"NOTACOMMAND").is_none());
934    }
935
936    #[test]
937    fn classify_get_is_arg0() {
938        assert_eq!(classify(MsgType::ReqRedisGet), CommandClass::Arg0);
939    }
940
941    #[test]
942    fn classify_mset_is_argkvx() {
943        assert_eq!(classify(MsgType::ReqRedisMset), CommandClass::ArgKvX);
944    }
945
946    #[test]
947    fn error_lookup_wrongtype() {
948        assert_eq!(
949            error_lookup(b"-WRONGTYPE"),
950            Some(MsgType::RspRedisErrorWrongtype)
951        );
952    }
953}