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