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