crucial self reference allowed

This commit is contained in:
filipriec
2025-07-17 22:06:53 +02:00
parent 810ef5fc10
commit 24c2376ea1
12 changed files with 2312 additions and 190 deletions

1
Cargo.lock generated
View File

@@ -3006,6 +3006,7 @@ dependencies = [
"thiserror", "thiserror",
"time", "time",
"tokio", "tokio",
"tokio-test",
"tonic", "tonic",
"tonic-reflection", "tonic-reflection",
"tracing", "tracing",

View File

@@ -50,3 +50,4 @@ rstest = "0.25.0"
lazy_static = "1.5.0" lazy_static = "1.5.0"
rand = "0.9.1" rand = "0.9.1"
futures = "0.3.31" futures = "0.3.31"
tokio-test = "0.4.4"

View File

@@ -273,8 +273,10 @@ impl DependencyAnalyzer {
/// Validates that structured table access (steel_get_column functions) respects link constraints /// Validates that structured table access (steel_get_column functions) respects link constraints
/// Raw SQL access (steel_query_sql) is allowed to reference any table /// Raw SQL access (steel_query_sql) is allowed to reference any table
/// SELF-REFERENCES are always allowed (table can access its own columns)
/// ///
/// Example: /// 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 /// - 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 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") ❌
@@ -285,6 +287,15 @@ impl DependencyAnalyzer {
source_table_id: i64, source_table_id: i64,
dependencies: &[Dependency], dependencies: &[Dependency],
) -> Result<(), DependencyError> { ) -> 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 // Get all valid linked tables for the source table
let linked_tables = sqlx::query!( let linked_tables = sqlx::query!(
r#"SELECT td.table_name, tdl.is_required r#"SELECT td.table_name, tdl.is_required
@@ -298,30 +309,24 @@ impl DependencyAnalyzer {
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?; .map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?;
// Create a set of allowed table names for quick lookup // Create a set of allowed table names for quick lookup
let allowed_tables: std::collections::HashSet<String> = linked_tables let mut allowed_tables: std::collections::HashSet<String> = linked_tables
.into_iter() .into_iter()
.map(|row| row.table_name) .map(|row| row.table_name)
.collect(); .collect();
// Get the current table name for better error messages // ALWAYS allow self-references
let current_table_name = sqlx::query_scalar!( allowed_tables.insert(current_table_name.clone());
"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() })?;
// Validate each dependency // Validate each dependency
for dep in dependencies { for dep in dependencies {
match &dep.dependency_type { 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, .. } => { DependencyType::ColumnAccess { column } | DependencyType::IndexedAccess { column, .. } => {
if !allowed_tables.contains(&dep.target_table) { if !allowed_tables.contains(&dep.target_table) {
return Err(DependencyError::InvalidTableReference { return Err(DependencyError::InvalidTableReference {
table_name: dep.target_table.clone(), table_name: dep.target_table.clone(),
script_context: format!( 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, dep.target_table,
current_table_name, current_table_name,
dep.target_table, dep.target_table,

View File

@@ -223,6 +223,7 @@ mod tests {
let cycle = detect_cycle_dfs(1, &graph, &mut visited, &mut rec_stack, &table_names); let cycle = detect_cycle_dfs(1, &graph, &mut visited, &mut rec_stack, &table_names);
assert!(cycle.is_some()); 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"));
} }
} }

View File

@@ -1,4 +1,6 @@
// tests/mod.rs // tests/mod.rs
pub mod tables_data;
// pub mod tables_data;
pub mod common; pub mod common;
pub mod table_script;
// pub mod table_definition; // pub mod table_definition;

View File

@@ -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<String> = 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
);
}

View File

@@ -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<String> = 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");
}

View File

@@ -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::*;

View File

@@ -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<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();
});
}
}

View File

@@ -1,26 +1,65 @@
// tests/table_script/prohibited_types_test.rs // tests/table_script/prohibited_types_test.rs
#[cfg(test)] use crate::common::setup_isolated_db;
mod prohibited_types_tests { use server::table_script::handlers::post_table_script::post_table_script;
use super::*; use common::proto::multieko2::table_script::PostTableScriptRequest;
use common::proto::multieko2::table_script::PostTableScriptRequest; use serde_json::json;
use sqlx::PgPool; use sqlx::PgPool;
#[tokio::test] /// Helper function to create a test table with specified columns
async fn test_reject_bigint_target_column() { async fn create_test_table(
let pool = setup_test_db().await; pool: &PgPool,
schema_id: i64,
table_name: &str,
columns: Vec<(&str, &str)>,
) -> i64 {
let column_definitions: Vec<String> = 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")
}
#[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 // Create a table with a BIGINT column
let table_id = create_test_table_with_bigint_column(&pool).await; let table_id = create_test_table(
&pool,
schema_id,
"bigint_table",
vec![("name", "TEXT"), ("big_number", "BIGINT")]
).await;
let request = PostTableScriptRequest { let request = PostTableScriptRequest {
table_definition_id: table_id, table_definition_id: table_id,
target_column: "big_number".to_string(), // This is BIGINT target_column: "big_number".to_string(), // This is BIGINT
script: r#" script: r#"(+ "10" "20")"#.to_string(),
(define result "some calculation") description: "Test script".to_string(), // Remove Some() wrapper
result
"#.to_string(),
description: "Test script".to_string(),
}; };
let result = post_table_script(&pool, request).await; let result = post_table_script(&pool, request).await;
@@ -28,24 +67,30 @@ mod prohibited_types_tests {
// Should fail with prohibited type error // Should fail with prohibited type error
assert!(result.is_err()); assert!(result.is_err());
let error_msg = result.unwrap_err().to_string(); let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Cannot create script for column 'big_number' with type 'BIGINT'")); assert!(
assert!(error_msg.contains("Steel scripts cannot target columns of type: BIGINT, DATE, TIMESTAMPTZ")); error_msg.contains("prohibited type") || error_msg.contains("BIGINT"),
} "Error should mention prohibited type or BIGINT: {}",
error_msg
);
}
#[tokio::test] #[tokio::test]
async fn test_reject_date_target_column() { async fn test_reject_date_target_column() {
let pool = setup_test_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await;
// Create a table with a DATE column // Create a table with a DATE column
let table_id = create_test_table_with_date_column(&pool).await; let table_id = create_test_table(
&pool,
schema_id,
"date_table",
vec![("name", "TEXT"), ("event_date", "DATE")]
).await;
let request = PostTableScriptRequest { let request = PostTableScriptRequest {
table_definition_id: table_id, table_definition_id: table_id,
target_column: "event_date".to_string(), // This is DATE target_column: "event_date".to_string(), // This is DATE
script: r#" script: r#"(+ "10" "20")"#.to_string(),
(define result "2024-01-01")
result
"#.to_string(),
description: "Test script".to_string(), description: "Test script".to_string(),
}; };
@@ -54,23 +99,30 @@ mod prohibited_types_tests {
// Should fail with prohibited type error // Should fail with prohibited type error
assert!(result.is_err()); assert!(result.is_err());
let error_msg = result.unwrap_err().to_string(); let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Cannot create script for column 'event_date' with type 'DATE'")); assert!(
} error_msg.contains("prohibited type") || error_msg.contains("DATE"),
"Error should mention prohibited type or DATE: {}",
error_msg
);
}
#[tokio::test] #[tokio::test]
async fn test_reject_timestamptz_target_column() { async fn test_reject_timestamptz_target_column() {
let pool = setup_test_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await;
// Create a table with a TIMESTAMPTZ column // Create a table with a TIMESTAMPTZ column
let table_id = create_test_table_with_timestamptz_column(&pool).await; let table_id = create_test_table(
&pool,
schema_id,
"timestamp_table",
vec![("name", "TEXT"), ("created_time", "TIMESTAMPTZ")]
).await;
let request = PostTableScriptRequest { let request = PostTableScriptRequest {
table_definition_id: table_id, table_definition_id: table_id,
target_column: "created_time".to_string(), // This is TIMESTAMPTZ target_column: "created_time".to_string(), // This is TIMESTAMPTZ
script: r#" script: r#"(+ "10" "20")"#.to_string(),
(define result "2024-01-01T10:00:00Z")
result
"#.to_string(),
description: "Test script".to_string(), description: "Test script".to_string(),
}; };
@@ -79,103 +131,182 @@ mod prohibited_types_tests {
// Should fail with prohibited type error // Should fail with prohibited type error
assert!(result.is_err()); assert!(result.is_err());
let error_msg = result.unwrap_err().to_string(); let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Cannot create script for column 'created_time' with type 'TIMESTAMPTZ'")); assert!(
} error_msg.contains("prohibited type") || error_msg.contains("TIMESTAMPTZ"),
"Error should mention prohibited type or TIMESTAMPTZ: {}",
error_msg
);
}
#[tokio::test] #[tokio::test]
async fn test_reject_script_referencing_prohibited_column() { async fn test_reject_text_in_mathematical_operations() {
let pool = setup_test_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await;
// Create linked tables - one with BIGINT column, another with TEXT target // Create a table with TEXT and NUMERIC columns
let source_table_id = create_test_table_with_text_column(&pool).await; let table_id = create_test_table(
let linked_table_id = create_test_table_with_bigint_column(&pool).await; &pool,
schema_id,
// Create link between tables "text_math_table",
create_table_link(&pool, source_table_id, linked_table_id).await; vec![
("description", "TEXT"),
("amount", "NUMERIC(10, 2)"),
("result", "NUMERIC(10, 2)")
]
).await;
let request = PostTableScriptRequest { let request = PostTableScriptRequest {
table_definition_id: source_table_id, table_definition_id: table_id,
target_column: "description".to_string(), // This is TEXT (allowed) target_column: "result".to_string(),
script: r#" script: r#"(+ (steel_get_column "text_math_table" "description") "10")"#.to_string(),
(define big_val (steel_get_column "linked_table" "big_number")) description: "Script that tries to use TEXT in math".to_string(),
(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; let result = post_table_script(&pool, request).await;
// Should fail because script references BIGINT column // Should fail because script uses TEXT in mathematical operation
assert!(result.is_err()); assert!(result.is_err());
let error_msg = result.unwrap_err().to_string(); let error_msg = result.unwrap_err().to_string();
assert!(error_msg.contains("Script cannot reference column 'big_number'")); assert!(
assert!(error_msg.contains("prohibited type 'BIGINT'")); error_msg.contains("mathematical operations") || error_msg.contains("TEXT"),
} "Error should mention mathematical operations or TEXT: {}",
error_msg
);
}
#[tokio::test] #[tokio::test]
async fn test_allow_valid_script_with_allowed_types() { async fn test_reject_boolean_in_mathematical_operations() {
let pool = setup_test_db().await; let pool = setup_isolated_db().await;
let schema_id = get_default_schema_id(&pool).await;
// Create a table with allowed column types // Create a table with BOOLEAN and NUMERIC columns
let table_id = create_test_table_with_allowed_columns(&pool).await; 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 { let request = PostTableScriptRequest {
table_definition_id: table_id, table_definition_id: table_id,
target_column: "computed_value".to_string(), // This is TEXT (allowed) target_column: "result".to_string(),
script: r#" script: r#"(* (steel_get_column "boolean_math_table" "is_active") "5")"#.to_string(),
(define name_val (steel_get_column "test_table" "name")) description: "Script that tries to use BOOLEAN in math".to_string(),
(define count_val (steel_get_column "test_table" "count")) };
(string-append name_val " has " (number->string count_val) " items")
"#.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(), description: "Valid script using allowed types".to_string(),
}; };
let result = post_table_script(&pool, request).await; let result = post_table_script(&pool, request).await;
// Should succeed // Should succeed
assert!(result.is_ok()); 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(); let response = result.unwrap();
assert!(response.id > 0); 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")
}
} }

View File

@@ -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<String> = 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
);
}