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        // FT.SUG* family. SUGADD takes a key + suggestion +
251        // score + optional flags (variadic). SUGGET takes a
252        // key + prefix + optional flags (variadic). SUGDEL
253        // takes a key + suggestion (Arg1). SUGLEN takes only
254        // the key (Arg0).
255        M::ReqRedisFtSugadd | M::ReqRedisFtSugget => CommandClass::ArgN,
256        M::ReqRedisFtSugdel => CommandClass::Arg1,
257        M::ReqRedisFtSuglen => CommandClass::Arg0,
258        // Unknown FT.* keywords surface as a generic variadic
259        // command so the parser collects every token into the
260        // arg vector for the dispatcher's intercept; the
261        // intercept then synthesises a `-ERR not supported`
262        // reply.
263        M::ReqRedisFtUnknown => CommandClass::ArgN,
264
265        _ => CommandClass::Arg0,
266    }
267}
268
269/// True when `ty` is a Redis error response variant.
270///
271/// # Examples
272///
273/// ```
274/// use dynomite::msg::MsgType;
275/// use dynomite::proto::redis::commands::is_redis_error;
276///
277/// assert!(is_redis_error(MsgType::RspRedisErrorErr));
278/// assert!(!is_redis_error(MsgType::RspRedisStatus));
279/// ```
280#[must_use]
281pub fn is_redis_error(ty: MsgType) -> bool {
282    use MsgType as M;
283    matches!(
284        ty,
285        M::RspRedisError
286            | M::RspRedisErrorErr
287            | M::RspRedisErrorOom
288            | M::RspRedisErrorBusy
289            | M::RspRedisErrorNoauth
290            | M::RspRedisErrorLoading
291            | M::RspRedisErrorBusykey
292            | M::RspRedisErrorMisconf
293            | M::RspRedisErrorNoscript
294            | M::RspRedisErrorReadonly
295            | M::RspRedisErrorWrongtype
296            | M::RspRedisErrorExecabort
297            | M::RspRedisErrorMasterdown
298            | M::RspRedisErrorNoreplicas
299    )
300}
301
302/// Routing override the parser stamps on a request based on its
303/// command type. `None` means the default `Normal` routing.
304#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
305pub struct CommandTraits {
306    /// True when the command is a read.
307    pub is_read: bool,
308    /// True when the command sets the `quit` flag.
309    pub quit: bool,
310    /// Routing override class, if any.
311    pub routing: RoutingOverride,
312}
313
314/// Routing override stamped on a parsed request.
315#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
316pub enum RoutingOverride {
317    /// No override; use the configured per-pool routing.
318    #[default]
319    None,
320    /// Send to the local node only.
321    LocalNodeOnly,
322    /// Apply key hashing but stay within the local rack.
323    TokenOwnerLocalRackOnly,
324    /// Send to all nodes / racks / DCs.
325    AllNodesAllRacksAllDcs,
326}
327
328/// Lookup a Redis command keyword (case-insensitive ASCII) and
329/// return the message type plus its parser traits.
330///
331/// Returns `None` when the keyword is not a known command.
332///
333/// # Examples
334///
335/// ```
336/// use dynomite::msg::MsgType;
337/// use dynomite::proto::redis::commands::lookup;
338///
339/// let (ty, traits) = lookup(b"GET").unwrap();
340/// assert_eq!(ty, MsgType::ReqRedisGet);
341/// assert!(traits.is_read);
342/// ```
343#[must_use]
344pub fn lookup(keyword: &[u8]) -> Option<(MsgType, CommandTraits)> {
345    if keyword.is_empty() || keyword.len() > 32 {
346        return None;
347    }
348    let mut buf = [0u8; 32];
349    for (i, &b) in keyword.iter().enumerate() {
350        buf[i] = b.to_ascii_lowercase();
351    }
352    let key = &buf[..keyword.len()];
353    // RediSearch FT.* surface. Recognised keywords map to
354    // their typed `ReqRedisFt*` variants below; any other FT.*
355    // keyword is stamped as `ReqRedisFtUnknown` so it lands in
356    // the dispatcher's vector-registry intercept and surfaces
357    // a structured `-ERR` rather than being reflected at the
358    // backend (which would return its own opaque `unknown
359    // command` reply).
360    if key.starts_with(b"ft.")
361        && !matches!(
362            key,
363            b"ft.create"
364                | b"ft.search"
365                | b"ft.info"
366                | b"ft.list"
367                | b"ft._list"
368                | b"ft.dropindex"
369                | b"ft.regex"
370                | b"ft.sugadd"
371                | b"ft.sugget"
372                | b"ft.sugdel"
373                | b"ft.suglen"
374        )
375    {
376        return Some((
377            MsgType::ReqRedisFtUnknown,
378            CommandTraits {
379                is_read: true,
380                quit: false,
381                routing: RoutingOverride::LocalNodeOnly,
382            },
383        ));
384    }
385    let (ty, is_read, quit, routing) = match key {
386        // length 3
387        b"get" => (MsgType::ReqRedisGet, true, false, RoutingOverride::None),
388        b"set" => (MsgType::ReqRedisSet, false, false, RoutingOverride::None),
389        b"ttl" => (MsgType::ReqRedisTtl, false, false, RoutingOverride::None),
390        b"del" => (MsgType::ReqRedisDel, false, false, RoutingOverride::None),
391        // length 4
392        b"pttl" => (MsgType::ReqRedisPttl, true, false, RoutingOverride::None),
393        b"decr" => (MsgType::ReqRedisDecr, false, false, RoutingOverride::None),
394        b"dump" => (MsgType::ReqRedisDump, true, false, RoutingOverride::None),
395        b"hdel" => (MsgType::ReqRedisHdel, false, false, RoutingOverride::None),
396        b"hget" => (MsgType::ReqRedisHget, true, false, RoutingOverride::None),
397        b"hlen" => (MsgType::ReqRedisHlen, true, false, RoutingOverride::None),
398        b"hset" => (MsgType::ReqRedisHset, false, false, RoutingOverride::None),
399        b"incr" => (MsgType::ReqRedisIncr, false, false, RoutingOverride::None),
400        b"keys" => (
401            MsgType::ReqRedisKeys,
402            true,
403            false,
404            RoutingOverride::LocalNodeOnly,
405        ),
406        b"info" => (
407            MsgType::ReqRedisInfo,
408            true,
409            false,
410            RoutingOverride::LocalNodeOnly,
411        ),
412        b"llen" => (MsgType::ReqRedisLlen, true, false, RoutingOverride::None),
413        b"lpop" => (MsgType::ReqRedisLpop, false, false, RoutingOverride::None),
414        b"lrem" => (MsgType::ReqRedisLrem, false, false, RoutingOverride::None),
415        b"lset" => (MsgType::ReqRedisLset, false, false, RoutingOverride::None),
416        b"mget" => (MsgType::ReqRedisMget, true, false, RoutingOverride::None),
417        b"mset" => (MsgType::ReqRedisMset, false, false, RoutingOverride::None),
418        b"ping" => (
419            MsgType::ReqRedisPing,
420            true,
421            false,
422            RoutingOverride::LocalNodeOnly,
423        ),
424        b"rpop" => (MsgType::ReqRedisRpop, false, false, RoutingOverride::None),
425        b"sadd" => (MsgType::ReqRedisSadd, false, false, RoutingOverride::None),
426        b"scan" => (
427            MsgType::ReqRedisScan,
428            true,
429            false,
430            RoutingOverride::LocalNodeOnly,
431        ),
432        b"spop" => (MsgType::ReqRedisSpop, false, false, RoutingOverride::None),
433        b"srem" => (MsgType::ReqRedisSrem, false, false, RoutingOverride::None),
434        b"type" => (MsgType::ReqRedisType, true, false, RoutingOverride::None),
435        b"zadd" => (MsgType::ReqRedisZadd, false, false, RoutingOverride::None),
436        b"zrem" => (MsgType::ReqRedisZrem, false, false, RoutingOverride::None),
437        b"eval" => (MsgType::ReqRedisEval, false, false, RoutingOverride::None),
438        b"sort" => (MsgType::ReqRedisSort, true, false, RoutingOverride::None),
439        b"quit" => (MsgType::ReqRedisQuit, false, true, RoutingOverride::None),
440        b"load" => (
441            MsgType::ReqRedisScriptLoad,
442            false,
443            false,
444            RoutingOverride::AllNodesAllRacksAllDcs,
445        ),
446        b"kill" => (
447            MsgType::ReqRedisScriptKill,
448            false,
449            false,
450            RoutingOverride::AllNodesAllRacksAllDcs,
451        ),
452        // length 5
453        b"hkeys" => (
454            MsgType::ReqRedisHkeys,
455            true,
456            false,
457            RoutingOverride::TokenOwnerLocalRackOnly,
458        ),
459        b"hmget" => (MsgType::ReqRedisHmget, true, false, RoutingOverride::None),
460        b"hmset" => (MsgType::ReqRedisHmset, false, false, RoutingOverride::None),
461        b"hvals" => (
462            MsgType::ReqRedisHvals,
463            true,
464            false,
465            RoutingOverride::TokenOwnerLocalRackOnly,
466        ),
467        b"hscan" => (
468            MsgType::ReqRedisHscan,
469            true,
470            false,
471            RoutingOverride::TokenOwnerLocalRackOnly,
472        ),
473        b"lpush" => (MsgType::ReqRedisLpush, false, false, RoutingOverride::None),
474        b"ltrim" => (MsgType::ReqRedisLtrim, false, false, RoutingOverride::None),
475        b"rpush" => (MsgType::ReqRedisRpush, false, false, RoutingOverride::None),
476        b"scard" => (MsgType::ReqRedisScard, true, false, RoutingOverride::None),
477        b"sdiff" => (MsgType::ReqRedisSdiff, true, false, RoutingOverride::None),
478        b"setex" => (MsgType::ReqRedisSetex, false, false, RoutingOverride::None),
479        b"setnx" => (MsgType::ReqRedisSetnx, false, false, RoutingOverride::None),
480        b"smove" => (MsgType::ReqRedisSmove, false, false, RoutingOverride::None),
481        b"sscan" => (
482            MsgType::ReqRedisSscan,
483            true,
484            false,
485            RoutingOverride::TokenOwnerLocalRackOnly,
486        ),
487        b"zcard" => (MsgType::ReqRedisZcard, true, false, RoutingOverride::None),
488        b"zrank" => (MsgType::ReqRedisZrank, true, false, RoutingOverride::None),
489        b"zscan" => (
490            MsgType::ReqRedisZscan,
491            true,
492            false,
493            RoutingOverride::TokenOwnerLocalRackOnly,
494        ),
495        b"pfadd" => (MsgType::ReqRedisPfadd, false, false, RoutingOverride::None),
496        b"flush" => (
497            MsgType::ReqRedisScriptFlush,
498            false,
499            false,
500            RoutingOverride::AllNodesAllRacksAllDcs,
501        ),
502        // length 6
503        b"append" => (MsgType::ReqRedisAppend, false, false, RoutingOverride::None),
504        b"decrby" => (MsgType::ReqRedisDecrby, false, false, RoutingOverride::None),
505        b"exists" => (MsgType::ReqRedisExists, true, false, RoutingOverride::None),
506        b"expire" => (MsgType::ReqRedisExpire, false, false, RoutingOverride::None),
507        b"getbit" => (MsgType::ReqRedisGetbit, true, false, RoutingOverride::None),
508        b"getset" => (MsgType::ReqRedisGetset, false, false, RoutingOverride::None),
509        b"psetex" => (MsgType::ReqRedisPsetex, false, false, RoutingOverride::None),
510        b"hsetnx" => (MsgType::ReqRedisHsetnx, false, false, RoutingOverride::None),
511        b"incrby" => (MsgType::ReqRedisIncrby, false, false, RoutingOverride::None),
512        b"lindex" => (MsgType::ReqRedisLindex, true, false, RoutingOverride::None),
513        b"lpushx" => (MsgType::ReqRedisLpushx, false, false, RoutingOverride::None),
514        b"lrange" => (MsgType::ReqRedisLrange, true, false, RoutingOverride::None),
515        b"rpushx" => (MsgType::ReqRedisRpushx, false, false, RoutingOverride::None),
516        b"setbit" => (MsgType::ReqRedisSetbit, false, false, RoutingOverride::None),
517        b"sinter" => (MsgType::ReqRedisSinter, true, false, RoutingOverride::None),
518        b"strlen" => (MsgType::ReqRedisStrlen, true, false, RoutingOverride::None),
519        b"sunion" => (MsgType::ReqRedisSunion, true, false, RoutingOverride::None),
520        b"zcount" => (MsgType::ReqRedisZcount, true, false, RoutingOverride::None),
521        b"zrange" => (MsgType::ReqRedisZrange, true, false, RoutingOverride::None),
522        b"zscore" => (MsgType::ReqRedisZscore, true, false, RoutingOverride::None),
523        b"config" => (
524            MsgType::ReqRedisConfig,
525            true,
526            false,
527            RoutingOverride::LocalNodeOnly,
528        ),
529        b"geoadd" => (MsgType::ReqRedisGeoadd, false, false, RoutingOverride::None),
530        b"geopos" => (MsgType::ReqRedisGeopos, true, false, RoutingOverride::None),
531        b"unlink" => (MsgType::ReqRedisUnlink, false, false, RoutingOverride::None),
532        b"script" => (MsgType::ReqRedisScript, false, false, RoutingOverride::None),
533        b"bitpos" => (MsgType::ReqRedisBitpos, true, false, RoutingOverride::None),
534        // length 7
535        b"persist" => (
536            MsgType::ReqRedisPersist,
537            false,
538            false,
539            RoutingOverride::None,
540        ),
541        b"pexpire" => (
542            MsgType::ReqRedisPexpire,
543            false,
544            false,
545            RoutingOverride::None,
546        ),
547        b"hexists" => (MsgType::ReqRedisHexists, true, false, RoutingOverride::None),
548        b"hgetall" => (
549            MsgType::ReqRedisHgetall,
550            true,
551            false,
552            RoutingOverride::TokenOwnerLocalRackOnly,
553        ),
554        b"hincrby" => (
555            MsgType::ReqRedisHincrby,
556            false,
557            false,
558            RoutingOverride::None,
559        ),
560        b"linsert" => (
561            MsgType::ReqRedisLinsert,
562            false,
563            false,
564            RoutingOverride::None,
565        ),
566        b"zincrby" => (
567            MsgType::ReqRedisZincrby,
568            false,
569            false,
570            RoutingOverride::None,
571        ),
572        b"evalsha" => (
573            MsgType::ReqRedisEvalsha,
574            false,
575            false,
576            RoutingOverride::None,
577        ),
578        b"restore" => (
579            MsgType::ReqRedisRestore,
580            false,
581            false,
582            RoutingOverride::None,
583        ),
584        b"slaveof" => (
585            MsgType::ReqRedisSlaveof,
586            false,
587            false,
588            RoutingOverride::LocalNodeOnly,
589        ),
590        b"pfcount" => (
591            MsgType::ReqRedisPfcount,
592            false,
593            false,
594            RoutingOverride::None,
595        ),
596        b"geohash" => (MsgType::ReqRedisGeohash, true, false, RoutingOverride::None),
597        b"geodist" => (MsgType::ReqRedisGeodist, true, false, RoutingOverride::None),
598        b"hstrlen" => (MsgType::ReqRedisHstrlen, true, false, RoutingOverride::None),
599        // length 8
600        b"expireat" => (
601            MsgType::ReqRedisExpireat,
602            false,
603            false,
604            RoutingOverride::None,
605        ),
606        b"bitcount" => (
607            MsgType::ReqRedisBitcount,
608            true,
609            false,
610            RoutingOverride::None,
611        ),
612        b"getrange" => (
613            MsgType::ReqRedisGetrange,
614            true,
615            false,
616            RoutingOverride::None,
617        ),
618        b"setrange" => (
619            MsgType::ReqRedisSetrange,
620            false,
621            false,
622            RoutingOverride::None,
623        ),
624        b"smembers" => (
625            MsgType::ReqRedisSmembers,
626            true,
627            false,
628            RoutingOverride::None,
629        ),
630        b"zrevrank" => (
631            MsgType::ReqRedisZrevrank,
632            true,
633            false,
634            RoutingOverride::None,
635        ),
636        b"json.set" => (
637            MsgType::ReqRedisJsonset,
638            false,
639            false,
640            RoutingOverride::None,
641        ),
642        b"json.get" => (MsgType::ReqRedisJsonget, true, false, RoutingOverride::None),
643        b"json.del" => (
644            MsgType::ReqRedisJsondel,
645            false,
646            false,
647            RoutingOverride::None,
648        ),
649        // length 9
650        b"pexpireat" => (
651            MsgType::ReqRedisPexpireat,
652            false,
653            false,
654            RoutingOverride::None,
655        ),
656        b"rpoplpush" => (
657            MsgType::ReqRedisRpoplpush,
658            false,
659            false,
660            RoutingOverride::None,
661        ),
662        b"sismember" => (
663            MsgType::ReqRedisSismember,
664            true,
665            false,
666            RoutingOverride::None,
667        ),
668        b"zlexcount" => (
669            MsgType::ReqRedisZlexcount,
670            true,
671            false,
672            RoutingOverride::None,
673        ),
674        b"zrevrange" => (
675            MsgType::ReqRedisZrevrange,
676            true,
677            false,
678            RoutingOverride::None,
679        ),
680        b"georadius" => (
681            MsgType::ReqRedisGeoradius,
682            true,
683            false,
684            RoutingOverride::None,
685        ),
686        b"json.type" => (
687            MsgType::ReqRedisJsontype,
688            true,
689            false,
690            RoutingOverride::None,
691        ),
692        b"json.mget" => (
693            MsgType::ReqRedisJsonmget,
694            true,
695            false,
696            RoutingOverride::None,
697        ),
698        // length 10
699        b"sdiffstore" => (
700            MsgType::ReqRedisSdiffstore,
701            false,
702            false,
703            RoutingOverride::None,
704        ),
705        // length 11
706        b"incrbyfloat" => (
707            MsgType::ReqRedisIncrbyfloat,
708            false,
709            false,
710            RoutingOverride::None,
711        ),
712        b"sinterstore" => (
713            MsgType::ReqRedisSinterstore,
714            false,
715            false,
716            RoutingOverride::None,
717        ),
718        b"srandmember" => (
719            MsgType::ReqRedisSrandmember,
720            true,
721            false,
722            RoutingOverride::None,
723        ),
724        b"sunionstore" => (
725            MsgType::ReqRedisSunionstore,
726            true,
727            false,
728            RoutingOverride::None,
729        ),
730        b"zinterstore" => (
731            MsgType::ReqRedisZinterstore,
732            true,
733            false,
734            RoutingOverride::None,
735        ),
736        b"zrangebylex" => (
737            MsgType::ReqRedisZrangebylex,
738            true,
739            false,
740            RoutingOverride::None,
741        ),
742        b"zunionstore" => (
743            MsgType::ReqRedisZunionstore,
744            true,
745            false,
746            RoutingOverride::None,
747        ),
748        b"json.arrlen" => (
749            MsgType::ReqRedisJsonarrlen,
750            true,
751            false,
752            RoutingOverride::None,
753        ),
754        b"json.objlen" => (
755            MsgType::ReqRedisJsonobjlen,
756            true,
757            false,
758            RoutingOverride::None,
759        ),
760        // length 12
761        b"hincrbyfloat" => (
762            MsgType::ReqRedisHincrbyfloat,
763            false,
764            false,
765            RoutingOverride::None,
766        ),
767        b"json.objkeys" => (
768            MsgType::ReqRedisJsonobjkeys,
769            true,
770            false,
771            RoutingOverride::None,
772        ),
773        // length 13
774        b"zrangebyscore" => (
775            MsgType::ReqRedisZrangebyscore,
776            true,
777            false,
778            RoutingOverride::None,
779        ),
780        // length 14
781        b"zremrangebylex" => (
782            MsgType::ReqRedisZremrangebylex,
783            false,
784            false,
785            RoutingOverride::None,
786        ),
787        b"zrevrangebylex" => (
788            MsgType::ReqRedisZrevrangebylex,
789            true,
790            false,
791            RoutingOverride::None,
792        ),
793        b"json.arrappend" => (
794            MsgType::ReqRedisJsonarrappend,
795            false,
796            false,
797            RoutingOverride::None,
798        ),
799        b"json.arrinsert" => (
800            MsgType::ReqRedisJsonarrinsert,
801            false,
802            false,
803            RoutingOverride::None,
804        ),
805        // length 15
806        b"zremrangebyrank" => (
807            MsgType::ReqRedisZremrangebyrank,
808            false,
809            false,
810            RoutingOverride::None,
811        ),
812        // length 16
813        b"zremrangebyscore" => (
814            MsgType::ReqRedisZremrangebyscore,
815            false,
816            false,
817            RoutingOverride::None,
818        ),
819        b"zrevrangebyscore" => (
820            MsgType::ReqRedisZrevrangebyscore,
821            true,
822            false,
823            RoutingOverride::None,
824        ),
825        // length 17
826        b"georadiusbymember" => (
827            MsgType::ReqRedisGeoradiusbymember,
828            true,
829            false,
830            RoutingOverride::None,
831        ),
832        // RediSearch FT.* surface. The index name is carried as
833        // the first key (or there is no key for FT.LIST), so the
834        // standard parser key/arg machinery picks them up
835        // directly.
836        b"ft.create" => (
837            MsgType::ReqRedisFtCreate,
838            false,
839            false,
840            RoutingOverride::LocalNodeOnly,
841        ),
842        b"ft.search" => (
843            MsgType::ReqRedisFtSearch,
844            true,
845            false,
846            RoutingOverride::LocalNodeOnly,
847        ),
848        b"ft.info" => (
849            MsgType::ReqRedisFtInfo,
850            true,
851            false,
852            RoutingOverride::LocalNodeOnly,
853        ),
854        b"ft.list" | b"ft._list" => (
855            MsgType::ReqRedisFtList,
856            true,
857            false,
858            RoutingOverride::LocalNodeOnly,
859        ),
860        b"ft.dropindex" => (
861            MsgType::ReqRedisFtDropindex,
862            false,
863            false,
864            RoutingOverride::LocalNodeOnly,
865        ),
866        // FT.REGEX is a Dynomite extension (not standard
867        // RediSearch) for exact and approximate-regex search
868        // over `TEXT`-typed schema fields. Routed exactly like
869        // FT.SEARCH: index name carried as the lone key, the
870        // rest goes through the variadic arg vector.
871        b"ft.regex" => (
872            MsgType::ReqRedisFtRegex,
873            true,
874            false,
875            RoutingOverride::LocalNodeOnly,
876        ),
877        // FT.SUG* family backs the autocomplete
878        // suggestion-dictionary surface. The first argument
879        // is the suggestion-key (binary-safe), routed as the
880        // lone key so the parser collects the remaining
881        // tokens into the variadic arg vector. SUGLEN is
882        // single-key + zero-arg so it lands on Arg0.
883        b"ft.sugadd" => (
884            MsgType::ReqRedisFtSugadd,
885            false,
886            false,
887            RoutingOverride::LocalNodeOnly,
888        ),
889        b"ft.sugget" => (
890            MsgType::ReqRedisFtSugget,
891            true,
892            false,
893            RoutingOverride::LocalNodeOnly,
894        ),
895        b"ft.sugdel" => (
896            MsgType::ReqRedisFtSugdel,
897            false,
898            false,
899            RoutingOverride::LocalNodeOnly,
900        ),
901        b"ft.suglen" => (
902            MsgType::ReqRedisFtSuglen,
903            true,
904            false,
905            RoutingOverride::LocalNodeOnly,
906        ),
907        // length 28: dynomite config
908        b"dyno_config:conn_consistency" => (
909            MsgType::HackSettingConnConsistency,
910            false,
911            false,
912            RoutingOverride::None,
913        ),
914        _ => return None,
915    };
916    Some((
917        ty,
918        CommandTraits {
919            is_read,
920            quit,
921            routing,
922        },
923    ))
924}
925
926/// Look up a Redis error response keyword (case-sensitive: error
927/// keywords are uppercase on the wire) and return the
928/// corresponding [`MsgType`].
929///
930/// # Examples
931///
932/// ```
933/// use dynomite::msg::MsgType;
934/// use dynomite::proto::redis::commands::error_lookup;
935///
936/// assert_eq!(error_lookup(b"-ERR"), Some(MsgType::RspRedisErrorErr));
937/// assert_eq!(error_lookup(b"-WRONGTYPE"), Some(MsgType::RspRedisErrorWrongtype));
938/// ```
939#[must_use]
940pub fn error_lookup(token: &[u8]) -> Option<MsgType> {
941    match token {
942        b"-ERR" => Some(MsgType::RspRedisErrorErr),
943        b"-OOM" => Some(MsgType::RspRedisErrorOom),
944        b"-BUSY" => Some(MsgType::RspRedisErrorBusy),
945        b"-NOAUTH" => Some(MsgType::RspRedisErrorNoauth),
946        b"-LOADING" => Some(MsgType::RspRedisErrorLoading),
947        b"-BUSYKEY" => Some(MsgType::RspRedisErrorBusykey),
948        b"-MISCONF" => Some(MsgType::RspRedisErrorMisconf),
949        b"-NOSCRIPT" => Some(MsgType::RspRedisErrorNoscript),
950        b"-READONLY" => Some(MsgType::RspRedisErrorReadonly),
951        b"-WRONGTYPE" => Some(MsgType::RspRedisErrorWrongtype),
952        b"-EXECABORT" => Some(MsgType::RspRedisErrorExecabort),
953        b"-MASTERDOWN" => Some(MsgType::RspRedisErrorMasterdown),
954        b"-NOREPLICAS" => Some(MsgType::RspRedisErrorNoreplicas),
955        _ => None,
956    }
957}
958
959#[cfg(test)]
960mod tests {
961    use super::*;
962
963    #[test]
964    fn lookup_get_set() {
965        let (ty, t) = lookup(b"GET").unwrap();
966        assert_eq!(ty, MsgType::ReqRedisGet);
967        assert!(t.is_read);
968        let (ty, t) = lookup(b"set").unwrap();
969        assert_eq!(ty, MsgType::ReqRedisSet);
970        assert!(!t.is_read);
971    }
972
973    #[test]
974    fn lookup_unknown() {
975        assert!(lookup(b"NOTACOMMAND").is_none());
976    }
977
978    #[test]
979    fn classify_get_is_arg0() {
980        assert_eq!(classify(MsgType::ReqRedisGet), CommandClass::Arg0);
981    }
982
983    #[test]
984    fn classify_mset_is_argkvx() {
985        assert_eq!(classify(MsgType::ReqRedisMset), CommandClass::ArgKvX);
986    }
987
988    #[test]
989    fn error_lookup_wrongtype() {
990        assert_eq!(
991            error_lookup(b"-WRONGTYPE"),
992            Some(MsgType::RspRedisErrorWrongtype)
993        );
994    }
995}