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
136/// reference.
137fn extract_variable_name(expr: &Expression) -> Result<String, ExecutorError> {
138 match expr {
139 Expression::ColumnRef(col_id) if col_id.schema_canonical().is_none() && col_id.table_canonical().is_none() => {
140 // Column reference without table - treat as session variable
141 // If it starts with @, strip it; otherwise use as-is
142 let column = col_id.column_canonical();
143 let var_name = if let Some(stripped) = column.strip_prefix('@') {
144 stripped.to_string()
145 } else {
146 column.to_string()
147 };
148 Ok(var_name)
149 }
150 Expression::ColumnRef(col_id) if col_id.schema_canonical().is_none() && col_id.table_canonical().is_some() => {
151 Err(ExecutorError::Other(
152 "OUT/INOUT parameter target must be a session variable (e.g., @var_name), not a table.column reference".to_string()
153 ))
154 }
155 Expression::Literal(_) => {
156 Err(ExecutorError::Other(
157 "OUT/INOUT parameter target cannot be a literal value".to_string()
158 ))
159 }
160 _ => {
161 Err(ExecutorError::Other(
162 "OUT/INOUT parameter target must be a session variable (e.g., @var_name), not a complex expression".to_string()
163 ))
164 }
165 }
166}
167
168/// Execute CALL statement (SQL:1999 Feature P001)
169///
170/// Executes a stored procedure with parameter binding and procedural statement execution.
171///
172/// # Parameter Binding
173///
174/// - **IN parameters**: Values are evaluated and passed to the procedure
175/// - **OUT parameters**: Must be session variables (@var), initialized to NULL, procedure can
176/// assign values that are returned to caller
177/// - **INOUT parameters**: Must be session variables (@var), values are passed in and can be
178/// modified by the procedure
179///
180/// # Example
181///
182/// ```sql
183/// -- Create procedure with mixed parameter modes
184/// CREATE PROCEDURE update_counter(
185/// IN increment INT,
186/// INOUT counter INT,
187/// OUT message VARCHAR(100)
188/// )
189/// BEGIN
190/// SET counter = counter + increment;
191/// SET message = CONCAT('Counter updated to ', counter);
192/// END;
193///
194/// -- Call procedure
195/// SET @count = 10;
196/// CALL update_counter(5, @count, @msg);
197/// SELECT @count, @msg;
198/// -- Output: count=15, msg='Counter updated to 15'
199/// ```
200///
201/// # Control Flow
202///
203/// The procedure body executes sequentially until:
204/// - All statements complete (normal exit)
205/// - RETURN statement is encountered (early exit)
206/// - Error occurs (execution stops)
207///
208/// After execution, OUT and INOUT parameter values are written back to their
209/// corresponding session variables.
210///
211/// # Errors
212///
213/// Returns error if:
214/// - Procedure not found
215/// - Wrong number of arguments
216/// - OUT/INOUT parameter is not a session variable
217/// - Type mismatch in parameter binding
218/// - Execution error in procedure body
219pub fn execute_call(stmt: &CallStmt, db: &mut Database) -> Result<(), ExecutorError> {
220 use crate::procedural::{execute_procedural_statement, ControlFlow, ExecutionContext};
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
308 // later)
309 break;
310 }
311 ControlFlow::Leave(_) | ControlFlow::Iterate(_) => {
312 // Control flow statements not yet supported in Phase 2
313 return Err(ExecutorError::UnsupportedFeature(
314 "Control flow (LEAVE/ITERATE) not yet supported in Phase 2".to_string(),
315 ));
316 }
317 }
318 }
319 Ok(())
320 }
321 vibesql_catalog::ProcedureBody::RawSql(_) => {
322 // RawSql fallback - would require parsing
323 Err(ExecutorError::UnsupportedFeature(
324 "RawSql procedure bodies require parsing (use BEGIN/END block instead)".to_string(),
325 ))
326 }
327 }?;
328
329 // 5. Return output parameter values to session variables
330 for (param_name, target_var_name) in ctx.get_out_parameters() {
331 // Get the parameter value from the context
332 let value = ctx
333 .get_parameter(param_name)
334 .cloned()
335 .ok_or_else(|| ExecutorError::Other(format!("Parameter '{}' not found", param_name)))?;
336
337 // Store in session variable
338 db.set_session_variable(target_var_name, value);
339 }
340
341 Ok(())
342}