1use crate::{Batch, ExecResult, OwnedPhysicalRow, PhysicalOperator, PhysicalRow, RowSchema};
10
11pub struct ScopeOverlay<'a> {
13 child: Box<dyn PhysicalOperator + 'a>,
14 schema: RowSchema,
15 outer: PhysicalRow,
16}
17
18impl<'a> ScopeOverlay<'a> {
19 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}