From 810ef5fc10c508f3a00f1537bd1bc981ed153da3 Mon Sep 17 00:00:00 2001 From: filipriec Date: Thu, 17 Jul 2025 21:16:49 +0200 Subject: [PATCH] post table script is now aware of a type in the database --- .../handlers/post_table_script.rs | 274 +++++++++++++----- 1 file changed, 204 insertions(+), 70 deletions(-) diff --git a/server/src/table_script/handlers/post_table_script.rs b/server/src/table_script/handlers/post_table_script.rs index 22558aa..5c45b31 100644 --- a/server/src/table_script/handlers/post_table_script.rs +++ b/server/src/table_script/handlers/post_table_script.rs @@ -10,7 +10,6 @@ use regex::Regex; use std::collections::HashSet; use std::collections::HashMap; - use crate::table_script::handlers::dependency_analyzer::DependencyAnalyzer; const SYSTEM_COLUMNS: &[&str] = &["id", "deleted", "created_at"]; @@ -19,78 +18,219 @@ const SYSTEM_COLUMNS: &[&str] = &["id", "deleted", "created_at"]; const PROHIBITED_TYPES: &[&str] = &["BIGINT", "DATE", "TIMESTAMPTZ"]; const MATH_PROHIBITED_TYPES: &[&str] = &["TEXT", "BOOLEAN"]; +// Math operations that Steel Decimal will transform +const MATH_OPERATIONS: &[&str] = &[ + "+", "-", "*", "/", "^", "**", "pow", "sqrt", + ">", "<", "=", ">=", "<=", "min", "max", "abs", + "round", "ln", "log", "log10", "exp", "sin", "cos", "tan" +]; -/// Extract mathematical expressions from the original script (before steel_decimal transformation) -fn extract_math_operations_with_operands(script: &str) -> Vec { - let mut math_operands = Vec::new(); - - // Define math operation patterns that steel_decimal will transform - let math_patterns = [ - r"\(\s*\+\s+([^)]+)\)", // (+ operands) - r"\(\s*-\s+([^)]+)\)", // (- operands) - r"\(\s*\*\s+([^)]+)\)", // (* operands) - r"\(\s*/\s+([^)]+)\)", // (/ operands) - r"\(\s*\^\s+([^)]+)\)", // (^ operands) - r"\(\s*\*\*\s+([^)]+)\)", // (** operands) - r"\(\s*pow\s+([^)]+)\)", // (pow operands) - r"\(\s*sqrt\s+([^)]+)\)", // (sqrt operands) - r"\(\s*>\s+([^)]+)\)", // (> operands) - r"\(\s*<\s+([^)]+)\)", // (< operands) - r"\(\s*=\s+([^)]+)\)", // (= operands) - r"\(\s*>=\s+([^)]+)\)", // (>= operands) - r"\(\s*<=\s+([^)]+)\)", // (<= operands) - r"\(\s*min\s+([^)]+)\)", // (min operands) - r"\(\s*max\s+([^)]+)\)", // (max operands) - r"\(\s*abs\s+([^)]+)\)", // (abs operands) - r"\(\s*round\s+([^)]+)\)", // (round operands) - r"\(\s*ln\s+([^)]+)\)", // (ln operands) - r"\(\s*log\s+([^)]+)\)", // (log operands) - r"\(\s*log10\s+([^)]+)\)", // (log10 operands) - r"\(\s*exp\s+([^)]+)\)", // (exp operands) - r"\(\s*sin\s+([^)]+)\)", // (sin operands) - r"\(\s*cos\s+([^)]+)\)", // (cos operands) - r"\(\s*tan\s+([^)]+)\)", // (tan operands) - ]; - - for pattern in &math_patterns { - if let Ok(re) = Regex::new(pattern) { - for cap in re.captures_iter(script) { - if let Some(operands_str) = cap.get(1) { - // Add all operands from this math operation - math_operands.push(operands_str.as_str().to_string()); - } - } - } - } - - math_operands +#[derive(Debug, Clone)] +enum SExpr { + Atom(String), + List(Vec), } -/// Extract column references from mathematical operands -fn extract_column_references_from_math_operands(operands: &[String]) -> Vec<(String, String)> { - let mut references = Vec::new(); +#[derive(Debug)] +struct Parser { + tokens: Vec, + position: usize, +} + +impl Parser { + fn new(script: &str) -> Self { + let tokens = Self::tokenize(script); + Self { tokens, position: 0 } + } - for operand_str in operands { - // Check for steel_get_column calls: (steel_get_column "table" "column") - if let Ok(re) = Regex::new(r#"\(steel_get_column\s+"([^"]+)"\s+"([^"]+)"\)"#) { - for cap in re.captures_iter(operand_str) { - if let (Some(table), Some(column)) = (cap.get(1), cap.get(2)) { - references.push((table.as_str().to_string(), column.as_str().to_string())); + fn tokenize(script: &str) -> Vec { + let mut tokens = Vec::new(); + let mut current_token = String::new(); + let mut in_string = false; + let mut escape_next = false; + + for ch in script.chars() { + if escape_next { + current_token.push(ch); + escape_next = false; + continue; + } + + match ch { + '\\' if in_string => { + escape_next = true; + current_token.push(ch); + } + '"' => { + current_token.push(ch); + if in_string { + // End of string - push the complete string token + tokens.push(current_token.clone()); + current_token.clear(); + } + in_string = !in_string; + } + '(' | ')' if !in_string => { + if !current_token.is_empty() { + tokens.push(current_token.clone()); + current_token.clear(); + } + tokens.push(ch.to_string()); + } + ch if ch.is_whitespace() && !in_string => { + if !current_token.is_empty() { + tokens.push(current_token.clone()); + current_token.clear(); + } + } + _ => { + current_token.push(ch); } } } - // Check for steel_get_column_with_index calls - if let Ok(re) = Regex::new(r#"\(steel_get_column_with_index\s+"([^"]+)"\s+\d+\s+"([^"]+)"\)"#) { - for cap in re.captures_iter(operand_str) { - if let (Some(table), Some(column)) = (cap.get(1), cap.get(2)) { - references.push((table.as_str().to_string(), column.as_str().to_string())); + if !current_token.is_empty() { + tokens.push(current_token); + } + + tokens + } + + fn parse(&mut self) -> Result, String> { + let mut expressions = Vec::new(); + + while self.position < self.tokens.len() { + expressions.push(self.parse_expr()?); + } + + Ok(expressions) + } + + fn parse_expr(&mut self) -> Result { + if self.position >= self.tokens.len() { + return Err("Unexpected end of input".to_string()); + } + + let token = &self.tokens[self.position]; + + if token == "(" { + self.position += 1; // consume '(' + let mut elements = Vec::new(); + + while self.position < self.tokens.len() && self.tokens[self.position] != ")" { + elements.push(self.parse_expr()?); + } + + if self.position >= self.tokens.len() { + return Err("Missing closing parenthesis".to_string()); + } + + self.position += 1; // consume ')' + Ok(SExpr::List(elements)) + } else { + self.position += 1; + Ok(SExpr::Atom(token.clone())) + } + } +} + +#[derive(Debug)] +struct MathValidator { + column_references: Vec<(String, String)>, // (table, column) pairs found in math contexts +} + +impl MathValidator { + fn new() -> Self { + Self { + column_references: Vec::new(), + } + } + + fn validate_expressions(&mut self, expressions: &[SExpr]) -> Result<(), String> { + for expr in expressions { + self.check_expression(expr, false)?; + } + Ok(()) + } + + fn check_expression(&mut self, expr: &SExpr, in_math_context: bool) -> Result<(), String> { + match expr { + SExpr::Atom(_) => Ok(()), + SExpr::List(elements) => { + if elements.is_empty() { + return Ok(()); } + + // Check if this is a math operation + let is_math = if let SExpr::Atom(op) = &elements[0] { + MATH_OPERATIONS.contains(&op.as_str()) + } else { + false + }; + + // Check if this is a column access function + if let SExpr::Atom(func) = &elements[0] { + if func == "steel_get_column" && in_math_context { + self.extract_column_reference_from_steel_get_column(elements)?; + } else if func == "steel_get_column_with_index" && in_math_context { + self.extract_column_reference_from_steel_get_column_with_index(elements)?; + } + } + + // Recursively check all elements, marking math context appropriately + for element in &elements[1..] { // Skip the operator/function name + self.check_expression(element, in_math_context || is_math)?; + } + + Ok(()) } } } - references + fn extract_column_reference_from_steel_get_column(&mut self, elements: &[SExpr]) -> Result<(), String> { + // (steel_get_column "table" "column") + if elements.len() >= 3 { + if let (SExpr::Atom(table), SExpr::Atom(column)) = (&elements[1], &elements[2]) { + let table_name = self.unquote_string(table)?; + let column_name = self.unquote_string(column)?; + self.column_references.push((table_name, column_name)); + } + } + Ok(()) + } + + fn extract_column_reference_from_steel_get_column_with_index(&mut self, elements: &[SExpr]) -> Result<(), String> { + // (steel_get_column_with_index "table" index "column") + if elements.len() >= 4 { + if let (SExpr::Atom(table), SExpr::Atom(column)) = (&elements[1], &elements[3]) { + let table_name = self.unquote_string(table)?; + let column_name = self.unquote_string(column)?; + self.column_references.push((table_name, column_name)); + } + } + Ok(()) + } + + fn unquote_string(&self, s: &str) -> Result { + if s.starts_with('"') && s.ends_with('"') && s.len() >= 2 { + Ok(s[1..s.len()-1].to_string()) + } else { + Err(format!("Expected quoted string, got: {}", s)) + } + } +} + +/// Parse Steel script and extract column references used in mathematical contexts +fn extract_math_column_references(script: &str) -> Result, String> { + let mut parser = Parser::new(script); + let expressions = parser.parse() + .map_err(|e| format!("Parse error: {}", e))?; + + let mut validator = MathValidator::new(); + validator.validate_expressions(&expressions) + .map_err(|e| format!("Validation error: {}", e))?; + + Ok(validator.column_references) } /// Validate that mathematical operations don't use TEXT or BOOLEAN columns @@ -99,15 +239,9 @@ async fn validate_math_operations_column_types( schema_id: i64, script: &str, ) -> Result<(), Status> { - // Extract all mathematical operations and their operands - let math_operands = extract_math_operations_with_operands(script); - - if math_operands.is_empty() { - return Ok(()); // No math operations to validate - } - - // Extract column references from math operands - let column_refs = extract_column_references_from_math_operands(&math_operands); + // Extract column references from mathematical contexts using proper S-expression parsing + let column_refs = extract_math_column_references(script) + .map_err(|e| Status::invalid_argument(format!("Script parsing failed: {}", e)))?; if column_refs.is_empty() { return Ok(()); // No column references in math operations @@ -364,7 +498,7 @@ fn validate_referenced_column_type(table_name: &str, column_name: &str, table_co PROHIBITED_TYPES.join(", ") )); } - + // Log info for boolean columns let normalized_type = normalize_data_type(column_type); if normalized_type == "BOOLEAN" || normalized_type == "BOOL" {