Compile FOR loop by unrolling into a nested CASE WHEN chain. `FOR i IN 1..3 LOOP RETURN i * 2; END LOOP;` compiles to: `CASE WHEN true THEN (1) * 2 END` Since function FOR loops must contain a RETURN (otherwise value is undefined), and the first iteration's RETURN determines the value, unrolling just evaluates the body with each iteration's variable binding. For FOR loops with conditional RETUR
(
var: &str,
start: &SqlExpr,
end: &SqlExpr,
reverse: bool,
body: &[Statement],
ctx: &mut CompileContext,
)
| 203 | /// `FOR i IN 1..3 LOOP IF arr[i] > threshold THEN RETURN i; END IF; END LOOP;` |
| 204 | /// → unrolled as nested CASE WHEN for each iteration. |
| 205 | fn compile_for( |
| 206 | var: &str, |
| 207 | start: &SqlExpr, |
| 208 | end: &SqlExpr, |
| 209 | reverse: bool, |
| 210 | body: &[Statement], |
| 211 | ctx: &mut CompileContext, |
| 212 | ) -> Result<String, ProceduralError> { |
| 213 | let start_val = start.sql.trim().parse::<i64>().map_err(|_| { |
| 214 | ProceduralError::compile("FOR loop start must be integer literal in function bodies") |
| 215 | })?; |
| 216 | let end_val = end.sql.trim().parse::<i64>().map_err(|_| { |
| 217 | ProceduralError::compile("FOR loop end must be integer literal in function bodies") |
| 218 | })?; |
| 219 | |
| 220 | let iterations: Vec<i64> = if reverse { |
| 221 | (end_val..=start_val).rev().collect() |
| 222 | } else { |
| 223 | (start_val..=end_val).collect() |
| 224 | }; |
| 225 | |
| 226 | if iterations.len() as u64 > MAX_LOOP_UNROLL { |
| 227 | return Err(ProceduralError::compile(format!( |
| 228 | "FOR loop has {} iterations, exceeds unrolling threshold ({MAX_LOOP_UNROLL})", |
| 229 | iterations.len() |
| 230 | ))); |
| 231 | } |
| 232 | |
| 233 | if iterations.is_empty() { |
| 234 | return Ok("NULL".into()); |
| 235 | } |
| 236 | |
| 237 | // Unroll: for each iteration, bind the variable and compile the body. |
| 238 | // Use nested CASE WHEN to try each iteration in order. |
| 239 | // The first RETURN hit determines the value. |
| 240 | let mut case_parts = Vec::new(); |
| 241 | for val in &iterations { |
| 242 | let mut iter_ctx = ctx.clone_vars(); |
| 243 | iter_ctx.variables.insert(var.to_string(), val.to_string()); |
| 244 | let body_sql = compile_statements(body, &mut iter_ctx)?; |
| 245 | // Each iteration becomes a WHEN clause. If the body is unconditional |
| 246 | // (always returns), it's `WHEN true THEN <body>`. If conditional, |
| 247 | // the inner CASE WHEN handles it. |
| 248 | if body_sql != "NULL" { |
| 249 | case_parts.push(format!("WHEN true THEN {body_sql}")); |
| 250 | // For unconditional returns, first iteration wins. |
| 251 | if !body.iter().any(|s| matches!(s, Statement::If { .. })) { |
| 252 | break; |
| 253 | } |
| 254 | } |
| 255 | } |
| 256 | |
| 257 | if case_parts.is_empty() { |
| 258 | return Ok("NULL".into()); |
| 259 | } |
| 260 | |
| 261 | case_parts.push("ELSE NULL".into()); |
| 262 | Ok(format!("CASE {} END", case_parts.join(" "))) |
no test coverage detected