Skip to main content

uqa_execution/
scope_overlay.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Positional overlay for a correlated outer query scope.
8
9use crate::{Batch, ExecResult, OwnedPhysicalRow, PhysicalOperator, PhysicalRow, RowSchema};
10
11/// Attach one outer row as hidden lookup state while keeping only the current relation's columns visible. The outer value fragment is shared by every row in a child batch, so correlated evaluation does not rebuild a merged map for each inner row.
12pub struct ScopeOverlay<'a> {
13    child: Box<dyn PhysicalOperator + 'a>,
14    schema: RowSchema,
15    outer: PhysicalRow,
16}
17
18impl<'a> ScopeOverlay<'a> {
19    /// Attach an already-positional outer row without materializing names or duplicating values for lookup aliases.
20    pub fn new(child: Box<dyn PhysicalOperator + 'a>, outer: OwnedPhysicalRow) -> Self {
21        let schema = RowSchema::with_outer_schema(child.row_schema(), &outer.schema);
22        Self {
23            child,
24            schema,
25            outer: outer.row,
26        }
27    }
28}
29
30impl PhysicalOperator for ScopeOverlay<'_> {
31    fn row_schema(&self) -> &RowSchema {
32        &self.schema
33    }
34
35    fn estimated_cardinality(&self) -> Option<u64> {
36        self.child.estimated_cardinality()
37    }
38
39    fn open(&mut self) -> ExecResult<()> {
40        self.child.open()
41    }
42
43    fn next(&mut self) -> ExecResult<Option<Batch>> {
44        let Some(batch) = self.child.next()? else {
45            return Ok(None);
46        };
47        let rows = batch
48            .rows
49            .into_iter()
50            .map(|row| PhysicalRow::concat_left_owned(row, &self.outer))
51            .collect();
52        Ok(Some(Batch::from_physical_rows(self.schema.clone(), rows)))
53    }
54
55    fn close(&mut self) -> ExecResult<()> {
56        self.child.close()
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use uqa_core::Value;
63    use uqa_sql::expr::RowLookup;
64
65    use super::*;
66    use crate::{ColumnIdentity, ColumnSelection, TableScan};
67
68    fn one_row(qualifier: &str, columns: &[&str], values: &[i64]) -> Box<dyn PhysicalOperator> {
69        let row = columns
70            .iter()
71            .zip(values)
72            .map(|(column, value)| ((*column).to_string(), Value::Int(*value)))
73            .collect();
74        let scan: Box<dyn PhysicalOperator> = Box::new(TableScan::from_rows(
75            columns.iter().map(|column| (*column).to_string()).collect(),
76            vec![row],
77        ));
78        let mapping = columns
79            .iter()
80            .enumerate()
81            .map(|(position, column)| {
82                (
83                    (*column).to_string(),
84                    ColumnIdentity::qualified(qualifier, *column),
85                    position,
86                )
87            })
88            .collect();
89        Box::new(ColumnSelection::with_identities(scan, mapping))
90    }
91
92    #[test]
93    fn current_scope_shadows_outer_names_without_exposing_outer_star_columns() {
94        let child = one_row("inner", &["id", "value"], &[1, 2]);
95        let outer_schema = RowSchema::with_qualified_types(
96            "outer",
97            vec!["id".into(), "note".into()],
98            vec![None, None],
99        );
100        let outer = OwnedPhysicalRow::new(
101            outer_schema,
102            PhysicalRow::from_values(vec![Value::Int(9), Value::Int(10)]),
103        );
104        let mut overlay = ScopeOverlay::new(child, outer);
105        overlay.open().unwrap();
106        let batch = overlay.next().unwrap().unwrap();
107        assert_eq!(batch.schema.columns(), ["id", "value"]);
108        let view = batch.schema.view(&batch.rows[0]);
109        assert_eq!(view.column("id"), Some(&Value::Int(1)));
110        assert_eq!(view.column("note"), Some(&Value::Int(10)));
111        assert_eq!(view.qualified_column("outer", "id"), Some(&Value::Int(9)));
112    }
113
114    #[test]
115    fn ambiguous_current_and_outer_names_are_not_resolved_arbitrarily() {
116        let row = [
117            ("left slot".into(), Value::Int(1)),
118            ("right slot".into(), Value::Int(2)),
119        ]
120        .into_iter()
121        .collect();
122        let scan: Box<dyn PhysicalOperator> = Box::new(TableScan::from_rows(
123            vec!["left slot".into(), "right slot".into()],
124            vec![row],
125        ));
126        let child: Box<dyn PhysicalOperator> = Box::new(ColumnSelection::with_identities(
127            scan,
128            vec![
129                ("id".into(), ColumnIdentity::qualified("left", "id"), 0),
130                ("id".into(), ColumnIdentity::qualified("right", "id"), 1),
131            ],
132        ));
133        let outer = OwnedPhysicalRow::new(
134            RowSchema::new(vec!["id".into()]),
135            PhysicalRow::from_values(vec![Value::Int(9)]),
136        );
137        let mut overlay = ScopeOverlay::new(child, outer);
138        overlay.open().unwrap();
139        let batch = overlay.next().unwrap().unwrap();
140        assert!(batch.schema.view(&batch.rows[0]).column_is_ambiguous("id"));
141
142        let child = one_row("inner", &["value"], &[1]);
143        let outer_schema = RowSchema::with_identities(
144            vec!["left slot".into(), "right slot".into()],
145            vec![
146                ColumnIdentity::qualified("left", "id"),
147                ColumnIdentity::qualified("right", "id"),
148            ],
149            vec![None, None],
150        );
151        let outer = OwnedPhysicalRow::new(
152            outer_schema,
153            PhysicalRow::from_values(vec![Value::Int(9), Value::Int(10)]),
154        );
155        let mut overlay = ScopeOverlay::new(child, outer);
156        overlay.open().unwrap();
157        let batch = overlay.next().unwrap().unwrap();
158        assert!(batch.schema.view(&batch.rows[0]).column_is_ambiguous("id"));
159    }
160
161    #[test]
162    fn typed_outer_scope_preserves_declared_sql_identity() {
163        let child = one_row("inner", &["id"], &[1]);
164        let outer_schema = RowSchema::with_qualified_types(
165            "outer",
166            vec!["value".into()],
167            vec![Some(uqa_sql::ast::ColumnType::SmallInteger)],
168        );
169        let outer =
170            OwnedPhysicalRow::new(outer_schema, PhysicalRow::from_values(vec![Value::Int(7)]));
171        let overlay = ScopeOverlay::new(child, outer);
172        assert_eq!(
173            overlay.row_schema().qualified_type("outer", "value"),
174            Some(&uqa_sql::ast::ColumnType::SmallInteger)
175        );
176        assert_eq!(
177            overlay.row_schema().type_of("value"),
178            Some(&uqa_sql::ast::ColumnType::SmallInteger)
179        );
180    }
181
182    #[test]
183    fn outer_scope_preserves_hidden_structured_aliases_without_extra_value_slots() {
184        let child = one_row("inner", &["id"], &[1]);
185        let outer_schema = RowSchema::with_identity_aliases(
186            &RowSchema::new(vec!["payload".into()]),
187            &[(ColumnIdentity::qualified("outer.dot", "column.dot"), 0)],
188        );
189        let outer =
190            OwnedPhysicalRow::new(outer_schema, PhysicalRow::from_values(vec![Value::Int(9)]));
191        let mut overlay = ScopeOverlay::new(child, outer);
192        assert_eq!(overlay.row_schema().physical_width(), 2);
193        overlay.open().unwrap();
194        let batch = overlay.next().unwrap().unwrap();
195        let view = batch.schema.view(&batch.rows[0]);
196        assert_eq!(
197            view.qualified_column("outer.dot", "column.dot"),
198            Some(&Value::Int(9))
199        );
200        assert_eq!(view.qualified_column("outer", "dot.column.dot"), None);
201    }
202}