Skip to main content

uqa_sql/binding/
cte_controls.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Static row types and validation for recursive CTE control columns.
8
9use super::{BindingContext, QueryPlan, RowSchema, SQLError, SQLParam, SchemaScope};
10use crate::ast::ColumnType;
11use crate::routines::RoutineResolution;
12
13pub fn hide_recursive_generated_schema(schema: &RowSchema, visible: usize) -> RowSchema {
14    let visible = visible.min(schema.len());
15    let base = RowSchema::with_types(
16        schema.columns()[..visible].to_vec(),
17        schema.column_types()[..visible].to_vec(),
18    )
19    .with_record_fields_from(schema);
20    let generated = schema
21        .columns()
22        .iter()
23        .enumerate()
24        .skip(visible)
25        .map(|(position, column)| {
26            (
27                crate::ColumnIdentity::unqualified(column),
28                schema.column_type(position).cloned(),
29            )
30        })
31        .collect::<Vec<_>>();
32    RowSchema::with_typed_virtual_identities(&base, &generated)
33}
34
35pub fn extend_cte_generated_schema(
36    routines: &dyn RoutineResolution,
37    cte: &crate::plan::CtePlan,
38    schema: RowSchema,
39    params: &[SQLParam],
40) -> Result<RowSchema, SQLError> {
41    extend_cte_generated_schema_mode(routines, cte, schema, params, true)
42}
43
44fn extend_cte_generated_schema_mode(
45    routines: &dyn RoutineResolution,
46    cte: &crate::plan::CtePlan,
47    schema: RowSchema,
48    params: &[SQLParam],
49    reject_output_conflicts: bool,
50) -> Result<RowSchema, SQLError> {
51    if cte.search.is_none() && cte.cycle.is_none() {
52        return Ok(schema);
53    }
54    let mut columns = schema.columns().to_vec();
55    let mut types = schema.column_types().to_vec();
56    let base_columns = columns.clone();
57    let require_columns = |requested: &[String], kind: &str| -> Result<(), SQLError> {
58        let mut seen = std::collections::BTreeSet::new();
59        for column in requested {
60            if !seen.insert(column) {
61                return Err(SQLError::Routine {
62                    sqlstate: "42701".into(),
63                    message: format!("{kind} column \"{column}\" specified more than once"),
64                });
65            }
66            if !base_columns.iter().any(|candidate| candidate == column) {
67                return Err(SQLError::Routine {
68                    sqlstate: "42601".into(),
69                    message: format!("{kind} column \"{column}\" not in WITH query column list"),
70                });
71            }
72        }
73        Ok(())
74    };
75    let reject_conflict = |name: &str, kind: &str| -> Result<(), SQLError> {
76        if reject_output_conflicts && base_columns.iter().any(|column| column == name) {
77            return Err(SQLError::Routine {
78                sqlstate: "42601".into(),
79                message: format!(
80                    "{kind} column name \"{name}\" already used in WITH query column list"
81                ),
82            });
83        }
84        Ok(())
85    };
86    if let Some(search) = &cte.search {
87        require_columns(&search.columns, "search")?;
88        reject_conflict(&search.sequence_column, "search sequence")?;
89        columns.push(search.sequence_column.clone());
90        types.push(Some(if search.breadth_first {
91            ColumnType::Record
92        } else {
93            ColumnType::Array(Box::new(ColumnType::Record))
94        }));
95    }
96    if let Some(cycle) = &cte.cycle {
97        require_columns(&cycle.columns, "cycle")?;
98        if cycle.mark_column == cycle.path_column {
99            return Err(SQLError::Routine {
100                sqlstate: "42601".into(),
101                message: "cycle mark column name and cycle path column name are the same".into(),
102            });
103        }
104        reject_conflict(&cycle.mark_column, "cycle mark")?;
105        reject_conflict(&cycle.path_column, "cycle path")?;
106        let empty = RowSchema::default();
107        // `transformCycleClause` selects the mark's type as `select_common_type` does, an `unknown` literal taking the other value's type.
108        let mark_type = match (
109            crate::type_resolution::common_context_expression_type(
110                &cycle.mark_value,
111                &empty,
112                params,
113                Some(routines),
114            )?,
115            crate::type_resolution::common_context_expression_type(
116                &cycle.mark_default,
117                &empty,
118                params,
119                Some(routines),
120            )?,
121        ) {
122            (Some(left), Some(right)) => Some(crate::type_resolution::common_type_in(
123                crate::type_resolution::CommonTypeContext::Cycle,
124                &left,
125                &right,
126            )?),
127            (left @ Some(_), None) | (None, left @ Some(_)) => left,
128            (None, None) => None,
129        };
130        if let Some(mark_type) = mark_type.as_ref() {
131            crate::equality_operand_type(mark_type, mark_type)?;
132        }
133        for column in &cycle.columns {
134            if let Some(column_type) = base_columns
135                .iter()
136                .position(|candidate| candidate == column)
137                .and_then(|position| schema.column_type(position))
138            {
139                crate::equality_operand_type(column_type, column_type)?;
140            }
141        }
142        columns.push(cycle.mark_column.clone());
143        types.push(mark_type);
144        columns.push(cycle.path_column.clone());
145        types.push(Some(ColumnType::Array(Box::new(ColumnType::Record))));
146    }
147    Ok(RowSchema::with_types(columns, types).with_record_fields_from(&schema))
148}
149
150pub fn extend_recursive_cte_binding_schema(
151    routines: &dyn RoutineResolution,
152    cte: &crate::plan::CtePlan,
153    schema: RowSchema,
154    params: &[SQLParam],
155) -> Result<RowSchema, SQLError> {
156    let extended = extend_cte_generated_schema_mode(routines, cte, schema.clone(), params, false)?;
157    let generated = extended
158        .columns()
159        .iter()
160        .enumerate()
161        .skip(schema.len())
162        .map(|(position, column)| {
163            (
164                crate::ColumnIdentity::unqualified(column),
165                extended.column_type(position).cloned(),
166            )
167        })
168        .collect::<Vec<_>>();
169    Ok(RowSchema::with_typed_conflicting_virtual_identities(
170        &schema, &generated,
171    ))
172}
173
174pub fn analyze_recursive_control_step(
175    routines: &dyn RoutineResolution,
176    cte: &crate::plan::CtePlan,
177    step: &QueryPlan,
178    base_schema: RowSchema,
179    params: &[SQLParam],
180    ctes: &BindingContext,
181) -> Result<(), SQLError> {
182    let provisional = extend_recursive_cte_binding_schema(routines, cte, base_schema, params)?;
183    let mut scope = SchemaScope::for_analysis(ctes)?;
184    scope.ctes.insert(cte.name.clone(), provisional);
185    scope.bind_query(routines, step, params, None).map(|_| ())
186}