Skip to main content

uqa_sql/schema/
join_output.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Static layouts shared by qualified joins and physical output shaping.
8
9use super::SchemaLayoutError;
10use crate::{ColumnIdentity, ColumnType, RowSchema};
11
12/// Source of one visible or hidden join-output identity.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum JoinOutputSource {
15    /// Reuse an existing logical input position without copying its value.
16    Input(usize),
17    /// Apply an implicit binder-selected coercion to one input position.
18    Cast { input: usize, ty: ColumnType },
19    /// SQL `COALESCE(left::type, right::type)` over two logical input
20    /// positions. This is required only for a merged column of `FULL JOIN`.
21    Coalesce {
22        left: usize,
23        right: usize,
24        ty: ColumnType,
25    },
26}
27
28pub fn compile_layout(
29    input: &RowSchema,
30    columns: &[(String, ColumnIdentity, JoinOutputSource)],
31    aliases: &[(ColumnIdentity, JoinOutputSource)],
32) -> Result<(RowSchema, Vec<JoinOutputSource>), SchemaLayoutError> {
33    let input_width = input.len();
34    let mut computed = Vec::<JoinOutputSource>::new();
35    for source in columns
36        .iter()
37        .map(|(_, _, source)| source)
38        .chain(aliases.iter().map(|(_, source)| source))
39    {
40        match source {
41            JoinOutputSource::Input(position) if *position >= input_width => {
42                return Err(SchemaLayoutError(format!(
43                    "join output input position {position} is outside width {input_width}"
44                )));
45            }
46            JoinOutputSource::Cast { input, .. } if *input >= input_width => {
47                return Err(SchemaLayoutError(format!(
48                    "join output cast position {input} is outside width {input_width}"
49                )));
50            }
51            JoinOutputSource::Coalesce { left, right, .. }
52                if *left >= input_width || *right >= input_width =>
53            {
54                return Err(SchemaLayoutError(format!(
55                    "join output coalesce positions ({left}, {right}) are outside width {input_width}"
56                )));
57            }
58            source @ (JoinOutputSource::Cast { .. } | JoinOutputSource::Coalesce { .. }) => {
59                if !computed.contains(source) {
60                    computed.push(source.clone());
61                }
62            }
63            JoinOutputSource::Input(_) => {}
64        }
65    }
66
67    let computed_types = computed.iter().map(source_type).collect::<Vec<_>>();
68    let intermediate = RowSchema::append_hidden_typed(input, &computed_types);
69    let source_position = |source: &JoinOutputSource| -> usize {
70        match source {
71            JoinOutputSource::Input(position) => input
72                .physical_slot(*position)
73                .expect("validated join output input position has a physical slot"),
74            JoinOutputSource::Cast { .. } | JoinOutputSource::Coalesce { .. } => {
75                let index = computed
76                    .iter()
77                    .position(|candidate| candidate == source)
78                    .expect("computed join output source was registered");
79                input.physical_width() + index
80            }
81        }
82    };
83    let columns = columns
84        .iter()
85        .map(|(name, identity, source)| {
86            let ty = match source {
87                JoinOutputSource::Input(position) => intermediate.column_type(*position).cloned(),
88                JoinOutputSource::Cast { .. } | JoinOutputSource::Coalesce { .. } => {
89                    source_type(source)
90                }
91            };
92            (name.clone(), identity.clone(), source_position(source), ty)
93        })
94        .collect::<Vec<_>>();
95    let aliases = aliases
96        .iter()
97        .map(|(name, source)| {
98            let ty = match source {
99                JoinOutputSource::Input(position) => input.column_type(*position).cloned(),
100                JoinOutputSource::Cast { .. } | JoinOutputSource::Coalesce { .. } => {
101                    source_type(source)
102                }
103            };
104            (name.clone(), source_position(source), ty)
105        })
106        .collect::<Vec<_>>();
107    let schema = RowSchema::remap_typed_physical_identities(&intermediate, &columns, &aliases);
108    Ok((schema, computed))
109}
110
111pub fn source_type(source: &JoinOutputSource) -> Option<ColumnType> {
112    match source {
113        JoinOutputSource::Input(_) => None,
114        JoinOutputSource::Cast { ty, .. } | JoinOutputSource::Coalesce { ty, .. } => {
115            Some(ty.clone())
116        }
117    }
118}