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