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 92     92   142834 use strict;
  92         135  
  92         3351  
5 92     92   334 use warnings;
  92         132  
  92         4560  
6 92     92   1356 use 5.018;
  92         255  
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             # HyperLogLog commands
160             'PFADD' => sub { (0) },
161             'PFCOUNT' => sub { (0 .. $#_) },
162             'PFMERGE' => sub { (0 .. $#_) },
163              
164             # Bit commands (first arg is key)
165             'GETBIT' => sub { (0) },
166             'SETBIT' => sub { (0) },
167             'BITCOUNT' => sub { (0) },
168             'BITPOS' => sub { (0) },
169              
170             # Hash length
171             'HSTRLEN' => sub { (0) },
172              
173             # Sorted set multi-score lookup
174             'ZMSCORE' => sub { (0) },
175              
176             # BITOP - skip operation arg
177             'BITOP' => sub { (1 .. $#_) },
178              
179             # Stream commands
180             'XADD' => sub { (0) },
181             'XLEN' => sub { (0) },
182             'XRANGE' => sub { (0) },
183             'XREVRANGE' => sub { (0) },
184             'XREAD' => \&_keys_for_xread,
185             'XREADGROUP' => \&_keys_for_xread,
186             'XINFO' => \&_keys_for_xinfo,
187             'XGROUP' => \&_keys_for_xgroup,
188             'XACK' => sub { (0) },
189             'XCLAIM' => sub { (0) },
190             'XAUTOCLAIM' => sub { (0) },
191             'XPENDING' => sub { (0) },
192             'XTRIM' => sub { (0) },
193             'XDEL' => sub { (0) },
194             'XSETID' => sub { (0) },
195              
196             # Geo commands
197             'GEOADD' => sub { (0) },
198             'GEOPOS' => sub { (0) },
199             'GEODIST' => sub { (0) },
200             'GEOHASH' => sub { (0) },
201             'GEORADIUS' => \&_keys_for_georadius,
202             'GEORADIUSBYMEMBER' => \&_keys_for_georadius,
203             'GEOSEARCH' => sub { (0) },
204             'GEOSEARCHSTORE' => sub { (0, 1) },
205              
206             # MIGRATE - special handling
207             'MIGRATE' => \&_keys_for_migrate,
208              
209             # SORT
210             'SORT' => sub { (0) },
211             'SORT_RO' => sub { (0) },
212              
213             # SCAN commands return patterns, not keys - first arg is key for HSCAN/SSCAN/ZSCAN
214             'SCAN' => sub { () }, # No key, cursor-based
215              
216             # Pub/Sub - channels, not keys
217             'PUBLISH' => sub { () },
218             'SUBSCRIBE' => sub { () },
219             'UNSUBSCRIBE' => sub { () },
220             'PSUBSCRIBE' => sub { () },
221             'PUNSUBSCRIBE' => sub { () },
222              
223             # Server commands - no keys
224             'PING' => sub { () },
225             'ECHO' => sub { () },
226             'AUTH' => sub { () },
227             'SELECT' => sub { () },
228             'INFO' => sub { () },
229             'DBSIZE' => sub { () },
230             'FLUSHDB' => sub { () },
231             'FLUSHALL' => sub { () },
232             'SAVE' => sub { () },
233             'BGSAVE' => sub { () },
234             'LASTSAVE' => sub { () },
235             'TIME' => sub { () },
236             'CONFIG' => sub { () },
237             'CLIENT' => sub { () },
238             'SLOWLOG' => sub { () },
239             'DEBUG' => sub { () },
240             'MEMORY' => sub { () },
241             'MODULE' => sub { () },
242             'ACL' => sub { () },
243             'COMMAND' => sub { () },
244             'MULTI' => sub { () },
245             'EXEC' => sub { () },
246             'DISCARD' => sub { () },
247             'UNWATCH' => sub { () },
248             'SCRIPT' => sub { () },
249             'CLUSTER' => sub { () },
250             'READONLY' => sub { () },
251             'READWRITE' => sub { () },
252             'WAIT' => sub { () },
253             'KEYS' => sub { () }, # Pattern, not literal key
254             'RANDOMKEY' => sub { () },
255             );
256              
257             # Fallback patterns for unknown commands
258             our @FALLBACK_PATTERNS = (
259             # Hash commands: first arg is key
260             [ qr/^H(?:SET|GET|DEL|EXISTS|INCR|LEN|KEYS|VALS|GETALL|SCAN|MGET|MSET)/i, sub { (0) } ],
261              
262             # List commands: first arg is key
263             [ qr/^[LR](?:PUSH|POP|LEN|INDEX|RANGE|SET|TRIM|REM|INSERT|POS)/i, sub { (0) } ],
264              
265             # Set commands: first arg is key
266             [ qr/^S(?:ADD|REM|MEMBERS|ISMEMBER|CARD|POP|RANDMEMBER|SCAN)/i, sub { (0) } ],
267              
268             # Sorted set commands: first arg is key
269             [ qr/^Z(?:ADD|REM|SCORE|RANK|RANGE|CARD|COUNT|INCRBY|SCAN)/i, sub { (0) } ],
270              
271             # Generic fallback: assume first arg is key for unknown X* commands (streams)
272             [ qr/^X/i, sub { (0) } ],
273             );
274              
275             sub extract_key_indices {
276 26     26 0 163761 my ($command, @args) = @_;
277 26         41 $command = uc($command);
278              
279             # Check explicit handlers
280 26 100       69 if (my $handler = $KEY_POSITIONS{$command}) {
281 25         56 return $handler->(@args);
282             }
283              
284             # Try fallback patterns
285 1         3 for my $pattern (@FALLBACK_PATTERNS) {
286 5 50       20 if ($command =~ $pattern->[0]) {
287 0         0 return $pattern->[1]->(@args);
288             }
289             }
290              
291             # Unknown command - no prefixing, warn in debug mode
292 1 50       3 warn "Unknown command '$command': key prefixing skipped" if $ENV{REDIS_DEBUG};
293 1         3 return ();
294             }
295              
296             sub apply_prefix {
297 6     6 0 5227 my ($prefix, $command, @args) = @_;
298 6 100 100     26 return @args unless defined $prefix && $prefix ne '';
299              
300 4         8 my @key_indices = extract_key_indices($command, @args);
301 4         5 for my $i (@key_indices) {
302 7 50       12 next if $i > $#args; # Safety check
303 7         12 $args[$i] = $prefix . $args[$i];
304             }
305              
306 4         11 return @args;
307             }
308              
309             # --- Custom handlers for complex commands ---
310              
311             sub _keys_for_eval {
312 3     3   6 my (@args) = @_;
313             # EVAL script numkeys [key ...] [arg ...]
314             # EVALSHA sha1 numkeys [key ...] [arg ...]
315 3 50       7 return () unless @args >= 2;
316              
317 3         4 my $numkeys = $args[1];
318 3 100 33     26 return () unless defined $numkeys && $numkeys =~ /^\d+$/ && $numkeys > 0;
      66        
319              
320             # Keys are at indices 2 through 2+numkeys-1
321 2         8 return (2 .. 2 + $numkeys - 1);
322             }
323              
324             sub _keys_for_xread {
325 2     2   5 my (@args) = @_;
326              
327             # Find STREAMS keyword
328 2         3 my $streams_idx;
329 2         5 for my $i (0 .. $#args) {
330 6 100       16 if (uc($args[$i]) eq 'STREAMS') {
331 2         2 $streams_idx = $i;
332 2         3 last;
333             }
334             }
335 2 50       5 return () unless defined $streams_idx;
336              
337             # Keys are between STREAMS and the IDs
338             # Number of streams = number of IDs = (remaining args after STREAMS) / 2
339 2         7 my $remaining = $#args - $streams_idx;
340 2         5 my $num_streams = int($remaining / 2);
341              
342 2 50       5 return () unless $num_streams > 0;
343 2         8 return ($streams_idx + 1 .. $streams_idx + $num_streams);
344             }
345              
346             sub _keys_for_migrate {
347 2     2   5 my (@args) = @_;
348 2         3 my @key_indices;
349              
350             # MIGRATE host port key|"" db timeout [COPY] [REPLACE] [AUTH pw] [KEYS k1 k2 ...]
351              
352             # Single key at position 2 (unless empty string for multi-key)
353 2 100 66     11 if (@args > 2 && $args[2] ne '') {
354 1         2 push @key_indices, 2;
355             }
356              
357             # Multi-key after KEYS keyword
358 2         5 for my $i (0 .. $#args) {
359 11 100       17 if (uc($args[$i]) eq 'KEYS') {
360 1         3 push @key_indices, ($i + 1 .. $#args);
361 1         2 last;
362             }
363             }
364              
365 2         7 return @key_indices;
366             }
367              
368             sub _keys_for_object {
369 1     1   3 my (@args) = @_;
370             # OBJECT subcommand [key] [...]
371 1 50       18 return () unless @args >= 2;
372              
373 1         2 my $subcmd = uc($args[0]);
374             # Most OBJECT subcommands take key as second arg
375 1 50       7 if ($subcmd =~ /^(ENCODING|FREQ|IDLETIME|REFCOUNT)$/) {
376 1         5 return (1);
377             }
378 0           return ();
379             }
380              
381             sub _keys_for_blocking_list {
382 0     0     my (@args) = @_;
383             # BLPOP key [key ...] timeout
384             # Last arg is timeout, rest are keys
385 0 0         return () unless @args >= 2;
386 0           return (0 .. $#args - 1);
387             }
388              
389             sub _keys_for_zinter {
390 0     0     my (@args) = @_;
391             # ZINTER numkeys key [key ...] [WEIGHTS ...] [AGGREGATE ...]
392 0 0         return () unless @args >= 1;
393 0           my $numkeys = $args[0];
394 0 0 0       return () unless $numkeys =~ /^\d+$/ && $numkeys > 0;
395 0           return (1 .. $numkeys);
396             }
397              
398             sub _keys_for_zinterstore {
399 0     0     my (@args) = @_;
400             # ZINTERSTORE destination numkeys key [key ...] [WEIGHTS ...] [AGGREGATE ...]
401 0 0         return () unless @args >= 2;
402 0           my $numkeys = $args[1];
403 0 0 0       return () unless $numkeys =~ /^\d+$/ && $numkeys > 0;
404 0           return (0, 2 .. 1 + $numkeys); # dest + source keys
405             }
406              
407             sub _keys_for_sintercard {
408 0     0     my (@args) = @_;
409             # SINTERCARD numkeys key [key ...] [LIMIT limit]
410 0 0         return () unless @args >= 1;
411 0           my $numkeys = $args[0];
412 0 0 0       return () unless $numkeys =~ /^\d+$/ && $numkeys > 0;
413 0           return (1 .. $numkeys);
414             }
415              
416             sub _keys_for_zmpop {
417 0     0     my (@args) = @_;
418             # ZMPOP numkeys key [key ...] MIN|MAX [COUNT count]
419 0 0         return () unless @args >= 1;
420 0           my $numkeys = $args[0];
421 0 0 0       return () unless $numkeys =~ /^\d+$/ && $numkeys > 0;
422 0           return (1 .. $numkeys);
423             }
424              
425             sub _keys_for_bzmpop {
426 0     0     my (@args) = @_;
427             # BZMPOP timeout numkeys key [key ...] MIN|MAX [COUNT count]
428 0 0         return () unless @args >= 2;
429 0           my $numkeys = $args[1];
430 0 0 0       return () unless $numkeys =~ /^\d+$/ && $numkeys > 0;
431 0           return (2 .. 1 + $numkeys);
432             }
433              
434             sub _keys_for_xinfo {
435 0     0     my (@args) = @_;
436             # XINFO STREAM key, XINFO GROUPS key, etc.
437 0 0         return () unless @args >= 2;
438 0           my $subcmd = uc($args[0]);
439 0 0         if ($subcmd =~ /^(STREAM|GROUPS|CONSUMERS)$/) {
440 0           return (1);
441             }
442 0           return ();
443             }
444              
445             sub _keys_for_xgroup {
446 0     0     my (@args) = @_;
447             # XGROUP CREATE key groupname id, XGROUP DESTROY key groupname, etc.
448 0 0         return () unless @args >= 2;
449 0           my $subcmd = uc($args[0]);
450 0 0         if ($subcmd =~ /^(CREATE|DESTROY|SETID|DELCONSUMER|CREATECONSUMER)$/) {
451 0           return (1);
452             }
453 0           return ();
454             }
455              
456             sub _keys_for_georadius {
457 0     0     my (@args) = @_;
458 0           my @indices = (0); # First arg is always key
459              
460             # Look for STORE and STOREDIST
461 0           for my $i (0 .. $#args - 1) {
462 0 0         if (uc($args[$i]) =~ /^(STORE|STOREDIST)$/) {
463 0           push @indices, $i + 1;
464             }
465             }
466              
467 0           return @indices;
468             }
469              
470             1;
471              
472             __END__