post table script is now aware of a type in the database

This commit is contained in:
filipriec
2025-07-17 21:16:49 +02:00
parent fe246b1fe6
commit 810ef5fc10

View File

@@ -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<String> {
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<SExpr>),
}
/// 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<String>,
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<String> {
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<Vec<SExpr>, 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<SExpr, String> {
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<String, String> {
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<Vec<(String, String)>, 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" {