From 24c2376ea1f57ae4b2693c259482c310d51ae991 Mon Sep 17 00:00:00 2001 From: filipriec Date: Thu, 17 Jul 2025 22:06:53 +0200 Subject: [PATCH] crucial self reference allowed --- Cargo.lock | 1 + server/Cargo.toml | 1 + .../handlers/dependency_analyzer.rs | 31 +- .../table_script/handlers/dependency_utils.rs | 3 +- server/tests/mod.rs | 4 +- .../comprehensive_error_scenarios_tests.rs | 499 ++++++++++++++++ .../mathematical_operations_tests.rs | 453 ++++++++++++++ server/tests/table_script/mod.rs | 18 + .../post_scripts_integration_tests.rs | 557 ++++++++++++++++++ .../tests/table_script/post_scripts_tests.rs | 0 .../table_script/prohibited_types_test.rs | 481 +++++++++------ .../type_safety_comprehensive_tests.rs | 454 ++++++++++++++ 12 files changed, 2312 insertions(+), 190 deletions(-) create mode 100644 server/tests/table_script/comprehensive_error_scenarios_tests.rs create mode 100644 server/tests/table_script/mathematical_operations_tests.rs create mode 100644 server/tests/table_script/mod.rs create mode 100644 server/tests/table_script/post_scripts_integration_tests.rs create mode 100644 server/tests/table_script/post_scripts_tests.rs create mode 100644 server/tests/table_script/type_safety_comprehensive_tests.rs diff --git a/Cargo.lock b/Cargo.lock index 1ed7165..9a4fcd4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3006,6 +3006,7 @@ dependencies = [ "thiserror", "time", "tokio", + "tokio-test", "tonic", "tonic-reflection", "tracing", diff --git a/server/Cargo.toml b/server/Cargo.toml index 1fdfec0..d7ded2b 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -50,3 +50,4 @@ rstest = "0.25.0" lazy_static = "1.5.0" rand = "0.9.1" futures = "0.3.31" +tokio-test = "0.4.4" diff --git a/server/src/table_script/handlers/dependency_analyzer.rs b/server/src/table_script/handlers/dependency_analyzer.rs index 1db3def..8781f40 100644 --- a/server/src/table_script/handlers/dependency_analyzer.rs +++ b/server/src/table_script/handlers/dependency_analyzer.rs @@ -273,11 +273,13 @@ impl DependencyAnalyzer { /// Validates that structured table access (steel_get_column functions) respects link constraints /// Raw SQL access (steel_query_sql) is allowed to reference any table - /// + /// SELF-REFERENCES are always allowed (table can access its own columns) + /// /// Example: + /// - Table A can ALWAYS use: (steel_get_column "table_a" "column_name") ✅ (self-reference) /// - Table A is linked to Table B via table_definition_links /// - Script for Table A can use: (steel_get_column "table_b" "column_name") ✅ - /// - Script for Table A CANNOT use: (steel_get_column "table_c" "column_name") ❌ + /// - Script for Table A CANNOT use: (steel_get_column "table_c" "column_name") ❌ /// - Script for Table A CAN use: (steel_query_sql "SELECT * FROM table_c") ✅ async fn validate_link_constraints( &self, @@ -285,6 +287,15 @@ impl DependencyAnalyzer { source_table_id: i64, dependencies: &[Dependency], ) -> Result<(), DependencyError> { + // Get the current table name for self-reference checking + let current_table_name = sqlx::query_scalar!( + "SELECT table_name FROM table_definitions WHERE id = $1", + source_table_id + ) + .fetch_one(&mut **tx) + .await + .map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?; + // Get all valid linked tables for the source table let linked_tables = sqlx::query!( r#"SELECT td.table_name, tdl.is_required @@ -298,30 +309,24 @@ impl DependencyAnalyzer { .map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?; // Create a set of allowed table names for quick lookup - let allowed_tables: std::collections::HashSet = linked_tables + let mut allowed_tables: std::collections::HashSet = linked_tables .into_iter() .map(|row| row.table_name) .collect(); - // Get the current table name for better error messages - let current_table_name = sqlx::query_scalar!( - "SELECT table_name FROM table_definitions WHERE id = $1", - source_table_id - ) - .fetch_one(&mut **tx) - .await - .map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?; + // ALWAYS allow self-references + allowed_tables.insert(current_table_name.clone()); // Validate each dependency for dep in dependencies { match &dep.dependency_type { - // Structured access must respect link constraints + // Structured access must respect link constraints (but self-references are always allowed) DependencyType::ColumnAccess { column } | DependencyType::IndexedAccess { column, .. } => { if !allowed_tables.contains(&dep.target_table) { return Err(DependencyError::InvalidTableReference { table_name: dep.target_table.clone(), script_context: format!( - "Table '{}' is not linked to '{}'. Add a link in the table definition to access '{}' via steel_get_column functions. Column attempted: '{}'", + "Table '{}' is not linked to '{}'. Add a link in the table definition to access '{}' via steel_get_column functions. Column attempted: '{}'. Note: Self-references are always allowed.", dep.target_table, current_table_name, dep.target_table, diff --git a/server/src/table_script/handlers/dependency_utils.rs b/server/src/table_script/handlers/dependency_utils.rs index d63ef9b..09a38cd 100644 --- a/server/src/table_script/handlers/dependency_utils.rs +++ b/server/src/table_script/handlers/dependency_utils.rs @@ -223,6 +223,7 @@ mod tests { let cycle = detect_cycle_dfs(1, &graph, &mut visited, &mut rec_stack, &table_names); assert!(cycle.is_some()); - assert!(cycle.unwrap().contains("table_a") && cycle.unwrap().contains("table_b")); + let cycle_str = cycle.unwrap(); + assert!(cycle_str.contains("table_a") && cycle_str.contains("table_b")); } } diff --git a/server/tests/mod.rs b/server/tests/mod.rs index 09829a3..1a9e1dc 100644 --- a/server/tests/mod.rs +++ b/server/tests/mod.rs @@ -1,4 +1,6 @@ // tests/mod.rs -pub mod tables_data; + +// pub mod tables_data; pub mod common; +pub mod table_script; // pub mod table_definition; diff --git a/server/tests/table_script/comprehensive_error_scenarios_tests.rs b/server/tests/table_script/comprehensive_error_scenarios_tests.rs new file mode 100644 index 0000000..d6370bb --- /dev/null +++ b/server/tests/table_script/comprehensive_error_scenarios_tests.rs @@ -0,0 +1,499 @@ +// tests/table_script/comprehensive_error_scenarios_tests.rs + +use crate::common::setup_isolated_db; +use multieko2_server::table_script::handlers::post_table_script::post_table_script; +use common::proto::multieko2::table_script::PostTableScriptRequest; +use rstest::*; +use serde_json::json; +use sqlx::PgPool; + +/// Helper function to create a test table with specified columns +async fn create_test_table( + pool: &PgPool, + schema_id: i64, + table_name: &str, + columns: Vec<(&str, &str)>, +) -> i64 { + let column_definitions: Vec = columns + .iter() + .map(|(name, type_def)| format!("\"{}\" {}", name, type_def)) + .collect(); + + let columns_json = json!(column_definitions); + let indexes_json = json!([]); + + sqlx::query_scalar!( + r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes) + VALUES ($1, $2, $3, $4) RETURNING id"#, + schema_id, + table_name, + columns_json, + indexes_json + ) + .fetch_one(pool) + .await + .expect("Failed to create test table") +} + +/// Helper function to get default schema ID +async fn get_default_schema_id(pool: &PgPool) -> i64 { + sqlx::query_scalar!("SELECT id FROM schemas WHERE name = 'default'") + .fetch_one(pool) + .await + .expect("Failed to get default schema ID") +} + +/// Test fixture providing detailed error scenarios +#[fixture] +#[once] +pub fn error_scenarios() -> Vec<(&'static str, &'static str, Vec<&'static str>, &'static str)> { + vec![ + // [target_column, script, expected_error_keywords, description] + ( + "bigint_target", + r#"(+ "10" "20")"#, + vec!["prohibited", "BIGINT", "target"], + "BIGINT target column should be rejected" + ), + ( + "date_target", + r#"(+ "10" "20")"#, + vec!["prohibited", "DATE", "target"], + "DATE target column should be rejected" + ), + ( + "timestamp_target", + r#"(+ "10" "20")"#, + vec!["prohibited", "TIMESTAMPTZ", "target"], + "TIMESTAMPTZ target column should be rejected" + ), + ( + "valid_numeric", + r#"(+ (steel_get_column "error_table" "text_col") "10")"#, + vec!["mathematical", "TEXT", "operations"], + "TEXT in mathematical operations should be rejected" + ), + ( + "valid_numeric", + r#"(* (steel_get_column "error_table" "boolean_col") "5")"#, + vec!["mathematical", "BOOLEAN", "operations"], + "BOOLEAN in mathematical operations should be rejected" + ), + ( + "valid_numeric", + r#"(/ (steel_get_column "error_table" "bigint_col") "2")"#, + vec!["mathematical", "BIGINT", "operations"], + "BIGINT in mathematical operations should be rejected" + ), + ( + "valid_numeric", + r#"(sqrt (steel_get_column "error_table" "date_col"))"#, + vec!["mathematical", "DATE", "operations"], + "DATE in mathematical operations should be rejected" + ), + ( + "valid_numeric", + r#"(+ (steel_get_column "nonexistent_table" "column") "10")"#, + vec!["table", "does not exist", "nonexistent_table"], + "Nonexistent table should be rejected" + ), + ( + "valid_numeric", + r#"(+ (steel_get_column "error_table" "nonexistent_column") "10")"#, + vec!["column", "does not exist", "nonexistent_column"], + "Nonexistent column should be rejected" + ), + ] +} + +#[rstest] +#[tokio::test] +async fn test_comprehensive_error_scenarios( + error_scenarios: &Vec<(&'static str, &'static str, Vec<&'static str>, &'static str)>, +) { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create comprehensive error test table + let columns = vec![ + // Valid types + ("valid_numeric", "NUMERIC(10, 2)"), + ("valid_integer", "INTEGER"), + + // Invalid for math operations + ("text_col", "TEXT"), + ("boolean_col", "BOOLEAN"), + ("bigint_col", "BIGINT"), + ("date_col", "DATE"), + ("timestamp_col", "TIMESTAMPTZ"), + + // Invalid target types + ("bigint_target", "BIGINT"), + ("date_target", "DATE"), + ("timestamp_target", "TIMESTAMPTZ"), + ]; + + let table_id = create_test_table(&pool, schema_id, "error_table", columns).await; + + for (target_column, script, expected_keywords, description) in error_scenarios { + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: target_column.to_string(), + script: script.to_string(), + description: Some(description.to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_err(), "{}", description); + + let error_message = result.unwrap_err().to_string().to_lowercase(); + + for keyword in expected_keywords { + assert!( + error_message.contains(&keyword.to_lowercase()), + "Error message should contain '{}' for: {}. Got: {}", + keyword, description, error_message + ); + } + } +} + +#[rstest] +#[case::empty_script("", "Empty script should be rejected")] +#[case::malformed_parentheses("(+ 10 20", "Unbalanced parentheses should be rejected")] +#[case::invalid_function("(invalid_function 10 20)", "Invalid function should be rejected")] +#[case::mixed_quotes(r#"(' "10" 20)"#, "Mixed quote types should be handled")] +#[tokio::test] +async fn test_malformed_script_scenarios( + #[case] script: &str, + #[case] description: &str, +) { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![("result", "NUMERIC(10, 2)")]; + let table_id = create_test_table(&pool, schema_id, "malformed_test", columns).await; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script: script.to_string(), + description: Some(description.to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_err(), "{}", description); +} + +#[tokio::test] +async fn test_dependency_cycle_detection() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create two tables for dependency testing + let table_a_columns = vec![ + ("value_a", "NUMERIC(10, 2)"), + ("result_a", "NUMERIC(10, 2)"), + ]; + let table_a_id = create_test_table(&pool, schema_id, "table_a", table_a_columns).await; + + let table_b_columns = vec![ + ("value_b", "NUMERIC(10, 2)"), + ("result_b", "NUMERIC(10, 2)"), + ]; + let table_b_id = create_test_table(&pool, schema_id, "table_b", table_b_columns).await; + + // Create first dependency: table_a.result_a depends on table_b.value_b + let script_a = r#"(+ (steel_get_column "table_b" "value_b") "10")"#; + let request_a = PostTableScriptRequest { + table_definition_id: table_a_id, + target_column: "result_a".to_string(), + script: script_a.to_string(), + description: Some("First dependency".to_string()), + }; + + let result_a = post_table_script(&pool, request_a).await; + assert!(result_a.is_ok(), "First dependency should succeed"); + + // Try to create circular dependency: table_b.result_b depends on table_a.result_a + let script_b = r#"(* (steel_get_column "table_a" "result_a") "2")"#; + let request_b = PostTableScriptRequest { + table_definition_id: table_b_id, + target_column: "result_b".to_string(), + script: script_b.to_string(), + description: Some("Circular dependency attempt".to_string()), + }; + + let result_b = post_table_script(&pool, request_b).await; + + // Depending on implementation, this should either succeed or detect the cycle + match result_b { + Ok(_) => { + // Implementation allows this pattern + } + Err(error) => { + // Implementation detects circular dependencies + let error_msg = error.to_string(); + assert!( + error_msg.contains("cycle") || error_msg.contains("circular"), + "Circular dependency should be detected properly: {}", + error_msg + ); + } + } +} + +#[rstest] +#[case::extremely_long_column_name("a".repeat(100), "Extremely long column name")] +#[case::special_chars_table("test-table", "Table with special characters")] +#[case::unicode_column("测试列", "Unicode column name")] +#[tokio::test] +async fn test_edge_case_identifiers( + #[case] identifier: String, + #[case] description: &str, +) { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![("result", "NUMERIC(10, 2)")]; + let table_id = create_test_table(&pool, schema_id, "identifier_test", columns).await; + + // Test with edge case identifier in script + let script = format!(r#"(+ "10" "20")"#); // Simple script, focus on identifier handling + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script, + description: Some(description.to_string()), + }; + + let result = post_table_script(&pool, request).await; + + // The behavior may vary based on identifier validation rules + match result { + Ok(_) => { + // Identifier was accepted + } + Err(error) => { + // Should provide meaningful error message + let error_msg = error.to_string(); + assert!( + !error_msg.contains("panic") && !error_msg.contains("internal error"), + "Should provide meaningful error for edge case identifier: {}", + error_msg + ); + } + } +} + +#[tokio::test] +async fn test_sql_injection_prevention() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![("result", "NUMERIC(10, 2)")]; + let table_id = create_test_table(&pool, schema_id, "injection_test", columns).await; + + // Attempt SQL injection through script content + let malicious_scripts = vec![ + r#"(+ "10"; DROP TABLE injection_test; --" "20")"#, + r#"(steel_query_sql "SELECT * FROM schemas; DROP TABLE injection_test;")"#, + r#"(steel_get_column "injection_test\"; DROP TABLE schemas; --" "result")"#, + ]; + + for script in malicious_scripts { + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script: script.to_string(), + description: Some("SQL injection attempt".to_string()), + }; + + let result = post_table_script(&pool, request).await; + + // Should either reject the script or handle it safely + match result { + Ok(_) => { + // If accepted, verify that no injection occurred by checking table still exists + let table_exists = sqlx::query_scalar!( + "SELECT COUNT(*) FROM table_definitions WHERE id = $1", + table_id + ) + .fetch_one(&pool) + .await + .expect("Should be able to check table existence"); + + assert_eq!(table_exists, Some(1), "Table should still exist after potential injection attempt"); + } + Err(_) => { + // Script was rejected, which is also acceptable + } + } + } +} + +#[tokio::test] +async fn test_performance_with_deeply_nested_expressions() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("x", "NUMERIC(15, 8)"), + ("performance_result", "NUMERIC(25, 12)"), + ]; + let table_id = create_test_table(&pool, schema_id, "performance_test", columns).await; + + // Create a very deeply nested expression + let deeply_nested_script = r#" + (+ + (+ + (+ + (+ (steel_get_column "performance_test" "x") "1") + (+ (steel_get_column "performance_test" "x") "2")) + (+ + (+ (steel_get_column "performance_test" "x") "3") + (+ (steel_get_column "performance_test" "x") "4"))) + (+ + (+ + (+ (steel_get_column "performance_test" "x") "5") + (+ (steel_get_column "performance_test" "x") "6")) + (+ + (+ (steel_get_column "performance_test" "x") "7") + (+ (steel_get_column "performance_test" "x") "8")))) + "#; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "performance_result".to_string(), + script: deeply_nested_script.to_string(), + description: Some("Performance test with deeply nested expressions".to_string()), + }; + + let start_time = std::time::Instant::now(); + let result = post_table_script(&pool, request).await; + let duration = start_time.elapsed(); + + assert!(result.is_ok(), "Deeply nested expression should succeed"); + assert!( + duration.as_millis() < 5000, + "Script validation should complete within reasonable time (5s), took: {:?}", + duration + ); +} + +#[tokio::test] +async fn test_concurrent_script_creation() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("value", "NUMERIC(10, 2)"), + ("result1", "NUMERIC(10, 2)"), + ("result2", "NUMERIC(10, 2)"), + ("result3", "NUMERIC(10, 2)"), + ]; + let table_id = create_test_table(&pool, schema_id, "concurrent_test", columns).await; + + // Create multiple scripts concurrently on different columns + let handles = vec![ + tokio::spawn({ + let pool = pool.clone(); + async move { + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result1".to_string(), + script: r#"(+ (steel_get_column "concurrent_test" "value") "10")"#.to_string(), + description: Some("Concurrent script 1".to_string()), + }; + post_table_script(&pool, request).await + } + }), + tokio::spawn({ + let pool = pool.clone(); + async move { + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result2".to_string(), + script: r#"(* (steel_get_column "concurrent_test" "value") "2")"#.to_string(), + description: Some("Concurrent script 2".to_string()), + }; + post_table_script(&pool, request).await + } + }), + tokio::spawn({ + let pool = pool.clone(); + async move { + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result3".to_string(), + script: r#"(/ (steel_get_column "concurrent_test" "value") "3")"#.to_string(), + description: Some("Concurrent script 3".to_string()), + }; + post_table_script(&pool, request).await + } + }), + ]; + + // Wait for all concurrent operations to complete + let results = futures::future::join_all(handles).await; + + // All should succeed + for (i, result) in results.into_iter().enumerate() { + let script_result = result.expect("Task should not panic"); + assert!(script_result.is_ok(), "Concurrent script {} should succeed", i + 1); + } +} + +#[tokio::test] +async fn test_error_message_localization_and_clarity() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("text_col", "TEXT"), + ("result", "NUMERIC(10, 2)"), + ]; + let table_id = create_test_table(&pool, schema_id, "error_clarity_test", columns).await; + + // Test that error messages are clear and specific + let script = r#"(+ (steel_get_column "error_clarity_test" "text_col") "10")"#; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script: script.to_string(), + description: Some("Error message clarity test".to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_err(), "Should reject TEXT in mathematical operations"); + + let error_message = result.unwrap_err().to_string(); + + // Verify error message quality + assert!( + error_message.len() > 20, + "Error message should be descriptive, got: {}", + error_message + ); + + assert!( + !error_message.contains("panic") && !error_message.contains("unwrap"), + "Error message should not expose internal implementation details: {}", + error_message + ); + + // Should mention specific problematic elements + let error_lower = error_message.to_lowercase(); + let relevant_keywords = vec!["text", "mathematical", "operation", "column"]; + let keyword_count = relevant_keywords.iter() + .filter(|&&keyword| error_lower.contains(keyword)) + .count(); + + assert!( + keyword_count >= 2, + "Error message should contain relevant keywords. Got: {}", + error_message + ); +} diff --git a/server/tests/table_script/mathematical_operations_tests.rs b/server/tests/table_script/mathematical_operations_tests.rs new file mode 100644 index 0000000..6620948 --- /dev/null +++ b/server/tests/table_script/mathematical_operations_tests.rs @@ -0,0 +1,453 @@ +// tests/table_script/mathematical_operations_tests.rs + +use crate::common::setup_isolated_db; +use multieko2_server::table_script::handlers::post_table_script::post_table_script; +use common::proto::multieko2::table_script::PostTableScriptRequest; +use rstest::*; +use serde_json::json; +use sqlx::PgPool; + +/// Helper function to create a test table with specified columns +async fn create_test_table( + pool: &PgPool, + schema_id: i64, + table_name: &str, + columns: Vec<(&str, &str)>, +) -> i64 { + let column_definitions: Vec = columns + .iter() + .map(|(name, type_def)| format!("\"{}\" {}", name, type_def)) + .collect(); + + let columns_json = json!(column_definitions); + let indexes_json = json!([]); + + sqlx::query_scalar!( + r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes) + VALUES ($1, $2, $3, $4) RETURNING id"#, + schema_id, + table_name, + columns_json, + indexes_json + ) + .fetch_one(pool) + .await + .expect("Failed to create test table") +} + +/// Helper function to get default schema ID +async fn get_default_schema_id(pool: &PgPool) -> i64 { + sqlx::query_scalar!("SELECT id FROM schemas WHERE name = 'default'") + .fetch_one(pool) + .await + .expect("Failed to get default schema ID") +} + +/// Test fixture providing all mathematical operations that should work with steel_decimal +#[fixture] +#[once] +pub fn steel_decimal_operations() -> Vec<(&'static str, &'static str, &'static str)> { + vec![ + // Basic arithmetic + ("+", "addition", "binary"), + ("-", "subtraction", "binary"), + ("*", "multiplication", "binary"), + ("/", "division", "binary"), + + // Advanced math + ("sqrt", "square_root", "unary"), + ("abs", "absolute_value", "unary"), + ("min", "minimum", "binary"), + ("max", "maximum", "binary"), + ("pow", "power", "binary"), + + // Comparison operations + (">", "greater_than", "binary"), + ("<", "less_than", "binary"), + ("=", "equals", "binary"), + (">=", "greater_equal", "binary"), + ("<=", "less_equal", "binary"), + ] +} + +/// Test fixture providing precision test scenarios +#[fixture] +#[once] +pub fn precision_scenarios() -> Vec<(&'static str, &'static str, &'static str)> { + vec![ + ("NUMERIC(5, 2)", "999.99", "Low precision"), + ("NUMERIC(10, 4)", "999999.9999", "Medium precision"), + ("NUMERIC(28, 15)", "9999999999999.999999999999999", "High precision"), + ("NUMERIC(14, 4)", "9999999999.9999", "Currency precision"), + ("NUMERIC(5, 4)", "9.9999", "Percentage precision"), + ] +} + +#[rstest] +#[case::basic_addition("+", "10", "20")] +#[case::decimal_multiplication("*", "100.50", "2.5")] +#[case::high_precision_division("/", "123.456789012345", "3")] +#[case::scientific_notation("+", "1.5e-10", "2.3e-8")] +#[tokio::test] +async fn test_steel_decimal_literal_operations( + #[case] operation: &str, + #[case] value1: &str, + #[case] value2: &str, +) { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![("result", "NUMERIC(30, 15)")]; + let table_id = create_test_table(&pool, schema_id, "literal_test", columns).await; + + let script = format!(r#"({} "{}" "{}")"#, operation, value1, value2); + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script, + description: Some(format!("Steel decimal {} with literals", operation)), + }; + + let result = post_table_script(&pool, request).await; + assert!( + result.is_ok(), + "Steel decimal {} with literals should succeed", + operation + ); +} + +#[rstest] +#[case::integer_basic("INTEGER", "+")] +#[case::integer_multiplication("INTEGER", "*")] +#[case::integer_sqrt("INTEGER", "sqrt")] +#[case::numeric_basic("NUMERIC(10, 2)", "+")] +#[case::numeric_division("NUMERIC(15, 6)", "/")] +#[case::numeric_power("NUMERIC(8, 4)", "pow")] +#[case::high_precision("NUMERIC(28, 15)", "*")] +#[tokio::test] +async fn test_steel_decimal_column_operations( + #[case] column_type: &str, + #[case] operation: &str, +) { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("test_value", column_type), + ("result", "NUMERIC(30, 15)"), + ]; + let table_id = create_test_table(&pool, schema_id, "column_test", columns).await; + + let script = match operation { + "sqrt" | "abs" => { + format!( + r#"({} (steel_get_column "column_test" "test_value"))"#, + operation + ) + } + _ => { + format!( + r#"({} (steel_get_column "column_test" "test_value") "10")"#, + operation + ) + } + }; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script, + description: Some(format!("Steel decimal {} with {} column", operation, column_type)), + }; + + let result = post_table_script(&pool, request).await; + assert!( + result.is_ok(), + "Steel decimal {} with {} column should succeed", + operation, + column_type + ); +} + +#[rstest] +#[tokio::test] +async fn test_complex_financial_calculation( + precision_scenarios: &Vec<(&'static str, &'static str, &'static str)>, +) { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create a realistic financial calculation table + let columns = vec![ + ("principal", "NUMERIC(16, 2)"), // Principal amount + ("annual_rate", "NUMERIC(6, 5)"), // Interest rate + ("years", "INTEGER"), // Time period + ("compounding_periods", "INTEGER"), // Compounding frequency + ("compound_interest", "NUMERIC(20, 8)"), // Result + ]; + + let table_id = create_test_table(&pool, schema_id, "financial_calc", columns).await; + + // Complex compound interest formula: P * (1 + r/n)^(n*t) + let compound_script = r#" + (* + (steel_get_column "financial_calc" "principal") + (pow + (+ "1" + (/ (steel_get_column "financial_calc" "annual_rate") + (steel_get_column "financial_calc" "compounding_periods"))) + (* (steel_get_column "financial_calc" "years") + (steel_get_column "financial_calc" "compounding_periods")))) + "#; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "compound_interest".to_string(), + script: compound_script.to_string(), + description: Some("Complex compound interest calculation".to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_ok(), "Complex financial calculation should succeed"); +} + +#[tokio::test] +async fn test_scientific_precision_calculations() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("measurement_a", "NUMERIC(25, 15)"), + ("measurement_b", "NUMERIC(25, 15)"), + ("coefficient", "NUMERIC(10, 8)"), + ("scientific_result", "NUMERIC(30, 18)"), + ]; + + let table_id = create_test_table(&pool, schema_id, "scientific_data", columns).await; + + // Complex scientific calculation with high precision + let scientific_script = r#" + (+ + (sqrt (pow (steel_get_column "scientific_data" "measurement_a") "2")) + (* (steel_get_column "scientific_data" "coefficient") + (abs (- (steel_get_column "scientific_data" "measurement_b") + (steel_get_column "scientific_data" "measurement_a"))))) + "#; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "scientific_result".to_string(), + script: scientific_script.to_string(), + description: Some("High precision scientific calculation".to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_ok(), "High precision scientific calculation should succeed"); +} + +#[rstest] +#[case::min_precision("NUMERIC(1, 0)", "Minimum precision")] +#[case::max_scale("NUMERIC(10, 10)", "Maximum relative scale")] +#[case::currency_standard("NUMERIC(14, 4)", "Standard currency precision")] +#[case::percentage_precision("NUMERIC(5, 4)", "Percentage precision")] +#[tokio::test] +async fn test_precision_boundary_conditions( + #[case] numeric_type: &str, + #[case] description: &str, +) { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("boundary_value", numeric_type), + ("result", "NUMERIC(30, 15)"), + ]; + + let table_id = create_test_table(&pool, schema_id, "boundary_test", columns).await; + + let script = r#"(+ (steel_get_column "boundary_test" "boundary_value") "0.00001")"#; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script: script.to_string(), + description: Some(description.to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_ok(), "{} should work with steel_decimal", description); +} + +#[tokio::test] +async fn test_mixed_integer_and_numeric_operations() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("integer_quantity", "INTEGER"), + ("numeric_price", "NUMERIC(10, 4)"), + ("numeric_tax_rate", "NUMERIC(5, 4)"), + ("total_with_tax", "NUMERIC(15, 4)"), + ]; + + let table_id = create_test_table(&pool, schema_id, "mixed_types_calc", columns).await; + + // Calculate total with tax: (quantity * price) * (1 + tax_rate) + let mixed_script = r#" + (* + (* (steel_get_column "mixed_types_calc" "integer_quantity") + (steel_get_column "mixed_types_calc" "numeric_price")) + (+ "1" (steel_get_column "mixed_types_calc" "numeric_tax_rate"))) + "#; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "total_with_tax".to_string(), + script: mixed_script.to_string(), + description: Some("Mixed INTEGER and NUMERIC calculation".to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_ok(), "Mixed INTEGER and NUMERIC operations should succeed"); +} + +#[rstest] +#[case::zero_division("/", "0", "Division by zero should be handled")] +#[case::negative_sqrt("sqrt", "-1", "Square root of negative should be handled")] +#[case::large_power("pow", "999999999", "Very large power should be handled")] +#[tokio::test] +async fn test_mathematical_edge_cases( + #[case] operation: &str, + #[case] problematic_value: &str, + #[case] description: &str, +) { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("test_value", "NUMERIC(15, 6)"), + ("result", "NUMERIC(20, 8)"), + ]; + + let table_id = create_test_table(&pool, schema_id, "edge_case_test", columns).await; + + let script = match operation { + "sqrt" => { + format!(r#"(sqrt "{}")"#, problematic_value) + } + "/" => { + format!(r#"(/ "10" "{}")"#, problematic_value) + } + "pow" => { + format!(r#"(pow "2" "{}")"#, problematic_value) + } + _ => { + format!(r#"({} "10" "{}")"#, operation, problematic_value) + } + }; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script, + description: Some(description.to_string()), + }; + + // Note: These operations should either succeed (if steel_decimal handles them gracefully) + // or fail with appropriate error messages (if they're genuinely problematic) + let result = post_table_script(&pool, request).await; + + // For now, we just ensure the validation doesn't crash + // The specific behavior (success vs failure) depends on steel_decimal implementation + match result { + Ok(_) => { + // Steel decimal handled the edge case gracefully + } + Err(error) => { + // Should fail with meaningful error message, not a crash + let error_msg = error.to_string(); + assert!( + !error_msg.contains("panic") && !error_msg.contains("internal error"), + "Should fail gracefully, not crash: {}", + error_msg + ); + } + } +} + +#[tokio::test] +async fn test_comparison_operations_with_valid_types() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("value_a", "NUMERIC(10, 2)"), + ("value_b", "INTEGER"), + ("comparison_result", "BOOLEAN"), + ]; + + let table_id = create_test_table(&pool, schema_id, "comparison_test", columns).await; + + let comparison_operations = vec![">", "<", "=", ">=", "<="]; + + for operation in comparison_operations { + let script = format!( + r#"({} (steel_get_column "comparison_test" "value_a") + (steel_get_column "comparison_test" "value_b"))"#, + operation + ); + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "comparison_result".to_string(), + script, + description: Some(format!("Comparison operation: {}", operation)), + }; + + let result = post_table_script(&pool, request).await; + assert!( + result.is_ok(), + "Comparison operation {} should succeed with allowed types", + operation + ); + } +} + +#[tokio::test] +async fn test_nested_mathematical_expressions() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("x", "NUMERIC(15, 8)"), + ("y", "NUMERIC(15, 8)"), + ("z", "INTEGER"), + ("nested_result", "NUMERIC(25, 12)"), + ]; + + let table_id = create_test_table(&pool, schema_id, "nested_calc", columns).await; + + // Deeply nested expression: sqrt((x^2 + y^2) / z) + abs(x - y) + let nested_script = r#" + (+ + (sqrt + (/ + (+ (pow (steel_get_column "nested_calc" "x") "2") + (pow (steel_get_column "nested_calc" "y") "2")) + (steel_get_column "nested_calc" "z"))) + (abs + (- (steel_get_column "nested_calc" "x") + (steel_get_column "nested_calc" "y")))) + "#; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "nested_result".to_string(), + script: nested_script.to_string(), + description: Some("Deeply nested mathematical expression".to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_ok(), "Deeply nested mathematical expressions should succeed"); +} diff --git a/server/tests/table_script/mod.rs b/server/tests/table_script/mod.rs new file mode 100644 index 0000000..7cea2ab --- /dev/null +++ b/server/tests/table_script/mod.rs @@ -0,0 +1,18 @@ +// tests/table_script/mod.rs +pub mod prohibited_types_test; + +// // tests/table_script/mod.rs + +// mod post_scripts_tests; +// mod post_scripts_integration_tests; +// mod prohibited_types_test; +// mod type_safety_comprehensive_tests; +// mod mathematical_operations_tests; +// mod comprehensive_error_scenarios_tests; + +// pub use post_scripts_tests::*; +// pub use post_scripts_integration_tests::*; +// pub use prohibited_types_test::*; +// pub use type_safety_comprehensive_tests::*; +// pub use mathematical_operations_tests::*; +// pub use comprehensive_error_scenarios_tests::*; diff --git a/server/tests/table_script/post_scripts_integration_tests.rs b/server/tests/table_script/post_scripts_integration_tests.rs new file mode 100644 index 0000000..adf4fee --- /dev/null +++ b/server/tests/table_script/post_scripts_integration_tests.rs @@ -0,0 +1,557 @@ +#[cfg(test)] +mod integration_tests { + use super::*; + use sqlx::PgPool; + use tokio_test; + use serde_json::json; + use common::proto::multieko2::table_script::{PostTableScriptRequest, TableScriptResponse}; + use std::collections::HashMap; + + /// Test utilities for table script integration testing + pub struct TableScriptTestHelper { + pub pool: PgPool, + pub schema_id: i64, + pub schema_name: String, + } + + impl TableScriptTestHelper { + pub async fn new(test_name: &str) -> Self { + let database_url = std::env::var("TEST_DATABASE_URL") + .unwrap_or_else(|_| "postgresql://postgres:postgres@localhost/multieko2_test".to_string()); + + let pool = PgPool::connect(&database_url).await.expect("Failed to connect to test database"); + sqlx::migrate!("./migrations").run(&pool).await.expect("Failed to run migrations"); + + let schema_name = format!("test_schema_{}", test_name); + let schema_id = sqlx::query_scalar!( + "INSERT INTO schemas (name) VALUES ($1) RETURNING id", + schema_name + ) + .fetch_one(&pool) + .await + .expect("Failed to create test schema"); + + Self { + pool, + schema_id, + schema_name, + } + } + + pub async fn create_table_with_types(&self, table_name: &str, column_definitions: Vec<(&str, &str)>) -> i64 { + let columns: Vec = column_definitions + .iter() + .map(|(name, type_def)| format!("\"{}\" {}", name, type_def)) + .collect(); + + let columns_json = json!(columns); + let indexes_json = json!([]); + + sqlx::query_scalar!( + r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes) + VALUES ($1, $2, $3, $4) RETURNING id"#, + self.schema_id, + table_name, + columns_json, + indexes_json + ) + .fetch_one(&self.pool) + .await + .expect("Failed to create test table") + } + + pub async fn create_script(&self, table_id: i64, target_column: &str, script: &str) -> Result { + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: target_column.to_string(), + script: script.to_string(), + description: Some("Test script".to_string()), + }; + + post_table_script(&self.pool, request).await + } + + pub async fn cleanup(&self) { + let _ = sqlx::query(&format!("DROP SCHEMA IF EXISTS \"{}\" CASCADE", self.schema_name)) + .execute(&self.pool) + .await; + + let _ = sqlx::query("DELETE FROM schemas WHERE name = $1") + .bind(&self.schema_name) + .execute(&self.pool) + .await; + } + } + + #[tokio::test] + async fn test_comprehensive_type_validation_matrix() { + let helper = TableScriptTestHelper::new("type_validation_matrix").await; + + // Create a comprehensive table with all supported and unsupported types + let table_id = helper.create_table_with_types( + "comprehensive_table", + vec![ + // Supported types for math operations + ("integer_col", "INTEGER"), + ("numeric_basic", "NUMERIC(10, 2)"), + ("numeric_high_precision", "NUMERIC(28, 15)"), + ("numeric_currency", "NUMERIC(14, 4)"), + + // Supported but not for math operations + ("text_col", "TEXT"), + ("boolean_col", "BOOLEAN"), + + // Prohibited types entirely + ("bigint_col", "BIGINT"), + ("date_col", "DATE"), + ("timestamp_col", "TIMESTAMPTZ"), + + // Result columns of various types + ("result_integer", "INTEGER"), + ("result_numeric", "NUMERIC(15, 5)"), + ("result_text", "TEXT"), + ] + ).await; + + // Test matrix: [source_type, operation, expected_result] + let test_matrix = vec![ + // Valid mathematical operations + ("integer_col", "+", "result_numeric", true), + ("numeric_basic", "*", "result_numeric", true), + ("numeric_high_precision", "/", "result_numeric", true), + ("integer_col", "sqrt", "result_numeric", true), + + // Invalid mathematical operations - prohibited types in math + ("text_col", "+", "result_numeric", false), + ("boolean_col", "*", "result_numeric", false), + ("bigint_col", "/", "result_numeric", false), + ("date_col", "-", "result_numeric", false), + ("timestamp_col", "+", "result_numeric", false), + + // Invalid target columns - prohibited types as targets + ("integer_col", "+", "bigint_col", false), + ("numeric_basic", "*", "date_col", false), + ("integer_col", "/", "timestamp_col", false), + ]; + + for (source_col, operation, target_col, should_succeed) in test_matrix { + let script = match operation { + "sqrt" => format!(r#"(sqrt (steel_get_column "comprehensive_table" "{}"))"#, source_col), + _ => format!( + r#"({} (steel_get_column "comprehensive_table" "{}") "10")"#, + operation, source_col + ), + }; + + let result = helper.create_script(table_id, target_col, &script).await; + + if should_succeed { + assert!( + result.is_ok(), + "Should succeed: {} {} -> {}", + source_col, operation, target_col + ); + } else { + assert!( + result.is_err(), + "Should fail: {} {} -> {}", + source_col, operation, target_col + ); + } + } + + helper.cleanup().await; + } + + #[tokio::test] + async fn test_steel_decimal_precision_requirements() { + let helper = TableScriptTestHelper::new("precision_requirements").await; + + // Create table with various precision requirements + let table_id = helper.create_table_with_types( + "precision_table", + vec![ + ("low_precision", "NUMERIC(5, 2)"), // e.g., 999.99 + ("medium_precision", "NUMERIC(10, 4)"), // e.g., 999999.9999 + ("high_precision", "NUMERIC(28, 15)"), // Maximum PostgreSQL precision + ("currency", "NUMERIC(14, 4)"), // Standard currency precision + ("percentage", "NUMERIC(5, 4)"), // e.g., 0.9999 (99.99%) + ("integer_val", "INTEGER"), + ("result", "NUMERIC(30, 15)"), + ] + ).await; + + // Test that steel_decimal can handle various precision levels + let precision_tests = vec![ + ( + r#"(+ (steel_get_column "precision_table" "low_precision") "0.01")"#, + "Low precision addition" + ), + ( + r#"(* (steel_get_column "precision_table" "currency") "1.0001")"#, + "Currency calculation with high precision multiplier" + ), + ( + r#"(/ (steel_get_column "precision_table" "high_precision") "3")"#, + "High precision division" + ), + ( + r#"(pow (steel_get_column "precision_table" "percentage") "2")"#, + "Percentage squared" + ), + ( + r#"(sqrt (steel_get_column "precision_table" "medium_precision"))"#, + "Square root of medium precision number" + ), + ]; + + for (script, description) in precision_tests { + let result = helper.create_script(table_id, "result", script).await; + assert!( + result.is_ok(), + "Precision test should succeed: {}", + description + ); + } + + helper.cleanup().await; + } + + #[tokio::test] + async fn test_complex_financial_calculations() { + let helper = TableScriptTestHelper::new("financial_calculations").await; + + // Create a realistic financial table + let table_id = helper.create_table_with_types( + "financial_instruments", + vec![ + ("principal", "NUMERIC(16, 2)"), // Principal amount + ("annual_rate", "NUMERIC(6, 5)"), // Interest rate (e.g., 0.05250) + ("years", "INTEGER"), // Time period + ("compounding_periods", "INTEGER"), // Compounding frequency + ("fees", "NUMERIC(10, 2)"), // Transaction fees + ("compound_interest", "NUMERIC(20, 8)"), // Result column + ] + ).await; + + // Complex compound interest calculation + let compound_interest_script = r#" + (- + (* + (steel_get_column "financial_instruments" "principal") + (pow + (+ "1" + (/ (steel_get_column "financial_instruments" "annual_rate") + (steel_get_column "financial_instruments" "compounding_periods"))) + (* (steel_get_column "financial_instruments" "years") + (steel_get_column "financial_instruments" "compounding_periods")))) + (+ (steel_get_column "financial_instruments" "principal") + (steel_get_column "financial_instruments" "fees"))) + "#; + + let result = helper.create_script(table_id, "compound_interest", compound_interest_script).await; + assert!(result.is_ok(), "Complex financial calculation should succeed"); + + helper.cleanup().await; + } + + #[tokio::test] + async fn test_scientific_notation_support() { + let helper = TableScriptTestHelper::new("scientific_notation").await; + + let table_id = helper.create_table_with_types( + "scientific_data", + vec![ + ("large_number", "NUMERIC(30, 10)"), + ("small_number", "NUMERIC(30, 20)"), + ("result", "NUMERIC(35, 25)"), + ] + ).await; + + // Test that steel_decimal can handle scientific notation in scripts + let scientific_script = r#" + (+ + (steel_get_column "scientific_data" "large_number") + (* "1.5e-10" (steel_get_column "scientific_data" "small_number"))) + "#; + + let result = helper.create_script(table_id, "result", scientific_script).await; + assert!(result.is_ok(), "Scientific notation should be supported"); + + helper.cleanup().await; + } + + #[tokio::test] + async fn test_script_dependencies_and_cycles() { + let helper = TableScriptTestHelper::new("dependencies_cycles").await; + + // Create multiple tables to test dependencies + let table_a_id = helper.create_table_with_types( + "table_a", + vec![ + ("value_a", "NUMERIC(10, 2)"), + ("result_a", "NUMERIC(10, 2)"), + ] + ).await; + + let table_b_id = helper.create_table_with_types( + "table_b", + vec![ + ("value_b", "NUMERIC(10, 2)"), + ("result_b", "NUMERIC(10, 2)"), + ] + ).await; + + // Create first script: table_a.result_a depends on table_b.value_b + let script_a = r#"(+ (steel_get_column "table_b" "value_b") "10")"#; + let result_a = helper.create_script(table_a_id, "result_a", script_a).await; + assert!(result_a.is_ok(), "First dependency script should succeed"); + + // Try to create circular dependency: table_b.result_b depends on table_a.result_a + let script_b = r#"(* (steel_get_column "table_a" "result_a") "2")"#; + let result_b = helper.create_script(table_b_id, "result_b", script_b).await; + + // This should either succeed (if cycle detection allows this pattern) + // or fail (if cycle detection is strict) + // Based on the code, it should detect and prevent cycles + if result_b.is_err() { + let error = result_b.unwrap_err(); + assert!( + error.to_string().contains("cycle") || error.to_string().contains("circular"), + "Circular dependency should be detected" + ); + } + + helper.cleanup().await; + } + + #[tokio::test] + async fn test_error_message_quality() { + let helper = TableScriptTestHelper::new("error_messages").await; + + let table_id = helper.create_table_with_types( + "error_test_table", + vec![ + ("text_field", "TEXT"), + ("numeric_field", "NUMERIC(10, 2)"), + ("boolean_field", "BOOLEAN"), + ("bigint_field", "BIGINT"), + ] + ).await; + + // Test various error scenarios and check error message quality + let error_scenarios = vec![ + ( + "bigint_field", + "(decimal-add \"10\" \"20\")", + vec!["prohibited", "BIGINT", "target"], + "Targeting prohibited type should give clear error" + ), + ( + "numeric_field", + r#"(+ (steel_get_column "error_test_table" "text_field") "10")"#, + vec!["mathematical", "TEXT", "operations"], + "TEXT in math operations should give clear error" + ), + ( + "numeric_field", + r#"(* (steel_get_column "error_test_table" "boolean_field") "5")"#, + vec!["mathematical", "BOOLEAN", "operations"], + "BOOLEAN in math operations should give clear error" + ), + ( + "numeric_field", + r#"(+ (steel_get_column "nonexistent_table" "field") "10")"#, + vec!["table", "does not exist", "nonexistent_table"], + "Nonexistent table should give clear error" + ), + ( + "numeric_field", + r#"(+ (steel_get_column "error_test_table" "nonexistent_field") "10")"#, + vec!["column", "does not exist", "nonexistent_field"], + "Nonexistent column should give clear error" + ), + ]; + + for (target_column, script, expected_keywords, description) in error_scenarios { + let result = helper.create_script(table_id, target_column, script).await; + assert!(result.is_err(), "{}", description); + + let error_message = result.unwrap_err().to_string().to_lowercase(); + for keyword in expected_keywords { + assert!( + error_message.contains(&keyword.to_lowercase()), + "Error message should contain '{}' for: {}. Got: {}", + keyword, description, error_message + ); + } + } + + helper.cleanup().await; + } + + #[tokio::test] + async fn test_performance_with_complex_nested_expressions() { + let helper = TableScriptTestHelper::new("performance_test").await; + + let table_id = helper.create_table_with_types( + "performance_table", + vec![ + ("x", "NUMERIC(15, 8)"), + ("y", "NUMERIC(15, 8)"), + ("z", "NUMERIC(15, 8)"), + ("w", "NUMERIC(15, 8)"), + ("complex_result", "NUMERIC(25, 12)"), + ] + ).await; + + // Create a deeply nested mathematical expression + let complex_script = r#" + (+ + (* + (pow (steel_get_column "performance_table" "x") "3") + (sqrt (steel_get_column "performance_table" "y"))) + (- + (/ + (+ (steel_get_column "performance_table" "z") "100") + (max (steel_get_column "performance_table" "w") "1")) + (* "0.5" + (abs (- (steel_get_column "performance_table" "x") + (steel_get_column "performance_table" "y")))))) + "#; + + let start_time = std::time::Instant::now(); + let result = helper.create_script(table_id, "complex_result", complex_script).await; + let duration = start_time.elapsed(); + + assert!(result.is_ok(), "Complex nested expression should succeed"); + assert!(duration.as_millis() < 1000, "Script validation should complete within 1 second"); + + helper.cleanup().await; + } + + #[tokio::test] + async fn test_boundary_conditions() { + let helper = TableScriptTestHelper::new("boundary_conditions").await; + + // Test boundary conditions for NUMERIC types + let table_id = helper.create_table_with_types( + "boundary_table", + vec![ + ("min_numeric", "NUMERIC(1, 0)"), // Minimum: single digit, no decimal + ("max_numeric", "NUMERIC(1000, 999)"), // Maximum PostgreSQL allows + ("zero_scale", "NUMERIC(10, 0)"), // Integer-like numeric + ("max_scale", "NUMERIC(28, 28)"), // Maximum scale + ("result", "NUMERIC(1000, 999)"), + ] + ).await; + + let boundary_script = r#" + (+ + (steel_get_column "boundary_table" "min_numeric") + (steel_get_column "boundary_table" "zero_scale")) + "#; + + let result = helper.create_script(table_id, "result", boundary_script).await; + assert!(result.is_ok(), "Boundary condition numeric types should be supported"); + + helper.cleanup().await; + } +} + +#[cfg(test)] +mod steel_decimal_integration_tests { + use super::*; + use crate::steel::server::execution::execute_script; + use std::sync::Arc; + use std::collections::HashMap; + + #[tokio::test] + async fn test_steel_decimal_execution_with_valid_types() { + let database_url = std::env::var("TEST_DATABASE_URL") + .unwrap_or_else(|_| "postgresql://postgres:postgres@localhost/multieko2_test".to_string()); + + let pool = Arc::new(PgPool::connect(&database_url).await.expect("Failed to connect")); + + // Test that steel_decimal execution works with INTEGER and NUMERIC types + let mut row_data = HashMap::new(); + row_data.insert("amount".to_string(), "100.50".to_string()); + row_data.insert("quantity".to_string(), "5".to_string()); + row_data.insert("tax_rate".to_string(), "0.0825".to_string()); + + let script = r#" + (+ + (* amount quantity) + (* amount tax_rate)) + "#; + + let result = execute_script( + script.to_string(), + "STRINGS", + pool, + 1, + "test_schema".to_string(), + "test_table".to_string(), + row_data, + ).await; + + assert!(result.is_ok(), "Steel decimal execution should succeed with valid numeric types"); + } + + #[tokio::test] + async fn test_steel_decimal_precision_handling() { + let database_url = std::env::var("TEST_DATABASE_URL") + .unwrap_or_else(|_| "postgresql://postgres:postgres@localhost/multieko2_test".to_string()); + + let pool = Arc::new(PgPool::connect(&database_url).await.expect("Failed to connect")); + + // Test high precision calculations + let mut row_data = HashMap::new(); + row_data.insert("precise_value".to_string(), "123.456789012345".to_string()); + row_data.insert("multiplier".to_string(), "2.718281828459045".to_string()); + + let script = r#"(* precise_value multiplier)"#; + + let result = execute_script( + script.to_string(), + "STRINGS", + pool, + 1, + "test_schema".to_string(), + "test_table".to_string(), + row_data, + ).await; + + assert!(result.is_ok(), "Steel decimal should handle high precision calculations"); + + if let Ok(crate::steel::server::execution::Value::Strings(values)) = result { + assert!(!values.is_empty(), "Should return calculated values"); + // The result should maintain precision + let result_value: f64 = values[0].parse().unwrap_or(0.0); + assert!(result_value > 300.0, "Calculation result should be reasonable"); + } + } +} + +// Test configuration helpers +#[cfg(test)] +pub mod test_config { + use std::sync::Once; + + static INIT: Once = Once::new(); + + pub fn setup_test_environment() { + INIT.call_once(|| { + // Set up test environment variables + std::env::set_var("TEST_DATABASE_URL", + std::env::var("TEST_DATABASE_URL") + .unwrap_or_else(|_| "postgresql://postgres:postgres@localhost/multieko2_test".to_string()) + ); + + // Initialize logging for tests if needed + let _ = env_logger::builder() + .filter_level(log::LevelFilter::Warn) + .try_init(); + }); + } +} diff --git a/server/tests/table_script/post_scripts_tests.rs b/server/tests/table_script/post_scripts_tests.rs new file mode 100644 index 0000000..e69de29 diff --git a/server/tests/table_script/prohibited_types_test.rs b/server/tests/table_script/prohibited_types_test.rs index 3121638..36412b5 100644 --- a/server/tests/table_script/prohibited_types_test.rs +++ b/server/tests/table_script/prohibited_types_test.rs @@ -1,181 +1,312 @@ // tests/table_script/prohibited_types_test.rs -#[cfg(test)] -mod prohibited_types_tests { - use super::*; - use common::proto::multieko2::table_script::PostTableScriptRequest; - use sqlx::PgPool; +use crate::common::setup_isolated_db; +use server::table_script::handlers::post_table_script::post_table_script; +use common::proto::multieko2::table_script::PostTableScriptRequest; +use serde_json::json; +use sqlx::PgPool; - #[tokio::test] - async fn test_reject_bigint_target_column() { - let pool = setup_test_db().await; - - // Create a table with a BIGINT column - let table_id = create_test_table_with_bigint_column(&pool).await; - - let request = PostTableScriptRequest { - table_definition_id: table_id, - target_column: "big_number".to_string(), // This is BIGINT - script: r#" - (define result "some calculation") - result - "#.to_string(), - description: "Test script".to_string(), - }; +/// Helper function to create a test table with specified columns +async fn create_test_table( + pool: &PgPool, + schema_id: i64, + table_name: &str, + columns: Vec<(&str, &str)>, +) -> i64 { + let column_definitions: Vec = columns + .iter() + .map(|(name, type_def)| format!("\"{}\" {}", name, type_def)) + .collect(); - let result = post_table_script(&pool, request).await; - - // Should fail with prohibited type error - assert!(result.is_err()); - let error_msg = result.unwrap_err().to_string(); - assert!(error_msg.contains("Cannot create script for column 'big_number' with type 'BIGINT'")); - assert!(error_msg.contains("Steel scripts cannot target columns of type: BIGINT, DATE, TIMESTAMPTZ")); - } + let columns_json = json!(column_definitions); + let indexes_json = json!([]); - #[tokio::test] - async fn test_reject_date_target_column() { - let pool = setup_test_db().await; - - // Create a table with a DATE column - let table_id = create_test_table_with_date_column(&pool).await; - - let request = PostTableScriptRequest { - table_definition_id: table_id, - target_column: "event_date".to_string(), // This is DATE - script: r#" - (define result "2024-01-01") - result - "#.to_string(), - description: "Test script".to_string(), - }; - - let result = post_table_script(&pool, request).await; - - // Should fail with prohibited type error - assert!(result.is_err()); - let error_msg = result.unwrap_err().to_string(); - assert!(error_msg.contains("Cannot create script for column 'event_date' with type 'DATE'")); - } - - #[tokio::test] - async fn test_reject_timestamptz_target_column() { - let pool = setup_test_db().await; - - // Create a table with a TIMESTAMPTZ column - let table_id = create_test_table_with_timestamptz_column(&pool).await; - - let request = PostTableScriptRequest { - table_definition_id: table_id, - target_column: "created_time".to_string(), // This is TIMESTAMPTZ - script: r#" - (define result "2024-01-01T10:00:00Z") - result - "#.to_string(), - description: "Test script".to_string(), - }; - - let result = post_table_script(&pool, request).await; - - // Should fail with prohibited type error - assert!(result.is_err()); - let error_msg = result.unwrap_err().to_string(); - assert!(error_msg.contains("Cannot create script for column 'created_time' with type 'TIMESTAMPTZ'")); - } - - #[tokio::test] - async fn test_reject_script_referencing_prohibited_column() { - let pool = setup_test_db().await; - - // Create linked tables - one with BIGINT column, another with TEXT target - let source_table_id = create_test_table_with_text_column(&pool).await; - let linked_table_id = create_test_table_with_bigint_column(&pool).await; - - // Create link between tables - create_table_link(&pool, source_table_id, linked_table_id).await; - - let request = PostTableScriptRequest { - table_definition_id: source_table_id, - target_column: "description".to_string(), // This is TEXT (allowed) - script: r#" - (define big_val (steel_get_column "linked_table" "big_number")) - (string-append "Value: " (number->string big_val)) - "#.to_string(), - description: "Script that tries to access BIGINT column".to_string(), - }; - - let result = post_table_script(&pool, request).await; - - // Should fail because script references BIGINT column - assert!(result.is_err()); - let error_msg = result.unwrap_err().to_string(); - assert!(error_msg.contains("Script cannot reference column 'big_number'")); - assert!(error_msg.contains("prohibited type 'BIGINT'")); - } - - #[tokio::test] - async fn test_allow_valid_script_with_allowed_types() { - let pool = setup_test_db().await; - - // Create a table with allowed column types - let table_id = create_test_table_with_allowed_columns(&pool).await; - - let request = PostTableScriptRequest { - table_definition_id: table_id, - target_column: "computed_value".to_string(), // This is TEXT (allowed) - script: r#" - (define name_val (steel_get_column "test_table" "name")) - (define count_val (steel_get_column "test_table" "count")) - (string-append name_val " has " (number->string count_val) " items") - "#.to_string(), - description: "Valid script using allowed types".to_string(), - }; - - let result = post_table_script(&pool, request).await; - - // Should succeed - assert!(result.is_ok()); - let response = result.unwrap(); - assert!(response.id > 0); - } - - // Helper functions for test setup - async fn setup_test_db() -> PgPool { - // Your test database setup code here - todo!("Implement test DB setup") - } - - async fn create_test_table_with_bigint_column(pool: &PgPool) -> i64 { - // Create table definition with BIGINT column - // JSON columns would be: ["name TEXT", "big_number BIGINT"] - todo!("Implement table creation with BIGINT") - } - - async fn create_test_table_with_date_column(pool: &PgPool) -> i64 { - // Create table definition with DATE column - // JSON columns would be: ["name TEXT", "event_date DATE"] - todo!("Implement table creation with DATE") - } - - async fn create_test_table_with_timestamptz_column(pool: &PgPool) -> i64 { - // Create table definition with TIMESTAMPTZ column - // JSON columns would be: ["name TEXT", "created_time TIMESTAMPTZ"] - todo!("Implement table creation with TIMESTAMPTZ") - } - - async fn create_test_table_with_text_column(pool: &PgPool) -> i64 { - // Create table definition with TEXT columns only - // JSON columns would be: ["name TEXT", "description TEXT"] - todo!("Implement table creation with TEXT") - } - - async fn create_test_table_with_allowed_columns(pool: &PgPool) -> i64 { - // Create table definition with only allowed column types - // JSON columns would be: ["name TEXT", "count INTEGER", "computed_value TEXT"] - todo!("Implement table creation with allowed types") - } - - async fn create_table_link(pool: &PgPool, source_id: i64, target_id: i64) { - // Create a link in table_definition_links - todo!("Implement table linking") - } + sqlx::query_scalar!( + r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes) + VALUES ($1, $2, $3, $4) RETURNING id"#, + schema_id, + table_name, + columns_json, + indexes_json + ) + .fetch_one(pool) + .await + .expect("Failed to create test table") +} + +/// Helper function to get default schema ID +async fn get_default_schema_id(pool: &PgPool) -> i64 { + sqlx::query_scalar!("SELECT id FROM schemas WHERE name = 'default'") + .fetch_one(pool) + .await + .expect("Failed to get default schema ID") +} + +#[tokio::test] +async fn test_reject_bigint_target_column() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create a table with a BIGINT column + let table_id = create_test_table( + &pool, + schema_id, + "bigint_table", + vec![("name", "TEXT"), ("big_number", "BIGINT")] + ).await; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "big_number".to_string(), // This is BIGINT + script: r#"(+ "10" "20")"#.to_string(), + description: "Test script".to_string(), // Remove Some() wrapper + }; + + let result = post_table_script(&pool, request).await; + + // Should fail with prohibited type error + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!( + error_msg.contains("prohibited type") || error_msg.contains("BIGINT"), + "Error should mention prohibited type or BIGINT: {}", + error_msg + ); +} + +#[tokio::test] +async fn test_reject_date_target_column() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create a table with a DATE column + let table_id = create_test_table( + &pool, + schema_id, + "date_table", + vec![("name", "TEXT"), ("event_date", "DATE")] + ).await; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "event_date".to_string(), // This is DATE + script: r#"(+ "10" "20")"#.to_string(), + description: "Test script".to_string(), + }; + + let result = post_table_script(&pool, request).await; + + // Should fail with prohibited type error + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!( + error_msg.contains("prohibited type") || error_msg.contains("DATE"), + "Error should mention prohibited type or DATE: {}", + error_msg + ); +} + +#[tokio::test] +async fn test_reject_timestamptz_target_column() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create a table with a TIMESTAMPTZ column + let table_id = create_test_table( + &pool, + schema_id, + "timestamp_table", + vec![("name", "TEXT"), ("created_time", "TIMESTAMPTZ")] + ).await; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "created_time".to_string(), // This is TIMESTAMPTZ + script: r#"(+ "10" "20")"#.to_string(), + description: "Test script".to_string(), + }; + + let result = post_table_script(&pool, request).await; + + // Should fail with prohibited type error + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!( + error_msg.contains("prohibited type") || error_msg.contains("TIMESTAMPTZ"), + "Error should mention prohibited type or TIMESTAMPTZ: {}", + error_msg + ); +} + +#[tokio::test] +async fn test_reject_text_in_mathematical_operations() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create a table with TEXT and NUMERIC columns + let table_id = create_test_table( + &pool, + schema_id, + "text_math_table", + vec![ + ("description", "TEXT"), + ("amount", "NUMERIC(10, 2)"), + ("result", "NUMERIC(10, 2)") + ] + ).await; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script: r#"(+ (steel_get_column "text_math_table" "description") "10")"#.to_string(), + description: "Script that tries to use TEXT in math".to_string(), + }; + + let result = post_table_script(&pool, request).await; + + // Should fail because script uses TEXT in mathematical operation + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!( + error_msg.contains("mathematical operations") || error_msg.contains("TEXT"), + "Error should mention mathematical operations or TEXT: {}", + error_msg + ); +} + +#[tokio::test] +async fn test_reject_boolean_in_mathematical_operations() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create a table with BOOLEAN and NUMERIC columns + let table_id = create_test_table( + &pool, + schema_id, + "boolean_math_table", + vec![ + ("is_active", "BOOLEAN"), + ("amount", "NUMERIC(10, 2)"), + ("result", "NUMERIC(10, 2)") + ] + ).await; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script: r#"(* (steel_get_column "boolean_math_table" "is_active") "5")"#.to_string(), + description: "Script that tries to use BOOLEAN in math".to_string(), + }; + + let result = post_table_script(&pool, request).await; + + // Should fail because script uses BOOLEAN in mathematical operation + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!( + error_msg.contains("mathematical operations") || error_msg.contains("BOOLEAN"), + "Error should mention mathematical operations or BOOLEAN: {}", + error_msg + ); +} + +#[tokio::test] +async fn test_reject_bigint_in_mathematical_operations() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create a table with BIGINT and NUMERIC columns + let table_id = create_test_table( + &pool, + schema_id, + "bigint_math_table", + vec![ + ("big_value", "BIGINT"), + ("result", "NUMERIC(10, 2)") + ] + ).await; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script: r#"(/ (steel_get_column "bigint_math_table" "big_value") "2")"#.to_string(), + description: "Script that tries to use BIGINT in math".to_string(), + }; + + let result = post_table_script(&pool, request).await; + + // Should fail because script uses BIGINT in mathematical operation + assert!(result.is_err()); + let error_msg = result.unwrap_err().to_string(); + assert!( + error_msg.contains("mathematical operations") || error_msg.contains("BIGINT"), + "Error should mention mathematical operations or BIGINT: {}", + error_msg + ); +} + +#[tokio::test] +async fn test_allow_valid_script_with_allowed_types() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create a table with allowed column types + let table_id = create_test_table( + &pool, + schema_id, + "allowed_types_table", + vec![ + ("name", "TEXT"), + ("count", "INTEGER"), + ("amount", "NUMERIC(10, 2)"), + ("computed_value", "TEXT") + ] + ).await; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "computed_value".to_string(), // This is TEXT (allowed as target) + script: r#"(steel_get_column "allowed_types_table" "name")"#.to_string(), + description: "Valid script using allowed types".to_string(), + }; + + let result = post_table_script(&pool, request).await; + + // Should succeed + assert!(result.is_ok(), "Valid script with allowed types should succeed"); + let response = result.unwrap(); + assert!(response.id > 0); +} + +#[tokio::test] +async fn test_allow_integer_and_numeric_in_math_operations() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create a table with allowed mathematical types + let table_id = create_test_table( + &pool, + schema_id, + "math_allowed_table", + vec![ + ("quantity", "INTEGER"), + ("price", "NUMERIC(10, 2)"), + ("total", "NUMERIC(12, 2)") + ] + ).await; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "total".to_string(), + script: r#"(* (steel_get_column "math_allowed_table" "quantity") + (steel_get_column "math_allowed_table" "price"))"#.to_string(), + description: "Valid mathematical operation with INTEGER and NUMERIC".to_string(), + }; + + let result = post_table_script(&pool, request).await; + + // Should succeed + assert!(result.is_ok(), "Mathematical operations with INTEGER and NUMERIC should succeed"); + let response = result.unwrap(); + assert!(response.id > 0); } diff --git a/server/tests/table_script/type_safety_comprehensive_tests.rs b/server/tests/table_script/type_safety_comprehensive_tests.rs new file mode 100644 index 0000000..64937bd --- /dev/null +++ b/server/tests/table_script/type_safety_comprehensive_tests.rs @@ -0,0 +1,454 @@ +// tests/table_script/type_safety_comprehensive_tests.rs + +use crate::common::setup_isolated_db; +use multieko2_server::table_script::handlers::post_table_script::post_table_script; +use common::proto::multieko2::table_script::PostTableScriptRequest; +use rstest::*; +use serde_json::json; +use sqlx::PgPool; + +/// Test fixture for allowed mathematical types +#[fixture] +#[once] +pub fn allowed_math_types() -> Vec<(&'static str, &'static str)> { + vec![ + ("basic_integer", "INTEGER"), + ("small_numeric", "NUMERIC(5, 2)"), + ("currency_numeric", "NUMERIC(14, 4)"), + ("high_precision", "NUMERIC(28, 15)"), + ("zero_scale", "NUMERIC(10, 0)"), + ("percentage", "NUMERIC(5, 4)"), + ("scientific", "NUMERIC(25, 15)"), + ] +} + +/// Test fixture for prohibited mathematical types +#[fixture] +#[once] +pub fn prohibited_math_types() -> Vec<(&'static str, &'static str)> { + vec![ + ("text_field", "TEXT"), + ("boolean_field", "BOOLEAN"), + ("bigint_field", "BIGINT"), + ("date_field", "DATE"), + ("timestamp_field", "TIMESTAMPTZ"), + ] +} + +/// Test fixture for prohibited target types +#[fixture] +#[once] +pub fn prohibited_target_types() -> Vec<(&'static str, &'static str)> { + vec![ + ("bigint_target", "BIGINT"), + ("date_target", "DATE"), + ("timestamp_target", "TIMESTAMPTZ"), + ] +} + +/// Test fixture for basic mathematical operations +#[fixture] +#[once] +pub fn basic_math_operations() -> Vec<(&'static str, &'static str)> { + vec![ + ("+", "addition"), + ("-", "subtraction"), + ("*", "multiplication"), + ("/", "division"), + ] +} + +/// Test fixture for advanced mathematical operations +#[fixture] +#[once] +pub fn advanced_math_operations() -> Vec<(&'static str, &'static str)> { + vec![ + ("sqrt", "square root"), + ("abs", "absolute value"), + ("min", "minimum"), + ("max", "maximum"), + ("pow", "power"), + ] +} + +/// Helper function to create a test table with specified columns +async fn create_test_table( + pool: &PgPool, + schema_id: i64, + table_name: &str, + columns: Vec<(&str, &str)>, +) -> i64 { + let column_definitions: Vec = columns + .iter() + .map(|(name, type_def)| format!("\"{}\" {}", name, type_def)) + .collect(); + + let columns_json = json!(column_definitions); + let indexes_json = json!([]); + + sqlx::query_scalar!( + r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes) + VALUES ($1, $2, $3, $4) RETURNING id"#, + schema_id, + table_name, + columns_json, + indexes_json + ) + .fetch_one(pool) + .await + .expect("Failed to create test table") +} + +/// Helper function to get default schema ID +async fn get_default_schema_id(pool: &PgPool) -> i64 { + sqlx::query_scalar!("SELECT id FROM schemas WHERE name = 'default'") + .fetch_one(pool) + .await + .expect("Failed to get default schema ID") +} + +#[rstest] +#[case::integer_addition("basic_integer", "+", "INTEGER addition should work")] +#[case::numeric_multiplication("currency_numeric", "*", "NUMERIC multiplication should work")] +#[case::high_precision_division("high_precision", "/", "High precision division should work")] +#[case::zero_scale_subtraction("zero_scale", "-", "Zero scale NUMERIC subtraction should work")] +#[tokio::test] +async fn test_allowed_types_in_math_operations( + #[case] column_name: &str, + #[case] operation: &str, + #[case] description: &str, + allowed_math_types: &Vec<(&'static str, &'static str)>, +) { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create table with all allowed mathematical types plus a result column + let mut columns = allowed_math_types.clone(); + columns.push(("result", "NUMERIC(30, 15)")); + + let table_id = create_test_table(&pool, schema_id, "math_test_table", columns).await; + + // Create script using the specified operation and column + let script = match operation { + "sqrt" | "abs" => { + format!( + r#"({} (steel_get_column "math_test_table" "{}"))"#, + operation, column_name + ) + } + _ => { + format!( + r#"({} (steel_get_column "math_test_table" "{}") "10")"#, + operation, column_name + ) + } + }; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script, + description: Some(description.to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_ok(), "{}", description); +} + +#[rstest] +#[case::text_addition("text_field", "+", "TEXT in addition should fail")] +#[case::boolean_multiplication("boolean_field", "*", "BOOLEAN in multiplication should fail")] +#[case::bigint_division("bigint_field", "/", "BIGINT in division should fail")] +#[case::date_subtraction("date_field", "-", "DATE in subtraction should fail")] +#[case::timestamp_sqrt("timestamp_field", "sqrt", "TIMESTAMPTZ in sqrt should fail")] +#[tokio::test] +async fn test_prohibited_types_in_math_operations( + #[case] column_name: &str, + #[case] operation: &str, + #[case] description: &str, + prohibited_math_types: &Vec<(&'static str, &'static str)>, +) { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create table with prohibited types plus a valid result column + let mut columns = prohibited_math_types.clone(); + columns.push(("result", "NUMERIC(15, 6)")); + + let table_id = create_test_table(&pool, schema_id, "prohibited_math_table", columns).await; + + // Create script using prohibited type in mathematical operation + let script = match operation { + "sqrt" | "abs" => { + format!( + r#"({} (steel_get_column "prohibited_math_table" "{}"))"#, + operation, column_name + ) + } + _ => { + format!( + r#"({} (steel_get_column "prohibited_math_table" "{}") "10")"#, + operation, column_name + ) + } + }; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script, + description: Some(description.to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_err(), "{}", description); + + let error_message = result.unwrap_err().to_string(); + assert!( + error_message.contains("mathematical operations") || error_message.contains("prohibited"), + "Error should mention mathematical operations or prohibited types: {}", + error_message + ); +} + +#[rstest] +#[case::bigint_target("bigint_target", "BIGINT target should be rejected")] +#[case::date_target("date_target", "DATE target should be rejected")] +#[case::timestamp_target("timestamp_target", "TIMESTAMPTZ target should be rejected")] +#[tokio::test] +async fn test_prohibited_target_column_types( + #[case] target_column: &str, + #[case] description: &str, + prohibited_target_types: &Vec<(&'static str, &'static str)>, +) { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create table with prohibited target types plus some valid source columns + let mut columns = prohibited_target_types.clone(); + columns.push(("amount", "NUMERIC(10, 2)")); + + let table_id = create_test_table(&pool, schema_id, "prohibited_target_table", columns).await; + + // Try to create script targeting prohibited column type + let script = r#"(+ "10" "20")"#; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: target_column.to_string(), + script: script.to_string(), + description: Some(description.to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_err(), "{}", description); + + let error_message = result.unwrap_err().to_string(); + assert!( + error_message.contains("prohibited type") || error_message.contains("cannot create script"), + "Error should mention prohibited type: {}", + error_message + ); +} + +#[rstest] +#[case::system_id("id", "System column 'id' should be rejected")] +#[case::system_deleted("deleted", "System column 'deleted' should be rejected")] +#[case::system_created_at("created_at", "System column 'created_at' should be rejected")] +#[tokio::test] +async fn test_system_column_restrictions(#[case] target_column: &str, #[case] description: &str) { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![("amount", "NUMERIC(10, 2)")]; + let table_id = create_test_table(&pool, schema_id, "system_test_table", columns).await; + + let script = r#"(+ "10" "20")"#; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: target_column.to_string(), + script: script.to_string(), + description: Some(description.to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_err(), "{}", description); + + let error_message = result.unwrap_err().to_string(); + assert!( + error_message.contains("system column") || error_message.contains("cannot override"), + "Error should mention system column: {}", + error_message + ); +} + +#[tokio::test] +async fn test_comprehensive_type_matrix() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + // Create comprehensive table with all type categories + let all_columns = vec![ + // Allowed math types + ("integer_col", "INTEGER"), + ("numeric_col", "NUMERIC(10, 2)"), + ("high_precision", "NUMERIC(28, 15)"), + + // Prohibited math types + ("text_col", "TEXT"), + ("boolean_col", "BOOLEAN"), + ("bigint_col", "BIGINT"), + ("date_col", "DATE"), + ("timestamp_col", "TIMESTAMPTZ"), + + // Result columns + ("result_numeric", "NUMERIC(20, 8)"), + ("result_text", "TEXT"), + ]; + + let table_id = create_test_table(&pool, schema_id, "comprehensive_table", all_columns).await; + + // Test matrix: [source_column, operation, target_column, should_succeed] + let test_matrix = vec![ + // Valid combinations + ("integer_col", "+", "result_numeric", true), + ("numeric_col", "*", "result_numeric", true), + ("high_precision", "/", "result_numeric", true), + + // Invalid source types in math + ("text_col", "+", "result_numeric", false), + ("boolean_col", "*", "result_numeric", false), + ("bigint_col", "/", "result_numeric", false), + ("date_col", "-", "result_numeric", false), + ("timestamp_col", "+", "result_numeric", false), + ]; + + for (source_col, operation, target_col, should_succeed) in test_matrix { + let script = format!( + r#"({} (steel_get_column "comprehensive_table" "{}") "10")"#, + operation, source_col + ); + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: target_col.to_string(), + script, + description: Some(format!("Matrix test: {} {} -> {}", source_col, operation, target_col)), + }; + + let result = post_table_script(&pool, request).await; + + if should_succeed { + assert!( + result.is_ok(), + "Should succeed: {} {} -> {}", + source_col, operation, target_col + ); + } else { + assert!( + result.is_err(), + "Should fail: {} {} -> {}", + source_col, operation, target_col + ); + } + } +} + +#[tokio::test] +async fn test_complex_mathematical_expressions() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("principal", "NUMERIC(16, 2)"), + ("rate", "NUMERIC(6, 5)"), + ("years", "INTEGER"), + ("compound_result", "NUMERIC(20, 8)"), + ]; + + let table_id = create_test_table(&pool, schema_id, "financial_table", columns).await; + + // Complex compound interest calculation - all using allowed types + let complex_script = r#" + (* + (steel_get_column "financial_table" "principal") + (pow + (+ "1" (steel_get_column "financial_table" "rate")) + (steel_get_column "financial_table" "years"))) + "#; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "compound_result".to_string(), + script: complex_script.to_string(), + description: Some("Complex compound interest calculation".to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_ok(), "Complex mathematical expression with allowed types should succeed"); +} + +#[tokio::test] +async fn test_nonexistent_column_reference() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("amount", "NUMERIC(10, 2)"), + ("result", "NUMERIC(10, 2)"), + ]; + + let table_id = create_test_table(&pool, schema_id, "simple_table", columns).await; + + let script = r#"(+ (steel_get_column "simple_table" "nonexistent_column") "10")"#; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script: script.to_string(), + description: Some("Test nonexistent column".to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_err(), "Should reject script referencing nonexistent column"); + + let error_message = result.unwrap_err().to_string(); + assert!( + error_message.contains("does not exist") || error_message.contains("not found"), + "Error should mention column not found: {}", + error_message + ); +} + +#[tokio::test] +async fn test_nonexistent_table_reference() { + let pool = setup_isolated_db().await; + let schema_id = get_default_schema_id(&pool).await; + + let columns = vec![ + ("amount", "NUMERIC(10, 2)"), + ("result", "NUMERIC(10, 2)"), + ]; + + let table_id = create_test_table(&pool, schema_id, "existing_table", columns).await; + + let script = r#"(+ (steel_get_column "nonexistent_table" "amount") "10")"#; + + let request = PostTableScriptRequest { + table_definition_id: table_id, + target_column: "result".to_string(), + script: script.to_string(), + description: Some("Test nonexistent table".to_string()), + }; + + let result = post_table_script(&pool, request).await; + assert!(result.is_err(), "Should reject script referencing nonexistent table"); + + let error_message = result.unwrap_err().to_string(); + assert!( + error_message.contains("does not exist") || error_message.contains("not found"), + "Error should mention table not found: {}", + error_message + ); +}