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(¶m.name, value);
276 }
277 vibesql_catalog::ParameterMode::Out => {
278 // OUT parameter: Initialize to NULL
279 ctx.set_parameter(¶m.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(¶m.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(¶m.name, value);
289
290 // Extract and register target variable name
291 let var_name = extract_variable_name(arg_expr)?;
292 ctx.register_out_parameter(¶m.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}