558 lines
21 KiB
Rust
558 lines
21 KiB
Rust
#[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<String> = 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<TableScriptResponse, tonic::Status> {
|
|
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();
|
|
});
|
|
}
|
|
}
|