Skip to main content

hawdb_executor/
profile.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::binding::value_payload_bytes;
16use crate::columnar::ColumnarRowRef;
17use crate::QueryMemoryLease;
18use hawdb_core::{HawDBError, Result, Value, ValueRef};
19use hawdb_plan_cypher::{PhysicalOperatorId, PhysicalPlanKind};
20use std::collections::BTreeMap;
21use std::ops::Index;
22use std::sync::{Arc, OnceLock};
23
24pub type Row = BTreeMap<String, Value>;
25
26/// Column names shared by every row of one query result.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct QuerySchema {
29    columns: Arc<[String]>,
30}
31
32impl QuerySchema {
33    pub fn try_new(columns: impl IntoIterator<Item = String>) -> Result<Self> {
34        let columns = columns.into_iter().collect::<Vec<_>>();
35        let mut unique = std::collections::BTreeSet::new();
36        if let Some(duplicate) = columns
37            .iter()
38            .find(|column| !unique.insert(column.as_str()))
39        {
40            return Err(HawDBError::Semantic(format!(
41                "query result schema contains duplicate column {duplicate}"
42            )));
43        }
44        Ok(Self {
45            columns: Arc::from(columns),
46        })
47    }
48
49    pub fn empty() -> Self {
50        Self {
51            columns: Arc::from([]),
52        }
53    }
54
55    pub fn columns(&self) -> &[String] {
56        &self.columns
57    }
58
59    pub fn len(&self) -> usize {
60        self.columns.len()
61    }
62
63    pub fn is_empty(&self) -> bool {
64        self.columns.is_empty()
65    }
66
67    pub fn position(&self, name: &str) -> Option<usize> {
68        self.columns.iter().position(|column| column == name)
69    }
70}
71
72#[derive(Debug)]
73struct QueryRowStorage {
74    schema: QuerySchema,
75    values: QueryStoredValues,
76    row_count: usize,
77    // Keep the exact result-materialization charge alive for every owned row
78    // handle that can retain this storage after execution returns.
79    retained_memory: Option<Arc<QueryMemoryLease>>,
80}
81
82#[derive(Debug)]
83enum QueryStoredValues {
84    Flat(Vec<Value>),
85    Segmented(Vec<Vec<Value>>),
86}
87
88impl QueryStoredValues {
89    fn len(&self) -> usize {
90        match self {
91            Self::Flat(values) => values.len(),
92            Self::Segmented(segments) => segments.iter().map(Vec::len).sum(),
93        }
94    }
95}
96
97mod owned_result;
98pub(crate) use owned_result::cloned_row_memory_bytes;
99
100/// One owned handle into a query's immutable row storage.
101#[derive(Clone)]
102pub struct QueryRow {
103    storage: Arc<QueryRowStorage>,
104    row: usize,
105}
106
107impl QueryRow {
108    pub fn schema(&self) -> &QuerySchema {
109        &self.storage.schema
110    }
111
112    pub fn len(&self) -> usize {
113        self.storage.schema.len()
114    }
115
116    pub fn is_empty(&self) -> bool {
117        self.len() == 0
118    }
119
120    pub fn value(&self, column: usize) -> Option<&Value> {
121        row_values(&self.storage, self.row)?.get(column)
122    }
123
124    pub fn get(&self, column: &str) -> Option<&Value> {
125        self.value(self.storage.schema.position(column)?)
126    }
127
128    pub fn contains_key(&self, column: &str) -> bool {
129        self.storage.schema.position(column).is_some()
130    }
131
132    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &Value)> {
133        self.storage
134            .schema
135            .columns()
136            .iter()
137            .map(String::as_str)
138            .zip(row_values(&self.storage, self.row).unwrap_or_default())
139    }
140
141    pub fn keys(&self) -> impl ExactSizeIterator<Item = &str> {
142        self.storage.schema.columns().iter().map(String::as_str)
143    }
144
145    pub fn values(&self) -> impl ExactSizeIterator<Item = &Value> {
146        row_values(&self.storage, self.row)
147            .unwrap_or_default()
148            .iter()
149    }
150
151    pub fn as_ref(&self) -> QueryRowRef<'_> {
152        QueryRowRef {
153            schema: &self.storage.schema,
154            values: row_values(&self.storage, self.row).unwrap_or_default(),
155        }
156    }
157
158    pub fn to_owned_row(&self) -> Row {
159        self.iter()
160            .map(|(name, value)| (name.to_owned(), value.clone()))
161            .collect()
162    }
163}
164
165impl std::fmt::Debug for QueryRow {
166    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167        f.debug_map().entries(self.iter()).finish()
168    }
169}
170
171impl PartialEq for QueryRow {
172    fn eq(&self, other: &Self) -> bool {
173        self.schema() == other.schema()
174            && row_values(&self.storage, self.row) == row_values(&other.storage, other.row)
175    }
176}
177
178impl Eq for QueryRow {}
179
180impl PartialEq<Row> for QueryRow {
181    fn eq(&self, other: &Row) -> bool {
182        row_equals_map(self.as_ref(), other)
183    }
184}
185
186impl PartialEq<QueryRow> for Row {
187    fn eq(&self, other: &QueryRow) -> bool {
188        other == self
189    }
190}
191
192impl Index<&str> for QueryRow {
193    type Output = Value;
194
195    fn index(&self, column: &str) -> &Self::Output {
196        self.get(column)
197            .unwrap_or_else(|| panic!("query result has no column {column}"))
198    }
199}
200
201/// A borrowed row view over a query's immutable contiguous row buffer.
202#[derive(Debug, Clone, Copy)]
203pub struct QueryRowRef<'a> {
204    schema: &'a QuerySchema,
205    values: &'a [Value],
206}
207
208impl<'a> QueryRowRef<'a> {
209    pub fn schema(self) -> &'a QuerySchema {
210        self.schema
211    }
212
213    pub fn len(self) -> usize {
214        self.values.len()
215    }
216
217    pub fn is_empty(self) -> bool {
218        self.values.is_empty()
219    }
220
221    pub fn value(self, column: usize) -> Option<&'a Value> {
222        self.values.get(column)
223    }
224
225    pub fn get(self, column: &str) -> Option<&'a Value> {
226        self.value(self.schema.position(column)?)
227    }
228
229    pub fn contains_key(self, column: &str) -> bool {
230        self.schema.position(column).is_some()
231    }
232
233    pub fn iter(self) -> impl ExactSizeIterator<Item = (&'a str, &'a Value)> {
234        self.schema
235            .columns()
236            .iter()
237            .map(String::as_str)
238            .zip(self.values)
239    }
240
241    pub fn keys(self) -> impl ExactSizeIterator<Item = &'a str> {
242        self.schema.columns().iter().map(String::as_str)
243    }
244
245    pub fn values(self) -> impl ExactSizeIterator<Item = &'a Value> {
246        self.values.iter()
247    }
248
249    pub fn to_owned_row(self) -> Row {
250        self.iter()
251            .map(|(name, value)| (name.to_owned(), value.clone()))
252            .collect()
253    }
254}
255
256impl PartialEq<Row> for QueryRowRef<'_> {
257    fn eq(&self, other: &Row) -> bool {
258        row_equals_map(*self, other)
259    }
260}
261
262impl PartialEq<QueryRowRef<'_>> for Row {
263    fn eq(&self, other: &QueryRowRef<'_>) -> bool {
264        other == self
265    }
266}
267
268impl Index<&str> for QueryRowRef<'_> {
269    type Output = Value;
270
271    fn index(&self, column: &str) -> &Self::Output {
272        self.get(column)
273            .unwrap_or_else(|| panic!("query result has no column {column}"))
274    }
275}
276
277/// Schema-bearing result rows backed by immutable contiguous row buffers.
278pub struct QueryRows {
279    storage: Arc<QueryRowStorage>,
280    indexed_rows: OnceLock<Vec<QueryRow>>,
281}
282
283impl QueryRows {
284    pub fn empty() -> Self {
285        Self::from_flat_values_unchecked(QuerySchema::empty(), Vec::new(), 0)
286    }
287
288    /// Builds rows whose columns follow `schema` rather than map key order.
289    ///
290    /// The schema is kept when `rows` is empty. Each row must name exactly the
291    /// schema's columns: a missing column fails instead of reading as NULL.
292    pub fn try_from_named_rows(schema: QuerySchema, rows: Vec<Row>) -> Result<Self> {
293        let mut builder = QueryRowsBuilder::with_schema(schema.clone(), rows.len());
294        for (ordinal, mut row) in rows.into_iter().enumerate() {
295            if row.len() != schema.len() {
296                return Err(HawDBError::Execution(format!(
297                    "query result row {ordinal} has width {}, expected {}",
298                    row.len(),
299                    schema.len()
300                )));
301            }
302            builder.try_push_values(schema.columns().iter().map(|column| {
303                row.remove(column).ok_or_else(|| {
304                    HawDBError::Execution(format!(
305                        "query result row {ordinal} has no column {column}"
306                    ))
307                })
308            }))?;
309        }
310        Ok(builder.finish())
311    }
312
313    fn from_flat_values_unchecked(
314        schema: QuerySchema,
315        values: Vec<Value>,
316        row_count: usize,
317    ) -> Self {
318        Self {
319            storage: Arc::new(QueryRowStorage {
320                schema,
321                values: QueryStoredValues::Flat(values),
322                row_count,
323                retained_memory: None,
324            }),
325            indexed_rows: OnceLock::new(),
326        }
327    }
328
329    pub fn schema(&self) -> &QuerySchema {
330        &self.storage.schema
331    }
332
333    pub fn value_rows(&self) -> QueryValueRows<'_> {
334        QueryValueRows {
335            rows: self,
336            next: 0,
337        }
338    }
339
340    pub fn len(&self) -> usize {
341        self.storage.row_count
342    }
343
344    pub fn is_empty(&self) -> bool {
345        self.storage.row_count == 0
346    }
347
348    pub fn value(&self, row: usize, column: usize) -> Option<ValueRef<'_>> {
349        row_values(&self.storage, row)?
350            .get(column)
351            .map(Value::as_ref)
352    }
353
354    pub fn get(&self, row: usize, column: &str) -> Option<ValueRef<'_>> {
355        self.value(row, self.schema().position(column)?)
356    }
357
358    pub fn into_rows(self) -> Vec<Row> {
359        (0..self.len())
360            .map(|row| {
361                self.row(row)
362                    .expect("row ordinal is in range")
363                    .to_owned_row()
364            })
365            .collect()
366    }
367
368    pub fn row(&self, row: usize) -> Option<QueryRowRef<'_>> {
369        if row >= self.len() {
370            return None;
371        }
372        Some(QueryRowRef {
373            schema: &self.storage.schema,
374            values: row_values(&self.storage, row)?,
375        })
376    }
377
378    pub fn first(&self) -> Option<QueryRowRef<'_>> {
379        self.row(0)
380    }
381
382    pub fn last(&self) -> Option<QueryRowRef<'_>> {
383        self.len().checked_sub(1).and_then(|row| self.row(row))
384    }
385
386    pub fn iter(&self) -> QueryRowsIter<'_> {
387        QueryRowsIter {
388            rows: self,
389            next: 0,
390        }
391    }
392
393    pub fn retain(&mut self, mut keep: impl FnMut(QueryRowRef<'_>) -> bool) {
394        let width = self.schema().len();
395        let mut values = Vec::with_capacity(self.storage.values.len());
396        let mut row_count = 0usize;
397        for row in self.iter() {
398            if keep(row) {
399                values.extend_from_slice(row.values);
400                row_count = row_count.saturating_add(1);
401            }
402        }
403        let retained_row_count = values.len().checked_div(width).unwrap_or(row_count);
404        self.storage = Arc::new(QueryRowStorage {
405            schema: self.schema().clone(),
406            values: QueryStoredValues::Flat(values),
407            row_count: retained_row_count,
408            retained_memory: self.storage.retained_memory.clone(),
409        });
410        self.indexed_rows = OnceLock::new();
411    }
412
413    /// Returns stable owned row handles for APIs that require slice pattern
414    /// matching or indexing. No map or value is copied.
415    pub fn as_slice(&self) -> &[QueryRow] {
416        self.indexed_rows()
417    }
418
419    pub fn payload_bytes(&self) -> usize {
420        let names = self
421            .schema()
422            .columns()
423            .iter()
424            .fold(0usize, |total, name| total.saturating_add(name.len()));
425        stored_values(&self.storage).fold(names, |total, value| {
426            total.saturating_add(value_payload_bytes(value))
427        })
428    }
429
430    /// Exact result-materialization bytes retained by this owned result.
431    ///
432    /// Results built outside the admitted executor return zero.
433    #[doc(hidden)]
434    pub fn retained_memory_bytes(&self) -> usize {
435        self.storage
436            .retained_memory
437            .as_deref()
438            .map_or(0, QueryMemoryLease::bytes)
439    }
440
441    fn indexed_rows(&self) -> &[QueryRow] {
442        self.indexed_rows.get_or_init(|| {
443            (0..self.len())
444                .map(|row| QueryRow {
445                    storage: Arc::clone(&self.storage),
446                    row,
447                })
448                .collect()
449        })
450    }
451}
452
453impl Default for QueryRows {
454    fn default() -> Self {
455        Self::empty()
456    }
457}
458
459impl From<Vec<Row>> for QueryRows {
460    fn from(rows: Vec<Row>) -> Self {
461        let columns = rows
462            .iter()
463            .flat_map(|row| row.keys().cloned())
464            .collect::<std::collections::BTreeSet<_>>()
465            .into_iter()
466            .collect::<Vec<_>>();
467        let schema = QuerySchema::try_new(columns).expect("BTreeSet columns are unique");
468        let mut builder = QueryRowsBuilder::with_schema(schema.clone(), rows.len());
469        for mut row in rows {
470            builder
471                .push_values(
472                    schema
473                        .columns()
474                        .iter()
475                        .map(|column| row.remove(column).unwrap_or(Value::Null)),
476                )
477                .expect("row normalization always matches the inferred schema");
478        }
479        builder.finish()
480    }
481}
482
483impl FromIterator<Row> for QueryRows {
484    fn from_iter<T: IntoIterator<Item = Row>>(iter: T) -> Self {
485        iter.into_iter().collect::<Vec<_>>().into()
486    }
487}
488
489/// Builds immutable query results without retaining one map or vector per row.
490///
491/// The first named row binds column names to stable ordinals. Later rows are
492/// consumed in the same deterministic `BTreeMap` order and their values move
493/// directly into bounded row segments when cardinality is unknown. Explicit
494/// capacity constructors retain the flat-buffer path.
495pub struct QueryRowsBuilder {
496    schema: Option<QuerySchema>,
497    values: Vec<Value>,
498    row_count: usize,
499    row_capacity_hint: usize,
500    segments: Option<Vec<Vec<Value>>>,
501    input_order: Option<owned_result::InputOrder>,
502}
503
504impl QueryRowsBuilder {
505    pub fn new() -> Self {
506        let mut builder = Self::with_row_capacity(0);
507        builder.segments = Some(Vec::new());
508        builder
509    }
510
511    pub fn with_row_capacity(row_capacity: usize) -> Self {
512        Self {
513            schema: None,
514            values: Vec::new(),
515            row_count: 0,
516            row_capacity_hint: row_capacity,
517            segments: None,
518            input_order: None,
519        }
520    }
521
522    pub fn with_schema(schema: QuerySchema, row_capacity: usize) -> Self {
523        Self {
524            values: Vec::with_capacity(row_capacity.saturating_mul(schema.len())),
525            schema: Some(schema),
526            row_count: 0,
527            row_capacity_hint: row_capacity,
528            segments: None,
529            input_order: None,
530        }
531    }
532
533    pub fn push_values(&mut self, values: impl IntoIterator<Item = Value>) -> Result<()> {
534        self.try_push_values(values.into_iter().map(Ok))
535    }
536
537    /// Appends one fallible row into immutable-result storage.
538    ///
539    /// A conversion error or width mismatch rolls the partial row back.
540    pub fn try_push_values(
541        &mut self,
542        values: impl IntoIterator<Item = Result<Value>>,
543    ) -> Result<()> {
544        let schema = self.schema.as_ref().ok_or_else(|| {
545            HawDBError::Execution(
546                "query result values require a schema before ordinal insertion".to_string(),
547            )
548        })?;
549        if self.segments.is_some() {
550            let values = values.into_iter().collect::<Result<Vec<_>>>()?;
551            if values.len() != schema.len() {
552                return Err(HawDBError::Execution(format!(
553                    "query result row {} has width {}, expected {}",
554                    self.row_count,
555                    values.len(),
556                    schema.len()
557                )));
558            }
559            return self.append_named_values(values.into_iter());
560        }
561        let start = self.values.len();
562        for value in values {
563            match value {
564                Ok(value) => self.values.push(value),
565                Err(error) => {
566                    self.values.truncate(start);
567                    return Err(error);
568                }
569            }
570        }
571        let width = self.values.len().saturating_sub(start);
572        if width != schema.len() {
573            self.values.truncate(start);
574            return Err(HawDBError::Execution(format!(
575                "query result row {} has width {width}, expected {}",
576                self.row_count,
577                schema.len()
578            )));
579        }
580        self.row_count = self.row_count.saturating_add(1);
581        Ok(())
582    }
583
584    pub fn push_named_row(&mut self, row: Row) -> Result<()> {
585        if self.schema.is_none() {
586            self.schema = Some(QuerySchema::try_new(row.keys().cloned())?);
587            self.values.reserve(
588                row.len()
589                    .saturating_mul(self.row_capacity_hint.max(self.row_count.saturating_add(1))),
590            );
591        }
592        let schema = self.schema.as_ref().expect("query result schema is bound");
593        if row.len() != schema.len() {
594            return Err(HawDBError::Execution(format!(
595                "query result row {} has width {}, expected {}",
596                self.row_count,
597                row.len(),
598                schema.len()
599            )));
600        }
601        if let Some((ordinal, (name, expected))) = row
602            .keys()
603            .zip(schema.columns())
604            .enumerate()
605            .find(|(_, (name, expected))| *name != *expected)
606        {
607            return Err(HawDBError::Execution(format!(
608                "query result row {} column {ordinal} is {name}, expected {expected}",
609                self.row_count
610            )));
611        }
612        self.append_named_values(row.into_values())
613    }
614
615    fn append_named_values(&mut self, values: impl Iterator<Item = Value>) -> Result<()> {
616        if let Some(segments) = &mut self.segments {
617            let width = self
618                .schema
619                .as_ref()
620                .expect("query result schema is bound")
621                .len();
622            let rows_in_segment = self.row_count % OWNED_RESULT_SEGMENT_ROWS;
623            if rows_in_segment == 0 && self.row_count != 0 && width != 0 {
624                segments.push(std::mem::take(&mut self.values));
625            }
626            let needed = (rows_in_segment + 1).checked_mul(width).ok_or_else(|| {
627                HawDBError::Execution("query result segment capacity overflow".into())
628            })?;
629            if needed > self.values.capacity() {
630                let target_rows = (rows_in_segment + 1).next_power_of_two();
631                let target = target_rows.checked_mul(width).ok_or_else(|| {
632                    HawDBError::Execution("query result segment capacity overflow".into())
633                })?;
634                self.values.reserve_exact(target - self.values.len());
635            }
636        }
637        self.values.extend(values);
638        self.row_count = self.row_count.saturating_add(1);
639        Ok(())
640    }
641
642    pub fn finish(self) -> QueryRows {
643        self.finish_with_retained_memory(None)
644    }
645
646    pub(crate) fn finish_with_retained_memory(
647        mut self,
648        retained_memory: Option<QueryMemoryLease>,
649    ) -> QueryRows {
650        let retained_memory = retained_memory.map(Arc::new);
651        if let Some(mut segments) = self.segments.take() {
652            if segments.is_empty() {
653                return QueryRows {
654                    storage: Arc::new(QueryRowStorage {
655                        schema: self.schema.unwrap_or_else(QuerySchema::empty),
656                        values: QueryStoredValues::Flat(self.values),
657                        row_count: self.row_count,
658                        retained_memory,
659                    }),
660                    indexed_rows: OnceLock::new(),
661                };
662            }
663            if !self.values.is_empty() {
664                segments.push(self.values);
665            }
666            return QueryRows {
667                storage: Arc::new(QueryRowStorage {
668                    schema: self.schema.unwrap_or_else(QuerySchema::empty),
669                    values: QueryStoredValues::Segmented(segments),
670                    row_count: self.row_count,
671                    retained_memory,
672                }),
673                indexed_rows: OnceLock::new(),
674            };
675        }
676        QueryRows {
677            storage: Arc::new(QueryRowStorage {
678                schema: self.schema.unwrap_or_else(QuerySchema::empty),
679                values: QueryStoredValues::Flat(self.values),
680                row_count: self.row_count,
681                retained_memory,
682            }),
683            indexed_rows: OnceLock::new(),
684        }
685    }
686}
687
688impl Default for QueryRowsBuilder {
689    fn default() -> Self {
690        Self::new()
691    }
692}
693
694impl Clone for QueryRows {
695    fn clone(&self) -> Self {
696        Self {
697            storage: Arc::clone(&self.storage),
698            indexed_rows: OnceLock::new(),
699        }
700    }
701}
702
703impl std::fmt::Debug for QueryRows {
704    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
705        f.debug_struct("QueryRows")
706            .field("schema", self.schema())
707            .field("values", &self.storage.values)
708            .finish()
709    }
710}
711
712impl PartialEq for QueryRows {
713    fn eq(&self, other: &Self) -> bool {
714        self.schema() == other.schema()
715            && self.len() == other.len()
716            && stored_values(&self.storage).eq(stored_values(&other.storage))
717    }
718}
719
720impl Eq for QueryRows {}
721
722impl PartialEq<Vec<Row>> for QueryRows {
723    fn eq(&self, other: &Vec<Row>) -> bool {
724        self.len() == other.len() && self.iter().zip(other).all(|(left, right)| left == *right)
725    }
726}
727
728impl PartialEq<QueryRows> for Vec<Row> {
729    fn eq(&self, other: &QueryRows) -> bool {
730        other == self
731    }
732}
733
734impl Index<usize> for QueryRows {
735    type Output = QueryRow;
736
737    fn index(&self, row: usize) -> &Self::Output {
738        &self.indexed_rows()[row]
739    }
740}
741
742impl<'a> IntoIterator for &'a QueryRows {
743    type Item = QueryRowRef<'a>;
744    type IntoIter = QueryRowsIter<'a>;
745
746    fn into_iter(self) -> Self::IntoIter {
747        self.iter()
748    }
749}
750
751impl IntoIterator for QueryRows {
752    type Item = QueryRow;
753    type IntoIter = QueryRowsIntoIter;
754
755    fn into_iter(self) -> Self::IntoIter {
756        QueryRowsIntoIter {
757            storage: self.storage,
758            next: 0,
759        }
760    }
761}
762
763pub struct QueryRowsIter<'a> {
764    rows: &'a QueryRows,
765    next: usize,
766}
767
768impl<'a> Iterator for QueryRowsIter<'a> {
769    type Item = QueryRowRef<'a>;
770
771    fn next(&mut self) -> Option<Self::Item> {
772        let row = self.rows.row(self.next)?;
773        self.next = self.next.saturating_add(1);
774        Some(row)
775    }
776
777    fn size_hint(&self) -> (usize, Option<usize>) {
778        let remaining = self.rows.len().saturating_sub(self.next);
779        (remaining, Some(remaining))
780    }
781}
782
783impl ExactSizeIterator for QueryRowsIter<'_> {}
784
785pub struct QueryValueRows<'a> {
786    rows: &'a QueryRows,
787    next: usize,
788}
789
790impl QueryValueRows<'_> {
791    pub fn is_empty(&self) -> bool {
792        self.len() == 0
793    }
794}
795
796impl<'a> Iterator for QueryValueRows<'a> {
797    type Item = &'a [Value];
798
799    fn next(&mut self) -> Option<Self::Item> {
800        let values = self.rows.row(self.next)?.values;
801        self.next = self.next.saturating_add(1);
802        Some(values)
803    }
804
805    fn size_hint(&self) -> (usize, Option<usize>) {
806        let remaining = self.rows.len().saturating_sub(self.next);
807        (remaining, Some(remaining))
808    }
809}
810
811impl ExactSizeIterator for QueryValueRows<'_> {}
812
813pub struct QueryRowsIntoIter {
814    storage: Arc<QueryRowStorage>,
815    next: usize,
816}
817
818impl Iterator for QueryRowsIntoIter {
819    type Item = QueryRow;
820
821    fn next(&mut self) -> Option<Self::Item> {
822        if self.next >= self.storage.row_count {
823            return None;
824        }
825        let row = QueryRow {
826            storage: Arc::clone(&self.storage),
827            row: self.next,
828        };
829        self.next = self.next.saturating_add(1);
830        Some(row)
831    }
832
833    fn size_hint(&self) -> (usize, Option<usize>) {
834        let remaining = self.storage.row_count.saturating_sub(self.next);
835        (remaining, Some(remaining))
836    }
837}
838
839impl ExactSizeIterator for QueryRowsIntoIter {}
840
841const OWNED_RESULT_SEGMENT_ROWS: usize = 256;
842
843fn stored_values(storage: &QueryRowStorage) -> impl Iterator<Item = &Value> {
844    let (flat, segments) = match &storage.values {
845        QueryStoredValues::Flat(values) => (Some(values), None),
846        QueryStoredValues::Segmented(segments) => (None, Some(segments)),
847    };
848    flat.into_iter().flatten().chain(
849        segments
850            .into_iter()
851            .flatten()
852            .flat_map(|segment| segment.iter()),
853    )
854}
855
856fn row_values(storage: &QueryRowStorage, row: usize) -> Option<&[Value]> {
857    if row >= storage.row_count {
858        return None;
859    }
860    let width = storage.schema.len();
861    if width == 0 {
862        return Some(&[]);
863    }
864    let (values, ordinal) = match &storage.values {
865        QueryStoredValues::Segmented(segments) => (
866            segments.get(row / OWNED_RESULT_SEGMENT_ROWS)?,
867            row % OWNED_RESULT_SEGMENT_ROWS,
868        ),
869        QueryStoredValues::Flat(values) => (values, row),
870    };
871    let start = ordinal.checked_mul(width)?;
872    let end = start.checked_add(width)?;
873    values.get(start..end)
874}
875
876fn row_equals_map(row: QueryRowRef<'_>, other: &Row) -> bool {
877    row.len() == other.len()
878        && row
879            .iter()
880            .all(|(name, value)| other.get(name).is_some_and(|other| other == value))
881}
882
883/// A row view whose values remain valid only for the current consumer call.
884///
885/// The map representation covers the scalar fallback executor. The columnar
886/// representation lets a downstream consumer pull selected values directly
887/// from an immutable batch without constructing an intermediate row map.
888#[derive(Debug, Clone, Copy)]
889pub enum RowRef<'a> {
890    Map(&'a Row),
891    Columnar(ColumnarRowRef<'a>),
892    Compact(QueryRowRef<'a>),
893    Projected(ProjectedRowRef<'a>),
894}
895
896impl<'a> RowRef<'a> {
897    pub fn len(self) -> usize {
898        match self {
899            Self::Map(row) => row.len(),
900            Self::Columnar(_) => self.iter().count(),
901            Self::Compact(row) => row.len(),
902            Self::Projected(row) => row.columns.len(),
903        }
904    }
905
906    pub fn is_empty(self) -> bool {
907        self.len() == 0
908    }
909
910    pub fn get(self, name: &str) -> Option<ValueRef<'a>> {
911        match self {
912            Self::Map(row) => row.get(name).map(Value::as_ref),
913            Self::Columnar(row) => row.get(name),
914            Self::Compact(row) => row.get(name).map(Value::as_ref),
915            Self::Projected(row) => row.get(name),
916        }
917    }
918
919    pub fn column(self, index: usize) -> Option<(&'a str, ValueRef<'a>)> {
920        match self {
921            Self::Map(row) => row
922                .iter()
923                .nth(index)
924                .map(|(name, value)| (name.as_str(), value.as_ref())),
925            Self::Columnar(row) => row.column(index),
926            Self::Compact(row) => Some((
927                row.schema().columns().get(index)?.as_str(),
928                row.value(index)?.as_ref(),
929            )),
930            Self::Projected(row) => row.column(index),
931        }
932    }
933
934    pub fn iter(self) -> RowRefIter<'a> {
935        match self {
936            Self::Map(row) => RowRefIter::Map(row.iter()),
937            Self::Columnar(row) => RowRefIter::Columnar { row, index: 0 },
938            Self::Compact(row) => RowRefIter::Compact { row, index: 0 },
939            Self::Projected(row) => RowRefIter::Projected { row, index: 0 },
940        }
941    }
942
943    pub fn to_owned_row(self) -> Row {
944        self.iter()
945            .map(|(name, value)| (name.to_owned(), value.to_owned_value()))
946            .collect()
947    }
948}
949
950/// A schema-bound view of native projection values. Payloads belong to the
951/// source batch or plan and are valid only during the consumer invocation.
952#[derive(Clone, Copy)]
953pub struct ProjectedRowRef<'a> {
954    pub(crate) columns: &'a [(String, usize)],
955    pub(crate) value: &'a (dyn Fn(usize) -> ValueRef<'a> + Sync),
956}
957
958impl std::fmt::Debug for ProjectedRowRef<'_> {
959    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
960        formatter
961            .debug_map()
962            .entries(RowRef::Projected(*self).iter())
963            .finish()
964    }
965}
966
967impl<'a> ProjectedRowRef<'a> {
968    pub fn column(self, index: usize) -> Option<(&'a str, ValueRef<'a>)> {
969        let (name, projection) = self.columns.get(index)?;
970        Some((name.as_str(), (self.value)(*projection)))
971    }
972
973    pub fn get(self, name: &str) -> Option<ValueRef<'a>> {
974        let index = self
975            .columns
976            .binary_search_by(|(column, _)| column.as_str().cmp(name))
977            .ok()?;
978        self.column(index).map(|(_, value)| value)
979    }
980}
981
982impl<'a> From<&'a Row> for RowRef<'a> {
983    fn from(row: &'a Row) -> Self {
984        Self::Map(row)
985    }
986}
987
988impl<'a> From<ColumnarRowRef<'a>> for RowRef<'a> {
989    fn from(row: ColumnarRowRef<'a>) -> Self {
990        Self::Columnar(row)
991    }
992}
993
994impl<'a> From<QueryRowRef<'a>> for RowRef<'a> {
995    fn from(row: QueryRowRef<'a>) -> Self {
996        Self::Compact(row)
997    }
998}
999
1000#[derive(Clone)]
1001pub enum RowRefIter<'a> {
1002    Map(std::collections::btree_map::Iter<'a, String, Value>),
1003    Columnar {
1004        row: ColumnarRowRef<'a>,
1005        index: usize,
1006    },
1007    Compact {
1008        row: QueryRowRef<'a>,
1009        index: usize,
1010    },
1011    Projected {
1012        row: ProjectedRowRef<'a>,
1013        index: usize,
1014    },
1015}
1016
1017impl<'a> Iterator for RowRefIter<'a> {
1018    type Item = (&'a str, ValueRef<'a>);
1019
1020    fn next(&mut self) -> Option<Self::Item> {
1021        match self {
1022            Self::Map(iter) => iter
1023                .next()
1024                .map(|(name, value)| (name.as_str(), value.as_ref())),
1025            Self::Columnar { row, index } => loop {
1026                let current = *index;
1027                *index = index.saturating_add(1);
1028                if current >= row.schema().len() {
1029                    return None;
1030                }
1031                if let Some(column) = row.column(current) {
1032                    return Some(column);
1033                }
1034            },
1035            Self::Compact { row, index } => {
1036                let column = RowRef::Compact(*row).column(*index)?;
1037                *index += 1;
1038                Some(column)
1039            }
1040            Self::Projected { row, index } => {
1041                let column = row.column(*index)?;
1042                *index += 1;
1043                Some(column)
1044            }
1045        }
1046    }
1047}
1048
1049#[derive(Debug, Clone, PartialEq, Eq)]
1050pub struct BlockingOperatorMemoryReport {
1051    pub operator: String,
1052    pub budget_bytes: usize,
1053    pub peak_tracked_bytes: usize,
1054    pub input_rows: usize,
1055    /// Equal-hash candidate pairs examined before domain-specific filtering.
1056    pub candidate_rows: usize,
1057    /// Records decoded from spill runs, including repartition and hot-key replay.
1058    pub replay_rows: usize,
1059    /// Successful spill-run partition splits.
1060    pub repartitions: usize,
1061    pub max_spill_bytes: u64,
1062    pub max_spill_runs: usize,
1063    pub spilled_bytes: u64,
1064    pub spill_run_count: usize,
1065    pub spilled_rows: usize,
1066}
1067
1068#[derive(Debug, Clone, PartialEq, Eq, Default)]
1069pub struct PipelineMemoryReport {
1070    /// Query-owned runtime ledger budget across pipeline and blocking state.
1071    pub query_memory_budget_bytes: usize,
1072    /// Highest aggregate tracked resident bytes charged to the query ledger.
1073    pub query_memory_peak_bytes: usize,
1074    /// Tracked bytes still owned at the execution completion boundary.
1075    pub query_memory_completion_bytes: usize,
1076    /// Number of operator or transfer accounts created by the query.
1077    pub query_memory_account_count: usize,
1078    /// Sum of rows emitted at every physical operator boundary.
1079    pub intermediate_rows: usize,
1080    /// Sum of retained payload estimates emitted at every physical operator boundary.
1081    pub intermediate_payload_bytes: usize,
1082    pub peak_batch_rows: usize,
1083    pub peak_batch_payload_bytes: usize,
1084    /// Rows projected from source property references before owned output admission.
1085    /// This counter does not claim source-to-host zero-copy delivery.
1086    pub borrowed_node_projection_rows: usize,
1087    /// Query-owned string CASE dispatch tables prepared from the bound plan.
1088    pub simple_case_dispatch_tables: usize,
1089    /// CASE evaluations served by prepared key lookup (including misses/NULL).
1090    pub simple_case_dispatch_lookups: usize,
1091    /// Native projected row views delivered without constructing a row map.
1092    /// Owned/validated consumers may still copy admitted values for retention.
1093    pub native_projection_rows: usize,
1094    /// Number of typed columnar batches evaluated by eligible pipeline fragments.
1095    pub columnar_batches: usize,
1096    /// Rows loaded into typed columns before selection.
1097    pub columnar_input_rows: usize,
1098    /// Rows retained by columnar selections.
1099    pub columnar_selected_rows: usize,
1100    /// Morsels consumed by columnar fragments.
1101    pub morsel_count: usize,
1102    /// Highest resource-admitted worker count across morsel pipelines.
1103    pub morsel_max_admitted_workers: usize,
1104    /// Highest worker count actually used by a morsel scheduler.
1105    pub morsel_peak_active_workers: usize,
1106    /// Highest number of completed morsel outputs awaiting or crossing the
1107    /// coordinator's ordered consumer boundary.
1108    pub morsel_peak_buffered_outputs: usize,
1109    /// Highest estimated resident bytes held by those completed outputs.
1110    pub morsel_peak_buffered_output_bytes: usize,
1111    /// Highest number of out-of-order outputs held by the ordinal merger.
1112    pub morsel_peak_reorder_entries: usize,
1113    pub output_rows: usize,
1114    pub output_payload_bytes: usize,
1115    /// Resident memory before execution, when process sampling is supported.
1116    pub start_resident_bytes: Option<u64>,
1117    /// Process high-water resident memory before execution.
1118    pub start_peak_resident_bytes: Option<u64>,
1119    /// Resident memory after the output rows have been materialized.
1120    pub steady_resident_bytes: Option<u64>,
1121    /// Process high-water resident memory at completion.
1122    pub peak_resident_bytes: Option<u64>,
1123    /// Positive resident-memory delta retained at completion.
1124    pub steady_resident_growth_bytes: Option<u64>,
1125    /// Positive process high-water delta observed during execution.
1126    pub lifetime_peak_resident_growth_bytes: Option<u64>,
1127    /// Aggregate page-fault delta when the platform exposes it.
1128    pub total_page_faults: Option<u64>,
1129    /// Split page-fault deltas only on platforms that expose this distinction.
1130    pub minor_page_faults: Option<u64>,
1131    pub major_page_faults: Option<u64>,
1132}
1133
1134/// The observed output cardinality for one physical operator.
1135#[derive(Debug, Clone, PartialEq, Eq)]
1136pub struct OperatorCardinalityProfile {
1137    pub operator_id: PhysicalOperatorId,
1138    pub operator: PhysicalPlanKind,
1139    /// `None` means the operator was not invoked, while `Some(0)` means it ran
1140    /// and produced no output rows.
1141    pub actual_rows: Option<usize>,
1142}
1143
1144#[derive(Debug, Clone, PartialEq, Eq)]
1145pub struct ReadExecutionProfile<TScanPruningReport> {
1146    pub max_rows: Option<usize>,
1147    pub detection_row_cap: Option<usize>,
1148    pub row_limit_enforced_before_output: bool,
1149    pub operator_row_cap_enabled: bool,
1150    pub operator_cardinality_profiles: Vec<OperatorCardinalityProfile>,
1151    pub blocking_operator_kinds: Vec<String>,
1152    pub scan_pruning_reports: Vec<TScanPruningReport>,
1153    pub vector_execution_reports: Vec<crate::VectorExecutionReport>,
1154    pub graph_expansion_reports: Vec<crate::GraphExpansionExecutionReport>,
1155    pub text_seed_execution_reports: Vec<crate::TextSeedExecutionReport>,
1156    pub grouped_evidence_reports: Vec<crate::GroupedEvidenceExecutionReport>,
1157    pub blocking_operator_memory_reports: Vec<BlockingOperatorMemoryReport>,
1158    pub pipeline_memory_report: PipelineMemoryReport,
1159}
1160
1161impl<TScanPruningReport> ReadExecutionProfile<TScanPruningReport> {
1162    pub fn blocking_operator_count(&self) -> usize {
1163        self.blocking_operator_kinds.len()
1164    }
1165}
1166
1167#[derive(Debug, Clone, PartialEq, Eq)]
1168pub struct ProfiledQueryRows<TScanPruningReport> {
1169    pub rows: QueryRows,
1170    pub profile: ReadExecutionProfile<TScanPruningReport>,
1171}
1172
1173#[derive(Debug, Clone, PartialEq, Eq)]
1174pub struct ProfiledQueryStream<TScanPruningReport> {
1175    /// True when the physical plan emitted batches directly to the consumer.
1176    /// False means an unsupported operator still materialized bindings before
1177    /// the bounded consumer boundary.
1178    pub fully_streamed: bool,
1179    pub profile: ReadExecutionProfile<TScanPruningReport>,
1180}
1181
1182#[cfg(test)]
1183mod tests {
1184    use super::*;
1185    use crate::{
1186        BindingSchema, ColumnVector, ColumnarBatch, SlotDescriptor, SlotId, SlotType, Validity,
1187    };
1188    use hawdb_core::LogicalType;
1189    use std::sync::Arc;
1190
1191    #[test]
1192    fn segmented_results_preserve_access_ownership_and_flat_equality() {
1193        let expected = (0..1000)
1194            .map(|id| {
1195                Row::from([
1196                    ("id".into(), Value::Int(id)),
1197                    ("payload".into(), Value::String(format!("value-{id}\0"))),
1198                ])
1199            })
1200            .collect::<Vec<_>>();
1201        let mut builder = QueryRowsBuilder::new();
1202        for row in &expected {
1203            builder.push_named_row(row.clone()).unwrap();
1204        }
1205        let rows = builder.finish();
1206        assert_eq!(rows, QueryRows::from(expected.clone()));
1207        assert_eq!(
1208            rows.payload_bytes(),
1209            QueryRows::from(expected.clone()).payload_bytes()
1210        );
1211        for ordinal in [0, 255, 256, 511, 512, 999] {
1212            assert_eq!(rows.row(ordinal).unwrap(), expected[ordinal]);
1213            assert_eq!(rows[ordinal], expected[ordinal]);
1214        }
1215        let handle = rows[256].clone();
1216        let mut retained = rows.clone();
1217        retained.retain(|row| row["id"] == Value::Int(256));
1218        assert_eq!(retained.len(), 1);
1219        assert_eq!(retained.first().unwrap(), expected[256]);
1220        assert_eq!(rows.clone().into_rows(), expected);
1221        drop(rows);
1222        assert_eq!(handle["id"], Value::Int(256));
1223    }
1224
1225    #[test]
1226    fn named_rows_follow_the_declared_schema_even_when_empty() {
1227        let schema = QuerySchema::try_new(["name".to_string(), "id".to_string()]).unwrap();
1228        let rows = QueryRows::try_from_named_rows(
1229            schema.clone(),
1230            vec![Row::from([
1231                ("id".into(), Value::Int(1)),
1232                ("name".into(), Value::String("one".into())),
1233            ])],
1234        )
1235        .unwrap();
1236        assert_eq!(rows.schema(), &schema);
1237        assert_eq!(
1238            rows.value_rows().collect::<Vec<_>>(),
1239            vec![&[Value::String("one".into()), Value::Int(1)][..]]
1240        );
1241
1242        let empty = QueryRows::try_from_named_rows(schema.clone(), Vec::new()).unwrap();
1243        assert_eq!(empty.schema(), &schema);
1244        assert!(empty.is_empty());
1245
1246        for (row, message) in [
1247            (
1248                Row::from([("id".into(), Value::Int(1))]),
1249                "query result row 0 has width 1, expected 2",
1250            ),
1251            (
1252                Row::from([("id".into(), Value::Int(1)), ("title".into(), Value::Null)]),
1253                "query result row 0 has no column name",
1254            ),
1255        ] {
1256            assert_eq!(
1257                QueryRows::try_from_named_rows(schema.clone(), vec![row]),
1258                Err(HawDBError::Execution(message.to_string()))
1259            );
1260        }
1261    }
1262
1263    #[test]
1264    fn segmented_results_bound_unknown_cardinality_capacity() {
1265        let mut builder = QueryRowsBuilder::new();
1266        for id in 0..100_000 {
1267            builder
1268                .push_named_row(Row::from([
1269                    ("id".into(), Value::Int(id)),
1270                    ("score".into(), Value::Float(id as f64)),
1271                    ("title".into(), Value::String("x".repeat(24))),
1272                ]))
1273                .unwrap();
1274        }
1275        let rows = builder.finish();
1276        let QueryStoredValues::Segmented(segments) = &rows.storage.values else {
1277            panic!("large results require segmented storage");
1278        };
1279        assert_eq!(
1280            segments.len(),
1281            100_000_usize.div_ceil(OWNED_RESULT_SEGMENT_ROWS)
1282        );
1283        let capacity = segments.iter().map(Vec::capacity).sum::<usize>();
1284        assert!(capacity >= 300_000);
1285        assert!(capacity <= 300_000 + 3 * OWNED_RESULT_SEGMENT_ROWS);
1286        assert!(segments
1287            .iter()
1288            .all(|segment| segment.capacity() <= 3 * OWNED_RESULT_SEGMENT_ROWS));
1289        assert_eq!(rows.get(99_999, "id"), Some(ValueRef::Int(99_999)));
1290    }
1291
1292    #[test]
1293    fn segmented_results_handle_empty_rows_and_boundary_rollback() {
1294        let mut empty = QueryRowsBuilder::new();
1295        for _ in 0..513 {
1296            empty.push_named_row(Row::new()).unwrap();
1297        }
1298        let empty = empty.finish();
1299        assert_eq!(empty.len(), 513);
1300        assert!(empty.row(512).unwrap().is_empty());
1301        let mut builder = QueryRowsBuilder::new();
1302        for id in 0..256 {
1303            builder
1304                .push_named_row(Row::from([("id".into(), Value::Int(id))]))
1305                .unwrap();
1306        }
1307        assert!(builder
1308            .push_named_row(Row::from([("wrong".into(), Value::Int(256))]))
1309            .is_err());
1310        assert!(builder
1311            .try_push_values([Err(HawDBError::Execution("failed".into()))])
1312            .is_err());
1313        assert!(builder.push_values([]).is_err());
1314        builder.push_values([Value::Int(256)]).unwrap();
1315        let rows = builder.finish();
1316        assert_eq!(rows.len(), 257);
1317        assert_eq!(rows.get(256, "id"), Some(ValueRef::Int(256)));
1318    }
1319
1320    #[test]
1321    fn counts_blocking_operator_kinds() {
1322        let profile = ReadExecutionProfile::<()> {
1323            max_rows: Some(10),
1324            detection_row_cap: Some(11),
1325            row_limit_enforced_before_output: true,
1326            operator_row_cap_enabled: true,
1327            operator_cardinality_profiles: Vec::new(),
1328            blocking_operator_kinds: vec!["sort".to_string(), "aggregate".to_string()],
1329            scan_pruning_reports: Vec::new(),
1330            vector_execution_reports: Vec::new(),
1331            graph_expansion_reports: Vec::new(),
1332            text_seed_execution_reports: Vec::new(),
1333            grouped_evidence_reports: Vec::new(),
1334            blocking_operator_memory_reports: Vec::new(),
1335            pipeline_memory_report: PipelineMemoryReport::default(),
1336        };
1337        assert_eq!(profile.blocking_operator_count(), 2);
1338    }
1339
1340    #[test]
1341    fn borrowed_map_row_materializes_only_on_request() {
1342        let row = Row::from([
1343            ("id".to_string(), Value::Int(7)),
1344            ("content".to_string(), Value::String("payload".into())),
1345        ]);
1346        let row_ref = RowRef::from(&row);
1347
1348        assert_eq!(row_ref.get("id"), Some(ValueRef::Int(7)));
1349        assert_eq!(row_ref.get("content").unwrap().as_str(), Some("payload"));
1350        assert_eq!(row_ref.to_owned_row(), row);
1351    }
1352
1353    #[test]
1354    fn schema_bearing_rows_keep_names_once_in_a_flat_value_buffer() {
1355        let schema = QuerySchema::try_new(["payload".to_string(), "id".to_string()]).unwrap();
1356        let mut builder = QueryRowsBuilder::with_schema(schema, 2);
1357        builder
1358            .push_values([Value::String("one".to_string()), Value::Int(1)])
1359            .unwrap();
1360        builder
1361            .push_values([Value::String("two".to_string()), Value::Int(2)])
1362            .unwrap();
1363        let rows = builder.finish();
1364
1365        assert_eq!(rows.schema().columns(), ["payload", "id"]);
1366        assert_eq!(rows.get(1, "payload").unwrap().as_str(), Some("two"));
1367        assert_eq!(rows.value(0, 1), Some(ValueRef::Int(1)));
1368        assert_eq!(rows.payload_bytes(), "payload".len() + "id".len() + 6 + 16);
1369        assert_eq!(rows.len(), 2);
1370        assert!(!rows.is_empty());
1371        assert_eq!(rows.storage.values.len(), 4);
1372        assert_eq!(rows[0]["payload"], Value::String("one".to_string()));
1373        assert_eq!(rows.iter().count(), 2);
1374        assert_eq!(rows.storage.values.len(), 4);
1375    }
1376
1377    #[test]
1378    fn schema_bearing_rows_reject_duplicate_columns_and_width_mismatch() {
1379        assert!(QuerySchema::try_new(["id".to_string(), "id".to_string()]).is_err());
1380        let schema = QuerySchema::try_new(["id".to_string()]).unwrap();
1381        let mut invalid = QueryRowsBuilder::with_schema(schema, 1);
1382        assert!(invalid.push_values([]).is_err());
1383
1384        let mut left = QueryRowsBuilder::with_schema(
1385            QuerySchema::try_new(["left".to_string(), "right".to_string()]).unwrap(),
1386            1,
1387        );
1388        left.push_values([Value::Int(1), Value::Int(2)]).unwrap();
1389        let left = left.finish();
1390        let mut right = QueryRowsBuilder::with_schema(
1391            QuerySchema::try_new(["right".to_string(), "left".to_string()]).unwrap(),
1392            1,
1393        );
1394        right.push_values([Value::Int(1), Value::Int(2)]).unwrap();
1395        let right = right.finish();
1396        assert_ne!(left, right);
1397    }
1398
1399    #[test]
1400    fn fallible_flat_row_append_rolls_back_partial_values() {
1401        let schema = QuerySchema::try_new(["left".to_string(), "right".to_string()]).unwrap();
1402        let mut builder = QueryRowsBuilder::with_schema(schema, 1);
1403        assert!(builder
1404            .try_push_values([
1405                Ok(Value::Int(1)),
1406                Err(HawDBError::Execution("projection failed".to_string())),
1407            ])
1408            .is_err());
1409        builder.push_values([Value::Int(2), Value::Int(3)]).unwrap();
1410        let rows = builder.finish();
1411
1412        assert_eq!(rows.len(), 1);
1413        assert_eq!(rows.value(0, 0), Some(ValueRef::Int(2)));
1414        assert_eq!(rows.value(0, 1), Some(ValueRef::Int(3)));
1415        assert_eq!(rows.storage.values.len(), 2);
1416    }
1417
1418    #[test]
1419    fn named_row_builder_binds_schema_once_and_moves_values_by_ordinal() {
1420        let mut builder = QueryRowsBuilder::with_row_capacity(2);
1421        builder
1422            .push_named_row(Row::from([
1423                ("id".to_string(), Value::Int(1)),
1424                ("payload".to_string(), Value::String("one".to_string())),
1425            ]))
1426            .unwrap();
1427        builder
1428            .push_named_row(Row::from([
1429                ("id".to_string(), Value::Int(2)),
1430                ("payload".to_string(), Value::String("two".to_string())),
1431            ]))
1432            .unwrap();
1433        let rows = builder.finish();
1434
1435        assert_eq!(rows.schema().columns(), ["id", "payload"]);
1436        assert_eq!(rows.value(1, 0), Some(ValueRef::Int(2)));
1437        assert_eq!(rows.get(0, "payload").unwrap().as_str(), Some("one"));
1438        assert_eq!(rows.storage.values.len(), 4);
1439    }
1440
1441    #[test]
1442    fn named_row_builder_rejects_schema_drift_without_partial_append() {
1443        let mut builder = QueryRowsBuilder::new();
1444        builder
1445            .push_named_row(Row::from([("id".to_string(), Value::Int(1))]))
1446            .unwrap();
1447        assert!(builder
1448            .push_named_row(Row::from([("name".to_string(), Value::Int(2))]))
1449            .is_err());
1450        let rows = builder.finish();
1451
1452        assert_eq!(rows.len(), 1);
1453        assert_eq!(rows.get(0, "id"), Some(ValueRef::Int(1)));
1454    }
1455
1456    #[test]
1457    fn borrowed_columnar_row_pulls_values_without_a_row_map() {
1458        let schema = Arc::new(
1459            BindingSchema::try_new(vec![
1460                SlotDescriptor {
1461                    id: SlotId(0),
1462                    name: "id".into(),
1463                    slot_type: SlotType::NodeId,
1464                },
1465                SlotDescriptor {
1466                    id: SlotId(1),
1467                    name: "content".into(),
1468                    slot_type: SlotType::logical(LogicalType::Text),
1469                },
1470            ])
1471            .unwrap(),
1472        );
1473        let batch = ColumnarBatch::try_new(
1474            schema,
1475            vec![
1476                Arc::new(ColumnVector::node_ids(vec![7])),
1477                Arc::new(ColumnVector::Utf8 {
1478                    values: vec!["payload".to_string()].into(),
1479                    validity: Validity::all(1),
1480                }),
1481            ],
1482        )
1483        .unwrap();
1484        let row_ref = RowRef::from(batch.rows().next().unwrap());
1485
1486        assert_eq!(row_ref.get("id"), Some(ValueRef::Int(7)));
1487        assert_eq!(row_ref.get("content").unwrap().as_str(), Some("payload"));
1488        assert_eq!(
1489            row_ref.to_owned_row(),
1490            Row::from([
1491                ("content".into(), Value::String("payload".into())),
1492                ("id".into(), Value::Int(7)),
1493            ])
1494        );
1495    }
1496}