vibesql_executor/advanced_objects/
procedures.rs

1//! Executor for PROCEDURE objects and CALL statement (SQL:1999 Feature P001)
2
3use vibesql_ast::*;
4use vibesql_storage::Database;
5
6use crate::errors::ExecutorError;
7
8/// Execute CREATE PROCEDURE statement (SQL:1999 Feature P001)
9///
10/// Creates a stored procedure in the database catalog with optional characteristics.
11///
12/// # Parameters
13///
14/// Procedures support three parameter modes:
15/// - **IN**: Input-only parameters (read by procedure)
16/// - **OUT**: Output-only parameters (written by procedure to session variables)
17/// - **INOUT**: Both input and output (read and written)
18///
19/// # Characteristics (Phase 6)
20///
21/// - **SQL SECURITY**: DEFINER (default) or INVOKER
22/// - **COMMENT**: Documentation string
23/// - **LANGUAGE**: SQL (only supported language)
24///
25/// # Example
26///
27/// ```sql
28/// CREATE PROCEDURE calculate_stats(
29///   IN input_value INT,
30///   OUT sum_result INT,
31///   OUT count_result INT
32/// )
33///   SQL SECURITY INVOKER
34///   COMMENT 'Calculate sum and count from data_table'
35/// BEGIN
36///   SELECT SUM(value), COUNT(*)
37///   INTO sum_result, count_result
38///   FROM data_table
39///   WHERE value > input_value;
40/// END;
41/// ```
42///
43/// # Errors
44///
45/// Returns error if:
46/// - Procedure name already exists
47/// - Invalid parameter types
48/// - Body parsing fails
49pub fn execute_create_procedure(
50    stmt: &CreateProcedureStmt,
51    db: &mut Database,
52) -> Result<(), ExecutorError> {
53    use vibesql_catalog::{Procedure, ProcedureBody, ProcedureParam, ParameterMode, SqlSecurity};
54
55    // Convert AST parameters to catalog parameters
56    let catalog_params = stmt
57        .parameters
58        .iter()
59        .map(|param| {
60            let mode = match param.mode {
61                vibesql_ast::ParameterMode::In => ParameterMode::In,
62                vibesql_ast::ParameterMode::Out => ParameterMode::Out,
63                vibesql_ast::ParameterMode::InOut => ParameterMode::InOut,
64            };
65            ProcedureParam {
66                mode,
67                name: param.name.clone(),
68                data_type: param.data_type.clone(),
69            }
70        })
71        .collect();
72
73    // Convert AST body to catalog body
74    let catalog_body = match &stmt.body {
75        vibesql_ast::ProcedureBody::BeginEnd(statements) => {
76            // Store the parsed AST for execution
77            ProcedureBody::BeginEnd(statements.clone())
78        }
79        vibesql_ast::ProcedureBody::RawSql(sql) => ProcedureBody::RawSql(sql.clone()),
80    };
81
82    // Convert characteristics (Phase 6)
83    let sql_security = stmt.sql_security.as_ref().map(|sec| match sec {
84        vibesql_ast::SqlSecurity::Definer => SqlSecurity::Definer,
85        vibesql_ast::SqlSecurity::Invoker => SqlSecurity::Invoker,
86    }).unwrap_or(SqlSecurity::Definer);
87
88    let procedure = if stmt.sql_security.is_some() || stmt.comment.is_some() || stmt.language.is_some() {
89        Procedure::with_characteristics(
90            stmt.procedure_name.clone(),
91            db.catalog.get_current_schema().to_string(),
92            catalog_params,
93            catalog_body,
94            sql_security,
95            stmt.comment.clone(),
96            stmt.language.clone().unwrap_or_else(|| "SQL".to_string()),
97        )
98    } else {
99        Procedure::new(
100            stmt.procedure_name.clone(),
101            db.catalog.get_current_schema().to_string(),
102            catalog_params,
103            catalog_body,
104        )
105    };
106
107    db.catalog.create_procedure_with_characteristics(procedure)?;
108    Ok(())
109}
110
111/// Execute DROP PROCEDURE statement (SQL:1999 Feature P001)
112pub fn execute_drop_procedure(
113    stmt: &DropProcedureStmt,
114    db: &mut Database,
115) -> Result<(), ExecutorError> {
116    // Check if procedure exists
117    let procedure_exists = db.catalog.procedure_exists(&stmt.procedure_name);
118
119    // If IF EXISTS is specified and procedure doesn't exist, succeed silently
120    if stmt.if_exists && !procedure_exists {
121        return Ok(());
122    }
123
124    db.catalog.drop_procedure(&stmt.procedure_name)?;
125    Ok(())
126}
127
128/// Extract variable name from an expression for OUT/INOUT parameter binding
129///
130/// Valid patterns:
131/// - ColumnRef without table qualifier (treated as session variable): @var_name
132/// - Function call to session variable function (if we add one)
133///
134/// Returns the variable name (without @ prefix) or error if expression is not a valid variable reference.
135fn extract_variable_name(expr: &Expression) -> Result<String, ExecutorError> {
136    match expr {
137        Expression::ColumnRef { table: None, column } => {
138            // Column reference without table - treat as session variable
139            // If it starts with @, strip it; otherwise use as-is
140            let var_name = if let Some(stripped) = column.strip_prefix('@') {
141                stripped.to_string()
142            } else {
143                column.clone()
144            };
145            Ok(var_name)
146        }
147        Expression::ColumnRef { table: Some(_), column: _ } => {
148            Err(ExecutorError::Other(
149                "OUT/INOUT parameter target must be a session variable (e.g., @var_name), not a table.column reference".to_string()
150            ))
151        }
152        Expression::Literal(_) => {
153            Err(ExecutorError::Other(
154                "OUT/INOUT parameter target cannot be a literal value".to_string()
155            ))
156        }
157        _ => {
158            Err(ExecutorError::Other(
159                "OUT/INOUT parameter target must be a session variable (e.g., @var_name), not a complex expression".to_string()
160            ))
161        }
162    }
163}
164
165/// Execute CALL statement (SQL:1999 Feature P001)
166///
167/// Executes a stored procedure with parameter binding and procedural statement execution.
168///
169/// # Parameter Binding
170///
171/// - **IN parameters**: Values are evaluated and passed to the procedure
172/// - **OUT parameters**: Must be session variables (@var), initialized to NULL,
173///   procedure can assign values that are returned to caller
174/// - **INOUT parameters**: Must be session variables (@var), values are passed in
175///   and can be modified by the procedure
176///
177/// # Example
178///
179/// ```sql
180/// -- Create procedure with mixed parameter modes
181/// CREATE PROCEDURE update_counter(
182///   IN increment INT,
183///   INOUT counter INT,
184///   OUT message VARCHAR(100)
185/// )
186/// BEGIN
187///   SET counter = counter + increment;
188///   SET message = CONCAT('Counter updated to ', counter);
189/// END;
190///
191/// -- Call procedure
192/// SET @count = 10;
193/// CALL update_counter(5, @count, @msg);
194/// SELECT @count, @msg;
195/// -- Output: count=15, msg='Counter updated to 15'
196/// ```
197///
198/// # Control Flow
199///
200/// The procedure body executes sequentially until:
201/// - All statements complete (normal exit)
202/// - RETURN statement is encountered (early exit)
203/// - Error occurs (execution stops)
204///
205/// After execution, OUT and INOUT parameter values are written back to their
206/// corresponding session variables.
207///
208/// # Errors
209///
210/// Returns error if:
211/// - Procedure not found
212/// - Wrong number of arguments
213/// - OUT/INOUT parameter is not a session variable
214/// - Type mismatch in parameter binding
215/// - Execution error in procedure body
216pub fn execute_call(
217    stmt: &CallStmt,
218    db: &mut Database,
219) -> Result<(), ExecutorError> {
220    use crate::procedural::{ExecutionContext, execute_procedural_statement, ControlFlow};
221
222    // 1. Look up the procedure definition and clone what we need
223    let (parameters, body) = {
224        let procedure = db.catalog.get_procedure(&stmt.procedure_name);
225
226        match procedure {
227            Some(proc) => (proc.parameters.clone(), proc.body.clone()),
228            None => {
229                // Procedure not found - provide helpful error with suggestions
230                let schema_name = db.catalog.get_current_schema().to_string();
231                let available_procedures = db.catalog.list_procedures();
232
233                return Err(ExecutorError::ProcedureNotFound {
234                    procedure_name: stmt.procedure_name.clone(),
235                    schema_name,
236                    available_procedures,
237                });
238            }
239        }
240    };
241
242    // 2. Validate parameter count
243    if stmt.arguments.len() != parameters.len() {
244        // Build parameter signature string
245        let param_sig = parameters
246            .iter()
247            .map(|p| {
248                let mode = match p.mode {
249                    vibesql_catalog::ParameterMode::In => "IN",
250                    vibesql_catalog::ParameterMode::Out => "OUT",
251                    vibesql_catalog::ParameterMode::InOut => "INOUT",
252                };
253                format!("{} {} {:?}", mode, p.name, p.data_type)
254            })
255            .collect::<Vec<_>>()
256            .join(", ");
257
258        return Err(ExecutorError::ParameterCountMismatch {
259            routine_name: stmt.procedure_name.clone(),
260            routine_type: "Procedure".to_string(),
261            expected: parameters.len(),
262            actual: stmt.arguments.len(),
263            parameter_signature: param_sig,
264        });
265    }
266
267    // 3. Create execution context and bind parameters
268    let mut ctx = ExecutionContext::new();
269
270    for (param, arg_expr) in parameters.iter().zip(&stmt.arguments) {
271        match param.mode {
272            vibesql_catalog::ParameterMode::In => {
273                // IN parameter: Evaluate and bind the value
274                let value = crate::procedural::executor::evaluate_expression(arg_expr, db, &ctx)?;
275                ctx.set_parameter(&param.name, value);
276            }
277            vibesql_catalog::ParameterMode::Out => {
278                // OUT parameter: Initialize to NULL
279                ctx.set_parameter(&param.name, vibesql_types::SqlValue::Null);
280
281                // Extract and register target variable name
282                let var_name = extract_variable_name(arg_expr)?;
283                ctx.register_out_parameter(&param.name, var_name);
284            }
285            vibesql_catalog::ParameterMode::InOut => {
286                // INOUT parameter: Evaluate and bind input value
287                let value = crate::procedural::executor::evaluate_expression(arg_expr, db, &ctx)?;
288                ctx.set_parameter(&param.name, value);
289
290                // Extract and register target variable name
291                let var_name = extract_variable_name(arg_expr)?;
292                ctx.register_out_parameter(&param.name, var_name);
293            }
294        }
295    }
296
297    // 4. Execute procedure body
298    match &body {
299        vibesql_catalog::ProcedureBody::BeginEnd(statements) => {
300            // Execute each procedural statement sequentially
301            for stmt in statements {
302                match execute_procedural_statement(stmt, &mut ctx, db)? {
303                    ControlFlow::Continue => {
304                        // Continue to next statement
305                    }
306                    ControlFlow::Return(_) => {
307                        // RETURN in procedures exits early (functions will handle return value later)
308                        break;
309                    }
310                    ControlFlow::Leave(_) | ControlFlow::Iterate(_) => {
311                        // Control flow statements not yet supported in Phase 2
312                        return Err(ExecutorError::UnsupportedFeature(
313                            "Control flow (LEAVE/ITERATE) not yet supported in Phase 2".to_string()
314                        ));
315                    }
316                }
317            }
318            Ok(())
319        }
320        vibesql_catalog::ProcedureBody::RawSql(_) => {
321            // RawSql fallback - would require parsing
322            Err(ExecutorError::UnsupportedFeature(
323                "RawSql procedure bodies require parsing (use BEGIN/END block instead)".to_string()
324            ))
325        }
326    }?;
327
328    // 5. Return output parameter values to session variables
329    for (param_name, target_var_name) in ctx.get_out_parameters() {
330        // Get the parameter value from the context
331        let value = ctx.get_parameter(param_name)
332            .cloned()
333            .ok_or_else(|| ExecutorError::Other(format!("Parameter '{}' not found", param_name)))?;
334
335        // Store in session variable
336        db.set_session_variable(target_var_name, value);
337    }
338
339    Ok(())
340}