1use std::path::PathBuf;
6
7use clap::{Parser, Subcommand, ValueEnum};
8use clap_complete::Shell;
9
10fn parse_shell(value: &str) -> Result<Shell, String> {
11 match value {
12 "bash" => Ok(Shell::Bash),
13 "zsh" => Ok(Shell::Zsh),
14 "fish" => Ok(Shell::Fish),
15 "pwsh" | "powershell" => Ok(Shell::PowerShell),
16 _ => Err(format!(
17 "Unsupported shell: {}. Use bash, zsh, fish, or pwsh.",
18 value
19 )),
20 }
21}
22
23#[derive(Parser, Debug)]
25#[command(name = "alopex")]
26#[command(version, about, long_about = None)]
27pub struct Cli {
28 #[arg(long)]
30 pub data_dir: Option<String>,
31
32 #[arg(long)]
34 pub profile: Option<String>,
35
36 #[arg(long, conflicts_with = "data_dir")]
38 pub in_memory: bool,
39
40 #[arg(long, value_enum)]
42 pub output: Option<OutputFormat>,
43
44 #[arg(long)]
46 pub limit: Option<usize>,
47
48 #[arg(long)]
50 pub quiet: bool,
51
52 #[arg(long)]
54 pub verbose: bool,
55
56 #[arg(long)]
58 pub insecure: bool,
59
60 #[arg(long, value_enum, default_value = "multi")]
62 pub thread_mode: ThreadMode,
63
64 #[arg(long, short = 'b')]
66 pub batch: bool,
67
68 #[arg(long)]
70 pub yes: bool,
71
72 #[command(subcommand)]
74 pub command: Option<Command>,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
79pub enum OutputFormat {
80 Table,
82 Json,
84 Jsonl,
86 Csv,
88 Tsv,
90}
91
92impl OutputFormat {
93 #[allow(dead_code)]
95 pub fn supports_streaming(&self) -> bool {
96 matches!(self, Self::Json | Self::Jsonl | Self::Csv | Self::Tsv)
97 }
98}
99
100impl Cli {
101 pub fn output_format(&self) -> OutputFormat {
102 self.output.unwrap_or(OutputFormat::Table)
103 }
104
105 pub fn output_is_explicit(&self) -> bool {
106 self.output.is_some()
107 }
108}
109
110#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
112pub enum ThreadMode {
113 Multi,
115 Single,
117}
118
119#[derive(Subcommand, Debug)]
121pub enum Command {
122 Profile {
124 #[command(subcommand)]
125 command: Option<ProfileCommand>,
126 },
127 Kv {
129 #[command(subcommand)]
130 command: Option<KvCommand>,
131 },
132 Sql(SqlCommand),
134 Vector {
136 #[command(subcommand)]
137 command: Option<VectorCommand>,
138 },
139 Hnsw {
141 #[command(subcommand)]
142 command: Option<HnswCommand>,
143 },
144 Columnar {
146 #[command(subcommand)]
147 command: Option<ColumnarCommand>,
148 },
149 Server {
151 #[command(subcommand)]
152 command: Option<ServerCommand>,
153 },
154 Lifecycle {
156 #[command(subcommand)]
157 command: Option<LifecycleCommand>,
158 },
159 Version,
161 Completions {
163 #[arg(value_parser = parse_shell, value_name = "SHELL")]
165 shell: Shell,
166 },
167}
168
169#[derive(Subcommand, Debug, Clone)]
171pub enum ProfileCommand {
172 Create {
174 name: String,
176 #[arg(long)]
178 data_dir: String,
179 },
180 List,
182 Show {
184 name: String,
186 },
187 Delete {
189 name: String,
191 },
192 SetDefault {
194 name: String,
196 },
197}
198
199#[derive(Subcommand, Debug)]
201pub enum KvCommand {
202 Get {
204 key: String,
206 },
207 Put {
209 key: String,
211 value: String,
213 },
214 Delete {
216 key: String,
218 },
219 List {
221 #[arg(long)]
223 prefix: Option<String>,
224 },
225 #[command(subcommand)]
227 Txn(KvTxnCommand),
228}
229
230#[derive(Subcommand, Debug)]
232pub enum KvTxnCommand {
233 Begin {
235 #[arg(long)]
237 timeout_secs: Option<u64>,
238 },
239 Get {
241 key: String,
243 #[arg(long)]
245 txn_id: String,
246 },
247 Put {
249 key: String,
251 value: String,
253 #[arg(long)]
255 txn_id: String,
256 },
257 Delete {
259 key: String,
261 #[arg(long)]
263 txn_id: String,
264 },
265 Commit {
267 #[arg(long)]
269 txn_id: String,
270 },
271 Rollback {
273 #[arg(long)]
275 txn_id: String,
276 },
277}
278
279#[derive(Parser, Debug)]
281pub struct SqlCommand {
282 #[arg(conflicts_with = "file")]
284 pub query: Option<String>,
285
286 #[arg(long, short = 'f')]
288 pub file: Option<String>,
289
290 #[arg(long)]
292 pub fetch_size: Option<usize>,
293
294 #[arg(long)]
296 pub max_rows: Option<usize>,
297
298 #[arg(long)]
300 pub deadline: Option<String>,
301
302 #[arg(long)]
304 pub tui: bool,
305}
306
307#[derive(Subcommand, Debug)]
309pub enum VectorCommand {
310 Search {
312 #[arg(long)]
314 index: String,
315 #[arg(long)]
317 query: String,
318 #[arg(long, short = 'k', default_value = "10")]
320 k: usize,
321 #[arg(long)]
323 progress: bool,
324 },
325 Upsert {
327 #[arg(long)]
329 index: String,
330 #[arg(long)]
332 key: String,
333 #[arg(long)]
335 vector: String,
336 },
337 Delete {
339 #[arg(long)]
341 index: String,
342 #[arg(long)]
344 key: String,
345 },
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
350pub enum DistanceMetric {
351 #[default]
353 Cosine,
354 L2,
356 Ip,
358}
359
360#[derive(Subcommand, Debug)]
362pub enum HnswCommand {
363 Create {
365 name: String,
367 #[arg(long)]
369 dim: usize,
370 #[arg(long, value_enum, default_value = "cosine")]
372 metric: DistanceMetric,
373 },
374 Stats {
376 name: String,
378 },
379 Drop {
381 name: String,
383 },
384}
385
386#[derive(Subcommand, Debug)]
388pub enum ColumnarCommand {
389 Scan {
391 #[arg(long)]
393 segment: String,
394 #[arg(long)]
396 progress: bool,
397 },
398 Stats {
400 #[arg(long)]
402 segment: String,
403 },
404 List,
406 Ingest {
408 #[arg(long)]
410 file: PathBuf,
411 #[arg(long)]
413 table: String,
414 #[arg(long, default_value = ",", value_parser = clap::value_parser!(char))]
416 delimiter: char,
417 #[arg(
419 long,
420 default_value = "true",
421 value_parser = clap::value_parser!(bool),
422 action = clap::ArgAction::Set
423 )]
424 header: bool,
425 #[arg(long, default_value = "zstd")]
427 compression: String,
428 #[arg(long)]
430 row_group_size: Option<usize>,
431 },
432 #[command(subcommand)]
434 Index(IndexCommand),
435}
436
437#[derive(Subcommand, Debug)]
439pub enum IndexCommand {
440 Create {
442 #[arg(long)]
444 segment: String,
445 #[arg(long)]
447 column: String,
448 #[arg(long = "type")]
450 index_type: String,
451 },
452 List {
454 #[arg(long)]
456 segment: String,
457 },
458 Drop {
460 #[arg(long)]
462 segment: String,
463 #[arg(long)]
465 column: String,
466 },
467}
468
469#[derive(Subcommand, Debug)]
471pub enum ServerCommand {
472 Status,
474 Metrics,
476 Health,
478 Compaction {
480 #[command(subcommand)]
481 command: CompactionCommand,
482 },
483}
484
485#[derive(Subcommand, Debug)]
487pub enum LifecycleCommand {
488 Archive,
490 Restore {
492 #[arg(long)]
494 source: Option<String>,
495 #[command(subcommand)]
497 command: Option<LifecycleRestoreCommand>,
498 },
499 Backup {
501 #[command(subcommand)]
503 command: Option<LifecycleBackupCommand>,
504 },
505 Export,
507}
508
509#[derive(Subcommand, Debug)]
511pub enum LifecycleBackupCommand {
512 Status {
514 #[arg(long)]
516 handle: String,
517 },
518}
519
520#[derive(Subcommand, Debug)]
522pub enum LifecycleRestoreCommand {
523 Status {
525 #[arg(long)]
527 handle: String,
528 },
529}
530
531#[derive(Subcommand, Debug)]
533pub enum CompactionCommand {
534 Trigger,
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 #[test]
543 fn test_parse_in_memory_kv_get() {
544 let args = vec!["alopex", "--in-memory", "kv", "get", "mykey"];
545 let cli = Cli::try_parse_from(args).unwrap();
546
547 assert!(cli.in_memory);
548 assert!(cli.data_dir.is_none());
549 assert_eq!(cli.output_format(), OutputFormat::Table);
550 assert!(matches!(
551 cli.command,
552 Some(Command::Kv {
553 command: Some(KvCommand::Get { key })
554 }) if key == "mykey"
555 ));
556 }
557
558 #[test]
559 fn test_parse_data_dir_sql() {
560 let args = vec![
561 "alopex",
562 "--data-dir",
563 "/path/to/db",
564 "sql",
565 "SELECT * FROM users",
566 ];
567 let cli = Cli::try_parse_from(args).unwrap();
568
569 assert!(!cli.in_memory);
570 assert_eq!(cli.data_dir, Some("/path/to/db".to_string()));
571 assert!(matches!(
572 cli.command,
573 Some(Command::Sql(SqlCommand { query: Some(q), file: None, .. })) if q == "SELECT * FROM users"
574 ));
575 }
576
577 #[test]
578 fn test_parse_output_format() {
579 let args = vec!["alopex", "--in-memory", "--output", "jsonl", "kv", "list"];
580 let cli = Cli::try_parse_from(args).unwrap();
581
582 assert_eq!(cli.output_format(), OutputFormat::Jsonl);
583 assert!(cli.output_is_explicit());
584 }
585
586 #[test]
587 fn test_parse_limit() {
588 let args = vec!["alopex", "--in-memory", "--limit", "100", "kv", "list"];
589 let cli = Cli::try_parse_from(args).unwrap();
590
591 assert_eq!(cli.limit, Some(100));
592 }
593
594 #[test]
595 fn test_parse_sql_streaming_options() {
596 let args = vec![
597 "alopex",
598 "sql",
599 "--fetch-size",
600 "500",
601 "--max-rows",
602 "250",
603 "--deadline",
604 "30s",
605 "SELECT 1",
606 ];
607 let cli = Cli::try_parse_from(args).unwrap();
608
609 match cli.command {
610 Some(Command::Sql(cmd)) => {
611 assert_eq!(cmd.fetch_size, Some(500));
612 assert_eq!(cmd.max_rows, Some(250));
613 assert_eq!(cmd.deadline.as_deref(), Some("30s"));
614 assert!(!cmd.tui);
615 }
616 _ => panic!("expected sql command"),
617 }
618 }
619
620 #[test]
621 fn test_parse_sql_tui_flag() {
622 let args = vec!["alopex", "sql", "--tui", "SELECT 1"];
623 let cli = Cli::try_parse_from(args).unwrap();
624
625 match cli.command {
626 Some(Command::Sql(cmd)) => {
627 assert!(cmd.tui);
628 assert_eq!(cmd.query.as_deref(), Some("SELECT 1"));
629 }
630 _ => panic!("expected sql command"),
631 }
632 }
633
634 #[test]
635 fn test_parse_server_status() {
636 let args = vec!["alopex", "server", "status"];
637 let cli = Cli::try_parse_from(args).unwrap();
638
639 assert!(matches!(
640 cli.command,
641 Some(Command::Server {
642 command: Some(ServerCommand::Status)
643 })
644 ));
645 }
646
647 #[test]
648 fn test_parse_server_compaction_trigger() {
649 let args = vec!["alopex", "server", "compaction", "trigger"];
650 let cli = Cli::try_parse_from(args).unwrap();
651
652 assert!(matches!(
653 cli.command,
654 Some(Command::Server {
655 command: Some(ServerCommand::Compaction {
656 command: CompactionCommand::Trigger
657 })
658 })
659 ));
660 }
661
662 #[test]
663 fn test_parse_verbose_quiet() {
664 let args = vec!["alopex", "--in-memory", "--verbose", "kv", "list"];
665 let cli = Cli::try_parse_from(args).unwrap();
666
667 assert!(cli.verbose);
668 assert!(!cli.quiet);
669 }
670
671 #[test]
672 fn test_parse_thread_mode() {
673 let args = vec![
674 "alopex",
675 "--in-memory",
676 "--thread-mode",
677 "single",
678 "kv",
679 "list",
680 ];
681 let cli = Cli::try_parse_from(args).unwrap();
682
683 assert_eq!(cli.thread_mode, ThreadMode::Single);
684 }
685
686 #[test]
687 fn test_parse_profile_option_batch_yes() {
688 let args = vec![
689 "alopex",
690 "--profile",
691 "dev",
692 "--batch",
693 "--yes",
694 "--in-memory",
695 "kv",
696 "list",
697 ];
698 let cli = Cli::try_parse_from(args).unwrap();
699
700 assert_eq!(cli.profile.as_deref(), Some("dev"));
701 assert!(cli.batch);
702 assert!(cli.yes);
703 }
704
705 #[test]
706 fn test_parse_batch_short_flag() {
707 let args = vec!["alopex", "-b", "--in-memory", "kv", "list"];
708 let cli = Cli::try_parse_from(args).unwrap();
709
710 assert!(cli.batch);
711 }
712
713 #[test]
714 fn test_parse_profile_create_subcommand() {
715 let args = vec![
716 "alopex",
717 "profile",
718 "create",
719 "dev",
720 "--data-dir",
721 "/path/to/db",
722 ];
723 let cli = Cli::try_parse_from(args).unwrap();
724
725 assert!(matches!(
726 cli.command,
727 Some(Command::Profile {
728 command: Some(ProfileCommand::Create { name, data_dir })
729 })
730 if name == "dev" && data_dir == "/path/to/db"
731 ));
732 }
733
734 #[test]
735 fn test_parse_completions_bash() {
736 let args = vec!["alopex", "completions", "bash"];
737 let cli = Cli::try_parse_from(args).unwrap();
738
739 assert!(matches!(
740 cli.command,
741 Some(Command::Completions { shell }) if shell == Shell::Bash
742 ));
743 }
744
745 #[test]
746 fn test_parse_completions_pwsh() {
747 let args = vec!["alopex", "completions", "pwsh"];
748 let cli = Cli::try_parse_from(args).unwrap();
749
750 assert!(matches!(
751 cli.command,
752 Some(Command::Completions { shell }) if shell == Shell::PowerShell
753 ));
754 }
755
756 #[test]
757 fn test_parse_kv_put() {
758 let args = vec!["alopex", "--in-memory", "kv", "put", "mykey", "myvalue"];
759 let cli = Cli::try_parse_from(args).unwrap();
760
761 assert!(matches!(
762 cli.command,
763 Some(Command::Kv {
764 command: Some(KvCommand::Put { key, value })
765 }) if key == "mykey" && value == "myvalue"
766 ));
767 }
768
769 #[test]
770 fn test_parse_kv_delete() {
771 let args = vec!["alopex", "--in-memory", "kv", "delete", "mykey"];
772 let cli = Cli::try_parse_from(args).unwrap();
773
774 assert!(matches!(
775 cli.command,
776 Some(Command::Kv {
777 command: Some(KvCommand::Delete { key })
778 }) if key == "mykey"
779 ));
780 }
781
782 #[test]
783 fn test_parse_kv_txn_begin() {
784 let args = vec!["alopex", "kv", "txn", "begin", "--timeout-secs", "30"];
785 let cli = Cli::try_parse_from(args).unwrap();
786
787 assert!(matches!(
788 cli.command,
789 Some(Command::Kv {
790 command: Some(KvCommand::Txn(KvTxnCommand::Begin {
791 timeout_secs: Some(30)
792 }))
793 })
794 ));
795 }
796
797 #[test]
798 fn test_parse_kv_txn_get_requires_txn_id() {
799 let args = vec!["alopex", "kv", "txn", "get", "mykey"];
800
801 assert!(Cli::try_parse_from(args).is_err());
802 }
803
804 #[test]
805 fn test_parse_kv_txn_get() {
806 let args = vec!["alopex", "kv", "txn", "get", "mykey", "--txn-id", "txn123"];
807 let cli = Cli::try_parse_from(args).unwrap();
808
809 assert!(matches!(
810 cli.command,
811 Some(Command::Kv {
812 command: Some(KvCommand::Txn(KvTxnCommand::Get { key, txn_id }))
813 }) if key == "mykey" && txn_id == "txn123"
814 ));
815 }
816
817 #[test]
818 fn test_parse_kv_list_with_prefix() {
819 let args = vec!["alopex", "--in-memory", "kv", "list", "--prefix", "user:"];
820 let cli = Cli::try_parse_from(args).unwrap();
821
822 assert!(matches!(
823 cli.command,
824 Some(Command::Kv {
825 command: Some(KvCommand::List { prefix: Some(p) })
826 }) if p == "user:"
827 ));
828 }
829
830 #[test]
831 fn test_parse_sql_from_file() {
832 let args = vec!["alopex", "--in-memory", "sql", "-f", "query.sql"];
833 let cli = Cli::try_parse_from(args).unwrap();
834
835 assert!(matches!(
836 cli.command,
837 Some(Command::Sql(SqlCommand { query: None, file: Some(f), .. })) if f == "query.sql"
838 ));
839 }
840
841 #[test]
842 fn test_parse_vector_search() {
843 let args = vec![
844 "alopex",
845 "--in-memory",
846 "vector",
847 "search",
848 "--index",
849 "my_index",
850 "--query",
851 "[1.0,2.0,3.0]",
852 "-k",
853 "5",
854 ];
855 let cli = Cli::try_parse_from(args).unwrap();
856
857 assert!(matches!(
858 cli.command,
859 Some(Command::Vector {
860 command: Some(VectorCommand::Search { index, query, k, progress })
861 }) if index == "my_index" && query == "[1.0,2.0,3.0]" && k == 5 && !progress
862 ));
863 }
864
865 #[test]
866 fn test_parse_vector_upsert() {
867 let args = vec![
868 "alopex",
869 "--in-memory",
870 "vector",
871 "upsert",
872 "--index",
873 "my_index",
874 "--key",
875 "vec1",
876 "--vector",
877 "[1.0,2.0,3.0]",
878 ];
879 let cli = Cli::try_parse_from(args).unwrap();
880
881 assert!(matches!(
882 cli.command,
883 Some(Command::Vector {
884 command: Some(VectorCommand::Upsert { index, key, vector })
885 }) if index == "my_index" && key == "vec1" && vector == "[1.0,2.0,3.0]"
886 ));
887 }
888
889 #[test]
890 fn test_parse_vector_delete() {
891 let args = vec![
892 "alopex",
893 "--in-memory",
894 "vector",
895 "delete",
896 "--index",
897 "my_index",
898 "--key",
899 "vec1",
900 ];
901 let cli = Cli::try_parse_from(args).unwrap();
902
903 assert!(matches!(
904 cli.command,
905 Some(Command::Vector {
906 command: Some(VectorCommand::Delete { index, key })
907 }) if index == "my_index" && key == "vec1"
908 ));
909 }
910
911 #[test]
912 fn test_parse_hnsw_create() {
913 let args = vec![
914 "alopex",
915 "--in-memory",
916 "hnsw",
917 "create",
918 "my_index",
919 "--dim",
920 "128",
921 "--metric",
922 "l2",
923 ];
924 let cli = Cli::try_parse_from(args).unwrap();
925
926 assert!(matches!(
927 cli.command,
928 Some(Command::Hnsw {
929 command: Some(HnswCommand::Create { name, dim, metric })
930 }) if name == "my_index" && dim == 128 && metric == DistanceMetric::L2
931 ));
932 }
933
934 #[test]
935 fn test_parse_hnsw_create_default_metric() {
936 let args = vec![
937 "alopex",
938 "--in-memory",
939 "hnsw",
940 "create",
941 "my_index",
942 "--dim",
943 "128",
944 ];
945 let cli = Cli::try_parse_from(args).unwrap();
946
947 assert!(matches!(
948 cli.command,
949 Some(Command::Hnsw {
950 command: Some(HnswCommand::Create { name, dim, metric })
951 }) if name == "my_index" && dim == 128 && metric == DistanceMetric::Cosine
952 ));
953 }
954
955 #[test]
956 fn test_parse_columnar_scan() {
957 let args = vec![
958 "alopex",
959 "--in-memory",
960 "columnar",
961 "scan",
962 "--segment",
963 "seg_001",
964 ];
965 let cli = Cli::try_parse_from(args).unwrap();
966
967 assert!(matches!(
968 cli.command,
969 Some(Command::Columnar {
970 command: Some(ColumnarCommand::Scan { segment, progress })
971 }) if segment == "seg_001" && !progress
972 ));
973 }
974
975 #[test]
976 fn test_parse_columnar_stats() {
977 let args = vec![
978 "alopex",
979 "--in-memory",
980 "columnar",
981 "stats",
982 "--segment",
983 "seg_001",
984 ];
985 let cli = Cli::try_parse_from(args).unwrap();
986
987 assert!(matches!(
988 cli.command,
989 Some(Command::Columnar {
990 command: Some(ColumnarCommand::Stats { segment })
991 }) if segment == "seg_001"
992 ));
993 }
994
995 #[test]
996 fn test_parse_columnar_list() {
997 let args = vec!["alopex", "--in-memory", "columnar", "list"];
998 let cli = Cli::try_parse_from(args).unwrap();
999
1000 assert!(matches!(
1001 cli.command,
1002 Some(Command::Columnar {
1003 command: Some(ColumnarCommand::List)
1004 })
1005 ));
1006 }
1007
1008 #[test]
1009 fn test_parse_columnar_ingest_defaults() {
1010 let args = vec![
1011 "alopex",
1012 "--in-memory",
1013 "columnar",
1014 "ingest",
1015 "--file",
1016 "data.csv",
1017 "--table",
1018 "events",
1019 ];
1020 let cli = Cli::try_parse_from(args).unwrap();
1021
1022 assert!(matches!(
1023 cli.command,
1024 Some(Command::Columnar {
1025 command: Some(ColumnarCommand::Ingest {
1026 file,
1027 table,
1028 delimiter,
1029 header,
1030 compression,
1031 row_group_size,
1032 })
1033 }) if file == std::path::Path::new("data.csv")
1034 && table == "events"
1035 && delimiter == ','
1036 && header
1037 && compression == "zstd"
1038 && row_group_size.is_none()
1039 ));
1040 }
1041
1042 #[test]
1043 fn test_parse_columnar_ingest_custom_options() {
1044 let args = vec![
1045 "alopex",
1046 "--in-memory",
1047 "columnar",
1048 "ingest",
1049 "--file",
1050 "data.csv",
1051 "--table",
1052 "events",
1053 "--delimiter",
1054 ";",
1055 "--header",
1056 "false",
1057 "--compression",
1058 "zstd",
1059 "--row-group-size",
1060 "500",
1061 ];
1062 let cli = Cli::try_parse_from(args).unwrap();
1063
1064 assert!(matches!(
1065 cli.command,
1066 Some(Command::Columnar {
1067 command: Some(ColumnarCommand::Ingest {
1068 file,
1069 table,
1070 delimiter,
1071 header,
1072 compression,
1073 row_group_size,
1074 })
1075 }) if file == std::path::Path::new("data.csv")
1076 && table == "events"
1077 && delimiter == ';'
1078 && !header
1079 && compression == "zstd"
1080 && row_group_size == Some(500)
1081 ));
1082 }
1083
1084 #[test]
1085 fn test_parse_columnar_index_create() {
1086 let args = vec![
1087 "alopex",
1088 "--in-memory",
1089 "columnar",
1090 "index",
1091 "create",
1092 "--segment",
1093 "123:1",
1094 "--column",
1095 "col1",
1096 "--type",
1097 "bloom",
1098 ];
1099 let cli = Cli::try_parse_from(args).unwrap();
1100
1101 assert!(matches!(
1102 cli.command,
1103 Some(Command::Columnar {
1104 command: Some(ColumnarCommand::Index(IndexCommand::Create {
1105 segment,
1106 column,
1107 index_type,
1108 }))
1109 }) if segment == "123:1"
1110 && column == "col1"
1111 && index_type == "bloom"
1112 ));
1113 }
1114
1115 #[test]
1116 fn test_output_format_supports_streaming() {
1117 assert!(!OutputFormat::Table.supports_streaming());
1118 assert!(OutputFormat::Json.supports_streaming());
1119 assert!(OutputFormat::Jsonl.supports_streaming());
1120 assert!(OutputFormat::Csv.supports_streaming());
1121 assert!(OutputFormat::Tsv.supports_streaming());
1122 }
1123
1124 #[test]
1125 fn test_default_values() {
1126 let args = vec!["alopex", "--in-memory", "kv", "list"];
1127 let cli = Cli::try_parse_from(args).unwrap();
1128
1129 assert_eq!(cli.output_format(), OutputFormat::Table);
1130 assert!(!cli.output_is_explicit());
1131 assert_eq!(cli.thread_mode, ThreadMode::Multi);
1132 assert!(cli.limit.is_none());
1133 assert!(!cli.quiet);
1134 assert!(!cli.verbose);
1135 }
1136
1137 #[test]
1138 fn test_s3_data_dir() {
1139 let args = vec![
1140 "alopex",
1141 "--data-dir",
1142 "s3://my-bucket/prefix",
1143 "kv",
1144 "list",
1145 ];
1146 let cli = Cli::try_parse_from(args).unwrap();
1147
1148 assert_eq!(cli.data_dir, Some("s3://my-bucket/prefix".to_string()));
1149 }
1150}