post table script is now aware of a type in the database
This commit is contained in:
@@ -10,7 +10,6 @@ use regex::Regex;
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
|
||||||
use crate::table_script::handlers::dependency_analyzer::DependencyAnalyzer;
|
use crate::table_script::handlers::dependency_analyzer::DependencyAnalyzer;
|
||||||
|
|
||||||
const SYSTEM_COLUMNS: &[&str] = &["id", "deleted", "created_at"];
|
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 PROHIBITED_TYPES: &[&str] = &["BIGINT", "DATE", "TIMESTAMPTZ"];
|
||||||
const MATH_PROHIBITED_TYPES: &[&str] = &["TEXT", "BOOLEAN"];
|
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)
|
#[derive(Debug, Clone)]
|
||||||
fn extract_math_operations_with_operands(script: &str) -> Vec<String> {
|
enum SExpr {
|
||||||
let mut math_operands = Vec::new();
|
Atom(String),
|
||||||
|
List(Vec<SExpr>),
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Extract column references from mathematical operands
|
#[derive(Debug)]
|
||||||
fn extract_column_references_from_math_operands(operands: &[String]) -> Vec<(String, String)> {
|
struct Parser {
|
||||||
let mut references = Vec::new();
|
tokens: Vec<String>,
|
||||||
|
position: usize,
|
||||||
|
}
|
||||||
|
|
||||||
for operand_str in operands {
|
impl Parser {
|
||||||
// Check for steel_get_column calls: (steel_get_column "table" "column")
|
fn new(script: &str) -> Self {
|
||||||
if let Ok(re) = Regex::new(r#"\(steel_get_column\s+"([^"]+)"\s+"([^"]+)"\)"#) {
|
let tokens = Self::tokenize(script);
|
||||||
for cap in re.captures_iter(operand_str) {
|
Self { tokens, position: 0 }
|
||||||
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 !current_token.is_empty() {
|
||||||
if let Ok(re) = Regex::new(r#"\(steel_get_column_with_index\s+"([^"]+)"\s+\d+\s+"([^"]+)"\)"#) {
|
tokens.push(current_token);
|
||||||
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()));
|
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
|
/// 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,
|
schema_id: i64,
|
||||||
script: &str,
|
script: &str,
|
||||||
) -> Result<(), Status> {
|
) -> Result<(), Status> {
|
||||||
// Extract all mathematical operations and their operands
|
// Extract column references from mathematical contexts using proper S-expression parsing
|
||||||
let math_operands = extract_math_operations_with_operands(script);
|
let column_refs = extract_math_column_references(script)
|
||||||
|
.map_err(|e| Status::invalid_argument(format!("Script parsing failed: {}", e)))?;
|
||||||
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);
|
|
||||||
|
|
||||||
if column_refs.is_empty() {
|
if column_refs.is_empty() {
|
||||||
return Ok(()); // No column references in math operations
|
return Ok(()); // No column references in math operations
|
||||||
|
|||||||
Reference in New Issue
Block a user