File Coverage

blib/lib/Async/Redis/KeyExtractor.pm
Criterion Covered Total %
statement 53 101 52.4
branch 20 60 33.3
condition 8 27 29.6
subroutine 9 18 50.0
pod 0 2 0.0
total 90 208 43.2


line stmt bran cond sub pod time code
1             # lib/Future/IO/Redis/KeyExtractor.pm
2             package Async::Redis::KeyExtractor;
3              
4 74     74   178888 use strict;
  74         173  
  74         4078  
5 74     74   531 use warnings;
  74         145  
  74         6300  
6 74     74   1625 use 5.018;
  74         291  
7              
8             # Key position handlers for each command
9             # Generated from commands.json key_specs + manual overrides
10             our %KEY_POSITIONS = (
11             # Simple single-key commands (first arg is key)
12             'GET' => sub { (0) },
13             'SET' => sub { (0) },
14             'GETEX' => sub { (0) },
15             'GETDEL' => sub { (0) },
16             'GETSET' => sub { (0) },
17             'APPEND' => sub { (0) },
18             'STRLEN' => sub { (0) },
19             'SETEX' => sub { (0) },
20             'PSETEX' => sub { (0) },
21             'SETNX' => sub { (0) },
22             'SETRANGE' => sub { (0) },
23             'GETRANGE' => sub { (0) },
24             'INCR' => sub { (0) },
25             'DECR' => sub { (0) },
26             'INCRBY' => sub { (0) },
27             'DECRBY' => sub { (0) },
28             'INCRBYFLOAT' => sub { (0) },
29              
30             # Multi-key commands (all args are keys)
31             'MGET' => sub { (0 .. $#_) },
32             'DEL' => sub { (0 .. $#_) },
33             'UNLINK' => sub { (0 .. $#_) },
34             'EXISTS' => sub { (0 .. $#_) },
35             'TOUCH' => sub { (0 .. $#_) },
36             'WATCH' => sub { (0 .. $#_) },
37              
38             # MSET: even indices are keys
39             'MSET' => sub { grep { $_ % 2 == 0 } (0 .. $#_) },
40             'MSETNX' => sub { grep { $_ % 2 == 0 } (0 .. $#_) },
41              
42             # Hash commands (first arg is key)
43             'HSET' => sub { (0) },
44             'HGET' => sub { (0) },
45             'HDEL' => sub { (0) },
46             'HEXISTS' => sub { (0) },
47             'HLEN' => sub { (0) },
48             'HKEYS' => sub { (0) },
49             'HVALS' => sub { (0) },
50             'HGETALL' => sub { (0) },
51             'HMSET' => sub { (0) },
52             'HMGET' => sub { (0) },
53             'HSETNX' => sub { (0) },
54             'HINCRBY' => sub { (0) },
55             'HINCRBYFLOAT' => sub { (0) },
56             'HSCAN' => sub { (0) },
57             'HRANDFIELD' => sub { (0) },
58              
59             # List commands (first arg is key)
60             'LPUSH' => sub { (0) },
61             'RPUSH' => sub { (0) },
62             'LPOP' => sub { (0) },
63             'RPOP' => sub { (0) },
64             'LLEN' => sub { (0) },
65             'LRANGE' => sub { (0) },
66             'LINDEX' => sub { (0) },
67             'LSET' => sub { (0) },
68             'LREM' => sub { (0) },
69             'LINSERT' => sub { (0) },
70             'LTRIM' => sub { (0) },
71             'LPOS' => sub { (0) },
72             'LPUSHX' => sub { (0) },
73             'RPUSHX' => sub { (0) },
74              
75             # Blocking list commands (first arg is key, or multiple keys)
76             'BLPOP' => \&_keys_for_blocking_list,
77             'BRPOP' => \&_keys_for_blocking_list,
78             'BLMOVE' => sub { (0, 1) }, # source and dest
79             'BRPOPLPUSH' => sub { (0, 1) },
80             'LMOVE' => sub { (0, 1) },
81              
82             # Set commands (first arg is key)
83             'SADD' => sub { (0) },
84             'SREM' => sub { (0) },
85             'SMEMBERS' => sub { (0) },
86             'SISMEMBER' => sub { (0) },
87             'SMISMEMBER' => sub { (0) },
88             'SCARD' => sub { (0) },
89             'SPOP' => sub { (0) },
90             'SRANDMEMBER' => sub { (0) },
91             'SSCAN' => sub { (0) },
92             'SMOVE' => sub { (0, 1) }, # source and dest
93             'SINTER' => sub { (0 .. $#_) },
94             'SUNION' => sub { (0 .. $#_) },
95             'SDIFF' => sub { (0 .. $#_) },
96             'SINTERSTORE' => sub { (0 .. $#_) },
97             'SUNIONSTORE' => sub { (0 .. $#_) },
98             'SDIFFSTORE' => sub { (0 .. $#_) },
99             'SINTERCARD' => \&_keys_for_sintercard,
100              
101             # Sorted set commands (first arg is key)
102             'ZADD' => sub { (0) },
103             'ZREM' => sub { (0) },
104             'ZSCORE' => sub { (0) },
105             'ZRANK' => sub { (0) },
106             'ZREVRANK' => sub { (0) },
107             'ZRANGE' => sub { (0) },
108             'ZREVRANGE' => sub { (0) },
109             'ZRANGEBYSCORE' => sub { (0) },
110             'ZREVRANGEBYSCORE' => sub { (0) },
111             'ZCARD' => sub { (0) },
112             'ZCOUNT' => sub { (0) },
113             'ZINCRBY' => sub { (0) },
114             'ZLEXCOUNT' => sub { (0) },
115             'ZRANGEBYLEX' => sub { (0) },
116             'ZREVRANGEBYLEX' => sub { (0) },
117             'ZPOPMIN' => sub { (0) },
118             'ZPOPMAX' => sub { (0) },
119             'BZPOPMIN' => \&_keys_for_blocking_list,
120             'BZPOPMAX' => \&_keys_for_blocking_list,
121             'ZRANGESTORE' => sub { (0, 1) },
122             'ZINTER' => \&_keys_for_zinter,
123             'ZUNION' => \&_keys_for_zinter,
124             'ZDIFF' => \&_keys_for_zinter,
125             'ZINTERSTORE' => \&_keys_for_zinterstore,
126             'ZUNIONSTORE' => \&_keys_for_zinterstore,
127             'ZDIFFSTORE' => \&_keys_for_zinterstore,
128             'ZSCAN' => sub { (0) },
129             'ZRANDMEMBER' => sub { (0) },
130             'ZMPOP' => \&_keys_for_zmpop,
131             'BZMPOP' => \&_keys_for_bzmpop,
132              
133             # Key commands
134             'EXPIRE' => sub { (0) },
135             'EXPIREAT' => sub { (0) },
136             'PEXPIRE' => sub { (0) },
137             'PEXPIREAT' => sub { (0) },
138             'TTL' => sub { (0) },
139             'PTTL' => sub { (0) },
140             'PERSIST' => sub { (0) },
141             'TYPE' => sub { (0) },
142             'RENAME' => sub { (0, 1) },
143             'RENAMENX' => sub { (0, 1) },
144             'COPY' => sub { (0, 1) },
145             'DUMP' => sub { (0) },
146             'RESTORE' => sub { (0) },
147             'EXPIRETIME' => sub { (0) },
148             'PEXPIRETIME' => sub { (0) },
149             'OBJECT' => \&_keys_for_object,
150              
151             # EVAL/EVALSHA - dynamic based on numkeys
152             'EVAL' => \&_keys_for_eval,
153             'EVALSHA' => \&_keys_for_eval,
154             'EVALSHA_RO' => \&_keys_for_eval,
155             'EVAL_RO' => \&_keys_for_eval,
156             'FCALL' => \&_keys_for_eval,
157             'FCALL_RO' => \&_keys_for_eval,
158              
159             # BITOP - skip operation arg
160             'BITOP' => sub { (1 .. $#_) },
161              
162             # Stream commands
163             'XADD' => sub { (0) },
164             'XLEN' => sub { (0) },
165             'XRANGE' => sub { (0) },
166             'XREVRANGE' => sub { (0) },
167             'XREAD' => \&_keys_for_xread,
168             'XREADGROUP' => \&_keys_for_xread,
169             'XINFO' => \&_keys_for_xinfo,
170             'XGROUP' => \&_keys_for_xgroup,
171             'XACK' => sub { (0) },
172             'XCLAIM' => sub { (0) },
173             'XAUTOCLAIM' => sub { (0) },
174             'XPENDING' => sub { (0) },
175             'XTRIM' => sub { (0) },
176             'XDEL' => sub { (0) },
177             'XSETID' => sub { (0) },
178              
179             # Geo commands
180             'GEOADD' => sub { (0) },
181             'GEOPOS' => sub { (0) },
182             'GEODIST' => sub { (0) },
183             'GEOHASH' => sub { (0) },
184             'GEORADIUS' => \&_keys_for_georadius,
185             'GEORADIUSBYMEMBER' => \&_keys_for_georadius,
186             'GEOSEARCH' => sub { (0) },
187             'GEOSEARCHSTORE' => sub { (0, 1) },
188              
189             # MIGRATE - special handling
190             'MIGRATE' => \&_keys_for_migrate,
191              
192             # SORT
193             'SORT' => sub { (0) },
194             'SORT_RO' => sub { (0) },
195              
196             # SCAN commands return patterns, not keys - first arg is key for HSCAN/SSCAN/ZSCAN
197             'SCAN' => sub { () }, # No key, cursor-based
198              
199             # Pub/Sub - channels, not keys
200             'PUBLISH' => sub { () },
201             'SUBSCRIBE' => sub { () },
202             'UNSUBSCRIBE' => sub { () },
203             'PSUBSCRIBE' => sub { () },
204             'PUNSUBSCRIBE' => sub { () },
205              
206             # Server commands - no keys
207             'PING' => sub { () },
208             'ECHO' => sub { () },
209             'AUTH' => sub { () },
210             'SELECT' => sub { () },
211             'INFO' => sub { () },
212             'DBSIZE' => sub { () },
213             'FLUSHDB' => sub { () },
214             'FLUSHALL' => sub { () },
215             'SAVE' => sub { () },
216             'BGSAVE' => sub { () },
217             'LASTSAVE' => sub { () },
218             'TIME' => sub { () },
219             'CONFIG' => sub { () },
220             'CLIENT' => sub { () },
221             'SLOWLOG' => sub { () },
222             'DEBUG' => sub { () },
223             'MEMORY' => sub { () },
224             'MODULE' => sub { () },
225             'ACL' => sub { () },
226             'COMMAND' => sub { () },
227             'MULTI' => sub { () },
228             'EXEC' => sub { () },
229             'DISCARD' => sub { () },
230             'UNWATCH' => sub { () },
231             'SCRIPT' => sub { () },
232             'CLUSTER' => sub { () },
233             'READONLY' => sub { () },
234             'READWRITE' => sub { () },
235             'WAIT' => sub { () },
236             'KEYS' => sub { () }, # Pattern, not literal key
237             'RANDOMKEY' => sub { () },
238             );
239              
240             # Fallback patterns for unknown commands
241             our @FALLBACK_PATTERNS = (
242             # Hash commands: first arg is key
243             [ qr/^H(?:SET|GET|DEL|EXISTS|INCR|LEN|KEYS|VALS|GETALL|SCAN|MGET|MSET)/i, sub { (0) } ],
244              
245             # List commands: first arg is key
246             [ qr/^[LR](?:PUSH|POP|LEN|INDEX|RANGE|SET|TRIM|REM|INSERT|POS)/i, sub { (0) } ],
247              
248             # Set commands: first arg is key
249             [ qr/^S(?:ADD|REM|MEMBERS|ISMEMBER|CARD|POP|RANDMEMBER|SCAN)/i, sub { (0) } ],
250              
251             # Sorted set commands: first arg is key
252             [ qr/^Z(?:ADD|REM|SCORE|RANK|RANGE|CARD|COUNT|INCRBY|SCAN)/i, sub { (0) } ],
253              
254             # Generic fallback: assume first arg is key for unknown X* commands (streams)
255             [ qr/^X/i, sub { (0) } ],
256             );
257              
258             sub extract_key_indices {
259 26     26 0 223175 my ($command, @args) = @_;
260 26         44 $command = uc($command);
261              
262             # Check explicit handlers
263 26 100       76 if (my $handler = $KEY_POSITIONS{$command}) {
264 25         55 return $handler->(@args);
265             }
266              
267             # Try fallback patterns
268 1         3 for my $pattern (@FALLBACK_PATTERNS) {
269 5 50       21 if ($command =~ $pattern->[0]) {
270 0         0 return $pattern->[1]->(@args);
271             }
272             }
273              
274             # Unknown command - no prefixing, warn in debug mode
275 1 50       2 warn "Unknown command '$command': key prefixing skipped" if $ENV{REDIS_DEBUG};
276 1         2 return ();
277             }
278              
279             sub apply_prefix {
280 6     6 0 5147 my ($prefix, $command, @args) = @_;
281 6 100 100     27 return @args unless defined $prefix && $prefix ne '';
282              
283 4         9 my @key_indices = extract_key_indices($command, @args);
284 4         6 for my $i (@key_indices) {
285 7 50       12 next if $i > $#args; # Safety check
286 7         12 $args[$i] = $prefix . $args[$i];
287             }
288              
289 4         12 return @args;
290             }
291              
292             # --- Custom handlers for complex commands ---
293              
294             sub _keys_for_eval {
295 3     3   7 my (@args) = @_;
296             # EVAL script numkeys [key ...] [arg ...]
297             # EVALSHA sha1 numkeys [key ...] [arg ...]
298 3 50       7 return () unless @args >= 2;
299              
300 3         4 my $numkeys = $args[1];
301 3 100 33     23 return () unless defined $numkeys && $numkeys =~ /^\d+$/ && $numkeys > 0;
      66        
302              
303             # Keys are at indices 2 through 2+numkeys-1
304 2         10 return (2 .. 2 + $numkeys - 1);
305             }
306              
307             sub _keys_for_xread {
308 2     2   5 my (@args) = @_;
309              
310             # Find STREAMS keyword
311 2         2 my $streams_idx;
312 2         6 for my $i (0 .. $#args) {
313 6 100       15 if (uc($args[$i]) eq 'STREAMS') {
314 2         3 $streams_idx = $i;
315 2         3 last;
316             }
317             }
318 2 50       5 return () unless defined $streams_idx;
319              
320             # Keys are between STREAMS and the IDs
321             # Number of streams = number of IDs = (remaining args after STREAMS) / 2
322 2         3 my $remaining = $#args - $streams_idx;
323 2         5 my $num_streams = int($remaining / 2);
324              
325 2 50       21 return () unless $num_streams > 0;
326 2         9 return ($streams_idx + 1 .. $streams_idx + $num_streams);
327             }
328              
329             sub _keys_for_migrate {
330 2     2   33 my (@args) = @_;
331 2         4 my @key_indices;
332              
333             # MIGRATE host port key|"" db timeout [COPY] [REPLACE] [AUTH pw] [KEYS k1 k2 ...]
334              
335             # Single key at position 2 (unless empty string for multi-key)
336 2 100 66     11 if (@args > 2 && $args[2] ne '') {
337 1         2 push @key_indices, 2;
338             }
339              
340             # Multi-key after KEYS keyword
341 2         6 for my $i (0 .. $#args) {
342 11 100       17 if (uc($args[$i]) eq 'KEYS') {
343 1         3 push @key_indices, ($i + 1 .. $#args);
344 1         2 last;
345             }
346             }
347              
348 2         8 return @key_indices;
349             }
350              
351             sub _keys_for_object {
352 1     1   3 my (@args) = @_;
353             # OBJECT subcommand [key] [...]
354 1 50       4 return () unless @args >= 2;
355              
356 1         2 my $subcmd = uc($args[0]);
357             # Most OBJECT subcommands take key as second arg
358 1 50       8 if ($subcmd =~ /^(ENCODING|FREQ|IDLETIME|REFCOUNT)$/) {
359 1         4 return (1);
360             }
361 0           return ();
362             }
363              
364             sub _keys_for_blocking_list {
365 0     0     my (@args) = @_;
366             # BLPOP key [key ...] timeout
367             # Last arg is timeout, rest are keys
368 0 0         return () unless @args >= 2;
369 0           return (0 .. $#args - 1);
370             }
371              
372             sub _keys_for_zinter {
373 0     0     my (@args) = @_;
374             # ZINTER numkeys key [key ...] [WEIGHTS ...] [AGGREGATE ...]
375 0 0         return () unless @args >= 1;
376 0           my $numkeys = $args[0];
377 0 0 0       return () unless $numkeys =~ /^\d+$/ && $numkeys > 0;
378 0           return (1 .. $numkeys);
379             }
380              
381             sub _keys_for_zinterstore {
382 0     0     my (@args) = @_;
383             # ZINTERSTORE destination numkeys key [key ...] [WEIGHTS ...] [AGGREGATE ...]
384 0 0         return () unless @args >= 2;
385 0           my $numkeys = $args[1];
386 0 0 0       return () unless $numkeys =~ /^\d+$/ && $numkeys > 0;
387 0           return (0, 2 .. 1 + $numkeys); # dest + source keys
388             }
389              
390             sub _keys_for_sintercard {
391 0     0     my (@args) = @_;
392             # SINTERCARD numkeys key [key ...] [LIMIT limit]
393 0 0         return () unless @args >= 1;
394 0           my $numkeys = $args[0];
395 0 0 0       return () unless $numkeys =~ /^\d+$/ && $numkeys > 0;
396 0           return (1 .. $numkeys);
397             }
398              
399             sub _keys_for_zmpop {
400 0     0     my (@args) = @_;
401             # ZMPOP numkeys key [key ...] MIN|MAX [COUNT count]
402 0 0         return () unless @args >= 1;
403 0           my $numkeys = $args[0];
404 0 0 0       return () unless $numkeys =~ /^\d+$/ && $numkeys > 0;
405 0           return (1 .. $numkeys);
406             }
407              
408             sub _keys_for_bzmpop {
409 0     0     my (@args) = @_;
410             # BZMPOP timeout numkeys key [key ...] MIN|MAX [COUNT count]
411 0 0         return () unless @args >= 2;
412 0           my $numkeys = $args[1];
413 0 0 0       return () unless $numkeys =~ /^\d+$/ && $numkeys > 0;
414 0           return (2 .. 1 + $numkeys);
415             }
416              
417             sub _keys_for_xinfo {
418 0     0     my (@args) = @_;
419             # XINFO STREAM key, XINFO GROUPS key, etc.
420 0 0         return () unless @args >= 2;
421 0           my $subcmd = uc($args[0]);
422 0 0         if ($subcmd =~ /^(STREAM|GROUPS|CONSUMERS)$/) {
423 0           return (1);
424             }
425 0           return ();
426             }
427              
428             sub _keys_for_xgroup {
429 0     0     my (@args) = @_;
430             # XGROUP CREATE key groupname id, XGROUP DESTROY key groupname, etc.
431 0 0         return () unless @args >= 2;
432 0           my $subcmd = uc($args[0]);
433 0 0         if ($subcmd =~ /^(CREATE|DESTROY|SETID|DELCONSUMER|CREATECONSUMER)$/) {
434 0           return (1);
435             }
436 0           return ();
437             }
438              
439             sub _keys_for_georadius {
440 0     0     my (@args) = @_;
441 0           my @indices = (0); # First arg is always key
442              
443             # Look for STORE and STOREDIST
444 0           for my $i (0 .. $#args - 1) {
445 0 0         if (uc($args[$i]) =~ /^(STORE|STOREDIST)$/) {
446 0           push @indices, $i + 1;
447             }
448             }
449              
450 0           return @indices;
451             }
452              
453             1;
454              
455             __END__