From 26898d474fb9f9aebf966e0ed7f140f0aa991894 Mon Sep 17 00:00:00 2001 From: filipriec Date: Tue, 8 Jul 2025 22:04:11 +0200 Subject: [PATCH] tests for steel in post request is fixed with all the errors found in the codebase --- server/src/steel/server/execution.rs | 6 +- .../tables_data/handlers/post_table_data.rs | 22 +- .../post_table_data_steel_decimal_test.rs | 1560 +++++++---------- 3 files changed, 672 insertions(+), 916 deletions(-) diff --git a/server/src/steel/server/execution.rs b/server/src/steel/server/execution.rs index c2c1268..f03f027 100644 --- a/server/src/steel/server/execution.rs +++ b/server/src/steel/server/execution.rs @@ -1,4 +1,4 @@ -// Updated src/steel/server/execution.rs +// src/steel/server/execution.rs use steel::steel_vm::engine::Engine; use steel::steel_vm::register_fn::RegisterFn; use steel::rvals::SteelVal; @@ -40,6 +40,10 @@ pub fn execute_script( // Register all decimal math functions using the steel_decimal crate register_decimal_math_functions(&mut vm); + // IMPORTANT: Register variables from the context with the Steel VM + // This makes the get-var function available with the actual variable values + FunctionRegistry::register_variables(&mut vm, context.row_data.clone()); + // Execute script and process results let results = vm.compile_and_run_raw_program(script) .map_err(|e| ExecutionError::RuntimeError(e.to_string()))?; diff --git a/server/src/tables_data/handlers/post_table_data.rs b/server/src/tables_data/handlers/post_table_data.rs index 9847622..46737c0 100644 --- a/server/src/tables_data/handlers/post_table_data.rs +++ b/server/src/tables_data/handlers/post_table_data.rs @@ -149,9 +149,27 @@ pub async fn post_table_data( let expected_value = script_output.pop() .ok_or_else(|| Status::internal("Script returned no values"))?; - if user_value != &expected_value { + // FIX: Compare as Decimal numbers, not strings! + // Parse the user's value into a Decimal + let user_decimal = Decimal::from_str(user_value).map_err(|_| { + Status::invalid_argument(format!( + "Invalid decimal format provided for column '{}': {}", + target_column, user_value + )) + })?; + + // Parse the script's calculated value into a Decimal + let expected_decimal = Decimal::from_str(&expected_value).map_err(|_| { + Status::internal(format!( + "Script for column '{}' produced an invalid decimal format: {}", + target_column, expected_value + )) + })?; + + // Now compare the actual numbers - this correctly sees that 76.5 == 76.50 + if user_decimal != expected_decimal { return Err(Status::invalid_argument(format!( - "Validation failed for column '{}': Expected '{}', Got '{}'", + "Validation failed for column '{}': Script calculated '{}', but user provided '{}'", target_column, expected_value, user_value ))); } diff --git a/server/tests/tables_data/post/post_table_data_steel_decimal_test.rs b/server/tests/tables_data/post/post_table_data_steel_decimal_test.rs index 2d09bc8..36c31a8 100644 --- a/server/tests/tables_data/post/post_table_data_steel_decimal_test.rs +++ b/server/tests/tables_data/post/post_table_data_steel_decimal_test.rs @@ -1,968 +1,702 @@ // tests/tables_data/post/post_table_data_steel_decimal_test.rs -use rstest::{fixture, rstest}; use sqlx::PgPool; -use sqlx::Row; -use std::collections::HashMap; -use prost_types::Value; -use prost_types::value::Kind; -use common::proto::multieko2::tables_data::PostTableDataRequest; -use common::proto::multieko2::table_definition::{ - PostTableDefinitionRequest, ColumnDefinition as TableColumnDefinition +use common::proto::multieko2::{ + table_definition::{PostTableDefinitionRequest, ColumnDefinition}, + table_script::PostTableScriptRequest, + tables_data::PostTableDataRequest, }; -use server::tables_data::handlers::post_table_data; -use server::table_definition::handlers::post_table_definition; -use crate::common::setup_test_db; -use tonic::Status; +use prost_types::value::Kind; +use prost_types::Value as ProtoValue; +use std::collections::HashMap; use tokio::sync::mpsc; +use std::sync::Arc; + use server::indexer::IndexCommand; -use rust_decimal::Decimal; -use rust_decimal_macros::dec; -use std::str::FromStr; -use rand::distr::Alphanumeric; -use rand::Rng; +use server::table_definition::handlers::post_table_definition::post_table_definition; +use server::table_script::handlers::post_table_script::post_table_script; +use server::tables_data::handlers::post_table_data::post_table_data; +use server::steel::server::execution::{self, Value}; +use server::steel::server::functions::SteelContext; -// Helper function to generate unique identifiers for test isolation -fn generate_unique_id() -> String { - rand::rng() - .sample_iter(&Alphanumeric) - .take(8) - .map(char::from) - .collect::() - .to_lowercase() -} - -// Helper to create a protobuf Value from a string -fn proto_string(s: &str) -> Value { - Value { - kind: Some(Kind::StringValue(s.to_string())), - } -} - -// Helper to create a protobuf Value from a number -fn proto_number(n: f64) -> Value { - Value { - kind: Some(Kind::NumberValue(n)), - } -} - -// Test context for Steel decimal integration tests -#[derive(Clone)] -struct SteelDecimalTestContext { - pool: PgPool, - profile_name: String, - invoice_table: String, - calculation_table: String, - validation_table: String, -} - -// Create tables with Steel scripts that use decimal operations -async fn create_steel_decimal_test_tables( +// Helper function to discover what steel_decimal actually produces +async fn discover_steel_decimal_output( pool: &PgPool, - profile_name: &str, -) -> Result { - let unique_id = generate_unique_id(); - let invoice_table = format!("invoice_steel_{}", unique_id); - let calculation_table = format!("calc_steel_{}", unique_id); - let validation_table = format!("valid_steel_{}", unique_id); - - // Table 1: Invoice table with price calculation script - let invoice_def = PostTableDefinitionRequest { - profile_name: profile_name.into(), - table_name: invoice_table.clone(), - columns: vec![ - TableColumnDefinition { name: "item_name".into(), field_type: "text".into() }, - TableColumnDefinition { name: "quantity".into(), field_type: "integer".into() }, - TableColumnDefinition { name: "unit_price".into(), field_type: "decimal(10,2)".into() }, - TableColumnDefinition { name: "total_price".into(), field_type: "decimal(10,2)".into() }, - TableColumnDefinition { name: "tax_rate".into(), field_type: "decimal(5,4)".into() }, - TableColumnDefinition { name: "final_amount".into(), field_type: "decimal(12,2)".into() }, - ], - indexes: vec![], - links: vec![], + script: &str, + input_data: HashMap, + column_type: &str, +) -> Result { + // Execute steel script to get the calculated value + let context = SteelContext { + current_table: "test_table".to_string(), + schema_id: 1, + schema_name: "test_schema".to_string(), + row_data: input_data, + db_pool: Arc::new(pool.clone()), }; - post_table_definition(pool, invoice_def).await?; - // Table 2: Calculation table with complex decimal arithmetic - let calc_def = PostTableDefinitionRequest { - profile_name: profile_name.into(), - table_name: calculation_table.clone(), - columns: vec![ - TableColumnDefinition { name: "operation_name".into(), field_type: "text".into() }, - TableColumnDefinition { name: "input_a".into(), field_type: "decimal(15,5)".into() }, - TableColumnDefinition { name: "input_b".into(), field_type: "decimal(15,5)".into() }, - TableColumnDefinition { name: "result".into(), field_type: "decimal(20,5)".into() }, - TableColumnDefinition { name: "precision_test".into(), field_type: "decimal(30,10)".into() }, - ], - indexes: vec![], - links: vec![], + let script_result = execution::execute_script( + script.to_string(), + "STRINGS", + Arc::new(pool.clone()), + context, + ).map_err(|e| format!("Script execution failed: {}", e))?; + + let Value::Strings(mut script_output) = script_result else { + return Err("Script must return string values".to_string()); }; - post_table_definition(pool, calc_def).await?; - // Table 3: Validation table with business logic scripts - let validation_def = PostTableDefinitionRequest { - profile_name: profile_name.into(), - table_name: validation_table.clone(), - columns: vec![ - TableColumnDefinition { name: "product_code".into(), field_type: "text".into() }, - TableColumnDefinition { name: "price".into(), field_type: "decimal(10,2)".into() }, - TableColumnDefinition { name: "discount_percent".into(), field_type: "decimal(5,2)".into() }, - TableColumnDefinition { name: "discounted_price".into(), field_type: "decimal(10,2)".into() }, - TableColumnDefinition { name: "margin_check".into(), field_type: "text".into() }, - ], - indexes: vec![], - links: vec![], - }; - post_table_definition(pool, validation_def).await?; + let steel_value = script_output.pop() + .ok_or_else(|| "Script returned no values".to_string())?; - // Insert Steel scripts that use decimal operations - - // Script 1: Calculate total price (quantity * unit_price) - let total_price_script = r#" -(define (calculate-total-price quantity unit-price) - (steel/decimal/multiply - (steel/decimal/from-string quantity) - (steel/decimal/from-string unit-price))) - -(steel/decimal/to-string - (calculate-total-price - (hash-get row-data "quantity") - (hash-get row-data "unit_price"))) -"#; - - // Script 2: Calculate final amount with tax (total_price * (1 + tax_rate)) - let final_amount_script = r#" -(define (calculate-final-amount total-price tax-rate) - (let ([total-decimal (steel/decimal/from-string total-price)] - [tax-decimal (steel/decimal/from-string tax-rate)] - [one (steel/decimal/from-string "1")]) - (steel/decimal/multiply - total-decimal - (steel/decimal/add one tax-decimal)))) - -(steel/decimal/to-string - (calculate-final-amount - (hash-get row-data "total_price") - (hash-get row-data "tax_rate"))) -"#; - - // Script 3: Complex arithmetic result (input_a + input_b) * sqrt(input_a) - let arithmetic_script = r#" -(define (complex-calculation a b) - (let ([a-decimal (steel/decimal/from-string a)] - [b-decimal (steel/decimal/from-string b)]) - (let ([sum (steel/decimal/add a-decimal b-decimal)] - [sqrt-a (steel/decimal/sqrt a-decimal)]) - (steel/decimal/multiply sum sqrt-a)))) - -(steel/decimal/to-string - (complex-calculation - (hash-get row-data "input_a") - (hash-get row-data "input_b"))) -"#; - - // Script 4: High precision calculation - let precision_script = r#" -(define (high-precision-calc a b) - (let ([a-decimal (steel/decimal/from-string a)] - [b-decimal (steel/decimal/from-string b)] - [pi (steel/decimal/from-string "3.1415926535")]) - (steel/decimal/multiply - (steel/decimal/divide a-decimal b-decimal) - pi))) - -(steel/decimal/to-string - (high-precision-calc - (hash-get row-data "input_a") - (hash-get row-data "input_b"))) -"#; - - // Script 5: Calculate discounted price (price * (1 - discount_percent/100)) - let discount_script = r#" -(define (calculate-discount price discount-percent) - (let ([price-decimal (steel/decimal/from-string price)] - [discount-decimal (steel/decimal/from-string discount-percent)] - [hundred (steel/decimal/from-string "100")] - [one (steel/decimal/from-string "1")]) - (let ([discount-ratio (steel/decimal/divide discount-decimal hundred)] - [multiplier (steel/decimal/subtract one discount-ratio)]) - (steel/decimal/multiply price-decimal multiplier)))) - -(steel/decimal/to-string - (calculate-discount - (hash-get row-data "price") - (hash-get row-data "discount_percent"))) -"#; - - // Script 6: Margin validation check - let margin_check_script = r#" -(define (check-margin price discounted-price) - (let ([price-decimal (steel/decimal/from-string price)] - [discounted-decimal (steel/decimal/from-string discounted-price)] - [min-margin (steel/decimal/from-string "0.20")]) - (let ([difference (steel/decimal/subtract price-decimal discounted-decimal)] - [margin-ratio (steel/decimal/divide difference price-decimal)]) - (if (steel/decimal/greater-than? margin-ratio min-margin) - "ACCEPTABLE" - "TOO_LOW")))) - -(check-margin - (hash-get row-data "price") - (hash-get row-data "discounted_price")) -"#; - - // Get table definition IDs for script insertion - let invoice_table_id = sqlx::query_scalar!( - "SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2", - profile_name, - invoice_table - ) + // See how PostgreSQL formats this value when cast to the target column type + let stored_value = sqlx::query_scalar::<_, String>(&format!( + "SELECT ($1::{})::TEXT", column_type + )) + .bind(&steel_value) .fetch_one(pool) .await - .map_err(|e| Status::internal(format!("Failed to get invoice table ID: {}", e)))?; + .map_err(|e| format!("Failed to cast value: {}", e))?; - let calc_table_id = sqlx::query_scalar!( - "SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2", - profile_name, - calculation_table - ) - .fetch_one(pool) - .await - .map_err(|e| Status::internal(format!("Failed to get calc table ID: {}", e)))?; + println!("STEEL_DECIMAL DEBUG: Script '{}' -> Raw: '{}' -> DB Formatted: '{}'", + script, steel_value, stored_value); - let validation_table_id = sqlx::query_scalar!( - "SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2", - profile_name, - validation_table - ) - .fetch_one(pool) - .await - .map_err(|e| Status::internal(format!("Failed to get validation table ID: {}", e)))?; - - // Get the schema_id for script insertion - let schema_id = sqlx::query_scalar!( - "SELECT id FROM schemas WHERE name = $1", - profile_name - ) - .fetch_one(pool) - .await - .map_err(|e| Status::internal(format!("Failed to get schema ID: {}", e)))?; - - // Insert scripts into table_scripts table with all required columns - let scripts = vec![ - (invoice_table_id, &invoice_table, "total_price", "NUMERIC(10,2)", total_price_script, "Calculate total price (quantity * unit_price)"), - (invoice_table_id, &invoice_table, "final_amount", "NUMERIC(12,2)", final_amount_script, "Calculate final amount with tax"), - (calc_table_id, &calculation_table, "result", "NUMERIC(20,5)", arithmetic_script, "Complex arithmetic calculation"), - (calc_table_id, &calculation_table, "precision_test", "NUMERIC(30,10)", precision_script, "High precision calculation"), - (validation_table_id, &validation_table, "discounted_price", "NUMERIC(10,2)", discount_script, "Calculate discounted price"), - (validation_table_id, &validation_table, "margin_check", "TEXT", margin_check_script, "Validate margin requirements"), - ]; - - for (table_id, target_table, target_column, target_column_type, script, description) in scripts { - sqlx::query!( - "INSERT INTO table_scripts (table_definitions_id, target_table, target_column, target_column_type, script, description, schema_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", - table_id, - target_table, - target_column, - target_column_type, - script, - description, - schema_id - ) - .execute(pool) - .await - .map_err(|e| Status::internal(format!("Failed to insert script: {}", e)))?; - } - - Ok(SteelDecimalTestContext { - pool: pool.clone(), - profile_name: profile_name.to_string(), - invoice_table, - calculation_table, - validation_table, - }) + Ok(stored_value) } -async fn create_test_indexer_channel() -> mpsc::Sender { - let (tx, mut rx) = mpsc::channel(100); +async fn setup_test_schema(pool: &PgPool, profile_name: &str, table_name: &str) -> i64 { + // Create table definition + let request = PostTableDefinitionRequest { + profile_name: profile_name.to_string(), + table_name: table_name.to_string(), + columns: vec![ + ColumnDefinition { + name: "price".to_string(), + field_type: "decimal(10, 2)".to_string(), + }, + ColumnDefinition { + name: "quantity".to_string(), + field_type: "integer".to_string(), + }, + ColumnDefinition { + name: "total".to_string(), + field_type: "decimal(10, 2)".to_string(), + }, + ColumnDefinition { + name: "percentage".to_string(), + field_type: "decimal(5, 2)".to_string(), + }, + ], + indexes: vec![], + links: vec![], + }; - // Spawn a task to consume indexer messages to prevent blocking + let response = post_table_definition(pool, request).await.unwrap(); + assert!(response.success); + + // Get table definition ID + let schema_row = sqlx::query!("SELECT id FROM schemas WHERE name = $1", profile_name) + .fetch_one(pool) + .await + .unwrap(); + + let table_row = sqlx::query!( + "SELECT id FROM table_definitions WHERE schema_id = $1 AND table_name = $2", + schema_row.id, + table_name + ) + .fetch_one(pool) + .await + .unwrap(); + + table_row.id +} + +async fn create_indexer_channel() -> mpsc::Sender { + let (tx, mut rx) = mpsc::channel(100); + + // Spawn a task to consume messages to prevent blocking tokio::spawn(async move { while let Some(_) = rx.recv().await { - // Just consume the messages + // Just consume messages for testing } }); - + tx } -#[fixture] -async fn steel_decimal_context() -> SteelDecimalTestContext { - let pool = setup_test_db().await; - let unique_id = generate_unique_id(); - let profile_name = format!("steel_decimal_profile_{}", unique_id); +#[sqlx::test] +async fn test_basic_arithmetic_validation_success(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_arithmetic", "invoice").await; + let indexer_tx = create_indexer_channel().await; - create_steel_decimal_test_tables(&pool, &profile_name).await - .expect("Failed to create steel decimal test tables") + // Create script: total should equal price * quantity + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(* $price $quantity)".to_string(), + description: "Total = Price × Quantity".to_string(), + }; + + let script_response = post_table_script(&pool, script_request).await.unwrap(); + assert!(script_response.id > 0); + + // Test data insertion with correct calculation: 25.50 * 3 = 76.5 + let mut data = HashMap::new(); + data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("25.50".to_string())), + }); + data.insert("quantity".to_string(), ProtoValue { + kind: Some(Kind::NumberValue(3.0)), // INTEGER column expects NumberValue + }); + data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("76.5".to_string())), // Steel decimal normalizes to "76.5" + }); + + let data_request = PostTableDataRequest { + profile_name: "test_arithmetic".to_string(), + table_name: "invoice".to_string(), + data, + }; + + let response = post_table_data(&pool, data_request, &indexer_tx).await.unwrap(); + assert!(response.success); + assert!(response.inserted_id > 0); } -// ======================================================================== -// DEBUGGING TESTS TO UNDERSTAND STEEL DECIMAL INTEGRATION -// ======================================================================== +#[sqlx::test] +async fn test_basic_arithmetic_validation_failure(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_arithmetic_fail", "invoice").await; + let indexer_tx = create_indexer_channel().await; -#[rstest] -#[tokio::test] -async fn test_steel_decimal_basic_function_registration(#[future] steel_decimal_context: SteelDecimalTestContext) { - let context = steel_decimal_context.await; - let indexer_tx = create_test_indexer_channel().await; + // Create script: total should equal price * quantity + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(* $price $quantity)".to_string(), + description: "Total = Price × Quantity".to_string(), + }; - // Test the most basic steel-decimal function to see if it's registered - let debug_table = format!("basic_debug_{}", generate_unique_id()); - let debug_def = PostTableDefinitionRequest { - profile_name: context.profile_name.clone(), - table_name: debug_table.clone(), + post_table_script(&pool, script_request).await.unwrap(); + + // Test data insertion with incorrect calculation + let mut data = HashMap::new(); + data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("25.50".to_string())), + }); + data.insert("quantity".to_string(), ProtoValue { + kind: Some(Kind::NumberValue(3.0)), + }); + data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("70.00".to_string())), // Wrong! Should be 76.50 + }); + + let data_request = PostTableDataRequest { + profile_name: "test_arithmetic_fail".to_string(), + table_name: "invoice".to_string(), + data, + }; + + let result = post_table_data(&pool, data_request, &indexer_tx).await; + assert!(result.is_err()); + + let error = result.unwrap_err(); + assert_eq!(error.code(), tonic::Code::InvalidArgument); + + // --- START OF FIX --- + // The old assertion was looking for "Expected '...'". + // The new error message is "Script calculated '...' but user provided '...'" + // We update the assertion to match the new format. + let msg = error.message(); + assert!(msg.contains("Validation failed for column 'total'")); + assert!(msg.contains("Script calculated '76.50'")); + assert!(msg.contains("but user provided '70.00'")); + // --- END OF FIX --- +} + +#[sqlx::test] +async fn test_division_with_precision(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_division", "calculation").await; + let indexer_tx = create_indexer_channel().await; + + // Create script: percentage should equal (total / price) * 100 + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "percentage".to_string(), + script: "(* (/ $total $price) 100.0)".to_string(), + description: "Percentage = (Total / Price) × 100".to_string(), + }; + + post_table_script(&pool, script_request).await.unwrap(); + + // Test with precise division + let mut data = HashMap::new(); + data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("33.33".to_string())), + }); + data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("100.00".to_string())), + }); + data.insert("percentage".to_string(), ProtoValue { + kind: Some(Kind::StringValue("300.03000300030003000300030003".to_string())), // Full precision result from steel_decimal + }); + + let data_request = PostTableDataRequest { + profile_name: "test_division".to_string(), + table_name: "calculation".to_string(), + data, + }; + + let response = post_table_data(&pool, data_request, &indexer_tx).await.unwrap(); + assert!(response.success); +} + +#[sqlx::test] +async fn test_complex_formula_validation(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_complex", "order").await; + let indexer_tx = create_indexer_channel().await; + + // Create script: total = (price * quantity) + (price * quantity * 0.08) [with 8% tax] + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(+ (* $price $quantity) (* (* $price $quantity) 0.08))".to_string(), + description: "Total with 8% tax".to_string(), + }; + + post_table_script(&pool, script_request).await.unwrap(); + + // Test complex calculation: (100 * 2) + (100 * 2 * 0.08) = 200 + 16 = 216 + let mut data = HashMap::new(); + data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("100.00".to_string())), + }); + data.insert("quantity".to_string(), ProtoValue { + kind: Some(Kind::NumberValue(2.0)), // INTEGER column expects NumberValue + }); + data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("216".to_string())), // Steel decimal normalizes to "216" + }); + + let data_request = PostTableDataRequest { + profile_name: "test_complex".to_string(), + table_name: "order".to_string(), + data, + }; + + let response = post_table_data(&pool, data_request, &indexer_tx).await.unwrap(); + assert!(response.success); +} + +#[sqlx::test] +async fn test_steel_decimal_function_calls_directly(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_direct", "test").await; + let indexer_tx = create_indexer_channel().await; + + // Create script using direct steel decimal function syntax + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: r#"(decimal-add (decimal-mul $price $quantity) "5.00")"#.to_string(), + description: "Direct steel decimal functions".to_string(), + }; + + post_table_script(&pool, script_request).await.unwrap(); + + // Test: (10.50 * 2) + 5.00 = 21 + 5 = 26 + let mut data = HashMap::new(); + data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("10.50".to_string())), + }); + data.insert("quantity".to_string(), ProtoValue { + kind: Some(Kind::NumberValue(2.0)), // INTEGER column expects NumberValue + }); + data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("26".to_string())), // Steel decimal normalizes to "26" + }); + + let data_request = PostTableDataRequest { + profile_name: "test_direct".to_string(), + table_name: "test".to_string(), + data, + }; + + let response = post_table_data(&pool, data_request, &indexer_tx).await.unwrap(); + assert!(response.success); +} + +#[sqlx::test] +async fn test_scientific_notation_support(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_scientific", "scientific").await; + let indexer_tx = create_indexer_channel().await; + + // Create script using scientific notation + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(+ $price 1e2)".to_string(), // Add 100 (1e2) to price + description: "Scientific notation test".to_string(), + }; + + post_table_script(&pool, script_request).await.unwrap(); + + // Test: 25.50 + 100 = 125.5 + let mut data = HashMap::new(); + data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("25.50".to_string())), + }); + data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("125.5".to_string())), // Steel decimal normalizes to "125.5" + }); + + let data_request = PostTableDataRequest { + profile_name: "test_scientific".to_string(), + table_name: "scientific".to_string(), + data, + }; + + let response = post_table_data(&pool, data_request, &indexer_tx).await.unwrap(); + assert!(response.success); +} + +#[sqlx::test] +async fn test_advanced_math_functions(pool: PgPool) { + // Create a table with advanced math requirements + let request = PostTableDefinitionRequest { + profile_name: "test_advanced_math".to_string(), + table_name: "calculations".to_string(), columns: vec![ - TableColumnDefinition { name: "test_input".into(), field_type: "text".into() }, - TableColumnDefinition { name: "result".into(), field_type: "text".into() }, + ColumnDefinition { + name: "input".to_string(), + field_type: "decimal(10, 4)".to_string(), + }, + ColumnDefinition { + name: "square_root".to_string(), + field_type: "decimal(10, 4)".to_string(), + }, + ColumnDefinition { + name: "power_result".to_string(), + field_type: "decimal(10, 4)".to_string(), + }, ], indexes: vec![], links: vec![], }; - post_table_definition(&context.pool, debug_def).await.unwrap(); - // Get table and schema IDs - let debug_table_id = sqlx::query_scalar!( - "SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2", - context.profile_name, - debug_table + let response = post_table_definition(&pool, request).await.unwrap(); + assert!(response.success); + + let schema_row = sqlx::query!("SELECT id FROM schemas WHERE name = $1", "test_advanced_math") + .fetch_one(&pool) + .await + .unwrap(); + + let table_row = sqlx::query!( + "SELECT id FROM table_definitions WHERE schema_id = $1 AND table_name = $2", + schema_row.id, + "calculations" ) - .fetch_one(&context.pool) + .fetch_one(&pool) .await .unwrap(); - let schema_id = sqlx::query_scalar!( - "SELECT id FROM schemas WHERE name = $1", - context.profile_name - ) - .fetch_one(&context.pool) - .await - .unwrap(); + let indexer_tx = create_indexer_channel().await; - // Test the simplest possible steel-decimal function: decimal-add - let simple_script = r#"(decimal-add "1.0" "2.0")"#; + // Test square root + let sqrt_script = PostTableScriptRequest { + table_definition_id: table_row.id, + target_column: "square_root".to_string(), + script: "(sqrt $input)".to_string(), + description: "Square root validation".to_string(), + }; - // Insert script - sqlx::query!( - "INSERT INTO table_scripts (table_definitions_id, target_table, target_column, target_column_type, script, description, schema_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", - debug_table_id, - debug_table, - "result", - "TEXT", - simple_script, - "Test basic decimal-add function", - schema_id - ) - .execute(&context.pool) - .await - .unwrap(); + post_table_script(&pool, sqrt_script).await.unwrap(); - // Try to execute + // Test power function + let power_script = PostTableScriptRequest { + table_definition_id: table_row.id, + target_column: "power_result".to_string(), + script: "(^ $input 2.0)".to_string(), + description: "Power function validation".to_string(), + }; + + post_table_script(&pool, power_script).await.unwrap(); + + // Insert test data: sqrt(16) = 4, 16^2 = 256 let mut data = HashMap::new(); - data.insert("test_input".into(), proto_string("test")); - data.insert("result".into(), proto_string("3.0")); // Expected result of 1.0 + 2.0 + data.insert("input".to_string(), ProtoValue { + kind: Some(Kind::StringValue("16".to_string())), // Steel decimal format + }); + data.insert("square_root".to_string(), ProtoValue { + kind: Some(Kind::StringValue("4".to_string())), // Steel decimal normalizes: sqrt(16) = "4" + }); + data.insert("power_result".to_string(), ProtoValue { + kind: Some(Kind::StringValue("256".to_string())), // Steel decimal normalizes: 16^2 = "256" + }); - let request = PostTableDataRequest { - profile_name: context.profile_name.clone(), - table_name: debug_table.clone(), + let data_request = PostTableDataRequest { + profile_name: "test_advanced_math".to_string(), + table_name: "calculations".to_string(), data, }; - let result = post_table_data(&context.pool, request, &indexer_tx).await; + let response = post_table_data(&pool, data_request, &indexer_tx).await.unwrap(); + assert!(response.success); +} + +#[sqlx::test] +async fn test_financial_calculations(pool: PgPool) { + // Create financial calculation table + let request = PostTableDefinitionRequest { + profile_name: "test_financial".to_string(), + table_name: "finance".to_string(), + columns: vec![ + ColumnDefinition { + name: "principal".to_string(), + field_type: "decimal(12, 2)".to_string(), + }, + ColumnDefinition { + name: "rate".to_string(), + field_type: "decimal(6, 4)".to_string(), + }, + ColumnDefinition { + name: "time".to_string(), + field_type: "decimal(4, 1)".to_string(), + }, + ColumnDefinition { + name: "compound_result".to_string(), + field_type: "decimal(12, 2)".to_string(), + }, + ColumnDefinition { + name: "percentage_result".to_string(), + field_type: "decimal(10, 2)".to_string(), + }, + ], + indexes: vec![], + links: vec![], + }; + + let response = post_table_definition(&pool, request).await.unwrap(); + assert!(response.success); + + let schema_row = sqlx::query!("SELECT id FROM schemas WHERE name = $1", "test_financial") + .fetch_one(&pool) + .await + .unwrap(); + + let table_row = sqlx::query!( + "SELECT id FROM table_definitions WHERE schema_id = $1 AND table_name = $2", + schema_row.id, + "finance" + ) + .fetch_one(&pool) + .await + .unwrap(); + + let indexer_tx = create_indexer_channel().await; + + // Test using steel decimal's advanced functions (if available) + // Using basic operations to simulate compound interest: P * (1 + r)^t + let compound_script = PostTableScriptRequest { + table_definition_id: table_row.id, + target_column: "compound_result".to_string(), + script: "(* $principal (^ (+ 1.0 $rate) $time))".to_string(), + description: "Compound interest calculation".to_string(), + }; + + post_table_script(&pool, compound_script).await.unwrap(); + + // Test percentage calculation: principal * rate + let percentage_script = PostTableScriptRequest { + table_definition_id: table_row.id, + target_column: "percentage_result".to_string(), + script: "(* $principal $rate)".to_string(), + description: "Percentage calculation".to_string(), + }; + + post_table_script(&pool, percentage_script).await.unwrap(); + + // Test: 1000 * (1.05)^2 = 1102.5 and 1000 * 0.05 = 50 + let mut data = HashMap::new(); + data.insert("principal".to_string(), ProtoValue { + kind: Some(Kind::StringValue("1000".to_string())), + }); + data.insert("rate".to_string(), ProtoValue { + kind: Some(Kind::StringValue("0.05".to_string())), + }); + data.insert("time".to_string(), ProtoValue { + kind: Some(Kind::StringValue("2".to_string())), + }); + data.insert("compound_result".to_string(), ProtoValue { + kind: Some(Kind::StringValue("1102.5".to_string())), // Steel decimal normalizes to "1102.5" + }); + data.insert("percentage_result".to_string(), ProtoValue { + kind: Some(Kind::StringValue("50".to_string())), // Steel decimal normalizes to "50" + }); + + let data_request = PostTableDataRequest { + profile_name: "test_financial".to_string(), + table_name: "finance".to_string(), + data, + }; + + let response = post_table_data(&pool, data_request, &indexer_tx).await.unwrap(); + assert!(response.success); +} + +#[sqlx::test] +async fn test_precision_handling(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_precision", "precision_test").await; + let indexer_tx = create_indexer_channel().await; + + // Test that steel decimal handles precision correctly + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(/ 1.0 3.0)".to_string(), // 1/3 should be handled with proper precision + description: "Precision test".to_string(), + }; + + post_table_script(&pool, script_request).await.unwrap(); + + let mut data = HashMap::new(); + data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("0.3333333333333333333333333333".to_string())), + }); + + let data_request = PostTableDataRequest { + profile_name: "test_precision".to_string(), + table_name: "precision_test".to_string(), + data, + }; + + let response = post_table_data(&pool, data_request, &indexer_tx).await.unwrap(); + assert!(response.success); +} + +#[sqlx::test] +async fn test_multiple_script_validations(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_multiple", "multi_validation").await; + let indexer_tx = create_indexer_channel().await; + + // Create multiple validation scripts + let script1 = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(* $price $quantity)".to_string(), + description: "Basic multiplication".to_string(), + }; + + let script2 = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "percentage".to_string(), + script: "(/ $total $price)".to_string(), + description: "Percentage calculation".to_string(), + }; + + post_table_script(&pool, script1).await.unwrap(); + post_table_script(&pool, script2).await.unwrap(); + + // Test: 50 * 4 = 200 and 200 / 50 = 4 + let mut data = HashMap::new(); + data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("50.00".to_string())), + }); + data.insert("quantity".to_string(), ProtoValue { + kind: Some(Kind::NumberValue(4.0)), // INTEGER column expects NumberValue + }); + data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("200".to_string())), // Steel decimal normalizes to "200" + }); + data.insert("percentage".to_string(), ProtoValue { + kind: Some(Kind::StringValue("4".to_string())), // Steel decimal normalizes to "4" + }); + + let data_request = PostTableDataRequest { + profile_name: "test_multiple".to_string(), + table_name: "multi_validation".to_string(), + data, + }; + + let response = post_table_data(&pool, data_request, &indexer_tx).await.unwrap(); + assert!(response.success); +} + +#[sqlx::test] +async fn test_steel_script_error_handling(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_errors", "error_test").await; + let indexer_tx = create_indexer_channel().await; + + // Create script with division by zero + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(/ $price 0.0)".to_string(), // Division by zero + description: "Error test".to_string(), + }; + + post_table_script(&pool, script_request).await.unwrap(); + + let mut data = HashMap::new(); + data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("100.00".to_string())), + }); + data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("999.99".to_string())), + }); + + let data_request = PostTableDataRequest { + profile_name: "test_errors".to_string(), + table_name: "error_test".to_string(), + data, + }; + + let result = post_table_data(&pool, data_request, &indexer_tx).await; + assert!(result.is_err()); + + let error = result.unwrap_err(); + assert_eq!(error.code(), tonic::Code::InvalidArgument); + assert!(error.message().contains("Script execution failed")); +} + +#[sqlx::test] +async fn test_decimal_overflow_protection(pool: PgPool) { + let table_def_id = setup_test_schema(&pool, "test_overflow", "overflow_test").await; + let indexer_tx = create_indexer_channel().await; + + // Create script that might cause overflow + let script_request = PostTableScriptRequest { + table_definition_id: table_def_id, + target_column: "total".to_string(), + script: "(^ $price 100.0)".to_string(), // Large power operation + description: "Overflow test".to_string(), + }; + + post_table_script(&pool, script_request).await.unwrap(); + + let mut data = HashMap::new(); + data.insert("price".to_string(), ProtoValue { + kind: Some(Kind::StringValue("999.99".to_string())), + }); + data.insert("total".to_string(), ProtoValue { + kind: Some(Kind::StringValue("1.0".to_string())), + }); + + let data_request = PostTableDataRequest { + profile_name: "test_overflow".to_string(), + table_name: "overflow_test".to_string(), + data, + }; + + // This should either handle the overflow gracefully or fail with appropriate error + let result = post_table_data(&pool, data_request, &indexer_tx).await; + // The behavior depends on steel_decimal implementation - could succeed or fail + // but should not crash the system match result { - Ok(response) => { - println!("✓ Basic steel-decimal function works!"); - - // Verify the result - let query = format!( - r#"SELECT result FROM "{}"."{}" WHERE id = $1"#, - context.profile_name, debug_table - ); - let stored_result: String = sqlx::query_scalar(&query) - .bind(response.inserted_id) - .fetch_one(&context.pool) - .await - .unwrap(); - - assert_eq!(stored_result, "3.0", "Steel-decimal function should return correct result"); - } + Ok(_) => println!("Overflow handled successfully"), Err(e) => { - println!("✗ Basic steel-decimal function failed: {}", e.message()); - panic!("Steel-decimal functions are not properly registered"); + assert_eq!(e.code(), tonic::Code::InvalidArgument); + println!("Overflow properly caught: {}", e.message()); } } } - -#[rstest] -#[tokio::test] -async fn test_debug_steel_decimal_function_availability(#[future] steel_decimal_context: SteelDecimalTestContext) { - let context = steel_decimal_context.await; - let indexer_tx = create_test_indexer_channel().await; - - // Try different possible function name formats to see what works - let test_scripts = vec![ - ("decimal-add", r#"(decimal-add "1.0" "2.0")"#), - ("decimal-mul", r#"(decimal-mul "2.0" "3.0")"#), - ("decimal-sub", r#"(decimal-sub "5.0" "2.0")"#), - ("decimal-div", r#"(decimal-div "10.0" "2.0")"#), - ("decimal-gt", r#"(decimal-gt "5.0" "3.0")"#), - ]; - - // Create a simple table for debugging - let debug_table = format!("debug_table_{}", generate_unique_id()); - let debug_def = PostTableDefinitionRequest { - profile_name: context.profile_name.clone(), - table_name: debug_table.clone(), - columns: vec![ - TableColumnDefinition { name: "test_name".into(), field_type: "text".into() }, - TableColumnDefinition { name: "result".into(), field_type: "text".into() }, - ], - indexes: vec![], - links: vec![], - }; - post_table_definition(&context.pool, debug_def).await.unwrap(); - - // Get table ID for script insertion - let debug_table_id = sqlx::query_scalar!( - "SELECT td.id FROM table_definitions td JOIN schemas s ON td.schema_id = s.id WHERE s.name = $1 AND td.table_name = $2", - context.profile_name, - debug_table - ) - .fetch_one(&context.pool) - .await - .unwrap(); - - let schema_id = sqlx::query_scalar!( - "SELECT id FROM schemas WHERE name = $1", - context.profile_name - ) - .fetch_one(&context.pool) - .await - .unwrap(); - - for (test_name, script) in test_scripts { - // Insert debug script - let insert_result = sqlx::query!( - "INSERT INTO table_scripts (table_definitions_id, target_table, target_column, target_column_type, script, description, schema_id) VALUES ($1, $2, $3, $4, $5, $6, $7)", - debug_table_id, - debug_table, - "result", - "TEXT", - script, - format!("Debug test for {}", test_name), - schema_id - ) - .execute(&context.pool) - .await; - - if insert_result.is_ok() { - // Try to execute the script - let mut data = HashMap::new(); - data.insert("test_name".into(), proto_string(test_name)); - data.insert("result".into(), proto_string("expected_value")); - - let request = PostTableDataRequest { - profile_name: context.profile_name.clone(), - table_name: debug_table.clone(), - data, - }; - - let result = post_table_data(&context.pool, request, &indexer_tx).await; - match result { - Ok(_) => println!("✓ Function {} works!", test_name), - Err(e) => println!("✗ Function {} failed: {}", test_name, e.message()), - } - - // Clean up the script for next test - sqlx::query!( - "DELETE FROM table_scripts WHERE table_definitions_id = $1 AND target_column = $2", - debug_table_id, - "result" - ) - .execute(&context.pool) - .await - .unwrap(); - } else { - println!("✗ Failed to insert debug script for {}", test_name); - } - } -} - -// ======================================================================== -// BASIC STEEL DECIMAL FUNCTIONALITY TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_steel_decimal_basic_arithmetic(#[future] steel_decimal_context: SteelDecimalTestContext) { - let context = steel_decimal_context.await; - let indexer_tx = create_test_indexer_channel().await; - - // Test basic multiplication: quantity * unit_price = total_price - let mut data = HashMap::new(); - data.insert("item_name".into(), proto_string("Test Item")); - data.insert("quantity".into(), proto_number(5.0)); - data.insert("unit_price".into(), proto_string("19.99")); - data.insert("total_price".into(), proto_string("99.95")); // 5 * 19.99 = 99.95 - data.insert("tax_rate".into(), proto_string("0.1000")); // 10% - data.insert("final_amount".into(), proto_string("109.95")); // 99.95 * 1.1 = 109.945 -> 109.95 - - let request = PostTableDataRequest { - profile_name: context.profile_name.clone(), - table_name: context.invoice_table.clone(), - data, - }; - - let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap(); - assert!(response.success); - - // Verify the calculated values were stored correctly - let query = format!( - r#"SELECT total_price, final_amount FROM "{}"."{}" WHERE id = $1"#, - context.profile_name, context.invoice_table - ); - let row = sqlx::query(&query) - .bind(response.inserted_id) - .fetch_one(&context.pool) - .await - .unwrap(); - - let stored_total: Decimal = row.get("total_price"); - let stored_final: Decimal = row.get("final_amount"); - - assert_eq!(stored_total, dec!(99.95)); - assert_eq!(stored_final, dec!(109.95)); -} - -#[rstest] -#[tokio::test] -async fn test_steel_decimal_precision_preservation(#[future] steel_decimal_context: SteelDecimalTestContext) { - let context = steel_decimal_context.await; - let indexer_tx = create_test_indexer_channel().await; - - // Test high precision calculations - let mut data = HashMap::new(); - data.insert("operation_name".into(), proto_string("High Precision Test")); - data.insert("input_a".into(), proto_string("123.45678")); - data.insert("input_b".into(), proto_string("987.65432")); - // Steel script should calculate (a + b) * sqrt(a) and a/b * pi - data.insert("result".into(), proto_string("12345.67890")); // Placeholder, will be calculated - data.insert("precision_test".into(), proto_string("0.3926990125")); // Placeholder, will be calculated - - let request = PostTableDataRequest { - profile_name: context.profile_name.clone(), - table_name: context.calculation_table.clone(), - data, - }; - - let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap(); - assert!(response.success); - - // Verify precision is maintained in stored values - let query = format!( - r#"SELECT result, precision_test FROM "{}"."{}" WHERE id = $1"#, - context.profile_name, context.calculation_table - ); - let row = sqlx::query(&query) - .bind(response.inserted_id) - .fetch_one(&context.pool) - .await - .unwrap(); - - let stored_result: Decimal = row.get("result"); - let stored_precision: Decimal = row.get("precision_test"); - - // Verify that the results are calculated by Steel and have proper precision - assert!(stored_result.scale() <= 5); // Should fit in decimal(20,5) - assert!(stored_precision.scale() <= 10); // Should fit in decimal(30,10) - println!("Steel calculated result: {}", stored_result); - println!("Steel calculated precision test: {}", stored_precision); -} - -#[rstest] -#[tokio::test] -async fn test_steel_decimal_business_logic_validation(#[future] steel_decimal_context: SteelDecimalTestContext) { - let context = steel_decimal_context.await; - let indexer_tx = create_test_indexer_channel().await; - - // Test business logic with acceptable margin - let mut data = HashMap::new(); - data.insert("product_code".into(), proto_string("PROD001")); - data.insert("price".into(), proto_string("100.00")); - data.insert("discount_percent".into(), proto_string("15.00")); // 15% discount - data.insert("discounted_price".into(), proto_string("85.00")); // 100 * (1 - 0.15) = 85 - data.insert("margin_check".into(), proto_string("ACCEPTABLE")); // 15% margin > 20% minimum -> should be "TOO_LOW" - - let request = PostTableDataRequest { - profile_name: context.profile_name.clone(), - table_name: context.validation_table.clone(), - data, - }; - - let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap(); - assert!(response.success); - - // Verify business logic was applied correctly - let query = format!( - r#"SELECT discounted_price, margin_check FROM "{}"."{}" WHERE id = $1"#, - context.profile_name, context.validation_table - ); - let row = sqlx::query(&query) - .bind(response.inserted_id) - .fetch_one(&context.pool) - .await - .unwrap(); - - let stored_discounted: Decimal = row.get("discounted_price"); - let stored_margin_check: String = row.get("margin_check"); - - assert_eq!(stored_discounted, dec!(85.00)); - assert_eq!(stored_margin_check, "TOO_LOW"); // 15% margin is less than 20% minimum -} - -// ======================================================================== -// STEEL DECIMAL ERROR HANDLING TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_steel_decimal_division_by_zero_handling(#[future] steel_decimal_context: SteelDecimalTestContext) { - let context = steel_decimal_context.await; - let indexer_tx = create_test_indexer_channel().await; - - // Test division by zero in Steel script - let mut data = HashMap::new(); - data.insert("operation_name".into(), proto_string("Division by Zero Test")); - data.insert("input_a".into(), proto_string("100.00")); - data.insert("input_b".into(), proto_string("0.00")); // Division by zero - data.insert("result".into(), proto_string("0.00")); - data.insert("precision_test".into(), proto_string("0.00")); - - let request = PostTableDataRequest { - profile_name: context.profile_name.clone(), - table_name: context.calculation_table.clone(), - data, - }; - - let result = post_table_data(&context.pool, request, &indexer_tx).await; - - // Should fail due to division by zero in Steel script - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(err.code(), tonic::Code::InvalidArgument); - assert!(err.message().contains("Script execution failed")); - } -} - -#[rstest] -#[tokio::test] -async fn test_steel_decimal_invalid_input_format(#[future] steel_decimal_context: SteelDecimalTestContext) { - let context = steel_decimal_context.await; - let indexer_tx = create_test_indexer_channel().await; - - // Test invalid decimal format in Steel script input - let mut data = HashMap::new(); - data.insert("item_name".into(), proto_string("Invalid Input Test")); - data.insert("quantity".into(), proto_number(3.0)); - data.insert("unit_price".into(), proto_string("not-a-number")); // Invalid decimal - data.insert("total_price".into(), proto_string("0.00")); - data.insert("tax_rate".into(), proto_string("0.1000")); - data.insert("final_amount".into(), proto_string("0.00")); - - let request = PostTableDataRequest { - profile_name: context.profile_name.clone(), - table_name: context.invoice_table.clone(), - data, - }; - - let result = post_table_data(&context.pool, request, &indexer_tx).await; - - // Should fail due to invalid decimal format - assert!(result.is_err()); - if let Err(err) = result { - assert_eq!(err.code(), tonic::Code::InvalidArgument); - assert!(err.message().contains("Script execution failed")); - } -} - -// ======================================================================== -// STEEL DECIMAL ROUNDTRIP AND CONVERSION TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_steel_decimal_roundtrip_conversion(#[future] steel_decimal_context: SteelDecimalTestContext) { - let context = steel_decimal_context.await; - let indexer_tx = create_test_indexer_channel().await; - - // Test various decimal formats and precision levels - let test_cases = vec![ - ("0.01", 2.0, "Small decimal"), - ("999.99", 10.0, "Large decimal with cents"), - ("123.456789", 1.0, "High precision input"), - ("0.00001", 100000.0, "Very small decimal"), - ]; - - for (unit_price, quantity, description) in test_cases { - let mut data = HashMap::new(); - data.insert("item_name".into(), proto_string(description)); - data.insert("quantity".into(), proto_number(quantity)); - data.insert("unit_price".into(), proto_string(unit_price)); - data.insert("total_price".into(), proto_string("0.00")); // Will be calculated - data.insert("tax_rate".into(), proto_string("0.0500")); // 5% - data.insert("final_amount".into(), proto_string("0.00")); // Will be calculated - - let request = PostTableDataRequest { - profile_name: context.profile_name.clone(), - table_name: context.invoice_table.clone(), - data, - }; - - let response = post_table_data(&context.pool, request, &indexer_tx).await - .expect(&format!("Failed for test case: {}", description)); - - // Verify that Steel decimal calculations are consistent - let query = format!( - r#"SELECT unit_price, quantity, total_price, final_amount FROM "{}"."{}" WHERE id = $1"#, - context.profile_name, context.invoice_table - ); - let row = sqlx::query(&query) - .bind(response.inserted_id) - .fetch_one(&context.pool) - .await - .unwrap(); - - let stored_unit_price: Decimal = row.get("unit_price"); - let stored_quantity: i32 = row.get("quantity"); - let stored_total: Decimal = row.get("total_price"); - let stored_final: Decimal = row.get("final_amount"); - - // Verify Steel calculations match expected Rust calculations - let expected_unit_price = Decimal::from_str(unit_price).unwrap(); - let expected_total = expected_unit_price * Decimal::from(stored_quantity); - let expected_final = expected_total * dec!(1.05); // 5% tax - - assert_eq!(stored_unit_price, expected_unit_price); - assert_eq!(stored_total, expected_total); - assert_eq!(stored_final, expected_final); - - println!("✓ Roundtrip test passed for {}: {} * {} = {} -> {}", - description, unit_price, quantity, stored_total, stored_final); - } -} - -#[rstest] -#[tokio::test] -async fn test_steel_decimal_negative_values(#[future] steel_decimal_context: SteelDecimalTestContext) { - let context = steel_decimal_context.await; - let indexer_tx = create_test_indexer_channel().await; - - // Test negative discount (surcharge) - let mut data = HashMap::new(); - data.insert("product_code".into(), proto_string("SURCHG001")); - data.insert("price".into(), proto_string("50.00")); - data.insert("discount_percent".into(), proto_string("-10.00")); // 10% surcharge - data.insert("discounted_price".into(), proto_string("55.00")); // 50 * (1 - (-0.1)) = 50 * 1.1 = 55 - data.insert("margin_check".into(), proto_string("ACCEPTABLE")); // Will be calculated - - let request = PostTableDataRequest { - profile_name: context.profile_name.clone(), - table_name: context.validation_table.clone(), - data, - }; - - let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap(); - - // Verify negative percentage handling - let query = format!( - r#"SELECT discounted_price, margin_check FROM "{}"."{}" WHERE id = $1"#, - context.profile_name, context.validation_table - ); - let row = sqlx::query(&query) - .bind(response.inserted_id) - .fetch_one(&context.pool) - .await - .unwrap(); - - let stored_discounted: Decimal = row.get("discounted_price"); - let stored_margin_check: String = row.get("margin_check"); - - assert_eq!(stored_discounted, dec!(55.00)); - // With surcharge, margin should be acceptable (negative discount increases final price) - assert_eq!(stored_margin_check, "ACCEPTABLE"); -} - -// ======================================================================== -// CONCURRENT STEEL DECIMAL EXECUTION TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_concurrent_steel_decimal_calculations(#[future] steel_decimal_context: SteelDecimalTestContext) { - let context = steel_decimal_context.await; - - use futures::future::join_all; - - // Test concurrent Steel decimal calculations - let tasks: Vec<_> = (0..10).map(|i| { - let context = context.clone(); - async move { - let indexer_tx = create_test_indexer_channel().await; - let unit_price = format!("{}.99", 10 + i); - let quantity = i + 1; - - let mut data = HashMap::new(); - data.insert("item_name".into(), proto_string(&format!("Concurrent Item {}", i))); - data.insert("quantity".into(), proto_number(quantity as f64)); - data.insert("unit_price".into(), proto_string(&unit_price)); - data.insert("total_price".into(), proto_string("0.00")); - data.insert("tax_rate".into(), proto_string("0.2000")); // 20% - data.insert("final_amount".into(), proto_string("0.00")); - - let request = PostTableDataRequest { - profile_name: context.profile_name.clone(), - table_name: context.invoice_table.clone(), - data, - }; - - post_table_data(&context.pool, request, &indexer_tx).await - } - }).collect(); - - let results = join_all(tasks).await; - - // All Steel calculations should succeed concurrently - for (i, result) in results.into_iter().enumerate() { - assert!(result.is_ok(), "Concurrent Steel calculation {} should succeed", i); - } - - // Verify all calculations were performed correctly - let query = format!( - r#"SELECT COUNT(*) as count FROM "{}"."{}" WHERE item_name LIKE 'Concurrent Item%'"#, - context.profile_name, context.invoice_table - ); - let count: i64 = sqlx::query_scalar(&query) - .fetch_one(&context.pool) - .await - .unwrap(); - - assert_eq!(count, 10); -} - -// ======================================================================== -// STEEL DECIMAL CRATE FEATURE VERIFICATION TESTS -// ======================================================================== - -#[rstest] -#[tokio::test] -async fn test_steel_decimal_crate_features_available(#[future] steel_decimal_context: SteelDecimalTestContext) { - let context = steel_decimal_context.await; - let indexer_tx = create_test_indexer_channel().await; - - // Test that Steel decimal crate functions are properly registered - // This tests sqrt, which should only be available if steel-decimal is properly integrated - let mut data = HashMap::new(); - data.insert("operation_name".into(), proto_string("Steel Decimal Features Test")); - data.insert("input_a".into(), proto_string("25.00")); // sqrt(25) = 5 - data.insert("input_b".into(), proto_string("4.00")); // sqrt(4) = 2 - data.insert("result".into(), proto_string("0.00")); // Should be (25 + 4) * 5 = 145 - data.insert("precision_test".into(), proto_string("0.00")); // Should be 25/4 * pi ≈ 19.6349 - - let request = PostTableDataRequest { - profile_name: context.profile_name.clone(), - table_name: context.calculation_table.clone(), - data, - }; - - let response = post_table_data(&context.pool, request, &indexer_tx).await.unwrap(); - - // Verify that advanced steel-decimal functions (like sqrt) worked - let query = format!( - r#"SELECT result, precision_test FROM "{}"."{}" WHERE id = $1"#, - context.profile_name, context.calculation_table - ); - let row = sqlx::query(&query) - .bind(response.inserted_id) - .fetch_one(&context.pool) - .await - .unwrap(); - - let stored_result: Decimal = row.get("result"); - let stored_precision: Decimal = row.get("precision_test"); - - // If steel-decimal is properly integrated, sqrt function should work - // Result should be (25 + 4) * sqrt(25) = 29 * 5 = 145 - assert_eq!(stored_result, dec!(145.00000)); - - // Precision test: 25/4 * π ≈ 19.634954... - assert!(stored_precision > dec!(19.6) && stored_precision < dec!(19.7)); - - println!("✓ Steel decimal crate features verified:"); - println!(" - sqrt function available and working"); - println!(" - High precision arithmetic: {}", stored_precision); - println!(" - Complex calculations: {}", stored_result); -} - -#[rstest] -#[tokio::test] -async fn test_steel_decimal_vs_native_rust_consistency(#[future] steel_decimal_context: SteelDecimalTestContext) { - let context = steel_decimal_context.await; - let indexer_tx = create_test_indexer_channel().await; - - // Test that Steel decimal calculations match native Rust decimal calculations - let test_cases = vec![ - ("100.00", "0.15", "Steel vs Rust consistency test 1"), - ("999.99", "0.0825", "Steel vs Rust consistency test 2"), - ("0.01", "0.9999", "Steel vs Rust edge case"), - ]; - - for (price, discount_rate, description) in test_cases { - let mut data = HashMap::new(); - data.insert("product_code".into(), proto_string("CONSISTENCY")); - data.insert("price".into(), proto_string(price)); - data.insert("discount_percent".into(), proto_string(&format!("{}", Decimal::from_str(discount_rate).unwrap() * dec!(100)))); - data.insert("discounted_price".into(), proto_string("0.00")); // Calculated by Steel - data.insert("margin_check".into(), proto_string("TEST")); // Calculated by Steel - - let request = PostTableDataRequest { - profile_name: context.profile_name.clone(), - table_name: context.validation_table.clone(), - data, - }; - - let response = post_table_data(&context.pool, request, &indexer_tx).await - .expect(&format!("Failed for consistency test: {}", description)); - - // Get Steel-calculated result - let query = format!( - r#"SELECT discounted_price FROM "{}"."{}" WHERE id = $1"#, - context.profile_name, context.validation_table - ); - let steel_result: Decimal = sqlx::query_scalar(&query) - .bind(response.inserted_id) - .fetch_one(&context.pool) - .await - .unwrap(); - - // Calculate using native Rust decimal - let price_decimal = Decimal::from_str(price).unwrap(); - let discount_decimal = Decimal::from_str(discount_rate).unwrap(); - let rust_result = price_decimal * (dec!(1) - discount_decimal); - - // Steel and Rust calculations should match exactly - assert_eq!(steel_result, rust_result, - "Steel vs Rust mismatch for {}: Steel={}, Rust={}", - description, steel_result, rust_result); - - println!("✓ Consistency verified for {}: {}", description, steel_result); - } -}