diff --git a/tester/src/e2e/alter_table.rs b/tester/src/e2e/alter_table.rs new file mode 100644 index 0000000..f0c3f88 --- /dev/null +++ b/tester/src/e2e/alter_table.rs @@ -0,0 +1,953 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, StatementType}; + +use crate::{ + TesterError, + suite::{Suite, default_client}, +}; + +use super::response_helpers::{ + expect_error, extract_f64, extract_i32, extract_string, validate_non_select_statement, + validate_select_query, +}; + +pub struct AlterTableE2ETest; + +pub struct Setup { + pub database_name: String, +} + +pub struct Test { + pub database_name: String, +} + +pub struct Cleanup { + pub database_name: String, +} + +pub struct E2ETestResult { + pub tests_passed: usize, +} + +impl Suite for AlterTableE2ETest { + type SetupArgs = Setup; + + async fn setup(args: &Self::SetupArgs) -> Result<(), TesterError> { + info!("Creating database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::CreateDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database created"); + Ok(()) + } + + type TestArgs = Test; + + async fn run(args: &Self::TestArgs) -> Result { + let mut tests_passed = 0; + + // Test 1: Rename table + info!("\n=== Test 1: Rename table ==="); + if let Err(e) = test_rename_table(args).await { + error!("Test 1 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 1: Rename table passed"); + tests_passed += 1; + + // Test 2: Rename table with data + info!("\n=== Test 2: Rename table with data ==="); + if let Err(e) = test_rename_table_with_data(args).await { + error!("Test 2 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 2: Rename table with data passed"); + tests_passed += 1; + + // Test 3: Rename column + info!("\n=== Test 3: Rename column ==="); + if let Err(e) = test_rename_column(args).await { + error!("Test 3 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 3: Rename column passed"); + tests_passed += 1; + + // Test 4: Rename column with data + info!("\n=== Test 4: Rename column with data ==="); + if let Err(e) = test_rename_column_with_data(args).await { + error!("Test 4 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 4: Rename column with data passed"); + tests_passed += 1; + + // Test 5: Add column to empty table + info!("\n=== Test 5: Add column to empty table ==="); + if let Err(e) = test_add_column_empty_table(args).await { + error!("Test 5 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 5: Add column to empty table passed"); + tests_passed += 1; + + // Test 6: Add column to table with data + info!("\n=== Test 6: Add column to table with data ==="); + if let Err(e) = test_add_column_with_data(args).await { + error!("Test 6 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 6: Add column to table with data passed"); + tests_passed += 1; + + // Test 7: Add multiple columns + info!("\n=== Test 7: Add multiple columns ==="); + if let Err(e) = test_add_multiple_columns(args).await { + error!("Test 7 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 7: Add multiple columns passed"); + tests_passed += 1; + + // Test 8: Drop column from empty table + info!("\n=== Test 8: Drop column from empty table ==="); + if let Err(e) = test_drop_column_empty_table(args).await { + error!("Test 8 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 8: Drop column from empty table passed"); + tests_passed += 1; + + // Test 9: Drop column from table with data + info!("\n=== Test 9: Drop column from table with data ==="); + if let Err(e) = test_drop_column_with_data(args).await { + error!("Test 9 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 9: Drop column from table with data passed"); + tests_passed += 1; + + // Test 10: Drop multiple columns + info!("\n=== Test 10: Drop multiple columns ==="); + if let Err(e) = test_drop_multiple_columns(args).await { + error!("Test 10 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 10: Drop multiple columns passed"); + tests_passed += 1; + + Ok(E2ETestResult { tests_passed }) + } + + type CleanupArgs = Cleanup; + + async fn cleanup(args: &Self::CleanupArgs) -> Result<(), TesterError> { + info!("Deleting database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::DeleteDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database deleted"); + Ok(()) + } +} + +/// Test 1: Rename table +async fn test_rename_table(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_sql = "CREATE TABLE old_name (id INT32 PRIMARY_KEY, value INT64);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Rename table + let rename_sql = "ALTER TABLE old_name RENAME TABLE TO new_name;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: rename_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Verify old name doesn't exist + let select_old_sql = "SELECT * FROM old_name;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_old_sql.to_string(), + }) + .await?; + + expect_error(&mut client).await?; + info!("✓ Old table name correctly not found"); + + // Verify new name works + let select_new_sql = "SELECT * FROM new_name;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_new_sql.to_string(), + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("value", ColumnType::I64)]; + validate_select_query(&mut client, &expected_columns).await?; + + // Cleanup + let drop_sql = "DROP TABLE new_name;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + + info!("✓ Table renamed successfully"); + Ok(()) +} + +/// Test 2: Rename table with data +async fn test_rename_table_with_data(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_sql = "CREATE TABLE users (id INT32 PRIMARY_KEY, name STRING);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Insert data + info!("Inserting 100 records..."); + for i in 0..100 { + let insert_sql = format!("INSERT INTO users (id, name) VALUES ({}, 'user{}');", i, i); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + } + + // Rename table + let rename_sql = "ALTER TABLE users RENAME TABLE TO accounts;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: rename_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Verify data is still there with new name + let select_sql = "SELECT * FROM accounts ORDER BY id ASC;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("name", ColumnType::String)]; + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != 100 { + return Err(TesterError::ServerError { + message: format!("Expected 100 records but got {}", records.len()), + }); + } + + // Verify all data + for (idx, record) in records.iter().enumerate() { + let id = extract_i32(record, 0)?; + let name = extract_string(record, 1)?; + + let expected_id = idx as i32; + let expected_name = format!("user{}", idx); + + if id != expected_id || name != expected_name { + return Err(TesterError::ServerError { + message: format!( + "Record {} mismatch: got ({}, {}), expected ({}, {})", + idx, id, name, expected_id, expected_name + ), + }); + } + } + + // Cleanup + let drop_sql = "DROP TABLE accounts;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + + info!("✓ Table with data renamed and verified successfully"); + Ok(()) +} + +/// Test 3: Rename column +async fn test_rename_column(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_sql = "CREATE TABLE test_rename (id INT32 PRIMARY_KEY, old_col STRING);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Rename column + let rename_sql = "ALTER TABLE test_rename RENAME COLUMN old_col TO new_col;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: rename_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Verify new column name works + let select_sql = "SELECT * FROM test_rename;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("new_col", ColumnType::String)]; + validate_select_query(&mut client, &expected_columns).await?; + + // Cleanup + let drop_sql = "DROP TABLE test_rename;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + + info!("✓ Column renamed successfully"); + Ok(()) +} + +/// Test 4: Rename column with data +async fn test_rename_column_with_data(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_sql = "CREATE TABLE products (id INT32 PRIMARY_KEY, price FLOAT64);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Insert data + info!("Inserting 50 records..."); + for i in 0..50 { + let insert_sql = format!( + "INSERT INTO products (id, price) VALUES ({}, {:.2});", + i, + (i as f64) * 10.5 + ); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + } + + // Rename column + let rename_sql = "ALTER TABLE products RENAME COLUMN price TO cost;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: rename_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Verify data with new column name + let select_sql = "SELECT * FROM products ORDER BY id ASC;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("cost", ColumnType::F64)]; + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != 50 { + return Err(TesterError::ServerError { + message: format!("Expected 50 records but got {}", records.len()), + }); + } + + // Verify all data + for (idx, record) in records.iter().enumerate() { + let id = extract_i32(record, 0)?; + let cost = extract_f64(record, 1)?; + + let expected_id = idx as i32; + let expected_cost = (idx as f64) * 10.5; + + if id != expected_id || (cost - expected_cost).abs() > 0.001 { + return Err(TesterError::ServerError { + message: format!( + "Record {} mismatch: got ({}, {}), expected ({}, {})", + idx, id, cost, expected_id, expected_cost + ), + }); + } + } + + // Cleanup + let drop_sql = "DROP TABLE products;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + + info!("✓ Column with data renamed and verified successfully"); + Ok(()) +} + +/// Test 5: Add column to empty table +async fn test_add_column_empty_table(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_sql = "CREATE TABLE test_add (id INT32 PRIMARY_KEY);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Add column + let add_sql = "ALTER TABLE test_add ADD COLUMN name STRING;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: add_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Verify new column exists + let select_sql = "SELECT * FROM test_add;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("name", ColumnType::String)]; + validate_select_query(&mut client, &expected_columns).await?; + + // Cleanup + let drop_sql = "DROP TABLE test_add;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + + info!("✓ Column added to empty table successfully"); + Ok(()) +} + +/// Test 6: Add column to table with data +async fn test_add_column_with_data(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_sql = "CREATE TABLE employees (id INT32 PRIMARY_KEY, name STRING);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Insert data + info!("Inserting 75 records..."); + for i in 0..75 { + let insert_sql = format!( + "INSERT INTO employees (id, name) VALUES ({}, 'employee{}');", + i, i + ); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + } + + // Add column + let add_sql = "ALTER TABLE employees ADD COLUMN salary FLOAT64;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: add_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Verify new column exists and old data is intact + // Note: String columns are always at the end, so order is: id, salary, name + let select_sql = "SELECT * FROM employees ORDER BY id ASC;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![ + ("id", ColumnType::I32), + ("salary", ColumnType::F64), + ("name", ColumnType::String), + ]; + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != 75 { + return Err(TesterError::ServerError { + message: format!("Expected 75 records but got {}", records.len()), + }); + } + + // Verify all data (old columns should be intact, new column should be default) + for (idx, record) in records.iter().enumerate() { + let id = extract_i32(record, 0)?; + let name = extract_string(record, 2)?; // name is now at index 2 + + let expected_id = idx as i32; + let expected_name = format!("employee{}", idx); + + if id != expected_id || name != expected_name { + return Err(TesterError::ServerError { + message: format!( + "Record {} mismatch: got ({}, {}), expected ({}, {})", + idx, id, name, expected_id, expected_name + ), + }); + } + } + + // Now insert a record with the new column + let insert_new_sql = + "INSERT INTO employees (id, name, salary) VALUES (100, 'new_employee', 50000.0);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_new_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + + // Verify the new record + let select_new_sql = "SELECT * FROM employees WHERE id = 100;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_new_sql.to_string(), + }) + .await?; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != 1 { + return Err(TesterError::ServerError { + message: format!("Expected 1 record but got {}", records.len()), + }); + } + + let salary = extract_f64(&records[0], 1)?; // salary is now at index 1 + + if (salary - 50000.0).abs() > 0.001 { + return Err(TesterError::ServerError { + message: format!("Expected salary 50000.0 but got {}", salary), + }); + } + + // Cleanup + let drop_sql = "DROP TABLE employees;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + + info!("✓ Column added to table with data and verified successfully"); + Ok(()) +} + +/// Test 7: Add multiple columns +async fn test_add_multiple_columns(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_sql = "CREATE TABLE multi_add (id INT32 PRIMARY_KEY);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Add first column + let add_sql1 = "ALTER TABLE multi_add ADD COLUMN col1 STRING;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: add_sql1.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Add second column + let add_sql2 = "ALTER TABLE multi_add ADD COLUMN col2 INT64;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: add_sql2.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Add third column + let add_sql3 = "ALTER TABLE multi_add ADD COLUMN col3 BOOL;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: add_sql3.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Verify all columns exist + // Note: String columns are always at the end, so order is: id, col2, col3, col1 + let select_sql = "SELECT * FROM multi_add;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![ + ("id", ColumnType::I32), + ("col2", ColumnType::I64), + ("col3", ColumnType::Bool), + ("col1", ColumnType::String), + ]; + validate_select_query(&mut client, &expected_columns).await?; + + // Cleanup + let drop_sql = "DROP TABLE multi_add;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + + info!("✓ Multiple columns added successfully"); + Ok(()) +} + +/// Test 8: Drop column from empty table +async fn test_drop_column_empty_table(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table with two columns + let create_sql = "CREATE TABLE test_drop (id INT32 PRIMARY_KEY, old_col STRING);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Drop column + let drop_sql = "ALTER TABLE test_drop DROP COLUMN old_col;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Verify column is gone + let select_sql = "SELECT * FROM test_drop;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32)]; + validate_select_query(&mut client, &expected_columns).await?; + + // Cleanup + let drop_table_sql = "DROP TABLE test_drop;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + + info!("✓ Column dropped from empty table successfully"); + Ok(()) +} + +/// Test 9: Drop column from table with data +async fn test_drop_column_with_data(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_sql = "CREATE TABLE orders (id INT32 PRIMARY_KEY, product STRING, quantity INT32);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Insert data + info!("Inserting 60 records..."); + for i in 0..60 { + let insert_sql = format!( + "INSERT INTO orders (id, product, quantity) VALUES ({}, 'product{}', {});", + i, + i, + i * 10 + ); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + } + + // Drop column + let drop_sql = "ALTER TABLE orders DROP COLUMN quantity;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Verify column is gone and other data is intact + let select_sql = "SELECT * FROM orders ORDER BY id ASC;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("product", ColumnType::String)]; + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != 60 { + return Err(TesterError::ServerError { + message: format!("Expected 60 records but got {}", records.len()), + }); + } + + // Verify remaining data + for (idx, record) in records.iter().enumerate() { + let id = extract_i32(record, 0)?; + let product = extract_string(record, 1)?; + + let expected_id = idx as i32; + let expected_product = format!("product{}", idx); + + if id != expected_id || product != expected_product { + return Err(TesterError::ServerError { + message: format!( + "Record {} mismatch: got ({}, {}), expected ({}, {})", + idx, id, product, expected_id, expected_product + ), + }); + } + } + + // Cleanup + let drop_table_sql = "DROP TABLE orders;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + + info!("✓ Column dropped from table with data and verified successfully"); + Ok(()) +} + +/// Test 10: Drop multiple columns +async fn test_drop_multiple_columns(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table with many columns (string at the end) + let create_sql = "CREATE TABLE multi_drop (id INT32 PRIMARY_KEY, col2 INT64, col3 BOOL, col4 FLOAT32, col1 STRING);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Drop first column + let drop_sql1 = "ALTER TABLE multi_drop DROP COLUMN col2;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql1.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Drop second column + let drop_sql2 = "ALTER TABLE multi_drop DROP COLUMN col4;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql2.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::AlterTable).await?; + + // Verify only remaining columns exist (string still at the end) + let select_sql = "SELECT * FROM multi_drop;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![ + ("id", ColumnType::I32), + ("col3", ColumnType::Bool), + ("col1", ColumnType::String), + ]; + validate_select_query(&mut client, &expected_columns).await?; + + // Cleanup + let drop_table_sql = "DROP TABLE multi_drop;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + + info!("✓ Multiple columns dropped successfully"); + Ok(()) +} diff --git a/tester/src/e2e/create_table.rs b/tester/src/e2e/create_table.rs new file mode 100644 index 0000000..36ef832 --- /dev/null +++ b/tester/src/e2e/create_table.rs @@ -0,0 +1,381 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, Response, StatementType}; + +use crate::{ + TesterError, + client::ReadResult, + suite::{E2ETestResult, Suite, default_client}, +}; + +use super::response_helpers::{ + expect_acknowledge, validate_non_select_statement, validate_select_query, +}; + +pub struct CreateTableE2ETest; + +pub struct Setup { + pub database_name: String, +} + +pub struct Test { + pub database_name: String, +} + +pub struct Cleanup { + pub database_name: String, +} + +impl Suite for CreateTableE2ETest { + type SetupArgs = Setup; + + async fn setup(args: &Self::SetupArgs) -> Result<(), TesterError> { + info!("Creating database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::CreateDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database created"); + Ok(()) + } + + type TestArgs = Test; + + async fn run(args: &Self::TestArgs) -> Result { + let mut tests_passed = 0; + + // Test 1: Create simple table with one column + info!("\n=== Test 1: Create simple table with one column ==="); + if let Err(e) = test_create_simple_table(args).await { + error!("Test 1 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 1: Create simple table passed"); + tests_passed += 1; + + // Test 2: Create table with primary key + info!("\n=== Test 2: Create table with primary key ==="); + if let Err(e) = test_create_table_with_primary_key(args).await { + error!("Test 2 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 2: Create table with primary key passed"); + tests_passed += 1; + + // Test 3: Create table with all data types + info!("\n=== Test 3: Create table with all data types ==="); + if let Err(e) = test_create_table_all_types(args).await { + error!("Test 3 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 3: Create table with all data types passed"); + tests_passed += 1; + + // Test 4: Create table with multiple columns + info!("\n=== Test 4: Create table with multiple columns ==="); + if let Err(e) = test_create_table_multiple_columns(args).await { + error!("Test 4 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 4: Create table with multiple columns passed"); + tests_passed += 1; + + // Test 5: Create multiple tables in same database + info!("\n=== Test 5: Create multiple tables in same database ==="); + if let Err(e) = test_create_multiple_tables(args).await { + error!("Test 5 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 5: Create multiple tables passed"); + tests_passed += 1; + + // Test 6: Create table that already exists (should fail) + info!("\n=== Test 6: Create table that already exists (should fail) ==="); + if let Err(e) = test_create_duplicate_table(args).await { + error!("Test 6 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 6: Duplicate table creation correctly rejected"); + tests_passed += 1; + + // Test 7: Verify table by inserting and selecting data + info!("\n=== Test 7: Verify table by inserting and selecting data ==="); + if let Err(e) = test_verify_table_with_data(args).await { + error!("Test 7 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 7: Table verification with data passed"); + tests_passed += 1; + + Ok(E2ETestResult { tests_passed }) + } + + type CleanupArgs = Cleanup; + + async fn cleanup(args: &Self::CleanupArgs) -> Result<(), TesterError> { + info!("Deleting database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::DeleteDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database deleted"); + Ok(()) + } +} + +/// Test 1: Create simple table with one column +async fn test_create_simple_table(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let create_table_sql = "CREATE TABLE simple_table (id INT32 PRIMARY_KEY);"; + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + info!("✓ Simple table created successfully"); + Ok(()) +} + +/// Test 2: Create table with primary key +async fn test_create_table_with_primary_key(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let create_table_sql = "CREATE TABLE pk_table (user_id INT64 PRIMARY_KEY, name STRING);"; + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + info!("✓ Table with primary key created successfully"); + Ok(()) +} + +/// Test 3: Create table with all data types +async fn test_create_table_all_types(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let create_table_sql = "CREATE TABLE all_types_table (\ + id INT32 PRIMARY_KEY, \ + big_num INT64, \ + small_price FLOAT32, \ + big_price FLOAT64, \ + is_active BOOL, \ + birth_date DATE, \ + created_at DATETIME, \ + description STRING\ + );"; + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + info!("✓ Table with all data types created successfully"); + Ok(()) +} + +/// Test 4: Create table with multiple columns +async fn test_create_table_multiple_columns(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let create_table_sql = "CREATE TABLE multi_column_table (\ + id INT32 PRIMARY_KEY, \ + col1 STRING, \ + col2 STRING, \ + col3 STRING, \ + col4 STRING, \ + col5 STRING\ + );"; + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + info!("✓ Table with multiple columns created successfully"); + Ok(()) +} + +/// Test 5: Create multiple tables in same database +async fn test_create_multiple_tables(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create first table + let create_table1_sql = "CREATE TABLE users (id INT32 PRIMARY_KEY, name STRING);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table1_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Create second table + let create_table2_sql = "CREATE TABLE orders (order_id INT32 PRIMARY_KEY, amount FLOAT32);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table2_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Create third table + let create_table3_sql = "CREATE TABLE products (product_id INT32 PRIMARY_KEY, price FLOAT64);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table3_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + info!("✓ Multiple tables created successfully"); + Ok(()) +} + +/// Test 6: Create table that already exists (should fail) +async fn test_create_duplicate_table(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // First, create a table + let create_table_sql = "CREATE TABLE duplicate_test (id INT32 PRIMARY_KEY);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Now try to create the same table again (should fail) + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql.to_string(), + }) + .await?; + + // We expect an error here + match expect_acknowledge(&mut client).await { + Ok(_) => { + // If we got acknowledge, check if we get an error in the next response + match client.read_response().await? { + ReadResult::Response(Response::Error { message, .. }) => { + info!( + "✓ Got expected error when creating duplicate table: {}", + message + ); + Ok(()) + } + ReadResult::Response(Response::StatementCompleted { .. }) => { + error!("Creating duplicate table should have failed but succeeded!"); + Err(TesterError::ServerError { + message: "Creating duplicate table should have failed but succeeded" + .to_string(), + }) + } + _ => { + error!("Unexpected response type when creating duplicate table"); + Err(TesterError::ServerError { + message: "Unexpected response type when creating duplicate table" + .to_string(), + }) + } + } + } + Err(_) => { + // Got error immediately, that's good + info!("✓ Duplicate table creation correctly rejected"); + Ok(()) + } + } +} + +/// Test 7: Verify table by inserting and selecting data +async fn test_verify_table_with_data(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_table_sql = "CREATE TABLE verification_table (\ + id INT32 PRIMARY_KEY, \ + value INT64, \ + name STRING\ + );"; + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Insert some data + let insert_sql = "INSERT INTO verification_table (id, value, name) VALUES (1, 1000, 'Test');"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + + // Select the data back + let select_sql = "SELECT * FROM verification_table;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![ + ("id", ColumnType::I32), + ("value", ColumnType::I64), + ("name", ColumnType::String), + ]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != 1 { + return Err(TesterError::ServerError { + message: format!("Expected 1 record but got {}", records.len()), + }); + } + + info!("✓ Table verified with data successfully"); + Ok(()) +} diff --git a/tester/src/e2e/delete.rs b/tester/src/e2e/delete.rs new file mode 100644 index 0000000..785df80 --- /dev/null +++ b/tester/src/e2e/delete.rs @@ -0,0 +1,876 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, StatementType}; + +use crate::{ + TesterError, + suite::{E2ETestResult, Suite, default_client}, +}; + +use super::response_helpers::{validate_non_select_statement, validate_select_query}; + +/// Test record structure for DELETE tests +#[derive(Debug, Clone)] +pub struct DeleteTestRecord { + pub id: i32, + pub big_id: i64, + pub price: f32, + pub precise_price: f64, + pub active: bool, + pub birth_date: String, + pub last_login: String, + pub name: String, +} + +impl DeleteTestRecord { + /// Generate test records + pub fn generate(num_records: usize) -> Vec { + (0..num_records) + .map(|i| DeleteTestRecord { + id: i as i32, + big_id: (i as i64) * 1000, + price: (i as f32) * 1.5, + precise_price: (i as f64) * 2.75, + active: i % 2 == 0, + birth_date: format!("2024-01-{:02}", (i % 28) + 1), + last_login: format!("2024-01-{:02}T12:00:00", (i % 28) + 1), + name: format!("User_{}", i), + }) + .collect() + } + + /// Verify this record does NOT exist in the database + pub async fn verify_not_in_db( + &self, + database_name: &str, + table_name: &str, + ) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let sql = format!("SELECT * FROM {} WHERE id = {};", table_name, self.id); + client + .send_request(&Request::Query { + database_name: Some(database_name.to_string()), + sql, + }) + .await?; + + let expected_columns = vec![ + ("id", ColumnType::I32), + ("big_id", ColumnType::I64), + ("price", ColumnType::F32), + ("precise_price", ColumnType::F64), + ("active", ColumnType::Bool), + ("birth_date", ColumnType::Date), + ("last_login", ColumnType::DateTime), + ("name", ColumnType::String), + ]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + if !records.is_empty() { + return Err(TesterError::ServerError { + message: format!( + "Expected record with id={} to be deleted but it still exists", + self.id + ), + }); + } + + Ok(()) + } + + /// Verify this record still exists in the database + pub async fn verify_still_exists( + &self, + database_name: &str, + table_name: &str, + ) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let sql = format!("SELECT id FROM {} WHERE id = {};", table_name, self.id); + client + .send_request(&Request::Query { + database_name: Some(database_name.to_string()), + sql, + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32)]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.is_empty() { + return Err(TesterError::ServerError { + message: format!( + "Expected record with id={} to still exist but it was deleted", + self.id + ), + }); + } + + Ok(()) + } +} + +/// Helper function to count all records in the table +async fn count_records(database_name: &str, table_name: &str) -> Result { + let mut client = default_client().await?; + + let sql = format!("SELECT id FROM {};", table_name); + client + .send_request(&Request::Query { + database_name: Some(database_name.to_string()), + sql, + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32)]; + let records = validate_select_query(&mut client, &expected_columns).await?; + + Ok(records.len()) +} + +pub struct DeleteE2ETest; + +pub struct Setup { + pub database_name: String, + pub table_name: String, + pub num_records: usize, +} + +pub struct Test { + pub database_name: String, + pub table_name: String, + pub test_data: Vec, +} + +pub struct Cleanup { + pub database_name: String, +} + +impl Suite for DeleteE2ETest { + type SetupArgs = Setup; + + async fn setup(args: &Self::SetupArgs) -> Result<(), TesterError> { + info!("Creating database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::CreateDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database created"); + + // Create table with all data types + let create_table_sql = format!( + "CREATE TABLE {} (\ + id INT32 PRIMARY_KEY, \ + big_id INT64, \ + price FLOAT32, \ + precise_price FLOAT64, \ + active BOOL, \ + birth_date DATE, \ + last_login DATETIME, \ + name STRING\ + );", + args.table_name + ); + + info!("Creating table with all data types..."); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + info!("✓ Table created"); + + // Generate and insert test data + let test_data = DeleteTestRecord::generate(args.num_records); + + info!("Inserting {} records...", test_data.len()); + for record in &test_data { + let insert_sql = format!( + "INSERT INTO {} (id, big_id, price, precise_price, active, birth_date, last_login, name) \ + VALUES ({}, {}, {:.1}, {:.2}, {}, '{}', '{}', '{}');", + args.table_name, + record.id, + record.big_id, + record.price, + record.precise_price, + record.active, + record.birth_date, + record.last_login, + record.name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + } + + info!("✓ {} records inserted", test_data.len()); + Ok(()) + } + + type TestArgs = Test; + + async fn run(args: &Self::TestArgs) -> Result { + let mut tests_passed = 0; + + // Test 1: Delete one record by primary key + info!("\n=== Test 1: Delete one record by primary key ==="); + if let Err(e) = test_delete_one_record(args).await { + error!("Test 1 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 1: Delete one record passed"); + tests_passed += 1; + + // Test 2: Delete no records (WHERE matches nothing) + info!("\n=== Test 2: Delete no records (WHERE matches nothing) ==="); + if let Err(e) = test_delete_no_records(args).await { + error!("Test 2 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 2: Delete no records passed"); + tests_passed += 1; + + // Test 3: Delete records by INT64 field + info!("\n=== Test 3: Delete records by INT64 field ==="); + if let Err(e) = test_delete_by_int64(args).await { + error!("Test 3 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 3: Delete by INT64 passed"); + tests_passed += 1; + + // Test 4: Delete records by FLOAT32 field + info!("\n=== Test 4: Delete records by FLOAT32 field ==="); + if let Err(e) = test_delete_by_float32(args).await { + error!("Test 4 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 4: Delete by FLOAT32 passed"); + tests_passed += 1; + + // Test 5: Delete records by BOOL field + info!("\n=== Test 5: Delete records by BOOL field ==="); + if let Err(e) = test_delete_by_bool(args).await { + error!("Test 5 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 5: Delete by BOOL passed"); + tests_passed += 1; + + // Test 6: Delete records by DATE field + info!("\n=== Test 6: Delete records by DATE field ==="); + if let Err(e) = test_delete_by_date(args).await { + error!("Test 6 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 6: Delete by DATE passed"); + tests_passed += 1; + + // Test 7: Delete records by STRING field + info!("\n=== Test 7: Delete records by STRING field ==="); + if let Err(e) = test_delete_by_string(args).await { + error!("Test 7 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 7: Delete by STRING passed"); + tests_passed += 1; + + // Test 8: Delete range of records with WHERE clause + info!("\n=== Test 8: Delete range of records with WHERE ==="); + if let Err(e) = test_delete_range(args).await { + error!("Test 8 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 8: Delete range passed"); + tests_passed += 1; + + // Test 9: Delete with complex WHERE clause + info!("\n=== Test 9: Delete with complex WHERE clause ==="); + if let Err(e) = test_delete_complex_where(args).await { + error!("Test 9 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 9: Delete with complex WHERE passed"); + tests_passed += 1; + + // Test 10: Delete ALL records (no WHERE clause) - runs last + info!("\n=== Test 10: Delete ALL records (no WHERE) ==="); + if let Err(e) = test_delete_all_records(args).await { + error!("Test 10 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 10: Delete ALL records passed"); + tests_passed += 1; + + Ok(E2ETestResult { tests_passed }) + } + + type CleanupArgs = Cleanup; + + async fn cleanup(args: &Self::CleanupArgs) -> Result<(), TesterError> { + info!("Deleting database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::DeleteDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database deleted"); + Ok(()) + } +} + +/// Test 1: Delete one record by primary key +async fn test_delete_one_record(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Count records before deletion + let count_before = count_records(&args.database_name, &args.table_name).await?; + + // Delete record with id = 100 + let delete_sql = format!("DELETE FROM {} WHERE id = 100;", args.table_name); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: delete_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Delete).await?; + + // Verify record count decreased by exactly 1 + let count_after = count_records(&args.database_name, &args.table_name).await?; + if count_after != count_before - 1 { + return Err(TesterError::ServerError { + message: format!( + "Expected {} records after deletion but got {}", + count_before - 1, + count_after + ), + }); + } + + // Verify the record is deleted + args.test_data[100] + .verify_not_in_db(&args.database_name, &args.table_name) + .await?; + + // Verify adjacent records still exist + args.test_data[99] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + args.test_data[101] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + + info!("✓ Single record deleted and verified"); + Ok(()) +} + +/// Test 2: Delete no records (WHERE matches nothing) +async fn test_delete_no_records(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Count records before deletion + let count_before = count_records(&args.database_name, &args.table_name).await?; + + // Try to delete record with id that doesn't exist + let delete_sql = format!("DELETE FROM {} WHERE id = 999999;", args.table_name); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: delete_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::Delete).await?; + + // Verify record count stayed the same + let count_after = count_records(&args.database_name, &args.table_name).await?; + if count_after != count_before { + return Err(TesterError::ServerError { + message: format!( + "Expected {} records after no-op deletion but got {}", + count_before, count_after + ), + }); + } + + // Verify some records still exist + args.test_data[0] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + args.test_data[500] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + + info!("✓ No records deleted as expected"); + Ok(()) +} + +/// Test 3: Delete records by INT64 field +async fn test_delete_by_int64(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Count records before deletion + let count_before = count_records(&args.database_name, &args.table_name).await?; + + // Delete records where big_id >= 200000 AND big_id < 210000 + // This corresponds to ids 200-209 (10 records) + let delete_sql = format!( + "DELETE FROM {} WHERE big_id >= 200000 AND big_id < 210000;", + args.table_name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: delete_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 10, StatementType::Delete).await?; + + // Verify record count decreased by exactly 10 + let count_after = count_records(&args.database_name, &args.table_name).await?; + if count_after != count_before - 10 { + return Err(TesterError::ServerError { + message: format!( + "Expected {} records after deletion but got {}", + count_before - 10, + count_after + ), + }); + } + + // Verify ALL 10 records are deleted + info!("Verifying all 10 deleted records..."); + for id in 200..210 { + args.test_data[id] + .verify_not_in_db(&args.database_name, &args.table_name) + .await?; + } + + // Verify records outside the range still exist + args.test_data[199] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + args.test_data[210] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + + info!("✓ Records deleted by INT64 field verified"); + Ok(()) +} + +/// Test 4: Delete records by FLOAT32 field +async fn test_delete_by_float32(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Count records before deletion + let count_before = count_records(&args.database_name, &args.table_name).await?; + + // Delete records where price >= 450.0 AND price < 465.0 + // This corresponds to ids 300-309 (10 records, since price = id * 1.5) + let delete_sql = format!( + "DELETE FROM {} WHERE price >= 450.0 AND price < 465.0;", + args.table_name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: delete_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 10, StatementType::Delete).await?; + + // Verify record count decreased by exactly 10 + let count_after = count_records(&args.database_name, &args.table_name).await?; + if count_after != count_before - 10 { + return Err(TesterError::ServerError { + message: format!( + "Expected {} records after deletion but got {}", + count_before - 10, + count_after + ), + }); + } + + // Verify ALL 10 records are deleted + info!("Verifying all 10 deleted records..."); + for id in 300..310 { + args.test_data[id] + .verify_not_in_db(&args.database_name, &args.table_name) + .await?; + } + + // Verify records outside the range still exist + args.test_data[299] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + args.test_data[310] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + + info!("✓ Records deleted by FLOAT32 field verified"); + Ok(()) +} + +/// Test 5: Delete records by BOOL field +async fn test_delete_by_bool(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Count records before deletion + let count_before = count_records(&args.database_name, &args.table_name).await?; + + // Delete records where active = FALSE AND id >= 400 AND id < 450 + // This will delete odd ids in that range (25 records) + let delete_sql = format!( + "DELETE FROM {} WHERE active = FALSE AND id >= 400 AND id < 450;", + args.table_name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: delete_sql, + }) + .await?; + + let expected_deletes = (400..450).filter(|id| id % 2 != 0).count(); + validate_non_select_statement(&mut client, expected_deletes, StatementType::Delete).await?; + + // Verify record count decreased by expected amount + let count_after = count_records(&args.database_name, &args.table_name).await?; + if count_after != count_before - expected_deletes { + return Err(TesterError::ServerError { + message: format!( + "Expected {} records after deletion but got {}", + count_before - expected_deletes, + count_after + ), + }); + } + + // Verify ALL odd records in range are deleted + info!("Verifying all {} deleted records...", expected_deletes); + for id in (400..450).filter(|id| id % 2 != 0) { + args.test_data[id] + .verify_not_in_db(&args.database_name, &args.table_name) + .await?; + } + + // Verify even records in range still exist + info!("Verifying {} records still exist...", 25); + for id in (400..450).filter(|id| id % 2 == 0) { + args.test_data[id] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + } + + info!("✓ Records deleted by BOOL field verified"); + Ok(()) +} + +/// Test 6: Delete records by DATE field +async fn test_delete_by_date(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Count records before deletion + let count_before = count_records(&args.database_name, &args.table_name).await?; + + // Delete records where birth_date = '2024-01-15' + // Since birth_date cycles every 28 days, this will delete multiple records + let delete_sql = format!( + "DELETE FROM {} WHERE birth_date = '2024-01-15' AND id >= 500 AND id < 600;", + args.table_name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: delete_sql, + }) + .await?; + + // Calculate expected: ids where (id % 28) + 1 == 15, i.e., id % 28 == 14 + let expected_deletes = (500..600).filter(|id| id % 28 == 14).count(); + validate_non_select_statement(&mut client, expected_deletes, StatementType::Delete).await?; + + // Verify record count decreased by expected amount + let count_after = count_records(&args.database_name, &args.table_name).await?; + if count_after != count_before - expected_deletes { + return Err(TesterError::ServerError { + message: format!( + "Expected {} records after deletion but got {}", + count_before - expected_deletes, + count_after + ), + }); + } + + // Verify ALL matching records are deleted + info!("Verifying all {} deleted records...", expected_deletes); + for id in (500..600).filter(|id| id % 28 == 14) { + args.test_data[id] + .verify_not_in_db(&args.database_name, &args.table_name) + .await?; + } + + // Verify some non-matching records still exist + for id in (500..600).filter(|id| id % 28 != 14).step_by(10) { + args.test_data[id] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + } + + info!("✓ Records deleted by DATE field verified"); + Ok(()) +} + +/// Test 7: Delete records by STRING field +async fn test_delete_by_string(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Count records before deletion + let count_before = count_records(&args.database_name, &args.table_name).await?; + + // Delete specific records by name pattern (ids 700-709) + let mut delete_count = 0; + for id in 700..710 { + let delete_sql = format!( + "DELETE FROM {} WHERE name = 'User_{}';", + args.table_name, id + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: delete_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Delete).await?; + delete_count += 1; + } + + // Verify record count decreased by exactly delete_count + let count_after = count_records(&args.database_name, &args.table_name).await?; + if count_after != count_before - delete_count { + return Err(TesterError::ServerError { + message: format!( + "Expected {} records after deletion but got {}", + count_before - delete_count, + count_after + ), + }); + } + + // Verify ALL 10 records are deleted + info!("Verifying all {} deleted records...", delete_count); + for id in 700..710 { + args.test_data[id] + .verify_not_in_db(&args.database_name, &args.table_name) + .await?; + } + + // Verify adjacent records still exist + args.test_data[699] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + args.test_data[710] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + + info!("✓ Records deleted by STRING field verified"); + Ok(()) +} + +/// Test 8: Delete range of records with WHERE clause +async fn test_delete_range(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Count records before deletion + let count_before = count_records(&args.database_name, &args.table_name).await?; + + // Delete records where id >= 1000 AND id < 1200 (200 records) + let delete_sql = format!( + "DELETE FROM {} WHERE id >= 1000 AND id < 1200;", + args.table_name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: delete_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 200, StatementType::Delete).await?; + + // Verify record count decreased by exactly 200 + let count_after = count_records(&args.database_name, &args.table_name).await?; + if count_after != count_before - 200 { + return Err(TesterError::ServerError { + message: format!( + "Expected {} records after deletion but got {}", + count_before - 200, + count_after + ), + }); + } + + // Verify ALL 200 records are deleted + info!("Verifying all 200 deleted records..."); + for id in 1000..1200 { + args.test_data[id] + .verify_not_in_db(&args.database_name, &args.table_name) + .await?; + } + + // Verify records outside the range still exist + args.test_data[999] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + args.test_data[1200] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + + info!("✓ Range delete verified"); + Ok(()) +} + +/// Test 9: Delete with complex WHERE clause +async fn test_delete_complex_where(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Count records before deletion + let count_before = count_records(&args.database_name, &args.table_name).await?; + + // Delete records where id >= 2000 AND id < 2100 AND active = TRUE AND price > 3000.0 + // active = TRUE means even ids + // price > 3000.0 means id > 2000 (since price = id * 1.5) + // So this will delete even ids from 2001 to 2099 where price > 3000.0 + let delete_sql = format!( + "DELETE FROM {} WHERE id >= 2000 AND id < 2100 AND active = TRUE AND price > 3000.0;", + args.table_name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: delete_sql, + }) + .await?; + + // Calculate expected: even ids in range where id * 1.5 > 3000.0, i.e., id > 2000 + let expected_deletes = (2001..2100).filter(|id| id % 2 == 0).count(); + validate_non_select_statement(&mut client, expected_deletes, StatementType::Delete).await?; + + // Verify record count decreased by expected amount + let count_after = count_records(&args.database_name, &args.table_name).await?; + if count_after != count_before - expected_deletes { + return Err(TesterError::ServerError { + message: format!( + "Expected {} records after deletion but got {}", + count_before - expected_deletes, + count_after + ), + }); + } + + // Verify ALL matching records are deleted + info!("Verifying all {} deleted records...", expected_deletes); + for id in (2001..2100).filter(|id| id % 2 == 0) { + args.test_data[id] + .verify_not_in_db(&args.database_name, &args.table_name) + .await?; + } + + // Verify odd records in range still exist + info!("Verifying {} unaffected records...", 50); + for id in (2000..2100).filter(|id| id % 2 != 0) { + args.test_data[id] + .verify_still_exists(&args.database_name, &args.table_name) + .await?; + } + + info!("✓ Complex WHERE clause delete verified"); + Ok(()) +} + +/// Test 10: Delete ALL records (no WHERE clause) +async fn test_delete_all_records(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // First, count how many records are left + let count_sql = format!("SELECT id FROM {};", args.table_name); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: count_sql, + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32)]; + let records_before = validate_select_query(&mut client, &expected_columns).await?; + let remaining_count = records_before.len(); + + info!("Deleting all {} remaining records...", remaining_count); + + // Delete ALL records (no WHERE clause) + let delete_sql = format!("DELETE FROM {};", args.table_name); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: delete_sql, + }) + .await?; + + validate_non_select_statement(&mut client, remaining_count, StatementType::Delete).await?; + + // Verify table is empty + let count_sql = format!("SELECT id FROM {};", args.table_name); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: count_sql, + }) + .await?; + + let records_after = validate_select_query(&mut client, &expected_columns).await?; + + if !records_after.is_empty() { + return Err(TesterError::ServerError { + message: format!( + "Expected table to be empty but found {} records", + records_after.len() + ), + }); + } + + info!("✓ All {} records deleted successfully", remaining_count); + Ok(()) +} diff --git a/tester/src/e2e/drop_table.rs b/tester/src/e2e/drop_table.rs new file mode 100644 index 0000000..86b1353 --- /dev/null +++ b/tester/src/e2e/drop_table.rs @@ -0,0 +1,267 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, StatementType}; + +use crate::{ + TesterError, + suite::{E2ETestResult, Suite, default_client}, +}; + +use super::response_helpers::{expect_error, validate_non_select_statement, validate_select_query}; + +pub struct DropTableE2ETest; + +pub struct Setup { + pub database_name: String, +} + +pub struct Test { + pub database_name: String, +} + +pub struct Cleanup { + pub database_name: String, +} + +impl Suite for DropTableE2ETest { + type SetupArgs = Setup; + + async fn setup(args: &Self::SetupArgs) -> Result<(), TesterError> { + info!("Creating database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::CreateDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database created"); + Ok(()) + } + + type TestArgs = Test; + + async fn run(args: &Self::TestArgs) -> Result { + let mut tests_passed = 0; + + // Test 1: Drop empty table + info!("\n=== Test 1: Drop empty table ==="); + if let Err(e) = test_drop_empty_table(args).await { + error!("Test 1 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 1: Drop empty table passed"); + tests_passed += 1; + + // Test 2: Drop table with data + info!("\n=== Test 2: Drop table with data ==="); + if let Err(e) = test_drop_table_with_data(args).await { + error!("Test 2 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 2: Drop table with data passed"); + tests_passed += 1; + + // Test 3: Drop non-existent table (should fail) + info!("\n=== Test 3: Drop non-existent table (should fail) ==="); + if let Err(e) = test_drop_nonexistent_table(args).await { + error!("Test 3 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 3: Drop non-existent table correctly rejected"); + tests_passed += 1; + + // Test 4: Drop multiple tables + info!("\n=== Test 4: Drop multiple tables ==="); + if let Err(e) = test_drop_multiple_tables(args).await { + error!("Test 4 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 4: Drop multiple tables passed"); + tests_passed += 1; + + Ok(E2ETestResult { tests_passed }) + } + + type CleanupArgs = Cleanup; + + async fn cleanup(args: &Self::CleanupArgs) -> Result<(), TesterError> { + info!("Deleting database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::DeleteDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database deleted"); + Ok(()) + } +} + +/// Test 1: Drop empty table +async fn test_drop_empty_table(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_table_sql = "CREATE TABLE empty_drop (id INT32 PRIMARY_KEY);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Drop table + let drop_sql = "DROP TABLE empty_drop;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + + // Verify table is gone by trying to select from it (should fail) + let select_sql = "SELECT * FROM empty_drop;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + // Expect error since table doesn't exist + expect_error(&mut client).await?; + info!("✓ Table correctly dropped and not found"); + Ok(()) +} + +/// Test 2: Drop table with data +async fn test_drop_table_with_data(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_table_sql = "CREATE TABLE data_drop (id INT32 PRIMARY_KEY, value INT64);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Insert data + info!("Inserting 50 records..."); + for i in 0..50 { + let insert_sql = format!( + "INSERT INTO data_drop (id, value) VALUES ({}, {});", + i, + i * 100 + ); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + } + + // Verify data exists + let select_sql = "SELECT * FROM data_drop;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("value", ColumnType::I64)]; + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != 50 { + return Err(TesterError::ServerError { + message: format!("Expected 50 records before drop but got {}", records.len()), + }); + } + + // Drop table + info!("Dropping table with data..."); + let drop_sql = "DROP TABLE data_drop;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + + info!("✓ Table with data dropped successfully"); + Ok(()) +} + +/// Test 3: Drop non-existent table (should fail) +async fn test_drop_nonexistent_table(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Try to drop table that doesn't exist + let drop_sql = "DROP TABLE nonexistent_table;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql.to_string(), + }) + .await?; + + // We expect an error here + let message = expect_error(&mut client).await?; + info!( + "✓ Got expected error when dropping non-existent table: {}", + message + ); + Ok(()) +} + +/// Test 4: Drop multiple tables +async fn test_drop_multiple_tables(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create three tables + let tables = vec!["table1", "table2", "table3"]; + + for table_name in &tables { + let create_sql = format!("CREATE TABLE {} (id INT32 PRIMARY_KEY);", table_name); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + } + + // Drop all three tables + for table_name in &tables { + let drop_sql = format!("DROP TABLE {};", table_name); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: drop_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::DropTable).await?; + } + + info!("✓ All three tables dropped successfully"); + Ok(()) +} diff --git a/tester/src/e2e/insert.rs b/tester/src/e2e/insert.rs new file mode 100644 index 0000000..50a7b7b --- /dev/null +++ b/tester/src/e2e/insert.rs @@ -0,0 +1,666 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, StatementType}; + +use crate::{ + TesterError, + suite::{E2ETestResult, Suite, default_client}, +}; + +use super::response_helpers::{ + extract_bool, extract_f32, extract_f64, extract_i32, extract_i64, extract_string, + validate_field_count, validate_non_select_statement, validate_select_query, +}; + +/// Test record structure for INSERT tests +#[derive(Debug, Clone)] +pub struct InsertTestRecord { + pub id: i32, + pub big_id: i64, + pub price: f32, + pub precise_price: f64, + pub active: bool, + pub birth_date: String, + pub last_login: String, + pub name: String, +} + +impl InsertTestRecord { + /// Validate that this record exists in the database + pub async fn verify_in_db( + &self, + database_name: &str, + table_name: &str, + ) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let sql = format!("SELECT * FROM {} WHERE id = {};", table_name, self.id); + client + .send_request(&Request::Query { + database_name: Some(database_name.to_string()), + sql, + }) + .await?; + + let expected_columns = vec![ + ("id", ColumnType::I32), + ("big_id", ColumnType::I64), + ("price", ColumnType::F32), + ("precise_price", ColumnType::F64), + ("active", ColumnType::Bool), + ("birth_date", ColumnType::Date), + ("last_login", ColumnType::DateTime), + ("name", ColumnType::String), + ]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != 1 { + error!( + "Expected 1 record with id={} but got {}", + self.id, + records.len() + ); + return Err(TesterError::ServerError { + message: format!( + "Expected 1 record with id={} but got {}", + self.id, + records.len() + ), + }); + } + + let record = &records[0]; + validate_field_count(record, 8)?; + + let id = extract_i32(record, 0)?; + let big_id = extract_i64(record, 1)?; + let price = extract_f32(record, 2)?; + let precise_price = extract_f64(record, 3)?; + let active = extract_bool(record, 4)?; + let name = extract_string(record, 7)?; + + if id != self.id { + return Err(TesterError::ServerError { + message: format!("ID mismatch: expected {}, got {}", self.id, id), + }); + } + if big_id != self.big_id { + return Err(TesterError::ServerError { + message: format!("big_id mismatch: expected {}, got {}", self.big_id, big_id), + }); + } + if (price - self.price).abs() > 0.01 { + return Err(TesterError::ServerError { + message: format!("price mismatch: expected {}, got {}", self.price, price), + }); + } + if (precise_price - self.precise_price).abs() > 0.001 { + return Err(TesterError::ServerError { + message: format!( + "precise_price mismatch: expected {}, got {}", + self.precise_price, precise_price + ), + }); + } + if active != self.active { + return Err(TesterError::ServerError { + message: format!("active mismatch: expected {}, got {}", self.active, active), + }); + } + if name != self.name { + return Err(TesterError::ServerError { + message: format!("name mismatch: expected '{}', got '{}'", self.name, name), + }); + } + + Ok(()) + } +} + +pub struct InsertE2ETest; + +pub struct Setup { + pub database_name: String, + pub table_name: String, +} + +pub struct Test { + pub database_name: String, + pub table_name: String, +} + +pub struct Cleanup { + pub database_name: String, +} + +impl Suite for InsertE2ETest { + type SetupArgs = Setup; + + async fn setup(args: &Self::SetupArgs) -> Result<(), TesterError> { + info!("Creating database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::CreateDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database created"); + + // Create table with all data types + let create_table_sql = format!( + "CREATE TABLE {} (\ + id INT32 PRIMARY_KEY, \ + big_id INT64, \ + price FLOAT32, \ + precise_price FLOAT64, \ + active BOOL, \ + birth_date DATE, \ + last_login DATETIME, \ + name STRING\ + );", + args.table_name + ); + + info!("Creating table with all data types..."); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + info!("✓ Table created"); + + Ok(()) + } + + type TestArgs = Test; + + async fn run(args: &Self::TestArgs) -> Result { + let mut tests_passed = 0; + + // Test 1: Insert single record + info!("\n=== Test 1: Insert single record ==="); + if let Err(e) = test_insert_single_record(args).await { + error!("Test 1 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 1: Insert single record passed"); + tests_passed += 1; + + // Test 2: Insert 100 more records (cumulative: 101) + info!("\n=== Test 2: Insert 100 more records ==="); + if let Err(e) = test_insert_100_records(args).await { + error!("Test 2 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 2: Insert 100 more records passed"); + tests_passed += 1; + + // Test 3: Insert 1000 more records (cumulative: 1101) + info!("\n=== Test 3: Insert 1000 more records ==="); + if let Err(e) = test_insert_1000_records(args).await { + error!("Test 3 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 3: Insert 1000 more records passed"); + tests_passed += 1; + + // Test 4: Insert with different column order + info!("\n=== Test 4: Insert with different column order ==="); + if let Err(e) = test_insert_different_column_order(args).await { + error!("Test 4 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 4: Insert with different column order passed"); + tests_passed += 1; + + // Test 5: Insert with partial columns (omitting some) + info!("\n=== Test 5: Insert with reversed column order ==="); + if let Err(e) = test_insert_reversed_column_order(args).await { + error!("Test 5 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 5: Insert with reversed column order passed"); + tests_passed += 1; + + // Test 6: Insert with random column order + info!("\n=== Test 6: Insert with random column order ==="); + if let Err(e) = test_insert_random_column_order(args).await { + error!("Test 6 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 6: Insert with random column order passed"); + tests_passed += 1; + + Ok(E2ETestResult { tests_passed }) + } + + type CleanupArgs = Cleanup; + + async fn cleanup(args: &Self::CleanupArgs) -> Result<(), TesterError> { + info!("Deleting database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::DeleteDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database deleted"); + Ok(()) + } +} + +/// Test 1: Insert single record and verify it's stored correctly +async fn test_insert_single_record(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let record = InsertTestRecord { + id: 1, + big_id: 1000, + price: 15.5, + precise_price: 27.75, + active: true, + birth_date: "2024-01-15".to_string(), + last_login: "2024-01-15T10:30:00".to_string(), + name: "FirstUser".to_string(), + }; + + let insert_sql = format!( + "INSERT INTO {} (id, big_id, price, precise_price, active, birth_date, last_login, name) \ + VALUES ({}, {}, {:.1}, {:.2}, {}, '{}', '{}', '{}');", + args.table_name, + record.id, + record.big_id, + record.price, + record.precise_price, + record.active, + record.birth_date, + record.last_login, + record.name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + + // Verify the record was inserted correctly + record + .verify_in_db(&args.database_name, &args.table_name) + .await?; + + info!("✓ Single record inserted and verified"); + Ok(()) +} + +/// Test 2: Insert 100 more records and verify all 101 exist +async fn test_insert_100_records(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + let mut inserted_records = Vec::new(); + + // Insert 100 records (id 100-199) + for i in 100..200 { + let record = InsertTestRecord { + id: i, + big_id: (i as i64) * 1000, + price: (i as f32) * 1.5, + precise_price: (i as f64) * 2.75, + active: i % 2 == 0, + birth_date: format!("2024-01-{:02}", ((i % 28) + 1)), + last_login: format!("2024-01-{:02}T12:00:00", ((i % 28) + 1)), + name: format!("User_{}", i), + }; + + let insert_sql = format!( + "INSERT INTO {} (id, big_id, price, precise_price, active, birth_date, last_login, name) \ + VALUES ({}, {}, {:.1}, {:.2}, {}, '{}', '{}', '{}');", + args.table_name, + record.id, + record.big_id, + record.price, + record.precise_price, + record.active, + record.birth_date, + record.last_login, + record.name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + inserted_records.push(record); + } + + info!("✓ 100 records inserted"); + + // Verify total count is 101 (1 from test 1 + 100 from this test) + let mut verify_client = default_client().await?; + let count_sql = format!("SELECT * FROM {};", args.table_name); + verify_client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: count_sql, + }) + .await?; + + let expected_columns = vec![ + ("id", ColumnType::I32), + ("big_id", ColumnType::I64), + ("price", ColumnType::F32), + ("precise_price", ColumnType::F64), + ("active", ColumnType::Bool), + ("birth_date", ColumnType::Date), + ("last_login", ColumnType::DateTime), + ("name", ColumnType::String), + ]; + + let records = validate_select_query(&mut verify_client, &expected_columns).await?; + + if records.len() != 101 { + error!("Expected 101 total records but got {}", records.len()); + return Err(TesterError::ServerError { + message: format!("Expected 101 total records but got {}", records.len()), + }); + } + + // Verify all inserted records + for record in &inserted_records { + record + .verify_in_db(&args.database_name, &args.table_name) + .await?; + } + + info!("✓ All 101 records verified in database"); + Ok(()) +} + +/// Test 3: Insert 1000 more records and verify all 1101 exist +async fn test_insert_1000_records(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + let mut inserted_records = Vec::new(); + + // Insert 1000 records (id 1000-1999) + for i in 1000..2000 { + let record = InsertTestRecord { + id: i, + big_id: (i as i64) * 1000, + price: (i as f32) * 1.5, + precise_price: (i as f64) * 2.75, + active: i % 2 == 0, + birth_date: format!("2024-01-{:02}", ((i % 28) + 1)), + last_login: format!("2024-01-{:02}T12:00:00", ((i % 28) + 1)), + name: format!("User_{}", i), + }; + + let insert_sql = format!( + "INSERT INTO {} (id, big_id, price, precise_price, active, birth_date, last_login, name) \ + VALUES ({}, {}, {:.1}, {:.2}, {}, '{}', '{}', '{}');", + args.table_name, + record.id, + record.big_id, + record.price, + record.precise_price, + record.active, + record.birth_date, + record.last_login, + record.name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + inserted_records.push(record); + } + + info!("✓ 1000 records inserted"); + + // Verify total count is 1101 + let mut verify_client = default_client().await?; + let count_sql = format!("SELECT * FROM {};", args.table_name); + verify_client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: count_sql, + }) + .await?; + + let expected_columns = vec![ + ("id", ColumnType::I32), + ("big_id", ColumnType::I64), + ("price", ColumnType::F32), + ("precise_price", ColumnType::F64), + ("active", ColumnType::Bool), + ("birth_date", ColumnType::Date), + ("last_login", ColumnType::DateTime), + ("name", ColumnType::String), + ]; + + let records = validate_select_query(&mut verify_client, &expected_columns).await?; + + if records.len() != 1101 { + error!("Expected 1101 total records but got {}", records.len()); + return Err(TesterError::ServerError { + message: format!("Expected 1101 total records but got {}", records.len()), + }); + } + + // Verify all inserted records + for record in &inserted_records { + record + .verify_in_db(&args.database_name, &args.table_name) + .await?; + } + + info!("✓ All 1101 records verified in database"); + Ok(()) +} + +/// Test 4: Insert with different column order (swap some columns) +async fn test_insert_different_column_order(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let record = InsertTestRecord { + id: 5000, + big_id: 5000000, + price: 99.9, + precise_price: 199.99, + active: false, + birth_date: "2024-06-15".to_string(), + last_login: "2024-06-15T14:30:00".to_string(), + name: "OrderTest1".to_string(), + }; + + // Different order: name, active, id, price, big_id, precise_price, birth_date, last_login + let insert_sql = format!( + "INSERT INTO {} (name, active, id, price, big_id, precise_price, birth_date, last_login) \ + VALUES ('{}', {}, {}, {:.1}, {}, {:.2}, '{}', '{}');", + args.table_name, + record.name, + record.active, + record.id, + record.price, + record.big_id, + record.precise_price, + record.birth_date, + record.last_login + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + + // Verify the record was inserted correctly + record + .verify_in_db(&args.database_name, &args.table_name) + .await?; + + info!("✓ Record with different column order inserted and verified"); + Ok(()) +} + +/// Test 5: Insert with completely reversed column order +async fn test_insert_reversed_column_order(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let record = InsertTestRecord { + id: 5001, + big_id: 5001000, + price: 88.8, + precise_price: 188.88, + active: true, + birth_date: "2024-07-20".to_string(), + last_login: "2024-07-20T16:45:00".to_string(), + name: "OrderTest2".to_string(), + }; + + // Reversed order: name, last_login, birth_date, active, precise_price, price, big_id, id + let insert_sql = format!( + "INSERT INTO {} (name, last_login, birth_date, active, precise_price, price, big_id, id) \ + VALUES ('{}', '{}', '{}', {}, {:.2}, {:.1}, {}, {});", + args.table_name, + record.name, + record.last_login, + record.birth_date, + record.active, + record.precise_price, + record.price, + record.big_id, + record.id + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + + // Verify the record was inserted correctly + record + .verify_in_db(&args.database_name, &args.table_name) + .await?; + + info!("✓ Record with reversed column order inserted and verified"); + Ok(()) +} + +/// Test 6: Insert with random column order (multiple records) +async fn test_insert_random_column_order(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + let mut inserted_records = Vec::new(); + + // Insert 10 records with different column orders + for i in 6000..6010 { + let record = InsertTestRecord { + id: i, + big_id: (i as i64) * 1000, + price: (i as f32) * 0.5, + precise_price: (i as f64) * 0.75, + active: i % 3 == 0, + birth_date: format!("2024-{:02}-15", ((i % 12) + 1)), + last_login: format!("2024-{:02}-15T10:00:00", ((i % 12) + 1)), + name: format!("RandomOrder_{}", i), + }; + + // Cycle through different column orders + let insert_sql = match i % 3 { + 0 => { + // Order 1: id, name, price, active, big_id, precise_price, birth_date, last_login + format!( + "INSERT INTO {} (id, name, price, active, big_id, precise_price, birth_date, last_login) \ + VALUES ({}, '{}', {:.1}, {}, {}, {:.2}, '{}', '{}');", + args.table_name, + record.id, + record.name, + record.price, + record.active, + record.big_id, + record.precise_price, + record.birth_date, + record.last_login + ) + } + 1 => { + // Order 2: active, birth_date, id, last_login, name, big_id, price, precise_price + format!( + "INSERT INTO {} (active, birth_date, id, last_login, name, big_id, price, precise_price) \ + VALUES ({}, '{}', {}, '{}', '{}', {}, {:.1}, {:.2});", + args.table_name, + record.active, + record.birth_date, + record.id, + record.last_login, + record.name, + record.big_id, + record.price, + record.precise_price + ) + } + _ => { + // Order 3: big_id, precise_price, price, name, last_login, birth_date, active, id + format!( + "INSERT INTO {} (big_id, precise_price, price, name, last_login, birth_date, active, id) \ + VALUES ({}, {:.2}, {:.1}, '{}', '{}', '{}', {}, {});", + args.table_name, + record.big_id, + record.precise_price, + record.price, + record.name, + record.last_login, + record.birth_date, + record.active, + record.id + ) + } + }; + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + inserted_records.push(record); + } + + info!("✓ 10 records with random column orders inserted"); + + // Verify all records + for record in &inserted_records { + record + .verify_in_db(&args.database_name, &args.table_name) + .await?; + } + + info!("✓ All records with random column orders verified"); + Ok(()) +} diff --git a/tester/src/e2e/mod.rs b/tester/src/e2e/mod.rs new file mode 100644 index 0000000..56c5140 --- /dev/null +++ b/tester/src/e2e/mod.rs @@ -0,0 +1,9 @@ +pub mod alter_table; +pub mod create_table; +pub mod delete; +pub mod drop_table; +pub mod insert; +pub mod response_helpers; +pub mod select; +pub mod truncate_table; +pub mod update; diff --git a/tester/src/e2e/response_helpers.rs b/tester/src/e2e/response_helpers.rs new file mode 100644 index 0000000..639c5cb --- /dev/null +++ b/tester/src/e2e/response_helpers.rs @@ -0,0 +1,492 @@ +use log::{error, info}; +use protocol::{ColumnType, Field, Record, Response, StatementType}; + +use crate::{ + TesterError, + client::{BinaryClient, ReadResult}, +}; + +/// Helper to expect and validate an Acknowledge response +pub async fn expect_acknowledge(client: &mut BinaryClient) -> Result<(), TesterError> { + match client.read_response().await? { + ReadResult::Disconnected => { + error!("Expected Acknowledge but got disconnected"); + Err(TesterError::Disconnected) + } + ReadResult::Response(Response::Acknowledge) => { + info!("✓ Received Acknowledge"); + Ok(()) + } + ReadResult::Response(Response::Error { + message, + error_type, + }) => { + error!( + "Expected Acknowledge but got Error: {} ({:?})", + message, error_type + ); + Err(TesterError::ServerError { message }) + } + ReadResult::Response(other) => { + error!("Expected Acknowledge but got: {:?}", other); + Err(TesterError::ServerError { + message: format!("Expected Acknowledge but got: {:?}", other), + }) + } + } +} + +/// Helper to expect and validate a ColumnInfo response +pub async fn expect_column_info( + client: &mut BinaryClient, + expected_columns: &[(&str, ColumnType)], +) -> Result<(), TesterError> { + match client.read_response().await? { + ReadResult::Disconnected => { + error!("Expected ColumnInfo but got disconnected"); + Err(TesterError::Disconnected) + } + ReadResult::Response(Response::ColumnInfo { column_metadata }) => { + if column_metadata.len() != expected_columns.len() { + error!( + "Expected {} columns but got {}", + expected_columns.len(), + column_metadata.len() + ); + return Err(TesterError::ServerError { + message: format!( + "Expected {} columns but got {}", + expected_columns.len(), + column_metadata.len() + ), + }); + } + + for (idx, (expected_name, expected_type)) in expected_columns.iter().enumerate() { + let actual = &column_metadata[idx]; + if actual.name != *expected_name { + error!( + "Column {} name mismatch: expected '{}', got '{}'", + idx, expected_name, actual.name + ); + return Err(TesterError::ServerError { + message: format!( + "Column {} name mismatch: expected '{}', got '{}'", + idx, expected_name, actual.name + ), + }); + } + + if !types_match(&actual.ty, expected_type) { + error!( + "Column {} ('{}') type mismatch: expected {:?}, got {:?}", + idx, expected_name, expected_type, actual.ty + ); + return Err(TesterError::ServerError { + message: format!( + "Column {} ('{}') type mismatch: expected {:?}, got {:?}", + idx, expected_name, expected_type, actual.ty + ), + }); + } + } + + info!( + "✓ Received ColumnInfo with {} columns", + column_metadata.len() + ); + Ok(()) + } + ReadResult::Response(Response::Error { + message, + error_type, + }) => { + error!( + "Expected ColumnInfo but got Error: {} ({:?})", + message, error_type + ); + Err(TesterError::ServerError { message }) + } + ReadResult::Response(other) => { + error!("Expected ColumnInfo but got: {:?}", other); + Err(TesterError::ServerError { + message: format!("Expected ColumnInfo but got: {:?}", other), + }) + } + } +} + +fn types_match(actual: &ColumnType, expected: &ColumnType) -> bool { + matches!( + (actual, expected), + (ColumnType::String, ColumnType::String) + | (ColumnType::F32, ColumnType::F32) + | (ColumnType::F64, ColumnType::F64) + | (ColumnType::I32, ColumnType::I32) + | (ColumnType::I64, ColumnType::I64) + | (ColumnType::Bool, ColumnType::Bool) + | (ColumnType::Date, ColumnType::Date) + | (ColumnType::DateTime, ColumnType::DateTime) + ) +} + +/// Helper to collect all rows from a SELECT query +/// Returns the total number of rows collected +pub async fn collect_all_rows(client: &mut BinaryClient) -> Result, TesterError> { + let mut all_records = Vec::new(); + + loop { + match client.read_response().await? { + ReadResult::Disconnected => { + error!("Connection lost while collecting rows"); + return Err(TesterError::Disconnected); + } + ReadResult::Response(Response::Rows { mut records, count }) => { + info!("✓ Received batch of {} rows", count); + all_records.append(&mut records); + } + ReadResult::Response(Response::StatementCompleted { + rows_affected, + statement_type, + }) => { + info!( + "✓ StatementCompleted: {} rows affected ({:?})", + rows_affected, statement_type + ); + + if all_records.len() != rows_affected { + error!( + "Row count mismatch: collected {} rows but statement says {} rows affected", + all_records.len(), + rows_affected + ); + return Err(TesterError::ServerError { + message: format!( + "Row count mismatch: collected {} rows but statement says {} rows affected", + all_records.len(), + rows_affected + ), + }); + } + + return Ok(all_records); + } + ReadResult::Response(Response::Error { + message, + error_type, + }) => { + error!( + "Error while collecting rows: {} ({:?})", + message, error_type + ); + return Err(TesterError::ServerError { message }); + } + ReadResult::Response(other) => { + error!("Unexpected response while collecting rows: {:?}", other); + return Err(TesterError::ServerError { + message: format!("Unexpected response while collecting rows: {:?}", other), + }); + } + } + } +} + +/// Helper to expect a StatementCompleted response with specific parameters +pub async fn expect_statement_completed( + client: &mut BinaryClient, + expected_rows_affected: usize, + expected_type: StatementType, +) -> Result<(), TesterError> { + match client.read_response().await? { + ReadResult::Disconnected => { + error!("Expected StatementCompleted but got disconnected"); + Err(TesterError::Disconnected) + } + ReadResult::Response(Response::StatementCompleted { + rows_affected, + statement_type, + }) => { + if rows_affected != expected_rows_affected { + error!( + "Expected {} rows affected but got {}", + expected_rows_affected, rows_affected + ); + return Err(TesterError::ServerError { + message: format!( + "Expected {} rows affected but got {}", + expected_rows_affected, rows_affected + ), + }); + } + + // Check statement type matches (using debug format for comparison) + let actual_type_str = format!("{:?}", statement_type); + let expected_type_str = format!("{:?}", expected_type); + if actual_type_str != expected_type_str { + error!( + "Expected statement type {:?} but got {:?}", + expected_type, statement_type + ); + return Err(TesterError::ServerError { + message: format!( + "Expected statement type {:?} but got {:?}", + expected_type, statement_type + ), + }); + } + + info!( + "✓ Received StatementCompleted: {} rows affected ({:?})", + rows_affected, statement_type + ); + Ok(()) + } + ReadResult::Response(Response::Error { + message, + error_type, + }) => { + error!( + "Expected StatementCompleted but got Error: {} ({:?})", + message, error_type + ); + Err(TesterError::ServerError { message }) + } + ReadResult::Response(other) => { + error!("Expected StatementCompleted but got: {:?}", other); + Err(TesterError::ServerError { + message: format!("Expected StatementCompleted but got: {:?}", other), + }) + } + } +} + +/// Helper to expect a QueryCompleted response +pub async fn expect_query_completed(client: &mut BinaryClient) -> Result<(), TesterError> { + match client.read_response().await? { + ReadResult::Disconnected => { + error!("Expected QueryCompleted but got disconnected"); + Err(TesterError::Disconnected) + } + ReadResult::Response(Response::QueryCompleted) => { + info!("✓ Received QueryCompleted"); + Ok(()) + } + ReadResult::Response(Response::Error { + message, + error_type, + }) => { + error!( + "Expected QueryCompleted but got Error: {} ({:?})", + message, error_type + ); + Err(TesterError::ServerError { message }) + } + ReadResult::Response(other) => { + error!("Expected QueryCompleted but got: {:?}", other); + Err(TesterError::ServerError { + message: format!("Expected QueryCompleted but got: {:?}", other), + }) + } + } +} + +/// Helper to validate a complete SELECT query flow +/// Returns the collected records +pub async fn validate_select_query( + client: &mut BinaryClient, + expected_columns: &[(&str, ColumnType)], +) -> Result, TesterError> { + expect_acknowledge(client).await?; + expect_column_info(client, expected_columns).await?; + let records = collect_all_rows(client).await?; + expect_query_completed(client).await?; + Ok(records) +} + +/// Helper to validate a non-SELECT statement (INSERT, CREATE, DELETE, etc.) +pub async fn validate_non_select_statement( + client: &mut BinaryClient, + expected_rows_affected: usize, + statement_type: StatementType, +) -> Result<(), TesterError> { + expect_acknowledge(client).await?; + expect_statement_completed(client, expected_rows_affected, statement_type).await?; + expect_query_completed(client).await?; + Ok(()) +} + +/// Validate that a record has the expected number of fields +pub fn validate_field_count(record: &Record, expected_count: usize) -> Result<(), TesterError> { + if record.fields.len() != expected_count { + error!( + "Expected {} fields but got {}", + expected_count, + record.fields.len() + ); + return Err(TesterError::ServerError { + message: format!( + "Expected {} fields but got {}", + expected_count, + record.fields.len() + ), + }); + } + Ok(()) +} + +/// Extract an i32 field from a record at the given index +pub fn extract_i32(record: &Record, index: usize) -> Result { + match &record.fields.get(index) { + Some(Field::Int32(val)) => Ok(*val), + Some(other) => { + error!("Expected Int32 at index {} but got {:?}", index, other); + Err(TesterError::ServerError { + message: format!("Expected Int32 at index {} but got {:?}", index, other), + }) + } + None => { + error!("No field at index {}", index); + Err(TesterError::ServerError { + message: format!("No field at index {}", index), + }) + } + } +} + +/// Extract an i64 field from a record at the given index +pub fn extract_i64(record: &Record, index: usize) -> Result { + match &record.fields.get(index) { + Some(Field::Int64(val)) => Ok(*val), + Some(other) => { + error!("Expected Int64 at index {} but got {:?}", index, other); + Err(TesterError::ServerError { + message: format!("Expected Int64 at index {} but got {:?}", index, other), + }) + } + None => { + error!("No field at index {}", index); + Err(TesterError::ServerError { + message: format!("No field at index {}", index), + }) + } + } +} + +/// Extract a string field from a record at the given index +pub fn extract_string(record: &Record, index: usize) -> Result { + match &record.fields.get(index) { + Some(Field::String(val)) => Ok(val.clone()), + Some(other) => { + error!("Expected String at index {} but got {:?}", index, other); + Err(TesterError::ServerError { + message: format!("Expected String at index {} but got {:?}", index, other), + }) + } + None => { + error!("No field at index {}", index); + Err(TesterError::ServerError { + message: format!("No field at index {}", index), + }) + } + } +} + +/// Extract a bool field from a record at the given index +pub fn extract_bool(record: &Record, index: usize) -> Result { + match &record.fields.get(index) { + Some(Field::Bool(val)) => Ok(*val), + Some(other) => { + error!("Expected Bool at index {} but got {:?}", index, other); + Err(TesterError::ServerError { + message: format!("Expected Bool at index {} but got {:?}", index, other), + }) + } + None => { + error!("No field at index {}", index); + Err(TesterError::ServerError { + message: format!("No field at index {}", index), + }) + } + } +} + +/// Extract an f32 field from a record at the given index +pub fn extract_f32(record: &Record, index: usize) -> Result { + match &record.fields.get(index) { + Some(Field::Float32(val)) => Ok(*val), + Some(other) => { + error!("Expected Float32 at index {} but got {:?}", index, other); + Err(TesterError::ServerError { + message: format!("Expected Float32 at index {} but got {:?}", index, other), + }) + } + None => { + error!("No field at index {}", index); + Err(TesterError::ServerError { + message: format!("No field at index {}", index), + }) + } + } +} + +/// Extract an f64 field from a record at the given index +pub fn extract_f64(record: &Record, index: usize) -> Result { + match &record.fields.get(index) { + Some(Field::Float64(val)) => Ok(*val), + Some(other) => { + error!("Expected Float64 at index {} but got {:?}", index, other); + Err(TesterError::ServerError { + message: format!("Expected Float64 at index {} but got {:?}", index, other), + }) + } + None => { + error!("No field at index {}", index); + Err(TesterError::ServerError { + message: format!("No field at index {}", index), + }) + } + } +} + +/// Helper to expect an error response from the server +/// This properly handles the query flow: Acknowledge -> Error -> QueryCompleted +pub async fn expect_error(client: &mut BinaryClient) -> Result { + // First, expect acknowledge + expect_acknowledge(client).await?; + + // Then expect an error + let error_message = match client.read_response().await? { + ReadResult::Disconnected => { + error!("Expected Error but got disconnected"); + return Err(TesterError::Disconnected); + } + ReadResult::Response(Response::Error { message, .. }) => { + info!("✓ Received expected error: {}", message); + message + } + ReadResult::Response(other) => { + error!("Expected Error but got: {:?}", other); + return Err(TesterError::ServerError { + message: format!("Expected Error but got: {:?}", other), + }); + } + }; + + // Finally, expect QueryCompleted even after error + match client.read_response().await? { + ReadResult::Disconnected => { + error!("Expected QueryCompleted but got disconnected"); + Err(TesterError::Disconnected) + } + ReadResult::Response(Response::QueryCompleted) => { + info!("✓ Received QueryCompleted after error"); + Ok(error_message) + } + ReadResult::Response(other) => { + error!("Expected QueryCompleted but got: {:?}", other); + Err(TesterError::ServerError { + message: format!("Expected QueryCompleted after error but got: {:?}", other), + }) + } + } +} diff --git a/tester/src/e2e/select.rs b/tester/src/e2e/select.rs new file mode 100644 index 0000000..53cadff --- /dev/null +++ b/tester/src/e2e/select.rs @@ -0,0 +1,854 @@ +use std::collections::HashMap; + +use log::{error, info}; +use protocol::{ColumnType, Record, Request, StatementType}; + +use crate::{ + TesterError, + suite::{E2ETestResult, Suite, default_client}, +}; + +use super::response_helpers::{ + extract_bool, extract_f32, extract_f64, extract_i32, extract_i64, extract_string, + validate_field_count, validate_non_select_statement, validate_select_query, +}; + +/// Test record structure matching the table schema +#[derive(Debug, Clone)] +pub struct TestRecord { + pub id: i32, + pub big_id: i64, + pub price: f32, + pub precise_price: f64, + pub active: bool, + pub birth_date: String, // Format: YYYY-MM-DD + pub last_login: String, // Format: YYYY-MM-DDTHH:MM:SS + pub name: String, +} + +impl TestRecord { + /// Generate test records + pub fn generate(num_records: usize) -> Vec { + (0..num_records) + .map(|i| TestRecord { + id: i as i32, + big_id: (i as i64) * 1000, + price: (i as f32) * 1.5, + precise_price: (i as f64) * 2.75, + active: i % 2 == 0, + birth_date: format!("2024-01-{:02}", (i % 28) + 1), + last_login: format!("2024-01-{:02}T12:00:00", (i % 28) + 1), + name: format!("User_{}", i), + }) + .collect() + } + + /// Validate that a protocol Record matches this test record (all 8 fields) + pub fn validate_full_record(&self, record: &Record) -> Result<(), TesterError> { + validate_field_count(record, 8)?; + + let id = extract_i32(record, 0)?; + let big_id = extract_i64(record, 1)?; + let price = extract_f32(record, 2)?; + let precise_price = extract_f64(record, 3)?; + let active = extract_bool(record, 4)?; + // Skip date/datetime validation for now (fields 5, 6) + let name = extract_string(record, 7)?; + + if id != self.id { + return Err(TesterError::ServerError { + message: format!("ID mismatch: expected {}, got {}", self.id, id), + }); + } + if big_id != self.big_id { + return Err(TesterError::ServerError { + message: format!("big_id mismatch: expected {}, got {}", self.big_id, big_id), + }); + } + if (price - self.price).abs() > 0.01 { + return Err(TesterError::ServerError { + message: format!("price mismatch: expected {}, got {}", self.price, price), + }); + } + if (precise_price - self.precise_price).abs() > 0.001 { + return Err(TesterError::ServerError { + message: format!( + "precise_price mismatch: expected {}, got {}", + self.precise_price, precise_price + ), + }); + } + if active != self.active { + return Err(TesterError::ServerError { + message: format!("active mismatch: expected {}, got {}", self.active, active), + }); + } + if name != self.name { + return Err(TesterError::ServerError { + message: format!("name mismatch: expected '{}', got '{}'", self.name, name), + }); + } + + Ok(()) + } + + /// Validate subset record (id, name, price) + pub fn validate_subset_record(&self, record: &Record) -> Result<(), TesterError> { + validate_field_count(record, 3)?; + + let id = extract_i32(record, 0)?; + let name = extract_string(record, 1)?; + let price = extract_f32(record, 2)?; + + if id != self.id { + return Err(TesterError::ServerError { + message: format!("ID mismatch: expected {}, got {}", self.id, id), + }); + } + if name != self.name { + return Err(TesterError::ServerError { + message: format!("name mismatch: expected '{}', got '{}'", self.name, name), + }); + } + if (price - self.price).abs() > 0.01 { + return Err(TesterError::ServerError { + message: format!("price mismatch: expected {}, got {}", self.price, price), + }); + } + + Ok(()) + } + + /// Validate id and name only + pub fn validate_id_name_record(&self, record: &Record) -> Result<(), TesterError> { + validate_field_count(record, 2)?; + + let id = extract_i32(record, 0)?; + let name = extract_string(record, 1)?; + + if id != self.id { + return Err(TesterError::ServerError { + message: format!("ID mismatch: expected {}, got {}", self.id, id), + }); + } + if name != self.name { + return Err(TesterError::ServerError { + message: format!("name mismatch: expected '{}', got '{}'", self.name, name), + }); + } + + Ok(()) + } + + /// Validate id and big_id only + pub fn validate_id_bigid_record(&self, record: &Record) -> Result<(), TesterError> { + validate_field_count(record, 2)?; + + let id = extract_i32(record, 0)?; + let big_id = extract_i64(record, 1)?; + + if id != self.id { + return Err(TesterError::ServerError { + message: format!("ID mismatch: expected {}, got {}", self.id, id), + }); + } + if big_id != self.big_id { + return Err(TesterError::ServerError { + message: format!("big_id mismatch: expected {}, got {}", self.big_id, big_id), + }); + } + + Ok(()) + } + + /// Validate record with id and active + pub fn validate_id_active_record(&self, record: &Record) -> Result<(), TesterError> { + validate_field_count(record, 2)?; + + let id = extract_i32(record, 0)?; + let active = extract_bool(record, 1)?; + + if id != self.id { + return Err(TesterError::ServerError { + message: format!("ID mismatch: expected {}, got {}", self.id, id), + }); + } + if active != self.active { + return Err(TesterError::ServerError { + message: format!("active mismatch: expected {}, got {}", self.active, active), + }); + } + + Ok(()) + } + + /// Validate record with id, name, price, active + pub fn validate_complex_record(&self, record: &Record) -> Result<(), TesterError> { + validate_field_count(record, 4)?; + + let id = extract_i32(record, 0)?; + let name = extract_string(record, 1)?; + let price = extract_f32(record, 2)?; + let active = extract_bool(record, 3)?; + + if id != self.id { + return Err(TesterError::ServerError { + message: format!("ID mismatch: expected {}, got {}", self.id, id), + }); + } + if name != self.name { + return Err(TesterError::ServerError { + message: format!("name mismatch: expected '{}', got '{}'", self.name, name), + }); + } + if (price - self.price).abs() > 0.01 { + return Err(TesterError::ServerError { + message: format!("price mismatch: expected {}, got {}", self.price, price), + }); + } + if active != self.active { + return Err(TesterError::ServerError { + message: format!("active mismatch: expected {}, got {}", self.active, active), + }); + } + + Ok(()) + } +} + +pub struct SelectE2ETest; + +pub struct Setup { + pub database_name: String, + pub table_name: String, + pub num_records: usize, +} + +pub struct Test { + pub database_name: String, + pub table_name: String, + pub test_data: Vec, +} + +pub struct Cleanup { + pub database_name: String, +} + +impl Suite for SelectE2ETest { + type SetupArgs = Setup; + + async fn setup(args: &Self::SetupArgs) -> Result<(), TesterError> { + info!("Creating database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::CreateDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database created"); + + // Create table with all data types + let create_table_sql = format!( + "CREATE TABLE {} (\ + id INT32 PRIMARY_KEY, \ + big_id INT64, \ + price FLOAT32, \ + precise_price FLOAT64, \ + active BOOL, \ + birth_date DATE, \ + last_login DATETIME, \ + name STRING\ + );", + args.table_name + ); + + info!("Creating table with all data types..."); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + info!("✓ Table created"); + + // Generate test data + let test_data = TestRecord::generate(args.num_records); + + // Insert test data + info!("Inserting {} records...", test_data.len()); + for record in &test_data { + let insert_sql = format!( + "INSERT INTO {} (id, big_id, price, precise_price, active, birth_date, last_login, name) \ + VALUES ({}, {}, {:.1}, {:.2}, {}, '{}', '{}', '{}');", + args.table_name, + record.id, + record.big_id, + record.price, + record.precise_price, + record.active, + record.birth_date, + record.last_login, + record.name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + } + + info!("✓ {} records inserted", test_data.len()); + Ok(()) + } + + type TestArgs = Test; + + async fn run(args: &Self::TestArgs) -> Result { + let mut tests_passed = 0; + + // Test 1: SELECT * FROM table + info!("\n=== Test 1: SELECT * ==="); + if let Err(e) = test_select_all(args).await { + error!("Test 1 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 1: SELECT * passed"); + tests_passed += 1; + + // Test 2: SELECT (subset of columns) + info!("\n=== Test 2: SELECT subset of columns ==="); + if let Err(e) = test_select_subset(args).await { + error!("Test 2 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 2: SELECT subset passed"); + tests_passed += 1; + + // Test 3: SELECT with ORDER BY + info!("\n=== Test 3: SELECT with ORDER BY ==="); + if let Err(e) = test_select_order_by(args).await { + error!("Test 3 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 3: SELECT with ORDER BY passed"); + tests_passed += 1; + + // Test 4: SELECT with ORDER BY + LIMIT + info!("\n=== Test 4: SELECT with ORDER BY + LIMIT ==="); + if let Err(e) = test_select_order_by_limit(args).await { + error!("Test 4 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 4: SELECT with ORDER BY + LIMIT passed"); + tests_passed += 1; + + // Test 5: SELECT with WHERE clause + info!("\n=== Test 5: SELECT with WHERE clause ==="); + if let Err(e) = test_select_where(args).await { + error!("Test 5 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 5: SELECT with WHERE passed"); + tests_passed += 1; + + // Test 6: SELECT with ORDER BY + OFFSET + info!("\n=== Test 6: SELECT with ORDER BY + OFFSET ==="); + if let Err(e) = test_select_order_by_offset(args).await { + error!("Test 6 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 6: SELECT with ORDER BY + OFFSET passed"); + tests_passed += 1; + + // Test 7: SELECT with everything (WHERE + ORDER BY + OFFSET + LIMIT) + info!("\n=== Test 7: SELECT with WHERE + ORDER BY + OFFSET + LIMIT ==="); + if let Err(e) = test_select_everything(args).await { + error!("Test 7 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 7: SELECT with WHERE + ORDER BY + OFFSET + LIMIT passed"); + tests_passed += 1; + + Ok(E2ETestResult { tests_passed }) + } + + type CleanupArgs = Cleanup; + + async fn cleanup(args: &Self::CleanupArgs) -> Result<(), TesterError> { + info!("Deleting database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::DeleteDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database deleted"); + Ok(()) + } +} + +/// Test 1: SELECT * FROM table - should return all records with all columns +async fn test_select_all(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let sql = format!("SELECT * FROM {};", args.table_name); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql, + }) + .await?; + + let expected_columns = vec![ + ("id", ColumnType::I32), + ("big_id", ColumnType::I64), + ("price", ColumnType::F32), + ("precise_price", ColumnType::F64), + ("active", ColumnType::Bool), + ("birth_date", ColumnType::Date), + ("last_login", ColumnType::DateTime), + ("name", ColumnType::String), + ]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + // Validate we got all records + if records.len() != args.test_data.len() { + error!( + "Expected {} records but got {}", + args.test_data.len(), + records.len() + ); + return Err(TesterError::ServerError { + message: format!( + "Expected {} records but got {}", + args.test_data.len(), + records.len() + ), + }); + } + + // Build a map of returned records by id for easy lookup + let mut returned_by_id = HashMap::new(); + for record in &records { + let id = extract_i32(record, 0)?; + returned_by_id.insert(id, record); + } + + // Validate each expected record is present and correct + for expected in &args.test_data { + match returned_by_id.get(&expected.id) { + Some(actual) => expected.validate_full_record(actual)?, + None => { + error!( + "Expected record with id={} not found in results", + expected.id + ); + return Err(TesterError::ServerError { + message: format!( + "Expected record with id={} not found in results", + expected.id + ), + }); + } + } + } + + info!( + "✓ All {} records retrieved and validated with correct values", + records.len() + ); + Ok(()) +} + +/// Test 2: SELECT subset of columns +async fn test_select_subset(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let sql = format!("SELECT id, name, price FROM {};", args.table_name); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql, + }) + .await?; + + let expected_columns = vec![ + ("id", ColumnType::I32), + ("name", ColumnType::String), + ("price", ColumnType::F32), + ]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != args.test_data.len() { + error!( + "Expected {} records but got {}", + args.test_data.len(), + records.len() + ); + return Err(TesterError::ServerError { + message: format!( + "Expected {} records but got {}", + args.test_data.len(), + records.len() + ), + }); + } + + // Build a map of returned records by id + let mut returned_by_id = HashMap::new(); + for record in &records { + let id = extract_i32(record, 0)?; + returned_by_id.insert(id, record); + } + + // Validate each expected record + for expected in &args.test_data { + match returned_by_id.get(&expected.id) { + Some(actual) => expected.validate_subset_record(actual)?, + None => { + error!( + "Expected record with id={} not found in results", + expected.id + ); + return Err(TesterError::ServerError { + message: format!( + "Expected record with id={} not found in results", + expected.id + ), + }); + } + } + } + + info!( + "✓ Subset query returned {} records with validated values", + records.len() + ); + Ok(()) +} + +/// Test 3: SELECT with ORDER BY +async fn test_select_order_by(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Order by id descending + let sql = format!("SELECT id, name FROM {} ORDER BY id DESC;", args.table_name); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql, + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("name", ColumnType::String)]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != args.test_data.len() { + error!( + "Expected {} records but got {}", + args.test_data.len(), + records.len() + ); + return Err(TesterError::ServerError { + message: format!( + "Expected {} records but got {}", + args.test_data.len(), + records.len() + ), + }); + } + + // Create expected order (DESC by id) + let mut expected_order = args.test_data.clone(); + expected_order.sort_by(|a, b| b.id.cmp(&a.id)); + + // Validate ordering and values + for (idx, (expected, actual)) in expected_order.iter().zip(records.iter()).enumerate() { + expected + .validate_id_name_record(actual) + .map_err(|e| TesterError::ServerError { + message: format!("Record {} mismatch: {}", idx, e), + })?; + } + + let first_id = extract_i32(&records[0], 0)?; + let last_id = extract_i32(&records[records.len() - 1], 0)?; + + info!( + "✓ Records correctly ordered DESC and validated (first_id={}, last_id={})", + first_id, last_id + ); + Ok(()) +} + +/// Test 4: SELECT with ORDER BY + LIMIT +async fn test_select_order_by_limit(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let limit = 100; + let sql = format!( + "SELECT id, name FROM {} ORDER BY id ASC LIMIT {};", + args.table_name, limit + ); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql, + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("name", ColumnType::String)]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != limit { + error!("Expected {} records but got {}", limit, records.len()); + return Err(TesterError::ServerError { + message: format!("Expected {} records but got {}", limit, records.len()), + }); + } + + // Create expected order (first 100 records, ASC by id) + let mut expected_order = args.test_data.clone(); + expected_order.sort_by(|a, b| a.id.cmp(&b.id)); + let expected_order: Vec<_> = expected_order.into_iter().take(limit).collect(); + + // Validate ordering and values + for (idx, (expected, actual)) in expected_order.iter().zip(records.iter()).enumerate() { + expected + .validate_id_name_record(actual) + .map_err(|e| TesterError::ServerError { + message: format!("Record {} mismatch: {}", idx, e), + })?; + } + + let first_id = extract_i32(&records[0], 0)?; + let last_id = extract_i32(&records[records.len() - 1], 0)?; + + info!( + "✓ LIMIT correctly returned {} validated records (id {} to {})", + limit, first_id, last_id + ); + Ok(()) +} + +/// Test 5: SELECT with WHERE clause +async fn test_select_where(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // WHERE id >= 100 AND id < 200 + let sql = format!( + "SELECT id, active FROM {} WHERE id >= 100 AND id < 200;", + args.table_name + ); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql, + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("active", ColumnType::Bool)]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + // Filter expected records + let expected_filtered: Vec<_> = args + .test_data + .iter() + .filter(|r| r.id >= 100 && r.id < 200) + .collect(); + + if records.len() != expected_filtered.len() { + error!( + "Expected {} records but got {}", + expected_filtered.len(), + records.len() + ); + return Err(TesterError::ServerError { + message: format!( + "Expected {} records but got {}", + expected_filtered.len(), + records.len() + ), + }); + } + + // Build a map of returned records by id + let mut returned_by_id = HashMap::new(); + for record in &records { + let id = extract_i32(record, 0)?; + returned_by_id.insert(id, record); + } + + // Validate each expected record + for expected in &expected_filtered { + match returned_by_id.get(&expected.id) { + Some(actual) => expected.validate_id_active_record(actual)?, + None => { + error!( + "Expected record with id={} not found in results", + expected.id + ); + return Err(TesterError::ServerError { + message: format!( + "Expected record with id={} not found in results", + expected.id + ), + }); + } + } + } + + info!( + "✓ WHERE clause correctly filtered and validated {} records", + records.len() + ); + Ok(()) +} + +/// Test 6: SELECT with ORDER BY + OFFSET +async fn test_select_order_by_offset(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let offset = 50; + let sql = format!( + "SELECT id, big_id FROM {} ORDER BY id ASC OFFSET {};", + args.table_name, offset + ); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql, + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("big_id", ColumnType::I64)]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + // Create expected order (ASC by id, skip first 50) + let mut expected_order = args.test_data.clone(); + expected_order.sort_by(|a, b| a.id.cmp(&b.id)); + let expected_order: Vec<_> = expected_order.into_iter().skip(offset).collect(); + + if records.len() != expected_order.len() { + error!( + "Expected {} records but got {}", + expected_order.len(), + records.len() + ); + return Err(TesterError::ServerError { + message: format!( + "Expected {} records but got {}", + expected_order.len(), + records.len() + ), + }); + } + + // Validate ordering and values + for (idx, (expected, actual)) in expected_order.iter().zip(records.iter()).enumerate() { + expected + .validate_id_bigid_record(actual) + .map_err(|e| TesterError::ServerError { + message: format!("Record {} mismatch: {}", idx, e), + })?; + } + + if let Some(first_record) = records.first() { + let first_id = extract_i32(first_record, 0)?; + info!( + "✓ OFFSET correctly skipped first {} records and validated (first_id={})", + offset, first_id + ); + } + + Ok(()) +} + +/// Test 7: SELECT with WHERE + ORDER BY + OFFSET + LIMIT +async fn test_select_everything(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let limit = 20; + let offset = 10; + + // WHERE id >= 100, ORDER BY id DESC, OFFSET 10, LIMIT 20 + let sql = format!( + "SELECT id, name, price, active FROM {} WHERE id >= 100 ORDER BY id DESC OFFSET {} LIMIT {};", + args.table_name, offset, limit + ); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql, + }) + .await?; + + let expected_columns = vec![ + ("id", ColumnType::I32), + ("name", ColumnType::String), + ("price", ColumnType::F32), + ("active", ColumnType::Bool), + ]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + // Create expected order: filter WHERE id >= 100, ORDER BY id DESC, OFFSET 10, LIMIT 20 + let mut expected_order: Vec<_> = args + .test_data + .iter() + .filter(|r| r.id >= 100) + .cloned() + .collect(); + expected_order.sort_by(|a, b| b.id.cmp(&a.id)); // DESC + let expected_order: Vec<_> = expected_order + .into_iter() + .skip(offset) + .take(limit) + .collect(); + + if records.len() != expected_order.len() { + error!( + "Expected {} records but got {}", + expected_order.len(), + records.len() + ); + return Err(TesterError::ServerError { + message: format!( + "Expected {} records but got {}", + expected_order.len(), + records.len() + ), + }); + } + + // Validate each record matches expected + for (idx, (expected, actual)) in expected_order.iter().zip(records.iter()).enumerate() { + expected + .validate_complex_record(actual) + .map_err(|e| TesterError::ServerError { + message: format!("Record {} mismatch: {}", idx, e), + })?; + } + + info!( + "✓ Complex query (WHERE + ORDER BY + OFFSET + LIMIT) returned {} validated records", + records.len() + ); + Ok(()) +} diff --git a/tester/src/e2e/truncate_table.rs b/tester/src/e2e/truncate_table.rs new file mode 100644 index 0000000..f2807c1 --- /dev/null +++ b/tester/src/e2e/truncate_table.rs @@ -0,0 +1,294 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, StatementType}; + +use crate::{ + TesterError, + suite::{E2ETestResult, Suite, default_client}, +}; + +use super::response_helpers::{validate_non_select_statement, validate_select_query}; + +pub struct TruncateTableE2ETest; + +pub struct Setup { + pub database_name: String, +} + +pub struct Test { + pub database_name: String, +} + +pub struct Cleanup { + pub database_name: String, +} + +impl Suite for TruncateTableE2ETest { + type SetupArgs = Setup; + + async fn setup(args: &Self::SetupArgs) -> Result<(), TesterError> { + info!("Creating database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::CreateDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database created"); + Ok(()) + } + + type TestArgs = Test; + + async fn run(args: &Self::TestArgs) -> Result { + let mut tests_passed = 0; + + // Test 1: Truncate table removes all records + info!("\n=== Test 1: Truncate table removes all records ==="); + if let Err(e) = test_truncate_removes_all_records(args).await { + error!("Test 1 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 1: Truncate removes all records passed"); + tests_passed += 1; + + // Test 2: Truncate empty table + info!("\n=== Test 2: Truncate empty table ==="); + if let Err(e) = test_truncate_empty_table(args).await { + error!("Test 2 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 2: Truncate empty table passed"); + tests_passed += 1; + + // Test 3: Truncate and re-insert + info!("\n=== Test 3: Truncate and re-insert ==="); + if let Err(e) = test_truncate_and_reinsert(args).await { + error!("Test 3 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 3: Truncate and re-insert passed"); + tests_passed += 1; + + Ok(E2ETestResult { tests_passed }) + } + + type CleanupArgs = Cleanup; + + async fn cleanup(args: &Self::CleanupArgs) -> Result<(), TesterError> { + info!("Deleting database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::DeleteDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database deleted"); + Ok(()) + } +} + +/// Test 1: Truncate table removes all records +async fn test_truncate_removes_all_records(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_table_sql = "CREATE TABLE truncate_test (id INT32 PRIMARY_KEY, value INT64);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Insert 100 records + info!("Inserting 100 records..."); + for i in 0..100 { + let insert_sql = format!( + "INSERT INTO truncate_test (id, value) VALUES ({}, {});", + i, + i * 10 + ); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + } + + // Verify records exist + let select_sql = "SELECT * FROM truncate_test;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("value", ColumnType::I64)]; + let records_before = validate_select_query(&mut client, &expected_columns).await?; + + if records_before.len() != 100 { + return Err(TesterError::ServerError { + message: format!( + "Expected 100 records before truncate but got {}", + records_before.len() + ), + }); + } + + // Truncate table + info!("Truncating table..."); + let truncate_sql = "TRUNCATE TABLE truncate_test;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: truncate_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::TruncateTable).await?; + + // Verify table is empty + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let records_after = validate_select_query(&mut client, &expected_columns).await?; + + if !records_after.is_empty() { + return Err(TesterError::ServerError { + message: format!( + "Expected 0 records after truncate but got {}", + records_after.len() + ), + }); + } + + info!("✓ All 100 records removed by truncate"); + Ok(()) +} + +/// Test 2: Truncate empty table +async fn test_truncate_empty_table(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_table_sql = "CREATE TABLE empty_truncate (id INT32 PRIMARY_KEY);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Truncate empty table + let truncate_sql = "TRUNCATE TABLE empty_truncate;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: truncate_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::TruncateTable).await?; + + info!("✓ Truncate on empty table succeeded"); + Ok(()) +} + +/// Test 3: Truncate and re-insert +async fn test_truncate_and_reinsert(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Create table + let create_table_sql = "CREATE TABLE reinsert_test (id INT32 PRIMARY_KEY, name STRING);"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + + // Insert some records + for i in 0..50 { + let insert_sql = format!( + "INSERT INTO reinsert_test (id, name) VALUES ({}, 'User_{}');", + i, i + ); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + } + + // Truncate + let truncate_sql = "TRUNCATE TABLE reinsert_test;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: truncate_sql.to_string(), + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::TruncateTable).await?; + + // Re-insert records (can reuse same IDs) + for i in 0..50 { + let insert_sql = format!( + "INSERT INTO reinsert_test (id, name) VALUES ({}, 'NewUser_{}');", + i, i + ); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + } + + // Verify new records exist + let select_sql = "SELECT * FROM reinsert_test;"; + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql.to_string(), + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("name", ColumnType::String)]; + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != 50 { + return Err(TesterError::ServerError { + message: format!( + "Expected 50 records after re-insert but got {}", + records.len() + ), + }); + } + + info!("✓ Table truncated and re-inserted successfully"); + Ok(()) +} diff --git a/tester/src/e2e/update.rs b/tester/src/e2e/update.rs new file mode 100644 index 0000000..c2394d8 --- /dev/null +++ b/tester/src/e2e/update.rs @@ -0,0 +1,819 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, Response, StatementType}; + +use crate::{ + TesterError, + client::ReadResult, + e2e::response_helpers::expect_acknowledge, + suite::{E2ETestResult, Suite, default_client}, +}; + +use super::response_helpers::{ + extract_bool, extract_f32, extract_f64, extract_i32, extract_i64, extract_string, + validate_field_count, validate_non_select_statement, validate_select_query, +}; + +/// Test record structure for UPDATE tests +#[derive(Debug, Clone)] +pub struct UpdateTestRecord { + pub id: i32, + pub big_id: i64, + pub price: f32, + pub precise_price: f64, + pub active: bool, + pub birth_date: String, + pub last_login: String, + pub name: String, +} + +impl UpdateTestRecord { + /// Generate test records + pub fn generate(num_records: usize) -> Vec { + (0..num_records) + .map(|i| UpdateTestRecord { + id: i as i32, + big_id: (i as i64) * 1000, + price: (i as f32) * 1.5, + precise_price: (i as f64) * 2.75, + active: i % 2 == 0, + birth_date: format!("2024-01-{:02}", (i % 28) + 1), + last_login: format!("2024-01-{:02}T12:00:00", (i % 28) + 1), + name: format!("User_{}", i), + }) + .collect() + } + + /// Verify this record in the database + pub async fn verify_in_db( + &self, + database_name: &str, + table_name: &str, + ) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let sql = format!("SELECT * FROM {} WHERE id = {};", table_name, self.id); + client + .send_request(&Request::Query { + database_name: Some(database_name.to_string()), + sql, + }) + .await?; + + let expected_columns = vec![ + ("id", ColumnType::I32), + ("big_id", ColumnType::I64), + ("price", ColumnType::F32), + ("precise_price", ColumnType::F64), + ("active", ColumnType::Bool), + ("birth_date", ColumnType::Date), + ("last_login", ColumnType::DateTime), + ("name", ColumnType::String), + ]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != 1 { + error!( + "Expected 1 record with id={} but got {}", + self.id, + records.len() + ); + return Err(TesterError::ServerError { + message: format!( + "Expected 1 record with id={} but got {}", + self.id, + records.len() + ), + }); + } + + let record = &records[0]; + validate_field_count(record, 8)?; + + let id = extract_i32(record, 0)?; + let big_id = extract_i64(record, 1)?; + let price = extract_f32(record, 2)?; + let precise_price = extract_f64(record, 3)?; + let active = extract_bool(record, 4)?; + let name = extract_string(record, 7)?; + + if id != self.id { + return Err(TesterError::ServerError { + message: format!("ID mismatch: expected {}, got {}", self.id, id), + }); + } + if big_id != self.big_id { + return Err(TesterError::ServerError { + message: format!("big_id mismatch: expected {}, got {}", self.big_id, big_id), + }); + } + if (price - self.price).abs() > 0.01 { + return Err(TesterError::ServerError { + message: format!("price mismatch: expected {}, got {}", self.price, price), + }); + } + if (precise_price - self.precise_price).abs() > 0.001 { + return Err(TesterError::ServerError { + message: format!( + "precise_price mismatch: expected {}, got {}", + self.precise_price, precise_price + ), + }); + } + if active != self.active { + return Err(TesterError::ServerError { + message: format!("active mismatch: expected {}, got {}", self.active, active), + }); + } + if name != self.name { + return Err(TesterError::ServerError { + message: format!("name mismatch: expected '{}', got '{}'", self.name, name), + }); + } + + Ok(()) + } +} + +pub struct UpdateE2ETest; + +pub struct Setup { + pub database_name: String, + pub table_name: String, + pub num_records: usize, +} + +pub struct Test { + pub database_name: String, + pub table_name: String, + pub test_data: Vec, +} + +pub struct Cleanup { + pub database_name: String, +} + +impl Suite for UpdateE2ETest { + type SetupArgs = Setup; + + async fn setup(args: &Self::SetupArgs) -> Result<(), TesterError> { + info!("Creating database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::CreateDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database created"); + + // Create table with all data types + let create_table_sql = format!( + "CREATE TABLE {} (\ + id INT32 PRIMARY_KEY, \ + big_id INT64, \ + price FLOAT32, \ + precise_price FLOAT64, \ + active BOOL, \ + birth_date DATE, \ + last_login DATETIME, \ + name STRING\ + );", + args.table_name + ); + + info!("Creating table with all data types..."); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: create_table_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 0, StatementType::CreateTable).await?; + info!("✓ Table created"); + + // Generate and insert test data + let test_data = UpdateTestRecord::generate(args.num_records); + + info!("Inserting {} records...", test_data.len()); + for record in &test_data { + let insert_sql = format!( + "INSERT INTO {} (id, big_id, price, precise_price, active, birth_date, last_login, name) \ + VALUES ({}, {}, {:.1}, {:.2}, {}, '{}', '{}', '{}');", + args.table_name, + record.id, + record.big_id, + record.price, + record.precise_price, + record.active, + record.birth_date, + record.last_login, + record.name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: insert_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Insert).await?; + } + + info!("✓ {} records inserted", test_data.len()); + Ok(()) + } + + type TestArgs = Test; + + async fn run(args: &Self::TestArgs) -> Result { + let mut tests_passed = 0; + + // Test 1: Try to update primary key (should fail) + info!("\n=== Test 1: Update primary key (should fail) ==="); + if let Err(e) = test_update_primary_key_fails(args).await { + error!("Test 1 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 1: Update primary key correctly rejected"); + tests_passed += 1; + + // Test 2: Update INT64 column (big_id) + info!("\n=== Test 2: Update INT64 column ==="); + if let Err(e) = test_update_int64_column(args).await { + error!("Test 2 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 2: Update INT64 column passed"); + tests_passed += 1; + + // Test 3: Update FLOAT32 column (price) + info!("\n=== Test 3: Update FLOAT32 column ==="); + if let Err(e) = test_update_float32_column(args).await { + error!("Test 3 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 3: Update FLOAT32 column passed"); + tests_passed += 1; + + // Test 4: Update FLOAT64 column (precise_price) + info!("\n=== Test 4: Update FLOAT64 column ==="); + if let Err(e) = test_update_float64_column(args).await { + error!("Test 4 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 4: Update FLOAT64 column passed"); + tests_passed += 1; + + // Test 5: Update BOOL column (active) + info!("\n=== Test 5: Update BOOL column ==="); + if let Err(e) = test_update_bool_column(args).await { + error!("Test 5 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 5: Update BOOL column passed"); + tests_passed += 1; + + // Test 6: Update DATE column (birth_date) + info!("\n=== Test 6: Update DATE column ==="); + if let Err(e) = test_update_date_column(args).await { + error!("Test 6 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 6: Update DATE column passed"); + tests_passed += 1; + + // Test 7: Update DATETIME column (last_login) + info!("\n=== Test 7: Update DATETIME column ==="); + if let Err(e) = test_update_datetime_column(args).await { + error!("Test 7 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 7: Update DATETIME column passed"); + tests_passed += 1; + + // Test 8: Update STRING column (name) + info!("\n=== Test 8: Update STRING column ==="); + if let Err(e) = test_update_string_column(args).await { + error!("Test 8 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 8: Update STRING column passed"); + tests_passed += 1; + + // Test 9: Update with WHERE clause (range) + info!("\n=== Test 9: Update with WHERE clause (range) ==="); + if let Err(e) = test_update_with_where_clause(args).await { + error!("Test 9 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 9: Update with WHERE clause passed"); + tests_passed += 1; + + // Test 10: Update multiple columns at once + info!("\n=== Test 10: Update multiple columns at once ==="); + if let Err(e) = test_update_multiple_columns(args).await { + error!("Test 10 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 10: Update multiple columns passed"); + tests_passed += 1; + + // Test 11: Update with complex WHERE clause + info!("\n=== Test 11: Update with complex WHERE clause ==="); + if let Err(e) = test_update_complex_where(args).await { + error!("Test 11 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 11: Update with complex WHERE clause passed"); + tests_passed += 1; + + // Test 12: Update ALL records (no WHERE clause) - runs last as it modifies all data + info!("\n=== Test 12: Update ALL records (no WHERE) ==="); + if let Err(e) = test_update_all_records(args).await { + error!("Test 12 failed: {:?}", e); + return Err(e); + } + info!("✓ Test 12: Update ALL records passed"); + tests_passed += 1; + + Ok(E2ETestResult { tests_passed }) + } + + type CleanupArgs = Cleanup; + + async fn cleanup(args: &Self::CleanupArgs) -> Result<(), TesterError> { + info!("Deleting database '{}'...", args.database_name); + let mut client = default_client().await?; + + client + .execute_and_wait(Request::DeleteDatabase { + database_name: args.database_name.clone(), + }) + .await?; + + info!("✓ Database deleted"); + Ok(()) + } +} + +/// Test 1: Attempt to update primary key (should fail) +#[allow(dead_code)] +async fn test_update_primary_key_fails(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + let update_sql = format!("UPDATE {} SET id = 99999 WHERE id = 0;", args.table_name); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: update_sql, + }) + .await?; + + // We expect an error here + match expect_acknowledge(&mut client).await { + Ok(_) => { + // If we got acknowledge, check if we get an error in the next response + match client.read_response().await? { + ReadResult::Response(Response::Error { message, .. }) => { + info!( + "✓ Got expected error when updating primary key: {}", + message + ); + Ok(()) + } + ReadResult::Response(Response::StatementCompleted { .. }) => { + error!("UPDATE on primary key should have failed but succeeded!"); + Err(TesterError::ServerError { + message: "UPDATE on primary key should have failed but succeeded" + .to_string(), + }) + } + _ => { + error!("Unexpected response type when updating primary key"); + Err(TesterError::ServerError { + message: "Unexpected response type when updating primary key".to_string(), + }) + } + } + } + Err(_) => { + // Got error immediately, that's good + info!("✓ Update primary key correctly rejected"); + Ok(()) + } + } +} + +/// Test 2: Update INT64 column +async fn test_update_int64_column(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Update big_id for id 100 + let new_big_id = 999999; + let update_sql = format!( + "UPDATE {} SET big_id = {} WHERE id = 100;", + args.table_name, new_big_id + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: update_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Update).await?; + + // Verify the update + let mut expected = args.test_data[100].clone(); + expected.big_id = new_big_id; + expected + .verify_in_db(&args.database_name, &args.table_name) + .await?; + + info!("✓ INT64 column updated and verified"); + Ok(()) +} + +/// Test 3: Update FLOAT32 column +async fn test_update_float32_column(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Update price for id 200 + let new_price = 123.45; + let update_sql = format!( + "UPDATE {} SET price = {:.2} WHERE id = 200;", + args.table_name, new_price + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: update_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Update).await?; + + // Verify the update + let mut expected = args.test_data[200].clone(); + expected.price = new_price; + expected + .verify_in_db(&args.database_name, &args.table_name) + .await?; + + info!("✓ FLOAT32 column updated and verified"); + Ok(()) +} + +/// Test 4: Update FLOAT64 column +async fn test_update_float64_column(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Update precise_price for id 300 + let new_precise_price = 987.654321; + let update_sql = format!( + "UPDATE {} SET precise_price = {:.6} WHERE id = 300;", + args.table_name, new_precise_price + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: update_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Update).await?; + + // Verify the update + let mut expected = args.test_data[300].clone(); + expected.precise_price = new_precise_price; + expected + .verify_in_db(&args.database_name, &args.table_name) + .await?; + + info!("✓ FLOAT64 column updated and verified"); + Ok(()) +} + +/// Test 5: Update BOOL column +async fn test_update_bool_column(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Update active for id 400 (flip it) + let original_active = args.test_data[400].active; + let new_active = !original_active; + let update_sql = format!( + "UPDATE {} SET active = {} WHERE id = 400;", + args.table_name, new_active + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: update_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Update).await?; + + // Verify the update + let mut expected = args.test_data[400].clone(); + expected.active = new_active; + expected + .verify_in_db(&args.database_name, &args.table_name) + .await?; + + info!("✓ BOOL column updated and verified"); + Ok(()) +} + +/// Test 6: Update DATE column +async fn test_update_date_column(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Update birth_date for id 500 + let new_birth_date = "2025-12-25".to_string(); + let update_sql = format!( + "UPDATE {} SET birth_date = '{}' WHERE id = 500;", + args.table_name, new_birth_date + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: update_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Update).await?; + + // Verify the update + let mut expected = args.test_data[500].clone(); + expected.birth_date = new_birth_date; + expected + .verify_in_db(&args.database_name, &args.table_name) + .await?; + + info!("✓ DATE column updated and verified"); + Ok(()) +} + +/// Test 7: Update DATETIME column +async fn test_update_datetime_column(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Update last_login for id 600 + let new_last_login = "2025-12-31T23:59:59".to_string(); + let update_sql = format!( + "UPDATE {} SET last_login = '{}' WHERE id = 600;", + args.table_name, new_last_login + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: update_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Update).await?; + + // Verify the update + let mut expected = args.test_data[600].clone(); + expected.last_login = new_last_login; + expected + .verify_in_db(&args.database_name, &args.table_name) + .await?; + + info!("✓ DATETIME column updated and verified"); + Ok(()) +} + +/// Test 8: Update STRING column +async fn test_update_string_column(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Update name for id 700 + let new_name = "UpdatedUser_700".to_string(); + let update_sql = format!( + "UPDATE {} SET name = '{}' WHERE id = 700;", + args.table_name, new_name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: update_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Update).await?; + + // Verify the update + let mut expected = args.test_data[700].clone(); + expected.name = new_name; + expected + .verify_in_db(&args.database_name, &args.table_name) + .await?; + + info!("✓ STRING column updated and verified"); + Ok(()) +} + +/// Test 9: Update with WHERE clause (range) +async fn test_update_with_where_clause(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Update name for all records where id >= 1000 AND id < 1100 + let new_name = "RangeUpdated".to_string(); + let update_sql = format!( + "UPDATE {} SET name = '{}' WHERE id >= 1000 AND id < 1100;", + args.table_name, new_name + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: update_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 100, StatementType::Update).await?; + + // Verify ALL 100 records in the range + info!("Verifying all 100 updated records..."); + for id in 1000..1100 { + let mut expected = args.test_data[id].clone(); + expected.name = new_name.clone(); + expected + .verify_in_db(&args.database_name, &args.table_name) + .await?; + } + + // Verify a record outside the range hasn't changed + args.test_data[999] + .verify_in_db(&args.database_name, &args.table_name) + .await?; + args.test_data[1100] + .verify_in_db(&args.database_name, &args.table_name) + .await?; + + info!("✓ Range update with WHERE clause verified"); + Ok(()) +} + +/// Test 10: Update multiple columns at once +async fn test_update_multiple_columns(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Update multiple columns for id 2000 + let new_name = "MultiUpdate".to_string(); + let new_price = 555.55; + let new_active = false; + let update_sql = format!( + "UPDATE {} SET name = '{}', price = {:.2}, active = {} WHERE id = 2000;", + args.table_name, new_name, new_price, new_active + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: update_sql, + }) + .await?; + + validate_non_select_statement(&mut client, 1, StatementType::Update).await?; + + // Verify the update + let mut expected = args.test_data[2000].clone(); + expected.name = new_name; + expected.price = new_price; + expected.active = new_active; + expected + .verify_in_db(&args.database_name, &args.table_name) + .await?; + + info!("✓ Multiple columns updated and verified"); + Ok(()) +} + +/// Test 11: Update with complex WHERE clause +async fn test_update_complex_where(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Update records where id >= 3000 AND id < 3050 AND active = TRUE + let new_big_id = 123456789; + let update_sql = format!( + "UPDATE {} SET big_id = {} WHERE id >= 3000 AND id < 3050 AND active = TRUE;", + args.table_name, new_big_id + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: update_sql, + }) + .await?; + + // Count how many records should be updated (even ids from 3000 to 3048) + let expected_updates = (3000..3050).filter(|id| id % 2 == 0).count(); + validate_non_select_statement(&mut client, expected_updates, StatementType::Update).await?; + + // Verify ALL records that should be updated (all even ids in range) + info!("Verifying all {} updated records...", expected_updates); + for id in (3000..3050).filter(|id| id % 2 == 0) { + let mut expected = args.test_data[id].clone(); + expected.big_id = new_big_id; + expected + .verify_in_db(&args.database_name, &args.table_name) + .await?; + } + + // Verify ALL records that shouldn't be updated (all odd ids in range) + info!("Verifying all {} unaffected records...", 25); + for id in (3000..3050).filter(|id| id % 2 != 0) { + args.test_data[id] + .verify_in_db(&args.database_name, &args.table_name) + .await?; + } + + info!("✓ Complex WHERE clause update verified"); + Ok(()) +} + +/// Test 12: Update ALL records (no WHERE clause) +async fn test_update_all_records(args: &Test) -> Result<(), TesterError> { + let mut client = default_client().await?; + + // Update big_id for ALL records (no WHERE clause) + let new_big_id_multiplier = 5000; + let update_sql = format!( + "UPDATE {} SET big_id = {};", + args.table_name, new_big_id_multiplier + ); + + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: update_sql, + }) + .await?; + + validate_non_select_statement(&mut client, args.test_data.len(), StatementType::Update).await?; + + // Verify ALL records have been updated (only check big_id since previous tests may have modified other columns) + info!( + "Verifying all {} records have updated big_id...", + args.test_data.len() + ); + + let select_sql = format!("SELECT id, big_id FROM {};", args.table_name); + client + .send_request(&Request::Query { + database_name: Some(args.database_name.clone()), + sql: select_sql, + }) + .await?; + + let expected_columns = vec![("id", ColumnType::I32), ("big_id", ColumnType::I64)]; + + let records = validate_select_query(&mut client, &expected_columns).await?; + + if records.len() != args.test_data.len() { + return Err(TesterError::ServerError { + message: format!( + "Expected {} records but got {}", + args.test_data.len(), + records.len() + ), + }); + } + + // Check that every record has the new big_id value + for record in &records { + let id = extract_i32(record, 0)?; + let big_id = extract_i64(record, 1)?; + + if big_id != new_big_id_multiplier { + return Err(TesterError::ServerError { + message: format!( + "Record id={} has big_id={} but expected {}", + id, big_id, new_big_id_multiplier + ), + }); + } + } + + info!( + "✓ All {} records updated successfully", + args.test_data.len() + ); + Ok(()) +} diff --git a/tester/src/main.rs b/tester/src/main.rs index d107dd7..73eeab1 100644 --- a/tester/src/main.rs +++ b/tester/src/main.rs @@ -4,6 +4,14 @@ use std::time::Duration; use clap::{Parser, Subcommand}; use thiserror::Error; +use crate::e2e::alter_table::{self, AlterTableE2ETest}; +use crate::e2e::create_table::{self, CreateTableE2ETest}; +use crate::e2e::delete::{self, DeleteE2ETest}; +use crate::e2e::drop_table::{self, DropTableE2ETest}; +use crate::e2e::insert::{self, InsertE2ETest}; +use crate::e2e::select::{self, SelectE2ETest}; +use crate::e2e::truncate_table::{self, TruncateTableE2ETest}; +use crate::e2e::update::{self, UpdateE2ETest}; use crate::performance::concurrent_inserts::{self, ConcurrentInserts}; use crate::performance::concurrent_reads::{self, ReadMany}; use crate::performance::concurrent_reads_and_inserts::{self, ConcurrentReadsAndInserts}; @@ -12,6 +20,7 @@ use crate::performance::concurrent_reads_with_index::{self, ReadByIndex}; use crate::suite::{PerformanceTestResult, Suite}; mod client; +mod e2e; mod performance; mod suite; @@ -112,6 +121,33 @@ enum Command { #[arg(long, default_value_t = 1000)] records_per_writer: usize, }, + + /// E2E test for SELECT statements with comprehensive validation + E2eSelect, + + /// E2E test for INSERT statements with comprehensive validation + E2eInsert, + + /// E2E test for UPDATE statements with comprehensive validation + E2eUpdate, + + /// E2E test for DELETE statements with comprehensive validation + E2eDelete, + + /// E2E test for CREATE TABLE statements with comprehensive validation + E2eCreateTable, + + /// E2E test for TRUNCATE TABLE statements with comprehensive validation + E2eTruncateTable, + + /// E2E test for DROP TABLE statements with comprehensive validation + E2eDropTable, + + /// E2E test for ALTER TABLE statements with comprehensive validation + E2eAlterTable, + + /// Run all E2E tests (SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, TRUNCATE TABLE, DROP TABLE, and ALTER TABLE) + E2eAll, } #[derive(Debug, Error)] @@ -298,6 +334,326 @@ async fn concurrent_reads_non_index( Ok(test_results) } +async fn e2e_select() -> Result<(), TesterError> { + let db_name = "E2E_SELECT_TEST".to_string(); + let table_name = "TEST_TABLE".to_string(); + + const NUM_RECORDS: usize = 15000; + + let setup = select::Setup { + database_name: db_name.clone(), + table_name: table_name.clone(), + num_records: NUM_RECORDS, + }; + + // Generate test data + let test_data = select::TestRecord::generate(NUM_RECORDS); + + let test = select::Test { + database_name: db_name.clone(), + table_name: table_name.clone(), + test_data, + }; + + let cleanup = select::Cleanup { + database_name: db_name.clone(), + }; + + let result = SelectE2ETest::run_suite(&setup, &test, &cleanup).await?; + + println!("E2E SELECT test completed successfully!"); + println!("Tests passed: {}", result.tests_passed); + + Ok(()) +} + +async fn e2e_insert() -> Result<(), TesterError> { + let db_name = "E2E_INSERT_TEST".to_string(); + let table_name = "INSERT_TEST_TABLE".to_string(); + + let setup = insert::Setup { + database_name: db_name.clone(), + table_name: table_name.clone(), + }; + + let test = insert::Test { + database_name: db_name.clone(), + table_name: table_name.clone(), + }; + + let cleanup = insert::Cleanup { + database_name: db_name.clone(), + }; + + let result = InsertE2ETest::run_suite(&setup, &test, &cleanup).await?; + + println!("E2E INSERT test completed successfully!"); + println!("Tests passed: {}", result.tests_passed); + + Ok(()) +} + +async fn e2e_update() -> Result<(), TesterError> { + let db_name = "UPDATE_E2E_DB".to_string(); + let table_name = "UPDATE_E2E_TABLE".to_string(); + + const NUM_RECORDS: usize = 5000; + + let test_data = update::UpdateTestRecord::generate(NUM_RECORDS); + + let setup = update::Setup { + database_name: db_name.clone(), + table_name: table_name.clone(), + num_records: NUM_RECORDS, + }; + + let test = update::Test { + database_name: db_name.clone(), + table_name: table_name.clone(), + test_data, + }; + + let cleanup = update::Cleanup { + database_name: db_name.clone(), + }; + + let result = UpdateE2ETest::run_suite(&setup, &test, &cleanup).await?; + + println!("E2E UPDATE test completed successfully!"); + println!("Tests passed: {}", result.tests_passed); + + Ok(()) +} + +async fn e2e_delete() -> Result<(), TesterError> { + let db_name = "DELETE_E2E_DB".to_string(); + let table_name = "DELETE_E2E_TABLE".to_string(); + const NUM_RECORDS: usize = 5000; + + let test_data = delete::DeleteTestRecord::generate(NUM_RECORDS); + + let setup = delete::Setup { + database_name: db_name.clone(), + table_name: table_name.clone(), + num_records: NUM_RECORDS, + }; + + let test = delete::Test { + database_name: db_name.clone(), + table_name: table_name.clone(), + test_data, + }; + + let cleanup = delete::Cleanup { + database_name: db_name.clone(), + }; + + let result = DeleteE2ETest::run_suite(&setup, &test, &cleanup).await?; + + println!("E2E DELETE test completed successfully!"); + println!("Tests passed: {}", result.tests_passed); + + Ok(()) +} + +async fn e2e_create_table() -> Result<(), TesterError> { + let db_name = "CREATE_TABLE_E2E_DB".to_string(); + + let setup = create_table::Setup { + database_name: db_name.clone(), + }; + + let test = create_table::Test { + database_name: db_name.clone(), + }; + + let cleanup = create_table::Cleanup { + database_name: db_name.clone(), + }; + + let result = CreateTableE2ETest::run_suite(&setup, &test, &cleanup).await?; + + println!("E2E CREATE TABLE test completed successfully!"); + println!("Tests passed: {}", result.tests_passed); + + Ok(()) +} + +async fn e2e_truncate_table() -> Result<(), TesterError> { + let db_name = "TRUNCATE_TABLE_E2E_DB".to_string(); + + let setup = truncate_table::Setup { + database_name: db_name.clone(), + }; + + let test = truncate_table::Test { + database_name: db_name.clone(), + }; + + let cleanup = truncate_table::Cleanup { + database_name: db_name.clone(), + }; + + let result = TruncateTableE2ETest::run_suite(&setup, &test, &cleanup).await?; + + println!("E2E TRUNCATE TABLE test completed successfully!"); + println!("Tests passed: {}", result.tests_passed); + + Ok(()) +} + +async fn e2e_drop_table() -> Result<(), TesterError> { + let db_name = "DROP_TABLE_E2E_DB".to_string(); + + let setup = drop_table::Setup { + database_name: db_name.clone(), + }; + + let test = drop_table::Test { + database_name: db_name.clone(), + }; + + let cleanup = drop_table::Cleanup { + database_name: db_name.clone(), + }; + + let result = DropTableE2ETest::run_suite(&setup, &test, &cleanup).await?; + + println!("E2E DROP TABLE test completed successfully!"); + println!("Tests passed: {}", result.tests_passed); + + Ok(()) +} + +async fn e2e_alter_table() -> Result<(), TesterError> { + let db_name = "ALTER_TABLE_E2E_DB".to_string(); + + let setup = alter_table::Setup { + database_name: db_name.clone(), + }; + + let test = alter_table::Test { + database_name: db_name.clone(), + }; + + let cleanup = alter_table::Cleanup { + database_name: db_name.clone(), + }; + + let result = AlterTableE2ETest::run_suite(&setup, &test, &cleanup).await?; + + println!("E2E ALTER TABLE test completed successfully!"); + println!("Tests passed: {}", result.tests_passed); + + Ok(()) +} + +async fn e2e_all() -> Result<(), TesterError> { + println!("\n========================================"); + println!("Running ALL E2E Tests"); + println!("========================================\n"); + + // Run CREATE TABLE tests + println!("[1/8] Running CREATE TABLE E2E tests..."); + match e2e_create_table().await { + Ok(()) => { + println!("✓ CREATE TABLE E2E tests passed\n"); + } + Err(e) => { + println!("✗ CREATE TABLE E2E tests failed: {:?}\n", e); + return Err(e); + } + } + + // Run INSERT tests + println!("[2/8] Running INSERT E2E tests..."); + match e2e_insert().await { + Ok(()) => { + println!("✓ INSERT E2E tests passed\n"); + } + Err(e) => { + println!("✗ INSERT E2E tests failed: {:?}\n", e); + return Err(e); + } + } + + // Run SELECT tests + println!("[3/8] Running SELECT E2E tests..."); + match e2e_select().await { + Ok(()) => { + println!("✓ SELECT E2E tests passed\n"); + } + Err(e) => { + println!("✗ SELECT E2E tests failed: {:?}\n", e); + return Err(e); + } + } + + // Run UPDATE tests + println!("[4/8] Running UPDATE E2E tests..."); + match e2e_update().await { + Ok(()) => { + println!("✓ UPDATE E2E tests passed\n"); + } + Err(e) => { + println!("✗ UPDATE E2E tests failed: {:?}\n", e); + return Err(e); + } + } + + // Run DELETE tests + println!("[5/8] Running DELETE E2E tests..."); + match e2e_delete().await { + Ok(()) => { + println!("✓ DELETE E2E tests passed\n"); + } + Err(e) => { + println!("✗ DELETE E2E tests failed: {:?}\n", e); + return Err(e); + } + } + + // Run TRUNCATE TABLE tests + println!("[6/8] Running TRUNCATE TABLE E2E tests..."); + match e2e_truncate_table().await { + Ok(()) => { + println!("✓ TRUNCATE TABLE E2E tests passed\n"); + } + Err(e) => { + println!("✗ TRUNCATE TABLE E2E tests failed: {:?}\n", e); + return Err(e); + } + } + + // Run DROP TABLE tests + println!("[7/8] Running DROP TABLE E2E tests..."); + match e2e_drop_table().await { + Ok(()) => { + println!("✓ DROP TABLE E2E tests passed\n"); + } + Err(e) => { + println!("✗ DROP TABLE E2E tests failed: {:?}\n", e); + return Err(e); + } + } + // Run ALTER TABLE tests + println!("[8/8] Running ALTER TABLE E2E tests..."); + match e2e_alter_table().await { + Ok(()) => { + println!("✓ ALTER TABLE E2E tests passed\n"); + } + Err(e) => { + println!("✗ ALTER TABLE E2E tests failed: {:?}\n", e); + return Err(e); + } + } + println!("========================================"); + println!("All E2E Tests Completed Successfully!"); + println!("========================================"); + + Ok(()) +} + #[tokio::main] async fn main() -> Result<(), TesterError> { env_logger::init(); @@ -355,6 +711,42 @@ async fn main() -> Result<(), TesterError> { report_stats("concurrent-reads-and-inserts", &test_results); Ok(()) } + Command::E2eSelect => { + e2e_select().await?; + Ok(()) + } + Command::E2eInsert => { + e2e_insert().await?; + Ok(()) + } + Command::E2eUpdate => { + e2e_update().await?; + Ok(()) + } + Command::E2eDelete => { + e2e_delete().await?; + Ok(()) + } + Command::E2eCreateTable => { + e2e_create_table().await?; + Ok(()) + } + Command::E2eTruncateTable => { + e2e_truncate_table().await?; + Ok(()) + } + Command::E2eDropTable => { + e2e_drop_table().await?; + Ok(()) + } + Command::E2eAlterTable => { + e2e_alter_table().await?; + Ok(()) + } + Command::E2eAll => { + e2e_all().await?; + Ok(()) + } } } diff --git a/tester/src/suite.rs b/tester/src/suite.rs index e145f80..793481b 100644 --- a/tester/src/suite.rs +++ b/tester/src/suite.rs @@ -43,6 +43,10 @@ pub struct PerformanceTestResult { pub duration: Duration, } +pub struct E2ETestResult { + pub tests_passed: usize, +} + const TEST_HOST: &str = "127.0.0.1"; const TEST_PORT: u16 = BINARY_PROTOCOL_PORT;