From d4dabc2cbb264b8c13e5246f6076c06471e202f4 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 11 Jan 2026 18:07:30 +0100 Subject: [PATCH 01/10] e2e select tests --- tester/src/e2e/mod.rs | 2 + tester/src/e2e/response_helpers.rs | 449 +++++++++++++++ tester/src/e2e/select.rs | 858 +++++++++++++++++++++++++++++ tester/src/main.rs | 44 ++ 4 files changed, 1353 insertions(+) create mode 100644 tester/src/e2e/mod.rs create mode 100644 tester/src/e2e/response_helpers.rs create mode 100644 tester/src/e2e/select.rs diff --git a/tester/src/e2e/mod.rs b/tester/src/e2e/mod.rs new file mode 100644 index 0000000..0554a8f --- /dev/null +++ b/tester/src/e2e/mod.rs @@ -0,0 +1,2 @@ +pub mod response_helpers; +pub mod select; diff --git a/tester/src/e2e/response_helpers.rs b/tester/src/e2e/response_helpers.rs new file mode 100644 index 0000000..f1319d1 --- /dev/null +++ b/tester/src/e2e/response_helpers.rs @@ -0,0 +1,449 @@ +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), + }) + } + } +} diff --git a/tester/src/e2e/select.rs b/tester/src/e2e/select.rs new file mode 100644 index 0000000..2fe63ca --- /dev/null +++ b/tester/src/e2e/select.rs @@ -0,0 +1,858 @@ +use std::collections::HashMap; + +use log::{error, info}; +use protocol::{ColumnType, Record, Request, StatementType}; + +use crate::{ + TesterError, + suite::{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, +} + +pub struct E2ETestResult { + pub tests_passed: usize, +} + +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/main.rs b/tester/src/main.rs index d107dd7..be6f33a 100644 --- a/tester/src/main.rs +++ b/tester/src/main.rs @@ -4,6 +4,7 @@ use std::time::Duration; use clap::{Parser, Subcommand}; use thiserror::Error; +use crate::e2e::select::{self, SelectE2ETest}; 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 +13,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 +114,13 @@ enum Command { #[arg(long, default_value_t = 1000)] records_per_writer: usize, }, + + /// E2E test for SELECT statements with comprehensive validation + E2eSelect { + /// Number of records to insert for testing + #[arg(long, default_value_t = 5000)] + records: usize, + }, } #[derive(Debug, Error)] @@ -298,6 +307,37 @@ async fn concurrent_reads_non_index( Ok(test_results) } +async fn e2e_select(num_records: usize) -> Result<(), TesterError> { + let db_name = "E2E_SELECT_TEST".to_string(); + let table_name = "TEST_TABLE".to_string(); + + let setup = select::Setup { + database_name: db_name.clone(), + table_name: table_name.clone(), + 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(()) +} + #[tokio::main] async fn main() -> Result<(), TesterError> { env_logger::init(); @@ -355,6 +395,10 @@ async fn main() -> Result<(), TesterError> { report_stats("concurrent-reads-and-inserts", &test_results); Ok(()) } + Command::E2eSelect { records } => { + e2e_select(records).await?; + Ok(()) + } } } From c7efac5e6c0a335965f726573fb17fec913acc4f Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 11 Jan 2026 18:19:38 +0100 Subject: [PATCH 02/10] e2e insert --- tester/src/e2e/insert.rs | 670 +++++++++++++++++++++++++++++++++++++++ tester/src/e2e/mod.rs | 1 + tester/src/main.rs | 34 ++ 3 files changed, 705 insertions(+) create mode 100644 tester/src/e2e/insert.rs diff --git a/tester/src/e2e/insert.rs b/tester/src/e2e/insert.rs new file mode 100644 index 0000000..b246c9c --- /dev/null +++ b/tester/src/e2e/insert.rs @@ -0,0 +1,670 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, StatementType}; + +use crate::{ + TesterError, + suite::{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, +} + +pub struct E2ETestResult { + pub tests_passed: usize, +} + +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 index 0554a8f..5c92b59 100644 --- a/tester/src/e2e/mod.rs +++ b/tester/src/e2e/mod.rs @@ -1,2 +1,3 @@ +pub mod insert; pub mod response_helpers; pub mod select; diff --git a/tester/src/main.rs b/tester/src/main.rs index be6f33a..67d0c0b 100644 --- a/tester/src/main.rs +++ b/tester/src/main.rs @@ -4,6 +4,7 @@ use std::time::Duration; use clap::{Parser, Subcommand}; use thiserror::Error; +use crate::e2e::insert::{self, InsertE2ETest}; use crate::e2e::select::{self, SelectE2ETest}; use crate::performance::concurrent_inserts::{self, ConcurrentInserts}; use crate::performance::concurrent_reads::{self, ReadMany}; @@ -121,6 +122,9 @@ enum Command { #[arg(long, default_value_t = 5000)] records: usize, }, + + /// E2E test for INSERT statements with comprehensive validation + E2eInsert, } #[derive(Debug, Error)] @@ -338,6 +342,32 @@ async fn e2e_select(num_records: usize) -> Result<(), TesterError> { 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(()) +} + #[tokio::main] async fn main() -> Result<(), TesterError> { env_logger::init(); @@ -399,6 +429,10 @@ async fn main() -> Result<(), TesterError> { e2e_select(records).await?; Ok(()) } + Command::E2eInsert => { + e2e_insert().await?; + Ok(()) + } } } From 3f34c39ed6b0116a9ec0547a7e78d38d2e912b97 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 11 Jan 2026 18:25:56 +0100 Subject: [PATCH 03/10] command to run all e2e --- tester/src/main.rs | 61 ++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/tester/src/main.rs b/tester/src/main.rs index 67d0c0b..634b440 100644 --- a/tester/src/main.rs +++ b/tester/src/main.rs @@ -117,14 +117,13 @@ enum Command { }, /// E2E test for SELECT statements with comprehensive validation - E2eSelect { - /// Number of records to insert for testing - #[arg(long, default_value_t = 5000)] - records: usize, - }, + E2eSelect, /// E2E test for INSERT statements with comprehensive validation E2eInsert, + + /// Run all E2E tests (SELECT and INSERT) + E2eAll, } #[derive(Debug, Error)] @@ -311,18 +310,20 @@ async fn concurrent_reads_non_index( Ok(test_results) } -async fn e2e_select(num_records: usize) -> Result<(), TesterError> { +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: NUM_RECORDS, }; // Generate test data - let test_data = select::TestRecord::generate(num_records); + let test_data = select::TestRecord::generate(NUM_RECORDS); let test = select::Test { database_name: db_name.clone(), @@ -368,6 +369,42 @@ async fn e2e_insert() -> Result<(), TesterError> { Ok(()) } +async fn e2e_all() -> Result<(), TesterError> { + println!("\n========================================"); + println!("Running ALL E2E Tests"); + println!("========================================\n"); + + // Run INSERT tests + println!("[1/2] 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!("[2/2] 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); + } + } + + println!("========================================"); + println!("All E2E Tests Completed Successfully!"); + println!("========================================"); + + Ok(()) +} + #[tokio::main] async fn main() -> Result<(), TesterError> { env_logger::init(); @@ -425,14 +462,18 @@ async fn main() -> Result<(), TesterError> { report_stats("concurrent-reads-and-inserts", &test_results); Ok(()) } - Command::E2eSelect { records } => { - e2e_select(records).await?; + Command::E2eSelect => { + e2e_select().await?; Ok(()) } Command::E2eInsert => { e2e_insert().await?; Ok(()) } + Command::E2eAll => { + e2e_all().await?; + Ok(()) + } } } From 8135f6ec42d22a1792bc2e0deb584ff582283b2a Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Sun, 11 Jan 2026 19:01:56 +0100 Subject: [PATCH 04/10] update e2e --- tester/src/e2e/mod.rs | 1 + tester/src/e2e/update.rs | 824 +++++++++++++++++++++++++++++++++++++++ tester/src/main.rs | 58 ++- 3 files changed, 880 insertions(+), 3 deletions(-) create mode 100644 tester/src/e2e/update.rs diff --git a/tester/src/e2e/mod.rs b/tester/src/e2e/mod.rs index 5c92b59..fa29eba 100644 --- a/tester/src/e2e/mod.rs +++ b/tester/src/e2e/mod.rs @@ -1,3 +1,4 @@ pub mod insert; pub mod response_helpers; pub mod select; +pub mod update; diff --git a/tester/src/e2e/update.rs b/tester/src/e2e/update.rs new file mode 100644 index 0000000..61b8982 --- /dev/null +++ b/tester/src/e2e/update.rs @@ -0,0 +1,824 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, Response, StatementType}; + +use crate::{ + TesterError, + client::ReadResult, + e2e::response_helpers::expect_acknowledge, + suite::{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, +} + +pub struct E2ETestResult { + pub tests_passed: usize, +} + +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; + + // TODO: uncomment once its done + // 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 634b440..60069d4 100644 --- a/tester/src/main.rs +++ b/tester/src/main.rs @@ -6,6 +6,7 @@ use thiserror::Error; use crate::e2e::insert::{self, InsertE2ETest}; use crate::e2e::select::{self, SelectE2ETest}; +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}; @@ -122,7 +123,10 @@ enum Command { /// E2E test for INSERT statements with comprehensive validation E2eInsert, - /// Run all E2E tests (SELECT and INSERT) + /// E2E test for UPDATE statements with comprehensive validation + E2eUpdate, + + /// Run all E2E tests (SELECT, INSERT, and UPDATE) E2eAll, } @@ -369,13 +373,45 @@ async fn e2e_insert() -> Result<(), TesterError> { 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_all() -> Result<(), TesterError> { println!("\n========================================"); println!("Running ALL E2E Tests"); println!("========================================\n"); // Run INSERT tests - println!("[1/2] Running INSERT E2E tests..."); + println!("[1/3] Running INSERT E2E tests..."); match e2e_insert().await { Ok(()) => { println!("✓ INSERT E2E tests passed\n"); @@ -387,7 +423,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run SELECT tests - println!("[2/2] Running SELECT E2E tests..."); + println!("[2/3] Running SELECT E2E tests..."); match e2e_select().await { Ok(()) => { println!("✓ SELECT E2E tests passed\n"); @@ -398,6 +434,18 @@ async fn e2e_all() -> Result<(), TesterError> { } } + // Run UPDATE tests + println!("[3/3] 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); + } + } + println!("========================================"); println!("All E2E Tests Completed Successfully!"); println!("========================================"); @@ -470,6 +518,10 @@ async fn main() -> Result<(), TesterError> { e2e_insert().await?; Ok(()) } + Command::E2eUpdate => { + e2e_update().await?; + Ok(()) + } Command::E2eAll => { e2e_all().await?; Ok(()) From 1a019c237c51d819579e5fdd8d3e5363492feafe Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 12 Jan 2026 00:06:19 +0100 Subject: [PATCH 05/10] uncomment test --- tester/src/e2e/update.rs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tester/src/e2e/update.rs b/tester/src/e2e/update.rs index 61b8982..4893bea 100644 --- a/tester/src/e2e/update.rs +++ b/tester/src/e2e/update.rs @@ -236,15 +236,14 @@ impl Suite for UpdateE2ETest { async fn run(args: &Self::TestArgs) -> Result { let mut tests_passed = 0; - // TODO: uncomment once its done // 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; + 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 ==="); From 4f95c05f691f118a1a5d69195ace647483333089 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 12 Jan 2026 00:29:24 +0100 Subject: [PATCH 06/10] e2e delete --- tester/src/e2e/delete.rs | 880 +++++++++++++++++++++++++++++++++++++++ tester/src/e2e/mod.rs | 1 + tester/src/main.rs | 59 ++- 3 files changed, 936 insertions(+), 4 deletions(-) create mode 100644 tester/src/e2e/delete.rs diff --git a/tester/src/e2e/delete.rs b/tester/src/e2e/delete.rs new file mode 100644 index 0000000..07a7ce3 --- /dev/null +++ b/tester/src/e2e/delete.rs @@ -0,0 +1,880 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, StatementType}; + +use crate::{ + TesterError, + suite::{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, +} + +pub struct E2ETestResult { + pub tests_passed: usize, +} + +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/mod.rs b/tester/src/e2e/mod.rs index fa29eba..f5c54d4 100644 --- a/tester/src/e2e/mod.rs +++ b/tester/src/e2e/mod.rs @@ -1,3 +1,4 @@ +pub mod delete; pub mod insert; pub mod response_helpers; pub mod select; diff --git a/tester/src/main.rs b/tester/src/main.rs index 60069d4..675e23d 100644 --- a/tester/src/main.rs +++ b/tester/src/main.rs @@ -4,6 +4,7 @@ use std::time::Duration; use clap::{Parser, Subcommand}; use thiserror::Error; +use crate::e2e::delete::{self, DeleteE2ETest}; use crate::e2e::insert::{self, InsertE2ETest}; use crate::e2e::select::{self, SelectE2ETest}; use crate::e2e::update::{self, UpdateE2ETest}; @@ -126,7 +127,10 @@ enum Command { /// E2E test for UPDATE statements with comprehensive validation E2eUpdate, - /// Run all E2E tests (SELECT, INSERT, and UPDATE) + /// E2E test for DELETE statements with comprehensive validation + E2eDelete, + + /// Run all E2E tests (SELECT, INSERT, UPDATE, and DELETE) E2eAll, } @@ -405,13 +409,44 @@ async fn e2e_update() -> Result<(), TesterError> { 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_all() -> Result<(), TesterError> { println!("\n========================================"); println!("Running ALL E2E Tests"); println!("========================================\n"); // Run INSERT tests - println!("[1/3] Running INSERT E2E tests..."); + println!("[1/4] Running INSERT E2E tests..."); match e2e_insert().await { Ok(()) => { println!("✓ INSERT E2E tests passed\n"); @@ -423,7 +458,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run SELECT tests - println!("[2/3] Running SELECT E2E tests..."); + println!("[2/4] Running SELECT E2E tests..."); match e2e_select().await { Ok(()) => { println!("✓ SELECT E2E tests passed\n"); @@ -435,7 +470,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run UPDATE tests - println!("[3/3] Running UPDATE E2E tests..."); + println!("[3/4] Running UPDATE E2E tests..."); match e2e_update().await { Ok(()) => { println!("✓ UPDATE E2E tests passed\n"); @@ -446,6 +481,18 @@ async fn e2e_all() -> Result<(), TesterError> { } } + // Run DELETE tests + println!("[4/4] 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); + } + } + println!("========================================"); println!("All E2E Tests Completed Successfully!"); println!("========================================"); @@ -522,6 +569,10 @@ async fn main() -> Result<(), TesterError> { e2e_update().await?; Ok(()) } + Command::E2eDelete => { + e2e_delete().await?; + Ok(()) + } Command::E2eAll => { e2e_all().await?; Ok(()) From 024b3f9e18e9400547c369b7e6d9da8258c5d452 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 12 Jan 2026 19:24:18 +0100 Subject: [PATCH 07/10] create table e2e --- tester/src/e2e/create_table.rs | 385 +++++++++++++++++++++++++++++++++ tester/src/e2e/mod.rs | 1 + tester/src/main.rs | 53 ++++- 3 files changed, 434 insertions(+), 5 deletions(-) create mode 100644 tester/src/e2e/create_table.rs diff --git a/tester/src/e2e/create_table.rs b/tester/src/e2e/create_table.rs new file mode 100644 index 0000000..c8e2f98 --- /dev/null +++ b/tester/src/e2e/create_table.rs @@ -0,0 +1,385 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, Response, StatementType}; + +use crate::{ + TesterError, + client::ReadResult, + suite::{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, +} + +pub struct E2ETestResult { + pub tests_passed: usize, +} + +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/mod.rs b/tester/src/e2e/mod.rs index f5c54d4..d3e5511 100644 --- a/tester/src/e2e/mod.rs +++ b/tester/src/e2e/mod.rs @@ -1,3 +1,4 @@ +pub mod create_table; pub mod delete; pub mod insert; pub mod response_helpers; diff --git a/tester/src/main.rs b/tester/src/main.rs index 675e23d..afa0a14 100644 --- a/tester/src/main.rs +++ b/tester/src/main.rs @@ -4,6 +4,7 @@ use std::time::Duration; use clap::{Parser, Subcommand}; use thiserror::Error; +use crate::e2e::create_table::{self, CreateTableE2ETest}; use crate::e2e::delete::{self, DeleteE2ETest}; use crate::e2e::insert::{self, InsertE2ETest}; use crate::e2e::select::{self, SelectE2ETest}; @@ -130,7 +131,10 @@ enum Command { /// E2E test for DELETE statements with comprehensive validation E2eDelete, - /// Run all E2E tests (SELECT, INSERT, UPDATE, and DELETE) + /// E2E test for CREATE TABLE statements with comprehensive validation + E2eCreateTable, + + /// Run all E2E tests (SELECT, INSERT, UPDATE, DELETE, and CREATE TABLE) E2eAll, } @@ -440,13 +444,48 @@ async fn e2e_delete() -> Result<(), TesterError> { 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_all() -> Result<(), TesterError> { println!("\n========================================"); println!("Running ALL E2E Tests"); println!("========================================\n"); + // Run CREATE TABLE tests + println!("[1/5] 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!("[1/4] Running INSERT E2E tests..."); + println!("[2/5] Running INSERT E2E tests..."); match e2e_insert().await { Ok(()) => { println!("✓ INSERT E2E tests passed\n"); @@ -458,7 +497,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run SELECT tests - println!("[2/4] Running SELECT E2E tests..."); + println!("[3/5] Running SELECT E2E tests..."); match e2e_select().await { Ok(()) => { println!("✓ SELECT E2E tests passed\n"); @@ -470,7 +509,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run UPDATE tests - println!("[3/4] Running UPDATE E2E tests..."); + println!("[4/5] Running UPDATE E2E tests..."); match e2e_update().await { Ok(()) => { println!("✓ UPDATE E2E tests passed\n"); @@ -482,7 +521,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run DELETE tests - println!("[4/4] Running DELETE E2E tests..."); + println!("[5/5] Running DELETE E2E tests..."); match e2e_delete().await { Ok(()) => { println!("✓ DELETE E2E tests passed\n"); @@ -573,6 +612,10 @@ async fn main() -> Result<(), TesterError> { e2e_delete().await?; Ok(()) } + Command::E2eCreateTable => { + e2e_create_table().await?; + Ok(()) + } Command::E2eAll => { e2e_all().await?; Ok(()) From 235fce2ba7f99220dfe25e91c5f8ab6593dec284 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 12 Jan 2026 19:38:58 +0100 Subject: [PATCH 08/10] e2e truncate and drop table --- tester/src/e2e/drop_table.rs | 309 +++++++++++++++++++++++++++++++ tester/src/e2e/mod.rs | 2 + tester/src/e2e/truncate_table.rs | 298 +++++++++++++++++++++++++++++ tester/src/main.rs | 98 +++++++++- 4 files changed, 701 insertions(+), 6 deletions(-) create mode 100644 tester/src/e2e/drop_table.rs create mode 100644 tester/src/e2e/truncate_table.rs diff --git a/tester/src/e2e/drop_table.rs b/tester/src/e2e/drop_table.rs new file mode 100644 index 0000000..eacbf0f --- /dev/null +++ b/tester/src/e2e/drop_table.rs @@ -0,0 +1,309 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, Response, StatementType}; + +use crate::{ + TesterError, + client::ReadResult, + suite::{Suite, default_client}, +}; + +use super::response_helpers::{ + expect_acknowledge, 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, +} + +pub struct E2ETestResult { + pub tests_passed: usize, +} + +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 + match expect_acknowledge(&mut client).await { + Ok(_) => match client.read_response().await? { + ReadResult::Response(Response::Error { .. }) => { + info!("✓ Table correctly dropped and not found"); + Ok(()) + } + _ => Err(TesterError::ServerError { + message: "Expected error when selecting from dropped table".to_string(), + }), + }, + Err(_) => { + 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 + match expect_acknowledge(&mut client).await { + Ok(_) => match client.read_response().await? { + ReadResult::Response(Response::Error { message, .. }) => { + info!( + "✓ Got expected error when dropping non-existent table: {}", + message + ); + Ok(()) + } + ReadResult::Response(Response::StatementCompleted { .. }) => { + error!("Dropping non-existent table should have failed but succeeded!"); + Err(TesterError::ServerError { + message: "Dropping non-existent table should have failed but succeeded" + .to_string(), + }) + } + _ => { + error!("Unexpected response type when dropping non-existent table"); + Err(TesterError::ServerError { + message: "Unexpected response type when dropping non-existent table" + .to_string(), + }) + } + }, + Err(_) => { + info!("✓ Drop non-existent table correctly rejected"); + 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/mod.rs b/tester/src/e2e/mod.rs index d3e5511..447fd92 100644 --- a/tester/src/e2e/mod.rs +++ b/tester/src/e2e/mod.rs @@ -1,6 +1,8 @@ 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/truncate_table.rs b/tester/src/e2e/truncate_table.rs new file mode 100644 index 0000000..fcfbd70 --- /dev/null +++ b/tester/src/e2e/truncate_table.rs @@ -0,0 +1,298 @@ +use log::{error, info}; +use protocol::{ColumnType, Request, StatementType}; + +use crate::{ + TesterError, + suite::{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, +} + +pub struct E2ETestResult { + pub tests_passed: usize, +} + +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/main.rs b/tester/src/main.rs index afa0a14..770105b 100644 --- a/tester/src/main.rs +++ b/tester/src/main.rs @@ -6,8 +6,10 @@ use thiserror::Error; 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}; @@ -134,7 +136,13 @@ enum Command { /// E2E test for CREATE TABLE statements with comprehensive validation E2eCreateTable, - /// Run all E2E tests (SELECT, INSERT, UPDATE, DELETE, and CREATE TABLE) + /// E2E test for TRUNCATE TABLE statements with comprehensive validation + E2eTruncateTable, + + /// E2E test for DROP TABLE statements with comprehensive validation + E2eDropTable, + + /// Run all E2E tests (SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, TRUNCATE TABLE, and DROP TABLE) E2eAll, } @@ -467,13 +475,59 @@ async fn e2e_create_table() -> Result<(), TesterError> { 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_all() -> Result<(), TesterError> { println!("\n========================================"); println!("Running ALL E2E Tests"); println!("========================================\n"); // Run CREATE TABLE tests - println!("[1/5] Running CREATE TABLE E2E tests..."); + println!("[1/7] Running CREATE TABLE E2E tests..."); match e2e_create_table().await { Ok(()) => { println!("✓ CREATE TABLE E2E tests passed\n"); @@ -485,7 +539,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run INSERT tests - println!("[2/5] Running INSERT E2E tests..."); + println!("[2/7] Running INSERT E2E tests..."); match e2e_insert().await { Ok(()) => { println!("✓ INSERT E2E tests passed\n"); @@ -497,7 +551,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run SELECT tests - println!("[3/5] Running SELECT E2E tests..."); + println!("[3/7] Running SELECT E2E tests..."); match e2e_select().await { Ok(()) => { println!("✓ SELECT E2E tests passed\n"); @@ -509,7 +563,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run UPDATE tests - println!("[4/5] Running UPDATE E2E tests..."); + println!("[4/7] Running UPDATE E2E tests..."); match e2e_update().await { Ok(()) => { println!("✓ UPDATE E2E tests passed\n"); @@ -521,7 +575,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run DELETE tests - println!("[5/5] Running DELETE E2E tests..."); + println!("[5/7] Running DELETE E2E tests..."); match e2e_delete().await { Ok(()) => { println!("✓ DELETE E2E tests passed\n"); @@ -532,6 +586,30 @@ async fn e2e_all() -> Result<(), TesterError> { } } + // Run TRUNCATE TABLE tests + println!("[6/7] 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/7] 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); + } + } + println!("========================================"); println!("All E2E Tests Completed Successfully!"); println!("========================================"); @@ -616,6 +694,14 @@ async fn main() -> Result<(), TesterError> { e2e_create_table().await?; Ok(()) } + Command::E2eTruncateTable => { + e2e_truncate_table().await?; + Ok(()) + } + Command::E2eDropTable => { + e2e_drop_table().await?; + Ok(()) + } Command::E2eAll => { e2e_all().await?; Ok(()) From c9705cf4976d08312db0198b1e64f44b3993d985 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 12 Jan 2026 19:44:54 +0100 Subject: [PATCH 09/10] one common result structure --- tester/src/e2e/create_table.rs | 6 +----- tester/src/e2e/delete.rs | 6 +----- tester/src/e2e/drop_table.rs | 6 +----- tester/src/e2e/insert.rs | 6 +----- tester/src/e2e/select.rs | 6 +----- tester/src/e2e/truncate_table.rs | 6 +----- tester/src/e2e/update.rs | 6 +----- tester/src/suite.rs | 4 ++++ 8 files changed, 11 insertions(+), 35 deletions(-) diff --git a/tester/src/e2e/create_table.rs b/tester/src/e2e/create_table.rs index c8e2f98..36ef832 100644 --- a/tester/src/e2e/create_table.rs +++ b/tester/src/e2e/create_table.rs @@ -4,7 +4,7 @@ use protocol::{ColumnType, Request, Response, StatementType}; use crate::{ TesterError, client::ReadResult, - suite::{Suite, default_client}, + suite::{E2ETestResult, Suite, default_client}, }; use super::response_helpers::{ @@ -25,10 +25,6 @@ pub struct Cleanup { pub database_name: String, } -pub struct E2ETestResult { - pub tests_passed: usize, -} - impl Suite for CreateTableE2ETest { type SetupArgs = Setup; diff --git a/tester/src/e2e/delete.rs b/tester/src/e2e/delete.rs index 07a7ce3..785df80 100644 --- a/tester/src/e2e/delete.rs +++ b/tester/src/e2e/delete.rs @@ -3,7 +3,7 @@ use protocol::{ColumnType, Request, StatementType}; use crate::{ TesterError, - suite::{Suite, default_client}, + suite::{E2ETestResult, Suite, default_client}, }; use super::response_helpers::{validate_non_select_statement, validate_select_query}; @@ -148,10 +148,6 @@ pub struct Cleanup { pub database_name: String, } -pub struct E2ETestResult { - pub tests_passed: usize, -} - impl Suite for DeleteE2ETest { type SetupArgs = Setup; diff --git a/tester/src/e2e/drop_table.rs b/tester/src/e2e/drop_table.rs index eacbf0f..9773f0d 100644 --- a/tester/src/e2e/drop_table.rs +++ b/tester/src/e2e/drop_table.rs @@ -4,7 +4,7 @@ use protocol::{ColumnType, Request, Response, StatementType}; use crate::{ TesterError, client::ReadResult, - suite::{Suite, default_client}, + suite::{E2ETestResult, Suite, default_client}, }; use super::response_helpers::{ @@ -25,10 +25,6 @@ pub struct Cleanup { pub database_name: String, } -pub struct E2ETestResult { - pub tests_passed: usize, -} - impl Suite for DropTableE2ETest { type SetupArgs = Setup; diff --git a/tester/src/e2e/insert.rs b/tester/src/e2e/insert.rs index b246c9c..50a7b7b 100644 --- a/tester/src/e2e/insert.rs +++ b/tester/src/e2e/insert.rs @@ -3,7 +3,7 @@ use protocol::{ColumnType, Request, StatementType}; use crate::{ TesterError, - suite::{Suite, default_client}, + suite::{E2ETestResult, Suite, default_client}, }; use super::response_helpers::{ @@ -133,10 +133,6 @@ pub struct Cleanup { pub database_name: String, } -pub struct E2ETestResult { - pub tests_passed: usize, -} - impl Suite for InsertE2ETest { type SetupArgs = Setup; diff --git a/tester/src/e2e/select.rs b/tester/src/e2e/select.rs index 2fe63ca..53cadff 100644 --- a/tester/src/e2e/select.rs +++ b/tester/src/e2e/select.rs @@ -5,7 +5,7 @@ use protocol::{ColumnType, Record, Request, StatementType}; use crate::{ TesterError, - suite::{Suite, default_client}, + suite::{E2ETestResult, Suite, default_client}, }; use super::response_helpers::{ @@ -234,10 +234,6 @@ pub struct Cleanup { pub database_name: String, } -pub struct E2ETestResult { - pub tests_passed: usize, -} - impl Suite for SelectE2ETest { type SetupArgs = Setup; diff --git a/tester/src/e2e/truncate_table.rs b/tester/src/e2e/truncate_table.rs index fcfbd70..f2807c1 100644 --- a/tester/src/e2e/truncate_table.rs +++ b/tester/src/e2e/truncate_table.rs @@ -3,7 +3,7 @@ use protocol::{ColumnType, Request, StatementType}; use crate::{ TesterError, - suite::{Suite, default_client}, + suite::{E2ETestResult, Suite, default_client}, }; use super::response_helpers::{validate_non_select_statement, validate_select_query}; @@ -22,10 +22,6 @@ pub struct Cleanup { pub database_name: String, } -pub struct E2ETestResult { - pub tests_passed: usize, -} - impl Suite for TruncateTableE2ETest { type SetupArgs = Setup; diff --git a/tester/src/e2e/update.rs b/tester/src/e2e/update.rs index 4893bea..c2394d8 100644 --- a/tester/src/e2e/update.rs +++ b/tester/src/e2e/update.rs @@ -5,7 +5,7 @@ use crate::{ TesterError, client::ReadResult, e2e::response_helpers::expect_acknowledge, - suite::{Suite, default_client}, + suite::{E2ETestResult, Suite, default_client}, }; use super::response_helpers::{ @@ -153,10 +153,6 @@ pub struct Cleanup { pub database_name: String, } -pub struct E2ETestResult { - pub tests_passed: usize, -} - impl Suite for UpdateE2ETest { type SetupArgs = Setup; 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; From 0371c5ad9171697395ea5b47a7fef24a786ac399 Mon Sep 17 00:00:00 2001 From: kTrzcinskii Date: Mon, 12 Jan 2026 20:17:13 +0100 Subject: [PATCH 10/10] e2e alter table --- tester/src/e2e/alter_table.rs | 953 +++++++++++++++++++++++++++++ tester/src/e2e/drop_table.rs | 60 +- tester/src/e2e/mod.rs | 1 + tester/src/e2e/response_helpers.rs | 43 ++ tester/src/main.rs | 59 +- 5 files changed, 1058 insertions(+), 58 deletions(-) create mode 100644 tester/src/e2e/alter_table.rs 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/drop_table.rs b/tester/src/e2e/drop_table.rs index 9773f0d..86b1353 100644 --- a/tester/src/e2e/drop_table.rs +++ b/tester/src/e2e/drop_table.rs @@ -1,15 +1,12 @@ use log::{error, info}; -use protocol::{ColumnType, Request, Response, StatementType}; +use protocol::{ColumnType, Request, StatementType}; use crate::{ TesterError, - client::ReadResult, suite::{E2ETestResult, Suite, default_client}, }; -use super::response_helpers::{ - expect_acknowledge, validate_non_select_statement, validate_select_query, -}; +use super::response_helpers::{expect_error, validate_non_select_statement, validate_select_query}; pub struct DropTableE2ETest; @@ -139,21 +136,9 @@ async fn test_drop_empty_table(args: &Test) -> Result<(), TesterError> { .await?; // Expect error since table doesn't exist - match expect_acknowledge(&mut client).await { - Ok(_) => match client.read_response().await? { - ReadResult::Response(Response::Error { .. }) => { - info!("✓ Table correctly dropped and not found"); - Ok(()) - } - _ => Err(TesterError::ServerError { - message: "Expected error when selecting from dropped table".to_string(), - }), - }, - Err(_) => { - info!("✓ Table correctly dropped and not found"); - Ok(()) - } - } + expect_error(&mut client).await?; + info!("✓ Table correctly dropped and not found"); + Ok(()) } /// Test 2: Drop table with data @@ -237,35 +222,12 @@ async fn test_drop_nonexistent_table(args: &Test) -> Result<(), TesterError> { .await?; // We expect an error here - match expect_acknowledge(&mut client).await { - Ok(_) => match client.read_response().await? { - ReadResult::Response(Response::Error { message, .. }) => { - info!( - "✓ Got expected error when dropping non-existent table: {}", - message - ); - Ok(()) - } - ReadResult::Response(Response::StatementCompleted { .. }) => { - error!("Dropping non-existent table should have failed but succeeded!"); - Err(TesterError::ServerError { - message: "Dropping non-existent table should have failed but succeeded" - .to_string(), - }) - } - _ => { - error!("Unexpected response type when dropping non-existent table"); - Err(TesterError::ServerError { - message: "Unexpected response type when dropping non-existent table" - .to_string(), - }) - } - }, - Err(_) => { - info!("✓ Drop non-existent table correctly rejected"); - Ok(()) - } - } + let message = expect_error(&mut client).await?; + info!( + "✓ Got expected error when dropping non-existent table: {}", + message + ); + Ok(()) } /// Test 4: Drop multiple tables diff --git a/tester/src/e2e/mod.rs b/tester/src/e2e/mod.rs index 447fd92..56c5140 100644 --- a/tester/src/e2e/mod.rs +++ b/tester/src/e2e/mod.rs @@ -1,3 +1,4 @@ +pub mod alter_table; pub mod create_table; pub mod delete; pub mod drop_table; diff --git a/tester/src/e2e/response_helpers.rs b/tester/src/e2e/response_helpers.rs index f1319d1..639c5cb 100644 --- a/tester/src/e2e/response_helpers.rs +++ b/tester/src/e2e/response_helpers.rs @@ -447,3 +447,46 @@ pub fn extract_f64(record: &Record, index: usize) -> Result { } } } + +/// 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/main.rs b/tester/src/main.rs index 770105b..73eeab1 100644 --- a/tester/src/main.rs +++ b/tester/src/main.rs @@ -4,6 +4,7 @@ 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}; @@ -142,7 +143,10 @@ enum Command { /// E2E test for DROP TABLE statements with comprehensive validation E2eDropTable, - /// Run all E2E tests (SELECT, INSERT, UPDATE, DELETE, CREATE TABLE, TRUNCATE TABLE, and DROP TABLE) + /// 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, } @@ -521,13 +525,36 @@ async fn e2e_drop_table() -> Result<(), TesterError> { 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/7] Running CREATE TABLE E2E tests..."); + println!("[1/8] Running CREATE TABLE E2E tests..."); match e2e_create_table().await { Ok(()) => { println!("✓ CREATE TABLE E2E tests passed\n"); @@ -539,7 +566,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run INSERT tests - println!("[2/7] Running INSERT E2E tests..."); + println!("[2/8] Running INSERT E2E tests..."); match e2e_insert().await { Ok(()) => { println!("✓ INSERT E2E tests passed\n"); @@ -551,7 +578,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run SELECT tests - println!("[3/7] Running SELECT E2E tests..."); + println!("[3/8] Running SELECT E2E tests..."); match e2e_select().await { Ok(()) => { println!("✓ SELECT E2E tests passed\n"); @@ -563,7 +590,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run UPDATE tests - println!("[4/7] Running UPDATE E2E tests..."); + println!("[4/8] Running UPDATE E2E tests..."); match e2e_update().await { Ok(()) => { println!("✓ UPDATE E2E tests passed\n"); @@ -575,7 +602,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run DELETE tests - println!("[5/7] Running DELETE E2E tests..."); + println!("[5/8] Running DELETE E2E tests..."); match e2e_delete().await { Ok(()) => { println!("✓ DELETE E2E tests passed\n"); @@ -587,7 +614,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run TRUNCATE TABLE tests - println!("[6/7] Running TRUNCATE TABLE E2E tests..."); + println!("[6/8] Running TRUNCATE TABLE E2E tests..."); match e2e_truncate_table().await { Ok(()) => { println!("✓ TRUNCATE TABLE E2E tests passed\n"); @@ -599,7 +626,7 @@ async fn e2e_all() -> Result<(), TesterError> { } // Run DROP TABLE tests - println!("[7/7] Running DROP TABLE E2E tests..."); + println!("[7/8] Running DROP TABLE E2E tests..."); match e2e_drop_table().await { Ok(()) => { println!("✓ DROP TABLE E2E tests passed\n"); @@ -609,7 +636,17 @@ async fn e2e_all() -> Result<(), TesterError> { 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!("========================================"); @@ -702,6 +739,10 @@ async fn main() -> Result<(), TesterError> { e2e_drop_table().await?; Ok(()) } + Command::E2eAlterTable => { + e2e_alter_table().await?; + Ok(()) + } Command::E2eAll => { e2e_all().await?; Ok(())