Skip to main content

cqlite_cli/repl/
command_parser.rs

1// Command Parser for REPL Engine
2//
3// Parses user input and converts it into structured commands that can be executed
4// by the REPL engine. Handles both meta-commands (:help, :quit) and CQL queries.
5
6use super::{ReplError, ReplResult};
7#[allow(unused_imports)]
8use std::fmt;
9
10/// Types of commands that can be executed in the REPL
11#[derive(Debug, Clone, PartialEq)]
12pub enum CommandType {
13    /// Exit the REPL
14    Exit,
15    /// Show help information
16    Help { topic: Option<String> },
17    /// Configuration operations
18    Config { operation: String },
19    /// List tables
20    Tables,
21    /// Describe an object (table, keyspace, etc.)
22    Describe { object_name: String },
23    /// Switch to a keyspace
24    Use { keyspace: String },
25    /// Execute a CQL query
26    CqlQuery { query: String },
27    /// Clear the screen
28    Clear,
29    /// Show command history
30    History,
31    /// Execute commands from a file
32    Source { file_path: String },
33    /// Show discovery and schema coverage status
34    Status,
35    /// List keyspaces
36    Keyspaces,
37    /// Schema management commands
38    Schema { operation: SchemaOperation },
39    /// Show health diagnostics
40    Health,
41    /// Unknown command
42    Unknown { input: String },
43    /// Flush memtable to SSTable (Issue #392)
44    Flush,
45    /// Show write engine statistics (Issue #392)
46    WriteStats,
47    /// Run maintenance/compaction (Issue #392)
48    Maintenance { budget_ms: Option<u64> },
49}
50
51/// Schema operation types
52#[derive(Debug, Clone, PartialEq)]
53pub enum SchemaOperation {
54    /// Load schema from file(s)
55    Load { paths: Vec<String> },
56    /// Refresh currently loaded schemas
57    Refresh,
58    /// Unload all schemas
59    Unload,
60    /// Show schema status
61    Show,
62    /// List loaded schemas with coverage info
63    List,
64}
65
66/// A parsed command with metadata
67#[derive(Debug, Clone)]
68pub struct ParsedCommand {
69    /// The type of command
70    pub command_type: CommandType,
71    /// Original input text
72    pub original_input: String,
73    /// Whether this is a multi-line command
74    pub is_multiline: bool,
75    /// Command metadata
76    pub metadata: CommandMetadata,
77}
78
79/// Additional metadata about the command
80#[derive(Debug, Clone, Default)]
81pub struct CommandMetadata {
82    /// Estimated execution complexity (0-10)
83    pub complexity: u8,
84    /// Whether this command modifies state
85    pub modifies_state: bool,
86    /// Whether this command requires database access
87    pub requires_database: bool,
88    /// Command category
89    pub category: CommandCategory,
90}
91
92/// Categories of commands
93#[derive(Debug, Clone, PartialEq, Default)]
94pub enum CommandCategory {
95    #[default]
96    Unknown,
97    Meta,       // :help, :quit, etc.
98    Query,      // SELECT, INSERT, etc.
99    Schema,     // DESCRIBE, CREATE TABLE, etc.
100    Navigation, // USE, :tables, etc.
101    System,     // :clear, :history, etc.
102}
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
117/// Command parser
118pub struct CommandParser {
119    /// Enable strict parsing (reject ambiguous commands)
120    strict_mode: bool,
121}
122
123impl CommandParser {
124    /// Create a new command parser
125    pub fn new() -> Self {
126        Self { strict_mode: false }
127    }
128
129    /// Create a new command parser in strict mode
130    pub fn new_strict() -> Self {
131        Self { strict_mode: true }
132    }
133
134    /// Parse a command string into a structured command
135    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    /// Parse the command type from input
154    fn parse_command_type(&self, input: &str) -> ReplResult<CommandType> {
155        // Handle meta-commands (starting with : or .)
156        if let Some(meta_cmd) = self.try_parse_meta_command(input) {
157            return Ok(meta_cmd);
158        }
159
160        // Handle CQL commands
161        if let Some(cql_cmd) = self.try_parse_cql_command(input) {
162            return Ok(cql_cmd);
163        }
164
165        // Unknown command
166        Ok(CommandType::Unknown {
167            input: input.to_string(),
168        })
169    }
170
171    /// Try to parse as a meta-command
172    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        // Remove the prefix
178        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            // Exit commands
195            "quit" | "exit" | "q" => Some(CommandType::Exit),
196
197            // Help commands
198            "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            // Configuration commands
208            "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            // Navigation commands
218            "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 // Require object name in strict mode
226                } 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 // USE requires a keyspace
239                }
240            }
241
242            // System commands
243            "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 // SOURCE requires a file path
252                }
253            }
254            "status" => Some(CommandType::Status),
255            "keyspaces" => Some(CommandType::Keyspaces),
256            "health" => Some(CommandType::Health),
257
258            // Write commands (Issue #392)
259            "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 commands
271            "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                    // Default to show if no operation specified
279                    Some(CommandType::Schema {
280                        operation: SchemaOperation::Show,
281                    })
282                }
283            }
284
285            _ => None,
286        }
287    }
288
289    /// Parse schema operation subcommands
290    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                    // Collect all remaining parts as file paths
299                    let paths: Vec<String> = parts[1..].iter().map(|s| s.to_string()).collect();
300                    Some(SchemaOperation::Load { paths })
301                } else {
302                    None // LOAD requires at least one path
303                }
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    /// Try to parse as a CQL command
314    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        // Check for common CQL keywords
319        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                // Ensure it's actually a word boundary
343                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        // Check for CQLSH-style describe commands
353        if trimmed.starts_with("DESC ") {
354            return Some(CommandType::CqlQuery {
355                query: input.to_string(),
356            });
357        }
358
359        // If it contains SQL-like patterns, assume it's CQL
360        if self.looks_like_sql(input) {
361            return Some(CommandType::CqlQuery {
362                query: input.to_string(),
363            });
364        }
365
366        None
367    }
368
369    /// Check if input looks like SQL/CQL
370    fn looks_like_sql(&self, input: &str) -> bool {
371        let upper = input.to_uppercase();
372
373        // Contains SQL-like keywords
374        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        // Contains parentheses (function calls, subqueries)
399        (input.contains('(') && input.contains(')')) ||
400        // Contains semicolon (statement terminator)
401        input.contains(';') ||
402        // Contains common operators
403        input.contains('=') || input.contains('<') || input.contains('>') ||
404        // Contains string literals
405        (input.contains('\'') && input.matches('\'').count() >= 2)
406    }
407
408    /// Check if this is a multi-line command
409    fn is_multiline_command(&self, input: &str) -> bool {
410        input.contains('\n') || input.contains('\r')
411    }
412
413    /// Generate metadata for a command
414    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            // Issue #392: Write commands
533            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    /// Categorize a CQL query
557    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    /// Estimate query complexity (0-10 scale)
577    fn estimate_query_complexity(&self, query: &str) -> u8 {
578        let upper = query.to_uppercase();
579        let mut complexity = 1;
580
581        // Basic operations
582        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        // Joins increase complexity
590        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        // Subqueries
598        let paren_count = query.matches('(').count();
599        if paren_count > 1 {
600            complexity += paren_count.min(3) as u8;
601        }
602
603        // Aggregations
604        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        // Complex functions
615        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        // DDL operations
625        if upper.contains("CREATE TABLE") || upper.contains("ALTER TABLE") {
626            complexity += 2;
627        }
628
629        complexity.min(10)
630    }
631
632    /// Check if query modifies database state
633    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    /// Validate a parsed command
646    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                // Validate keyspace name format
662                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                // Basic CQL validation
678                if query.trim().is_empty() {
679                    return Err(ReplError::CommandParsing("Empty CQL query".to_string()));
680                }
681
682                // Check for balanced parentheses
683                if !self.has_balanced_parentheses(query) {
684                    return Err(ReplError::CommandParsing(
685                        "Unbalanced parentheses in query".to_string(),
686                    ));
687                }
688
689                // Check for balanced quotes
690                if !self.has_balanced_quotes(query) {
691                    return Err(ReplError::CommandParsing(
692                        "Unbalanced quotes in query".to_string(),
693                    ));
694                }
695            }
696            _ => {
697                // Other commands don't need special validation
698            }
699        }
700
701        Ok(())
702    }
703
704    /// Check if identifier is valid
705    fn is_valid_identifier(&self, name: &str) -> bool {
706        if name.is_empty() {
707            return false;
708        }
709
710        // Allow quoted identifiers
711        if name.starts_with('"') && name.ends_with('"') && name.len() > 2 {
712            return true;
713        }
714
715        // Check unquoted identifier rules
716        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    /// Check for balanced parentheses
725    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    /// Check for balanced quotes
754    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        // Valid commands
837        let result = parser.parse(":describe users").unwrap();
838        assert!(parser.validate(&result).is_ok());
839
840        // Invalid commands
841        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}