Skip to main content

uqa_execution/batch/
owned_row.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Owned physical rows used by row-at-a-time consumers.
8
9use uqa_core::Value;
10use uqa_sql::expr::RowLookup;
11use uqa_sql::ResultRow;
12
13use crate::physical::{ExecError, ExecResult};
14
15use super::{PhysicalRow, PhysicalRowView, RowSchema};
16
17/// Owned schema/row pair for row-at-a-time consumers that must outlive a decoded batch. Cloning this carrier shares the immutable schema index and row fragments; it does not build a named row or clone contained values.
18#[derive(Debug, Clone, PartialEq)]
19pub struct OwnedPhysicalRow {
20    pub schema: RowSchema,
21    pub row: PhysicalRow,
22}
23
24impl OwnedPhysicalRow {
25    pub fn new(schema: RowSchema, row: PhysicalRow) -> Self {
26        Self { schema, row }
27    }
28
29    pub fn view(&self) -> PhysicalRowView<'_> {
30        self.schema.view(&self.row)
31    }
32
33    pub fn get(&self, name: &str) -> Option<&Value> {
34        self.schema
35            .exact_slot(name)
36            .and_then(|slot| self.row.value(slot))
37    }
38
39    /// Read one flattened executor slot without introducing a SQL name.
40    pub fn physical_value_at(&self, position: usize) -> Option<&Value> {
41        self.row.value(position)
42    }
43
44    /// Apply a new logical schema by position while sharing the existing value fragments. Relation aliases and derived-column names therefore do not require an intermediate named row.
45    pub fn relabel(self, schema: RowSchema) -> ExecResult<Self> {
46        if self.schema.len() != schema.len() {
47            return Err(ExecError::Other(format!(
48                "cannot relabel {} columns as {} columns",
49                self.schema.len(),
50                schema.len()
51            )));
52        }
53        let slots = self.schema.index.slots.to_vec();
54        Ok(Self::new(schema, self.row.project_slots(&slots)))
55    }
56
57    pub fn into_result_row(self) -> ResultRow {
58        self.schema.materialize_result_row(self.row)
59    }
60}
61
62impl RowLookup for OwnedPhysicalRow {
63    fn column(&self, name: &str) -> Option<&Value> {
64        self.schema
65            .column_slot(name)
66            .and_then(|slot| self.row.value(slot))
67    }
68
69    fn column_is_ambiguous(&self, name: &str) -> bool {
70        self.schema.column_is_ambiguous(name)
71    }
72
73    fn qualified_column(&self, qualifier: &str, column: &str) -> Option<&Value> {
74        self.schema
75            .qualified_slot(qualifier, column)
76            .and_then(|slot| self.row.value(slot))
77    }
78
79    fn qualified_column_is_ambiguous(&self, qualifier: &str, column: &str) -> bool {
80        self.schema.qualified_column_is_ambiguous(qualifier, column)
81    }
82
83    fn positional_column(&self, index: usize) -> Option<&Value> {
84        self.schema
85            .slot(index)
86            .and_then(|slot| self.row.value(slot))
87    }
88
89    fn internal_column(&self, column: uqa_sql::ast::InternalColumnRef) -> Option<&Value> {
90        self.schema
91            .internal_slot(column)
92            .and_then(|slot| self.row.value(slot))
93    }
94
95    fn score_source(&self, qualifier: Option<&str>) -> Option<&Value> {
96        self.schema
97            .score_source_slot(qualifier)
98            .and_then(|slot| self.row.value(slot))
99    }
100
101    fn score_source_is_ambiguous(&self, qualifier: Option<&str>) -> bool {
102        self.schema.score_source_is_ambiguous(qualifier)
103    }
104
105    fn visit_columns(&self, visitor: &mut dyn FnMut(&str, &Value)) {
106        self.view().visit_columns(visitor);
107    }
108}