crucial self reference allowed
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3006,6 +3006,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-test",
|
||||
"tonic",
|
||||
"tonic-reflection",
|
||||
"tracing",
|
||||
|
||||
@@ -50,3 +50,4 @@ rstest = "0.25.0"
|
||||
lazy_static = "1.5.0"
|
||||
rand = "0.9.1"
|
||||
futures = "0.3.31"
|
||||
tokio-test = "0.4.4"
|
||||
|
||||
@@ -273,8 +273,10 @@ impl DependencyAnalyzer {
|
||||
|
||||
/// Validates that structured table access (steel_get_column functions) respects link constraints
|
||||
/// Raw SQL access (steel_query_sql) is allowed to reference any table
|
||||
/// SELF-REFERENCES are always allowed (table can access its own columns)
|
||||
///
|
||||
/// Example:
|
||||
/// - Table A can ALWAYS use: (steel_get_column "table_a" "column_name") ✅ (self-reference)
|
||||
/// - Table A is linked to Table B via table_definition_links
|
||||
/// - Script for Table A can use: (steel_get_column "table_b" "column_name") ✅
|
||||
/// - Script for Table A CANNOT use: (steel_get_column "table_c" "column_name") ❌
|
||||
@@ -285,6 +287,15 @@ impl DependencyAnalyzer {
|
||||
source_table_id: i64,
|
||||
dependencies: &[Dependency],
|
||||
) -> Result<(), DependencyError> {
|
||||
// Get the current table name for self-reference checking
|
||||
let current_table_name = sqlx::query_scalar!(
|
||||
"SELECT table_name FROM table_definitions WHERE id = $1",
|
||||
source_table_id
|
||||
)
|
||||
.fetch_one(&mut **tx)
|
||||
.await
|
||||
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?;
|
||||
|
||||
// Get all valid linked tables for the source table
|
||||
let linked_tables = sqlx::query!(
|
||||
r#"SELECT td.table_name, tdl.is_required
|
||||
@@ -298,30 +309,24 @@ impl DependencyAnalyzer {
|
||||
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?;
|
||||
|
||||
// Create a set of allowed table names for quick lookup
|
||||
let allowed_tables: std::collections::HashSet<String> = linked_tables
|
||||
let mut allowed_tables: std::collections::HashSet<String> = linked_tables
|
||||
.into_iter()
|
||||
.map(|row| row.table_name)
|
||||
.collect();
|
||||
|
||||
// Get the current table name for better error messages
|
||||
let current_table_name = sqlx::query_scalar!(
|
||||
"SELECT table_name FROM table_definitions WHERE id = $1",
|
||||
source_table_id
|
||||
)
|
||||
.fetch_one(&mut **tx)
|
||||
.await
|
||||
.map_err(|e| DependencyError::DatabaseError { error: e.to_string() })?;
|
||||
// ALWAYS allow self-references
|
||||
allowed_tables.insert(current_table_name.clone());
|
||||
|
||||
// Validate each dependency
|
||||
for dep in dependencies {
|
||||
match &dep.dependency_type {
|
||||
// Structured access must respect link constraints
|
||||
// Structured access must respect link constraints (but self-references are always allowed)
|
||||
DependencyType::ColumnAccess { column } | DependencyType::IndexedAccess { column, .. } => {
|
||||
if !allowed_tables.contains(&dep.target_table) {
|
||||
return Err(DependencyError::InvalidTableReference {
|
||||
table_name: dep.target_table.clone(),
|
||||
script_context: format!(
|
||||
"Table '{}' is not linked to '{}'. Add a link in the table definition to access '{}' via steel_get_column functions. Column attempted: '{}'",
|
||||
"Table '{}' is not linked to '{}'. Add a link in the table definition to access '{}' via steel_get_column functions. Column attempted: '{}'. Note: Self-references are always allowed.",
|
||||
dep.target_table,
|
||||
current_table_name,
|
||||
dep.target_table,
|
||||
|
||||
@@ -223,6 +223,7 @@ mod tests {
|
||||
|
||||
let cycle = detect_cycle_dfs(1, &graph, &mut visited, &mut rec_stack, &table_names);
|
||||
assert!(cycle.is_some());
|
||||
assert!(cycle.unwrap().contains("table_a") && cycle.unwrap().contains("table_b"));
|
||||
let cycle_str = cycle.unwrap();
|
||||
assert!(cycle_str.contains("table_a") && cycle_str.contains("table_b"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// tests/mod.rs
|
||||
pub mod tables_data;
|
||||
|
||||
// pub mod tables_data;
|
||||
pub mod common;
|
||||
pub mod table_script;
|
||||
// pub mod table_definition;
|
||||
|
||||
499
server/tests/table_script/comprehensive_error_scenarios_tests.rs
Normal file
499
server/tests/table_script/comprehensive_error_scenarios_tests.rs
Normal 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
|
||||
);
|
||||
}
|
||||
453
server/tests/table_script/mathematical_operations_tests.rs
Normal file
453
server/tests/table_script/mathematical_operations_tests.rs
Normal 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");
|
||||
}
|
||||
18
server/tests/table_script/mod.rs
Normal file
18
server/tests/table_script/mod.rs
Normal 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::*;
|
||||
557
server/tests/table_script/post_scripts_integration_tests.rs
Normal file
557
server/tests/table_script/post_scripts_integration_tests.rs
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
0
server/tests/table_script/post_scripts_tests.rs
Normal file
0
server/tests/table_script/post_scripts_tests.rs
Normal file
@@ -1,181 +1,312 @@
|
||||
// tests/table_script/prohibited_types_test.rs
|
||||
|
||||
#[cfg(test)]
|
||||
mod prohibited_types_tests {
|
||||
use super::*;
|
||||
use common::proto::multieko2::table_script::PostTableScriptRequest;
|
||||
use sqlx::PgPool;
|
||||
use crate::common::setup_isolated_db;
|
||||
use server::table_script::handlers::post_table_script::post_table_script;
|
||||
use common::proto::multieko2::table_script::PostTableScriptRequest;
|
||||
use serde_json::json;
|
||||
use sqlx::PgPool;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_bigint_target_column() {
|
||||
let pool = setup_test_db().await;
|
||||
/// 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();
|
||||
|
||||
// Create a table with a BIGINT column
|
||||
let table_id = create_test_table_with_bigint_column(&pool).await;
|
||||
let columns_json = json!(column_definitions);
|
||||
let indexes_json = json!([]);
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: table_id,
|
||||
target_column: "big_number".to_string(), // This is BIGINT
|
||||
script: r#"
|
||||
(define result "some calculation")
|
||||
result
|
||||
"#.to_string(),
|
||||
description: "Test script".to_string(),
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should fail with prohibited type error
|
||||
assert!(result.is_err());
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(error_msg.contains("Cannot create script for column 'big_number' with type 'BIGINT'"));
|
||||
assert!(error_msg.contains("Steel scripts cannot target columns of type: BIGINT, DATE, TIMESTAMPTZ"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_date_target_column() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
// Create a table with a DATE column
|
||||
let table_id = create_test_table_with_date_column(&pool).await;
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: table_id,
|
||||
target_column: "event_date".to_string(), // This is DATE
|
||||
script: r#"
|
||||
(define result "2024-01-01")
|
||||
result
|
||||
"#.to_string(),
|
||||
description: "Test script".to_string(),
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should fail with prohibited type error
|
||||
assert!(result.is_err());
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(error_msg.contains("Cannot create script for column 'event_date' with type 'DATE'"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_timestamptz_target_column() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
// Create a table with a TIMESTAMPTZ column
|
||||
let table_id = create_test_table_with_timestamptz_column(&pool).await;
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: table_id,
|
||||
target_column: "created_time".to_string(), // This is TIMESTAMPTZ
|
||||
script: r#"
|
||||
(define result "2024-01-01T10:00:00Z")
|
||||
result
|
||||
"#.to_string(),
|
||||
description: "Test script".to_string(),
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should fail with prohibited type error
|
||||
assert!(result.is_err());
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(error_msg.contains("Cannot create script for column 'created_time' with type 'TIMESTAMPTZ'"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_script_referencing_prohibited_column() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
// Create linked tables - one with BIGINT column, another with TEXT target
|
||||
let source_table_id = create_test_table_with_text_column(&pool).await;
|
||||
let linked_table_id = create_test_table_with_bigint_column(&pool).await;
|
||||
|
||||
// Create link between tables
|
||||
create_table_link(&pool, source_table_id, linked_table_id).await;
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: source_table_id,
|
||||
target_column: "description".to_string(), // This is TEXT (allowed)
|
||||
script: r#"
|
||||
(define big_val (steel_get_column "linked_table" "big_number"))
|
||||
(string-append "Value: " (number->string big_val))
|
||||
"#.to_string(),
|
||||
description: "Script that tries to access BIGINT column".to_string(),
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should fail because script references BIGINT column
|
||||
assert!(result.is_err());
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(error_msg.contains("Script cannot reference column 'big_number'"));
|
||||
assert!(error_msg.contains("prohibited type 'BIGINT'"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_allow_valid_script_with_allowed_types() {
|
||||
let pool = setup_test_db().await;
|
||||
|
||||
// Create a table with allowed column types
|
||||
let table_id = create_test_table_with_allowed_columns(&pool).await;
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: table_id,
|
||||
target_column: "computed_value".to_string(), // This is TEXT (allowed)
|
||||
script: r#"
|
||||
(define name_val (steel_get_column "test_table" "name"))
|
||||
(define count_val (steel_get_column "test_table" "count"))
|
||||
(string-append name_val " has " (number->string count_val) " items")
|
||||
"#.to_string(),
|
||||
description: "Valid script using allowed types".to_string(),
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should succeed
|
||||
assert!(result.is_ok());
|
||||
let response = result.unwrap();
|
||||
assert!(response.id > 0);
|
||||
}
|
||||
|
||||
// Helper functions for test setup
|
||||
async fn setup_test_db() -> PgPool {
|
||||
// Your test database setup code here
|
||||
todo!("Implement test DB setup")
|
||||
}
|
||||
|
||||
async fn create_test_table_with_bigint_column(pool: &PgPool) -> i64 {
|
||||
// Create table definition with BIGINT column
|
||||
// JSON columns would be: ["name TEXT", "big_number BIGINT"]
|
||||
todo!("Implement table creation with BIGINT")
|
||||
}
|
||||
|
||||
async fn create_test_table_with_date_column(pool: &PgPool) -> i64 {
|
||||
// Create table definition with DATE column
|
||||
// JSON columns would be: ["name TEXT", "event_date DATE"]
|
||||
todo!("Implement table creation with DATE")
|
||||
}
|
||||
|
||||
async fn create_test_table_with_timestamptz_column(pool: &PgPool) -> i64 {
|
||||
// Create table definition with TIMESTAMPTZ column
|
||||
// JSON columns would be: ["name TEXT", "created_time TIMESTAMPTZ"]
|
||||
todo!("Implement table creation with TIMESTAMPTZ")
|
||||
}
|
||||
|
||||
async fn create_test_table_with_text_column(pool: &PgPool) -> i64 {
|
||||
// Create table definition with TEXT columns only
|
||||
// JSON columns would be: ["name TEXT", "description TEXT"]
|
||||
todo!("Implement table creation with TEXT")
|
||||
}
|
||||
|
||||
async fn create_test_table_with_allowed_columns(pool: &PgPool) -> i64 {
|
||||
// Create table definition with only allowed column types
|
||||
// JSON columns would be: ["name TEXT", "count INTEGER", "computed_value TEXT"]
|
||||
todo!("Implement table creation with allowed types")
|
||||
}
|
||||
|
||||
async fn create_table_link(pool: &PgPool, source_id: i64, target_id: i64) {
|
||||
// Create a link in table_definition_links
|
||||
todo!("Implement table linking")
|
||||
}
|
||||
sqlx::query_scalar!(
|
||||
r#"INSERT INTO table_definitions (schema_id, table_name, columns, indexes)
|
||||
VALUES ($1, $2, $3, $4) RETURNING id"#,
|
||||
schema_id,
|
||||
table_name,
|
||||
columns_json,
|
||||
indexes_json
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.expect("Failed to create test table")
|
||||
}
|
||||
|
||||
/// Helper function to get default schema ID
|
||||
async fn get_default_schema_id(pool: &PgPool) -> i64 {
|
||||
sqlx::query_scalar!("SELECT id FROM schemas WHERE name = 'default'")
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.expect("Failed to get default schema ID")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_bigint_target_column() {
|
||||
let pool = setup_isolated_db().await;
|
||||
let schema_id = get_default_schema_id(&pool).await;
|
||||
|
||||
// Create a table with a BIGINT column
|
||||
let table_id = create_test_table(
|
||||
&pool,
|
||||
schema_id,
|
||||
"bigint_table",
|
||||
vec![("name", "TEXT"), ("big_number", "BIGINT")]
|
||||
).await;
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: table_id,
|
||||
target_column: "big_number".to_string(), // This is BIGINT
|
||||
script: r#"(+ "10" "20")"#.to_string(),
|
||||
description: "Test script".to_string(), // Remove Some() wrapper
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should fail with prohibited type error
|
||||
assert!(result.is_err());
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("prohibited type") || error_msg.contains("BIGINT"),
|
||||
"Error should mention prohibited type or BIGINT: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_date_target_column() {
|
||||
let pool = setup_isolated_db().await;
|
||||
let schema_id = get_default_schema_id(&pool).await;
|
||||
|
||||
// Create a table with a DATE column
|
||||
let table_id = create_test_table(
|
||||
&pool,
|
||||
schema_id,
|
||||
"date_table",
|
||||
vec![("name", "TEXT"), ("event_date", "DATE")]
|
||||
).await;
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: table_id,
|
||||
target_column: "event_date".to_string(), // This is DATE
|
||||
script: r#"(+ "10" "20")"#.to_string(),
|
||||
description: "Test script".to_string(),
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should fail with prohibited type error
|
||||
assert!(result.is_err());
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("prohibited type") || error_msg.contains("DATE"),
|
||||
"Error should mention prohibited type or DATE: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_timestamptz_target_column() {
|
||||
let pool = setup_isolated_db().await;
|
||||
let schema_id = get_default_schema_id(&pool).await;
|
||||
|
||||
// Create a table with a TIMESTAMPTZ column
|
||||
let table_id = create_test_table(
|
||||
&pool,
|
||||
schema_id,
|
||||
"timestamp_table",
|
||||
vec![("name", "TEXT"), ("created_time", "TIMESTAMPTZ")]
|
||||
).await;
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: table_id,
|
||||
target_column: "created_time".to_string(), // This is TIMESTAMPTZ
|
||||
script: r#"(+ "10" "20")"#.to_string(),
|
||||
description: "Test script".to_string(),
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should fail with prohibited type error
|
||||
assert!(result.is_err());
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("prohibited type") || error_msg.contains("TIMESTAMPTZ"),
|
||||
"Error should mention prohibited type or TIMESTAMPTZ: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_text_in_mathematical_operations() {
|
||||
let pool = setup_isolated_db().await;
|
||||
let schema_id = get_default_schema_id(&pool).await;
|
||||
|
||||
// Create a table with TEXT and NUMERIC columns
|
||||
let table_id = create_test_table(
|
||||
&pool,
|
||||
schema_id,
|
||||
"text_math_table",
|
||||
vec![
|
||||
("description", "TEXT"),
|
||||
("amount", "NUMERIC(10, 2)"),
|
||||
("result", "NUMERIC(10, 2)")
|
||||
]
|
||||
).await;
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: table_id,
|
||||
target_column: "result".to_string(),
|
||||
script: r#"(+ (steel_get_column "text_math_table" "description") "10")"#.to_string(),
|
||||
description: "Script that tries to use TEXT in math".to_string(),
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should fail because script uses TEXT in mathematical operation
|
||||
assert!(result.is_err());
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("mathematical operations") || error_msg.contains("TEXT"),
|
||||
"Error should mention mathematical operations or TEXT: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_boolean_in_mathematical_operations() {
|
||||
let pool = setup_isolated_db().await;
|
||||
let schema_id = get_default_schema_id(&pool).await;
|
||||
|
||||
// Create a table with BOOLEAN and NUMERIC columns
|
||||
let table_id = create_test_table(
|
||||
&pool,
|
||||
schema_id,
|
||||
"boolean_math_table",
|
||||
vec![
|
||||
("is_active", "BOOLEAN"),
|
||||
("amount", "NUMERIC(10, 2)"),
|
||||
("result", "NUMERIC(10, 2)")
|
||||
]
|
||||
).await;
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: table_id,
|
||||
target_column: "result".to_string(),
|
||||
script: r#"(* (steel_get_column "boolean_math_table" "is_active") "5")"#.to_string(),
|
||||
description: "Script that tries to use BOOLEAN in math".to_string(),
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should fail because script uses BOOLEAN in mathematical operation
|
||||
assert!(result.is_err());
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("mathematical operations") || error_msg.contains("BOOLEAN"),
|
||||
"Error should mention mathematical operations or BOOLEAN: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_reject_bigint_in_mathematical_operations() {
|
||||
let pool = setup_isolated_db().await;
|
||||
let schema_id = get_default_schema_id(&pool).await;
|
||||
|
||||
// Create a table with BIGINT and NUMERIC columns
|
||||
let table_id = create_test_table(
|
||||
&pool,
|
||||
schema_id,
|
||||
"bigint_math_table",
|
||||
vec![
|
||||
("big_value", "BIGINT"),
|
||||
("result", "NUMERIC(10, 2)")
|
||||
]
|
||||
).await;
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: table_id,
|
||||
target_column: "result".to_string(),
|
||||
script: r#"(/ (steel_get_column "bigint_math_table" "big_value") "2")"#.to_string(),
|
||||
description: "Script that tries to use BIGINT in math".to_string(),
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should fail because script uses BIGINT in mathematical operation
|
||||
assert!(result.is_err());
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(
|
||||
error_msg.contains("mathematical operations") || error_msg.contains("BIGINT"),
|
||||
"Error should mention mathematical operations or BIGINT: {}",
|
||||
error_msg
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_allow_valid_script_with_allowed_types() {
|
||||
let pool = setup_isolated_db().await;
|
||||
let schema_id = get_default_schema_id(&pool).await;
|
||||
|
||||
// Create a table with allowed column types
|
||||
let table_id = create_test_table(
|
||||
&pool,
|
||||
schema_id,
|
||||
"allowed_types_table",
|
||||
vec![
|
||||
("name", "TEXT"),
|
||||
("count", "INTEGER"),
|
||||
("amount", "NUMERIC(10, 2)"),
|
||||
("computed_value", "TEXT")
|
||||
]
|
||||
).await;
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: table_id,
|
||||
target_column: "computed_value".to_string(), // This is TEXT (allowed as target)
|
||||
script: r#"(steel_get_column "allowed_types_table" "name")"#.to_string(),
|
||||
description: "Valid script using allowed types".to_string(),
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should succeed
|
||||
assert!(result.is_ok(), "Valid script with allowed types should succeed");
|
||||
let response = result.unwrap();
|
||||
assert!(response.id > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_allow_integer_and_numeric_in_math_operations() {
|
||||
let pool = setup_isolated_db().await;
|
||||
let schema_id = get_default_schema_id(&pool).await;
|
||||
|
||||
// Create a table with allowed mathematical types
|
||||
let table_id = create_test_table(
|
||||
&pool,
|
||||
schema_id,
|
||||
"math_allowed_table",
|
||||
vec![
|
||||
("quantity", "INTEGER"),
|
||||
("price", "NUMERIC(10, 2)"),
|
||||
("total", "NUMERIC(12, 2)")
|
||||
]
|
||||
).await;
|
||||
|
||||
let request = PostTableScriptRequest {
|
||||
table_definition_id: table_id,
|
||||
target_column: "total".to_string(),
|
||||
script: r#"(* (steel_get_column "math_allowed_table" "quantity")
|
||||
(steel_get_column "math_allowed_table" "price"))"#.to_string(),
|
||||
description: "Valid mathematical operation with INTEGER and NUMERIC".to_string(),
|
||||
};
|
||||
|
||||
let result = post_table_script(&pool, request).await;
|
||||
|
||||
// Should succeed
|
||||
assert!(result.is_ok(), "Mathematical operations with INTEGER and NUMERIC should succeed");
|
||||
let response = result.unwrap();
|
||||
assert!(response.id > 0);
|
||||
}
|
||||
|
||||
454
server/tests/table_script/type_safety_comprehensive_tests.rs
Normal file
454
server/tests/table_script/type_safety_comprehensive_tests.rs
Normal 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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user