Skip to main content

uqa_execution/
projected_row.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Borrowed positional rows produced directly by projected storage reads.
8
9use uqa_core::Value;
10use uqa_sql::expr::RowLookup;
11
12use crate::RowSchema;
13
14/// Source of one logical value in a [`ProjectedRow`].
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum ProjectedValueSlot {
17    /// Borrowed value at this position in the storage projection.
18    Field(usize),
19    /// Query-local value at this position in the extra-value slice.
20    Extra(usize),
21    /// A logical column that is absent from the projected source.
22    Missing,
23}
24
25impl ProjectedValueSlot {
26    /// Compile logical output columns against borrowed storage fields and query-local extras.
27    #[must_use]
28    pub fn compile(columns: &[String], fields: &[String], extras: &[&str]) -> Vec<Self> {
29        columns
30            .iter()
31            .map(|column| {
32                fields
33                    .iter()
34                    .position(|field| field == column)
35                    .map(Self::Field)
36                    .or_else(|| {
37                        extras
38                            .iter()
39                            .position(|extra| *extra == column)
40                            .map(Self::Extra)
41                    })
42                    .unwrap_or(Self::Missing)
43            })
44            .collect()
45    }
46}
47
48/// Concrete borrowed row passed from a projected source into an aggregate executor.
49///
50/// Keeping this representation in the execution contract lets aggregate hot paths use positional values without a trait-object call for every group key and aggregate input. Extra values cover source metadata such as document identifiers and scores without materializing a named row.
51pub struct ProjectedRow<'schema, 'row> {
52    schema: &'schema RowSchema,
53    slots: &'schema [ProjectedValueSlot],
54    fields: &'row [&'row Value],
55    extras: &'row [Option<Value>],
56}
57
58impl<'schema, 'row> ProjectedRow<'schema, 'row> {
59    #[must_use]
60    pub fn new(
61        schema: &'schema RowSchema,
62        slots: &'schema [ProjectedValueSlot],
63        fields: &'row [&'row Value],
64        extras: &'row [Option<Value>],
65    ) -> Self {
66        debug_assert_eq!(schema.len(), slots.len());
67        Self {
68            schema,
69            slots,
70            fields,
71            extras,
72        }
73    }
74
75    #[inline]
76    #[must_use]
77    pub fn positional_column(&self, index: usize) -> Option<&Value> {
78        match self.slots.get(index)? {
79            ProjectedValueSlot::Field(index) => self.fields.get(*index).copied(),
80            ProjectedValueSlot::Extra(index) => self.extras.get(*index).and_then(Option::as_ref),
81            ProjectedValueSlot::Missing => None,
82        }
83    }
84
85    /// Materialize only at an operator boundary that requires owned physical values.
86    #[must_use]
87    pub fn into_values(self) -> Vec<Value> {
88        (0..self.slots.len())
89            .map(|index| {
90                self.positional_column(index)
91                    .cloned()
92                    .unwrap_or(Value::Null)
93            })
94            .collect()
95    }
96}
97
98impl RowLookup for ProjectedRow<'_, '_> {
99    fn column(&self, name: &str) -> Option<&Value> {
100        self.schema
101            .unqualified_position(name)
102            .and_then(|index| self.positional_column(index))
103    }
104
105    fn column_is_ambiguous(&self, name: &str) -> bool {
106        self.schema.column_is_ambiguous(name)
107    }
108
109    fn qualified_column(&self, qualifier: &str, column: &str) -> Option<&Value> {
110        self.schema
111            .qualified_position(qualifier, column)
112            .and_then(|index| self.positional_column(index))
113    }
114
115    fn qualified_column_is_ambiguous(&self, qualifier: &str, column: &str) -> bool {
116        self.schema.qualified_column_is_ambiguous(qualifier, column)
117    }
118
119    #[inline]
120    fn positional_column(&self, index: usize) -> Option<&Value> {
121        Self::positional_column(self, index)
122    }
123
124    fn visit_columns(&self, visitor: &mut dyn FnMut(&str, &Value)) {
125        for (index, column) in self.schema.columns().iter().enumerate() {
126            visitor(
127                column,
128                self.positional_column(index).unwrap_or(&Value::Null),
129            );
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    fn projected_row_resolves_fields_extras_and_missing_slots() {
140        let columns = vec!["name".into(), "score".into(), "missing".into()];
141        let fields = vec!["name".into()];
142        let slots = ProjectedValueSlot::compile(&columns, &fields, &["score"]);
143        let schema = RowSchema::new(columns);
144        let name = Value::Str("Ada".into());
145        let values = [&name];
146        let extras = [Some(Value::Float(0.75))];
147        let row = ProjectedRow::new(&schema, &slots, &values, &extras);
148
149        assert_eq!(row.positional_column(0), Some(&name));
150        assert_eq!(row.positional_column(1), Some(&Value::Float(0.75)));
151        assert_eq!(row.positional_column(2), None);
152        assert_eq!(row.column("name"), Some(&name));
153    }
154
155    #[test]
156    fn projected_row_preserves_structured_qualification() {
157        let columns = vec!["id".into(), "id".into()];
158        let schema = RowSchema::with_identities(
159            columns.clone(),
160            vec![
161                crate::ColumnIdentity::qualified("left.dot", "id.dot"),
162                crate::ColumnIdentity::qualified("right", "id.dot"),
163            ],
164            vec![None, None],
165        );
166        let slots = vec![ProjectedValueSlot::Field(0), ProjectedValueSlot::Field(1)];
167        let left = Value::Int(1);
168        let right = Value::Int(2);
169        let values = [&left, &right];
170        let row = ProjectedRow::new(&schema, &slots, &values, &[]);
171
172        assert!(row.column_is_ambiguous("id.dot"));
173        assert_eq!(
174            row.qualified_column("left.dot", "id.dot"),
175            Some(&Value::Int(1))
176        );
177        assert_eq!(row.qualified_column("left", "dot.id.dot"), None);
178    }
179}