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)]
287pub struct SqlCommand {
288 #[arg(conflicts_with = "file")]
290 pub query: Option<String>,
291
292 #[arg(long, short = 'f')]
294 pub file: Option<String>,
295
296 #[arg(long)]
298 pub fetch_size: Option<usize>,
299
300 #[arg(long)]
302 pub max_rows: Option<usize>,
303
304 #[arg(long)]
306 pub deadline: Option<String>,
307
308 #[arg(long)]
310 pub tui: bool,
311}
312
313#[derive(Subcommand, Debug)]
315pub enum VectorCommand {
316 Search {
318 #[arg(long)]
320 index: String,
321 #[arg(long)]
323 query: String,
324 #[arg(long, short = 'k', default_value = "10")]
326 k: usize,
327 #[arg(long)]
329 progress: bool,
330 },
331 Upsert {
333 #[arg(long)]
335 index: String,
336 #[arg(long)]
338 key: String,
339 #[arg(long)]
341 vector: String,
342 },
343 Delete {
345 #[arg(long)]
347 index: String,
348 #[arg(long)]
350 key: String,
351 },
352}
353
354#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum, Default)]
356pub enum DistanceMetric {
357 #[default]
359 Cosine,
360 L2,
362 Ip,
364}
365
366#[derive(Subcommand, Debug)]
368pub enum HnswCommand {
369 Create {
371 name: String,
373 #[arg(long)]
375 dim: usize,
376 #[arg(long, value_enum, default_value = "cosine")]
378 metric: DistanceMetric,
379 },
380 Stats {
382 name: String,
384 },
385 Drop {
387 name: String,
389 },
390}
391
392#[derive(Subcommand, Debug)]
394pub enum ColumnarCommand {
395 Scan {
397 #[arg(long)]
399 segment: String,
400 #[arg(long)]
402 progress: bool,
403 },
404 Stats {
406 #[arg(long)]
408 segment: String,
409 },
410 List,
412 Ingest {
414 #[arg(long)]
416 file: PathBuf,
417 #[arg(long)]
419 table: String,
420 #[arg(long, default_value = ",", value_parser = clap::value_parser!(char))]
422 delimiter: char,
423 #[arg(
425 long,
426 default_value = "true",
427 value_parser = clap::value_parser!(bool),
428 action = clap::ArgAction::Set
429 )]
430 header: bool,
431 #[arg(long, default_value = "zstd")]
433 compression: String,
434 #[arg(long)]
436 row_group_size: Option<usize>,
437 },
438 #[command(subcommand)]
440 Index(IndexCommand),
441}
442
443#[derive(Subcommand, Debug)]
445pub enum IndexCommand {
446 Create {
448 #[arg(long)]
450 segment: String,
451 #[arg(long)]
453 column: String,
454 #[arg(long = "type")]
456 index_type: String,
457 },
458 List {
460 #[arg(long)]
462 segment: String,
463 },
464 Drop {
466 #[arg(long)]
468 segment: String,
469 #[arg(long)]
471 column: String,
472 },
473}
474
475#[derive(Subcommand, Debug)]
477pub enum ServerCommand {
478 Status,
480 Metrics,
482 Health,
484 Join,
486 Leave,
488 Compaction {
490 #[command(subcommand)]
491 command: CompactionCommand,
492 },
493}
494
495#[derive(Subcommand, Debug)]
497pub enum LifecycleCommand {
498 Archive,
500 Restore {
502 #[arg(long)]
504 source: Option<String>,
505 #[command(subcommand)]
507 command: Option<LifecycleRestoreCommand>,
508 },
509 Backup {
511 #[command(subcommand)]
513 command: Option<LifecycleBackupCommand>,
514 },
515 Export,
517}
518
519#[derive(Subcommand, Debug)]
521pub enum LifecycleBackupCommand {
522 Status {
524 #[arg(long)]
526 handle: String,
527 },
528}
529
530#[derive(Subcommand, Debug)]
532pub enum LifecycleRestoreCommand {
533 Status {
535 #[arg(long)]
537 handle: String,
538 },
539}
540
541#[derive(Subcommand, Debug)]
543pub enum CompactionCommand {
544 Trigger,
546}
547
548#[cfg(test)]
549mod tests {
550 use super::*;
551
552 #[test]
553 fn test_parse_in_memory_kv_get() {
554 let args = vec!["alopex", "--in-memory", "kv", "get", "mykey"];
555 let cli = Cli::try_parse_from(args).unwrap();
556
557 assert!(cli.in_memory);
558 assert!(cli.data_dir.is_none());
559 assert_eq!(cli.output_format(), OutputFormat::Table);
560 assert!(matches!(
561 cli.command,
562 Some(Command::Kv {
563 command: Some(KvCommand::Get { key })
564 }) if key == "mykey"
565 ));
566 }
567
568 #[test]
569 fn test_parse_data_dir_sql() {
570 let args = vec![
571 "alopex",
572 "--data-dir",
573 "/path/to/db",
574 "sql",
575 "SELECT * FROM users",
576 ];
577 let cli = Cli::try_parse_from(args).unwrap();
578
579 assert!(!cli.in_memory);
580 assert_eq!(cli.data_dir, Some("/path/to/db".to_string()));
581 assert!(matches!(
582 cli.command,
583 Some(Command::Sql(SqlCommand { query: Some(q), file: None, .. })) if q == "SELECT * FROM users"
584 ));
585 }
586
587 #[test]
588 fn test_parse_output_format() {
589 let args = vec!["alopex", "--in-memory", "--output", "jsonl", "kv", "list"];
590 let cli = Cli::try_parse_from(args).unwrap();
591
592 assert_eq!(cli.output_format(), OutputFormat::Jsonl);
593 assert!(cli.output_is_explicit());
594 }
595
596 #[test]
597 fn test_parse_limit() {
598 let args = vec!["alopex", "--in-memory", "--limit", "100", "kv", "list"];
599 let cli = Cli::try_parse_from(args).unwrap();
600
601 assert_eq!(cli.limit, Some(100));
602 }
603
604 #[test]
605 fn test_parse_sql_streaming_options() {
606 let args = vec![
607 "alopex",
608 "sql",
609 "--fetch-size",
610 "500",
611 "--max-rows",
612 "250",
613 "--deadline",
614 "30s",
615 "SELECT 1",
616 ];
617 let cli = Cli::try_parse_from(args).unwrap();
618
619 match cli.command {
620 Some(Command::Sql(cmd)) => {
621 assert_eq!(cmd.fetch_size, Some(500));
622 assert_eq!(cmd.max_rows, Some(250));
623 assert_eq!(cmd.deadline.as_deref(), Some("30s"));
624 assert!(!cmd.tui);
625 }
626 _ => panic!("expected sql command"),
627 }
628 }
629
630 #[test]
631 fn test_parse_sql_tui_flag() {
632 let args = vec!["alopex", "sql", "--tui", "SELECT 1"];
633 let cli = Cli::try_parse_from(args).unwrap();
634
635 match cli.command {
636 Some(Command::Sql(cmd)) => {
637 assert!(cmd.tui);
638 assert_eq!(cmd.query.as_deref(), Some("SELECT 1"));
639 }
640 _ => panic!("expected sql command"),
641 }
642 }
643
644 #[test]
645 fn test_parse_server_status() {
646 let args = vec!["alopex", "server", "status"];
647 let cli = Cli::try_parse_from(args).unwrap();
648
649 assert!(matches!(
650 cli.command,
651 Some(Command::Server {
652 command: Some(ServerCommand::Status)
653 })
654 ));
655 }
656
657 #[test]
658 fn test_parse_server_compaction_trigger() {
659 let args = vec!["alopex", "server", "compaction", "trigger"];
660 let cli = Cli::try_parse_from(args).unwrap();
661
662 assert!(matches!(
663 cli.command,
664 Some(Command::Server {
665 command: Some(ServerCommand::Compaction {
666 command: CompactionCommand::Trigger
667 })
668 })
669 ));
670 }
671
672 #[test]
673 fn test_parse_server_join_leave() {
674 let join = Cli::try_parse_from(vec!["alopex", "server", "join"]).unwrap();
675 assert!(matches!(
676 join.command,
677 Some(Command::Server {
678 command: Some(ServerCommand::Join)
679 })
680 ));
681
682 let leave = Cli::try_parse_from(vec!["alopex", "server", "leave"]).unwrap();
683 assert!(matches!(
684 leave.command,
685 Some(Command::Server {
686 command: Some(ServerCommand::Leave)
687 })
688 ));
689 }
690
691 #[test]
692 fn test_parse_verbose_quiet() {
693 let args = vec!["alopex", "--in-memory", "--verbose", "kv", "list"];
694 let cli = Cli::try_parse_from(args).unwrap();
695
696 assert!(cli.verbose);
697 assert!(!cli.quiet);
698 }
699
700 #[test]
701 fn test_parse_thread_mode() {
702 let args = vec![
703 "alopex",
704 "--in-memory",
705 "--thread-mode",
706 "single",
707 "kv",
708 "list",
709 ];
710 let cli = Cli::try_parse_from(args).unwrap();
711
712 assert_eq!(cli.thread_mode, ThreadMode::Single);
713 }
714
715 #[test]
716 fn test_parse_profile_option_batch_yes() {
717 let args = vec![
718 "alopex",
719 "--profile",
720 "dev",
721 "--batch",
722 "--yes",
723 "--in-memory",
724 "kv",
725 "list",
726 ];
727 let cli = Cli::try_parse_from(args).unwrap();
728
729 assert_eq!(cli.profile.as_deref(), Some("dev"));
730 assert!(cli.batch);
731 assert!(cli.yes);
732 }
733
734 #[test]
735 fn test_parse_batch_short_flag() {
736 let args = vec!["alopex", "-b", "--in-memory", "kv", "list"];
737 let cli = Cli::try_parse_from(args).unwrap();
738
739 assert!(cli.batch);
740 }
741
742 #[test]
743 fn test_parse_profile_create_subcommand() {
744 let args = vec![
745 "alopex",
746 "profile",
747 "create",
748 "dev",
749 "--data-dir",
750 "/path/to/db",
751 ];
752 let cli = Cli::try_parse_from(args).unwrap();
753
754 assert!(matches!(
755 cli.command,
756 Some(Command::Profile {
757 command: Some(ProfileCommand::Create { name, data_dir })
758 })
759 if name == "dev" && data_dir == "/path/to/db"
760 ));
761 }
762
763 #[test]
764 fn test_parse_completions_bash() {
765 let args = vec!["alopex", "completions", "bash"];
766 let cli = Cli::try_parse_from(args).unwrap();
767
768 assert!(matches!(
769 cli.command,
770 Some(Command::Completions { shell }) if shell == Shell::Bash
771 ));
772 }
773
774 #[test]
775 fn test_parse_completions_pwsh() {
776 let args = vec!["alopex", "completions", "pwsh"];
777 let cli = Cli::try_parse_from(args).unwrap();
778
779 assert!(matches!(
780 cli.command,
781 Some(Command::Completions { shell }) if shell == Shell::PowerShell
782 ));
783 }
784
785 #[test]
786 fn test_parse_kv_put() {
787 let args = vec!["alopex", "--in-memory", "kv", "put", "mykey", "myvalue"];
788 let cli = Cli::try_parse_from(args).unwrap();
789
790 assert!(matches!(
791 cli.command,
792 Some(Command::Kv {
793 command: Some(KvCommand::Put { key, value })
794 }) if key == "mykey" && value == "myvalue"
795 ));
796 }
797
798 #[test]
799 fn test_parse_kv_delete() {
800 let args = vec!["alopex", "--in-memory", "kv", "delete", "mykey"];
801 let cli = Cli::try_parse_from(args).unwrap();
802
803 assert!(matches!(
804 cli.command,
805 Some(Command::Kv {
806 command: Some(KvCommand::Delete { key })
807 }) if key == "mykey"
808 ));
809 }
810
811 #[test]
812 fn test_parse_kv_txn_begin() {
813 let args = vec!["alopex", "kv", "txn", "begin", "--timeout-secs", "30"];
814 let cli = Cli::try_parse_from(args).unwrap();
815
816 assert!(matches!(
817 cli.command,
818 Some(Command::Kv {
819 command: Some(KvCommand::Txn(KvTxnCommand::Begin {
820 timeout_secs: Some(30)
821 }))
822 })
823 ));
824 }
825
826 #[test]
827 fn test_parse_kv_txn_get_requires_txn_id() {
828 let args = vec!["alopex", "kv", "txn", "get", "mykey"];
829
830 assert!(Cli::try_parse_from(args).is_err());
831 }
832
833 #[test]
834 fn test_parse_kv_txn_get() {
835 let args = vec!["alopex", "kv", "txn", "get", "mykey", "--txn-id", "txn123"];
836 let cli = Cli::try_parse_from(args).unwrap();
837
838 assert!(matches!(
839 cli.command,
840 Some(Command::Kv {
841 command: Some(KvCommand::Txn(KvTxnCommand::Get { key, txn_id }))
842 }) if key == "mykey" && txn_id == "txn123"
843 ));
844 }
845
846 #[test]
847 fn test_parse_kv_list_with_prefix() {
848 let args = vec!["alopex", "--in-memory", "kv", "list", "--prefix", "user:"];
849 let cli = Cli::try_parse_from(args).unwrap();
850
851 assert!(matches!(
852 cli.command,
853 Some(Command::Kv {
854 command: Some(KvCommand::List { prefix: Some(p) })
855 }) if p == "user:"
856 ));
857 }
858
859 #[test]
860 fn test_parse_sql_from_file() {
861 let args = vec!["alopex", "--in-memory", "sql", "-f", "query.sql"];
862 let cli = Cli::try_parse_from(args).unwrap();
863
864 assert!(matches!(
865 cli.command,
866 Some(Command::Sql(SqlCommand { query: None, file: Some(f), .. })) if f == "query.sql"
867 ));
868 }
869
870 #[test]
871 fn test_parse_vector_search() {
872 let args = vec![
873 "alopex",
874 "--in-memory",
875 "vector",
876 "search",
877 "--index",
878 "my_index",
879 "--query",
880 "[1.0,2.0,3.0]",
881 "-k",
882 "5",
883 ];
884 let cli = Cli::try_parse_from(args).unwrap();
885
886 assert!(matches!(
887 cli.command,
888 Some(Command::Vector {
889 command: Some(VectorCommand::Search { index, query, k, progress })
890 }) if index == "my_index" && query == "[1.0,2.0,3.0]" && k == 5 && !progress
891 ));
892 }
893
894 #[test]
895 fn test_parse_vector_upsert() {
896 let args = vec![
897 "alopex",
898 "--in-memory",
899 "vector",
900 "upsert",
901 "--index",
902 "my_index",
903 "--key",
904 "vec1",
905 "--vector",
906 "[1.0,2.0,3.0]",
907 ];
908 let cli = Cli::try_parse_from(args).unwrap();
909
910 assert!(matches!(
911 cli.command,
912 Some(Command::Vector {
913 command: Some(VectorCommand::Upsert { index, key, vector })
914 }) if index == "my_index" && key == "vec1" && vector == "[1.0,2.0,3.0]"
915 ));
916 }
917
918 #[test]
919 fn test_parse_vector_delete() {
920 let args = vec![
921 "alopex",
922 "--in-memory",
923 "vector",
924 "delete",
925 "--index",
926 "my_index",
927 "--key",
928 "vec1",
929 ];
930 let cli = Cli::try_parse_from(args).unwrap();
931
932 assert!(matches!(
933 cli.command,
934 Some(Command::Vector {
935 command: Some(VectorCommand::Delete { index, key })
936 }) if index == "my_index" && key == "vec1"
937 ));
938 }
939
940 #[test]
941 fn test_parse_hnsw_create() {
942 let args = vec![
943 "alopex",
944 "--in-memory",
945 "hnsw",
946 "create",
947 "my_index",
948 "--dim",
949 "128",
950 "--metric",
951 "l2",
952 ];
953 let cli = Cli::try_parse_from(args).unwrap();
954
955 assert!(matches!(
956 cli.command,
957 Some(Command::Hnsw {
958 command: Some(HnswCommand::Create { name, dim, metric })
959 }) if name == "my_index" && dim == 128 && metric == DistanceMetric::L2
960 ));
961 }
962
963 #[test]
964 fn test_parse_hnsw_create_default_metric() {
965 let args = vec![
966 "alopex",
967 "--in-memory",
968 "hnsw",
969 "create",
970 "my_index",
971 "--dim",
972 "128",
973 ];
974 let cli = Cli::try_parse_from(args).unwrap();
975
976 assert!(matches!(
977 cli.command,
978 Some(Command::Hnsw {
979 command: Some(HnswCommand::Create { name, dim, metric })
980 }) if name == "my_index" && dim == 128 && metric == DistanceMetric::Cosine
981 ));
982 }
983
984 #[test]
985 fn test_parse_columnar_scan() {
986 let args = vec![
987 "alopex",
988 "--in-memory",
989 "columnar",
990 "scan",
991 "--segment",
992 "seg_001",
993 ];
994 let cli = Cli::try_parse_from(args).unwrap();
995
996 assert!(matches!(
997 cli.command,
998 Some(Command::Columnar {
999 command: Some(ColumnarCommand::Scan { segment, progress })
1000 }) if segment == "seg_001" && !progress
1001 ));
1002 }
1003
1004 #[test]
1005 fn test_parse_columnar_stats() {
1006 let args = vec![
1007 "alopex",
1008 "--in-memory",
1009 "columnar",
1010 "stats",
1011 "--segment",
1012 "seg_001",
1013 ];
1014 let cli = Cli::try_parse_from(args).unwrap();
1015
1016 assert!(matches!(
1017 cli.command,
1018 Some(Command::Columnar {
1019 command: Some(ColumnarCommand::Stats { segment })
1020 }) if segment == "seg_001"
1021 ));
1022 }
1023
1024 #[test]
1025 fn test_parse_columnar_list() {
1026 let args = vec!["alopex", "--in-memory", "columnar", "list"];
1027 let cli = Cli::try_parse_from(args).unwrap();
1028
1029 assert!(matches!(
1030 cli.command,
1031 Some(Command::Columnar {
1032 command: Some(ColumnarCommand::List)
1033 })
1034 ));
1035 }
1036
1037 #[test]
1038 fn test_parse_columnar_ingest_defaults() {
1039 let args = vec![
1040 "alopex",
1041 "--in-memory",
1042 "columnar",
1043 "ingest",
1044 "--file",
1045 "data.csv",
1046 "--table",
1047 "events",
1048 ];
1049 let cli = Cli::try_parse_from(args).unwrap();
1050
1051 assert!(matches!(
1052 cli.command,
1053 Some(Command::Columnar {
1054 command: Some(ColumnarCommand::Ingest {
1055 file,
1056 table,
1057 delimiter,
1058 header,
1059 compression,
1060 row_group_size,
1061 })
1062 }) if file == std::path::Path::new("data.csv")
1063 && table == "events"
1064 && delimiter == ','
1065 && header
1066 && compression == "zstd"
1067 && row_group_size.is_none()
1068 ));
1069 }
1070
1071 #[test]
1072 fn test_parse_columnar_ingest_custom_options() {
1073 let args = vec![
1074 "alopex",
1075 "--in-memory",
1076 "columnar",
1077 "ingest",
1078 "--file",
1079 "data.csv",
1080 "--table",
1081 "events",
1082 "--delimiter",
1083 ";",
1084 "--header",
1085 "false",
1086 "--compression",
1087 "zstd",
1088 "--row-group-size",
1089 "500",
1090 ];
1091 let cli = Cli::try_parse_from(args).unwrap();
1092
1093 assert!(matches!(
1094 cli.command,
1095 Some(Command::Columnar {
1096 command: Some(ColumnarCommand::Ingest {
1097 file,
1098 table,
1099 delimiter,
1100 header,
1101 compression,
1102 row_group_size,
1103 })
1104 }) if file == std::path::Path::new("data.csv")
1105 && table == "events"
1106 && delimiter == ';'
1107 && !header
1108 && compression == "zstd"
1109 && row_group_size == Some(500)
1110 ));
1111 }
1112
1113 #[test]
1114 fn test_parse_columnar_index_create() {
1115 let args = vec![
1116 "alopex",
1117 "--in-memory",
1118 "columnar",
1119 "index",
1120 "create",
1121 "--segment",
1122 "123:1",
1123 "--column",
1124 "col1",
1125 "--type",
1126 "bloom",
1127 ];
1128 let cli = Cli::try_parse_from(args).unwrap();
1129
1130 assert!(matches!(
1131 cli.command,
1132 Some(Command::Columnar {
1133 command: Some(ColumnarCommand::Index(IndexCommand::Create {
1134 segment,
1135 column,
1136 index_type,
1137 }))
1138 }) if segment == "123:1"
1139 && column == "col1"
1140 && index_type == "bloom"
1141 ));
1142 }
1143
1144 #[test]
1145 fn test_output_format_supports_streaming() {
1146 assert!(!OutputFormat::Table.supports_streaming());
1147 assert!(OutputFormat::Json.supports_streaming());
1148 assert!(OutputFormat::Jsonl.supports_streaming());
1149 assert!(OutputFormat::Csv.supports_streaming());
1150 assert!(OutputFormat::Tsv.supports_streaming());
1151 }
1152
1153 #[test]
1154 fn test_default_values() {
1155 let args = vec!["alopex", "--in-memory", "kv", "list"];
1156 let cli = Cli::try_parse_from(args).unwrap();
1157
1158 assert_eq!(cli.output_format(), OutputFormat::Table);
1159 assert!(!cli.output_is_explicit());
1160 assert_eq!(cli.thread_mode, ThreadMode::Multi);
1161 assert!(cli.limit.is_none());
1162 assert!(!cli.quiet);
1163 assert!(!cli.verbose);
1164 }
1165
1166 #[test]
1167 fn test_s3_data_dir() {
1168 let args = vec![
1169 "alopex",
1170 "--data-dir",
1171 "s3://my-bucket/prefix",
1172 "kv",
1173 "list",
1174 ];
1175 let cli = Cli::try_parse_from(args).unwrap();
1176
1177 assert_eq!(cli.data_dir, Some("s3://my-bucket/prefix".to_string()));
1178 }
1179}