Validate that the CTE name appears exactly once in the recursive arm and not inside a subquery, aggregate function, or the nullable side of an outer join. Returns `Ok(())` if the reference is valid, or a typed error otherwise.
(expr: &SetExpr, cte_name: &str)
| 258 | /// |
| 259 | /// Returns `Ok(())` if the reference is valid, or a typed error otherwise. |
| 260 | fn validate_self_ref_count(expr: &SetExpr, cte_name: &str) -> Result<()> { |
| 261 | let select = match expr { |
| 262 | SetExpr::Select(s) => s, |
| 263 | // Non-SELECT arm: no self-ref needed. |
| 264 | _ => return Ok(()), |
| 265 | }; |
| 266 | |
| 267 | let mut count = 0usize; |
| 268 | |
| 269 | for from in &select.from { |
| 270 | if table_ref_matches(&from.relation, cte_name) { |
| 271 | count += 1; |
| 272 | } |
| 273 | for join in &from.joins { |
| 274 | if table_ref_matches(&join.relation, cte_name) { |
| 275 | // Reject self-ref on the nullable side of an outer join. |
| 276 | if is_nullable_join_side(&join.join_operator) { |
| 277 | return Err(SqlError::InvalidRecursiveSelfRef { |
| 278 | cte_name: cte_name.to_owned(), |
| 279 | reason: "self-reference on the nullable side of an outer join is not \ |
| 280 | permitted; use INNER JOIN or move the CTE reference to the \ |
| 281 | driving table position" |
| 282 | .into(), |
| 283 | }); |
| 284 | } |
| 285 | count += 1; |
| 286 | } |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | // Subquery self-references are not permitted. |
| 291 | if where_contains_subquery_ref(&select.selection, cte_name) { |
| 292 | return Err(SqlError::InvalidRecursiveSelfRef { |
| 293 | cte_name: cte_name.to_owned(), |
| 294 | reason: "self-reference inside a subquery is not permitted".into(), |
| 295 | }); |
| 296 | } |
| 297 | |
| 298 | if count > 1 { |
| 299 | return Err(SqlError::InvalidRecursiveSelfRef { |
| 300 | cte_name: cte_name.to_owned(), |
| 301 | reason: format!("self-reference appears {count} times; exactly one is required"), |
| 302 | }); |
| 303 | } |
| 304 | |
| 305 | // count == 0 is fine for the value-generating case (no table ref at all). |
| 306 | Ok(()) |
| 307 | } |
| 308 | |
| 309 | fn table_ref_matches(factor: &ast::TableFactor, cte_name: &str) -> bool { |
| 310 | match factor { |
no test coverage detected