1use super::args::Args;
54use super::table::Spec;
55use yo_common::parse_i64;
56
57#[derive(Debug, Clone, Copy)]
59pub struct KeySpec {
60 pub notes: &'static str,
65 pub flags: &'static [&'static str],
67 pub begin: Begin,
69 pub find: Find,
71}
72
73#[derive(Debug, Clone, Copy)]
75pub enum Begin {
76 At(u32),
78 After(&'static [u8], i32),
84 Unknown,
86}
87
88#[derive(Debug, Clone, Copy)]
90pub enum Find {
91 Range {
98 last: i32,
100 step: u32,
102 limit: u32,
104 },
105 Counted {
110 count: u32,
112 first: u32,
114 step: u32,
116 },
117 Unknown,
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
123pub struct Access(u8);
124
125impl Access {
126 pub const READ: Access = Access(1);
128 pub const WRITE: Access = Access(2);
130 pub const BOTH: Access = Access(3);
132 pub const NONE: Access = Access(0);
134
135 #[must_use]
137 pub const fn covers(self, other: Access) -> bool {
138 self.0 & other.0 == other.0
139 }
140
141 #[must_use]
143 pub const fn and(self, other: Access) -> Access {
144 Access(self.0 | other.0)
145 }
146
147 #[must_use]
149 pub const fn is_none(self) -> bool {
150 self.0 == 0
151 }
152
153 #[must_use]
160 pub const fn bits(self) -> u8 {
161 self.0
162 }
163
164 #[must_use]
170 pub const fn from_bits(bits: u8) -> Access {
171 Access(bits & 3)
172 }
173}
174
175impl KeySpec {
176 #[must_use]
178 pub fn access(&self) -> Access {
179 access_of(self.flags)
180 }
181
182 #[must_use]
184 pub fn incomplete(&self) -> bool {
185 self.flags.contains(&"incomplete")
186 || matches!(self.begin, Begin::Unknown)
187 || matches!(self.find, Find::Unknown)
188 }
189
190 #[must_use]
195 pub fn fake(&self) -> bool {
196 self.flags.contains(&"not_key")
197 }
198
199 fn step(&self) -> u32 {
201 match self.find {
202 Find::Range { step, .. } | Find::Counted { step, .. } => step.max(1),
203 Find::Unknown => 1,
204 }
205 }
206}
207
208#[derive(Debug, Clone, Copy)]
210pub struct Run {
211 pub first: usize,
213 pub count: usize,
215 pub step: usize,
217 pub flags: &'static [&'static str],
219}
220
221impl Run {
222 #[must_use]
224 pub fn need(&self) -> Access {
225 access_of(self.flags)
226 }
227}
228
229#[must_use]
236pub fn access_of(flags: &[&str]) -> Access {
237 if flags.contains(&"not_key") {
238 return Access::NONE;
239 }
240 let mut need = Access::NONE;
241 if flags.contains(&"access") {
242 need = need.and(Access::READ);
243 }
244 if flags.contains(&"update") || flags.contains(&"insert") || flags.contains(&"delete") {
245 need = need.and(Access::WRITE);
246 }
247 need
248}
249
250pub fn find(spec: &Spec, args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
266 let keys = keys_of(spec, args, base);
267 let real = keys.iter().any(|k| !k.fake());
268 let varying = keys.iter().any(|k| k.flags.contains(&"variable_flags"));
272 if real && !varying && walk(keys, args, base, each) {
273 return true;
274 }
275 if let Some(finder) = finder_for(spec.name) {
276 return finder(args, base, each);
277 }
278 !real
281}
282
283pub fn takes_keys(spec: &Spec, args: Args<'_>, base: usize) -> bool {
290 finder_for(spec.name).is_some() || keys_of(spec, args, base).iter().any(|k| !k.fake())
291}
292
293fn keys_of(spec: &Spec, args: Args<'_>, base: usize) -> &'static [KeySpec] {
299 if !spec.keys.is_empty() {
300 return spec.keys;
301 }
302 if args.len() <= base + 1 {
303 return &[];
304 }
305 of_sub(spec.name, args.get(base + 1))
306}
307
308fn walk(keys: &[KeySpec], args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
310 let mut runs = [None::<Run>; MOST_SPECS];
313 let mut at = 0;
314 for key in keys {
315 if key.fake() {
317 continue;
318 }
319 match resolve(key, args, base) {
320 Resolved::Run(run) => {
321 if at == runs.len() {
322 return false;
323 }
324 runs[at] = Some(run);
325 at += 1;
326 if key.incomplete() {
329 return false;
330 }
331 }
332 Resolved::None => {}
336 Resolved::Invalid => return false,
337 }
338 }
339 for run in runs.iter().flatten() {
340 if run.count > 0 {
341 each(*run);
342 }
343 }
344 true
345}
346
347const MOST_SPECS: usize = 4;
349
350enum Resolved {
352 Run(Run),
354 None,
356 Invalid,
358}
359
360fn resolve(key: &KeySpec, args: Args<'_>, base: usize) -> Resolved {
369 let argc = (args.len() - base) as i64;
370 let at = |i: i64| args.get(base + i as usize);
371 let mut first = match key.begin {
372 Begin::At(index) => i64::from(index),
373 Begin::After(word, from) => {
374 let start = if from > 0 {
379 i64::from(from)
380 } else {
381 argc + i64::from(from)
382 };
383 let end = if from > 0 { argc - 1 } else { 1 };
384 let mut found = 0;
385 let mut i = start;
386 while i != end {
387 if i >= argc || i < 1 {
388 break;
389 }
390 if at(i).eq_ignore_ascii_case(word) {
391 found = i + 1;
392 break;
393 }
394 i += if start <= end { 1 } else { -1 };
395 }
396 if found == 0 {
397 return Resolved::None;
398 }
399 found
400 }
401 Begin::Unknown => return Resolved::Invalid,
402 };
403 let step = i64::from(key.step());
404 let last = match key.find {
405 Find::Range { last, limit, .. } => {
406 if last >= 0 {
407 first + i64::from(last)
408 } else if limit == 0 {
409 argc + i64::from(last)
410 } else {
411 first + ((argc - first) / i64::from(limit) + i64::from(last))
415 }
416 }
417 Find::Counted {
418 count, first: from, ..
419 } => {
420 let index = first + i64::from(count);
421 if index >= argc || index < 0 {
422 return Resolved::Invalid;
423 }
424 let Some(n) = parse_i64(at(index)).filter(|&n| n >= 0) else {
428 return Resolved::Invalid;
429 };
430 first += i64::from(from);
431 match n
432 .checked_sub(1)
433 .and_then(|n| n.checked_mul(step))
434 .and_then(|n| first.checked_add(n))
435 {
436 Some(last) => last,
437 None => return Resolved::Invalid,
438 }
439 }
440 Find::Unknown => return Resolved::Invalid,
441 };
442 if last >= argc || last < first || first >= argc {
446 return Resolved::Invalid;
447 }
448 Resolved::Run(Run {
449 first: base + first as usize,
450 count: ((last - first) / step + 1) as usize,
451 step: step as usize,
452 flags: key.flags,
453 })
454}
455
456const READ: &[&str] = &["RO", "access"];
466const OVERWRITE: &[&str] = &["OW", "update"];
468const BOTH: &[&str] = &["RW", "access", "update"];
470const MOVED: &[&str] = &["RW", "access", "delete"];
472const MERGED: &[&str] = &["RW", "access", "insert"];
474const COMPARED: &[&str] = &["RW", "delete"];
476const REMOVED: &[&str] = &["RM", "delete"];
478
479type Finder = fn(Args<'_>, usize, &mut dyn FnMut(Run)) -> bool;
482
483fn finder_for(name: &str) -> Option<Finder> {
485 Some(match name {
486 "sort" => sort_keys,
487 "sort_ro" => sort_ro_keys,
488 "migrate" => migrate_keys,
489 "xread" | "xreadgroup" => xread_keys,
490 "georadius" | "georadiusbymember" => georadius_keys,
491 "set" => set_keys,
492 "bitfield" => bitfield_keys,
493 "delex" => delex_keys,
494 "pfmerge" => pfmerge_keys,
495 _ => return None,
496 })
497}
498
499fn one(at: usize, flags: &'static [&'static str], each: &mut dyn FnMut(Run)) {
501 each(Run {
502 first: at,
503 count: 1,
504 step: 1,
505 flags,
506 });
507}
508
509fn sort_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
518 one(base + 1, READ, each);
519 let argc = args.len() - base;
520 let mut store = None;
521 let mut i = 2;
522 while i < argc {
523 let arg = args.get(base + i);
524 if arg.eq_ignore_ascii_case(b"limit") {
525 i += 2;
526 } else if arg.eq_ignore_ascii_case(b"get") || arg.eq_ignore_ascii_case(b"by") {
527 i += 1;
528 } else if arg.eq_ignore_ascii_case(b"store") && i + 1 < argc {
529 store = Some(base + i + 1);
530 }
531 i += 1;
532 }
533 if let Some(at) = store {
534 one(at, OVERWRITE, each);
535 }
536 true
537}
538
539fn sort_ro_keys(_args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
541 one(base + 1, READ, each);
542 true
543}
544
545fn migrate_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
551 let argc = args.len() - base;
552 let mut first = 3;
553 let mut count = 1;
554 if argc > 6 {
555 let mut i = 6;
556 while i < argc {
557 let arg = args.get(base + i);
558 if arg.eq_ignore_ascii_case(b"keys") {
559 if args.get(base + 3).is_empty() {
560 first = i + 1;
561 count = argc - first;
562 } else {
563 count = 0;
564 }
565 break;
566 }
567 if arg.eq_ignore_ascii_case(b"auth") {
568 i += 1;
569 } else if arg.eq_ignore_ascii_case(b"auth2") {
570 i += 2;
571 }
572 i += 1;
573 }
574 }
575 if count > 0 {
576 each(Run {
577 first: base + first,
578 count,
579 step: 1,
580 flags: MOVED,
581 });
582 }
583 true
584}
585
586fn xread_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
592 let argc = args.len() - base;
593 let mut streams = None;
594 let mut i = 1;
595 while i < argc {
596 let arg = args.get(base + i);
597 if arg.eq_ignore_ascii_case(b"block") || arg.eq_ignore_ascii_case(b"count") {
598 i += 1;
599 } else if arg.eq_ignore_ascii_case(b"group") {
600 i += 2;
601 } else if arg.eq_ignore_ascii_case(b"noack") {
602 } else if arg.eq_ignore_ascii_case(b"streams") {
604 streams = Some(i);
605 break;
606 } else {
607 break;
609 }
610 i += 1;
611 }
612 let Some(streams) = streams else {
613 return false;
614 };
615 let tail = argc - streams - 1;
616 if tail == 0 || !tail.is_multiple_of(2) {
617 return false;
618 }
619 each(Run {
620 first: base + streams + 1,
621 count: tail / 2,
622 step: 1,
623 flags: READ,
624 });
625 true
626}
627
628fn georadius_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
633 one(base + 1, READ, each);
634 let argc = args.len() - base;
635 let mut store = None;
636 let mut i = 5;
637 while i < argc {
638 let arg = args.get(base + i);
639 if (arg.eq_ignore_ascii_case(b"store") || arg.eq_ignore_ascii_case(b"storedist"))
640 && i + 1 < argc
641 {
642 store = Some(base + i + 1);
643 i += 1;
644 }
645 i += 1;
646 }
647 if let Some(at) = store {
648 one(at, OVERWRITE, each);
649 }
650 true
651}
652
653fn set_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
659 let gets = (base + 3..args.len()).any(|i| args.get(i).eq_ignore_ascii_case(b"get"));
660 one(base + 1, if gets { BOTH } else { OVERWRITE }, each);
661 true
662}
663
664fn bitfield_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
669 let argc = args.len() - base;
670 let mut reads = true;
671 let mut i = 2;
672 while i < argc {
673 let left = argc - i - 1;
674 let arg = args.get(base + i);
675 if arg.eq_ignore_ascii_case(b"get") && left >= 2 {
676 i += 2;
677 } else if (arg.eq_ignore_ascii_case(b"set") || arg.eq_ignore_ascii_case(b"incrby"))
678 && left >= 3
679 {
680 reads = false;
681 break;
682 } else if arg.eq_ignore_ascii_case(b"overflow") && left >= 1 {
683 i += 1;
684 } else {
685 reads = false;
686 break;
687 }
688 i += 1;
689 }
690 one(base + 1, if reads { READ } else { BOTH }, each);
691 true
692}
693
694fn delex_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
702 let compares = args.opt(base + 2).is_some_and(|a| {
703 a.eq_ignore_ascii_case(b"ifeq")
704 || a.eq_ignore_ascii_case(b"ifne")
705 || a.eq_ignore_ascii_case(b"ifdeq")
706 || a.eq_ignore_ascii_case(b"ifdne")
707 });
708 one(base + 1, if compares { COMPARED } else { REMOVED }, each);
709 true
710}
711
712fn pfmerge_keys(args: Args<'_>, base: usize, each: &mut dyn FnMut(Run)) -> bool {
719 one(base + 1, MERGED, each);
720 let argc = args.len() - base;
721 if argc > 2 {
722 each(Run {
723 first: base + 2,
724 count: argc - 2,
725 step: 1,
726 flags: READ,
727 });
728 }
729 true
730}
731
732pub const NOT_KEY_AT1: KeySpec = KeySpec {
747 notes: "",
748 flags: &["not_key"],
749 begin: Begin::At(1),
750 find: Find::Range {
751 last: 0,
752 step: 1,
753 limit: 0,
754 },
755};
756
757pub const NOT_KEY_AT1_RM1_1_0: KeySpec = KeySpec {
759 notes: "",
760 flags: &["not_key"],
761 begin: Begin::At(1),
762 find: Find::Range {
763 last: -1,
764 step: 1,
765 limit: 0,
766 },
767};
768
769pub const OW_INSERT_AT1: KeySpec = KeySpec {
771 notes: "",
772 flags: &["OW", "insert"],
773 begin: Begin::At(1),
774 find: Find::Range {
775 last: 0,
776 step: 1,
777 limit: 0,
778 },
779};
780
781pub const OW_INSERT_AT1_RM1_2_0: KeySpec = KeySpec {
783 notes: "",
784 flags: &["OW", "insert"],
785 begin: Begin::At(1),
786 find: Find::Range {
787 last: -1,
788 step: 2,
789 limit: 0,
790 },
791};
792
793pub const OW_INSERT_AT2: KeySpec = KeySpec {
795 notes: "",
796 flags: &["OW", "insert"],
797 begin: Begin::At(2),
798 find: Find::Range {
799 last: 0,
800 step: 1,
801 limit: 0,
802 },
803};
804
805pub const OW_UPDATE_AT1: KeySpec = KeySpec {
807 notes: "",
808 flags: &["OW", "update"],
809 begin: Begin::At(1),
810 find: Find::Range {
811 last: 0,
812 step: 1,
813 limit: 0,
814 },
815};
816
817pub const OW_UPDATE_AT1_COUNTED: KeySpec = KeySpec {
819 notes: "",
820 flags: &["OW", "update"],
821 begin: Begin::At(1),
822 find: Find::Counted {
823 count: 0,
824 first: 1,
825 step: 2,
826 },
827};
828
829pub const OW_UPDATE_AT1_RM1_2_0: KeySpec = KeySpec {
831 notes: "",
832 flags: &["OW", "update"],
833 begin: Begin::At(1),
834 find: Find::Range {
835 last: -1,
836 step: 2,
837 limit: 0,
838 },
839};
840
841pub const OW_UPDATE_AT2: KeySpec = KeySpec {
843 notes: "",
844 flags: &["OW", "update"],
845 begin: Begin::At(2),
846 find: Find::Range {
847 last: 0,
848 step: 1,
849 limit: 0,
850 },
851};
852
853pub const GEORADIUS_STORE: KeySpec = KeySpec {
855 notes: "Incomplete because duplicate STORE options use last-wins; fall back to georadiusGetKeys",
856 flags: &["OW", "update", "incomplete"],
857 begin: Begin::After(b"STORE", 6),
858 find: Find::Range {
859 last: 0,
860 step: 1,
861 limit: 0,
862 },
863};
864
865pub const GEORADIUS_STOREDIST: KeySpec = KeySpec {
867 notes: "Incomplete because duplicate STOREDIST options use last-wins; fall back to georadiusGetKeys",
868 flags: &["OW", "update", "incomplete"],
869 begin: Begin::After(b"STOREDIST", 6),
870 find: Find::Range {
871 last: 0,
872 step: 1,
873 limit: 0,
874 },
875};
876
877pub const BYMEMBER_STOREDIST: KeySpec = KeySpec {
879 notes: "Incomplete because duplicate STOREDIST options use last-wins; fall back to georadiusGetKeys",
880 flags: &["OW", "update", "incomplete"],
881 begin: Begin::After(b"STOREDIST", 5),
882 find: Find::Range {
883 last: 0,
884 step: 1,
885 limit: 0,
886 },
887};
888
889pub const BYMEMBER_STORE: KeySpec = KeySpec {
891 notes: "Incomplete because duplicate STORE options use last-wins; fall back to georadiusGetKeys",
892 flags: &["OW", "update", "incomplete"],
893 begin: Begin::After(b"STORE", 5),
894 find: Find::Range {
895 last: 0,
896 step: 1,
897 limit: 0,
898 },
899};
900
901pub const SORT_STORE: KeySpec = KeySpec {
903 notes: "For the optional STORE keyword. It is marked 'unknown' because the keyword can appear anywhere in the argument array",
904 flags: &["OW", "update"],
905 begin: Begin::Unknown,
906 find: Find::Unknown,
907};
908
909pub const RM_DELETE_AT1_RM1_1_0: KeySpec = KeySpec {
911 notes: "",
912 flags: &["RM", "delete"],
913 begin: Begin::At(1),
914 find: Find::Range {
915 last: -1,
916 step: 1,
917 limit: 0,
918 },
919};
920
921pub const RO_ACCESS_AT1: KeySpec = KeySpec {
923 notes: "",
924 flags: &["RO", "access"],
925 begin: Begin::At(1),
926 find: Find::Range {
927 last: 0,
928 step: 1,
929 limit: 0,
930 },
931};
932
933pub const RO_ACCESS_AT1_COUNTED: KeySpec = KeySpec {
935 notes: "",
936 flags: &["RO", "access"],
937 begin: Begin::At(1),
938 find: Find::Counted {
939 count: 0,
940 first: 1,
941 step: 1,
942 },
943};
944
945pub const RO_ACCESS_AT1_R1_1_0: KeySpec = KeySpec {
947 notes: "",
948 flags: &["RO", "access"],
949 begin: Begin::At(1),
950 find: Find::Range {
951 last: 1,
952 step: 1,
953 limit: 0,
954 },
955};
956
957pub const RO_ACCESS_AT1_RM1_1_0: KeySpec = KeySpec {
959 notes: "",
960 flags: &["RO", "access"],
961 begin: Begin::At(1),
962 find: Find::Range {
963 last: -1,
964 step: 1,
965 limit: 0,
966 },
967};
968
969pub const RO_ACCESS_AT1_RM2_1_0: KeySpec = KeySpec {
971 notes: "",
972 flags: &["RO", "access"],
973 begin: Begin::At(1),
974 find: Find::Range {
975 last: -2,
976 step: 1,
977 limit: 0,
978 },
979};
980
981pub const RO_ACCESS_AT2: KeySpec = KeySpec {
983 notes: "",
984 flags: &["RO", "access"],
985 begin: Begin::At(2),
986 find: Find::Range {
987 last: 0,
988 step: 1,
989 limit: 0,
990 },
991};
992
993pub const SCRIPT_KEYS_RO: KeySpec = KeySpec {
995 notes: "We cannot tell how the keys will be used so we assume the worst, RO and ACCESS",
996 flags: &["RO", "access"],
997 begin: Begin::At(2),
998 find: Find::Counted {
999 count: 0,
1000 first: 1,
1001 step: 1,
1002 },
1003};
1004
1005pub const RO_ACCESS_AT2_COUNTED: KeySpec = KeySpec {
1007 notes: "",
1008 flags: &["RO", "access"],
1009 begin: Begin::At(2),
1010 find: Find::Counted {
1011 count: 0,
1012 first: 1,
1013 step: 1,
1014 },
1015};
1016
1017pub const RO_ACCESS_AT2_RM1_1_0: KeySpec = KeySpec {
1019 notes: "",
1020 flags: &["RO", "access"],
1021 begin: Begin::At(2),
1022 find: Find::Range {
1023 last: -1,
1024 step: 1,
1025 limit: 0,
1026 },
1027};
1028
1029pub const RO_ACCESS_AT3_RM1_1_0: KeySpec = KeySpec {
1031 notes: "",
1032 flags: &["RO", "access"],
1033 begin: Begin::At(3),
1034 find: Find::Range {
1035 last: -1,
1036 step: 1,
1037 limit: 0,
1038 },
1039};
1040
1041pub const XREAD_STREAMS: KeySpec = KeySpec {
1043 notes: "Incomplete because a stream key named STREAMS (or options before it) can shift the STREAMS keyword; fall back to xreadGetKeys",
1044 flags: &["RO", "access", "incomplete"],
1045 begin: Begin::After(b"STREAMS", 1),
1046 find: Find::Range {
1047 last: -1,
1048 step: 1,
1049 limit: 2,
1050 },
1051};
1052
1053pub const XREADGROUP_STREAMS: KeySpec = KeySpec {
1055 notes: "Incomplete because a consumer/group named STREAMS (or options before GROUP) can shift the STREAMS keyword; fall back to xreadGetKeys",
1056 flags: &["RO", "access", "incomplete"],
1057 begin: Begin::After(b"STREAMS", 4),
1058 find: Find::Range {
1059 last: -1,
1060 step: 1,
1061 limit: 2,
1062 },
1063};
1064
1065pub const SORT_BY_AND_GET: KeySpec = KeySpec {
1067 notes: "For the optional BY/GET keyword. It is marked 'unknown' because the key names derive from the content of the key we sort",
1068 flags: &["RO", "access"],
1069 begin: Begin::Unknown,
1070 find: Find::Unknown,
1071};
1072
1073pub const RO_AT1: KeySpec = KeySpec {
1075 notes: "",
1076 flags: &["RO"],
1077 begin: Begin::At(1),
1078 find: Find::Range {
1079 last: 0,
1080 step: 1,
1081 limit: 0,
1082 },
1083};
1084
1085pub const RO_AT1_RM1_1_0: KeySpec = KeySpec {
1087 notes: "",
1088 flags: &["RO"],
1089 begin: Begin::At(1),
1090 find: Find::Range {
1091 last: -1,
1092 step: 1,
1093 limit: 0,
1094 },
1095};
1096
1097pub const RO_AT2: KeySpec = KeySpec {
1099 notes: "",
1100 flags: &["RO"],
1101 begin: Begin::At(2),
1102 find: Find::Range {
1103 last: 0,
1104 step: 1,
1105 limit: 0,
1106 },
1107};
1108
1109pub const RW_ACCESS_AT1_RM1_1_0: KeySpec = KeySpec {
1111 notes: "RW because it may change the internal representation of the key, and propagate to replicas",
1112 flags: &["RW", "access"],
1113 begin: Begin::At(1),
1114 find: Find::Range {
1115 last: -1,
1116 step: 1,
1117 limit: 0,
1118 },
1119};
1120
1121pub const RW_ACCESS_AT2: KeySpec = KeySpec {
1123 notes: "",
1124 flags: &["RW", "access"],
1125 begin: Begin::At(2),
1126 find: Find::Range {
1127 last: 0,
1128 step: 1,
1129 limit: 0,
1130 },
1131};
1132
1133pub const RW_ACCESS_DELETE_AT1: KeySpec = KeySpec {
1135 notes: "",
1136 flags: &["RW", "access", "delete"],
1137 begin: Begin::At(1),
1138 find: Find::Range {
1139 last: 0,
1140 step: 1,
1141 limit: 0,
1142 },
1143};
1144
1145pub const RW_ACCESS_DELETE_AT1_COUNTED: KeySpec = KeySpec {
1147 notes: "",
1148 flags: &["RW", "access", "delete"],
1149 begin: Begin::At(1),
1150 find: Find::Counted {
1151 count: 0,
1152 first: 1,
1153 step: 1,
1154 },
1155};
1156
1157pub const RW_ACCESS_DELETE_AT1_RM2_1_0: KeySpec = KeySpec {
1159 notes: "",
1160 flags: &["RW", "access", "delete"],
1161 begin: Begin::At(1),
1162 find: Find::Range {
1163 last: -2,
1164 step: 1,
1165 limit: 0,
1166 },
1167};
1168
1169pub const RW_ACCESS_DELETE_AT2_COUNTED: KeySpec = KeySpec {
1171 notes: "",
1172 flags: &["RW", "access", "delete"],
1173 begin: Begin::At(2),
1174 find: Find::Counted {
1175 count: 0,
1176 first: 1,
1177 step: 1,
1178 },
1179};
1180
1181pub const RW_ACCESS_DELETE_AT3: KeySpec = KeySpec {
1183 notes: "",
1184 flags: &["RW", "access", "delete"],
1185 begin: Begin::At(3),
1186 find: Find::Range {
1187 last: 0,
1188 step: 1,
1189 limit: 0,
1190 },
1191};
1192
1193pub const MIGRATE_KEYS: KeySpec = KeySpec {
1195 notes: "",
1196 flags: &["RW", "access", "delete", "incomplete"],
1197 begin: Begin::After(b"KEYS", -2),
1198 find: Find::Range {
1199 last: -1,
1200 step: 1,
1201 limit: 0,
1202 },
1203};
1204
1205pub const RW_ACCESS_INSERT_AT1: KeySpec = KeySpec {
1207 notes: "",
1208 flags: &["RW", "access", "insert"],
1209 begin: Begin::At(1),
1210 find: Find::Range {
1211 last: 0,
1212 step: 1,
1213 limit: 0,
1214 },
1215};
1216
1217pub const RW_ACCESS_UPDATE_AT1: KeySpec = KeySpec {
1219 notes: "",
1220 flags: &["RW", "access", "update"],
1221 begin: Begin::At(1),
1222 find: Find::Range {
1223 last: 0,
1224 step: 1,
1225 limit: 0,
1226 },
1227};
1228
1229pub const RW_ACCESS_UPDATE_AT1_TTL: KeySpec = KeySpec {
1231 notes: "RW and UPDATE because it changes the TTL",
1232 flags: &["RW", "access", "update"],
1233 begin: Begin::At(1),
1234 find: Find::Range {
1235 last: 0,
1236 step: 1,
1237 limit: 0,
1238 },
1239};
1240
1241pub const RW_ACCESS_UPDATE_AT1_R1_1_0: KeySpec = KeySpec {
1243 notes: "",
1244 flags: &["RW", "access", "update"],
1245 begin: Begin::At(1),
1246 find: Find::Range {
1247 last: 1,
1248 step: 1,
1249 limit: 0,
1250 },
1251};
1252
1253pub const RW_ACCESS_UPDATE_AT1_RM1_3_0: KeySpec = KeySpec {
1255 notes: "",
1256 flags: &["RW", "access", "update"],
1257 begin: Begin::At(1),
1258 find: Find::Range {
1259 last: -1,
1260 step: 3,
1261 limit: 0,
1262 },
1263};
1264
1265pub const RW_ACCESS_UPDATE_AT2: KeySpec = KeySpec {
1267 notes: "",
1268 flags: &["RW", "access", "update"],
1269 begin: Begin::At(2),
1270 find: Find::Range {
1271 last: 0,
1272 step: 1,
1273 limit: 0,
1274 },
1275};
1276
1277pub const SCRIPT_KEYS_RW: KeySpec = KeySpec {
1279 notes: "We cannot tell how the keys will be used so we assume the worst, RW and UPDATE",
1280 flags: &["RW", "access", "update"],
1281 begin: Begin::At(2),
1282 find: Find::Counted {
1283 count: 0,
1284 first: 1,
1285 step: 1,
1286 },
1287};
1288
1289pub const RW_ACCESS_UPDATE_AT2_COUNTED: KeySpec = KeySpec {
1291 notes: "",
1292 flags: &["RW", "access", "update"],
1293 begin: Begin::At(2),
1294 find: Find::Counted {
1295 count: 0,
1296 first: 1,
1297 step: 1,
1298 },
1299};
1300
1301pub const BITFIELD_KEY: KeySpec = KeySpec {
1303 notes: "This command allows both access and modification of the key",
1304 flags: &["RW", "access", "update", "variable_flags"],
1305 begin: Begin::At(1),
1306 find: Find::Range {
1307 last: 0,
1308 step: 1,
1309 limit: 0,
1310 },
1311};
1312
1313pub const SET_KEY: KeySpec = KeySpec {
1315 notes: "RW and ACCESS due to the optional `GET` argument",
1316 flags: &["RW", "access", "update", "variable_flags"],
1317 begin: Begin::At(1),
1318 find: Find::Range {
1319 last: 0,
1320 step: 1,
1321 limit: 0,
1322 },
1323};
1324
1325pub const RW_DELETE_AT1: KeySpec = KeySpec {
1327 notes: "",
1328 flags: &["RW", "delete"],
1329 begin: Begin::At(1),
1330 find: Find::Range {
1331 last: 0,
1332 step: 1,
1333 limit: 0,
1334 },
1335};
1336
1337pub const RW_DELETE_AT2: KeySpec = KeySpec {
1339 notes: "",
1340 flags: &["RW", "delete"],
1341 begin: Begin::At(2),
1342 find: Find::Range {
1343 last: 0,
1344 step: 1,
1345 limit: 0,
1346 },
1347};
1348
1349pub const DELEX_KEY: KeySpec = KeySpec {
1351 notes: "",
1352 flags: &["RW", "delete", "variable_flags"],
1353 begin: Begin::At(1),
1354 find: Find::Range {
1355 last: 0,
1356 step: 1,
1357 limit: 0,
1358 },
1359};
1360
1361pub const RW_INSERT_AT1: KeySpec = KeySpec {
1363 notes: "",
1364 flags: &["RW", "insert"],
1365 begin: Begin::At(1),
1366 find: Find::Range {
1367 last: 0,
1368 step: 1,
1369 limit: 0,
1370 },
1371};
1372
1373pub const RW_INSERT_AT2: KeySpec = KeySpec {
1375 notes: "",
1376 flags: &["RW", "insert"],
1377 begin: Begin::At(2),
1378 find: Find::Range {
1379 last: 0,
1380 step: 1,
1381 limit: 0,
1382 },
1383};
1384
1385pub const RW_UPDATE_AT1: KeySpec = KeySpec {
1387 notes: "",
1388 flags: &["RW", "update"],
1389 begin: Begin::At(1),
1390 find: Find::Range {
1391 last: 0,
1392 step: 1,
1393 limit: 0,
1394 },
1395};
1396
1397pub const RW_UPDATE_AT1_TRIMMING: KeySpec = KeySpec {
1399 notes: "UPDATE instead of INSERT because of the optional trimming feature",
1400 flags: &["RW", "update"],
1401 begin: Begin::At(1),
1402 find: Find::Range {
1403 last: 0,
1404 step: 1,
1405 limit: 0,
1406 },
1407};
1408
1409pub const RW_UPDATE_AT2: KeySpec = KeySpec {
1411 notes: "",
1412 flags: &["RW", "update"],
1413 begin: Begin::At(2),
1414 find: Find::Range {
1415 last: 0,
1416 step: 1,
1417 limit: 0,
1418 },
1419};
1420
1421pub const RW_UPDATE_DELETE_AT1: KeySpec = KeySpec {
1423 notes: "",
1424 flags: &["RW", "update", "delete"],
1425 begin: Begin::At(1),
1426 find: Find::Range {
1427 last: 0,
1428 step: 1,
1429 limit: 0,
1430 },
1431};
1432
1433type SubSpec = (&'static [u8], &'static [u8], &'static [KeySpec]);
1441
1442static SUBS: &[SubSpec] = &[
1444 (b"himport", b"set", &[OW_UPDATE_AT2]),
1445 (b"json.debug", b"memory", &[RO_ACCESS_AT2]),
1449 (b"memory", b"usage", &[RO_AT2]),
1450 (b"object", b"encoding", &[RO_AT2]),
1451 (b"object", b"freq", &[RO_AT2]),
1452 (b"object", b"idletime", &[RO_AT2]),
1453 (b"object", b"refcount", &[RO_AT2]),
1454 (b"xgroup", b"create", &[RW_INSERT_AT2]),
1455 (b"xgroup", b"createconsumer", &[RW_INSERT_AT2]),
1456 (b"xgroup", b"delconsumer", &[RW_DELETE_AT2]),
1457 (b"xgroup", b"destroy", &[RW_DELETE_AT2]),
1458 (b"xgroup", b"setid", &[RW_UPDATE_AT2]),
1459 (b"xinfo", b"consumers", &[RO_ACCESS_AT2]),
1460 (b"xinfo", b"groups", &[RO_ACCESS_AT2]),
1461 (b"xinfo", b"stream", &[RO_ACCESS_AT2]),
1462];
1463
1464#[must_use]
1466pub fn of_sub(container: &str, sub: &[u8]) -> &'static [KeySpec] {
1467 SUBS.iter()
1468 .find(|(c, s, _)| *c == container.as_bytes() && s.eq_ignore_ascii_case(sub))
1469 .map_or(&[][..], |(_, _, keys)| keys)
1470}
1471
1472#[cfg(test)]
1473mod tests {
1474 use super::*;
1475 use crate::dispatch::table;
1476 use crate::proto::Limits;
1477 use crate::request::{Argv, Step};
1478
1479 fn flagged(words: &[&str]) -> Option<Vec<(String, Vec<&'static str>)>> {
1484 let mut buf = format!("*{}\r\n", words.len()).into_bytes();
1485 for word in words {
1486 buf.extend_from_slice(format!("${}\r\n{word}\r\n", word.len()).as_bytes());
1487 }
1488 let mut argv = Argv::new();
1489 let Ok(Step::Command { .. }) = argv.decode(&buf, &Limits::default()) else {
1490 panic!("the test wrote a command that does not decode");
1491 };
1492 let args = Args::new(&argv, &buf);
1493 let spec = table::lookup(args.name()).expect("a command this server has");
1494 let mut found = Vec::new();
1495 let whole = find(spec, args, 0, &mut |run| {
1496 for i in 0..run.count {
1497 let key = args.get(run.first + i * run.step);
1498 found.push((
1499 String::from_utf8_lossy(key).into_owned(),
1500 run.flags.to_vec(),
1501 ));
1502 }
1503 });
1504 whole.then_some(found)
1505 }
1506
1507 fn named(words: &[&str]) -> Option<Vec<String>> {
1509 flagged(words).map(|keys| keys.into_iter().map(|(key, _)| key).collect())
1510 }
1511
1512 #[test]
1513 fn a_counted_run_names_every_key_it_counts() {
1514 assert_eq!(
1515 named(&["zunionstore", "d", "2", "a", "b"]).unwrap(),
1516 ["d", "a", "b"]
1517 );
1518 assert_eq!(
1519 named(&["lmpop", "2", "a", "b", "LEFT"]).unwrap(),
1520 ["a", "b"]
1521 );
1522 assert_eq!(named(&["smove", "a", "b", "m"]).unwrap(), ["a", "b"]);
1523 }
1524
1525 #[test]
1526 fn a_run_that_reaches_past_the_last_argument_names_nothing() {
1527 assert_eq!(named(&["zunionstore", "d", "3", "a"]), None);
1530 assert_eq!(named(&["zunionstore", "d", "-1", "a"]), None);
1531 assert_eq!(named(&["zunionstore", "d", "x", "a"]), None);
1532 assert_eq!(named(&["eval", "body", "0"]), None);
1536 }
1537
1538 #[test]
1539 fn the_streams_keyword_is_walked_to_rather_than_counted_from() {
1540 let two = ["xread", "COUNT", "2", "STREAMS", "a", "b", "0", "0"];
1541 assert_eq!(named(&two).unwrap(), ["a", "b"]);
1542 assert_eq!(named(&["xread", "STREAMS", "a", "b", "0"]), None);
1544 let group = ["xreadgroup", "GROUP", "STREAMS", "c", "STREAMS", "a", ">"];
1546 assert_eq!(named(&group).unwrap(), ["a"]);
1547 }
1548
1549 #[test]
1550 fn pfmerge_names_its_destination_when_it_was_given_no_sources() {
1551 assert_eq!(named(&["pfmerge", "k0"]).unwrap(), ["k0"]);
1554 assert_eq!(named(&["pfmerge", "d", "a", "b"]).unwrap(), ["d", "a", "b"]);
1555 }
1556
1557 #[test]
1558 fn an_argument_that_only_looks_like_a_key_is_not_one() {
1559 let spec = table::lookup(b"spublish").expect("a command this server has");
1560 let mut argv = Argv::new();
1561 let buf = b"*3\r\n$8\r\nspublish\r\n$2\r\nch\r\n$1\r\nm\r\n";
1562 let Ok(Step::Command { .. }) = argv.decode(buf, &Limits::default()) else {
1563 panic!("the test wrote a command that does not decode");
1564 };
1565 assert!(!takes_keys(spec, Args::new(&argv, buf), 0));
1566 }
1567
1568 #[test]
1569 fn a_last_wins_destination_is_the_last_one_written() {
1570 let two = ["sort", "k", "STORE", "a", "STORE", "b"];
1571 assert_eq!(named(&two).unwrap(), ["k", "b"]);
1572 let geo = [
1573 "georadius",
1574 "k",
1575 "0",
1576 "0",
1577 "1",
1578 "m",
1579 "STORE",
1580 "d",
1581 "STOREDIST",
1582 "e",
1583 ];
1584 assert_eq!(named(&geo).unwrap(), ["k", "e"]);
1585 assert_eq!(
1588 named(&["sort", "k", "BY", "STORE", "GET", "d"]).unwrap(),
1589 ["k"]
1590 );
1591 }
1592
1593 #[test]
1594 fn a_keyword_the_command_does_not_carry_is_not_a_failure() {
1595 let single = ["migrate", "h", "1", "k", "0", "0"];
1596 assert_eq!(named(&single).unwrap(), ["k"]);
1597 let listed = ["migrate", "h", "1", "", "0", "0", "KEYS", "a", "b"];
1598 assert_eq!(named(&listed).unwrap(), ["a", "b"]);
1599 assert_eq!(
1603 named(&["migrate", "h", "1", "", "0", "0", "KEYS"]).unwrap(),
1604 [""]
1605 );
1606 }
1607
1608 #[test]
1609 fn what_a_command_does_to_a_key_can_depend_on_its_options() {
1610 let flags = |words: &[&str]| flagged(words).unwrap()[0].1.clone();
1611 assert_eq!(flags(&["set", "k", "v"]), ["OW", "update"]);
1612 assert_eq!(flags(&["set", "k", "v", "GET"]), ["RW", "access", "update"]);
1613 assert_eq!(
1614 flags(&["bitfield", "k", "GET", "u8", "0"]),
1615 ["RO", "access"]
1616 );
1617 assert_eq!(
1618 flags(&["bitfield", "k", "SET", "u8", "0", "1"]),
1619 ["RW", "access", "update"]
1620 );
1621 assert_eq!(flags(&["delex", "k"]), ["RM", "delete"]);
1622 assert_eq!(flags(&["delex", "k", "IFDEQ", "d"]), ["RW", "delete"]);
1623 }
1624
1625 #[test]
1626 fn a_container_is_asked_about_the_word_behind_it() {
1627 assert_eq!(named(&["object", "encoding", "k"]).unwrap(), ["k"]);
1628 assert_eq!(named(&["xgroup", "create", "s", "g", "$"]).unwrap(), ["s"]);
1629 assert_eq!(named(&["object", "help"]).unwrap(), Vec::<String>::new());
1630 }
1631}