1use super::{ReplError, ReplResult};
7#[allow(unused_imports)]
8use std::fmt;
9
10#[derive(Debug, Clone, PartialEq)]
12pub enum CommandType {
13 Exit,
15 Help { topic: Option<String> },
17 Config { operation: String },
19 Tables,
21 Describe { object_name: String },
23 Use { keyspace: String },
25 CqlQuery { query: String },
27 Clear,
29 History,
31 Source { file_path: String },
33 Status,
35 Keyspaces,
37 Schema { operation: SchemaOperation },
39 Health,
41 Unknown { input: String },
43 Flush,
45 WriteStats,
47 Maintenance { budget_ms: Option<u64> },
49}
50
51#[derive(Debug, Clone, PartialEq)]
53pub enum SchemaOperation {
54 Load { paths: Vec<String> },
56 Refresh,
58 Unload,
60 Show,
62 List,
64}
65
66#[derive(Debug, Clone)]
68pub struct ParsedCommand {
69 pub command_type: CommandType,
71 pub original_input: String,
73 pub is_multiline: bool,
75 pub metadata: CommandMetadata,
77}
78
79#[derive(Debug, Clone, Default)]
81pub struct CommandMetadata {
82 pub complexity: u8,
84 pub modifies_state: bool,
86 pub requires_database: bool,
88 pub category: CommandCategory,
90}
91
92#[derive(Debug, Clone, PartialEq, Default)]
94pub enum CommandCategory {
95 #[default]
96 Unknown,
97 Meta, Query, Schema, Navigation, System, }
103
104impl fmt::Display for CommandCategory {
105 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106 match self {
107 CommandCategory::Unknown => write!(f, "unknown"),
108 CommandCategory::Meta => write!(f, "meta"),
109 CommandCategory::Query => write!(f, "query"),
110 CommandCategory::Schema => write!(f, "schema"),
111 CommandCategory::Navigation => write!(f, "navigation"),
112 CommandCategory::System => write!(f, "system"),
113 }
114 }
115}
116
117pub struct CommandParser {
119 strict_mode: bool,
121}
122
123impl CommandParser {
124 pub fn new() -> Self {
126 Self { strict_mode: false }
127 }
128
129 pub fn new_strict() -> Self {
131 Self { strict_mode: true }
132 }
133
134 pub fn parse(&self, input: &str) -> ReplResult<ParsedCommand> {
136 let trimmed = input.trim();
137
138 if trimmed.is_empty() {
139 return Err(ReplError::CommandParsing("Empty command".to_string()));
140 }
141
142 let command_type = self.parse_command_type(trimmed)?;
143 let metadata = self.generate_metadata(&command_type, trimmed);
144
145 Ok(ParsedCommand {
146 command_type,
147 original_input: input.to_string(),
148 is_multiline: self.is_multiline_command(trimmed),
149 metadata,
150 })
151 }
152
153 fn parse_command_type(&self, input: &str) -> ReplResult<CommandType> {
155 if let Some(meta_cmd) = self.try_parse_meta_command(input) {
157 return Ok(meta_cmd);
158 }
159
160 if let Some(cql_cmd) = self.try_parse_cql_command(input) {
162 return Ok(cql_cmd);
163 }
164
165 Ok(CommandType::Unknown {
167 input: input.to_string(),
168 })
169 }
170
171 fn try_parse_meta_command(&self, input: &str) -> Option<CommandType> {
173 if !input.starts_with(':') && !input.starts_with('.') && !input.starts_with('\\') {
174 return None;
175 }
176
177 let cmd = if input.starts_with(':') {
179 &input[1..]
180 } else if input.starts_with('.') {
181 &input[1..]
182 } else if input.starts_with('\\') {
183 &input[1..]
184 } else {
185 input
186 };
187
188 let parts: Vec<&str> = cmd.split_whitespace().collect();
189 if parts.is_empty() {
190 return None;
191 }
192
193 match parts[0].to_lowercase().as_str() {
194 "quit" | "exit" | "q" => Some(CommandType::Exit),
196
197 "help" | "h" | "?" => {
199 let topic = if parts.len() > 1 {
200 Some(parts[1..].join(" "))
201 } else {
202 None
203 };
204 Some(CommandType::Help { topic })
205 }
206
207 "config" | "set" | "show" => {
209 let operation = if parts.len() > 1 {
210 parts[1..].join(" ")
211 } else {
212 "show".to_string()
213 };
214 Some(CommandType::Config { operation })
215 }
216
217 "tables" | "list" => Some(CommandType::Tables),
219 "describe" | "desc" | "d" => {
220 if parts.len() > 1 {
221 Some(CommandType::Describe {
222 object_name: parts[1..].join(" "),
223 })
224 } else if self.strict_mode {
225 None } else {
227 Some(CommandType::Describe {
228 object_name: "".to_string(),
229 })
230 }
231 }
232 "use" => {
233 if parts.len() > 1 {
234 Some(CommandType::Use {
235 keyspace: parts[1].to_string(),
236 })
237 } else {
238 None }
240 }
241
242 "clear" | "cls" => Some(CommandType::Clear),
244 "history" | "hist" => Some(CommandType::History),
245 "source" | "load" => {
246 if parts.len() > 1 {
247 Some(CommandType::Source {
248 file_path: parts[1..].join(" "),
249 })
250 } else {
251 None }
253 }
254 "status" => Some(CommandType::Status),
255 "keyspaces" => Some(CommandType::Keyspaces),
256 "health" => Some(CommandType::Health),
257
258 "flush" => Some(CommandType::Flush),
260 "write-stats" | "writestats" | "wstats" | "stats" => Some(CommandType::WriteStats),
261 "maintenance" | "maint" | "compact" => {
262 let budget_ms = if parts.len() > 1 {
263 parts[1].parse().ok()
264 } else {
265 None
266 };
267 Some(CommandType::Maintenance { budget_ms })
268 }
269
270 "schema" => {
272 if parts.len() > 1 {
273 let schema_op = self.parse_schema_operation(&parts[1..])?;
274 Some(CommandType::Schema {
275 operation: schema_op,
276 })
277 } else {
278 Some(CommandType::Schema {
280 operation: SchemaOperation::Show,
281 })
282 }
283 }
284
285 _ => None,
286 }
287 }
288
289 fn parse_schema_operation(&self, parts: &[&str]) -> Option<SchemaOperation> {
291 if parts.is_empty() {
292 return Some(SchemaOperation::Show);
293 }
294
295 match parts[0].to_lowercase().as_str() {
296 "load" => {
297 if parts.len() > 1 {
298 let paths: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
300 Some(SchemaOperation::Load { paths })
301 } else {
302 None }
304 }
305 "refresh" => Some(SchemaOperation::Refresh),
306 "unload" => Some(SchemaOperation::Unload),
307 "show" | "status" => Some(SchemaOperation::Show),
308 "list" => Some(SchemaOperation::List),
309 _ => None,
310 }
311 }
312
313 fn try_parse_cql_command(&self, input: &str) -> Option<CommandType> {
315 let upper_input = input.to_uppercase();
316 let trimmed = upper_input.trim();
317
318 let cql_keywords = [
320 "SELECT",
321 "INSERT",
322 "UPDATE",
323 "DELETE",
324 "CREATE",
325 "ALTER",
326 "DROP",
327 "TRUNCATE",
328 "DESCRIBE",
329 "USE",
330 "GRANT",
331 "REVOKE",
332 "BEGIN",
333 "COMMIT",
334 "ROLLBACK",
335 "COPY",
336 "EXPLAIN",
337 "CONSISTENCY",
338 ];
339
340 for keyword in &cql_keywords {
341 if trimmed.starts_with(keyword) {
342 let rest = &trimmed[keyword.len()..];
344 if rest.is_empty() || rest.starts_with(' ') || rest.starts_with('\t') {
345 return Some(CommandType::CqlQuery {
346 query: input.to_string(),
347 });
348 }
349 }
350 }
351
352 if trimmed.starts_with("DESC ") {
354 return Some(CommandType::CqlQuery {
355 query: input.to_string(),
356 });
357 }
358
359 if self.looks_like_sql(input) {
361 return Some(CommandType::CqlQuery {
362 query: input.to_string(),
363 });
364 }
365
366 None
367 }
368
369 fn looks_like_sql(&self, input: &str) -> bool {
371 let upper = input.to_uppercase();
372
373 let sql_patterns = [
375 " FROM ",
376 " WHERE ",
377 " GROUP BY ",
378 " ORDER BY ",
379 " HAVING ",
380 " LIMIT ",
381 " OFFSET ",
382 " JOIN ",
383 " INNER ",
384 " LEFT ",
385 " RIGHT ",
386 " FULL ",
387 " ON ",
388 " AS ",
389 " INTO ",
390 " VALUES ",
391 " SET ",
392 " AND ",
393 " OR ",
394 " NOT ",
395 ];
396
397 sql_patterns.iter().any(|pattern| upper.contains(pattern)) ||
398 (input.contains('(') && input.contains(')')) ||
400 input.contains(';') ||
402 input.contains('=') || input.contains('<') || input.contains('>') ||
404 (input.contains('\'') && input.matches('\'').count() >= 2)
406 }
407
408 fn is_multiline_command(&self, input: &str) -> bool {
410 input.contains('\n') || input.contains('\r')
411 }
412
413 fn generate_metadata(&self, command_type: &CommandType, _input: &str) -> CommandMetadata {
415 let mut metadata = CommandMetadata::default();
416
417 match command_type {
418 CommandType::Exit => {
419 metadata.category = CommandCategory::Meta;
420 metadata.complexity = 1;
421 metadata.modifies_state = false;
422 metadata.requires_database = false;
423 }
424 CommandType::Help { .. } => {
425 metadata.category = CommandCategory::Meta;
426 metadata.complexity = 1;
427 metadata.modifies_state = false;
428 metadata.requires_database = false;
429 }
430 CommandType::Config { .. } => {
431 metadata.category = CommandCategory::Meta;
432 metadata.complexity = 2;
433 metadata.modifies_state = true;
434 metadata.requires_database = false;
435 }
436 CommandType::Tables => {
437 metadata.category = CommandCategory::Navigation;
438 metadata.complexity = 3;
439 metadata.modifies_state = false;
440 metadata.requires_database = true;
441 }
442 CommandType::Describe { .. } => {
443 metadata.category = CommandCategory::Schema;
444 metadata.complexity = 4;
445 metadata.modifies_state = false;
446 metadata.requires_database = true;
447 }
448 CommandType::Use { .. } => {
449 metadata.category = CommandCategory::Navigation;
450 metadata.complexity = 2;
451 metadata.modifies_state = true;
452 metadata.requires_database = true;
453 }
454 CommandType::CqlQuery { query } => {
455 metadata.category = self.categorize_cql_query(query);
456 metadata.complexity = self.estimate_query_complexity(query);
457 metadata.modifies_state = self.query_modifies_state(query);
458 metadata.requires_database = true;
459 }
460 CommandType::Clear => {
461 metadata.category = CommandCategory::System;
462 metadata.complexity = 1;
463 metadata.modifies_state = false;
464 metadata.requires_database = false;
465 }
466 CommandType::History => {
467 metadata.category = CommandCategory::System;
468 metadata.complexity = 1;
469 metadata.modifies_state = false;
470 metadata.requires_database = false;
471 }
472 CommandType::Source { .. } => {
473 metadata.category = CommandCategory::System;
474 metadata.complexity = 5;
475 metadata.modifies_state = true;
476 metadata.requires_database = true;
477 }
478 CommandType::Status => {
479 metadata.category = CommandCategory::System;
480 metadata.complexity = 3;
481 metadata.modifies_state = false;
482 metadata.requires_database = false;
483 }
484 CommandType::Keyspaces => {
485 metadata.category = CommandCategory::Navigation;
486 metadata.complexity = 2;
487 metadata.modifies_state = false;
488 metadata.requires_database = false;
489 }
490 CommandType::Health => {
491 metadata.category = CommandCategory::System;
492 metadata.complexity = 2;
493 metadata.modifies_state = false;
494 metadata.requires_database = false;
495 }
496 CommandType::Schema { operation } => {
497 metadata.category = CommandCategory::Schema;
498 match operation {
499 SchemaOperation::Load { .. } => {
500 metadata.complexity = 6;
501 metadata.modifies_state = true;
502 metadata.requires_database = true;
503 }
504 SchemaOperation::Refresh => {
505 metadata.complexity = 5;
506 metadata.modifies_state = true;
507 metadata.requires_database = true;
508 }
509 SchemaOperation::Unload => {
510 metadata.complexity = 4;
511 metadata.modifies_state = true;
512 metadata.requires_database = true;
513 }
514 SchemaOperation::Show => {
515 metadata.complexity = 2;
516 metadata.modifies_state = false;
517 metadata.requires_database = false;
518 }
519 SchemaOperation::List => {
520 metadata.complexity = 2;
521 metadata.modifies_state = false;
522 metadata.requires_database = false;
523 }
524 }
525 }
526 CommandType::Unknown { .. } => {
527 metadata.category = CommandCategory::Unknown;
528 metadata.complexity = 0;
529 metadata.modifies_state = false;
530 metadata.requires_database = false;
531 }
532 CommandType::Flush => {
534 metadata.category = CommandCategory::System;
535 metadata.complexity = 5;
536 metadata.modifies_state = true;
537 metadata.requires_database = true;
538 }
539 CommandType::WriteStats => {
540 metadata.category = CommandCategory::System;
541 metadata.complexity = 2;
542 metadata.modifies_state = false;
543 metadata.requires_database = true;
544 }
545 CommandType::Maintenance { .. } => {
546 metadata.category = CommandCategory::System;
547 metadata.complexity = 6;
548 metadata.modifies_state = true;
549 metadata.requires_database = true;
550 }
551 }
552
553 metadata
554 }
555
556 fn categorize_cql_query(&self, query: &str) -> CommandCategory {
558 let upper = query.to_uppercase();
559 let trimmed = upper.trim();
560
561 if trimmed.starts_with("SELECT") || trimmed.starts_with("EXPLAIN") {
562 CommandCategory::Query
563 } else if trimmed.starts_with("CREATE")
564 || trimmed.starts_with("ALTER")
565 || trimmed.starts_with("DROP")
566 || trimmed.starts_with("DESCRIBE")
567 {
568 CommandCategory::Schema
569 } else if trimmed.starts_with("USE") {
570 CommandCategory::Navigation
571 } else {
572 CommandCategory::Query
573 }
574 }
575
576 fn estimate_query_complexity(&self, query: &str) -> u8 {
578 let upper = query.to_uppercase();
579 let mut complexity = 1;
580
581 if upper.contains("SELECT") {
583 complexity += 1;
584 }
585 if upper.contains("INSERT") || upper.contains("UPDATE") || upper.contains("DELETE") {
586 complexity += 2;
587 }
588
589 if upper.contains(" JOIN ") {
591 complexity += 2;
592 }
593 if upper.contains(" LEFT ") || upper.contains(" RIGHT ") || upper.contains(" FULL ") {
594 complexity += 1;
595 }
596
597 let paren_count = query.matches('(').count();
599 if paren_count > 1 {
600 complexity += paren_count.min(3) as u8;
601 }
602
603 if upper.contains("GROUP BY") {
605 complexity += 1;
606 }
607 if upper.contains("ORDER BY") {
608 complexity += 1;
609 }
610 if upper.contains("HAVING") {
611 complexity += 1;
612 }
613
614 if upper.contains("COUNT(")
616 || upper.contains("SUM(")
617 || upper.contains("AVG(")
618 || upper.contains("MAX(")
619 || upper.contains("MIN(")
620 {
621 complexity += 1;
622 }
623
624 if upper.contains("CREATE TABLE") || upper.contains("ALTER TABLE") {
626 complexity += 2;
627 }
628
629 complexity.min(10)
630 }
631
632 fn query_modifies_state(&self, query: &str) -> bool {
634 let upper = query.to_uppercase();
635 let modifying_keywords = [
636 "INSERT", "UPDATE", "DELETE", "TRUNCATE", "CREATE", "ALTER", "DROP", "GRANT", "REVOKE",
637 ];
638
639 modifying_keywords.iter().any(|keyword| {
640 let pattern = format!(" {} ", keyword);
641 format!(" {} ", upper).contains(&pattern) || upper.starts_with(keyword)
642 })
643 }
644
645 pub fn validate(&self, command: &ParsedCommand) -> ReplResult<()> {
647 match &command.command_type {
648 CommandType::Describe { object_name } => {
649 if object_name.is_empty() {
650 return Err(ReplError::CommandParsing(
651 "DESCRIBE command requires an object name".to_string(),
652 ));
653 }
654 }
655 CommandType::Use { keyspace } => {
656 if keyspace.is_empty() {
657 return Err(ReplError::CommandParsing(
658 "USE command requires a keyspace name".to_string(),
659 ));
660 }
661 if !self.is_valid_identifier(keyspace) {
663 return Err(ReplError::CommandParsing(format!(
664 "Invalid keyspace name: {}",
665 keyspace
666 )));
667 }
668 }
669 CommandType::Source { file_path } => {
670 if file_path.is_empty() {
671 return Err(ReplError::CommandParsing(
672 "SOURCE command requires a file path".to_string(),
673 ));
674 }
675 }
676 CommandType::CqlQuery { query } => {
677 if query.trim().is_empty() {
679 return Err(ReplError::CommandParsing("Empty CQL query".to_string()));
680 }
681
682 if !self.has_balanced_parentheses(query) {
684 return Err(ReplError::CommandParsing(
685 "Unbalanced parentheses in query".to_string(),
686 ));
687 }
688
689 if !self.has_balanced_quotes(query) {
691 return Err(ReplError::CommandParsing(
692 "Unbalanced quotes in query".to_string(),
693 ));
694 }
695 }
696 _ => {
697 }
699 }
700
701 Ok(())
702 }
703
704 fn is_valid_identifier(&self, name: &str) -> bool {
706 if name.is_empty() {
707 return false;
708 }
709
710 if name.starts_with('"') && name.ends_with('"') && name.len() > 2 {
712 return true;
713 }
714
715 let first_char = name.chars().next().unwrap();
717 if !first_char.is_ascii_alphabetic() && first_char != '_' {
718 return false;
719 }
720
721 name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
722 }
723
724 fn has_balanced_parentheses(&self, text: &str) -> bool {
726 let mut depth = 0;
727 let mut in_string = false;
728 let mut escape_next = false;
729
730 for ch in text.chars() {
731 if escape_next {
732 escape_next = false;
733 continue;
734 }
735
736 match ch {
737 '\\' if in_string => escape_next = true,
738 '\'' => in_string = !in_string,
739 '(' if !in_string => depth += 1,
740 ')' if !in_string => {
741 depth -= 1;
742 if depth < 0 {
743 return false;
744 }
745 }
746 _ => {}
747 }
748 }
749
750 depth == 0
751 }
752
753 fn has_balanced_quotes(&self, text: &str) -> bool {
755 let mut single_quote_count = 0;
756 let mut escape_next = false;
757
758 for ch in text.chars() {
759 if escape_next {
760 escape_next = false;
761 continue;
762 }
763
764 match ch {
765 '\\' => escape_next = true,
766 '\'' => single_quote_count += 1,
767 _ => {}
768 }
769 }
770
771 single_quote_count % 2 == 0
772 }
773}
774
775impl Default for CommandParser {
776 fn default() -> Self {
777 Self::new()
778 }
779}
780
781#[cfg(test)]
782mod tests {
783 use super::*;
784
785 #[test]
786 fn test_parse_exit_commands() {
787 let parser = CommandParser::new();
788
789 let cases = [":quit", ":exit", ":q", ".quit", "\\q"];
790 for case in &cases {
791 let result = parser.parse(case).unwrap();
792 assert_eq!(result.command_type, CommandType::Exit);
793 }
794 }
795
796 #[test]
797 fn test_parse_help_commands() {
798 let parser = CommandParser::new();
799
800 let result = parser.parse(":help").unwrap();
801 assert_eq!(result.command_type, CommandType::Help { topic: None });
802
803 let result = parser.parse(":help config").unwrap();
804 assert_eq!(
805 result.command_type,
806 CommandType::Help {
807 topic: Some("config".to_string())
808 }
809 );
810 }
811
812 #[test]
813 fn test_parse_cql_queries() {
814 let parser = CommandParser::new();
815
816 let cases = [
817 "SELECT * FROM users",
818 "INSERT INTO users (id, name) VALUES (1, 'test')",
819 "CREATE TABLE test (id int PRIMARY KEY)",
820 ];
821
822 for case in &cases {
823 let result = parser.parse(case).unwrap();
824 if let CommandType::CqlQuery { query } = result.command_type {
825 assert_eq!(query, case.to_string());
826 } else {
827 panic!("Expected CqlQuery, got {:?}", result.command_type);
828 }
829 }
830 }
831
832 #[test]
833 fn test_command_validation() {
834 let parser = CommandParser::new();
835
836 let result = parser.parse(":describe users").unwrap();
838 assert!(parser.validate(&result).is_ok());
839
840 let result = parser.parse(":describe").unwrap();
842 assert!(parser.validate(&result).is_err());
843 }
844
845 #[test]
846 fn test_complexity_estimation() {
847 let parser = CommandParser::new();
848
849 let simple_query = "SELECT * FROM users";
850 let result = parser.parse(simple_query).unwrap();
851 assert!(result.metadata.complexity <= 3);
852
853 let complex_query = "SELECT u.*, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id ORDER BY u.name";
854 let result = parser.parse(complex_query).unwrap();
855 assert!(result.metadata.complexity >= 5);
856 }
857}