Skip to main content

ember_protocol/
command.rs

1//! Command parsing from RESP3 frames.
2//!
3//! Converts a parsed [`Frame`] (expected to be an array) into a typed
4//! [`Command`] enum. This keeps protocol-level concerns separate from
5//! the engine that actually executes commands.
6
7use bytes::Bytes;
8
9use crate::error::ProtocolError;
10use crate::types::Frame;
11
12/// Maximum number of dimensions in a vector. 65,536 is generous for any
13/// real-world embedding model (OpenAI: 1536, Cohere: 4096) while preventing
14/// memory abuse from absurdly large vectors.
15const MAX_VECTOR_DIMS: usize = 65_536;
16
17/// Maximum value for HNSW connectivity (M) and expansion parameters.
18/// Values above 1024 give no practical benefit and waste memory.
19const MAX_HNSW_PARAM: u64 = 1024;
20
21/// Maximum number of results for VSIM. 10,000 is generous for any practical
22/// similarity search while preventing OOM from unbounded result allocation.
23const MAX_VSIM_COUNT: u64 = 10_000;
24
25/// Maximum search beam width for VSIM. Same cap as MAX_HNSW_PARAM —
26/// larger values cause worst-case O(n) graph traversal with no accuracy gain.
27const MAX_VSIM_EF: u64 = MAX_HNSW_PARAM;
28
29/// Maximum number of vectors in a single VADD_BATCH command. 10,000 keeps
30/// per-command latency bounded while still being large enough to amortize
31/// round-trip overhead for bulk inserts.
32const MAX_VADD_BATCH_SIZE: usize = 10_000;
33
34/// Maximum value for SCAN COUNT. Prevents clients from requesting a scan
35/// hint so large it causes pre-allocation issues.
36const MAX_SCAN_COUNT: u64 = 10_000_000;
37
38/// Expiration option for the SET command.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum SetExpire {
41    /// EX seconds — expire after N seconds.
42    Ex(u64),
43    /// PX milliseconds — expire after N milliseconds.
44    Px(u64),
45}
46
47/// A parsed client command, ready for execution.
48#[derive(Debug, Clone, PartialEq)]
49pub enum Command {
50    /// PING with an optional message. Returns PONG or echoes the message.
51    Ping(Option<Bytes>),
52
53    /// ECHO `message`. Returns the message back to the client.
54    Echo(Bytes),
55
56    /// GET `key`. Returns the value or nil.
57    Get { key: String },
58
59    /// SET `key` `value` \[EX seconds | PX milliseconds\] \[NX | XX\].
60    Set {
61        key: String,
62        value: Bytes,
63        expire: Option<SetExpire>,
64        /// Only set the key if it does not already exist.
65        nx: bool,
66        /// Only set the key if it already exists.
67        xx: bool,
68    },
69
70    /// INCR `key`. Increments the integer value of a key by 1.
71    Incr { key: String },
72
73    /// DECR `key`. Decrements the integer value of a key by 1.
74    Decr { key: String },
75
76    /// INCRBY `key` `increment`. Increments the integer value of a key by the given amount.
77    IncrBy { key: String, delta: i64 },
78
79    /// DECRBY `key` `decrement`. Decrements the integer value of a key by the given amount.
80    DecrBy { key: String, delta: i64 },
81
82    /// INCRBYFLOAT `key` `increment`. Increments the float value of a key by the given amount.
83    IncrByFloat { key: String, delta: f64 },
84
85    /// APPEND `key` `value`. Appends a value to a string key. Returns the new length.
86    Append { key: String, value: Bytes },
87
88    /// STRLEN `key`. Returns the length of the string value stored at key.
89    Strlen { key: String },
90
91    /// KEYS `pattern`. Returns all keys matching a glob pattern.
92    Keys { pattern: String },
93
94    /// RENAME `key` `newkey`. Renames a key.
95    Rename { key: String, newkey: String },
96
97    /// DEL `key` \[key ...\]. Returns the number of keys removed.
98    Del { keys: Vec<String> },
99
100    /// UNLINK `key` \[key ...\]. Like DEL but frees memory in the background.
101    Unlink { keys: Vec<String> },
102
103    /// EXISTS `key` \[key ...\]. Returns the number of keys that exist.
104    Exists { keys: Vec<String> },
105
106    /// MGET `key` \[key ...\]. Returns the values for all specified keys.
107    MGet { keys: Vec<String> },
108
109    /// MSET `key` `value` \[key value ...\]. Sets multiple key-value pairs.
110    MSet { pairs: Vec<(String, Bytes)> },
111
112    /// EXPIRE `key` `seconds`. Sets a TTL on an existing key.
113    Expire { key: String, seconds: u64 },
114
115    /// TTL `key`. Returns remaining time-to-live in seconds.
116    Ttl { key: String },
117
118    /// PERSIST `key`. Removes the expiration from a key.
119    Persist { key: String },
120
121    /// PTTL `key`. Returns remaining time-to-live in milliseconds.
122    Pttl { key: String },
123
124    /// PEXPIRE `key` `milliseconds`. Sets a TTL in milliseconds on an existing key.
125    Pexpire { key: String, milliseconds: u64 },
126
127    /// DBSIZE. Returns the number of keys in the database.
128    DbSize,
129
130    /// INFO \[section\]. Returns server info. Currently only supports "keyspace".
131    Info { section: Option<String> },
132
133    /// BGSAVE. Triggers a background snapshot.
134    BgSave,
135
136    /// BGREWRITEAOF. Triggers an AOF rewrite (snapshot + truncate).
137    BgRewriteAof,
138
139    /// FLUSHDB \[ASYNC\]. Removes all keys from the database.
140    FlushDb { async_mode: bool },
141
142    /// CONFIG GET `pattern`. Returns matching server configuration parameters.
143    ConfigGet { pattern: String },
144
145    /// CONFIG SET `param` `value`. Sets a server configuration parameter at runtime.
146    ConfigSet { param: String, value: String },
147
148    /// CONFIG REWRITE. Writes the current config to the file loaded at startup.
149    ConfigRewrite,
150
151    /// MULTI. Starts a transaction — subsequent commands are queued until EXEC.
152    Multi,
153
154    /// EXEC. Executes all queued commands atomically, returning an array of results.
155    Exec,
156
157    /// DISCARD. Aborts a transaction, discarding all queued commands.
158    Discard,
159
160    /// SCAN `cursor` \[MATCH pattern\] \[COUNT count\]. Iterates keys.
161    Scan {
162        cursor: u64,
163        pattern: Option<String>,
164        count: Option<usize>,
165    },
166
167    /// LPUSH `key` `value` \[value ...\]. Pushes values to the head of a list.
168    LPush { key: String, values: Vec<Bytes> },
169
170    /// RPUSH `key` `value` \[value ...\]. Pushes values to the tail of a list.
171    RPush { key: String, values: Vec<Bytes> },
172
173    /// LPOP `key`. Pops a value from the head of a list.
174    LPop { key: String },
175
176    /// RPOP `key`. Pops a value from the tail of a list.
177    RPop { key: String },
178
179    /// LRANGE `key` `start` `stop`. Returns a range of elements by index.
180    LRange { key: String, start: i64, stop: i64 },
181
182    /// LLEN `key`. Returns the length of a list.
183    LLen { key: String },
184
185    /// BLPOP `key` \[key ...\] `timeout`. Blocks until an element is available
186    /// at the head of one of the given lists, or the timeout expires.
187    BLPop {
188        keys: Vec<String>,
189        timeout_secs: f64,
190    },
191
192    /// BRPOP `key` \[key ...\] `timeout`. Blocks until an element is available
193    /// at the tail of one of the given lists, or the timeout expires.
194    BRPop {
195        keys: Vec<String>,
196        timeout_secs: f64,
197    },
198
199    /// TYPE `key`. Returns the type of the value stored at key.
200    Type { key: String },
201
202    /// ZADD `key` \[NX|XX\] \[GT|LT\] \[CH\] `score` `member` \[score member ...\].
203    ZAdd {
204        key: String,
205        flags: ZAddFlags,
206        members: Vec<(f64, String)>,
207    },
208
209    /// ZREM `key` `member` \[member ...\]. Removes members from a sorted set.
210    ZRem { key: String, members: Vec<String> },
211
212    /// ZSCORE `key` `member`. Returns the score of a member.
213    ZScore { key: String, member: String },
214
215    /// ZRANK `key` `member`. Returns the rank of a member (0-based).
216    ZRank { key: String, member: String },
217
218    /// ZCARD `key`. Returns the cardinality (number of members) of a sorted set.
219    ZCard { key: String },
220
221    /// ZRANGE `key` `start` `stop` \[WITHSCORES\]. Returns a range by rank.
222    ZRange {
223        key: String,
224        start: i64,
225        stop: i64,
226        with_scores: bool,
227    },
228
229    /// HSET `key` `field` `value` \[field value ...\]. Sets field-value pairs in a hash.
230    HSet {
231        key: String,
232        fields: Vec<(String, Bytes)>,
233    },
234
235    /// HGET `key` `field`. Gets a field's value from a hash.
236    HGet { key: String, field: String },
237
238    /// HGETALL `key`. Gets all field-value pairs from a hash.
239    HGetAll { key: String },
240
241    /// HDEL `key` `field` \[field ...\]. Deletes fields from a hash.
242    HDel { key: String, fields: Vec<String> },
243
244    /// HEXISTS `key` `field`. Checks if a field exists in a hash.
245    HExists { key: String, field: String },
246
247    /// HLEN `key`. Returns the number of fields in a hash.
248    HLen { key: String },
249
250    /// HINCRBY `key` `field` `increment`. Increments a hash field's integer value.
251    HIncrBy {
252        key: String,
253        field: String,
254        delta: i64,
255    },
256
257    /// HKEYS `key`. Returns all field names in a hash.
258    HKeys { key: String },
259
260    /// HVALS `key`. Returns all values in a hash.
261    HVals { key: String },
262
263    /// HMGET `key` `field` \[field ...\]. Gets multiple field values from a hash.
264    HMGet { key: String, fields: Vec<String> },
265
266    /// SADD `key` `member` \[member ...\]. Adds members to a set.
267    SAdd { key: String, members: Vec<String> },
268
269    /// SREM `key` `member` \[member ...\]. Removes members from a set.
270    SRem { key: String, members: Vec<String> },
271
272    /// SMEMBERS `key`. Returns all members of a set.
273    SMembers { key: String },
274
275    /// SISMEMBER `key` `member`. Checks if a member exists in a set.
276    SIsMember { key: String, member: String },
277
278    /// SCARD `key`. Returns the cardinality (number of members) of a set.
279    SCard { key: String },
280
281    // --- cluster commands ---
282    /// CLUSTER INFO. Returns cluster state and configuration information.
283    ClusterInfo,
284
285    /// CLUSTER NODES. Returns the list of cluster nodes.
286    ClusterNodes,
287
288    /// CLUSTER SLOTS. Returns the slot distribution across nodes.
289    ClusterSlots,
290
291    /// CLUSTER KEYSLOT `key`. Returns the hash slot for a key.
292    ClusterKeySlot { key: String },
293
294    /// CLUSTER MYID. Returns the node's ID.
295    ClusterMyId,
296
297    /// CLUSTER SETSLOT `slot` IMPORTING `node-id`. Mark slot as importing from node.
298    ClusterSetSlotImporting { slot: u16, node_id: String },
299
300    /// CLUSTER SETSLOT `slot` MIGRATING `node-id`. Mark slot as migrating to node.
301    ClusterSetSlotMigrating { slot: u16, node_id: String },
302
303    /// CLUSTER SETSLOT `slot` NODE `node-id`. Assign slot to node.
304    ClusterSetSlotNode { slot: u16, node_id: String },
305
306    /// CLUSTER SETSLOT `slot` STABLE. Clear importing/migrating state.
307    ClusterSetSlotStable { slot: u16 },
308
309    /// CLUSTER MEET `ip` `port`. Add a node to the cluster.
310    ClusterMeet { ip: String, port: u16 },
311
312    /// CLUSTER ADDSLOTS `slot` \[slot...\]. Assign slots to the local node.
313    ClusterAddSlots { slots: Vec<u16> },
314
315    /// CLUSTER ADDSLOTSRANGE `start` `end` \[start end ...\]. Assign a contiguous range of hash slots.
316    ClusterAddSlotsRange { ranges: Vec<(u16, u16)> },
317
318    /// CLUSTER DELSLOTS `slot` \[slot...\]. Remove slots from the local node.
319    ClusterDelSlots { slots: Vec<u16> },
320
321    /// CLUSTER FORGET `node-id`. Remove a node from the cluster.
322    ClusterForget { node_id: String },
323
324    /// CLUSTER REPLICATE `node-id`. Make this node a replica of another.
325    ClusterReplicate { node_id: String },
326
327    /// CLUSTER FAILOVER [FORCE|TAKEOVER]. Trigger a manual failover.
328    ClusterFailover { force: bool, takeover: bool },
329
330    /// CLUSTER COUNTKEYSINSLOT `slot`. Return the number of keys in a slot.
331    ClusterCountKeysInSlot { slot: u16 },
332
333    /// CLUSTER GETKEYSINSLOT `slot` `count`. Return keys in a slot.
334    ClusterGetKeysInSlot { slot: u16, count: u32 },
335
336    /// MIGRATE `host` `port` `key` `db` `timeout` \[COPY\] \[REPLACE\] \[KEYS key...\].
337    /// Migrate a key to another node.
338    Migrate {
339        host: String,
340        port: u16,
341        key: String,
342        db: u32,
343        timeout_ms: u64,
344        copy: bool,
345        replace: bool,
346    },
347
348    /// RESTORE `key` `ttl` `serialized-value` \[REPLACE\].
349    /// Insert a key from serialized data (used by MIGRATE).
350    Restore {
351        key: String,
352        ttl_ms: u64,
353        data: Bytes,
354        replace: bool,
355    },
356
357    /// ASKING. Signals that the next command is for a migrating slot.
358    Asking,
359
360    /// SLOWLOG GET [count]. Returns recent slow log entries.
361    SlowLogGet { count: Option<usize> },
362
363    /// SLOWLOG LEN. Returns the number of entries in the slow log.
364    SlowLogLen,
365
366    /// SLOWLOG RESET. Clears the slow log.
367    SlowLogReset,
368
369    // --- pub/sub commands ---
370    /// SUBSCRIBE `channel` \[channel ...\]. Subscribe to one or more channels.
371    Subscribe { channels: Vec<String> },
372
373    /// UNSUBSCRIBE \[channel ...\]. Unsubscribe from channels (all if none given).
374    Unsubscribe { channels: Vec<String> },
375
376    /// PSUBSCRIBE `pattern` \[pattern ...\]. Subscribe to channels matching patterns.
377    PSubscribe { patterns: Vec<String> },
378
379    /// PUNSUBSCRIBE \[pattern ...\]. Unsubscribe from patterns (all if none given).
380    PUnsubscribe { patterns: Vec<String> },
381
382    /// PUBLISH `channel` `message`. Publish a message to a channel.
383    Publish { channel: String, message: Bytes },
384
385    /// PUBSUB CHANNELS \[pattern\]. List active channels, optionally matching a glob.
386    PubSubChannels { pattern: Option<String> },
387
388    /// PUBSUB NUMSUB \[channel ...\]. Returns subscriber counts for given channels.
389    PubSubNumSub { channels: Vec<String> },
390
391    /// PUBSUB NUMPAT. Returns the number of active pattern subscriptions.
392    PubSubNumPat,
393
394    // --- vector commands ---
395    /// VADD key element f32 [f32 ...] [METRIC COSINE|L2|IP] [QUANT F32|F16|I8]
396    /// [M n] [EF n]. Adds a vector to a vector set.
397    VAdd {
398        key: String,
399        element: String,
400        vector: Vec<f32>,
401        /// 0 = cosine (default), 1 = l2, 2 = inner product
402        metric: u8,
403        /// 0 = f32 (default), 1 = f16, 2 = i8
404        quantization: u8,
405        /// HNSW connectivity parameter (default 16)
406        connectivity: u32,
407        /// HNSW construction beam width (default 64)
408        expansion_add: u32,
409    },
410
411    /// VADD_BATCH key DIM n element1 f32... element2 f32... [METRIC COSINE|L2|IP]
412    /// [QUANT F32|F16|I8] [M n] [EF n]. Adds multiple vectors in a single command.
413    VAddBatch {
414        key: String,
415        entries: Vec<(String, Vec<f32>)>,
416        dim: usize,
417        /// 0 = cosine (default), 1 = l2, 2 = inner product
418        metric: u8,
419        /// 0 = f32 (default), 1 = f16, 2 = i8
420        quantization: u8,
421        /// HNSW connectivity parameter (default 16)
422        connectivity: u32,
423        /// HNSW construction beam width (default 64)
424        expansion_add: u32,
425    },
426
427    /// VSIM key f32 [f32 ...] COUNT k [EF n] [WITHSCORES].
428    /// Searches for k nearest neighbors.
429    VSim {
430        key: String,
431        query: Vec<f32>,
432        count: usize,
433        ef_search: usize,
434        with_scores: bool,
435    },
436
437    /// VREM key element. Removes a vector from a vector set.
438    VRem { key: String, element: String },
439
440    /// VGET key element. Retrieves the stored vector for an element.
441    VGet { key: String, element: String },
442
443    /// VCARD key. Returns the number of elements in a vector set.
444    VCard { key: String },
445
446    /// VDIM key. Returns the dimensionality of a vector set.
447    VDim { key: String },
448
449    /// VINFO key. Returns metadata about a vector set.
450    VInfo { key: String },
451
452    // --- protobuf commands ---
453    /// PROTO.REGISTER `name` `descriptor_bytes`. Registers a protobuf schema
454    /// (pre-compiled FileDescriptorSet) under the given name.
455    ProtoRegister { name: String, descriptor: Bytes },
456
457    /// PROTO.SET `key` `type_name` `data` \[EX s | PX ms\] \[NX | XX\].
458    /// Stores a validated protobuf value.
459    ProtoSet {
460        key: String,
461        type_name: String,
462        data: Bytes,
463        expire: Option<SetExpire>,
464        /// Only set the key if it does not already exist.
465        nx: bool,
466        /// Only set the key if it already exists.
467        xx: bool,
468    },
469
470    /// PROTO.GET `key`. Returns \[type_name, data\] or nil.
471    ProtoGet { key: String },
472
473    /// PROTO.TYPE `key`. Returns the message type name or nil.
474    ProtoType { key: String },
475
476    /// PROTO.SCHEMAS. Lists all registered schema names.
477    ProtoSchemas,
478
479    /// PROTO.DESCRIBE `name`. Lists message types in a registered schema.
480    ProtoDescribe { name: String },
481
482    /// PROTO.GETFIELD `key` `field_path`. Reads a single field from a
483    /// protobuf value, returning it as a native RESP3 type.
484    ProtoGetField { key: String, field_path: String },
485
486    /// PROTO.SETFIELD `key` `field_path` `value`. Updates a single scalar
487    /// field in a stored protobuf value.
488    ProtoSetField {
489        key: String,
490        field_path: String,
491        value: String,
492    },
493
494    /// PROTO.DELFIELD `key` `field_path`. Clears a field to its default value.
495    ProtoDelField { key: String, field_path: String },
496
497    /// AUTH \[username\] password. Authenticate the connection.
498    Auth {
499        /// Username for ACL-style auth. None for legacy AUTH.
500        username: Option<String>,
501        /// The password to validate.
502        password: String,
503    },
504
505    /// QUIT. Requests the server to close the connection.
506    Quit,
507
508    /// MONITOR. Streams all commands processed by the server.
509    Monitor,
510
511    /// A command we don't recognize (yet).
512    Unknown(String),
513}
514
515/// Flags for the ZADD command.
516#[derive(Debug, Clone, Default, PartialEq)]
517pub struct ZAddFlags {
518    /// Only add new members, don't update existing scores.
519    pub nx: bool,
520    /// Only update existing members, don't add new ones.
521    pub xx: bool,
522    /// Only update when new score > current score.
523    pub gt: bool,
524    /// Only update when new score < current score.
525    pub lt: bool,
526    /// Return count of changed members (added + updated) instead of just added.
527    pub ch: bool,
528}
529
530impl Eq for ZAddFlags {}
531
532impl Command {
533    /// Returns the lowercase command name as a static string.
534    ///
535    /// Used for metrics labels and slow log entries. Zero allocation —
536    /// returns a `&'static str` for every known variant.
537    ///
538    /// The match is explicit rather than derive-generated: a proc macro would
539    /// obscure the string mappings, which are the thing most worth seeing at
540    /// a glance when auditing command names.
541    pub fn command_name(&self) -> &'static str {
542        match self {
543            // connection
544            Command::Ping(_) => "ping",
545            Command::Echo(_) => "echo",
546            Command::Auth { .. } => "auth",
547            Command::Quit => "quit",
548            Command::Monitor => "monitor",
549
550            // strings
551            Command::Get { .. } => "get",
552            Command::Set { .. } => "set",
553            Command::MGet { .. } => "mget",
554            Command::MSet { .. } => "mset",
555            Command::Incr { .. } => "incr",
556            Command::Decr { .. } => "decr",
557            Command::IncrBy { .. } => "incrby",
558            Command::DecrBy { .. } => "decrby",
559            Command::IncrByFloat { .. } => "incrbyfloat",
560            Command::Append { .. } => "append",
561            Command::Strlen { .. } => "strlen",
562
563            // key lifecycle
564            Command::Del { .. } => "del",
565            Command::Unlink { .. } => "unlink",
566            Command::Exists { .. } => "exists",
567            Command::Rename { .. } => "rename",
568            Command::Keys { .. } => "keys",
569            Command::Scan { .. } => "scan",
570            Command::Type { .. } => "type",
571            Command::Expire { .. } => "expire",
572            Command::Ttl { .. } => "ttl",
573            Command::Persist { .. } => "persist",
574            Command::Pttl { .. } => "pttl",
575            Command::Pexpire { .. } => "pexpire",
576
577            // server
578            Command::DbSize => "dbsize",
579            Command::Info { .. } => "info",
580            Command::BgSave => "bgsave",
581            Command::BgRewriteAof => "bgrewriteaof",
582            Command::FlushDb { .. } => "flushdb",
583            Command::ConfigGet { .. } => "config",
584            Command::ConfigSet { .. } => "config",
585            Command::ConfigRewrite => "config",
586            Command::Multi => "multi",
587            Command::Exec => "exec",
588            Command::Discard => "discard",
589
590            // list
591            Command::LPush { .. } => "lpush",
592            Command::RPush { .. } => "rpush",
593            Command::LPop { .. } => "lpop",
594            Command::RPop { .. } => "rpop",
595            Command::LRange { .. } => "lrange",
596            Command::LLen { .. } => "llen",
597            Command::BLPop { .. } => "blpop",
598            Command::BRPop { .. } => "brpop",
599
600            // sorted set
601            Command::ZAdd { .. } => "zadd",
602            Command::ZRem { .. } => "zrem",
603            Command::ZScore { .. } => "zscore",
604            Command::ZRank { .. } => "zrank",
605            Command::ZCard { .. } => "zcard",
606            Command::ZRange { .. } => "zrange",
607
608            // hash
609            Command::HSet { .. } => "hset",
610            Command::HGet { .. } => "hget",
611            Command::HGetAll { .. } => "hgetall",
612            Command::HDel { .. } => "hdel",
613            Command::HExists { .. } => "hexists",
614            Command::HLen { .. } => "hlen",
615            Command::HIncrBy { .. } => "hincrby",
616            Command::HKeys { .. } => "hkeys",
617            Command::HVals { .. } => "hvals",
618            Command::HMGet { .. } => "hmget",
619
620            // set
621            Command::SAdd { .. } => "sadd",
622            Command::SRem { .. } => "srem",
623            Command::SMembers { .. } => "smembers",
624            Command::SIsMember { .. } => "sismember",
625            Command::SCard { .. } => "scard",
626
627            // cluster
628            Command::ClusterInfo => "cluster_info",
629            Command::ClusterNodes => "cluster_nodes",
630            Command::ClusterSlots => "cluster_slots",
631            Command::ClusterKeySlot { .. } => "cluster_keyslot",
632            Command::ClusterMyId => "cluster_myid",
633            Command::ClusterSetSlotImporting { .. } => "cluster_setslot",
634            Command::ClusterSetSlotMigrating { .. } => "cluster_setslot",
635            Command::ClusterSetSlotNode { .. } => "cluster_setslot",
636            Command::ClusterSetSlotStable { .. } => "cluster_setslot",
637            Command::ClusterMeet { .. } => "cluster_meet",
638            Command::ClusterAddSlots { .. } => "cluster_addslots",
639            Command::ClusterAddSlotsRange { .. } => "cluster_addslotsrange",
640            Command::ClusterDelSlots { .. } => "cluster_delslots",
641            Command::ClusterForget { .. } => "cluster_forget",
642            Command::ClusterReplicate { .. } => "cluster_replicate",
643            Command::ClusterFailover { .. } => "cluster_failover",
644            Command::ClusterCountKeysInSlot { .. } => "cluster_countkeysinslot",
645            Command::ClusterGetKeysInSlot { .. } => "cluster_getkeysinslot",
646            Command::Migrate { .. } => "migrate",
647            Command::Restore { .. } => "restore",
648            Command::Asking => "asking",
649
650            // slow log
651            Command::SlowLogGet { .. } => "slowlog",
652            Command::SlowLogLen => "slowlog",
653            Command::SlowLogReset => "slowlog",
654
655            // pub/sub
656            Command::Subscribe { .. } => "subscribe",
657            Command::Unsubscribe { .. } => "unsubscribe",
658            Command::PSubscribe { .. } => "psubscribe",
659            Command::PUnsubscribe { .. } => "punsubscribe",
660            Command::Publish { .. } => "publish",
661            Command::PubSubChannels { .. } => "pubsub",
662            Command::PubSubNumSub { .. } => "pubsub",
663            Command::PubSubNumPat => "pubsub",
664
665            // vector
666            Command::VAdd { .. } => "vadd",
667            Command::VAddBatch { .. } => "vadd_batch",
668            Command::VSim { .. } => "vsim",
669            Command::VRem { .. } => "vrem",
670            Command::VGet { .. } => "vget",
671            Command::VCard { .. } => "vcard",
672            Command::VDim { .. } => "vdim",
673            Command::VInfo { .. } => "vinfo",
674
675            // protobuf
676            Command::ProtoRegister { .. } => "proto.register",
677            Command::ProtoSet { .. } => "proto.set",
678            Command::ProtoGet { .. } => "proto.get",
679            Command::ProtoType { .. } => "proto.type",
680            Command::ProtoSchemas => "proto.schemas",
681            Command::ProtoDescribe { .. } => "proto.describe",
682            Command::ProtoGetField { .. } => "proto.getfield",
683            Command::ProtoSetField { .. } => "proto.setfield",
684            Command::ProtoDelField { .. } => "proto.delfield",
685
686            Command::Unknown(_) => "unknown",
687        }
688    }
689
690    /// Returns `true` if this command mutates state.
691    ///
692    /// Used by the replica write-rejection layer: any command matching this
693    /// predicate is redirected to the primary via MOVED rather than executed
694    /// locally on a read-only replica.
695    pub fn is_write(&self) -> bool {
696        matches!(
697            self,
698            // string mutations
699            Command::Set { .. }
700                | Command::MSet { .. }
701                | Command::Append { .. }
702                | Command::Incr { .. }
703                | Command::Decr { .. }
704                | Command::IncrBy { .. }
705                | Command::DecrBy { .. }
706                | Command::IncrByFloat { .. }
707            // key lifecycle
708                | Command::Del { .. }
709                | Command::Unlink { .. }
710                | Command::Rename { .. }
711                | Command::Expire { .. }
712                | Command::Pexpire { .. }
713                | Command::Persist { .. }
714            // list
715                | Command::LPush { .. }
716                | Command::RPush { .. }
717                | Command::LPop { .. }
718                | Command::RPop { .. }
719                | Command::BLPop { .. }
720                | Command::BRPop { .. }
721            // sorted set
722                | Command::ZAdd { .. }
723                | Command::ZRem { .. }
724            // hash
725                | Command::HSet { .. }
726                | Command::HDel { .. }
727                | Command::HIncrBy { .. }
728            // set
729                | Command::SAdd { .. }
730                | Command::SRem { .. }
731            // server / persistence
732                | Command::FlushDb { .. }
733                | Command::ConfigSet { .. }
734                | Command::Exec
735                | Command::BgRewriteAof
736                | Command::BgSave
737                | Command::Restore { .. }
738            // vector
739                | Command::VAdd { .. }
740                | Command::VAddBatch { .. }
741                | Command::VRem { .. }
742            // protobuf
743                | Command::ProtoRegister { .. }
744                | Command::ProtoSet { .. }
745                | Command::ProtoSetField { .. }
746                | Command::ProtoDelField { .. }
747        )
748    }
749
750    /// Returns the primary key for this command, if there is one.
751    ///
752    /// Used to calculate the hash slot for MOVED redirects on replicas.
753    /// For multi-key commands, returns the first key.
754    pub fn primary_key(&self) -> Option<&str> {
755        match self {
756            Command::Get { key }
757            | Command::Set { key, .. }
758            | Command::Incr { key }
759            | Command::Decr { key }
760            | Command::IncrBy { key, .. }
761            | Command::DecrBy { key, .. }
762            | Command::IncrByFloat { key, .. }
763            | Command::Append { key, .. }
764            | Command::Strlen { key }
765            | Command::Persist { key }
766            | Command::Expire { key, .. }
767            | Command::Pexpire { key, .. }
768            | Command::Ttl { key }
769            | Command::Pttl { key }
770            | Command::Type { key }
771            | Command::Rename { key, .. }
772            | Command::LPush { key, .. }
773            | Command::RPush { key, .. }
774            | Command::LPop { key }
775            | Command::RPop { key }
776            | Command::LRange { key, .. }
777            | Command::LLen { key }
778            | Command::ZAdd { key, .. }
779            | Command::ZRem { key, .. }
780            | Command::ZScore { key, .. }
781            | Command::ZRank { key, .. }
782            | Command::ZRange { key, .. }
783            | Command::ZCard { key }
784            | Command::HSet { key, .. }
785            | Command::HGet { key, .. }
786            | Command::HGetAll { key }
787            | Command::HDel { key, .. }
788            | Command::HExists { key, .. }
789            | Command::HLen { key }
790            | Command::HIncrBy { key, .. }
791            | Command::HKeys { key }
792            | Command::HVals { key }
793            | Command::HMGet { key, .. }
794            | Command::SAdd { key, .. }
795            | Command::SRem { key, .. }
796            | Command::SMembers { key }
797            | Command::SIsMember { key, .. }
798            | Command::SCard { key }
799            | Command::VAdd { key, .. }
800            | Command::VAddBatch { key, .. }
801            | Command::VSim { key, .. }
802            | Command::VRem { key, .. }
803            | Command::VGet { key, .. }
804            | Command::VCard { key }
805            | Command::VDim { key }
806            | Command::VInfo { key }
807            | Command::ProtoSet { key, .. }
808            | Command::ProtoGet { key }
809            | Command::ProtoType { key }
810            | Command::ProtoGetField { key, .. }
811            | Command::ProtoSetField { key, .. }
812            | Command::ProtoDelField { key, .. }
813            | Command::Restore { key, .. } => Some(key),
814            Command::Del { keys }
815            | Command::Unlink { keys }
816            | Command::Exists { keys }
817            | Command::MGet { keys }
818            | Command::BLPop { keys, .. }
819            | Command::BRPop { keys, .. } => keys.first().map(String::as_str),
820            Command::MSet { pairs } => pairs.first().map(|(k, _)| k.as_str()),
821            _ => None,
822        }
823    }
824
825    /// Parses a [`Frame`] into a [`Command`].
826    ///
827    /// Expects an array frame where the first element is the command name
828    /// (as a bulk or simple string) and the rest are arguments.
829    pub fn from_frame(frame: Frame) -> Result<Command, ProtocolError> {
830        let frames = match frame {
831            Frame::Array(frames) => frames,
832            _ => {
833                return Err(ProtocolError::InvalidCommandFrame(
834                    "expected array frame".into(),
835                ));
836            }
837        };
838
839        if frames.is_empty() {
840            return Err(ProtocolError::InvalidCommandFrame(
841                "empty command array".into(),
842            ));
843        }
844
845        // command names are short ASCII keywords — uppercase on the stack to
846        // avoid two heap allocations (extract_string + to_ascii_uppercase).
847        let name_bytes = extract_raw_bytes(&frames[0])?;
848        let mut upper = [0u8; MAX_KEYWORD_LEN];
849        let len = name_bytes.len();
850        if len > MAX_KEYWORD_LEN {
851            let name = extract_string(&frames[0])?;
852            return Ok(Command::Unknown(name));
853        }
854        upper[..len].copy_from_slice(name_bytes);
855        upper[..len].make_ascii_uppercase();
856        let name_upper = std::str::from_utf8(&upper[..len]).map_err(|_| {
857            ProtocolError::InvalidCommandFrame("command name is not valid utf-8".into())
858        })?;
859
860        match name_upper {
861            "PING" => parse_ping(&frames[1..]),
862            "ECHO" => parse_echo(&frames[1..]),
863            "GET" => parse_get(&frames[1..]),
864            "SET" => parse_set(&frames[1..]),
865            "INCR" => parse_incr(&frames[1..]),
866            "DECR" => parse_decr(&frames[1..]),
867            "INCRBY" => parse_incrby(&frames[1..]),
868            "DECRBY" => parse_decrby(&frames[1..]),
869            "INCRBYFLOAT" => parse_incrbyfloat(&frames[1..]),
870            "APPEND" => parse_append(&frames[1..]),
871            "STRLEN" => parse_strlen(&frames[1..]),
872            "KEYS" => parse_keys(&frames[1..]),
873            "RENAME" => parse_rename(&frames[1..]),
874            "DEL" => parse_del(&frames[1..]),
875            "UNLINK" => parse_unlink(&frames[1..]),
876            "EXISTS" => parse_exists(&frames[1..]),
877            "MGET" => parse_mget(&frames[1..]),
878            "MSET" => parse_mset(&frames[1..]),
879            "EXPIRE" => parse_expire(&frames[1..]),
880            "TTL" => parse_ttl(&frames[1..]),
881            "PERSIST" => parse_persist(&frames[1..]),
882            "PTTL" => parse_pttl(&frames[1..]),
883            "PEXPIRE" => parse_pexpire(&frames[1..]),
884            "DBSIZE" => parse_dbsize(&frames[1..]),
885            "INFO" => parse_info(&frames[1..]),
886            "BGSAVE" => parse_bgsave(&frames[1..]),
887            "BGREWRITEAOF" => parse_bgrewriteaof(&frames[1..]),
888            "FLUSHDB" => parse_flushdb(&frames[1..]),
889            "SCAN" => parse_scan(&frames[1..]),
890            "LPUSH" => parse_lpush(&frames[1..]),
891            "RPUSH" => parse_rpush(&frames[1..]),
892            "LPOP" => parse_lpop(&frames[1..]),
893            "RPOP" => parse_rpop(&frames[1..]),
894            "LRANGE" => parse_lrange(&frames[1..]),
895            "LLEN" => parse_llen(&frames[1..]),
896            "BLPOP" => parse_blpop(&frames[1..]),
897            "BRPOP" => parse_brpop(&frames[1..]),
898            "TYPE" => parse_type(&frames[1..]),
899            "ZADD" => parse_zadd(&frames[1..]),
900            "ZREM" => parse_zrem(&frames[1..]),
901            "ZSCORE" => parse_zscore(&frames[1..]),
902            "ZRANK" => parse_zrank(&frames[1..]),
903            "ZCARD" => parse_zcard(&frames[1..]),
904            "ZRANGE" => parse_zrange(&frames[1..]),
905            "HSET" => parse_hset(&frames[1..]),
906            "HGET" => parse_hget(&frames[1..]),
907            "HGETALL" => parse_hgetall(&frames[1..]),
908            "HDEL" => parse_hdel(&frames[1..]),
909            "HEXISTS" => parse_hexists(&frames[1..]),
910            "HLEN" => parse_hlen(&frames[1..]),
911            "HINCRBY" => parse_hincrby(&frames[1..]),
912            "HKEYS" => parse_hkeys(&frames[1..]),
913            "HVALS" => parse_hvals(&frames[1..]),
914            "HMGET" => parse_hmget(&frames[1..]),
915            "SADD" => parse_sadd(&frames[1..]),
916            "SREM" => parse_srem(&frames[1..]),
917            "SMEMBERS" => parse_smembers(&frames[1..]),
918            "SISMEMBER" => parse_sismember(&frames[1..]),
919            "SCARD" => parse_scard(&frames[1..]),
920            "CLUSTER" => parse_cluster(&frames[1..]),
921            "ASKING" => parse_asking(&frames[1..]),
922            "MIGRATE" => parse_migrate(&frames[1..]),
923            "RESTORE" => parse_restore(&frames[1..]),
924            "CONFIG" => parse_config(&frames[1..]),
925            "MULTI" => parse_no_args("MULTI", &frames[1..], Command::Multi),
926            "EXEC" => parse_no_args("EXEC", &frames[1..], Command::Exec),
927            "DISCARD" => parse_no_args("DISCARD", &frames[1..], Command::Discard),
928            "SLOWLOG" => parse_slowlog(&frames[1..]),
929            "SUBSCRIBE" => parse_subscribe(&frames[1..]),
930            "UNSUBSCRIBE" => parse_unsubscribe(&frames[1..]),
931            "PSUBSCRIBE" => parse_psubscribe(&frames[1..]),
932            "PUNSUBSCRIBE" => parse_punsubscribe(&frames[1..]),
933            "PUBLISH" => parse_publish(&frames[1..]),
934            "PUBSUB" => parse_pubsub(&frames[1..]),
935            "VADD" => parse_vadd(&frames[1..]),
936            "VADD_BATCH" => parse_vadd_batch(&frames[1..]),
937            "VSIM" => parse_vsim(&frames[1..]),
938            "VREM" => parse_vrem(&frames[1..]),
939            "VGET" => parse_vget(&frames[1..]),
940            "VCARD" => parse_vcard(&frames[1..]),
941            "VDIM" => parse_vdim(&frames[1..]),
942            "VINFO" => parse_vinfo(&frames[1..]),
943            "PROTO.REGISTER" => parse_proto_register(&frames[1..]),
944            "PROTO.SET" => parse_proto_set(&frames[1..]),
945            "PROTO.GET" => parse_proto_get(&frames[1..]),
946            "PROTO.TYPE" => parse_proto_type(&frames[1..]),
947            "PROTO.SCHEMAS" => parse_proto_schemas(&frames[1..]),
948            "PROTO.DESCRIBE" => parse_proto_describe(&frames[1..]),
949            "PROTO.GETFIELD" => parse_proto_getfield(&frames[1..]),
950            "PROTO.SETFIELD" => parse_proto_setfield(&frames[1..]),
951            "PROTO.DELFIELD" => parse_proto_delfield(&frames[1..]),
952            "AUTH" => parse_auth(&frames[1..]),
953            "QUIT" => parse_quit(&frames[1..]),
954            "MONITOR" => parse_monitor(&frames[1..]),
955            _ => {
956                // only allocate for truly unknown commands
957                let name = extract_string(&frames[0])?;
958                Ok(Command::Unknown(name))
959            }
960        }
961    }
962}
963
964/// Extracts a UTF-8 string from a Bulk or Simple frame.
965///
966/// Validates UTF-8 in-place on the Bytes buffer to avoid an
967/// intermediate Vec<u8> allocation from `to_vec()`.
968fn extract_string(frame: &Frame) -> Result<String, ProtocolError> {
969    match frame {
970        Frame::Bulk(data) => {
971            let s = std::str::from_utf8(data).map_err(|_| {
972                ProtocolError::InvalidCommandFrame("command name is not valid utf-8".into())
973            })?;
974            Ok(s.to_owned())
975        }
976        Frame::Simple(s) => Ok(s.clone()),
977        _ => Err(ProtocolError::InvalidCommandFrame(
978            "expected bulk or simple string for command name".into(),
979        )),
980    }
981}
982
983/// Extracts raw bytes from a Bulk or Simple frame.
984fn extract_bytes(frame: &Frame) -> Result<Bytes, ProtocolError> {
985    match frame {
986        Frame::Bulk(data) => Ok(data.clone()),
987        Frame::Simple(s) => Ok(Bytes::copy_from_slice(s.as_bytes())),
988        _ => Err(ProtocolError::InvalidCommandFrame(
989            "expected bulk or simple string argument".into(),
990        )),
991    }
992}
993
994/// Extracts all frames in a slice as UTF-8 strings.
995fn extract_strings(frames: &[Frame]) -> Result<Vec<String>, ProtocolError> {
996    frames.iter().map(extract_string).collect()
997}
998
999/// Extracts all frames in a slice as raw byte buffers.
1000fn extract_bytes_vec(frames: &[Frame]) -> Result<Vec<Bytes>, ProtocolError> {
1001    frames.iter().map(extract_bytes).collect()
1002}
1003
1004/// Maximum length for a command name or keyword uppercased on the stack.
1005/// All Redis/Ember commands and subcommands are well under this limit.
1006const MAX_KEYWORD_LEN: usize = 32;
1007
1008/// Returns the raw bytes of a Bulk or Simple frame without allocating.
1009fn extract_raw_bytes(frame: &Frame) -> Result<&[u8], ProtocolError> {
1010    match frame {
1011        Frame::Bulk(data) => Ok(data.as_ref()),
1012        Frame::Simple(s) => Ok(s.as_bytes()),
1013        _ => Err(ProtocolError::InvalidCommandFrame(
1014            "expected bulk or simple string".into(),
1015        )),
1016    }
1017}
1018
1019/// Uppercases a frame's bytes into a stack buffer and returns the result as `&str`.
1020///
1021/// Used for command names, subcommands, and option flags where the value is always
1022/// a short ASCII keyword. Avoids the two heap allocations that
1023/// `extract_string()?.to_ascii_uppercase()` would require.
1024fn uppercase_arg<'b>(
1025    frame: &Frame,
1026    buf: &'b mut [u8; MAX_KEYWORD_LEN],
1027) -> Result<&'b str, ProtocolError> {
1028    let bytes = extract_raw_bytes(frame)?;
1029    let len = bytes.len();
1030    if len > MAX_KEYWORD_LEN {
1031        return Err(ProtocolError::InvalidCommandFrame(
1032            "keyword too long".into(),
1033        ));
1034    }
1035    buf[..len].copy_from_slice(bytes);
1036    buf[..len].make_ascii_uppercase();
1037    std::str::from_utf8(&buf[..len])
1038        .map_err(|_| ProtocolError::InvalidCommandFrame("keyword is not valid utf-8".into()))
1039}
1040
1041/// Parses a frame's bytes directly as a positive u64 without allocating a String.
1042fn parse_u64(frame: &Frame, cmd: &str) -> Result<u64, ProtocolError> {
1043    let bytes = extract_raw_bytes(frame)?;
1044    parse_u64_bytes(bytes).ok_or_else(|| {
1045        ProtocolError::InvalidCommandFrame(format!("value is not a valid integer for '{cmd}'"))
1046    })
1047}
1048
1049/// Parses an unsigned integer directly from a byte slice.
1050fn parse_u64_bytes(buf: &[u8]) -> Option<u64> {
1051    if buf.is_empty() {
1052        return None;
1053    }
1054    let mut n: u64 = 0;
1055    for &b in buf {
1056        if !b.is_ascii_digit() {
1057            return None;
1058        }
1059        n = n.checked_mul(10)?.checked_add((b - b'0') as u64)?;
1060    }
1061    Some(n)
1062}
1063
1064/// Shorthand for the wrong-arity error returned by every parser.
1065fn wrong_arity(cmd: &'static str) -> ProtocolError {
1066    ProtocolError::WrongArity(cmd.into())
1067}
1068
1069fn parse_ping(args: &[Frame]) -> Result<Command, ProtocolError> {
1070    match args.len() {
1071        0 => Ok(Command::Ping(None)),
1072        1 => {
1073            let msg = extract_bytes(&args[0])?;
1074            Ok(Command::Ping(Some(msg)))
1075        }
1076        _ => Err(wrong_arity("PING")),
1077    }
1078}
1079
1080fn parse_echo(args: &[Frame]) -> Result<Command, ProtocolError> {
1081    if args.len() != 1 {
1082        return Err(wrong_arity("ECHO"));
1083    }
1084    let msg = extract_bytes(&args[0])?;
1085    Ok(Command::Echo(msg))
1086}
1087
1088fn parse_get(args: &[Frame]) -> Result<Command, ProtocolError> {
1089    if args.len() != 1 {
1090        return Err(wrong_arity("GET"));
1091    }
1092    let key = extract_string(&args[0])?;
1093    Ok(Command::Get { key })
1094}
1095
1096/// Parses NX / XX / EX / PX options from a slice of command arguments.
1097///
1098/// Returns `(expire, nx, xx)`. `cmd` is used in error messages.
1099fn parse_set_options(
1100    args: &[Frame],
1101    cmd: &'static str,
1102) -> Result<(Option<SetExpire>, bool, bool), ProtocolError> {
1103    let mut expire = None;
1104    let mut nx = false;
1105    let mut xx = false;
1106    let mut idx = 0;
1107
1108    while idx < args.len() {
1109        let mut kw = [0u8; MAX_KEYWORD_LEN];
1110        let flag = uppercase_arg(&args[idx], &mut kw)?;
1111        match flag {
1112            "NX" => {
1113                nx = true;
1114                idx += 1;
1115            }
1116            "XX" => {
1117                xx = true;
1118                idx += 1;
1119            }
1120            "EX" => {
1121                idx += 1;
1122                if idx >= args.len() {
1123                    return Err(wrong_arity(cmd));
1124                }
1125                let amount = parse_u64(&args[idx], cmd)?;
1126                if amount == 0 {
1127                    return Err(ProtocolError::InvalidCommandFrame(format!(
1128                        "invalid expire time in '{cmd}' command"
1129                    )));
1130                }
1131                expire = Some(SetExpire::Ex(amount));
1132                idx += 1;
1133            }
1134            "PX" => {
1135                idx += 1;
1136                if idx >= args.len() {
1137                    return Err(wrong_arity(cmd));
1138                }
1139                let amount = parse_u64(&args[idx], cmd)?;
1140                if amount == 0 {
1141                    return Err(ProtocolError::InvalidCommandFrame(format!(
1142                        "invalid expire time in '{cmd}' command"
1143                    )));
1144                }
1145                expire = Some(SetExpire::Px(amount));
1146                idx += 1;
1147            }
1148            _ => {
1149                return Err(ProtocolError::InvalidCommandFrame(format!(
1150                    "unsupported {cmd} option '{flag}'"
1151                )));
1152            }
1153        }
1154    }
1155
1156    if nx && xx {
1157        return Err(ProtocolError::InvalidCommandFrame(
1158            "XX and NX options at the same time are not compatible".into(),
1159        ));
1160    }
1161
1162    Ok((expire, nx, xx))
1163}
1164
1165fn parse_set(args: &[Frame]) -> Result<Command, ProtocolError> {
1166    if args.len() < 2 {
1167        return Err(wrong_arity("SET"));
1168    }
1169
1170    let key = extract_string(&args[0])?;
1171    let value = extract_bytes(&args[1])?;
1172    let (expire, nx, xx) = parse_set_options(&args[2..], "SET")?;
1173
1174    Ok(Command::Set {
1175        key,
1176        value,
1177        expire,
1178        nx,
1179        xx,
1180    })
1181}
1182
1183fn parse_incr(args: &[Frame]) -> Result<Command, ProtocolError> {
1184    if args.len() != 1 {
1185        return Err(wrong_arity("INCR"));
1186    }
1187    let key = extract_string(&args[0])?;
1188    Ok(Command::Incr { key })
1189}
1190
1191fn parse_decr(args: &[Frame]) -> Result<Command, ProtocolError> {
1192    if args.len() != 1 {
1193        return Err(wrong_arity("DECR"));
1194    }
1195    let key = extract_string(&args[0])?;
1196    Ok(Command::Decr { key })
1197}
1198
1199fn parse_incrby(args: &[Frame]) -> Result<Command, ProtocolError> {
1200    if args.len() != 2 {
1201        return Err(wrong_arity("INCRBY"));
1202    }
1203    let key = extract_string(&args[0])?;
1204    let delta = parse_i64(&args[1], "INCRBY")?;
1205    Ok(Command::IncrBy { key, delta })
1206}
1207
1208fn parse_decrby(args: &[Frame]) -> Result<Command, ProtocolError> {
1209    if args.len() != 2 {
1210        return Err(wrong_arity("DECRBY"));
1211    }
1212    let key = extract_string(&args[0])?;
1213    let delta = parse_i64(&args[1], "DECRBY")?;
1214    Ok(Command::DecrBy { key, delta })
1215}
1216
1217fn parse_incrbyfloat(args: &[Frame]) -> Result<Command, ProtocolError> {
1218    if args.len() != 2 {
1219        return Err(wrong_arity("INCRBYFLOAT"));
1220    }
1221    let key = extract_string(&args[0])?;
1222    let s = extract_string(&args[1])?;
1223    let delta: f64 = s.parse().map_err(|_| {
1224        ProtocolError::InvalidCommandFrame("value is not a valid float for 'INCRBYFLOAT'".into())
1225    })?;
1226    if delta.is_nan() || delta.is_infinite() {
1227        return Err(ProtocolError::InvalidCommandFrame(
1228            "increment would produce NaN or Infinity".into(),
1229        ));
1230    }
1231    Ok(Command::IncrByFloat { key, delta })
1232}
1233
1234fn parse_append(args: &[Frame]) -> Result<Command, ProtocolError> {
1235    if args.len() != 2 {
1236        return Err(wrong_arity("APPEND"));
1237    }
1238    let key = extract_string(&args[0])?;
1239    let value = extract_bytes(&args[1])?;
1240    Ok(Command::Append { key, value })
1241}
1242
1243fn parse_strlen(args: &[Frame]) -> Result<Command, ProtocolError> {
1244    if args.len() != 1 {
1245        return Err(wrong_arity("STRLEN"));
1246    }
1247    let key = extract_string(&args[0])?;
1248    Ok(Command::Strlen { key })
1249}
1250
1251fn parse_keys(args: &[Frame]) -> Result<Command, ProtocolError> {
1252    if args.len() != 1 {
1253        return Err(wrong_arity("KEYS"));
1254    }
1255    let pattern = extract_string(&args[0])?;
1256    Ok(Command::Keys { pattern })
1257}
1258
1259fn parse_rename(args: &[Frame]) -> Result<Command, ProtocolError> {
1260    if args.len() != 2 {
1261        return Err(wrong_arity("RENAME"));
1262    }
1263    let key = extract_string(&args[0])?;
1264    let newkey = extract_string(&args[1])?;
1265    Ok(Command::Rename { key, newkey })
1266}
1267
1268fn parse_del(args: &[Frame]) -> Result<Command, ProtocolError> {
1269    if args.is_empty() {
1270        return Err(wrong_arity("DEL"));
1271    }
1272    let keys = extract_strings(args)?;
1273    Ok(Command::Del { keys })
1274}
1275
1276fn parse_exists(args: &[Frame]) -> Result<Command, ProtocolError> {
1277    if args.is_empty() {
1278        return Err(wrong_arity("EXISTS"));
1279    }
1280    let keys = extract_strings(args)?;
1281    Ok(Command::Exists { keys })
1282}
1283
1284fn parse_mget(args: &[Frame]) -> Result<Command, ProtocolError> {
1285    if args.is_empty() {
1286        return Err(wrong_arity("MGET"));
1287    }
1288    let keys = extract_strings(args)?;
1289    Ok(Command::MGet { keys })
1290}
1291
1292fn parse_mset(args: &[Frame]) -> Result<Command, ProtocolError> {
1293    if args.is_empty() || !args.len().is_multiple_of(2) {
1294        return Err(wrong_arity("MSET"));
1295    }
1296    let mut pairs = Vec::with_capacity(args.len() / 2);
1297    for chunk in args.chunks(2) {
1298        let key = extract_string(&chunk[0])?;
1299        let value = extract_bytes(&chunk[1])?;
1300        pairs.push((key, value));
1301    }
1302    Ok(Command::MSet { pairs })
1303}
1304
1305fn parse_expire(args: &[Frame]) -> Result<Command, ProtocolError> {
1306    if args.len() != 2 {
1307        return Err(wrong_arity("EXPIRE"));
1308    }
1309    let key = extract_string(&args[0])?;
1310    let seconds = parse_u64(&args[1], "EXPIRE")?;
1311
1312    if seconds == 0 {
1313        return Err(ProtocolError::InvalidCommandFrame(
1314            "invalid expire time in 'EXPIRE' command".into(),
1315        ));
1316    }
1317
1318    Ok(Command::Expire { key, seconds })
1319}
1320
1321fn parse_ttl(args: &[Frame]) -> Result<Command, ProtocolError> {
1322    if args.len() != 1 {
1323        return Err(wrong_arity("TTL"));
1324    }
1325    let key = extract_string(&args[0])?;
1326    Ok(Command::Ttl { key })
1327}
1328
1329fn parse_persist(args: &[Frame]) -> Result<Command, ProtocolError> {
1330    if args.len() != 1 {
1331        return Err(wrong_arity("PERSIST"));
1332    }
1333    let key = extract_string(&args[0])?;
1334    Ok(Command::Persist { key })
1335}
1336
1337fn parse_pttl(args: &[Frame]) -> Result<Command, ProtocolError> {
1338    if args.len() != 1 {
1339        return Err(wrong_arity("PTTL"));
1340    }
1341    let key = extract_string(&args[0])?;
1342    Ok(Command::Pttl { key })
1343}
1344
1345fn parse_pexpire(args: &[Frame]) -> Result<Command, ProtocolError> {
1346    if args.len() != 2 {
1347        return Err(wrong_arity("PEXPIRE"));
1348    }
1349    let key = extract_string(&args[0])?;
1350    let milliseconds = parse_u64(&args[1], "PEXPIRE")?;
1351
1352    if milliseconds == 0 {
1353        return Err(ProtocolError::InvalidCommandFrame(
1354            "invalid expire time in 'PEXPIRE' command".into(),
1355        ));
1356    }
1357
1358    Ok(Command::Pexpire { key, milliseconds })
1359}
1360
1361fn parse_dbsize(args: &[Frame]) -> Result<Command, ProtocolError> {
1362    if !args.is_empty() {
1363        return Err(wrong_arity("DBSIZE"));
1364    }
1365    Ok(Command::DbSize)
1366}
1367
1368fn parse_info(args: &[Frame]) -> Result<Command, ProtocolError> {
1369    match args.len() {
1370        0 => Ok(Command::Info { section: None }),
1371        1 => {
1372            let section = extract_string(&args[0])?;
1373            Ok(Command::Info {
1374                section: Some(section),
1375            })
1376        }
1377        _ => Err(wrong_arity("INFO")),
1378    }
1379}
1380
1381fn parse_bgsave(args: &[Frame]) -> Result<Command, ProtocolError> {
1382    if !args.is_empty() {
1383        return Err(wrong_arity("BGSAVE"));
1384    }
1385    Ok(Command::BgSave)
1386}
1387
1388fn parse_bgrewriteaof(args: &[Frame]) -> Result<Command, ProtocolError> {
1389    if !args.is_empty() {
1390        return Err(wrong_arity("BGREWRITEAOF"));
1391    }
1392    Ok(Command::BgRewriteAof)
1393}
1394
1395fn parse_flushdb(args: &[Frame]) -> Result<Command, ProtocolError> {
1396    if args.is_empty() {
1397        return Ok(Command::FlushDb { async_mode: false });
1398    }
1399    if args.len() == 1 {
1400        let arg = extract_string(&args[0])?;
1401        if arg.eq_ignore_ascii_case("ASYNC") {
1402            return Ok(Command::FlushDb { async_mode: true });
1403        }
1404    }
1405    Err(wrong_arity("FLUSHDB"))
1406}
1407
1408fn parse_unlink(args: &[Frame]) -> Result<Command, ProtocolError> {
1409    if args.is_empty() {
1410        return Err(wrong_arity("UNLINK"));
1411    }
1412    let keys = extract_strings(args)?;
1413    Ok(Command::Unlink { keys })
1414}
1415
1416fn parse_scan(args: &[Frame]) -> Result<Command, ProtocolError> {
1417    if args.is_empty() {
1418        return Err(wrong_arity("SCAN"));
1419    }
1420
1421    let cursor = parse_u64(&args[0], "SCAN")?;
1422    let mut pattern = None;
1423    let mut count = None;
1424    let mut idx = 1;
1425
1426    while idx < args.len() {
1427        let mut kw = [0u8; MAX_KEYWORD_LEN];
1428        let flag = uppercase_arg(&args[idx], &mut kw)?;
1429        match flag {
1430            "MATCH" => {
1431                idx += 1;
1432                if idx >= args.len() {
1433                    return Err(wrong_arity("SCAN"));
1434                }
1435                pattern = Some(extract_string(&args[idx])?);
1436                idx += 1;
1437            }
1438            "COUNT" => {
1439                idx += 1;
1440                if idx >= args.len() {
1441                    return Err(wrong_arity("SCAN"));
1442                }
1443                let n = parse_u64(&args[idx], "SCAN")?;
1444                if n > MAX_SCAN_COUNT {
1445                    return Err(ProtocolError::InvalidCommandFrame(format!(
1446                        "SCAN COUNT {n} exceeds max {MAX_SCAN_COUNT}"
1447                    )));
1448                }
1449                count = Some(n as usize);
1450                idx += 1;
1451            }
1452            _ => {
1453                return Err(ProtocolError::InvalidCommandFrame(format!(
1454                    "unsupported SCAN option '{flag}'"
1455                )));
1456            }
1457        }
1458    }
1459
1460    Ok(Command::Scan {
1461        cursor,
1462        pattern,
1463        count,
1464    })
1465}
1466
1467/// Parses a frame's bytes directly as an i64 without allocating a String.
1468fn parse_i64(frame: &Frame, cmd: &str) -> Result<i64, ProtocolError> {
1469    let bytes = extract_raw_bytes(frame)?;
1470    parse_i64_bytes(bytes).ok_or_else(|| {
1471        ProtocolError::InvalidCommandFrame(format!("value is not a valid integer for '{cmd}'"))
1472    })
1473}
1474
1475/// Parses a signed integer directly from a byte slice. Accumulates in the
1476/// negative direction for negative numbers so that `i64::MIN` is representable.
1477fn parse_i64_bytes(buf: &[u8]) -> Option<i64> {
1478    if buf.is_empty() {
1479        return None;
1480    }
1481    let (negative, digits) = if buf[0] == b'-' {
1482        (true, &buf[1..])
1483    } else {
1484        (false, buf)
1485    };
1486    if digits.is_empty() {
1487        return None;
1488    }
1489    if negative {
1490        let mut n: i64 = 0;
1491        for &b in digits {
1492            if !b.is_ascii_digit() {
1493                return None;
1494            }
1495            n = n.checked_mul(10)?.checked_sub((b - b'0') as i64)?;
1496        }
1497        Some(n)
1498    } else {
1499        let mut n: i64 = 0;
1500        for &b in digits {
1501            if !b.is_ascii_digit() {
1502                return None;
1503            }
1504            n = n.checked_mul(10)?.checked_add((b - b'0') as i64)?;
1505        }
1506        Some(n)
1507    }
1508}
1509
1510fn parse_lpush(args: &[Frame]) -> Result<Command, ProtocolError> {
1511    if args.len() < 2 {
1512        return Err(wrong_arity("LPUSH"));
1513    }
1514    let key = extract_string(&args[0])?;
1515    let values = extract_bytes_vec(&args[1..])?;
1516    Ok(Command::LPush { key, values })
1517}
1518
1519fn parse_rpush(args: &[Frame]) -> Result<Command, ProtocolError> {
1520    if args.len() < 2 {
1521        return Err(wrong_arity("RPUSH"));
1522    }
1523    let key = extract_string(&args[0])?;
1524    let values = extract_bytes_vec(&args[1..])?;
1525    Ok(Command::RPush { key, values })
1526}
1527
1528fn parse_lpop(args: &[Frame]) -> Result<Command, ProtocolError> {
1529    if args.len() != 1 {
1530        return Err(wrong_arity("LPOP"));
1531    }
1532    let key = extract_string(&args[0])?;
1533    Ok(Command::LPop { key })
1534}
1535
1536fn parse_rpop(args: &[Frame]) -> Result<Command, ProtocolError> {
1537    if args.len() != 1 {
1538        return Err(wrong_arity("RPOP"));
1539    }
1540    let key = extract_string(&args[0])?;
1541    Ok(Command::RPop { key })
1542}
1543
1544fn parse_lrange(args: &[Frame]) -> Result<Command, ProtocolError> {
1545    if args.len() != 3 {
1546        return Err(wrong_arity("LRANGE"));
1547    }
1548    let key = extract_string(&args[0])?;
1549    let start = parse_i64(&args[1], "LRANGE")?;
1550    let stop = parse_i64(&args[2], "LRANGE")?;
1551    Ok(Command::LRange { key, start, stop })
1552}
1553
1554fn parse_llen(args: &[Frame]) -> Result<Command, ProtocolError> {
1555    if args.len() != 1 {
1556        return Err(wrong_arity("LLEN"));
1557    }
1558    let key = extract_string(&args[0])?;
1559    Ok(Command::LLen { key })
1560}
1561
1562/// Parses BLPOP/BRPOP: all args except the last are keys, the last is the
1563/// timeout in seconds (float). At least one key is required.
1564fn parse_blpop(args: &[Frame]) -> Result<Command, ProtocolError> {
1565    if args.len() < 2 {
1566        return Err(wrong_arity("BLPOP"));
1567    }
1568    let timeout_secs = parse_timeout(&args[args.len() - 1], "BLPOP")?;
1569    let keys = extract_strings(&args[..args.len() - 1])?;
1570    Ok(Command::BLPop { keys, timeout_secs })
1571}
1572
1573fn parse_brpop(args: &[Frame]) -> Result<Command, ProtocolError> {
1574    if args.len() < 2 {
1575        return Err(wrong_arity("BRPOP"));
1576    }
1577    let timeout_secs = parse_timeout(&args[args.len() - 1], "BRPOP")?;
1578    let keys = extract_strings(&args[..args.len() - 1])?;
1579    Ok(Command::BRPop { keys, timeout_secs })
1580}
1581
1582/// Extracts the timeout argument for BLPOP/BRPOP. Redis accepts integer or
1583/// float seconds; negative values are an error.
1584fn parse_timeout(frame: &Frame, cmd: &str) -> Result<f64, ProtocolError> {
1585    let bytes = extract_raw_bytes(frame)?;
1586    let s = std::str::from_utf8(bytes).map_err(|_| {
1587        ProtocolError::InvalidCommandFrame(format!(
1588            "timeout is not a float or out of range for '{cmd}'"
1589        ))
1590    })?;
1591    let val: f64 = s.parse().map_err(|_| {
1592        ProtocolError::InvalidCommandFrame(format!(
1593            "timeout is not a float or out of range for '{cmd}'"
1594        ))
1595    })?;
1596    if val < 0.0 {
1597        return Err(ProtocolError::InvalidCommandFrame(format!(
1598            "timeout is negative for '{cmd}'"
1599        )));
1600    }
1601    Ok(val)
1602}
1603
1604fn parse_type(args: &[Frame]) -> Result<Command, ProtocolError> {
1605    if args.len() != 1 {
1606        return Err(wrong_arity("TYPE"));
1607    }
1608    let key = extract_string(&args[0])?;
1609    Ok(Command::Type { key })
1610}
1611
1612/// Parses a string argument as an f64 score.
1613fn parse_f64(frame: &Frame, cmd: &str) -> Result<f64, ProtocolError> {
1614    let bytes = extract_raw_bytes(frame)?;
1615    let s = std::str::from_utf8(bytes).map_err(|_| {
1616        ProtocolError::InvalidCommandFrame(format!("value is not a valid float for '{cmd}'"))
1617    })?;
1618    let v = s.parse::<f64>().map_err(|_| {
1619        ProtocolError::InvalidCommandFrame(format!("value is not a valid float for '{cmd}'"))
1620    })?;
1621    if v.is_nan() || v.is_infinite() {
1622        return Err(ProtocolError::InvalidCommandFrame(format!(
1623            "value is not a valid finite float for '{cmd}'"
1624        )));
1625    }
1626    Ok(v)
1627}
1628
1629fn parse_zadd(args: &[Frame]) -> Result<Command, ProtocolError> {
1630    // ZADD key [NX|XX] [GT|LT] [CH] score member [score member ...]
1631    if args.len() < 3 {
1632        return Err(wrong_arity("ZADD"));
1633    }
1634
1635    let key = extract_string(&args[0])?;
1636    let mut flags = ZAddFlags::default();
1637    let mut idx = 1;
1638
1639    // parse optional flags before score/member pairs
1640    while idx < args.len() {
1641        let mut kw = [0u8; MAX_KEYWORD_LEN];
1642        let s = uppercase_arg(&args[idx], &mut kw)?;
1643        match s {
1644            "NX" => {
1645                flags.nx = true;
1646                idx += 1;
1647            }
1648            "XX" => {
1649                flags.xx = true;
1650                idx += 1;
1651            }
1652            "GT" => {
1653                flags.gt = true;
1654                idx += 1;
1655            }
1656            "LT" => {
1657                flags.lt = true;
1658                idx += 1;
1659            }
1660            "CH" => {
1661                flags.ch = true;
1662                idx += 1;
1663            }
1664            _ => break,
1665        }
1666    }
1667
1668    // NX and XX are mutually exclusive
1669    if flags.nx && flags.xx {
1670        return Err(ProtocolError::InvalidCommandFrame(
1671            "XX and NX options at the same time are not compatible".into(),
1672        ));
1673    }
1674    // GT and LT are mutually exclusive
1675    if flags.gt && flags.lt {
1676        return Err(ProtocolError::InvalidCommandFrame(
1677            "GT and LT options at the same time are not compatible".into(),
1678        ));
1679    }
1680
1681    // remaining args must be score/member pairs
1682    let remaining = &args[idx..];
1683    if remaining.is_empty() || !remaining.len().is_multiple_of(2) {
1684        return Err(wrong_arity("ZADD"));
1685    }
1686
1687    let mut members = Vec::with_capacity(remaining.len() / 2);
1688    for pair in remaining.chunks(2) {
1689        let score = parse_f64(&pair[0], "ZADD")?;
1690        let member = extract_string(&pair[1])?;
1691        members.push((score, member));
1692    }
1693
1694    Ok(Command::ZAdd {
1695        key,
1696        flags,
1697        members,
1698    })
1699}
1700
1701fn parse_zcard(args: &[Frame]) -> Result<Command, ProtocolError> {
1702    if args.len() != 1 {
1703        return Err(wrong_arity("ZCARD"));
1704    }
1705    let key = extract_string(&args[0])?;
1706    Ok(Command::ZCard { key })
1707}
1708
1709fn parse_zrem(args: &[Frame]) -> Result<Command, ProtocolError> {
1710    if args.len() < 2 {
1711        return Err(wrong_arity("ZREM"));
1712    }
1713    let key = extract_string(&args[0])?;
1714    let members = extract_strings(&args[1..])?;
1715    Ok(Command::ZRem { key, members })
1716}
1717
1718fn parse_zscore(args: &[Frame]) -> Result<Command, ProtocolError> {
1719    if args.len() != 2 {
1720        return Err(wrong_arity("ZSCORE"));
1721    }
1722    let key = extract_string(&args[0])?;
1723    let member = extract_string(&args[1])?;
1724    Ok(Command::ZScore { key, member })
1725}
1726
1727fn parse_zrank(args: &[Frame]) -> Result<Command, ProtocolError> {
1728    if args.len() != 2 {
1729        return Err(wrong_arity("ZRANK"));
1730    }
1731    let key = extract_string(&args[0])?;
1732    let member = extract_string(&args[1])?;
1733    Ok(Command::ZRank { key, member })
1734}
1735
1736fn parse_zrange(args: &[Frame]) -> Result<Command, ProtocolError> {
1737    if args.len() < 3 || args.len() > 4 {
1738        return Err(wrong_arity("ZRANGE"));
1739    }
1740    let key = extract_string(&args[0])?;
1741    let start = parse_i64(&args[1], "ZRANGE")?;
1742    let stop = parse_i64(&args[2], "ZRANGE")?;
1743
1744    let with_scores = if args.len() == 4 {
1745        let mut kw = [0u8; MAX_KEYWORD_LEN];
1746        let opt = uppercase_arg(&args[3], &mut kw)?;
1747        if opt != "WITHSCORES" {
1748            return Err(ProtocolError::InvalidCommandFrame(format!(
1749                "unsupported ZRANGE option '{opt}'"
1750            )));
1751        }
1752        true
1753    } else {
1754        false
1755    };
1756
1757    Ok(Command::ZRange {
1758        key,
1759        start,
1760        stop,
1761        with_scores,
1762    })
1763}
1764
1765// --- hash commands ---
1766
1767fn parse_hset(args: &[Frame]) -> Result<Command, ProtocolError> {
1768    // HSET key field value [field value ...]
1769    // args = [key, field, value, ...]
1770    // Need at least 3 args, and after key we need pairs (so remaining count must be even)
1771    if args.len() < 3 || !(args.len() - 1).is_multiple_of(2) {
1772        return Err(wrong_arity("HSET"));
1773    }
1774
1775    let key = extract_string(&args[0])?;
1776    let mut fields = Vec::with_capacity((args.len() - 1) / 2);
1777
1778    for chunk in args[1..].chunks(2) {
1779        let field = extract_string(&chunk[0])?;
1780        let value = extract_bytes(&chunk[1])?;
1781        fields.push((field, value));
1782    }
1783
1784    Ok(Command::HSet { key, fields })
1785}
1786
1787fn parse_hget(args: &[Frame]) -> Result<Command, ProtocolError> {
1788    if args.len() != 2 {
1789        return Err(wrong_arity("HGET"));
1790    }
1791    let key = extract_string(&args[0])?;
1792    let field = extract_string(&args[1])?;
1793    Ok(Command::HGet { key, field })
1794}
1795
1796fn parse_hgetall(args: &[Frame]) -> Result<Command, ProtocolError> {
1797    if args.len() != 1 {
1798        return Err(wrong_arity("HGETALL"));
1799    }
1800    let key = extract_string(&args[0])?;
1801    Ok(Command::HGetAll { key })
1802}
1803
1804fn parse_hdel(args: &[Frame]) -> Result<Command, ProtocolError> {
1805    if args.len() < 2 {
1806        return Err(wrong_arity("HDEL"));
1807    }
1808    let key = extract_string(&args[0])?;
1809    let fields = extract_strings(&args[1..])?;
1810    Ok(Command::HDel { key, fields })
1811}
1812
1813fn parse_hexists(args: &[Frame]) -> Result<Command, ProtocolError> {
1814    if args.len() != 2 {
1815        return Err(wrong_arity("HEXISTS"));
1816    }
1817    let key = extract_string(&args[0])?;
1818    let field = extract_string(&args[1])?;
1819    Ok(Command::HExists { key, field })
1820}
1821
1822fn parse_hlen(args: &[Frame]) -> Result<Command, ProtocolError> {
1823    if args.len() != 1 {
1824        return Err(wrong_arity("HLEN"));
1825    }
1826    let key = extract_string(&args[0])?;
1827    Ok(Command::HLen { key })
1828}
1829
1830fn parse_hincrby(args: &[Frame]) -> Result<Command, ProtocolError> {
1831    if args.len() != 3 {
1832        return Err(wrong_arity("HINCRBY"));
1833    }
1834    let key = extract_string(&args[0])?;
1835    let field = extract_string(&args[1])?;
1836    let delta = parse_i64(&args[2], "HINCRBY")?;
1837    Ok(Command::HIncrBy { key, field, delta })
1838}
1839
1840fn parse_hkeys(args: &[Frame]) -> Result<Command, ProtocolError> {
1841    if args.len() != 1 {
1842        return Err(wrong_arity("HKEYS"));
1843    }
1844    let key = extract_string(&args[0])?;
1845    Ok(Command::HKeys { key })
1846}
1847
1848fn parse_hvals(args: &[Frame]) -> Result<Command, ProtocolError> {
1849    if args.len() != 1 {
1850        return Err(wrong_arity("HVALS"));
1851    }
1852    let key = extract_string(&args[0])?;
1853    Ok(Command::HVals { key })
1854}
1855
1856fn parse_hmget(args: &[Frame]) -> Result<Command, ProtocolError> {
1857    if args.len() < 2 {
1858        return Err(wrong_arity("HMGET"));
1859    }
1860    let key = extract_string(&args[0])?;
1861    let fields = extract_strings(&args[1..])?;
1862    Ok(Command::HMGet { key, fields })
1863}
1864
1865// --- set commands ---
1866
1867fn parse_sadd(args: &[Frame]) -> Result<Command, ProtocolError> {
1868    if args.len() < 2 {
1869        return Err(wrong_arity("SADD"));
1870    }
1871    let key = extract_string(&args[0])?;
1872    let members = extract_strings(&args[1..])?;
1873    Ok(Command::SAdd { key, members })
1874}
1875
1876fn parse_srem(args: &[Frame]) -> Result<Command, ProtocolError> {
1877    if args.len() < 2 {
1878        return Err(wrong_arity("SREM"));
1879    }
1880    let key = extract_string(&args[0])?;
1881    let members = extract_strings(&args[1..])?;
1882    Ok(Command::SRem { key, members })
1883}
1884
1885fn parse_smembers(args: &[Frame]) -> Result<Command, ProtocolError> {
1886    if args.len() != 1 {
1887        return Err(wrong_arity("SMEMBERS"));
1888    }
1889    let key = extract_string(&args[0])?;
1890    Ok(Command::SMembers { key })
1891}
1892
1893fn parse_sismember(args: &[Frame]) -> Result<Command, ProtocolError> {
1894    if args.len() != 2 {
1895        return Err(wrong_arity("SISMEMBER"));
1896    }
1897    let key = extract_string(&args[0])?;
1898    let member = extract_string(&args[1])?;
1899    Ok(Command::SIsMember { key, member })
1900}
1901
1902fn parse_scard(args: &[Frame]) -> Result<Command, ProtocolError> {
1903    if args.len() != 1 {
1904        return Err(wrong_arity("SCARD"));
1905    }
1906    let key = extract_string(&args[0])?;
1907    Ok(Command::SCard { key })
1908}
1909
1910// --- cluster commands ---
1911
1912fn parse_cluster(args: &[Frame]) -> Result<Command, ProtocolError> {
1913    if args.is_empty() {
1914        return Err(wrong_arity("CLUSTER"));
1915    }
1916
1917    let mut kw = [0u8; MAX_KEYWORD_LEN];
1918    let subcommand = uppercase_arg(&args[0], &mut kw)?;
1919    match subcommand {
1920        "INFO" => {
1921            if args.len() != 1 {
1922                return Err(wrong_arity("CLUSTER INFO"));
1923            }
1924            Ok(Command::ClusterInfo)
1925        }
1926        "NODES" => {
1927            if args.len() != 1 {
1928                return Err(wrong_arity("CLUSTER NODES"));
1929            }
1930            Ok(Command::ClusterNodes)
1931        }
1932        "SLOTS" => {
1933            if args.len() != 1 {
1934                return Err(wrong_arity("CLUSTER SLOTS"));
1935            }
1936            Ok(Command::ClusterSlots)
1937        }
1938        "KEYSLOT" => {
1939            if args.len() != 2 {
1940                return Err(wrong_arity("CLUSTER KEYSLOT"));
1941            }
1942            let key = extract_string(&args[1])?;
1943            Ok(Command::ClusterKeySlot { key })
1944        }
1945        "MYID" => {
1946            if args.len() != 1 {
1947                return Err(wrong_arity("CLUSTER MYID"));
1948            }
1949            Ok(Command::ClusterMyId)
1950        }
1951        "SETSLOT" => parse_cluster_setslot(&args[1..]),
1952        "MEET" => {
1953            if args.len() != 3 {
1954                return Err(wrong_arity("CLUSTER MEET"));
1955            }
1956            let ip = extract_string(&args[1])?;
1957            let p = parse_u64(&args[2], "CLUSTER MEET")?;
1958            let port = u16::try_from(p)
1959                .map_err(|_| ProtocolError::InvalidCommandFrame("invalid port number".into()))?;
1960            Ok(Command::ClusterMeet { ip, port })
1961        }
1962        "ADDSLOTS" => {
1963            if args.len() < 2 {
1964                return Err(wrong_arity("CLUSTER ADDSLOTS"));
1965            }
1966            let slots = parse_slot_list(&args[1..])?;
1967            Ok(Command::ClusterAddSlots { slots })
1968        }
1969        "ADDSLOTSRANGE" => {
1970            // arguments are pairs: start1 end1 [start2 end2 ...]
1971            if args.len() < 3 || !(args.len() - 1).is_multiple_of(2) {
1972                return Err(wrong_arity("CLUSTER ADDSLOTSRANGE"));
1973            }
1974            let mut ranges = Vec::new();
1975            for pair in args[1..].chunks(2) {
1976                let s = parse_u64(&pair[0], "CLUSTER ADDSLOTSRANGE")?;
1977                let start = u16::try_from(s)
1978                    .map_err(|_| ProtocolError::InvalidCommandFrame("invalid slot".into()))?;
1979                let e = parse_u64(&pair[1], "CLUSTER ADDSLOTSRANGE")?;
1980                let end = u16::try_from(e)
1981                    .map_err(|_| ProtocolError::InvalidCommandFrame("invalid slot".into()))?;
1982                if start > end || end >= 16384 {
1983                    return Err(ProtocolError::InvalidCommandFrame(
1984                        "invalid slot range: start must be <= end and both must be 0-16383".into(),
1985                    ));
1986                }
1987                ranges.push((start, end));
1988            }
1989            Ok(Command::ClusterAddSlotsRange { ranges })
1990        }
1991        "DELSLOTS" => {
1992            if args.len() < 2 {
1993                return Err(wrong_arity("CLUSTER DELSLOTS"));
1994            }
1995            let slots = parse_slot_list(&args[1..])?;
1996            Ok(Command::ClusterDelSlots { slots })
1997        }
1998        "FORGET" => {
1999            if args.len() != 2 {
2000                return Err(wrong_arity("CLUSTER FORGET"));
2001            }
2002            let node_id = extract_string(&args[1])?;
2003            Ok(Command::ClusterForget { node_id })
2004        }
2005        "REPLICATE" => {
2006            if args.len() != 2 {
2007                return Err(wrong_arity("CLUSTER REPLICATE"));
2008            }
2009            let node_id = extract_string(&args[1])?;
2010            Ok(Command::ClusterReplicate { node_id })
2011        }
2012        "FAILOVER" => {
2013            let mut force = false;
2014            let mut takeover = false;
2015            for arg in &args[1..] {
2016                let mut kw2 = [0u8; MAX_KEYWORD_LEN];
2017                let opt = uppercase_arg(arg, &mut kw2)?;
2018                match opt {
2019                    "FORCE" => force = true,
2020                    "TAKEOVER" => takeover = true,
2021                    _ => {
2022                        return Err(ProtocolError::InvalidCommandFrame(format!(
2023                            "unknown CLUSTER FAILOVER option '{opt}'"
2024                        )))
2025                    }
2026                }
2027            }
2028            Ok(Command::ClusterFailover { force, takeover })
2029        }
2030        "COUNTKEYSINSLOT" => {
2031            if args.len() != 2 {
2032                return Err(wrong_arity("CLUSTER COUNTKEYSINSLOT"));
2033            }
2034            let n = parse_u64(&args[1], "CLUSTER COUNTKEYSINSLOT")?;
2035            let slot = u16::try_from(n)
2036                .map_err(|_| ProtocolError::InvalidCommandFrame("invalid slot number".into()))?;
2037            Ok(Command::ClusterCountKeysInSlot { slot })
2038        }
2039        "GETKEYSINSLOT" => {
2040            if args.len() != 3 {
2041                return Err(wrong_arity("CLUSTER GETKEYSINSLOT"));
2042            }
2043            let n = parse_u64(&args[1], "CLUSTER GETKEYSINSLOT")?;
2044            let slot = u16::try_from(n)
2045                .map_err(|_| ProtocolError::InvalidCommandFrame("invalid slot number".into()))?;
2046            let c = parse_u64(&args[2], "CLUSTER GETKEYSINSLOT")?;
2047            let count = u32::try_from(c)
2048                .map_err(|_| ProtocolError::InvalidCommandFrame("invalid count".into()))?;
2049            Ok(Command::ClusterGetKeysInSlot { slot, count })
2050        }
2051        _ => Err(ProtocolError::InvalidCommandFrame(format!(
2052            "unknown CLUSTER subcommand '{subcommand}'"
2053        ))),
2054    }
2055}
2056
2057fn parse_asking(args: &[Frame]) -> Result<Command, ProtocolError> {
2058    if !args.is_empty() {
2059        return Err(wrong_arity("ASKING"));
2060    }
2061    Ok(Command::Asking)
2062}
2063
2064fn parse_no_args(
2065    name: &'static str,
2066    args: &[Frame],
2067    cmd: Command,
2068) -> Result<Command, ProtocolError> {
2069    if !args.is_empty() {
2070        return Err(wrong_arity(name));
2071    }
2072    Ok(cmd)
2073}
2074
2075fn parse_config(args: &[Frame]) -> Result<Command, ProtocolError> {
2076    if args.is_empty() {
2077        return Err(wrong_arity("CONFIG"));
2078    }
2079
2080    let mut kw = [0u8; MAX_KEYWORD_LEN];
2081    let subcmd = uppercase_arg(&args[0], &mut kw)?;
2082    match subcmd {
2083        "GET" => {
2084            if args.len() != 2 {
2085                return Err(wrong_arity("CONFIG|GET"));
2086            }
2087            let pattern = extract_string(&args[1])?;
2088            Ok(Command::ConfigGet { pattern })
2089        }
2090        "SET" => {
2091            if args.len() != 3 {
2092                return Err(wrong_arity("CONFIG|SET"));
2093            }
2094            let param = extract_string(&args[1])?;
2095            let value = extract_string(&args[2])?;
2096            Ok(Command::ConfigSet { param, value })
2097        }
2098        "REWRITE" => {
2099            if args.len() != 1 {
2100                return Err(wrong_arity("CONFIG|REWRITE"));
2101            }
2102            Ok(Command::ConfigRewrite)
2103        }
2104        other => Err(ProtocolError::InvalidCommandFrame(format!(
2105            "unknown CONFIG subcommand '{other}'"
2106        ))),
2107    }
2108}
2109
2110fn parse_slowlog(args: &[Frame]) -> Result<Command, ProtocolError> {
2111    if args.is_empty() {
2112        return Err(wrong_arity("SLOWLOG"));
2113    }
2114
2115    let mut kw = [0u8; MAX_KEYWORD_LEN];
2116    let subcmd = uppercase_arg(&args[0], &mut kw)?;
2117    match subcmd {
2118        "GET" => {
2119            let count = if args.len() > 1 {
2120                Some(parse_u64(&args[1], "SLOWLOG")? as usize)
2121            } else {
2122                None
2123            };
2124            Ok(Command::SlowLogGet { count })
2125        }
2126        "LEN" => Ok(Command::SlowLogLen),
2127        "RESET" => Ok(Command::SlowLogReset),
2128        other => Err(ProtocolError::InvalidCommandFrame(format!(
2129            "unknown SLOWLOG subcommand '{other}'"
2130        ))),
2131    }
2132}
2133
2134fn parse_slot_list(args: &[Frame]) -> Result<Vec<u16>, ProtocolError> {
2135    let mut slots = Vec::with_capacity(args.len());
2136    for arg in args {
2137        let n = parse_u64(arg, "CLUSTER")?;
2138        let slot = u16::try_from(n)
2139            .map_err(|_| ProtocolError::InvalidCommandFrame("invalid slot number".into()))?;
2140        if slot >= 16384 {
2141            return Err(ProtocolError::InvalidCommandFrame(format!(
2142                "invalid slot {slot}: must be 0-16383"
2143            )));
2144        }
2145        slots.push(slot);
2146    }
2147    Ok(slots)
2148}
2149
2150fn parse_cluster_setslot(args: &[Frame]) -> Result<Command, ProtocolError> {
2151    if args.is_empty() {
2152        return Err(wrong_arity("CLUSTER SETSLOT"));
2153    }
2154
2155    let n = parse_u64(&args[0], "CLUSTER SETSLOT")?;
2156    let slot = u16::try_from(n)
2157        .map_err(|_| ProtocolError::InvalidCommandFrame("invalid slot number".into()))?;
2158    if slot >= 16384 {
2159        return Err(ProtocolError::InvalidCommandFrame(format!(
2160            "invalid slot {slot}: must be 0-16383"
2161        )));
2162    }
2163
2164    if args.len() < 2 {
2165        return Err(wrong_arity("CLUSTER SETSLOT"));
2166    }
2167
2168    let mut kw = [0u8; MAX_KEYWORD_LEN];
2169    let action = uppercase_arg(&args[1], &mut kw)?;
2170    match action {
2171        "IMPORTING" => {
2172            if args.len() != 3 {
2173                return Err(ProtocolError::WrongArity(
2174                    "CLUSTER SETSLOT IMPORTING".into(),
2175                ));
2176            }
2177            let node_id = extract_string(&args[2])?;
2178            Ok(Command::ClusterSetSlotImporting { slot, node_id })
2179        }
2180        "MIGRATING" => {
2181            if args.len() != 3 {
2182                return Err(ProtocolError::WrongArity(
2183                    "CLUSTER SETSLOT MIGRATING".into(),
2184                ));
2185            }
2186            let node_id = extract_string(&args[2])?;
2187            Ok(Command::ClusterSetSlotMigrating { slot, node_id })
2188        }
2189        "NODE" => {
2190            if args.len() != 3 {
2191                return Err(wrong_arity("CLUSTER SETSLOT NODE"));
2192            }
2193            let node_id = extract_string(&args[2])?;
2194            Ok(Command::ClusterSetSlotNode { slot, node_id })
2195        }
2196        "STABLE" => {
2197            if args.len() != 2 {
2198                return Err(wrong_arity("CLUSTER SETSLOT STABLE"));
2199            }
2200            Ok(Command::ClusterSetSlotStable { slot })
2201        }
2202        _ => Err(ProtocolError::InvalidCommandFrame(format!(
2203            "unknown CLUSTER SETSLOT action '{action}'"
2204        ))),
2205    }
2206}
2207
2208fn parse_migrate(args: &[Frame]) -> Result<Command, ProtocolError> {
2209    // MIGRATE host port key db timeout [COPY] [REPLACE]
2210    if args.len() < 5 {
2211        return Err(wrong_arity("MIGRATE"));
2212    }
2213
2214    let host = extract_string(&args[0])?;
2215    let p = parse_u64(&args[1], "MIGRATE")?;
2216    let port = u16::try_from(p)
2217        .map_err(|_| ProtocolError::InvalidCommandFrame("invalid port number".into()))?;
2218    let key = extract_string(&args[2])?;
2219    let d = parse_u64(&args[3], "MIGRATE")?;
2220    let db = u32::try_from(d)
2221        .map_err(|_| ProtocolError::InvalidCommandFrame("invalid db number".into()))?;
2222    let timeout_ms = parse_u64(&args[4], "MIGRATE")?;
2223
2224    let mut copy = false;
2225    let mut replace = false;
2226
2227    for arg in &args[5..] {
2228        let mut kw = [0u8; MAX_KEYWORD_LEN];
2229        let opt = uppercase_arg(arg, &mut kw)?;
2230        match opt {
2231            "COPY" => copy = true,
2232            "REPLACE" => replace = true,
2233            _ => {
2234                return Err(ProtocolError::InvalidCommandFrame(format!(
2235                    "unknown MIGRATE option '{opt}'"
2236                )))
2237            }
2238        }
2239    }
2240
2241    Ok(Command::Migrate {
2242        host,
2243        port,
2244        key,
2245        db,
2246        timeout_ms,
2247        copy,
2248        replace,
2249    })
2250}
2251
2252fn parse_restore(args: &[Frame]) -> Result<Command, ProtocolError> {
2253    // RESTORE key ttl serialized-value [REPLACE]
2254    if args.len() < 3 {
2255        return Err(wrong_arity("RESTORE"));
2256    }
2257
2258    let key = extract_string(&args[0])?;
2259    let ttl_ms = parse_u64(&args[1], "RESTORE")?;
2260    let data = extract_bytes(&args[2])?;
2261
2262    let mut replace = false;
2263    for arg in &args[3..] {
2264        let mut kw = [0u8; MAX_KEYWORD_LEN];
2265        let opt = uppercase_arg(arg, &mut kw)?;
2266        if opt == "REPLACE" {
2267            replace = true;
2268        } else {
2269            return Err(ProtocolError::InvalidCommandFrame(format!(
2270                "unknown RESTORE option '{opt}'"
2271            )));
2272        }
2273    }
2274
2275    Ok(Command::Restore {
2276        key,
2277        ttl_ms,
2278        data,
2279        replace,
2280    })
2281}
2282
2283fn parse_subscribe(args: &[Frame]) -> Result<Command, ProtocolError> {
2284    if args.is_empty() {
2285        return Err(wrong_arity("SUBSCRIBE"));
2286    }
2287    let channels: Vec<String> = args.iter().map(extract_string).collect::<Result<_, _>>()?;
2288    Ok(Command::Subscribe { channels })
2289}
2290
2291fn parse_unsubscribe(args: &[Frame]) -> Result<Command, ProtocolError> {
2292    let channels: Vec<String> = args.iter().map(extract_string).collect::<Result<_, _>>()?;
2293    Ok(Command::Unsubscribe { channels })
2294}
2295
2296fn parse_psubscribe(args: &[Frame]) -> Result<Command, ProtocolError> {
2297    if args.is_empty() {
2298        return Err(wrong_arity("PSUBSCRIBE"));
2299    }
2300    let patterns: Vec<String> = args.iter().map(extract_string).collect::<Result<_, _>>()?;
2301    Ok(Command::PSubscribe { patterns })
2302}
2303
2304fn parse_punsubscribe(args: &[Frame]) -> Result<Command, ProtocolError> {
2305    let patterns: Vec<String> = args.iter().map(extract_string).collect::<Result<_, _>>()?;
2306    Ok(Command::PUnsubscribe { patterns })
2307}
2308
2309fn parse_publish(args: &[Frame]) -> Result<Command, ProtocolError> {
2310    if args.len() != 2 {
2311        return Err(wrong_arity("PUBLISH"));
2312    }
2313    let channel = extract_string(&args[0])?;
2314    let message = extract_bytes(&args[1])?;
2315    Ok(Command::Publish { channel, message })
2316}
2317
2318fn parse_pubsub(args: &[Frame]) -> Result<Command, ProtocolError> {
2319    if args.is_empty() {
2320        return Err(wrong_arity("PUBSUB"));
2321    }
2322
2323    let mut kw = [0u8; MAX_KEYWORD_LEN];
2324    let subcmd = uppercase_arg(&args[0], &mut kw)?;
2325    match subcmd {
2326        "CHANNELS" => {
2327            let pattern = if args.len() > 1 {
2328                Some(extract_string(&args[1])?)
2329            } else {
2330                None
2331            };
2332            Ok(Command::PubSubChannels { pattern })
2333        }
2334        "NUMSUB" => {
2335            let channels: Vec<String> = args[1..]
2336                .iter()
2337                .map(extract_string)
2338                .collect::<Result<_, _>>()?;
2339            Ok(Command::PubSubNumSub { channels })
2340        }
2341        "NUMPAT" => Ok(Command::PubSubNumPat),
2342        other => Err(ProtocolError::InvalidCommandFrame(format!(
2343            "unknown PUBSUB subcommand '{other}'"
2344        ))),
2345    }
2346}
2347
2348// --- vector command parsers ---
2349
2350/// Parses METRIC / QUANT / M / EF flags from a slice of command arguments.
2351///
2352/// Returns `(metric, quantization, connectivity, expansion_add)`.
2353/// `cmd` is used in error messages (e.g., "VADD" or "VADD_BATCH").
2354fn parse_vector_flags(
2355    args: &[Frame],
2356    cmd: &'static str,
2357) -> Result<(u8, u8, u32, u32), ProtocolError> {
2358    let mut metric: u8 = 0; // cosine default
2359    let mut quantization: u8 = 0; // f32 default
2360    let mut connectivity: u32 = 16;
2361    let mut expansion_add: u32 = 64;
2362    let mut idx = 0;
2363
2364    while idx < args.len() {
2365        let mut kw = [0u8; MAX_KEYWORD_LEN];
2366        let flag = uppercase_arg(&args[idx], &mut kw)?;
2367        match flag {
2368            "METRIC" => {
2369                idx += 1;
2370                if idx >= args.len() {
2371                    return Err(ProtocolError::InvalidCommandFrame(format!(
2372                        "{cmd}: METRIC requires a value"
2373                    )));
2374                }
2375                let mut kw2 = [0u8; MAX_KEYWORD_LEN];
2376                let val = uppercase_arg(&args[idx], &mut kw2)?;
2377                metric = match val {
2378                    "COSINE" => 0,
2379                    "L2" => 1,
2380                    "IP" => 2,
2381                    _ => {
2382                        return Err(ProtocolError::InvalidCommandFrame(format!(
2383                            "{cmd}: unknown metric '{val}'"
2384                        )))
2385                    }
2386                };
2387                idx += 1;
2388            }
2389            "QUANT" => {
2390                idx += 1;
2391                if idx >= args.len() {
2392                    return Err(ProtocolError::InvalidCommandFrame(format!(
2393                        "{cmd}: QUANT requires a value"
2394                    )));
2395                }
2396                let mut kw2 = [0u8; MAX_KEYWORD_LEN];
2397                let val = uppercase_arg(&args[idx], &mut kw2)?;
2398                quantization = match val {
2399                    "F32" => 0,
2400                    "F16" => 1,
2401                    "I8" | "Q8" => 2,
2402                    _ => {
2403                        return Err(ProtocolError::InvalidCommandFrame(format!(
2404                            "{cmd}: unknown quantization '{val}'"
2405                        )))
2406                    }
2407                };
2408                idx += 1;
2409            }
2410            "M" => {
2411                idx += 1;
2412                if idx >= args.len() {
2413                    return Err(ProtocolError::InvalidCommandFrame(format!(
2414                        "{cmd}: M requires a value"
2415                    )));
2416                }
2417                let m = parse_u64(&args[idx], cmd)?;
2418                if m > MAX_HNSW_PARAM {
2419                    return Err(ProtocolError::InvalidCommandFrame(format!(
2420                        "{cmd}: M value {m} exceeds max {MAX_HNSW_PARAM}"
2421                    )));
2422                }
2423                connectivity = m as u32;
2424                idx += 1;
2425            }
2426            "EF" => {
2427                idx += 1;
2428                if idx >= args.len() {
2429                    return Err(ProtocolError::InvalidCommandFrame(format!(
2430                        "{cmd}: EF requires a value"
2431                    )));
2432                }
2433                let ef = parse_u64(&args[idx], cmd)?;
2434                if ef > MAX_HNSW_PARAM {
2435                    return Err(ProtocolError::InvalidCommandFrame(format!(
2436                        "{cmd}: EF value {ef} exceeds max {MAX_HNSW_PARAM}"
2437                    )));
2438                }
2439                expansion_add = ef as u32;
2440                idx += 1;
2441            }
2442            _ => {
2443                return Err(ProtocolError::InvalidCommandFrame(format!(
2444                    "{cmd}: unexpected argument '{flag}'"
2445                )));
2446            }
2447        }
2448    }
2449
2450    Ok((metric, quantization, connectivity, expansion_add))
2451}
2452
2453/// VADD key element f32 [f32 ...] [METRIC COSINE|L2|IP] [QUANT F32|F16|I8] [M n] [EF n]
2454fn parse_vadd(args: &[Frame]) -> Result<Command, ProtocolError> {
2455    // minimum: key + element + at least one float
2456    if args.len() < 3 {
2457        return Err(wrong_arity("VADD"));
2458    }
2459
2460    let key = extract_string(&args[0])?;
2461    let element = extract_string(&args[1])?;
2462
2463    // parse vector values until we hit a non-numeric argument, end, or dim limit
2464    let mut idx = 2;
2465    let mut vector = Vec::new();
2466    while idx < args.len() {
2467        if vector.len() >= MAX_VECTOR_DIMS {
2468            return Err(ProtocolError::InvalidCommandFrame(format!(
2469                "VADD: vector exceeds {MAX_VECTOR_DIMS} dimensions"
2470            )));
2471        }
2472        let s = extract_string(&args[idx])?;
2473        if let Ok(v) = s.parse::<f32>() {
2474            if v.is_nan() || v.is_infinite() {
2475                return Err(ProtocolError::InvalidCommandFrame(
2476                    "VADD: vector components must be finite (no NaN/infinity)".into(),
2477                ));
2478            }
2479            vector.push(v);
2480            idx += 1;
2481        } else {
2482            break;
2483        }
2484    }
2485
2486    if vector.is_empty() {
2487        return Err(ProtocolError::InvalidCommandFrame(
2488            "VADD: at least one vector dimension required".into(),
2489        ));
2490    }
2491
2492    // parse optional flags
2493    let (metric, quantization, connectivity, expansion_add) =
2494        parse_vector_flags(&args[idx..], "VADD")?;
2495
2496    Ok(Command::VAdd {
2497        key,
2498        element,
2499        vector,
2500        metric,
2501        quantization,
2502        connectivity,
2503        expansion_add,
2504    })
2505}
2506
2507/// VADD_BATCH key DIM n element1 f32... element2 f32... [METRIC COSINE|L2|IP]
2508/// [QUANT F32|F16|I8] [M n] [EF n]
2509fn parse_vadd_batch(args: &[Frame]) -> Result<Command, ProtocolError> {
2510    // minimum: key + DIM + n (even an empty batch needs the DIM declaration)
2511    if args.len() < 3 {
2512        return Err(wrong_arity("VADD_BATCH"));
2513    }
2514
2515    let key = extract_string(&args[0])?;
2516
2517    // require DIM keyword
2518    let mut kw = [0u8; MAX_KEYWORD_LEN];
2519    let dim_kw = uppercase_arg(&args[1], &mut kw)?;
2520    if dim_kw != "DIM" {
2521        return Err(ProtocolError::InvalidCommandFrame(
2522            "VADD_BATCH: expected DIM keyword".into(),
2523        ));
2524    }
2525
2526    let dim = parse_u64(&args[2], "VADD_BATCH")? as usize;
2527    if dim == 0 {
2528        return Err(ProtocolError::InvalidCommandFrame(
2529            "VADD_BATCH: DIM must be at least 1".into(),
2530        ));
2531    }
2532    if dim > MAX_VECTOR_DIMS {
2533        return Err(ProtocolError::InvalidCommandFrame(format!(
2534            "VADD_BATCH: DIM {dim} exceeds max {MAX_VECTOR_DIMS}"
2535        )));
2536    }
2537
2538    // parse entries: each is element_name followed by exactly `dim` floats.
2539    // we detect the end of entries by checking whether enough args remain
2540    // for a full entry (1 name + dim floats). this avoids misinterpreting
2541    // element names like "metric" as flags.
2542    let mut idx = 3;
2543    let mut entries: Vec<(String, Vec<f32>)> = Vec::new();
2544    let entry_len = 1 + dim; // element name + dim floats
2545
2546    while idx < args.len() {
2547        // not enough remaining args for a full entry — must be flags
2548        if idx + entry_len > args.len() {
2549            break;
2550        }
2551
2552        // peek: if the token after the element name isn't a valid float,
2553        // we've reached the flags section
2554        if dim > 0 {
2555            let peek = extract_string(&args[idx + 1])?;
2556            if peek.parse::<f32>().is_err() {
2557                break;
2558            }
2559        }
2560
2561        let element = extract_string(&args[idx])?;
2562        idx += 1;
2563
2564        let mut vector = Vec::with_capacity(dim);
2565        for _ in 0..dim {
2566            let s = extract_string(&args[idx])?;
2567            let v = s.parse::<f32>().map_err(|_| {
2568                ProtocolError::InvalidCommandFrame(format!("VADD_BATCH: expected float, got '{s}'"))
2569            })?;
2570            if v.is_nan() || v.is_infinite() {
2571                return Err(ProtocolError::InvalidCommandFrame(
2572                    "VADD_BATCH: vector components must be finite (no NaN/infinity)".into(),
2573                ));
2574            }
2575            vector.push(v);
2576            idx += 1;
2577        }
2578
2579        entries.push((element, vector));
2580
2581        if entries.len() >= MAX_VADD_BATCH_SIZE {
2582            return Err(ProtocolError::InvalidCommandFrame(format!(
2583                "VADD_BATCH: batch size exceeds max {MAX_VADD_BATCH_SIZE}"
2584            )));
2585        }
2586    }
2587
2588    // parse optional flags (same logic as parse_vadd)
2589    let (metric, quantization, connectivity, expansion_add) =
2590        parse_vector_flags(&args[idx..], "VADD_BATCH")?;
2591
2592    Ok(Command::VAddBatch {
2593        key,
2594        entries,
2595        dim,
2596        metric,
2597        quantization,
2598        connectivity,
2599        expansion_add,
2600    })
2601}
2602
2603/// VSIM key f32 [f32 ...] COUNT k [EF n] [WITHSCORES]
2604fn parse_vsim(args: &[Frame]) -> Result<Command, ProtocolError> {
2605    // minimum: key + at least one float + COUNT + k
2606    if args.len() < 4 {
2607        return Err(wrong_arity("VSIM"));
2608    }
2609
2610    let key = extract_string(&args[0])?;
2611
2612    // parse query vector until we hit a non-numeric argument, end, or dim limit
2613    let mut idx = 1;
2614    let mut query = Vec::new();
2615    while idx < args.len() {
2616        if query.len() >= MAX_VECTOR_DIMS {
2617            return Err(ProtocolError::InvalidCommandFrame(format!(
2618                "VSIM: query exceeds {MAX_VECTOR_DIMS} dimensions"
2619            )));
2620        }
2621        let s = extract_string(&args[idx])?;
2622        if let Ok(v) = s.parse::<f32>() {
2623            if v.is_nan() || v.is_infinite() {
2624                return Err(ProtocolError::InvalidCommandFrame(
2625                    "VSIM: query components must be finite (no NaN/infinity)".into(),
2626                ));
2627            }
2628            query.push(v);
2629            idx += 1;
2630        } else {
2631            break;
2632        }
2633    }
2634
2635    if query.is_empty() {
2636        return Err(ProtocolError::InvalidCommandFrame(
2637            "VSIM: at least one query dimension required".into(),
2638        ));
2639    }
2640
2641    // COUNT k is required
2642    let mut count: Option<usize> = None;
2643    let mut ef_search: usize = 0;
2644    let mut with_scores = false;
2645
2646    while idx < args.len() {
2647        let mut kw = [0u8; MAX_KEYWORD_LEN];
2648        let flag = uppercase_arg(&args[idx], &mut kw)?;
2649        match flag {
2650            "COUNT" => {
2651                idx += 1;
2652                if idx >= args.len() {
2653                    return Err(ProtocolError::InvalidCommandFrame(
2654                        "VSIM: COUNT requires a value".into(),
2655                    ));
2656                }
2657                let c = parse_u64(&args[idx], "VSIM")?;
2658                if c > MAX_VSIM_COUNT {
2659                    return Err(ProtocolError::InvalidCommandFrame(format!(
2660                        "VSIM: COUNT {c} exceeds max {MAX_VSIM_COUNT}"
2661                    )));
2662                }
2663                count = Some(c as usize);
2664                idx += 1;
2665            }
2666            "EF" => {
2667                idx += 1;
2668                if idx >= args.len() {
2669                    return Err(ProtocolError::InvalidCommandFrame(
2670                        "VSIM: EF requires a value".into(),
2671                    ));
2672                }
2673                let ef = parse_u64(&args[idx], "VSIM")?;
2674                if ef > MAX_VSIM_EF {
2675                    return Err(ProtocolError::InvalidCommandFrame(format!(
2676                        "VSIM: EF {ef} exceeds max {MAX_VSIM_EF}"
2677                    )));
2678                }
2679                ef_search = ef as usize;
2680                idx += 1;
2681            }
2682            "WITHSCORES" => {
2683                with_scores = true;
2684                idx += 1;
2685            }
2686            _ => {
2687                return Err(ProtocolError::InvalidCommandFrame(format!(
2688                    "VSIM: unexpected argument '{flag}'"
2689                )));
2690            }
2691        }
2692    }
2693
2694    let count = count
2695        .ok_or_else(|| ProtocolError::InvalidCommandFrame("VSIM: COUNT is required".into()))?;
2696
2697    Ok(Command::VSim {
2698        key,
2699        query,
2700        count,
2701        ef_search,
2702        with_scores,
2703    })
2704}
2705
2706/// VREM key element
2707fn parse_vrem(args: &[Frame]) -> Result<Command, ProtocolError> {
2708    if args.len() != 2 {
2709        return Err(wrong_arity("VREM"));
2710    }
2711    let key = extract_string(&args[0])?;
2712    let element = extract_string(&args[1])?;
2713    Ok(Command::VRem { key, element })
2714}
2715
2716/// VGET key element
2717fn parse_vget(args: &[Frame]) -> Result<Command, ProtocolError> {
2718    if args.len() != 2 {
2719        return Err(wrong_arity("VGET"));
2720    }
2721    let key = extract_string(&args[0])?;
2722    let element = extract_string(&args[1])?;
2723    Ok(Command::VGet { key, element })
2724}
2725
2726/// VCARD key
2727fn parse_vcard(args: &[Frame]) -> Result<Command, ProtocolError> {
2728    if args.len() != 1 {
2729        return Err(wrong_arity("VCARD"));
2730    }
2731    let key = extract_string(&args[0])?;
2732    Ok(Command::VCard { key })
2733}
2734
2735/// VDIM key
2736fn parse_vdim(args: &[Frame]) -> Result<Command, ProtocolError> {
2737    if args.len() != 1 {
2738        return Err(wrong_arity("VDIM"));
2739    }
2740    let key = extract_string(&args[0])?;
2741    Ok(Command::VDim { key })
2742}
2743
2744/// VINFO key
2745fn parse_vinfo(args: &[Frame]) -> Result<Command, ProtocolError> {
2746    if args.len() != 1 {
2747        return Err(wrong_arity("VINFO"));
2748    }
2749    let key = extract_string(&args[0])?;
2750    Ok(Command::VInfo { key })
2751}
2752
2753// --- proto command parsers ---
2754
2755fn parse_proto_register(args: &[Frame]) -> Result<Command, ProtocolError> {
2756    if args.len() != 2 {
2757        return Err(wrong_arity("PROTO.REGISTER"));
2758    }
2759    let name = extract_string(&args[0])?;
2760    let descriptor = extract_bytes(&args[1])?;
2761    Ok(Command::ProtoRegister { name, descriptor })
2762}
2763
2764fn parse_proto_set(args: &[Frame]) -> Result<Command, ProtocolError> {
2765    if args.len() < 3 {
2766        return Err(wrong_arity("PROTO.SET"));
2767    }
2768
2769    let key = extract_string(&args[0])?;
2770    let type_name = extract_string(&args[1])?;
2771    let data = extract_bytes(&args[2])?;
2772    let (expire, nx, xx) = parse_set_options(&args[3..], "PROTO.SET")?;
2773
2774    Ok(Command::ProtoSet {
2775        key,
2776        type_name,
2777        data,
2778        expire,
2779        nx,
2780        xx,
2781    })
2782}
2783
2784fn parse_proto_get(args: &[Frame]) -> Result<Command, ProtocolError> {
2785    if args.len() != 1 {
2786        return Err(wrong_arity("PROTO.GET"));
2787    }
2788    let key = extract_string(&args[0])?;
2789    Ok(Command::ProtoGet { key })
2790}
2791
2792fn parse_proto_type(args: &[Frame]) -> Result<Command, ProtocolError> {
2793    if args.len() != 1 {
2794        return Err(wrong_arity("PROTO.TYPE"));
2795    }
2796    let key = extract_string(&args[0])?;
2797    Ok(Command::ProtoType { key })
2798}
2799
2800fn parse_proto_schemas(args: &[Frame]) -> Result<Command, ProtocolError> {
2801    if !args.is_empty() {
2802        return Err(wrong_arity("PROTO.SCHEMAS"));
2803    }
2804    Ok(Command::ProtoSchemas)
2805}
2806
2807fn parse_proto_describe(args: &[Frame]) -> Result<Command, ProtocolError> {
2808    if args.len() != 1 {
2809        return Err(wrong_arity("PROTO.DESCRIBE"));
2810    }
2811    let name = extract_string(&args[0])?;
2812    Ok(Command::ProtoDescribe { name })
2813}
2814
2815fn parse_proto_getfield(args: &[Frame]) -> Result<Command, ProtocolError> {
2816    if args.len() != 2 {
2817        return Err(wrong_arity("PROTO.GETFIELD"));
2818    }
2819    let key = extract_string(&args[0])?;
2820    let field_path = extract_string(&args[1])?;
2821    Ok(Command::ProtoGetField { key, field_path })
2822}
2823
2824fn parse_proto_setfield(args: &[Frame]) -> Result<Command, ProtocolError> {
2825    if args.len() != 3 {
2826        return Err(wrong_arity("PROTO.SETFIELD"));
2827    }
2828    let key = extract_string(&args[0])?;
2829    let field_path = extract_string(&args[1])?;
2830    let value = extract_string(&args[2])?;
2831    Ok(Command::ProtoSetField {
2832        key,
2833        field_path,
2834        value,
2835    })
2836}
2837
2838fn parse_proto_delfield(args: &[Frame]) -> Result<Command, ProtocolError> {
2839    if args.len() != 2 {
2840        return Err(wrong_arity("PROTO.DELFIELD"));
2841    }
2842    let key = extract_string(&args[0])?;
2843    let field_path = extract_string(&args[1])?;
2844    Ok(Command::ProtoDelField { key, field_path })
2845}
2846
2847fn parse_auth(args: &[Frame]) -> Result<Command, ProtocolError> {
2848    match args.len() {
2849        1 => {
2850            let password = extract_string(&args[0])?;
2851            Ok(Command::Auth {
2852                username: None,
2853                password,
2854            })
2855        }
2856        2 => {
2857            let username = extract_string(&args[0])?;
2858            let password = extract_string(&args[1])?;
2859            Ok(Command::Auth {
2860                username: Some(username),
2861                password,
2862            })
2863        }
2864        _ => Err(wrong_arity("AUTH")),
2865    }
2866}
2867
2868fn parse_quit(args: &[Frame]) -> Result<Command, ProtocolError> {
2869    if !args.is_empty() {
2870        return Err(wrong_arity("QUIT"));
2871    }
2872    Ok(Command::Quit)
2873}
2874
2875fn parse_monitor(args: &[Frame]) -> Result<Command, ProtocolError> {
2876    if !args.is_empty() {
2877        return Err(wrong_arity("MONITOR"));
2878    }
2879    Ok(Command::Monitor)
2880}
2881
2882#[cfg(test)]
2883mod tests {
2884    use super::*;
2885
2886    /// Helper: build an array frame from bulk strings.
2887    fn cmd(parts: &[&str]) -> Frame {
2888        Frame::Array(
2889            parts
2890                .iter()
2891                .map(|s| Frame::Bulk(Bytes::from(s.to_string())))
2892                .collect(),
2893        )
2894    }
2895
2896    // --- ping ---
2897
2898    #[test]
2899    fn ping_no_args() {
2900        assert_eq!(
2901            Command::from_frame(cmd(&["PING"])).unwrap(),
2902            Command::Ping(None),
2903        );
2904    }
2905
2906    #[test]
2907    fn ping_with_message() {
2908        assert_eq!(
2909            Command::from_frame(cmd(&["PING", "hello"])).unwrap(),
2910            Command::Ping(Some(Bytes::from("hello"))),
2911        );
2912    }
2913
2914    #[test]
2915    fn ping_case_insensitive() {
2916        assert_eq!(
2917            Command::from_frame(cmd(&["ping"])).unwrap(),
2918            Command::Ping(None),
2919        );
2920        assert_eq!(
2921            Command::from_frame(cmd(&["Ping"])).unwrap(),
2922            Command::Ping(None),
2923        );
2924    }
2925
2926    #[test]
2927    fn ping_too_many_args() {
2928        let err = Command::from_frame(cmd(&["PING", "a", "b"])).unwrap_err();
2929        assert!(matches!(err, ProtocolError::WrongArity(_)));
2930    }
2931
2932    // --- echo ---
2933
2934    #[test]
2935    fn echo() {
2936        assert_eq!(
2937            Command::from_frame(cmd(&["ECHO", "test"])).unwrap(),
2938            Command::Echo(Bytes::from("test")),
2939        );
2940    }
2941
2942    #[test]
2943    fn echo_missing_arg() {
2944        let err = Command::from_frame(cmd(&["ECHO"])).unwrap_err();
2945        assert!(matches!(err, ProtocolError::WrongArity(_)));
2946    }
2947
2948    // --- get ---
2949
2950    #[test]
2951    fn get_basic() {
2952        assert_eq!(
2953            Command::from_frame(cmd(&["GET", "mykey"])).unwrap(),
2954            Command::Get {
2955                key: "mykey".into()
2956            },
2957        );
2958    }
2959
2960    #[test]
2961    fn get_no_args() {
2962        let err = Command::from_frame(cmd(&["GET"])).unwrap_err();
2963        assert!(matches!(err, ProtocolError::WrongArity(_)));
2964    }
2965
2966    #[test]
2967    fn get_too_many_args() {
2968        let err = Command::from_frame(cmd(&["GET", "a", "b"])).unwrap_err();
2969        assert!(matches!(err, ProtocolError::WrongArity(_)));
2970    }
2971
2972    #[test]
2973    fn get_case_insensitive() {
2974        assert_eq!(
2975            Command::from_frame(cmd(&["get", "k"])).unwrap(),
2976            Command::Get { key: "k".into() },
2977        );
2978    }
2979
2980    // --- set ---
2981
2982    #[test]
2983    fn set_basic() {
2984        assert_eq!(
2985            Command::from_frame(cmd(&["SET", "key", "value"])).unwrap(),
2986            Command::Set {
2987                key: "key".into(),
2988                value: Bytes::from("value"),
2989                expire: None,
2990                nx: false,
2991                xx: false,
2992            },
2993        );
2994    }
2995
2996    #[test]
2997    fn set_with_ex() {
2998        assert_eq!(
2999            Command::from_frame(cmd(&["SET", "key", "val", "EX", "10"])).unwrap(),
3000            Command::Set {
3001                key: "key".into(),
3002                value: Bytes::from("val"),
3003                expire: Some(SetExpire::Ex(10)),
3004                nx: false,
3005                xx: false,
3006            },
3007        );
3008    }
3009
3010    #[test]
3011    fn set_with_px() {
3012        assert_eq!(
3013            Command::from_frame(cmd(&["SET", "key", "val", "PX", "5000"])).unwrap(),
3014            Command::Set {
3015                key: "key".into(),
3016                value: Bytes::from("val"),
3017                expire: Some(SetExpire::Px(5000)),
3018                nx: false,
3019                xx: false,
3020            },
3021        );
3022    }
3023
3024    #[test]
3025    fn set_ex_case_insensitive() {
3026        assert_eq!(
3027            Command::from_frame(cmd(&["set", "k", "v", "ex", "5"])).unwrap(),
3028            Command::Set {
3029                key: "k".into(),
3030                value: Bytes::from("v"),
3031                expire: Some(SetExpire::Ex(5)),
3032                nx: false,
3033                xx: false,
3034            },
3035        );
3036    }
3037
3038    #[test]
3039    fn set_nx_flag() {
3040        assert_eq!(
3041            Command::from_frame(cmd(&["SET", "key", "val", "NX"])).unwrap(),
3042            Command::Set {
3043                key: "key".into(),
3044                value: Bytes::from("val"),
3045                expire: None,
3046                nx: true,
3047                xx: false,
3048            },
3049        );
3050    }
3051
3052    #[test]
3053    fn set_xx_flag() {
3054        assert_eq!(
3055            Command::from_frame(cmd(&["SET", "key", "val", "XX"])).unwrap(),
3056            Command::Set {
3057                key: "key".into(),
3058                value: Bytes::from("val"),
3059                expire: None,
3060                nx: false,
3061                xx: true,
3062            },
3063        );
3064    }
3065
3066    #[test]
3067    fn set_nx_with_ex() {
3068        assert_eq!(
3069            Command::from_frame(cmd(&["SET", "key", "val", "EX", "10", "NX"])).unwrap(),
3070            Command::Set {
3071                key: "key".into(),
3072                value: Bytes::from("val"),
3073                expire: Some(SetExpire::Ex(10)),
3074                nx: true,
3075                xx: false,
3076            },
3077        );
3078    }
3079
3080    #[test]
3081    fn set_nx_before_ex() {
3082        assert_eq!(
3083            Command::from_frame(cmd(&["SET", "key", "val", "NX", "PX", "5000"])).unwrap(),
3084            Command::Set {
3085                key: "key".into(),
3086                value: Bytes::from("val"),
3087                expire: Some(SetExpire::Px(5000)),
3088                nx: true,
3089                xx: false,
3090            },
3091        );
3092    }
3093
3094    #[test]
3095    fn set_nx_xx_conflict() {
3096        let err = Command::from_frame(cmd(&["SET", "k", "v", "NX", "XX"])).unwrap_err();
3097        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3098    }
3099
3100    #[test]
3101    fn set_nx_case_insensitive() {
3102        assert_eq!(
3103            Command::from_frame(cmd(&["set", "k", "v", "nx"])).unwrap(),
3104            Command::Set {
3105                key: "k".into(),
3106                value: Bytes::from("v"),
3107                expire: None,
3108                nx: true,
3109                xx: false,
3110            },
3111        );
3112    }
3113
3114    #[test]
3115    fn set_missing_value() {
3116        let err = Command::from_frame(cmd(&["SET", "key"])).unwrap_err();
3117        assert!(matches!(err, ProtocolError::WrongArity(_)));
3118    }
3119
3120    #[test]
3121    fn set_invalid_expire_value() {
3122        let err = Command::from_frame(cmd(&["SET", "k", "v", "EX", "notanum"])).unwrap_err();
3123        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3124    }
3125
3126    #[test]
3127    fn set_zero_expire() {
3128        let err = Command::from_frame(cmd(&["SET", "k", "v", "EX", "0"])).unwrap_err();
3129        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3130    }
3131
3132    #[test]
3133    fn set_unknown_flag() {
3134        let err = Command::from_frame(cmd(&["SET", "k", "v", "ZZ", "10"])).unwrap_err();
3135        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3136    }
3137
3138    #[test]
3139    fn set_incomplete_expire() {
3140        // EX without a value
3141        let err = Command::from_frame(cmd(&["SET", "k", "v", "EX"])).unwrap_err();
3142        assert!(matches!(err, ProtocolError::WrongArity(_)));
3143    }
3144
3145    // --- del ---
3146
3147    #[test]
3148    fn del_single() {
3149        assert_eq!(
3150            Command::from_frame(cmd(&["DEL", "key"])).unwrap(),
3151            Command::Del {
3152                keys: vec!["key".into()]
3153            },
3154        );
3155    }
3156
3157    #[test]
3158    fn del_multiple() {
3159        assert_eq!(
3160            Command::from_frame(cmd(&["DEL", "a", "b", "c"])).unwrap(),
3161            Command::Del {
3162                keys: vec!["a".into(), "b".into(), "c".into()]
3163            },
3164        );
3165    }
3166
3167    #[test]
3168    fn del_no_args() {
3169        let err = Command::from_frame(cmd(&["DEL"])).unwrap_err();
3170        assert!(matches!(err, ProtocolError::WrongArity(_)));
3171    }
3172
3173    // --- exists ---
3174
3175    #[test]
3176    fn exists_single() {
3177        assert_eq!(
3178            Command::from_frame(cmd(&["EXISTS", "key"])).unwrap(),
3179            Command::Exists {
3180                keys: vec!["key".into()]
3181            },
3182        );
3183    }
3184
3185    #[test]
3186    fn exists_multiple() {
3187        assert_eq!(
3188            Command::from_frame(cmd(&["EXISTS", "a", "b"])).unwrap(),
3189            Command::Exists {
3190                keys: vec!["a".into(), "b".into()]
3191            },
3192        );
3193    }
3194
3195    #[test]
3196    fn exists_no_args() {
3197        let err = Command::from_frame(cmd(&["EXISTS"])).unwrap_err();
3198        assert!(matches!(err, ProtocolError::WrongArity(_)));
3199    }
3200
3201    // --- mget ---
3202
3203    #[test]
3204    fn mget_single() {
3205        assert_eq!(
3206            Command::from_frame(cmd(&["MGET", "key"])).unwrap(),
3207            Command::MGet {
3208                keys: vec!["key".into()]
3209            },
3210        );
3211    }
3212
3213    #[test]
3214    fn mget_multiple() {
3215        assert_eq!(
3216            Command::from_frame(cmd(&["MGET", "a", "b", "c"])).unwrap(),
3217            Command::MGet {
3218                keys: vec!["a".into(), "b".into(), "c".into()]
3219            },
3220        );
3221    }
3222
3223    #[test]
3224    fn mget_no_args() {
3225        let err = Command::from_frame(cmd(&["MGET"])).unwrap_err();
3226        assert!(matches!(err, ProtocolError::WrongArity(_)));
3227    }
3228
3229    // --- mset ---
3230
3231    #[test]
3232    fn mset_single_pair() {
3233        assert_eq!(
3234            Command::from_frame(cmd(&["MSET", "key", "val"])).unwrap(),
3235            Command::MSet {
3236                pairs: vec![("key".into(), Bytes::from("val"))]
3237            },
3238        );
3239    }
3240
3241    #[test]
3242    fn mset_multiple_pairs() {
3243        assert_eq!(
3244            Command::from_frame(cmd(&["MSET", "a", "1", "b", "2"])).unwrap(),
3245            Command::MSet {
3246                pairs: vec![
3247                    ("a".into(), Bytes::from("1")),
3248                    ("b".into(), Bytes::from("2")),
3249                ]
3250            },
3251        );
3252    }
3253
3254    #[test]
3255    fn mset_no_args() {
3256        let err = Command::from_frame(cmd(&["MSET"])).unwrap_err();
3257        assert!(matches!(err, ProtocolError::WrongArity(_)));
3258    }
3259
3260    #[test]
3261    fn mset_odd_args() {
3262        // missing value for second key
3263        let err = Command::from_frame(cmd(&["MSET", "a", "1", "b"])).unwrap_err();
3264        assert!(matches!(err, ProtocolError::WrongArity(_)));
3265    }
3266
3267    // --- expire ---
3268
3269    #[test]
3270    fn expire_basic() {
3271        assert_eq!(
3272            Command::from_frame(cmd(&["EXPIRE", "key", "60"])).unwrap(),
3273            Command::Expire {
3274                key: "key".into(),
3275                seconds: 60,
3276            },
3277        );
3278    }
3279
3280    #[test]
3281    fn expire_wrong_arity() {
3282        let err = Command::from_frame(cmd(&["EXPIRE", "key"])).unwrap_err();
3283        assert!(matches!(err, ProtocolError::WrongArity(_)));
3284    }
3285
3286    #[test]
3287    fn expire_invalid_seconds() {
3288        let err = Command::from_frame(cmd(&["EXPIRE", "key", "abc"])).unwrap_err();
3289        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3290    }
3291
3292    #[test]
3293    fn expire_zero_seconds() {
3294        let err = Command::from_frame(cmd(&["EXPIRE", "key", "0"])).unwrap_err();
3295        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3296    }
3297
3298    // --- ttl ---
3299
3300    #[test]
3301    fn ttl_basic() {
3302        assert_eq!(
3303            Command::from_frame(cmd(&["TTL", "key"])).unwrap(),
3304            Command::Ttl { key: "key".into() },
3305        );
3306    }
3307
3308    #[test]
3309    fn ttl_wrong_arity() {
3310        let err = Command::from_frame(cmd(&["TTL"])).unwrap_err();
3311        assert!(matches!(err, ProtocolError::WrongArity(_)));
3312    }
3313
3314    // --- dbsize ---
3315
3316    #[test]
3317    fn dbsize_basic() {
3318        assert_eq!(
3319            Command::from_frame(cmd(&["DBSIZE"])).unwrap(),
3320            Command::DbSize,
3321        );
3322    }
3323
3324    #[test]
3325    fn dbsize_case_insensitive() {
3326        assert_eq!(
3327            Command::from_frame(cmd(&["dbsize"])).unwrap(),
3328            Command::DbSize,
3329        );
3330    }
3331
3332    #[test]
3333    fn dbsize_extra_args() {
3334        let err = Command::from_frame(cmd(&["DBSIZE", "extra"])).unwrap_err();
3335        assert!(matches!(err, ProtocolError::WrongArity(_)));
3336    }
3337
3338    // --- info ---
3339
3340    #[test]
3341    fn info_no_section() {
3342        assert_eq!(
3343            Command::from_frame(cmd(&["INFO"])).unwrap(),
3344            Command::Info { section: None },
3345        );
3346    }
3347
3348    #[test]
3349    fn info_with_section() {
3350        assert_eq!(
3351            Command::from_frame(cmd(&["INFO", "keyspace"])).unwrap(),
3352            Command::Info {
3353                section: Some("keyspace".into())
3354            },
3355        );
3356    }
3357
3358    #[test]
3359    fn info_too_many_args() {
3360        let err = Command::from_frame(cmd(&["INFO", "a", "b"])).unwrap_err();
3361        assert!(matches!(err, ProtocolError::WrongArity(_)));
3362    }
3363
3364    // --- bgsave ---
3365
3366    #[test]
3367    fn bgsave_basic() {
3368        assert_eq!(
3369            Command::from_frame(cmd(&["BGSAVE"])).unwrap(),
3370            Command::BgSave,
3371        );
3372    }
3373
3374    #[test]
3375    fn bgsave_case_insensitive() {
3376        assert_eq!(
3377            Command::from_frame(cmd(&["bgsave"])).unwrap(),
3378            Command::BgSave,
3379        );
3380    }
3381
3382    #[test]
3383    fn bgsave_extra_args() {
3384        let err = Command::from_frame(cmd(&["BGSAVE", "extra"])).unwrap_err();
3385        assert!(matches!(err, ProtocolError::WrongArity(_)));
3386    }
3387
3388    // --- bgrewriteaof ---
3389
3390    #[test]
3391    fn bgrewriteaof_basic() {
3392        assert_eq!(
3393            Command::from_frame(cmd(&["BGREWRITEAOF"])).unwrap(),
3394            Command::BgRewriteAof,
3395        );
3396    }
3397
3398    #[test]
3399    fn bgrewriteaof_case_insensitive() {
3400        assert_eq!(
3401            Command::from_frame(cmd(&["bgrewriteaof"])).unwrap(),
3402            Command::BgRewriteAof,
3403        );
3404    }
3405
3406    #[test]
3407    fn bgrewriteaof_extra_args() {
3408        let err = Command::from_frame(cmd(&["BGREWRITEAOF", "extra"])).unwrap_err();
3409        assert!(matches!(err, ProtocolError::WrongArity(_)));
3410    }
3411
3412    // --- flushdb ---
3413
3414    #[test]
3415    fn flushdb_basic() {
3416        assert_eq!(
3417            Command::from_frame(cmd(&["FLUSHDB"])).unwrap(),
3418            Command::FlushDb { async_mode: false },
3419        );
3420    }
3421
3422    #[test]
3423    fn flushdb_case_insensitive() {
3424        assert_eq!(
3425            Command::from_frame(cmd(&["flushdb"])).unwrap(),
3426            Command::FlushDb { async_mode: false },
3427        );
3428    }
3429
3430    #[test]
3431    fn flushdb_async() {
3432        assert_eq!(
3433            Command::from_frame(cmd(&["FLUSHDB", "ASYNC"])).unwrap(),
3434            Command::FlushDb { async_mode: true },
3435        );
3436    }
3437
3438    #[test]
3439    fn flushdb_async_case_insensitive() {
3440        assert_eq!(
3441            Command::from_frame(cmd(&["flushdb", "async"])).unwrap(),
3442            Command::FlushDb { async_mode: true },
3443        );
3444    }
3445
3446    #[test]
3447    fn flushdb_extra_args() {
3448        let err = Command::from_frame(cmd(&["FLUSHDB", "extra"])).unwrap_err();
3449        assert!(matches!(err, ProtocolError::WrongArity(_)));
3450    }
3451
3452    // --- unlink ---
3453
3454    #[test]
3455    fn unlink_single() {
3456        assert_eq!(
3457            Command::from_frame(cmd(&["UNLINK", "mykey"])).unwrap(),
3458            Command::Unlink {
3459                keys: vec!["mykey".into()]
3460            },
3461        );
3462    }
3463
3464    #[test]
3465    fn unlink_multiple() {
3466        assert_eq!(
3467            Command::from_frame(cmd(&["UNLINK", "a", "b", "c"])).unwrap(),
3468            Command::Unlink {
3469                keys: vec!["a".into(), "b".into(), "c".into()]
3470            },
3471        );
3472    }
3473
3474    #[test]
3475    fn unlink_no_args() {
3476        let err = Command::from_frame(cmd(&["UNLINK"])).unwrap_err();
3477        assert!(matches!(err, ProtocolError::WrongArity(_)));
3478    }
3479
3480    // --- lpush ---
3481
3482    #[test]
3483    fn lpush_single() {
3484        assert_eq!(
3485            Command::from_frame(cmd(&["LPUSH", "list", "val"])).unwrap(),
3486            Command::LPush {
3487                key: "list".into(),
3488                values: vec![Bytes::from("val")],
3489            },
3490        );
3491    }
3492
3493    #[test]
3494    fn lpush_multiple() {
3495        assert_eq!(
3496            Command::from_frame(cmd(&["LPUSH", "list", "a", "b", "c"])).unwrap(),
3497            Command::LPush {
3498                key: "list".into(),
3499                values: vec![Bytes::from("a"), Bytes::from("b"), Bytes::from("c")],
3500            },
3501        );
3502    }
3503
3504    #[test]
3505    fn lpush_no_value() {
3506        let err = Command::from_frame(cmd(&["LPUSH", "key"])).unwrap_err();
3507        assert!(matches!(err, ProtocolError::WrongArity(_)));
3508    }
3509
3510    #[test]
3511    fn lpush_case_insensitive() {
3512        assert!(matches!(
3513            Command::from_frame(cmd(&["lpush", "k", "v"])).unwrap(),
3514            Command::LPush { .. }
3515        ));
3516    }
3517
3518    // --- rpush ---
3519
3520    #[test]
3521    fn rpush_single() {
3522        assert_eq!(
3523            Command::from_frame(cmd(&["RPUSH", "list", "val"])).unwrap(),
3524            Command::RPush {
3525                key: "list".into(),
3526                values: vec![Bytes::from("val")],
3527            },
3528        );
3529    }
3530
3531    #[test]
3532    fn rpush_no_value() {
3533        let err = Command::from_frame(cmd(&["RPUSH", "key"])).unwrap_err();
3534        assert!(matches!(err, ProtocolError::WrongArity(_)));
3535    }
3536
3537    // --- lpop ---
3538
3539    #[test]
3540    fn lpop_basic() {
3541        assert_eq!(
3542            Command::from_frame(cmd(&["LPOP", "list"])).unwrap(),
3543            Command::LPop { key: "list".into() },
3544        );
3545    }
3546
3547    #[test]
3548    fn lpop_wrong_arity() {
3549        let err = Command::from_frame(cmd(&["LPOP"])).unwrap_err();
3550        assert!(matches!(err, ProtocolError::WrongArity(_)));
3551    }
3552
3553    // --- rpop ---
3554
3555    #[test]
3556    fn rpop_basic() {
3557        assert_eq!(
3558            Command::from_frame(cmd(&["RPOP", "list"])).unwrap(),
3559            Command::RPop { key: "list".into() },
3560        );
3561    }
3562
3563    // --- blpop ---
3564
3565    #[test]
3566    fn blpop_single_key() {
3567        assert_eq!(
3568            Command::from_frame(cmd(&["BLPOP", "mylist", "5"])).unwrap(),
3569            Command::BLPop {
3570                keys: vec!["mylist".into()],
3571                timeout_secs: 5.0,
3572            },
3573        );
3574    }
3575
3576    #[test]
3577    fn blpop_multi_key() {
3578        assert_eq!(
3579            Command::from_frame(cmd(&["BLPOP", "a", "b", "c", "0"])).unwrap(),
3580            Command::BLPop {
3581                keys: vec!["a".into(), "b".into(), "c".into()],
3582                timeout_secs: 0.0,
3583            },
3584        );
3585    }
3586
3587    #[test]
3588    fn blpop_float_timeout() {
3589        assert_eq!(
3590            Command::from_frame(cmd(&["BLPOP", "q", "1.5"])).unwrap(),
3591            Command::BLPop {
3592                keys: vec!["q".into()],
3593                timeout_secs: 1.5,
3594            },
3595        );
3596    }
3597
3598    #[test]
3599    fn blpop_wrong_arity() {
3600        let err = Command::from_frame(cmd(&["BLPOP"])).unwrap_err();
3601        assert!(matches!(err, ProtocolError::WrongArity(_)));
3602    }
3603
3604    #[test]
3605    fn blpop_negative_timeout() {
3606        let err = Command::from_frame(cmd(&["BLPOP", "k", "-1"])).unwrap_err();
3607        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3608    }
3609
3610    // --- brpop ---
3611
3612    #[test]
3613    fn brpop_single_key() {
3614        assert_eq!(
3615            Command::from_frame(cmd(&["BRPOP", "mylist", "10"])).unwrap(),
3616            Command::BRPop {
3617                keys: vec!["mylist".into()],
3618                timeout_secs: 10.0,
3619            },
3620        );
3621    }
3622
3623    #[test]
3624    fn brpop_wrong_arity() {
3625        let err = Command::from_frame(cmd(&["BRPOP"])).unwrap_err();
3626        assert!(matches!(err, ProtocolError::WrongArity(_)));
3627    }
3628
3629    // --- lrange ---
3630
3631    #[test]
3632    fn lrange_basic() {
3633        assert_eq!(
3634            Command::from_frame(cmd(&["LRANGE", "list", "0", "-1"])).unwrap(),
3635            Command::LRange {
3636                key: "list".into(),
3637                start: 0,
3638                stop: -1,
3639            },
3640        );
3641    }
3642
3643    #[test]
3644    fn lrange_wrong_arity() {
3645        let err = Command::from_frame(cmd(&["LRANGE", "list", "0"])).unwrap_err();
3646        assert!(matches!(err, ProtocolError::WrongArity(_)));
3647    }
3648
3649    #[test]
3650    fn lrange_invalid_index() {
3651        let err = Command::from_frame(cmd(&["LRANGE", "list", "abc", "0"])).unwrap_err();
3652        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3653    }
3654
3655    // --- llen ---
3656
3657    #[test]
3658    fn llen_basic() {
3659        assert_eq!(
3660            Command::from_frame(cmd(&["LLEN", "list"])).unwrap(),
3661            Command::LLen { key: "list".into() },
3662        );
3663    }
3664
3665    #[test]
3666    fn llen_wrong_arity() {
3667        let err = Command::from_frame(cmd(&["LLEN"])).unwrap_err();
3668        assert!(matches!(err, ProtocolError::WrongArity(_)));
3669    }
3670
3671    // --- type ---
3672
3673    #[test]
3674    fn type_basic() {
3675        assert_eq!(
3676            Command::from_frame(cmd(&["TYPE", "key"])).unwrap(),
3677            Command::Type { key: "key".into() },
3678        );
3679    }
3680
3681    #[test]
3682    fn type_wrong_arity() {
3683        let err = Command::from_frame(cmd(&["TYPE"])).unwrap_err();
3684        assert!(matches!(err, ProtocolError::WrongArity(_)));
3685    }
3686
3687    #[test]
3688    fn type_case_insensitive() {
3689        assert!(matches!(
3690            Command::from_frame(cmd(&["type", "k"])).unwrap(),
3691            Command::Type { .. }
3692        ));
3693    }
3694
3695    // --- general ---
3696
3697    #[test]
3698    fn unknown_command() {
3699        assert_eq!(
3700            Command::from_frame(cmd(&["FOOBAR", "arg"])).unwrap(),
3701            Command::Unknown("FOOBAR".into()),
3702        );
3703    }
3704
3705    #[test]
3706    fn non_array_frame() {
3707        let err = Command::from_frame(Frame::Simple("PING".into())).unwrap_err();
3708        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3709    }
3710
3711    #[test]
3712    fn empty_array() {
3713        let err = Command::from_frame(Frame::Array(vec![])).unwrap_err();
3714        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3715    }
3716
3717    // --- zadd ---
3718
3719    #[test]
3720    fn zadd_basic() {
3721        let parsed = Command::from_frame(cmd(&["ZADD", "board", "100", "alice"])).unwrap();
3722        match parsed {
3723            Command::ZAdd {
3724                key,
3725                flags,
3726                members,
3727            } => {
3728                assert_eq!(key, "board");
3729                assert_eq!(flags, ZAddFlags::default());
3730                assert_eq!(members, vec![(100.0, "alice".into())]);
3731            }
3732            other => panic!("expected ZAdd, got {other:?}"),
3733        }
3734    }
3735
3736    #[test]
3737    fn zadd_multiple_members() {
3738        let parsed =
3739            Command::from_frame(cmd(&["ZADD", "board", "100", "alice", "200", "bob"])).unwrap();
3740        match parsed {
3741            Command::ZAdd { members, .. } => {
3742                assert_eq!(members.len(), 2);
3743                assert_eq!(members[0], (100.0, "alice".into()));
3744                assert_eq!(members[1], (200.0, "bob".into()));
3745            }
3746            other => panic!("expected ZAdd, got {other:?}"),
3747        }
3748    }
3749
3750    #[test]
3751    fn zadd_with_flags() {
3752        let parsed = Command::from_frame(cmd(&["ZADD", "z", "NX", "CH", "100", "alice"])).unwrap();
3753        match parsed {
3754            Command::ZAdd { flags, .. } => {
3755                assert!(flags.nx);
3756                assert!(flags.ch);
3757                assert!(!flags.xx);
3758                assert!(!flags.gt);
3759                assert!(!flags.lt);
3760            }
3761            other => panic!("expected ZAdd, got {other:?}"),
3762        }
3763    }
3764
3765    #[test]
3766    fn zadd_gt_flag() {
3767        let parsed = Command::from_frame(cmd(&["zadd", "z", "gt", "100", "alice"])).unwrap();
3768        match parsed {
3769            Command::ZAdd { flags, .. } => assert!(flags.gt),
3770            other => panic!("expected ZAdd, got {other:?}"),
3771        }
3772    }
3773
3774    #[test]
3775    fn zadd_nx_xx_conflict() {
3776        let err = Command::from_frame(cmd(&["ZADD", "z", "NX", "XX", "100", "alice"])).unwrap_err();
3777        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3778    }
3779
3780    #[test]
3781    fn zadd_gt_lt_conflict() {
3782        let err = Command::from_frame(cmd(&["ZADD", "z", "GT", "LT", "100", "alice"])).unwrap_err();
3783        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3784    }
3785
3786    #[test]
3787    fn zadd_wrong_arity() {
3788        let err = Command::from_frame(cmd(&["ZADD", "z"])).unwrap_err();
3789        assert!(matches!(err, ProtocolError::WrongArity(_)));
3790    }
3791
3792    #[test]
3793    fn zadd_odd_score_member_count() {
3794        // one score without a member
3795        let err = Command::from_frame(cmd(&["ZADD", "z", "100"])).unwrap_err();
3796        assert!(matches!(err, ProtocolError::WrongArity(_)));
3797    }
3798
3799    #[test]
3800    fn zadd_invalid_score() {
3801        let err = Command::from_frame(cmd(&["ZADD", "z", "notanum", "alice"])).unwrap_err();
3802        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3803    }
3804
3805    #[test]
3806    fn zadd_nan_score() {
3807        let err = Command::from_frame(cmd(&["ZADD", "z", "nan", "alice"])).unwrap_err();
3808        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3809    }
3810
3811    #[test]
3812    fn zadd_negative_score() {
3813        let parsed = Command::from_frame(cmd(&["ZADD", "z", "-50.5", "alice"])).unwrap();
3814        match parsed {
3815            Command::ZAdd { members, .. } => {
3816                assert_eq!(members[0].0, -50.5);
3817            }
3818            other => panic!("expected ZAdd, got {other:?}"),
3819        }
3820    }
3821
3822    // --- zrem ---
3823
3824    #[test]
3825    fn zrem_basic() {
3826        assert_eq!(
3827            Command::from_frame(cmd(&["ZREM", "z", "alice"])).unwrap(),
3828            Command::ZRem {
3829                key: "z".into(),
3830                members: vec!["alice".into()],
3831            },
3832        );
3833    }
3834
3835    #[test]
3836    fn zrem_multiple() {
3837        let parsed = Command::from_frame(cmd(&["ZREM", "z", "a", "b", "c"])).unwrap();
3838        match parsed {
3839            Command::ZRem { members, .. } => assert_eq!(members.len(), 3),
3840            other => panic!("expected ZRem, got {other:?}"),
3841        }
3842    }
3843
3844    #[test]
3845    fn zrem_wrong_arity() {
3846        let err = Command::from_frame(cmd(&["ZREM", "z"])).unwrap_err();
3847        assert!(matches!(err, ProtocolError::WrongArity(_)));
3848    }
3849
3850    // --- zscore ---
3851
3852    #[test]
3853    fn zscore_basic() {
3854        assert_eq!(
3855            Command::from_frame(cmd(&["ZSCORE", "z", "alice"])).unwrap(),
3856            Command::ZScore {
3857                key: "z".into(),
3858                member: "alice".into(),
3859            },
3860        );
3861    }
3862
3863    #[test]
3864    fn zscore_wrong_arity() {
3865        let err = Command::from_frame(cmd(&["ZSCORE", "z"])).unwrap_err();
3866        assert!(matches!(err, ProtocolError::WrongArity(_)));
3867    }
3868
3869    // --- zrank ---
3870
3871    #[test]
3872    fn zrank_basic() {
3873        assert_eq!(
3874            Command::from_frame(cmd(&["ZRANK", "z", "alice"])).unwrap(),
3875            Command::ZRank {
3876                key: "z".into(),
3877                member: "alice".into(),
3878            },
3879        );
3880    }
3881
3882    #[test]
3883    fn zrank_wrong_arity() {
3884        let err = Command::from_frame(cmd(&["ZRANK", "z"])).unwrap_err();
3885        assert!(matches!(err, ProtocolError::WrongArity(_)));
3886    }
3887
3888    // --- zcard ---
3889
3890    #[test]
3891    fn zcard_basic() {
3892        assert_eq!(
3893            Command::from_frame(cmd(&["ZCARD", "z"])).unwrap(),
3894            Command::ZCard { key: "z".into() },
3895        );
3896    }
3897
3898    #[test]
3899    fn zcard_wrong_arity() {
3900        let err = Command::from_frame(cmd(&["ZCARD"])).unwrap_err();
3901        assert!(matches!(err, ProtocolError::WrongArity(_)));
3902    }
3903
3904    // --- zrange ---
3905
3906    #[test]
3907    fn zrange_basic() {
3908        assert_eq!(
3909            Command::from_frame(cmd(&["ZRANGE", "z", "0", "-1"])).unwrap(),
3910            Command::ZRange {
3911                key: "z".into(),
3912                start: 0,
3913                stop: -1,
3914                with_scores: false,
3915            },
3916        );
3917    }
3918
3919    #[test]
3920    fn zrange_with_scores() {
3921        assert_eq!(
3922            Command::from_frame(cmd(&["ZRANGE", "z", "0", "-1", "WITHSCORES"])).unwrap(),
3923            Command::ZRange {
3924                key: "z".into(),
3925                start: 0,
3926                stop: -1,
3927                with_scores: true,
3928            },
3929        );
3930    }
3931
3932    #[test]
3933    fn zrange_withscores_case_insensitive() {
3934        assert_eq!(
3935            Command::from_frame(cmd(&["zrange", "z", "0", "-1", "withscores"])).unwrap(),
3936            Command::ZRange {
3937                key: "z".into(),
3938                start: 0,
3939                stop: -1,
3940                with_scores: true,
3941            },
3942        );
3943    }
3944
3945    #[test]
3946    fn zrange_invalid_option() {
3947        let err = Command::from_frame(cmd(&["ZRANGE", "z", "0", "-1", "BADOPT"])).unwrap_err();
3948        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
3949    }
3950
3951    #[test]
3952    fn zrange_wrong_arity() {
3953        let err = Command::from_frame(cmd(&["ZRANGE", "z", "0"])).unwrap_err();
3954        assert!(matches!(err, ProtocolError::WrongArity(_)));
3955    }
3956
3957    // --- incr ---
3958
3959    #[test]
3960    fn incr_basic() {
3961        assert_eq!(
3962            Command::from_frame(cmd(&["INCR", "counter"])).unwrap(),
3963            Command::Incr {
3964                key: "counter".into()
3965            },
3966        );
3967    }
3968
3969    #[test]
3970    fn incr_wrong_arity() {
3971        let err = Command::from_frame(cmd(&["INCR"])).unwrap_err();
3972        assert!(matches!(err, ProtocolError::WrongArity(_)));
3973    }
3974
3975    // --- decr ---
3976
3977    #[test]
3978    fn decr_basic() {
3979        assert_eq!(
3980            Command::from_frame(cmd(&["DECR", "counter"])).unwrap(),
3981            Command::Decr {
3982                key: "counter".into()
3983            },
3984        );
3985    }
3986
3987    #[test]
3988    fn decr_wrong_arity() {
3989        let err = Command::from_frame(cmd(&["DECR"])).unwrap_err();
3990        assert!(matches!(err, ProtocolError::WrongArity(_)));
3991    }
3992
3993    // --- persist ---
3994
3995    #[test]
3996    fn persist_basic() {
3997        assert_eq!(
3998            Command::from_frame(cmd(&["PERSIST", "key"])).unwrap(),
3999            Command::Persist { key: "key".into() },
4000        );
4001    }
4002
4003    #[test]
4004    fn persist_case_insensitive() {
4005        assert_eq!(
4006            Command::from_frame(cmd(&["persist", "key"])).unwrap(),
4007            Command::Persist { key: "key".into() },
4008        );
4009    }
4010
4011    #[test]
4012    fn persist_wrong_arity() {
4013        let err = Command::from_frame(cmd(&["PERSIST"])).unwrap_err();
4014        assert!(matches!(err, ProtocolError::WrongArity(_)));
4015    }
4016
4017    // --- pttl ---
4018
4019    #[test]
4020    fn pttl_basic() {
4021        assert_eq!(
4022            Command::from_frame(cmd(&["PTTL", "key"])).unwrap(),
4023            Command::Pttl { key: "key".into() },
4024        );
4025    }
4026
4027    #[test]
4028    fn pttl_wrong_arity() {
4029        let err = Command::from_frame(cmd(&["PTTL"])).unwrap_err();
4030        assert!(matches!(err, ProtocolError::WrongArity(_)));
4031    }
4032
4033    // --- pexpire ---
4034
4035    #[test]
4036    fn pexpire_basic() {
4037        assert_eq!(
4038            Command::from_frame(cmd(&["PEXPIRE", "key", "5000"])).unwrap(),
4039            Command::Pexpire {
4040                key: "key".into(),
4041                milliseconds: 5000,
4042            },
4043        );
4044    }
4045
4046    #[test]
4047    fn pexpire_wrong_arity() {
4048        let err = Command::from_frame(cmd(&["PEXPIRE", "key"])).unwrap_err();
4049        assert!(matches!(err, ProtocolError::WrongArity(_)));
4050    }
4051
4052    #[test]
4053    fn pexpire_zero_millis() {
4054        let err = Command::from_frame(cmd(&["PEXPIRE", "key", "0"])).unwrap_err();
4055        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
4056    }
4057
4058    #[test]
4059    fn pexpire_invalid_millis() {
4060        let err = Command::from_frame(cmd(&["PEXPIRE", "key", "notanum"])).unwrap_err();
4061        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
4062    }
4063
4064    // --- scan ---
4065
4066    #[test]
4067    fn scan_basic() {
4068        assert_eq!(
4069            Command::from_frame(cmd(&["SCAN", "0"])).unwrap(),
4070            Command::Scan {
4071                cursor: 0,
4072                pattern: None,
4073                count: None,
4074            },
4075        );
4076    }
4077
4078    #[test]
4079    fn scan_with_match() {
4080        assert_eq!(
4081            Command::from_frame(cmd(&["SCAN", "0", "MATCH", "user:*"])).unwrap(),
4082            Command::Scan {
4083                cursor: 0,
4084                pattern: Some("user:*".into()),
4085                count: None,
4086            },
4087        );
4088    }
4089
4090    #[test]
4091    fn scan_with_count() {
4092        assert_eq!(
4093            Command::from_frame(cmd(&["SCAN", "42", "COUNT", "100"])).unwrap(),
4094            Command::Scan {
4095                cursor: 42,
4096                pattern: None,
4097                count: Some(100),
4098            },
4099        );
4100    }
4101
4102    #[test]
4103    fn scan_with_match_and_count() {
4104        assert_eq!(
4105            Command::from_frame(cmd(&["SCAN", "0", "MATCH", "*:data", "COUNT", "50"])).unwrap(),
4106            Command::Scan {
4107                cursor: 0,
4108                pattern: Some("*:data".into()),
4109                count: Some(50),
4110            },
4111        );
4112    }
4113
4114    #[test]
4115    fn scan_count_before_match() {
4116        assert_eq!(
4117            Command::from_frame(cmd(&["SCAN", "0", "COUNT", "10", "MATCH", "foo*"])).unwrap(),
4118            Command::Scan {
4119                cursor: 0,
4120                pattern: Some("foo*".into()),
4121                count: Some(10),
4122            },
4123        );
4124    }
4125
4126    #[test]
4127    fn scan_case_insensitive() {
4128        assert_eq!(
4129            Command::from_frame(cmd(&["scan", "0", "match", "x*", "count", "5"])).unwrap(),
4130            Command::Scan {
4131                cursor: 0,
4132                pattern: Some("x*".into()),
4133                count: Some(5),
4134            },
4135        );
4136    }
4137
4138    #[test]
4139    fn scan_wrong_arity() {
4140        let err = Command::from_frame(cmd(&["SCAN"])).unwrap_err();
4141        assert!(matches!(err, ProtocolError::WrongArity(_)));
4142    }
4143
4144    #[test]
4145    fn scan_invalid_cursor() {
4146        let err = Command::from_frame(cmd(&["SCAN", "notanum"])).unwrap_err();
4147        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
4148    }
4149
4150    #[test]
4151    fn scan_invalid_count() {
4152        let err = Command::from_frame(cmd(&["SCAN", "0", "COUNT", "bad"])).unwrap_err();
4153        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
4154    }
4155
4156    #[test]
4157    fn scan_unknown_flag() {
4158        let err = Command::from_frame(cmd(&["SCAN", "0", "BADOPT", "val"])).unwrap_err();
4159        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
4160    }
4161
4162    #[test]
4163    fn scan_match_missing_pattern() {
4164        let err = Command::from_frame(cmd(&["SCAN", "0", "MATCH"])).unwrap_err();
4165        assert!(matches!(err, ProtocolError::WrongArity(_)));
4166    }
4167
4168    #[test]
4169    fn scan_count_missing_value() {
4170        let err = Command::from_frame(cmd(&["SCAN", "0", "COUNT"])).unwrap_err();
4171        assert!(matches!(err, ProtocolError::WrongArity(_)));
4172    }
4173
4174    // --- hash commands ---
4175
4176    #[test]
4177    fn hset_single_field() {
4178        assert_eq!(
4179            Command::from_frame(cmd(&["HSET", "h", "field", "value"])).unwrap(),
4180            Command::HSet {
4181                key: "h".into(),
4182                fields: vec![("field".into(), Bytes::from("value"))],
4183            },
4184        );
4185    }
4186
4187    #[test]
4188    fn hset_multiple_fields() {
4189        let parsed = Command::from_frame(cmd(&["HSET", "h", "f1", "v1", "f2", "v2"])).unwrap();
4190        match parsed {
4191            Command::HSet { key, fields } => {
4192                assert_eq!(key, "h");
4193                assert_eq!(fields.len(), 2);
4194            }
4195            other => panic!("expected HSet, got {other:?}"),
4196        }
4197    }
4198
4199    #[test]
4200    fn hset_wrong_arity() {
4201        let err = Command::from_frame(cmd(&["HSET", "h"])).unwrap_err();
4202        assert!(matches!(err, ProtocolError::WrongArity(_)));
4203        let err = Command::from_frame(cmd(&["HSET", "h", "f"])).unwrap_err();
4204        assert!(matches!(err, ProtocolError::WrongArity(_)));
4205    }
4206
4207    #[test]
4208    fn hget_basic() {
4209        assert_eq!(
4210            Command::from_frame(cmd(&["HGET", "h", "field"])).unwrap(),
4211            Command::HGet {
4212                key: "h".into(),
4213                field: "field".into(),
4214            },
4215        );
4216    }
4217
4218    #[test]
4219    fn hget_wrong_arity() {
4220        let err = Command::from_frame(cmd(&["HGET", "h"])).unwrap_err();
4221        assert!(matches!(err, ProtocolError::WrongArity(_)));
4222    }
4223
4224    #[test]
4225    fn hgetall_basic() {
4226        assert_eq!(
4227            Command::from_frame(cmd(&["HGETALL", "h"])).unwrap(),
4228            Command::HGetAll { key: "h".into() },
4229        );
4230    }
4231
4232    #[test]
4233    fn hgetall_wrong_arity() {
4234        let err = Command::from_frame(cmd(&["HGETALL"])).unwrap_err();
4235        assert!(matches!(err, ProtocolError::WrongArity(_)));
4236    }
4237
4238    #[test]
4239    fn hdel_single() {
4240        assert_eq!(
4241            Command::from_frame(cmd(&["HDEL", "h", "f"])).unwrap(),
4242            Command::HDel {
4243                key: "h".into(),
4244                fields: vec!["f".into()],
4245            },
4246        );
4247    }
4248
4249    #[test]
4250    fn hdel_multiple() {
4251        let parsed = Command::from_frame(cmd(&["HDEL", "h", "f1", "f2", "f3"])).unwrap();
4252        match parsed {
4253            Command::HDel { fields, .. } => assert_eq!(fields.len(), 3),
4254            other => panic!("expected HDel, got {other:?}"),
4255        }
4256    }
4257
4258    #[test]
4259    fn hdel_wrong_arity() {
4260        let err = Command::from_frame(cmd(&["HDEL", "h"])).unwrap_err();
4261        assert!(matches!(err, ProtocolError::WrongArity(_)));
4262    }
4263
4264    #[test]
4265    fn hexists_basic() {
4266        assert_eq!(
4267            Command::from_frame(cmd(&["HEXISTS", "h", "f"])).unwrap(),
4268            Command::HExists {
4269                key: "h".into(),
4270                field: "f".into(),
4271            },
4272        );
4273    }
4274
4275    #[test]
4276    fn hlen_basic() {
4277        assert_eq!(
4278            Command::from_frame(cmd(&["HLEN", "h"])).unwrap(),
4279            Command::HLen { key: "h".into() },
4280        );
4281    }
4282
4283    #[test]
4284    fn hincrby_basic() {
4285        assert_eq!(
4286            Command::from_frame(cmd(&["HINCRBY", "h", "f", "5"])).unwrap(),
4287            Command::HIncrBy {
4288                key: "h".into(),
4289                field: "f".into(),
4290                delta: 5,
4291            },
4292        );
4293    }
4294
4295    #[test]
4296    fn hincrby_negative() {
4297        assert_eq!(
4298            Command::from_frame(cmd(&["HINCRBY", "h", "f", "-3"])).unwrap(),
4299            Command::HIncrBy {
4300                key: "h".into(),
4301                field: "f".into(),
4302                delta: -3,
4303            },
4304        );
4305    }
4306
4307    #[test]
4308    fn hincrby_wrong_arity() {
4309        let err = Command::from_frame(cmd(&["HINCRBY", "h", "f"])).unwrap_err();
4310        assert!(matches!(err, ProtocolError::WrongArity(_)));
4311    }
4312
4313    #[test]
4314    fn hkeys_basic() {
4315        assert_eq!(
4316            Command::from_frame(cmd(&["HKEYS", "h"])).unwrap(),
4317            Command::HKeys { key: "h".into() },
4318        );
4319    }
4320
4321    #[test]
4322    fn hvals_basic() {
4323        assert_eq!(
4324            Command::from_frame(cmd(&["HVALS", "h"])).unwrap(),
4325            Command::HVals { key: "h".into() },
4326        );
4327    }
4328
4329    #[test]
4330    fn hmget_basic() {
4331        assert_eq!(
4332            Command::from_frame(cmd(&["HMGET", "h", "f1", "f2"])).unwrap(),
4333            Command::HMGet {
4334                key: "h".into(),
4335                fields: vec!["f1".into(), "f2".into()],
4336            },
4337        );
4338    }
4339
4340    #[test]
4341    fn hmget_wrong_arity() {
4342        let err = Command::from_frame(cmd(&["HMGET", "h"])).unwrap_err();
4343        assert!(matches!(err, ProtocolError::WrongArity(_)));
4344    }
4345
4346    #[test]
4347    fn hash_commands_case_insensitive() {
4348        assert!(matches!(
4349            Command::from_frame(cmd(&["hset", "h", "f", "v"])).unwrap(),
4350            Command::HSet { .. }
4351        ));
4352        assert!(matches!(
4353            Command::from_frame(cmd(&["hget", "h", "f"])).unwrap(),
4354            Command::HGet { .. }
4355        ));
4356        assert!(matches!(
4357            Command::from_frame(cmd(&["hgetall", "h"])).unwrap(),
4358            Command::HGetAll { .. }
4359        ));
4360    }
4361
4362    // --- set commands ---
4363
4364    #[test]
4365    fn sadd_single_member() {
4366        assert_eq!(
4367            Command::from_frame(cmd(&["SADD", "s", "member"])).unwrap(),
4368            Command::SAdd {
4369                key: "s".into(),
4370                members: vec!["member".into()],
4371            },
4372        );
4373    }
4374
4375    #[test]
4376    fn sadd_multiple_members() {
4377        let parsed = Command::from_frame(cmd(&["SADD", "s", "a", "b", "c"])).unwrap();
4378        match parsed {
4379            Command::SAdd { key, members } => {
4380                assert_eq!(key, "s");
4381                assert_eq!(members.len(), 3);
4382            }
4383            other => panic!("expected SAdd, got {other:?}"),
4384        }
4385    }
4386
4387    #[test]
4388    fn sadd_wrong_arity() {
4389        let err = Command::from_frame(cmd(&["SADD", "s"])).unwrap_err();
4390        assert!(matches!(err, ProtocolError::WrongArity(_)));
4391    }
4392
4393    #[test]
4394    fn srem_single_member() {
4395        assert_eq!(
4396            Command::from_frame(cmd(&["SREM", "s", "member"])).unwrap(),
4397            Command::SRem {
4398                key: "s".into(),
4399                members: vec!["member".into()],
4400            },
4401        );
4402    }
4403
4404    #[test]
4405    fn srem_multiple_members() {
4406        let parsed = Command::from_frame(cmd(&["SREM", "s", "a", "b"])).unwrap();
4407        match parsed {
4408            Command::SRem { key, members } => {
4409                assert_eq!(key, "s");
4410                assert_eq!(members.len(), 2);
4411            }
4412            other => panic!("expected SRem, got {other:?}"),
4413        }
4414    }
4415
4416    #[test]
4417    fn srem_wrong_arity() {
4418        let err = Command::from_frame(cmd(&["SREM", "s"])).unwrap_err();
4419        assert!(matches!(err, ProtocolError::WrongArity(_)));
4420    }
4421
4422    #[test]
4423    fn smembers_basic() {
4424        assert_eq!(
4425            Command::from_frame(cmd(&["SMEMBERS", "s"])).unwrap(),
4426            Command::SMembers { key: "s".into() },
4427        );
4428    }
4429
4430    #[test]
4431    fn smembers_wrong_arity() {
4432        let err = Command::from_frame(cmd(&["SMEMBERS"])).unwrap_err();
4433        assert!(matches!(err, ProtocolError::WrongArity(_)));
4434    }
4435
4436    #[test]
4437    fn sismember_basic() {
4438        assert_eq!(
4439            Command::from_frame(cmd(&["SISMEMBER", "s", "member"])).unwrap(),
4440            Command::SIsMember {
4441                key: "s".into(),
4442                member: "member".into(),
4443            },
4444        );
4445    }
4446
4447    #[test]
4448    fn sismember_wrong_arity() {
4449        let err = Command::from_frame(cmd(&["SISMEMBER", "s"])).unwrap_err();
4450        assert!(matches!(err, ProtocolError::WrongArity(_)));
4451    }
4452
4453    #[test]
4454    fn scard_basic() {
4455        assert_eq!(
4456            Command::from_frame(cmd(&["SCARD", "s"])).unwrap(),
4457            Command::SCard { key: "s".into() },
4458        );
4459    }
4460
4461    #[test]
4462    fn scard_wrong_arity() {
4463        let err = Command::from_frame(cmd(&["SCARD"])).unwrap_err();
4464        assert!(matches!(err, ProtocolError::WrongArity(_)));
4465    }
4466
4467    #[test]
4468    fn set_commands_case_insensitive() {
4469        assert!(matches!(
4470            Command::from_frame(cmd(&["sadd", "s", "m"])).unwrap(),
4471            Command::SAdd { .. }
4472        ));
4473        assert!(matches!(
4474            Command::from_frame(cmd(&["srem", "s", "m"])).unwrap(),
4475            Command::SRem { .. }
4476        ));
4477        assert!(matches!(
4478            Command::from_frame(cmd(&["smembers", "s"])).unwrap(),
4479            Command::SMembers { .. }
4480        ));
4481    }
4482
4483    // --- cluster commands ---
4484
4485    #[test]
4486    fn cluster_info_basic() {
4487        assert_eq!(
4488            Command::from_frame(cmd(&["CLUSTER", "INFO"])).unwrap(),
4489            Command::ClusterInfo,
4490        );
4491    }
4492
4493    #[test]
4494    fn cluster_nodes_basic() {
4495        assert_eq!(
4496            Command::from_frame(cmd(&["CLUSTER", "NODES"])).unwrap(),
4497            Command::ClusterNodes,
4498        );
4499    }
4500
4501    #[test]
4502    fn cluster_slots_basic() {
4503        assert_eq!(
4504            Command::from_frame(cmd(&["CLUSTER", "SLOTS"])).unwrap(),
4505            Command::ClusterSlots,
4506        );
4507    }
4508
4509    #[test]
4510    fn cluster_keyslot_basic() {
4511        assert_eq!(
4512            Command::from_frame(cmd(&["CLUSTER", "KEYSLOT", "mykey"])).unwrap(),
4513            Command::ClusterKeySlot {
4514                key: "mykey".into()
4515            },
4516        );
4517    }
4518
4519    #[test]
4520    fn cluster_keyslot_wrong_arity() {
4521        let err = Command::from_frame(cmd(&["CLUSTER", "KEYSLOT"])).unwrap_err();
4522        assert!(matches!(err, ProtocolError::WrongArity(_)));
4523    }
4524
4525    #[test]
4526    fn cluster_myid_basic() {
4527        assert_eq!(
4528            Command::from_frame(cmd(&["CLUSTER", "MYID"])).unwrap(),
4529            Command::ClusterMyId,
4530        );
4531    }
4532
4533    #[test]
4534    fn cluster_unknown_subcommand() {
4535        let err = Command::from_frame(cmd(&["CLUSTER", "BADCMD"])).unwrap_err();
4536        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
4537    }
4538
4539    #[test]
4540    fn cluster_no_subcommand() {
4541        let err = Command::from_frame(cmd(&["CLUSTER"])).unwrap_err();
4542        assert!(matches!(err, ProtocolError::WrongArity(_)));
4543    }
4544
4545    #[test]
4546    fn cluster_case_insensitive() {
4547        assert!(matches!(
4548            Command::from_frame(cmd(&["cluster", "info"])).unwrap(),
4549            Command::ClusterInfo
4550        ));
4551        assert!(matches!(
4552            Command::from_frame(cmd(&["cluster", "keyslot", "k"])).unwrap(),
4553            Command::ClusterKeySlot { .. }
4554        ));
4555    }
4556
4557    #[test]
4558    fn asking_basic() {
4559        assert_eq!(
4560            Command::from_frame(cmd(&["ASKING"])).unwrap(),
4561            Command::Asking,
4562        );
4563    }
4564
4565    #[test]
4566    fn asking_wrong_arity() {
4567        let err = Command::from_frame(cmd(&["ASKING", "extra"])).unwrap_err();
4568        assert!(matches!(err, ProtocolError::WrongArity(_)));
4569    }
4570
4571    #[test]
4572    fn cluster_setslot_importing() {
4573        assert_eq!(
4574            Command::from_frame(cmd(&["CLUSTER", "SETSLOT", "100", "IMPORTING", "node123"]))
4575                .unwrap(),
4576            Command::ClusterSetSlotImporting {
4577                slot: 100,
4578                node_id: "node123".into()
4579            },
4580        );
4581    }
4582
4583    #[test]
4584    fn cluster_setslot_migrating() {
4585        assert_eq!(
4586            Command::from_frame(cmd(&["CLUSTER", "SETSLOT", "200", "MIGRATING", "node456"]))
4587                .unwrap(),
4588            Command::ClusterSetSlotMigrating {
4589                slot: 200,
4590                node_id: "node456".into()
4591            },
4592        );
4593    }
4594
4595    #[test]
4596    fn cluster_setslot_node() {
4597        assert_eq!(
4598            Command::from_frame(cmd(&["CLUSTER", "SETSLOT", "300", "NODE", "node789"])).unwrap(),
4599            Command::ClusterSetSlotNode {
4600                slot: 300,
4601                node_id: "node789".into()
4602            },
4603        );
4604    }
4605
4606    #[test]
4607    fn cluster_setslot_stable() {
4608        assert_eq!(
4609            Command::from_frame(cmd(&["CLUSTER", "SETSLOT", "400", "STABLE"])).unwrap(),
4610            Command::ClusterSetSlotStable { slot: 400 },
4611        );
4612    }
4613
4614    #[test]
4615    fn cluster_setslot_invalid_slot() {
4616        let err =
4617            Command::from_frame(cmd(&["CLUSTER", "SETSLOT", "notanumber", "STABLE"])).unwrap_err();
4618        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
4619    }
4620
4621    #[test]
4622    fn cluster_setslot_wrong_arity() {
4623        let err = Command::from_frame(cmd(&["CLUSTER", "SETSLOT", "100"])).unwrap_err();
4624        assert!(matches!(err, ProtocolError::WrongArity(_)));
4625    }
4626
4627    #[test]
4628    fn migrate_basic() {
4629        assert_eq!(
4630            Command::from_frame(cmd(&["MIGRATE", "127.0.0.1", "6379", "mykey", "0", "5000"]))
4631                .unwrap(),
4632            Command::Migrate {
4633                host: "127.0.0.1".into(),
4634                port: 6379,
4635                key: "mykey".into(),
4636                db: 0,
4637                timeout_ms: 5000,
4638                copy: false,
4639                replace: false,
4640            },
4641        );
4642    }
4643
4644    #[test]
4645    fn migrate_with_options() {
4646        assert_eq!(
4647            Command::from_frame(cmd(&[
4648                "MIGRATE",
4649                "192.168.1.1",
4650                "6380",
4651                "testkey",
4652                "1",
4653                "10000",
4654                "COPY",
4655                "REPLACE"
4656            ]))
4657            .unwrap(),
4658            Command::Migrate {
4659                host: "192.168.1.1".into(),
4660                port: 6380,
4661                key: "testkey".into(),
4662                db: 1,
4663                timeout_ms: 10000,
4664                copy: true,
4665                replace: true,
4666            },
4667        );
4668    }
4669
4670    #[test]
4671    fn migrate_wrong_arity() {
4672        let err = Command::from_frame(cmd(&["MIGRATE", "host", "port", "key"])).unwrap_err();
4673        assert!(matches!(err, ProtocolError::WrongArity(_)));
4674    }
4675
4676    #[test]
4677    fn migrate_invalid_port() {
4678        let err = Command::from_frame(cmd(&["MIGRATE", "host", "notaport", "key", "0", "1000"]))
4679            .unwrap_err();
4680        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
4681    }
4682
4683    #[test]
4684    fn cluster_meet_basic() {
4685        assert_eq!(
4686            Command::from_frame(cmd(&["CLUSTER", "MEET", "192.168.1.1", "6379"])).unwrap(),
4687            Command::ClusterMeet {
4688                ip: "192.168.1.1".into(),
4689                port: 6379
4690            },
4691        );
4692    }
4693
4694    #[test]
4695    fn cluster_addslots_basic() {
4696        assert_eq!(
4697            Command::from_frame(cmd(&["CLUSTER", "ADDSLOTS", "0", "1", "2"])).unwrap(),
4698            Command::ClusterAddSlots {
4699                slots: vec![0, 1, 2]
4700            },
4701        );
4702    }
4703
4704    #[test]
4705    fn cluster_addslotsrange_basic() {
4706        assert_eq!(
4707            Command::from_frame(cmd(&["CLUSTER", "ADDSLOTSRANGE", "0", "5460"])).unwrap(),
4708            Command::ClusterAddSlotsRange {
4709                ranges: vec![(0, 5460)]
4710            },
4711        );
4712    }
4713
4714    #[test]
4715    fn cluster_addslotsrange_multiple() {
4716        assert_eq!(
4717            Command::from_frame(cmd(&["CLUSTER", "ADDSLOTSRANGE", "0", "100", "200", "300"]))
4718                .unwrap(),
4719            Command::ClusterAddSlotsRange {
4720                ranges: vec![(0, 100), (200, 300)]
4721            },
4722        );
4723    }
4724
4725    #[test]
4726    fn cluster_addslotsrange_invalid_range() {
4727        let err = Command::from_frame(cmd(&["CLUSTER", "ADDSLOTSRANGE", "100", "50"])).unwrap_err();
4728        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
4729    }
4730
4731    #[test]
4732    fn cluster_addslotsrange_wrong_arity() {
4733        // odd number of slot args
4734        let err = Command::from_frame(cmd(&["CLUSTER", "ADDSLOTSRANGE", "0"])).unwrap_err();
4735        assert!(matches!(err, ProtocolError::WrongArity(_)));
4736    }
4737
4738    #[test]
4739    fn cluster_delslots_basic() {
4740        assert_eq!(
4741            Command::from_frame(cmd(&["CLUSTER", "DELSLOTS", "100", "101"])).unwrap(),
4742            Command::ClusterDelSlots {
4743                slots: vec![100, 101]
4744            },
4745        );
4746    }
4747
4748    #[test]
4749    fn cluster_forget_basic() {
4750        assert_eq!(
4751            Command::from_frame(cmd(&["CLUSTER", "FORGET", "abc123"])).unwrap(),
4752            Command::ClusterForget {
4753                node_id: "abc123".into()
4754            },
4755        );
4756    }
4757
4758    #[test]
4759    fn cluster_replicate_basic() {
4760        assert_eq!(
4761            Command::from_frame(cmd(&["CLUSTER", "REPLICATE", "master-id"])).unwrap(),
4762            Command::ClusterReplicate {
4763                node_id: "master-id".into()
4764            },
4765        );
4766    }
4767
4768    #[test]
4769    fn cluster_failover_basic() {
4770        assert_eq!(
4771            Command::from_frame(cmd(&["CLUSTER", "FAILOVER"])).unwrap(),
4772            Command::ClusterFailover {
4773                force: false,
4774                takeover: false
4775            },
4776        );
4777    }
4778
4779    #[test]
4780    fn cluster_failover_force() {
4781        assert_eq!(
4782            Command::from_frame(cmd(&["CLUSTER", "FAILOVER", "FORCE"])).unwrap(),
4783            Command::ClusterFailover {
4784                force: true,
4785                takeover: false
4786            },
4787        );
4788    }
4789
4790    #[test]
4791    fn cluster_failover_takeover() {
4792        assert_eq!(
4793            Command::from_frame(cmd(&["CLUSTER", "FAILOVER", "TAKEOVER"])).unwrap(),
4794            Command::ClusterFailover {
4795                force: false,
4796                takeover: true
4797            },
4798        );
4799    }
4800
4801    #[test]
4802    fn cluster_countkeysinslot_basic() {
4803        assert_eq!(
4804            Command::from_frame(cmd(&["CLUSTER", "COUNTKEYSINSLOT", "100"])).unwrap(),
4805            Command::ClusterCountKeysInSlot { slot: 100 },
4806        );
4807    }
4808
4809    #[test]
4810    fn cluster_getkeysinslot_basic() {
4811        assert_eq!(
4812            Command::from_frame(cmd(&["CLUSTER", "GETKEYSINSLOT", "200", "10"])).unwrap(),
4813            Command::ClusterGetKeysInSlot {
4814                slot: 200,
4815                count: 10
4816            },
4817        );
4818    }
4819
4820    // --- pub/sub ---
4821
4822    #[test]
4823    fn subscribe_single_channel() {
4824        assert_eq!(
4825            Command::from_frame(cmd(&["SUBSCRIBE", "news"])).unwrap(),
4826            Command::Subscribe {
4827                channels: vec!["news".into()]
4828            },
4829        );
4830    }
4831
4832    #[test]
4833    fn subscribe_multiple_channels() {
4834        assert_eq!(
4835            Command::from_frame(cmd(&["SUBSCRIBE", "ch1", "ch2", "ch3"])).unwrap(),
4836            Command::Subscribe {
4837                channels: vec!["ch1".into(), "ch2".into(), "ch3".into()]
4838            },
4839        );
4840    }
4841
4842    #[test]
4843    fn subscribe_no_args() {
4844        let err = Command::from_frame(cmd(&["SUBSCRIBE"])).unwrap_err();
4845        assert!(matches!(err, ProtocolError::WrongArity(_)));
4846    }
4847
4848    #[test]
4849    fn unsubscribe_all() {
4850        assert_eq!(
4851            Command::from_frame(cmd(&["UNSUBSCRIBE"])).unwrap(),
4852            Command::Unsubscribe { channels: vec![] },
4853        );
4854    }
4855
4856    #[test]
4857    fn unsubscribe_specific() {
4858        assert_eq!(
4859            Command::from_frame(cmd(&["UNSUBSCRIBE", "news"])).unwrap(),
4860            Command::Unsubscribe {
4861                channels: vec!["news".into()]
4862            },
4863        );
4864    }
4865
4866    #[test]
4867    fn psubscribe_pattern() {
4868        assert_eq!(
4869            Command::from_frame(cmd(&["PSUBSCRIBE", "news.*"])).unwrap(),
4870            Command::PSubscribe {
4871                patterns: vec!["news.*".into()]
4872            },
4873        );
4874    }
4875
4876    #[test]
4877    fn psubscribe_no_args() {
4878        let err = Command::from_frame(cmd(&["PSUBSCRIBE"])).unwrap_err();
4879        assert!(matches!(err, ProtocolError::WrongArity(_)));
4880    }
4881
4882    #[test]
4883    fn punsubscribe_all() {
4884        assert_eq!(
4885            Command::from_frame(cmd(&["PUNSUBSCRIBE"])).unwrap(),
4886            Command::PUnsubscribe { patterns: vec![] },
4887        );
4888    }
4889
4890    #[test]
4891    fn publish_basic() {
4892        assert_eq!(
4893            Command::from_frame(cmd(&["PUBLISH", "news", "hello world"])).unwrap(),
4894            Command::Publish {
4895                channel: "news".into(),
4896                message: Bytes::from("hello world"),
4897            },
4898        );
4899    }
4900
4901    #[test]
4902    fn publish_wrong_arity() {
4903        let err = Command::from_frame(cmd(&["PUBLISH", "news"])).unwrap_err();
4904        assert!(matches!(err, ProtocolError::WrongArity(_)));
4905    }
4906
4907    #[test]
4908    fn subscribe_case_insensitive() {
4909        assert_eq!(
4910            Command::from_frame(cmd(&["subscribe", "ch"])).unwrap(),
4911            Command::Subscribe {
4912                channels: vec!["ch".into()]
4913            },
4914        );
4915    }
4916
4917    #[test]
4918    fn pubsub_channels_no_pattern() {
4919        assert_eq!(
4920            Command::from_frame(cmd(&["PUBSUB", "CHANNELS"])).unwrap(),
4921            Command::PubSubChannels { pattern: None },
4922        );
4923    }
4924
4925    #[test]
4926    fn pubsub_channels_with_pattern() {
4927        assert_eq!(
4928            Command::from_frame(cmd(&["PUBSUB", "CHANNELS", "news.*"])).unwrap(),
4929            Command::PubSubChannels {
4930                pattern: Some("news.*".into())
4931            },
4932        );
4933    }
4934
4935    #[test]
4936    fn pubsub_numsub_no_args() {
4937        assert_eq!(
4938            Command::from_frame(cmd(&["PUBSUB", "NUMSUB"])).unwrap(),
4939            Command::PubSubNumSub { channels: vec![] },
4940        );
4941    }
4942
4943    #[test]
4944    fn pubsub_numsub_with_channels() {
4945        assert_eq!(
4946            Command::from_frame(cmd(&["PUBSUB", "NUMSUB", "ch1", "ch2"])).unwrap(),
4947            Command::PubSubNumSub {
4948                channels: vec!["ch1".into(), "ch2".into()]
4949            },
4950        );
4951    }
4952
4953    #[test]
4954    fn pubsub_numpat() {
4955        assert_eq!(
4956            Command::from_frame(cmd(&["PUBSUB", "NUMPAT"])).unwrap(),
4957            Command::PubSubNumPat,
4958        );
4959    }
4960
4961    #[test]
4962    fn pubsub_no_subcommand() {
4963        let err = Command::from_frame(cmd(&["PUBSUB"])).unwrap_err();
4964        assert!(matches!(err, ProtocolError::WrongArity(_)));
4965    }
4966
4967    #[test]
4968    fn pubsub_unknown_subcommand() {
4969        let err = Command::from_frame(cmd(&["PUBSUB", "BOGUS"])).unwrap_err();
4970        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
4971    }
4972
4973    // --- INCRBY / DECRBY ---
4974
4975    #[test]
4976    fn incrby_basic() {
4977        assert_eq!(
4978            Command::from_frame(cmd(&["INCRBY", "counter", "5"])).unwrap(),
4979            Command::IncrBy {
4980                key: "counter".into(),
4981                delta: 5
4982            },
4983        );
4984    }
4985
4986    #[test]
4987    fn incrby_negative() {
4988        assert_eq!(
4989            Command::from_frame(cmd(&["INCRBY", "counter", "-3"])).unwrap(),
4990            Command::IncrBy {
4991                key: "counter".into(),
4992                delta: -3
4993            },
4994        );
4995    }
4996
4997    #[test]
4998    fn incrby_wrong_arity() {
4999        let err = Command::from_frame(cmd(&["INCRBY", "key"])).unwrap_err();
5000        assert!(matches!(err, ProtocolError::WrongArity(_)));
5001    }
5002
5003    #[test]
5004    fn incrby_not_integer() {
5005        let err = Command::from_frame(cmd(&["INCRBY", "key", "abc"])).unwrap_err();
5006        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5007    }
5008
5009    #[test]
5010    fn decrby_basic() {
5011        assert_eq!(
5012            Command::from_frame(cmd(&["DECRBY", "counter", "10"])).unwrap(),
5013            Command::DecrBy {
5014                key: "counter".into(),
5015                delta: 10
5016            },
5017        );
5018    }
5019
5020    #[test]
5021    fn decrby_wrong_arity() {
5022        let err = Command::from_frame(cmd(&["DECRBY"])).unwrap_err();
5023        assert!(matches!(err, ProtocolError::WrongArity(_)));
5024    }
5025
5026    // --- INCRBYFLOAT ---
5027
5028    #[test]
5029    fn incrbyfloat_basic() {
5030        let cmd = Command::from_frame(cmd(&["INCRBYFLOAT", "key", "2.5"])).unwrap();
5031        match cmd {
5032            Command::IncrByFloat { key, delta } => {
5033                assert_eq!(key, "key");
5034                assert!((delta - 2.5).abs() < f64::EPSILON);
5035            }
5036            other => panic!("expected IncrByFloat, got {other:?}"),
5037        }
5038    }
5039
5040    #[test]
5041    fn incrbyfloat_negative() {
5042        let cmd = Command::from_frame(cmd(&["INCRBYFLOAT", "key", "-1.5"])).unwrap();
5043        match cmd {
5044            Command::IncrByFloat { key, delta } => {
5045                assert_eq!(key, "key");
5046                assert!((delta - (-1.5)).abs() < f64::EPSILON);
5047            }
5048            other => panic!("expected IncrByFloat, got {other:?}"),
5049        }
5050    }
5051
5052    #[test]
5053    fn incrbyfloat_wrong_arity() {
5054        let err = Command::from_frame(cmd(&["INCRBYFLOAT", "key"])).unwrap_err();
5055        assert!(matches!(err, ProtocolError::WrongArity(_)));
5056    }
5057
5058    #[test]
5059    fn incrbyfloat_not_a_float() {
5060        let err = Command::from_frame(cmd(&["INCRBYFLOAT", "key", "abc"])).unwrap_err();
5061        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5062    }
5063
5064    // --- APPEND / STRLEN ---
5065
5066    #[test]
5067    fn append_basic() {
5068        assert_eq!(
5069            Command::from_frame(cmd(&["APPEND", "key", "value"])).unwrap(),
5070            Command::Append {
5071                key: "key".into(),
5072                value: Bytes::from("value")
5073            },
5074        );
5075    }
5076
5077    #[test]
5078    fn append_wrong_arity() {
5079        let err = Command::from_frame(cmd(&["APPEND", "key"])).unwrap_err();
5080        assert!(matches!(err, ProtocolError::WrongArity(_)));
5081    }
5082
5083    #[test]
5084    fn strlen_basic() {
5085        assert_eq!(
5086            Command::from_frame(cmd(&["STRLEN", "key"])).unwrap(),
5087            Command::Strlen { key: "key".into() },
5088        );
5089    }
5090
5091    #[test]
5092    fn strlen_wrong_arity() {
5093        let err = Command::from_frame(cmd(&["STRLEN"])).unwrap_err();
5094        assert!(matches!(err, ProtocolError::WrongArity(_)));
5095    }
5096
5097    // --- KEYS ---
5098
5099    #[test]
5100    fn keys_basic() {
5101        assert_eq!(
5102            Command::from_frame(cmd(&["KEYS", "user:*"])).unwrap(),
5103            Command::Keys {
5104                pattern: "user:*".into()
5105            },
5106        );
5107    }
5108
5109    #[test]
5110    fn keys_wrong_arity() {
5111        let err = Command::from_frame(cmd(&["KEYS"])).unwrap_err();
5112        assert!(matches!(err, ProtocolError::WrongArity(_)));
5113    }
5114
5115    // --- RENAME ---
5116
5117    #[test]
5118    fn rename_basic() {
5119        assert_eq!(
5120            Command::from_frame(cmd(&["RENAME", "old", "new"])).unwrap(),
5121            Command::Rename {
5122                key: "old".into(),
5123                newkey: "new".into()
5124            },
5125        );
5126    }
5127
5128    #[test]
5129    fn rename_wrong_arity() {
5130        let err = Command::from_frame(cmd(&["RENAME", "only"])).unwrap_err();
5131        assert!(matches!(err, ProtocolError::WrongArity(_)));
5132    }
5133
5134    // --- AUTH ---
5135
5136    #[test]
5137    fn auth_legacy() {
5138        assert_eq!(
5139            Command::from_frame(cmd(&["AUTH", "secret"])).unwrap(),
5140            Command::Auth {
5141                username: None,
5142                password: "secret".into()
5143            },
5144        );
5145    }
5146
5147    #[test]
5148    fn auth_with_username() {
5149        assert_eq!(
5150            Command::from_frame(cmd(&["AUTH", "default", "secret"])).unwrap(),
5151            Command::Auth {
5152                username: Some("default".into()),
5153                password: "secret".into()
5154            },
5155        );
5156    }
5157
5158    #[test]
5159    fn auth_no_args() {
5160        let err = Command::from_frame(cmd(&["AUTH"])).unwrap_err();
5161        assert!(matches!(err, ProtocolError::WrongArity(_)));
5162    }
5163
5164    #[test]
5165    fn auth_too_many_args() {
5166        let err = Command::from_frame(cmd(&["AUTH", "a", "b", "c"])).unwrap_err();
5167        assert!(matches!(err, ProtocolError::WrongArity(_)));
5168    }
5169
5170    // --- QUIT ---
5171
5172    #[test]
5173    fn quit_basic() {
5174        assert_eq!(Command::from_frame(cmd(&["QUIT"])).unwrap(), Command::Quit,);
5175    }
5176
5177    #[test]
5178    fn quit_wrong_arity() {
5179        let err = Command::from_frame(cmd(&["QUIT", "extra"])).unwrap_err();
5180        assert!(matches!(err, ProtocolError::WrongArity(_)));
5181    }
5182
5183    // --- PROTO.REGISTER ---
5184
5185    #[test]
5186    fn proto_register_basic() {
5187        assert_eq!(
5188            Command::from_frame(cmd(&["PROTO.REGISTER", "myschema", "descriptor"])).unwrap(),
5189            Command::ProtoRegister {
5190                name: "myschema".into(),
5191                descriptor: Bytes::from("descriptor"),
5192            },
5193        );
5194    }
5195
5196    #[test]
5197    fn proto_register_case_insensitive() {
5198        assert!(Command::from_frame(cmd(&["proto.register", "s", "d"])).is_ok());
5199    }
5200
5201    #[test]
5202    fn proto_register_wrong_arity() {
5203        let err = Command::from_frame(cmd(&["PROTO.REGISTER", "only"])).unwrap_err();
5204        assert!(matches!(err, ProtocolError::WrongArity(_)));
5205    }
5206
5207    // --- PROTO.SET ---
5208
5209    #[test]
5210    fn proto_set_basic() {
5211        assert_eq!(
5212            Command::from_frame(cmd(&["PROTO.SET", "key1", "my.Type", "data"])).unwrap(),
5213            Command::ProtoSet {
5214                key: "key1".into(),
5215                type_name: "my.Type".into(),
5216                data: Bytes::from("data"),
5217                expire: None,
5218                nx: false,
5219                xx: false,
5220            },
5221        );
5222    }
5223
5224    #[test]
5225    fn proto_set_with_ex() {
5226        assert_eq!(
5227            Command::from_frame(cmd(&["PROTO.SET", "k", "t", "d", "EX", "60"])).unwrap(),
5228            Command::ProtoSet {
5229                key: "k".into(),
5230                type_name: "t".into(),
5231                data: Bytes::from("d"),
5232                expire: Some(SetExpire::Ex(60)),
5233                nx: false,
5234                xx: false,
5235            },
5236        );
5237    }
5238
5239    #[test]
5240    fn proto_set_with_px_and_nx() {
5241        assert_eq!(
5242            Command::from_frame(cmd(&["PROTO.SET", "k", "t", "d", "PX", "5000", "NX"])).unwrap(),
5243            Command::ProtoSet {
5244                key: "k".into(),
5245                type_name: "t".into(),
5246                data: Bytes::from("d"),
5247                expire: Some(SetExpire::Px(5000)),
5248                nx: true,
5249                xx: false,
5250            },
5251        );
5252    }
5253
5254    #[test]
5255    fn proto_set_nx_xx_conflict() {
5256        let err = Command::from_frame(cmd(&["PROTO.SET", "k", "t", "d", "NX", "XX"])).unwrap_err();
5257        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5258    }
5259
5260    #[test]
5261    fn proto_set_wrong_arity() {
5262        let err = Command::from_frame(cmd(&["PROTO.SET", "k", "t"])).unwrap_err();
5263        assert!(matches!(err, ProtocolError::WrongArity(_)));
5264    }
5265
5266    #[test]
5267    fn proto_set_zero_expiry() {
5268        let err = Command::from_frame(cmd(&["PROTO.SET", "k", "t", "d", "EX", "0"])).unwrap_err();
5269        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5270    }
5271
5272    // --- PROTO.GET ---
5273
5274    #[test]
5275    fn proto_get_basic() {
5276        assert_eq!(
5277            Command::from_frame(cmd(&["PROTO.GET", "key1"])).unwrap(),
5278            Command::ProtoGet { key: "key1".into() },
5279        );
5280    }
5281
5282    #[test]
5283    fn proto_get_wrong_arity() {
5284        let err = Command::from_frame(cmd(&["PROTO.GET"])).unwrap_err();
5285        assert!(matches!(err, ProtocolError::WrongArity(_)));
5286    }
5287
5288    // --- PROTO.TYPE ---
5289
5290    #[test]
5291    fn proto_type_basic() {
5292        assert_eq!(
5293            Command::from_frame(cmd(&["PROTO.TYPE", "key1"])).unwrap(),
5294            Command::ProtoType { key: "key1".into() },
5295        );
5296    }
5297
5298    #[test]
5299    fn proto_type_wrong_arity() {
5300        let err = Command::from_frame(cmd(&["PROTO.TYPE"])).unwrap_err();
5301        assert!(matches!(err, ProtocolError::WrongArity(_)));
5302    }
5303
5304    // --- PROTO.SCHEMAS ---
5305
5306    #[test]
5307    fn proto_schemas_basic() {
5308        assert_eq!(
5309            Command::from_frame(cmd(&["PROTO.SCHEMAS"])).unwrap(),
5310            Command::ProtoSchemas,
5311        );
5312    }
5313
5314    #[test]
5315    fn proto_schemas_wrong_arity() {
5316        let err = Command::from_frame(cmd(&["PROTO.SCHEMAS", "extra"])).unwrap_err();
5317        assert!(matches!(err, ProtocolError::WrongArity(_)));
5318    }
5319
5320    // --- PROTO.DESCRIBE ---
5321
5322    #[test]
5323    fn proto_describe_basic() {
5324        assert_eq!(
5325            Command::from_frame(cmd(&["PROTO.DESCRIBE", "myschema"])).unwrap(),
5326            Command::ProtoDescribe {
5327                name: "myschema".into()
5328            },
5329        );
5330    }
5331
5332    #[test]
5333    fn proto_describe_wrong_arity() {
5334        let err = Command::from_frame(cmd(&["PROTO.DESCRIBE"])).unwrap_err();
5335        assert!(matches!(err, ProtocolError::WrongArity(_)));
5336    }
5337
5338    // --- proto.getfield ---
5339
5340    #[test]
5341    fn proto_getfield_basic() {
5342        assert_eq!(
5343            Command::from_frame(cmd(&["PROTO.GETFIELD", "user:1", "name"])).unwrap(),
5344            Command::ProtoGetField {
5345                key: "user:1".into(),
5346                field_path: "name".into(),
5347            },
5348        );
5349    }
5350
5351    #[test]
5352    fn proto_getfield_nested_path() {
5353        assert_eq!(
5354            Command::from_frame(cmd(&["PROTO.GETFIELD", "key", "address.city"])).unwrap(),
5355            Command::ProtoGetField {
5356                key: "key".into(),
5357                field_path: "address.city".into(),
5358            },
5359        );
5360    }
5361
5362    #[test]
5363    fn proto_getfield_wrong_arity() {
5364        let err = Command::from_frame(cmd(&["PROTO.GETFIELD"])).unwrap_err();
5365        assert!(matches!(err, ProtocolError::WrongArity(_)));
5366
5367        let err = Command::from_frame(cmd(&["PROTO.GETFIELD", "key"])).unwrap_err();
5368        assert!(matches!(err, ProtocolError::WrongArity(_)));
5369
5370        let err =
5371            Command::from_frame(cmd(&["PROTO.GETFIELD", "key", "field", "extra"])).unwrap_err();
5372        assert!(matches!(err, ProtocolError::WrongArity(_)));
5373    }
5374
5375    // --- proto.setfield ---
5376
5377    #[test]
5378    fn proto_setfield_basic() {
5379        assert_eq!(
5380            Command::from_frame(cmd(&["PROTO.SETFIELD", "user:1", "name", "bob"])).unwrap(),
5381            Command::ProtoSetField {
5382                key: "user:1".into(),
5383                field_path: "name".into(),
5384                value: "bob".into(),
5385            },
5386        );
5387    }
5388
5389    #[test]
5390    fn proto_setfield_wrong_arity() {
5391        let err = Command::from_frame(cmd(&["PROTO.SETFIELD"])).unwrap_err();
5392        assert!(matches!(err, ProtocolError::WrongArity(_)));
5393
5394        let err = Command::from_frame(cmd(&["PROTO.SETFIELD", "key"])).unwrap_err();
5395        assert!(matches!(err, ProtocolError::WrongArity(_)));
5396
5397        let err = Command::from_frame(cmd(&["PROTO.SETFIELD", "key", "field"])).unwrap_err();
5398        assert!(matches!(err, ProtocolError::WrongArity(_)));
5399
5400        let err = Command::from_frame(cmd(&["PROTO.SETFIELD", "key", "field", "value", "extra"]))
5401            .unwrap_err();
5402        assert!(matches!(err, ProtocolError::WrongArity(_)));
5403    }
5404
5405    // --- proto.delfield ---
5406
5407    #[test]
5408    fn proto_delfield_basic() {
5409        assert_eq!(
5410            Command::from_frame(cmd(&["PROTO.DELFIELD", "user:1", "name"])).unwrap(),
5411            Command::ProtoDelField {
5412                key: "user:1".into(),
5413                field_path: "name".into(),
5414            },
5415        );
5416    }
5417
5418    #[test]
5419    fn proto_delfield_wrong_arity() {
5420        let err = Command::from_frame(cmd(&["PROTO.DELFIELD"])).unwrap_err();
5421        assert!(matches!(err, ProtocolError::WrongArity(_)));
5422
5423        let err = Command::from_frame(cmd(&["PROTO.DELFIELD", "key"])).unwrap_err();
5424        assert!(matches!(err, ProtocolError::WrongArity(_)));
5425
5426        let err =
5427            Command::from_frame(cmd(&["PROTO.DELFIELD", "key", "field", "extra"])).unwrap_err();
5428        assert!(matches!(err, ProtocolError::WrongArity(_)));
5429    }
5430
5431    // --- vector commands ---
5432
5433    #[test]
5434    fn vadd_basic() {
5435        assert_eq!(
5436            Command::from_frame(cmd(&["VADD", "vecs", "elem1", "0.1", "0.2", "0.3"])).unwrap(),
5437            Command::VAdd {
5438                key: "vecs".into(),
5439                element: "elem1".into(),
5440                vector: vec![0.1, 0.2, 0.3],
5441                metric: 0,
5442                quantization: 0,
5443                connectivity: 16,
5444                expansion_add: 64,
5445            },
5446        );
5447    }
5448
5449    #[test]
5450    fn vadd_with_options() {
5451        assert_eq!(
5452            Command::from_frame(cmd(&[
5453                "VADD", "vecs", "elem1", "1.0", "2.0", "METRIC", "L2", "QUANT", "F16", "M", "32",
5454                "EF", "128"
5455            ]))
5456            .unwrap(),
5457            Command::VAdd {
5458                key: "vecs".into(),
5459                element: "elem1".into(),
5460                vector: vec![1.0, 2.0],
5461                metric: 1,
5462                quantization: 1,
5463                connectivity: 32,
5464                expansion_add: 128,
5465            },
5466        );
5467    }
5468
5469    #[test]
5470    fn vadd_wrong_arity() {
5471        // no args
5472        let err = Command::from_frame(cmd(&["VADD"])).unwrap_err();
5473        assert!(matches!(err, ProtocolError::WrongArity(_)));
5474
5475        // key only
5476        let err = Command::from_frame(cmd(&["VADD", "key"])).unwrap_err();
5477        assert!(matches!(err, ProtocolError::WrongArity(_)));
5478
5479        // key + element but no vector — "elem" is not numeric so vector is empty
5480        let err = Command::from_frame(cmd(&["VADD", "key", "elem"])).unwrap_err();
5481        assert!(matches!(
5482            err,
5483            ProtocolError::WrongArity(_) | ProtocolError::InvalidCommandFrame(_)
5484        ));
5485    }
5486
5487    #[test]
5488    fn vadd_m_exceeds_max() {
5489        let err =
5490            Command::from_frame(cmd(&["VADD", "key", "elem", "1.0", "M", "9999"])).unwrap_err();
5491        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5492    }
5493
5494    #[test]
5495    fn vadd_ef_exceeds_max() {
5496        let err =
5497            Command::from_frame(cmd(&["VADD", "key", "elem", "1.0", "EF", "9999"])).unwrap_err();
5498        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5499    }
5500
5501    #[test]
5502    fn vadd_unknown_metric() {
5503        let err = Command::from_frame(cmd(&["VADD", "key", "elem", "1.0", "METRIC", "HAMMING"]))
5504            .unwrap_err();
5505        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5506    }
5507
5508    #[test]
5509    fn vadd_unknown_quantization() {
5510        let err =
5511            Command::from_frame(cmd(&["VADD", "key", "elem", "1.0", "QUANT", "F64"])).unwrap_err();
5512        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5513    }
5514
5515    // --- vadd_batch ---
5516
5517    #[test]
5518    fn vadd_batch_basic() {
5519        assert_eq!(
5520            Command::from_frame(cmd(&[
5521                "VADD_BATCH",
5522                "vecs",
5523                "DIM",
5524                "3",
5525                "a",
5526                "0.1",
5527                "0.2",
5528                "0.3",
5529                "b",
5530                "0.4",
5531                "0.5",
5532                "0.6"
5533            ]))
5534            .unwrap(),
5535            Command::VAddBatch {
5536                key: "vecs".into(),
5537                entries: vec![
5538                    ("a".into(), vec![0.1, 0.2, 0.3]),
5539                    ("b".into(), vec![0.4, 0.5, 0.6]),
5540                ],
5541                dim: 3,
5542                metric: 0,
5543                quantization: 0,
5544                connectivity: 16,
5545                expansion_add: 64,
5546            },
5547        );
5548    }
5549
5550    #[test]
5551    fn vadd_batch_with_options() {
5552        assert_eq!(
5553            Command::from_frame(cmd(&[
5554                "VADD_BATCH",
5555                "vecs",
5556                "DIM",
5557                "2",
5558                "a",
5559                "1.0",
5560                "2.0",
5561                "METRIC",
5562                "L2",
5563                "QUANT",
5564                "F16",
5565                "M",
5566                "32",
5567                "EF",
5568                "128"
5569            ]))
5570            .unwrap(),
5571            Command::VAddBatch {
5572                key: "vecs".into(),
5573                entries: vec![("a".into(), vec![1.0, 2.0])],
5574                dim: 2,
5575                metric: 1,
5576                quantization: 1,
5577                connectivity: 32,
5578                expansion_add: 128,
5579            },
5580        );
5581    }
5582
5583    #[test]
5584    fn vadd_batch_single_entry() {
5585        assert_eq!(
5586            Command::from_frame(cmd(&["VADD_BATCH", "vecs", "DIM", "1", "x", "1.5"])).unwrap(),
5587            Command::VAddBatch {
5588                key: "vecs".into(),
5589                entries: vec![("x".into(), vec![1.5])],
5590                dim: 1,
5591                metric: 0,
5592                quantization: 0,
5593                connectivity: 16,
5594                expansion_add: 64,
5595            },
5596        );
5597    }
5598
5599    #[test]
5600    fn vadd_batch_empty_entries() {
5601        // key + DIM + n but no entries — valid, returns empty batch
5602        assert_eq!(
5603            Command::from_frame(cmd(&["VADD_BATCH", "vecs", "DIM", "3"])).unwrap(),
5604            Command::VAddBatch {
5605                key: "vecs".into(),
5606                entries: vec![],
5607                dim: 3,
5608                metric: 0,
5609                quantization: 0,
5610                connectivity: 16,
5611                expansion_add: 64,
5612            },
5613        );
5614    }
5615
5616    #[test]
5617    fn vadd_batch_wrong_arity() {
5618        let err = Command::from_frame(cmd(&["VADD_BATCH"])).unwrap_err();
5619        assert!(matches!(err, ProtocolError::WrongArity(_)));
5620
5621        let err = Command::from_frame(cmd(&["VADD_BATCH", "key"])).unwrap_err();
5622        assert!(matches!(err, ProtocolError::WrongArity(_)));
5623
5624        let err = Command::from_frame(cmd(&["VADD_BATCH", "key", "DIM"])).unwrap_err();
5625        assert!(matches!(err, ProtocolError::WrongArity(_)));
5626    }
5627
5628    #[test]
5629    fn vadd_batch_missing_dim_keyword() {
5630        let err = Command::from_frame(cmd(&["VADD_BATCH", "key", "3", "a", "1.0"])).unwrap_err();
5631        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5632    }
5633
5634    #[test]
5635    fn vadd_batch_dim_zero() {
5636        let err = Command::from_frame(cmd(&["VADD_BATCH", "key", "DIM", "0"])).unwrap_err();
5637        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5638    }
5639
5640    #[test]
5641    fn vadd_batch_dim_exceeds_max() {
5642        let err = Command::from_frame(cmd(&["VADD_BATCH", "key", "DIM", "99999"])).unwrap_err();
5643        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5644    }
5645
5646    #[test]
5647    fn vadd_batch_insufficient_floats() {
5648        // DIM=3 but only 2 floats for element "a"
5649        let err = Command::from_frame(cmd(&["VADD_BATCH", "key", "DIM", "3", "a", "1.0", "2.0"]))
5650            .unwrap_err();
5651        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5652    }
5653
5654    #[test]
5655    fn vadd_batch_m_exceeds_max() {
5656        let err = Command::from_frame(cmd(&[
5657            "VADD_BATCH",
5658            "key",
5659            "DIM",
5660            "2",
5661            "a",
5662            "1.0",
5663            "2.0",
5664            "M",
5665            "9999",
5666        ]))
5667        .unwrap_err();
5668        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5669    }
5670
5671    #[test]
5672    fn vadd_batch_ef_exceeds_max() {
5673        let err = Command::from_frame(cmd(&[
5674            "VADD_BATCH",
5675            "key",
5676            "DIM",
5677            "2",
5678            "a",
5679            "1.0",
5680            "2.0",
5681            "EF",
5682            "9999",
5683        ]))
5684        .unwrap_err();
5685        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5686    }
5687
5688    #[test]
5689    fn vsim_basic() {
5690        assert_eq!(
5691            Command::from_frame(cmd(&["VSIM", "vecs", "0.1", "0.2", "0.3", "COUNT", "5"])).unwrap(),
5692            Command::VSim {
5693                key: "vecs".into(),
5694                query: vec![0.1, 0.2, 0.3],
5695                count: 5,
5696                ef_search: 0,
5697                with_scores: false,
5698            },
5699        );
5700    }
5701
5702    #[test]
5703    fn vsim_with_ef_and_scores() {
5704        assert_eq!(
5705            Command::from_frame(cmd(&[
5706                "VSIM",
5707                "vecs",
5708                "1.0",
5709                "2.0",
5710                "COUNT",
5711                "10",
5712                "EF",
5713                "128",
5714                "WITHSCORES"
5715            ]))
5716            .unwrap(),
5717            Command::VSim {
5718                key: "vecs".into(),
5719                query: vec![1.0, 2.0],
5720                count: 10,
5721                ef_search: 128,
5722                with_scores: true,
5723            },
5724        );
5725    }
5726
5727    #[test]
5728    fn vsim_missing_count() {
5729        // all args are numeric so they're consumed as query — COUNT is never found
5730        let err = Command::from_frame(cmd(&["VSIM", "key", "1.0", "2.0"])).unwrap_err();
5731        assert!(matches!(
5732            err,
5733            ProtocolError::InvalidCommandFrame(_) | ProtocolError::WrongArity(_)
5734        ));
5735    }
5736
5737    #[test]
5738    fn vsim_wrong_arity() {
5739        let err = Command::from_frame(cmd(&["VSIM"])).unwrap_err();
5740        assert!(matches!(err, ProtocolError::WrongArity(_)));
5741    }
5742
5743    #[test]
5744    fn vsim_count_exceeds_max() {
5745        let err = Command::from_frame(cmd(&["VSIM", "key", "1.0", "COUNT", "99999"])).unwrap_err();
5746        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5747    }
5748
5749    #[test]
5750    fn vsim_ef_exceeds_max() {
5751        let err = Command::from_frame(cmd(&["VSIM", "key", "1.0", "COUNT", "5", "EF", "9999"]))
5752            .unwrap_err();
5753        assert!(matches!(err, ProtocolError::InvalidCommandFrame(_)));
5754    }
5755
5756    #[test]
5757    fn vrem_basic() {
5758        assert_eq!(
5759            Command::from_frame(cmd(&["VREM", "key", "elem"])).unwrap(),
5760            Command::VRem {
5761                key: "key".into(),
5762                element: "elem".into(),
5763            },
5764        );
5765    }
5766
5767    #[test]
5768    fn vrem_wrong_arity() {
5769        let err = Command::from_frame(cmd(&["VREM"])).unwrap_err();
5770        assert!(matches!(err, ProtocolError::WrongArity(_)));
5771
5772        let err = Command::from_frame(cmd(&["VREM", "key"])).unwrap_err();
5773        assert!(matches!(err, ProtocolError::WrongArity(_)));
5774    }
5775
5776    #[test]
5777    fn vget_basic() {
5778        assert_eq!(
5779            Command::from_frame(cmd(&["VGET", "key", "elem"])).unwrap(),
5780            Command::VGet {
5781                key: "key".into(),
5782                element: "elem".into(),
5783            },
5784        );
5785    }
5786
5787    #[test]
5788    fn vget_wrong_arity() {
5789        let err = Command::from_frame(cmd(&["VGET"])).unwrap_err();
5790        assert!(matches!(err, ProtocolError::WrongArity(_)));
5791    }
5792
5793    #[test]
5794    fn vcard_basic() {
5795        assert_eq!(
5796            Command::from_frame(cmd(&["VCARD", "key"])).unwrap(),
5797            Command::VCard { key: "key".into() },
5798        );
5799    }
5800
5801    #[test]
5802    fn vcard_wrong_arity() {
5803        let err = Command::from_frame(cmd(&["VCARD"])).unwrap_err();
5804        assert!(matches!(err, ProtocolError::WrongArity(_)));
5805    }
5806
5807    #[test]
5808    fn vdim_basic() {
5809        assert_eq!(
5810            Command::from_frame(cmd(&["VDIM", "key"])).unwrap(),
5811            Command::VDim { key: "key".into() },
5812        );
5813    }
5814
5815    #[test]
5816    fn vdim_wrong_arity() {
5817        let err = Command::from_frame(cmd(&["VDIM"])).unwrap_err();
5818        assert!(matches!(err, ProtocolError::WrongArity(_)));
5819    }
5820
5821    #[test]
5822    fn vinfo_basic() {
5823        assert_eq!(
5824            Command::from_frame(cmd(&["VINFO", "key"])).unwrap(),
5825            Command::VInfo { key: "key".into() },
5826        );
5827    }
5828
5829    #[test]
5830    fn vinfo_wrong_arity() {
5831        let err = Command::from_frame(cmd(&["VINFO"])).unwrap_err();
5832        assert!(matches!(err, ProtocolError::WrongArity(_)));
5833    }
5834
5835    #[test]
5836    fn restore_basic() {
5837        assert_eq!(
5838            Command::from_frame(cmd(&["RESTORE", "mykey", "5000", "serialized"])).unwrap(),
5839            Command::Restore {
5840                key: "mykey".into(),
5841                ttl_ms: 5000,
5842                data: Bytes::from("serialized"),
5843                replace: false,
5844            },
5845        );
5846    }
5847
5848    #[test]
5849    fn restore_with_replace() {
5850        assert_eq!(
5851            Command::from_frame(cmd(&["RESTORE", "mykey", "0", "data", "REPLACE"])).unwrap(),
5852            Command::Restore {
5853                key: "mykey".into(),
5854                ttl_ms: 0,
5855                data: Bytes::from("data"),
5856                replace: true,
5857            },
5858        );
5859    }
5860
5861    #[test]
5862    fn restore_wrong_arity() {
5863        let err = Command::from_frame(cmd(&["RESTORE", "key"])).unwrap_err();
5864        assert!(matches!(err, ProtocolError::WrongArity(_)));
5865    }
5866
5867    // -- is_write / primary_key --
5868
5869    #[test]
5870    fn is_write_returns_true_for_mutations() {
5871        assert!(Command::Set {
5872            key: "k".into(),
5873            value: bytes::Bytes::new(),
5874            expire: None,
5875            nx: false,
5876            xx: false,
5877        }
5878        .is_write());
5879        assert!(Command::Del {
5880            keys: vec!["k".into()]
5881        }
5882        .is_write());
5883        assert!(Command::Incr { key: "k".into() }.is_write());
5884        assert!(Command::HSet {
5885            key: "k".into(),
5886            fields: vec![],
5887        }
5888        .is_write());
5889        assert!(Command::LPush {
5890            key: "k".into(),
5891            values: vec![]
5892        }
5893        .is_write());
5894        assert!(Command::ZAdd {
5895            key: "k".into(),
5896            flags: crate::command::ZAddFlags::default(),
5897            members: vec![],
5898        }
5899        .is_write());
5900        assert!(Command::SAdd {
5901            key: "k".into(),
5902            members: vec![]
5903        }
5904        .is_write());
5905        assert!(Command::FlushDb { async_mode: false }.is_write());
5906    }
5907
5908    #[test]
5909    fn is_write_returns_false_for_reads() {
5910        assert!(!Command::Get { key: "k".into() }.is_write());
5911        assert!(!Command::HGet {
5912            key: "k".into(),
5913            field: "f".into()
5914        }
5915        .is_write());
5916        assert!(!Command::Ping(None).is_write());
5917        assert!(!Command::ClusterInfo.is_write());
5918        assert!(!Command::DbSize.is_write());
5919    }
5920
5921    #[test]
5922    fn primary_key_returns_first_key() {
5923        assert_eq!(
5924            Command::Get {
5925                key: "hello".into()
5926            }
5927            .primary_key(),
5928            Some("hello")
5929        );
5930        assert_eq!(
5931            Command::Del {
5932                keys: vec!["a".into(), "b".into()]
5933            }
5934            .primary_key(),
5935            Some("a")
5936        );
5937        assert_eq!(Command::Ping(None).primary_key(), None);
5938        assert_eq!(Command::DbSize.primary_key(), None);
5939    }
5940}