From 7b5f1d5835a545f263ce176dc09937d64e895fb0 Mon Sep 17 00:00:00 2001 From: overtrue Date: Tue, 27 May 2025 23:29:39 +0800 Subject: [PATCH] feat: add comprehensive test coverage for s3select query module --- s3select/query/src/sql/dialect.rs | 291 +++++++++++++++++++++++ s3select/query/src/sql/optimizer.rs | 99 ++++++++ s3select/query/src/sql/parser.rs | 343 ++++++++++++++++++++++++++++ 3 files changed, 733 insertions(+) diff --git a/s3select/query/src/sql/dialect.rs b/s3select/query/src/sql/dialect.rs index 33297093..c92cce17 100644 --- a/s3select/query/src/sql/dialect.rs +++ b/s3select/query/src/sql/dialect.rs @@ -16,3 +16,294 @@ impl Dialect for RustFsDialect { true } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rustfs_dialect_creation() { + let dialect = RustFsDialect::default(); + + // Test that dialect can be created successfully + assert!(std::mem::size_of::() == 0, "Dialect should be zero-sized"); + } + + #[test] + fn test_rustfs_dialect_debug() { + let dialect = RustFsDialect::default(); + + let debug_str = format!("{:?}", dialect); + assert!(!debug_str.is_empty(), "Debug output should not be empty"); + assert!(debug_str.contains("RustFsDialect"), "Debug output should contain dialect name"); + } + + #[test] + fn test_is_identifier_start_alphabetic() { + let dialect = RustFsDialect::default(); + + // Test alphabetic characters + assert!(dialect.is_identifier_start('a'), "Lowercase letter should be valid identifier start"); + assert!(dialect.is_identifier_start('A'), "Uppercase letter should be valid identifier start"); + assert!(dialect.is_identifier_start('z'), "Last lowercase letter should be valid identifier start"); + assert!(dialect.is_identifier_start('Z'), "Last uppercase letter should be valid identifier start"); + + // Test Unicode alphabetic characters + assert!(dialect.is_identifier_start('α'), "Greek letter should be valid identifier start"); + assert!(dialect.is_identifier_start('中'), "Chinese character should be valid identifier start"); + assert!(dialect.is_identifier_start('ñ'), "Accented letter should be valid identifier start"); + } + + #[test] + fn test_is_identifier_start_special_chars() { + let dialect = RustFsDialect::default(); + + // Test special characters that are allowed + assert!(dialect.is_identifier_start('_'), "Underscore should be valid identifier start"); + assert!(dialect.is_identifier_start('#'), "Hash should be valid identifier start"); + assert!(dialect.is_identifier_start('@'), "At symbol should be valid identifier start"); + } + + #[test] + fn test_is_identifier_start_invalid_chars() { + let dialect = RustFsDialect::default(); + + // Test characters that should not be valid identifier starts + assert!(!dialect.is_identifier_start('0'), "Digit should not be valid identifier start"); + assert!(!dialect.is_identifier_start('9'), "Digit should not be valid identifier start"); + assert!(!dialect.is_identifier_start('$'), "Dollar sign should not be valid identifier start"); + assert!(!dialect.is_identifier_start(' '), "Space should not be valid identifier start"); + assert!(!dialect.is_identifier_start('\t'), "Tab should not be valid identifier start"); + assert!(!dialect.is_identifier_start('\n'), "Newline should not be valid identifier start"); + assert!(!dialect.is_identifier_start('.'), "Dot should not be valid identifier start"); + assert!(!dialect.is_identifier_start(','), "Comma should not be valid identifier start"); + assert!(!dialect.is_identifier_start(';'), "Semicolon should not be valid identifier start"); + assert!(!dialect.is_identifier_start('('), "Left paren should not be valid identifier start"); + assert!(!dialect.is_identifier_start(')'), "Right paren should not be valid identifier start"); + assert!(!dialect.is_identifier_start('['), "Left bracket should not be valid identifier start"); + assert!(!dialect.is_identifier_start(']'), "Right bracket should not be valid identifier start"); + assert!(!dialect.is_identifier_start('{'), "Left brace should not be valid identifier start"); + assert!(!dialect.is_identifier_start('}'), "Right brace should not be valid identifier start"); + assert!(!dialect.is_identifier_start('='), "Equals should not be valid identifier start"); + assert!(!dialect.is_identifier_start('+'), "Plus should not be valid identifier start"); + assert!(!dialect.is_identifier_start('-'), "Minus should not be valid identifier start"); + assert!(!dialect.is_identifier_start('*'), "Asterisk should not be valid identifier start"); + assert!(!dialect.is_identifier_start('/'), "Slash should not be valid identifier start"); + assert!(!dialect.is_identifier_start('%'), "Percent should not be valid identifier start"); + assert!(!dialect.is_identifier_start('<'), "Less than should not be valid identifier start"); + assert!(!dialect.is_identifier_start('>'), "Greater than should not be valid identifier start"); + assert!(!dialect.is_identifier_start('!'), "Exclamation should not be valid identifier start"); + assert!(!dialect.is_identifier_start('?'), "Question mark should not be valid identifier start"); + assert!(!dialect.is_identifier_start('&'), "Ampersand should not be valid identifier start"); + assert!(!dialect.is_identifier_start('|'), "Pipe should not be valid identifier start"); + assert!(!dialect.is_identifier_start('^'), "Caret should not be valid identifier start"); + assert!(!dialect.is_identifier_start('~'), "Tilde should not be valid identifier start"); + assert!(!dialect.is_identifier_start('`'), "Backtick should not be valid identifier start"); + assert!(!dialect.is_identifier_start('"'), "Double quote should not be valid identifier start"); + assert!(!dialect.is_identifier_start('\''), "Single quote should not be valid identifier start"); + } + + #[test] + fn test_is_identifier_part_alphabetic() { + let dialect = RustFsDialect::default(); + + // Test alphabetic characters + assert!(dialect.is_identifier_part('a'), "Lowercase letter should be valid identifier part"); + assert!(dialect.is_identifier_part('A'), "Uppercase letter should be valid identifier part"); + assert!(dialect.is_identifier_part('z'), "Last lowercase letter should be valid identifier part"); + assert!(dialect.is_identifier_part('Z'), "Last uppercase letter should be valid identifier part"); + + // Test Unicode alphabetic characters + assert!(dialect.is_identifier_part('α'), "Greek letter should be valid identifier part"); + assert!(dialect.is_identifier_part('中'), "Chinese character should be valid identifier part"); + assert!(dialect.is_identifier_part('ñ'), "Accented letter should be valid identifier part"); + } + + #[test] + fn test_is_identifier_part_digits() { + let dialect = RustFsDialect::default(); + + // Test ASCII digits + assert!(dialect.is_identifier_part('0'), "Digit 0 should be valid identifier part"); + assert!(dialect.is_identifier_part('1'), "Digit 1 should be valid identifier part"); + assert!(dialect.is_identifier_part('5'), "Digit 5 should be valid identifier part"); + assert!(dialect.is_identifier_part('9'), "Digit 9 should be valid identifier part"); + } + + #[test] + fn test_is_identifier_part_special_chars() { + let dialect = RustFsDialect::default(); + + // Test special characters that are allowed + assert!(dialect.is_identifier_part('_'), "Underscore should be valid identifier part"); + assert!(dialect.is_identifier_part('#'), "Hash should be valid identifier part"); + assert!(dialect.is_identifier_part('@'), "At symbol should be valid identifier part"); + assert!(dialect.is_identifier_part('$'), "Dollar sign should be valid identifier part"); + } + + #[test] + fn test_is_identifier_part_invalid_chars() { + let dialect = RustFsDialect::default(); + + // Test characters that should not be valid identifier parts + assert!(!dialect.is_identifier_part(' '), "Space should not be valid identifier part"); + assert!(!dialect.is_identifier_part('\t'), "Tab should not be valid identifier part"); + assert!(!dialect.is_identifier_part('\n'), "Newline should not be valid identifier part"); + assert!(!dialect.is_identifier_part('.'), "Dot should not be valid identifier part"); + assert!(!dialect.is_identifier_part(','), "Comma should not be valid identifier part"); + assert!(!dialect.is_identifier_part(';'), "Semicolon should not be valid identifier part"); + assert!(!dialect.is_identifier_part('('), "Left paren should not be valid identifier part"); + assert!(!dialect.is_identifier_part(')'), "Right paren should not be valid identifier part"); + assert!(!dialect.is_identifier_part('['), "Left bracket should not be valid identifier part"); + assert!(!dialect.is_identifier_part(']'), "Right bracket should not be valid identifier part"); + assert!(!dialect.is_identifier_part('{'), "Left brace should not be valid identifier part"); + assert!(!dialect.is_identifier_part('}'), "Right brace should not be valid identifier part"); + assert!(!dialect.is_identifier_part('='), "Equals should not be valid identifier part"); + assert!(!dialect.is_identifier_part('+'), "Plus should not be valid identifier part"); + assert!(!dialect.is_identifier_part('-'), "Minus should not be valid identifier part"); + assert!(!dialect.is_identifier_part('*'), "Asterisk should not be valid identifier part"); + assert!(!dialect.is_identifier_part('/'), "Slash should not be valid identifier part"); + assert!(!dialect.is_identifier_part('%'), "Percent should not be valid identifier part"); + assert!(!dialect.is_identifier_part('<'), "Less than should not be valid identifier part"); + assert!(!dialect.is_identifier_part('>'), "Greater than should not be valid identifier part"); + assert!(!dialect.is_identifier_part('!'), "Exclamation should not be valid identifier part"); + assert!(!dialect.is_identifier_part('?'), "Question mark should not be valid identifier part"); + assert!(!dialect.is_identifier_part('&'), "Ampersand should not be valid identifier part"); + assert!(!dialect.is_identifier_part('|'), "Pipe should not be valid identifier part"); + assert!(!dialect.is_identifier_part('^'), "Caret should not be valid identifier part"); + assert!(!dialect.is_identifier_part('~'), "Tilde should not be valid identifier part"); + assert!(!dialect.is_identifier_part('`'), "Backtick should not be valid identifier part"); + assert!(!dialect.is_identifier_part('"'), "Double quote should not be valid identifier part"); + assert!(!dialect.is_identifier_part('\''), "Single quote should not be valid identifier part"); + } + + #[test] + fn test_supports_group_by_expr() { + let dialect = RustFsDialect::default(); + + assert!(dialect.supports_group_by_expr(), "RustFsDialect should support GROUP BY expressions"); + } + + #[test] + fn test_identifier_validation_comprehensive() { + let dialect = RustFsDialect::default(); + + // Test valid identifier patterns + let valid_starts = ['a', 'A', 'z', 'Z', '_', '#', '@', 'α', '中']; + let valid_parts = ['a', 'A', '0', '9', '_', '#', '@', '$', 'α', '中']; + + for start_char in valid_starts { + assert!(dialect.is_identifier_start(start_char), + "Character '{}' should be valid identifier start", start_char); + + for part_char in valid_parts { + assert!(dialect.is_identifier_part(part_char), + "Character '{}' should be valid identifier part", part_char); + } + } + } + + #[test] + fn test_identifier_edge_cases() { + let dialect = RustFsDialect::default(); + + // Test edge cases with control characters + assert!(!dialect.is_identifier_start('\0'), "Null character should not be valid identifier start"); + assert!(!dialect.is_identifier_part('\0'), "Null character should not be valid identifier part"); + + assert!(!dialect.is_identifier_start('\x01'), "Control character should not be valid identifier start"); + assert!(!dialect.is_identifier_part('\x01'), "Control character should not be valid identifier part"); + + assert!(!dialect.is_identifier_start('\x7F'), "DEL character should not be valid identifier start"); + assert!(!dialect.is_identifier_part('\x7F'), "DEL character should not be valid identifier part"); + } + + #[test] + fn test_identifier_unicode_support() { + let dialect = RustFsDialect::default(); + + // Test various Unicode categories + let unicode_letters = ['α', 'β', 'γ', 'Α', 'Β', 'Γ', '中', '文', '日', '本', 'ñ', 'ü', 'ç']; + + for ch in unicode_letters { + assert!(dialect.is_identifier_start(ch), + "Unicode letter '{}' should be valid identifier start", ch); + assert!(dialect.is_identifier_part(ch), + "Unicode letter '{}' should be valid identifier part", ch); + } + } + + #[test] + fn test_identifier_ascii_digits() { + let dialect = RustFsDialect::default(); + + // Test all ASCII digits + for digit in '0'..='9' { + assert!(!dialect.is_identifier_start(digit), + "ASCII digit '{}' should not be valid identifier start", digit); + assert!(dialect.is_identifier_part(digit), + "ASCII digit '{}' should be valid identifier part", digit); + } + } + + #[test] + fn test_dialect_consistency() { + let dialect = RustFsDialect::default(); + + // Test that all valid identifier starts are also valid identifier parts + let test_chars = [ + 'a', 'A', 'z', 'Z', '_', '#', '@', 'α', '中', 'ñ', + '0', '9', '$', ' ', '.', ',', ';', '(', ')', '=', '+', '-' + ]; + + for ch in test_chars { + if dialect.is_identifier_start(ch) { + assert!(dialect.is_identifier_part(ch), + "Character '{}' that is valid identifier start should also be valid identifier part", ch); + } + } + } + + #[test] + fn test_dialect_memory_efficiency() { + let dialect = RustFsDialect::default(); + + // Test that dialect doesn't use excessive memory + let dialect_size = std::mem::size_of_val(&dialect); + assert!(dialect_size < 100, "Dialect should not use excessive memory"); + } + + #[test] + fn test_dialect_trait_implementation() { + let dialect = RustFsDialect::default(); + + // Test that dialect properly implements the Dialect trait + let dialect_ref: &dyn Dialect = &dialect; + + // Test basic functionality through trait + assert!(dialect_ref.is_identifier_start('a'), "Trait method should work for valid start"); + assert!(!dialect_ref.is_identifier_start('0'), "Trait method should work for invalid start"); + assert!(dialect_ref.is_identifier_part('a'), "Trait method should work for valid part"); + assert!(dialect_ref.is_identifier_part('0'), "Trait method should work for digit part"); + assert!(dialect_ref.supports_group_by_expr(), "Trait method should return true for GROUP BY support"); + } + + #[test] + fn test_dialect_clone_and_default() { + let dialect1 = RustFsDialect::default(); + let dialect2 = RustFsDialect::default(); + + // Test that multiple instances behave the same + let test_chars = ['a', 'A', '0', '_', '#', '@', '$', ' ', '.']; + + for ch in test_chars { + assert_eq!(dialect1.is_identifier_start(ch), dialect2.is_identifier_start(ch), + "Different instances should behave the same for is_identifier_start"); + assert_eq!(dialect1.is_identifier_part(ch), dialect2.is_identifier_part(ch), + "Different instances should behave the same for is_identifier_part"); + } + + assert_eq!(dialect1.supports_group_by_expr(), dialect2.supports_group_by_expr(), + "Different instances should behave the same for supports_group_by_expr"); + } +} diff --git a/s3select/query/src/sql/optimizer.rs b/s3select/query/src/sql/optimizer.rs index b424b073..10b9f197 100644 --- a/s3select/query/src/sql/optimizer.rs +++ b/s3select/query/src/sql/optimizer.rs @@ -80,3 +80,102 @@ impl CascadeOptimizerBuilder { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_cascade_optimizer_builder_default() { + let builder = CascadeOptimizerBuilder::default(); + + // Test that builder can be created successfully + assert!(std::mem::size_of::() > 0, "Builder should be created successfully"); + } + + #[test] + fn test_cascade_optimizer_builder_build_with_defaults() { + let builder = CascadeOptimizerBuilder::default(); + let optimizer = builder.build(); + + // Test that optimizer can be built with default components + assert!(std::mem::size_of_val(&optimizer) > 0, "Optimizer should be built successfully"); + } + + #[test] + fn test_cascade_optimizer_builder_basic_functionality() { + // Test that builder methods can be called and return self + let builder = CascadeOptimizerBuilder::default(); + + // Test that we can call builder methods (even if we don't have mock implementations) + // This tests the builder pattern itself + assert!(std::mem::size_of::() > 0, "Builder should be created successfully"); + } + + #[test] + fn test_cascade_optimizer_builder_memory_efficiency() { + let builder = CascadeOptimizerBuilder::default(); + + // Test that builder doesn't use excessive memory + let builder_size = std::mem::size_of_val(&builder); + assert!(builder_size < 1000, "Builder should not use excessive memory"); + + let optimizer = builder.build(); + let optimizer_size = std::mem::size_of_val(&optimizer); + assert!(optimizer_size < 1000, "Optimizer should not use excessive memory"); + } + + #[test] + fn test_cascade_optimizer_builder_multiple_builds() { + let builder = CascadeOptimizerBuilder::default(); + + // Test that we can build multiple optimizers from the same configuration + let optimizer1 = builder.build(); + assert!(std::mem::size_of_val(&optimizer1) > 0, "First optimizer should be built successfully"); + + // Note: builder is consumed by build(), so we can't build again from the same instance + // This is the expected behavior + } + + #[test] + fn test_cascade_optimizer_builder_default_fallbacks() { + let builder = CascadeOptimizerBuilder::default(); + let optimizer = builder.build(); + + // Test that default components are used when none are specified + // We can't directly access the internal components, but we can verify the optimizer was built + assert!(std::mem::size_of_val(&optimizer) > 0, "Optimizer should use default components"); + } + + + + #[test] + fn test_cascade_optimizer_component_types() { + let optimizer = CascadeOptimizerBuilder::default().build(); + + // Test that optimizer contains the expected component types + // We can't directly access the components, but we can verify the optimizer structure + assert!(std::mem::size_of_val(&optimizer) > 0, "Optimizer should contain components"); + + // The optimizer should have three Arc fields for the components + // This is a basic structural test + } + + #[test] + fn test_cascade_optimizer_builder_consistency() { + // Test that multiple builders with the same configuration produce equivalent optimizers + let optimizer1 = CascadeOptimizerBuilder::default().build(); + let optimizer2 = CascadeOptimizerBuilder::default().build(); + + // Both optimizers should be built successfully + assert!(std::mem::size_of_val(&optimizer1) > 0, "First optimizer should be built"); + assert!(std::mem::size_of_val(&optimizer2) > 0, "Second optimizer should be built"); + + // They should have the same memory footprint (same structure) + assert_eq!( + std::mem::size_of_val(&optimizer1), + std::mem::size_of_val(&optimizer2), + "Optimizers with same configuration should have same size" + ); + } +} diff --git a/s3select/query/src/sql/parser.rs b/s3select/query/src/sql/parser.rs index ebd2b5d4..c561d43e 100644 --- a/s3select/query/src/sql/parser.rs +++ b/s3select/query/src/sql/parser.rs @@ -90,3 +90,346 @@ impl<'a> ExtParser<'a> { parser_err!(format!("Expected {}, found: {}", expected, found)) } } + +#[cfg(test)] +mod tests { + use super::*; + use api::query::ast::ExtStatement; + + #[test] + fn test_default_parser_creation() { + let parser = DefaultParser::default(); + + // Test that parser can be created successfully + assert!(std::mem::size_of::() == 0, "Parser should be zero-sized"); + } + + #[test] + fn test_default_parser_simple_select() { + let parser = DefaultParser::default(); + let sql = "SELECT * FROM S3Object"; + + let result = parser.parse(sql); + assert!(result.is_ok(), "Simple SELECT should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + + // Just verify we get a SQL statement without diving into AST details + match &statements[0] { + ExtStatement::SqlStatement(_) => { + // Successfully parsed as SQL statement + }, + } + } + + #[test] + fn test_default_parser_select_with_columns() { + let parser = DefaultParser::default(); + let sql = "SELECT id, name, age FROM S3Object"; + + let result = parser.parse(sql); + assert!(result.is_ok(), "SELECT with columns should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + + match &statements[0] { + ExtStatement::SqlStatement(_) => { + // Successfully parsed as SQL statement + }, + } + } + + #[test] + fn test_default_parser_select_with_where() { + let parser = DefaultParser::default(); + let sql = "SELECT * FROM S3Object WHERE age > 25"; + + let result = parser.parse(sql); + assert!(result.is_ok(), "SELECT with WHERE should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + + match &statements[0] { + ExtStatement::SqlStatement(_) => { + // Successfully parsed as SQL statement + }, + } + } + + #[test] + fn test_default_parser_multiple_statements() { + let parser = DefaultParser::default(); + let sql = "SELECT * FROM S3Object; SELECT id FROM S3Object;"; + + let result = parser.parse(sql); + assert!(result.is_ok(), "Multiple statements should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 2, "Should have exactly two statements"); + } + + #[test] + fn test_default_parser_empty_statements() { + let parser = DefaultParser::default(); + let sql = ";;; SELECT * FROM S3Object; ;;;"; + + let result = parser.parse(sql); + assert!(result.is_ok(), "Empty statements should be ignored"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one non-empty statement"); + } + + #[test] + fn test_default_parser_invalid_sql() { + let parser = DefaultParser::default(); + let sql = "INVALID SQL SYNTAX"; + + let result = parser.parse(sql); + assert!(result.is_err(), "Invalid SQL should return error"); + } + + #[test] + fn test_default_parser_empty_sql() { + let parser = DefaultParser::default(); + let sql = ""; + + let result = parser.parse(sql); + assert!(result.is_ok(), "Empty SQL should parse successfully"); + + let statements = result.unwrap(); + assert!(statements.is_empty(), "Should have no statements"); + } + + #[test] + fn test_default_parser_whitespace_only() { + let parser = DefaultParser::default(); + let sql = " \n\t "; + + let result = parser.parse(sql); + assert!(result.is_ok(), "Whitespace-only SQL should parse successfully"); + + let statements = result.unwrap(); + assert!(statements.is_empty(), "Should have no statements"); + } + + #[test] + fn test_ext_parser_parse_sql() { + let sql = "SELECT * FROM S3Object"; + + let result = ExtParser::parse_sql(sql); + assert!(result.is_ok(), "ExtParser::parse_sql should work"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + } + + #[test] + fn test_ext_parser_parse_sql_with_dialect() { + let sql = "SELECT * FROM S3Object"; + let dialect = &RustFsDialect::default(); + + let result = ExtParser::parse_sql_with_dialect(sql, dialect); + assert!(result.is_ok(), "ExtParser::parse_sql_with_dialect should work"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + } + + #[test] + fn test_ext_parser_new_with_dialect() { + let sql = "SELECT * FROM S3Object"; + let dialect = &RustFsDialect::default(); + + let result = ExtParser::new_with_dialect(sql, dialect); + assert!(result.is_ok(), "ExtParser::new_with_dialect should work"); + } + + #[test] + fn test_ext_parser_complex_query() { + let sql = "SELECT id, name, age FROM S3Object WHERE age > 25 AND department = 'IT' ORDER BY age DESC LIMIT 10"; + + let result = ExtParser::parse_sql(sql); + assert!(result.is_ok(), "Complex query should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + + match &statements[0] { + ExtStatement::SqlStatement(_) => { + // Successfully parsed as SQL statement + }, + } + } + + #[test] + fn test_ext_parser_aggregate_functions() { + let sql = "SELECT COUNT(*), AVG(age), MAX(salary) FROM S3Object GROUP BY department"; + + let result = ExtParser::parse_sql(sql); + assert!(result.is_ok(), "Aggregate functions should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + + match &statements[0] { + ExtStatement::SqlStatement(_) => { + // Successfully parsed as SQL statement + }, + } + } + + #[test] + fn test_ext_parser_join_query() { + let sql = "SELECT s1.id, s2.name FROM S3Object s1 JOIN S3Object s2 ON s1.id = s2.id"; + + let result = ExtParser::parse_sql(sql); + assert!(result.is_ok(), "JOIN query should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + } + + #[test] + fn test_ext_parser_subquery() { + let sql = "SELECT * FROM S3Object WHERE id IN (SELECT id FROM S3Object WHERE age > 30)"; + + let result = ExtParser::parse_sql(sql); + assert!(result.is_ok(), "Subquery should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + } + + #[test] + fn test_ext_parser_case_insensitive() { + let sql = "select * from s3object where age > 25"; + + let result = ExtParser::parse_sql(sql); + assert!(result.is_ok(), "Case insensitive SQL should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + } + + #[test] + fn test_ext_parser_quoted_identifiers() { + let sql = r#"SELECT "id", "name" FROM "S3Object" WHERE "age" > 25"#; + + let result = ExtParser::parse_sql(sql); + assert!(result.is_ok(), "Quoted identifiers should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + } + + #[test] + fn test_ext_parser_string_literals() { + let sql = "SELECT * FROM S3Object WHERE name = 'John Doe' AND department = 'IT'"; + + let result = ExtParser::parse_sql(sql); + assert!(result.is_ok(), "String literals should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + } + + #[test] + fn test_ext_parser_numeric_literals() { + let sql = "SELECT * FROM S3Object WHERE age = 25 AND salary = 50000.50"; + + let result = ExtParser::parse_sql(sql); + assert!(result.is_ok(), "Numeric literals should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + } + + #[test] + fn test_ext_parser_error_handling() { + let invalid_sqls = vec![ + "SELECT FROM", // Missing column list + "SELECT * FROM", // Missing table name + "SELECT * FROM S3Object WHERE", // Incomplete WHERE clause + "SELECT * FROM S3Object GROUP", // Incomplete GROUP BY + "SELECT * FROM S3Object ORDER", // Incomplete ORDER BY + ]; + + for sql in invalid_sqls { + let result = ExtParser::parse_sql(sql); + assert!(result.is_err(), "Invalid SQL '{}' should return error", sql); + } + } + + #[test] + fn test_ext_parser_memory_efficiency() { + let sql = "SELECT * FROM S3Object"; + + // Test that parser doesn't use excessive memory + let result = ExtParser::parse_sql(sql); + assert!(result.is_ok(), "Parser should work efficiently"); + + let statements = result.unwrap(); + let memory_size = std::mem::size_of_val(&statements); + assert!(memory_size < 10000, "Parsed statements should not use excessive memory"); + } + + #[test] + fn test_ext_parser_large_query() { + // Test with a reasonably large query + let mut sql = String::from("SELECT "); + for i in 0..100 { + if i > 0 { + sql.push_str(", "); + } + sql.push_str(&format!("col{}", i)); + } + sql.push_str(" FROM S3Object WHERE "); + for i in 0..50 { + if i > 0 { + sql.push_str(" AND "); + } + sql.push_str(&format!("col{} > {}", i, i)); + } + + let result = ExtParser::parse_sql(&sql); + assert!(result.is_ok(), "Large query should parse successfully"); + + let statements = result.unwrap(); + assert_eq!(statements.len(), 1, "Should have exactly one statement"); + } + + #[test] + fn test_parser_err_macro() { + let error: Result<()> = parser_err!("Test error message"); + assert!(error.is_err(), "parser_err! macro should create error"); + + match error { + Err(ParserError::ParserError(msg)) => { + assert_eq!(msg, "Test error message", "Error message should match"); + }, + _ => panic!("Expected ParserError::ParserError"), + } + } + + #[test] + fn test_ext_parser_expected_method() { + let sql = "SELECT * FROM S3Object"; + let dialect = &RustFsDialect::default(); + let parser = ExtParser::new_with_dialect(sql, dialect).unwrap(); + + let result: Result<()> = parser.expected("test token", "found token"); + assert!(result.is_err(), "expected method should return error"); + + match result { + Err(ParserError::ParserError(msg)) => { + assert!(msg.contains("Expected test token"), "Error should contain expected message"); + assert!(msg.contains("found: found token"), "Error should contain found message"); + }, + _ => panic!("Expected ParserError::ParserError"), + } + } +}