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 hawdb_core::{HawDBError, Result, Value, ValueRef};
18use hawdb_plan::{PhysicalOperatorId, PhysicalPlanKind};
19use std::collections::BTreeMap;
20use std::ops::Index;
21use std::sync::{Arc, OnceLock};
22
23pub type Row = BTreeMap<String, Value>;
24
25/// Column names shared by every row of one query result.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct QuerySchema {
28    columns: Arc<[String]>,
29}
30
31impl QuerySchema {
32    pub fn try_new(columns: impl IntoIterator<Item = String>) -> Result<Self> {
33        let columns = columns.into_iter().collect::<Vec<_>>();
34        let mut unique = std::collections::BTreeSet::new();
35        if let Some(duplicate) = columns
36            .iter()
37            .find(|column| !unique.insert(column.as_str()))
38        {
39            return Err(HawDBError::Semantic(format!(
40                "query result schema contains duplicate column {duplicate}"
41            )));
42        }
43        Ok(Self {
44            columns: Arc::from(columns),
45        })
46    }
47
48    pub fn empty() -> Self {
49        Self {
50            columns: Arc::from([]),
51        }
52    }
53
54    pub fn columns(&self) -> &[String] {
55        &self.columns
56    }
57
58    pub fn len(&self) -> usize {
59        self.columns.len()
60    }
61
62    pub fn is_empty(&self) -> bool {
63        self.columns.is_empty()
64    }
65
66    pub fn position(&self, name: &str) -> Option<usize> {
67        self.columns.iter().position(|column| column == name)
68    }
69}
70
71#[derive(Debug)]
72struct QueryRowStorage {
73    schema: QuerySchema,
74    values: Vec<Value>,
75    row_count: usize,
76}
77
78/// One owned handle into a query's immutable flat value buffer.
79#[derive(Clone)]
80pub struct QueryRow {
81    storage: Arc<QueryRowStorage>,
82    row: usize,
83}
84
85impl QueryRow {
86    pub fn schema(&self) -> &QuerySchema {
87        &self.storage.schema
88    }
89
90    pub fn len(&self) -> usize {
91        self.storage.schema.len()
92    }
93
94    pub fn is_empty(&self) -> bool {
95        self.len() == 0
96    }
97
98    pub fn value(&self, column: usize) -> Option<&Value> {
99        row_values(&self.storage, self.row)?.get(column)
100    }
101
102    pub fn get(&self, column: &str) -> Option<&Value> {
103        self.value(self.storage.schema.position(column)?)
104    }
105
106    pub fn contains_key(&self, column: &str) -> bool {
107        self.storage.schema.position(column).is_some()
108    }
109
110    pub fn iter(&self) -> impl ExactSizeIterator<Item = (&str, &Value)> {
111        self.storage
112            .schema
113            .columns()
114            .iter()
115            .map(String::as_str)
116            .zip(row_values(&self.storage, self.row).unwrap_or_default())
117    }
118
119    pub fn keys(&self) -> impl ExactSizeIterator<Item = &str> {
120        self.storage.schema.columns().iter().map(String::as_str)
121    }
122
123    pub fn values(&self) -> impl ExactSizeIterator<Item = &Value> {
124        row_values(&self.storage, self.row)
125            .unwrap_or_default()
126            .iter()
127    }
128
129    pub fn as_ref(&self) -> QueryRowRef<'_> {
130        QueryRowRef {
131            schema: &self.storage.schema,
132            values: row_values(&self.storage, self.row).unwrap_or_default(),
133        }
134    }
135
136    pub fn to_owned_row(&self) -> Row {
137        self.iter()
138            .map(|(name, value)| (name.to_owned(), value.clone()))
139            .collect()
140    }
141}
142
143impl std::fmt::Debug for QueryRow {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        f.debug_map().entries(self.iter()).finish()
146    }
147}
148
149impl PartialEq for QueryRow {
150    fn eq(&self, other: &Self) -> bool {
151        self.schema() == other.schema()
152            && row_values(&self.storage, self.row) == row_values(&other.storage, other.row)
153    }
154}
155
156impl Eq for QueryRow {}
157
158impl PartialEq<Row> for QueryRow {
159    fn eq(&self, other: &Row) -> bool {
160        row_equals_map(self.as_ref(), other)
161    }
162}
163
164impl PartialEq<QueryRow> for Row {
165    fn eq(&self, other: &QueryRow) -> bool {
166        other == self
167    }
168}
169
170impl Index<&str> for QueryRow {
171    type Output = Value;
172
173    fn index(&self, column: &str) -> &Self::Output {
174        self.get(column)
175            .unwrap_or_else(|| panic!("query result has no column {column}"))
176    }
177}
178
179/// A borrowed row view over a query's immutable flat value buffer.
180#[derive(Debug, Clone, Copy)]
181pub struct QueryRowRef<'a> {
182    schema: &'a QuerySchema,
183    values: &'a [Value],
184}
185
186impl<'a> QueryRowRef<'a> {
187    pub fn schema(self) -> &'a QuerySchema {
188        self.schema
189    }
190
191    pub fn len(self) -> usize {
192        self.values.len()
193    }
194
195    pub fn is_empty(self) -> bool {
196        self.values.is_empty()
197    }
198
199    pub fn value(self, column: usize) -> Option<&'a Value> {
200        self.values.get(column)
201    }
202
203    pub fn get(self, column: &str) -> Option<&'a Value> {
204        self.value(self.schema.position(column)?)
205    }
206
207    pub fn contains_key(self, column: &str) -> bool {
208        self.schema.position(column).is_some()
209    }
210
211    pub fn iter(self) -> impl ExactSizeIterator<Item = (&'a str, &'a Value)> {
212        self.schema
213            .columns()
214            .iter()
215            .map(String::as_str)
216            .zip(self.values)
217    }
218
219    pub fn keys(self) -> impl ExactSizeIterator<Item = &'a str> {
220        self.schema.columns().iter().map(String::as_str)
221    }
222
223    pub fn values(self) -> impl ExactSizeIterator<Item = &'a Value> {
224        self.values.iter()
225    }
226
227    pub fn to_owned_row(self) -> Row {
228        self.iter()
229            .map(|(name, value)| (name.to_owned(), value.clone()))
230            .collect()
231    }
232}
233
234impl PartialEq<Row> for QueryRowRef<'_> {
235    fn eq(&self, other: &Row) -> bool {
236        row_equals_map(*self, other)
237    }
238}
239
240impl PartialEq<QueryRowRef<'_>> for Row {
241    fn eq(&self, other: &QueryRowRef<'_>) -> bool {
242        other == self
243    }
244}
245
246impl Index<&str> for QueryRowRef<'_> {
247    type Output = Value;
248
249    fn index(&self, column: &str) -> &Self::Output {
250        self.get(column)
251            .unwrap_or_else(|| panic!("query result has no column {column}"))
252    }
253}
254
255/// Schema-bearing result rows backed by one flat, immutable value buffer.
256pub struct QueryRows {
257    storage: Arc<QueryRowStorage>,
258    indexed_rows: OnceLock<Vec<QueryRow>>,
259}
260
261impl QueryRows {
262    pub fn empty() -> Self {
263        Self::from_flat_values_unchecked(QuerySchema::empty(), Vec::new(), 0)
264    }
265
266    fn from_flat_values_unchecked(
267        schema: QuerySchema,
268        values: Vec<Value>,
269        row_count: usize,
270    ) -> Self {
271        Self {
272            storage: Arc::new(QueryRowStorage {
273                schema,
274                values,
275                row_count,
276            }),
277            indexed_rows: OnceLock::new(),
278        }
279    }
280
281    pub fn schema(&self) -> &QuerySchema {
282        &self.storage.schema
283    }
284
285    pub fn value_rows(&self) -> QueryValueRows<'_> {
286        QueryValueRows {
287            rows: self,
288            next: 0,
289        }
290    }
291
292    pub fn len(&self) -> usize {
293        self.storage.row_count
294    }
295
296    pub fn is_empty(&self) -> bool {
297        self.storage.row_count == 0
298    }
299
300    pub fn value(&self, row: usize, column: usize) -> Option<ValueRef<'_>> {
301        row_values(&self.storage, row)?
302            .get(column)
303            .map(Value::as_ref)
304    }
305
306    pub fn get(&self, row: usize, column: &str) -> Option<ValueRef<'_>> {
307        self.value(row, self.schema().position(column)?)
308    }
309
310    pub fn into_rows(self) -> Vec<Row> {
311        (0..self.len())
312            .map(|row| {
313                self.row(row)
314                    .expect("row ordinal is in range")
315                    .to_owned_row()
316            })
317            .collect()
318    }
319
320    pub fn row(&self, row: usize) -> Option<QueryRowRef<'_>> {
321        if row >= self.len() {
322            return None;
323        }
324        Some(QueryRowRef {
325            schema: &self.storage.schema,
326            values: row_values(&self.storage, row)?,
327        })
328    }
329
330    pub fn first(&self) -> Option<QueryRowRef<'_>> {
331        self.row(0)
332    }
333
334    pub fn last(&self) -> Option<QueryRowRef<'_>> {
335        self.len().checked_sub(1).and_then(|row| self.row(row))
336    }
337
338    pub fn iter(&self) -> QueryRowsIter<'_> {
339        QueryRowsIter {
340            rows: self,
341            next: 0,
342        }
343    }
344
345    pub fn retain(&mut self, mut keep: impl FnMut(QueryRowRef<'_>) -> bool) {
346        let width = self.schema().len();
347        let mut values = Vec::with_capacity(self.storage.values.len());
348        let mut row_count = 0usize;
349        for row in self.iter() {
350            if keep(row) {
351                values.extend_from_slice(row.values);
352                row_count = row_count.saturating_add(1);
353            }
354        }
355        let retained_row_count = values.len().checked_div(width).unwrap_or(row_count);
356        self.storage = Arc::new(QueryRowStorage {
357            schema: self.schema().clone(),
358            values,
359            row_count: retained_row_count,
360        });
361        self.indexed_rows = OnceLock::new();
362    }
363
364    /// Returns stable owned row handles for APIs that require slice pattern
365    /// matching or indexing. No map or value is copied.
366    pub fn as_slice(&self) -> &[QueryRow] {
367        self.indexed_rows()
368    }
369
370    pub fn payload_bytes(&self) -> usize {
371        let names = self
372            .schema()
373            .columns()
374            .iter()
375            .fold(0usize, |total, name| total.saturating_add(name.len()));
376        self.storage.values.iter().fold(names, |total, value| {
377            total.saturating_add(value_payload_bytes(value))
378        })
379    }
380
381    fn indexed_rows(&self) -> &[QueryRow] {
382        self.indexed_rows.get_or_init(|| {
383            (0..self.len())
384                .map(|row| QueryRow {
385                    storage: Arc::clone(&self.storage),
386                    row,
387                })
388                .collect()
389        })
390    }
391}
392
393impl Default for QueryRows {
394    fn default() -> Self {
395        Self::empty()
396    }
397}
398
399impl From<Vec<Row>> for QueryRows {
400    fn from(rows: Vec<Row>) -> Self {
401        let columns = rows
402            .iter()
403            .flat_map(|row| row.keys().cloned())
404            .collect::<std::collections::BTreeSet<_>>()
405            .into_iter()
406            .collect::<Vec<_>>();
407        let schema = QuerySchema::try_new(columns).expect("BTreeSet columns are unique");
408        let mut builder = QueryRowsBuilder::with_schema(schema.clone(), rows.len());
409        for mut row in rows {
410            builder
411                .push_values(
412                    schema
413                        .columns()
414                        .iter()
415                        .map(|column| row.remove(column).unwrap_or(Value::Null)),
416                )
417                .expect("row normalization always matches the inferred schema");
418        }
419        builder.finish()
420    }
421}
422
423impl FromIterator<Row> for QueryRows {
424    fn from_iter<T: IntoIterator<Item = Row>>(iter: T) -> Self {
425        iter.into_iter().collect::<Vec<_>>().into()
426    }
427}
428
429/// Builds immutable query results without retaining one map or vector per row.
430///
431/// The first named row binds column names to stable ordinals. Later rows are
432/// consumed in the same deterministic `BTreeMap` order and their values move
433/// directly into one flat buffer.
434pub struct QueryRowsBuilder {
435    schema: Option<QuerySchema>,
436    values: Vec<Value>,
437    row_count: usize,
438    row_capacity_hint: usize,
439}
440
441impl QueryRowsBuilder {
442    pub fn new() -> Self {
443        Self::with_row_capacity(0)
444    }
445
446    pub fn with_row_capacity(row_capacity: usize) -> Self {
447        Self {
448            schema: None,
449            values: Vec::new(),
450            row_count: 0,
451            row_capacity_hint: row_capacity,
452        }
453    }
454
455    pub fn with_schema(schema: QuerySchema, row_capacity: usize) -> Self {
456        Self {
457            values: Vec::with_capacity(row_capacity.saturating_mul(schema.len())),
458            schema: Some(schema),
459            row_count: 0,
460            row_capacity_hint: row_capacity,
461        }
462    }
463
464    pub fn push_values(&mut self, values: impl IntoIterator<Item = Value>) -> Result<()> {
465        self.try_push_values(values.into_iter().map(Ok))
466    }
467
468    /// Appends one fallible row directly into the flat value buffer.
469    ///
470    /// A conversion error or width mismatch rolls the partial row back.
471    pub fn try_push_values(
472        &mut self,
473        values: impl IntoIterator<Item = Result<Value>>,
474    ) -> Result<()> {
475        let schema = self.schema.as_ref().ok_or_else(|| {
476            HawDBError::Execution(
477                "query result values require a schema before ordinal insertion".to_string(),
478            )
479        })?;
480        let start = self.values.len();
481        for value in values {
482            match value {
483                Ok(value) => self.values.push(value),
484                Err(error) => {
485                    self.values.truncate(start);
486                    return Err(error);
487                }
488            }
489        }
490        let width = self.values.len().saturating_sub(start);
491        if width != schema.len() {
492            self.values.truncate(start);
493            return Err(HawDBError::Execution(format!(
494                "query result row {} has width {width}, expected {}",
495                self.row_count,
496                schema.len()
497            )));
498        }
499        self.row_count = self.row_count.saturating_add(1);
500        Ok(())
501    }
502
503    pub fn push_named_row(&mut self, row: Row) -> Result<()> {
504        if self.schema.is_none() {
505            self.schema = Some(QuerySchema::try_new(row.keys().cloned())?);
506            self.values.reserve(
507                row.len()
508                    .saturating_mul(self.row_capacity_hint.max(self.row_count.saturating_add(1))),
509            );
510        }
511        let schema = self.schema.as_ref().expect("query result schema is bound");
512        if row.len() != schema.len() {
513            return Err(HawDBError::Execution(format!(
514                "query result row {} has width {}, expected {}",
515                self.row_count,
516                row.len(),
517                schema.len()
518            )));
519        }
520        if let Some((ordinal, (name, expected))) = row
521            .keys()
522            .zip(schema.columns())
523            .enumerate()
524            .find(|(_, (name, expected))| *name != *expected)
525        {
526            return Err(HawDBError::Execution(format!(
527                "query result row {} column {ordinal} is {name}, expected {expected}",
528                self.row_count
529            )));
530        }
531        self.values.extend(row.into_values());
532        self.row_count = self.row_count.saturating_add(1);
533        Ok(())
534    }
535
536    pub fn finish(self) -> QueryRows {
537        QueryRows::from_flat_values_unchecked(
538            self.schema.unwrap_or_else(QuerySchema::empty),
539            self.values,
540            self.row_count,
541        )
542    }
543}
544
545impl Default for QueryRowsBuilder {
546    fn default() -> Self {
547        Self::new()
548    }
549}
550
551impl Clone for QueryRows {
552    fn clone(&self) -> Self {
553        Self {
554            storage: Arc::clone(&self.storage),
555            indexed_rows: OnceLock::new(),
556        }
557    }
558}
559
560impl std::fmt::Debug for QueryRows {
561    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
562        f.debug_struct("QueryRows")
563            .field("schema", self.schema())
564            .field("values", &self.storage.values)
565            .finish()
566    }
567}
568
569impl PartialEq for QueryRows {
570    fn eq(&self, other: &Self) -> bool {
571        self.schema() == other.schema()
572            && self.len() == other.len()
573            && self.storage.values == other.storage.values
574    }
575}
576
577impl Eq for QueryRows {}
578
579impl PartialEq<Vec<Row>> for QueryRows {
580    fn eq(&self, other: &Vec<Row>) -> bool {
581        self.len() == other.len() && self.iter().zip(other).all(|(left, right)| left == *right)
582    }
583}
584
585impl PartialEq<QueryRows> for Vec<Row> {
586    fn eq(&self, other: &QueryRows) -> bool {
587        other == self
588    }
589}
590
591impl Index<usize> for QueryRows {
592    type Output = QueryRow;
593
594    fn index(&self, row: usize) -> &Self::Output {
595        &self.indexed_rows()[row]
596    }
597}
598
599impl<'a> IntoIterator for &'a QueryRows {
600    type Item = QueryRowRef<'a>;
601    type IntoIter = QueryRowsIter<'a>;
602
603    fn into_iter(self) -> Self::IntoIter {
604        self.iter()
605    }
606}
607
608impl IntoIterator for QueryRows {
609    type Item = QueryRow;
610    type IntoIter = QueryRowsIntoIter;
611
612    fn into_iter(self) -> Self::IntoIter {
613        QueryRowsIntoIter {
614            storage: self.storage,
615            next: 0,
616        }
617    }
618}
619
620pub struct QueryRowsIter<'a> {
621    rows: &'a QueryRows,
622    next: usize,
623}
624
625impl<'a> Iterator for QueryRowsIter<'a> {
626    type Item = QueryRowRef<'a>;
627
628    fn next(&mut self) -> Option<Self::Item> {
629        let row = self.rows.row(self.next)?;
630        self.next = self.next.saturating_add(1);
631        Some(row)
632    }
633
634    fn size_hint(&self) -> (usize, Option<usize>) {
635        let remaining = self.rows.len().saturating_sub(self.next);
636        (remaining, Some(remaining))
637    }
638}
639
640impl ExactSizeIterator for QueryRowsIter<'_> {}
641
642pub struct QueryValueRows<'a> {
643    rows: &'a QueryRows,
644    next: usize,
645}
646
647impl QueryValueRows<'_> {
648    pub fn is_empty(&self) -> bool {
649        self.len() == 0
650    }
651}
652
653impl<'a> Iterator for QueryValueRows<'a> {
654    type Item = &'a [Value];
655
656    fn next(&mut self) -> Option<Self::Item> {
657        let values = self.rows.row(self.next)?.values;
658        self.next = self.next.saturating_add(1);
659        Some(values)
660    }
661
662    fn size_hint(&self) -> (usize, Option<usize>) {
663        let remaining = self.rows.len().saturating_sub(self.next);
664        (remaining, Some(remaining))
665    }
666}
667
668impl ExactSizeIterator for QueryValueRows<'_> {}
669
670pub struct QueryRowsIntoIter {
671    storage: Arc<QueryRowStorage>,
672    next: usize,
673}
674
675impl Iterator for QueryRowsIntoIter {
676    type Item = QueryRow;
677
678    fn next(&mut self) -> Option<Self::Item> {
679        if self.next >= self.storage.row_count {
680            return None;
681        }
682        let row = QueryRow {
683            storage: Arc::clone(&self.storage),
684            row: self.next,
685        };
686        self.next = self.next.saturating_add(1);
687        Some(row)
688    }
689
690    fn size_hint(&self) -> (usize, Option<usize>) {
691        let remaining = self.storage.row_count.saturating_sub(self.next);
692        (remaining, Some(remaining))
693    }
694}
695
696impl ExactSizeIterator for QueryRowsIntoIter {}
697
698fn row_values(storage: &QueryRowStorage, row: usize) -> Option<&[Value]> {
699    if row >= storage.row_count {
700        return None;
701    }
702    let width = storage.schema.len();
703    let start = row.checked_mul(width)?;
704    let end = start.checked_add(width)?;
705    storage.values.get(start..end)
706}
707
708fn row_equals_map(row: QueryRowRef<'_>, other: &Row) -> bool {
709    row.len() == other.len()
710        && row
711            .iter()
712            .all(|(name, value)| other.get(name).is_some_and(|other| other == value))
713}
714
715/// A row view whose values remain valid only for the current consumer call.
716///
717/// The map representation covers the scalar fallback executor. The columnar
718/// representation lets a downstream consumer pull selected values directly
719/// from an immutable batch without constructing an intermediate row map.
720#[derive(Debug, Clone, Copy)]
721pub enum RowRef<'a> {
722    Map(&'a Row),
723    Columnar(ColumnarRowRef<'a>),
724}
725
726impl<'a> RowRef<'a> {
727    pub fn len(self) -> usize {
728        match self {
729            Self::Map(row) => row.len(),
730            Self::Columnar(_) => self.iter().count(),
731        }
732    }
733
734    pub fn is_empty(self) -> bool {
735        self.len() == 0
736    }
737
738    pub fn get(self, name: &str) -> Option<ValueRef<'a>> {
739        match self {
740            Self::Map(row) => row.get(name).map(Value::as_ref),
741            Self::Columnar(row) => row.get(name),
742        }
743    }
744
745    pub fn column(self, index: usize) -> Option<(&'a str, ValueRef<'a>)> {
746        match self {
747            Self::Map(row) => row
748                .iter()
749                .nth(index)
750                .map(|(name, value)| (name.as_str(), value.as_ref())),
751            Self::Columnar(row) => row.column(index),
752        }
753    }
754
755    pub fn iter(self) -> RowRefIter<'a> {
756        match self {
757            Self::Map(row) => RowRefIter::Map(row.iter()),
758            Self::Columnar(row) => RowRefIter::Columnar { row, index: 0 },
759        }
760    }
761
762    pub fn to_owned_row(self) -> Row {
763        self.iter()
764            .map(|(name, value)| (name.to_owned(), value.to_owned_value()))
765            .collect()
766    }
767}
768
769impl<'a> From<&'a Row> for RowRef<'a> {
770    fn from(row: &'a Row) -> Self {
771        Self::Map(row)
772    }
773}
774
775impl<'a> From<ColumnarRowRef<'a>> for RowRef<'a> {
776    fn from(row: ColumnarRowRef<'a>) -> Self {
777        Self::Columnar(row)
778    }
779}
780
781pub enum RowRefIter<'a> {
782    Map(std::collections::btree_map::Iter<'a, String, Value>),
783    Columnar {
784        row: ColumnarRowRef<'a>,
785        index: usize,
786    },
787}
788
789impl<'a> Iterator for RowRefIter<'a> {
790    type Item = (&'a str, ValueRef<'a>);
791
792    fn next(&mut self) -> Option<Self::Item> {
793        match self {
794            Self::Map(iter) => iter
795                .next()
796                .map(|(name, value)| (name.as_str(), value.as_ref())),
797            Self::Columnar { row, index } => loop {
798                let current = *index;
799                *index = index.saturating_add(1);
800                if current >= row.schema().len() {
801                    return None;
802                }
803                if let Some(column) = row.column(current) {
804                    return Some(column);
805                }
806            },
807        }
808    }
809}
810
811#[derive(Debug, Clone, PartialEq, Eq)]
812pub struct BlockingOperatorMemoryReport {
813    pub operator: String,
814    pub budget_bytes: usize,
815    pub peak_tracked_bytes: usize,
816    pub input_rows: usize,
817    /// Equal-hash candidate pairs examined before domain-specific filtering.
818    pub candidate_rows: usize,
819    /// Records decoded from spill runs, including repartition and hot-key replay.
820    pub replay_rows: usize,
821    /// Successful spill-run partition splits.
822    pub repartitions: usize,
823    pub max_spill_bytes: u64,
824    pub max_spill_runs: usize,
825    pub spilled_bytes: u64,
826    pub spill_run_count: usize,
827    pub spilled_rows: usize,
828}
829
830#[derive(Debug, Clone, PartialEq, Eq, Default)]
831pub struct PipelineMemoryReport {
832    /// Query-owned runtime ledger budget across pipeline and blocking state.
833    pub query_memory_budget_bytes: usize,
834    /// Highest aggregate tracked resident bytes charged to the query ledger.
835    pub query_memory_peak_bytes: usize,
836    /// Tracked bytes still owned at the execution completion boundary.
837    pub query_memory_completion_bytes: usize,
838    /// Number of operator or transfer accounts created by the query.
839    pub query_memory_account_count: usize,
840    /// Sum of rows emitted at every physical operator boundary.
841    pub intermediate_rows: usize,
842    /// Sum of retained payload estimates emitted at every physical operator boundary.
843    pub intermediate_payload_bytes: usize,
844    pub peak_batch_rows: usize,
845    pub peak_batch_payload_bytes: usize,
846    /// Number of typed columnar batches evaluated by eligible pipeline fragments.
847    pub columnar_batches: usize,
848    /// Rows loaded into typed columns before selection.
849    pub columnar_input_rows: usize,
850    /// Rows retained by columnar selections.
851    pub columnar_selected_rows: usize,
852    /// Morsels consumed by columnar fragments.
853    pub morsel_count: usize,
854    /// Highest resource-admitted worker count across morsel pipelines.
855    pub morsel_max_admitted_workers: usize,
856    /// Highest worker count actually used by a morsel scheduler.
857    pub morsel_peak_active_workers: usize,
858    /// Highest number of completed morsel outputs awaiting or crossing the
859    /// coordinator's ordered consumer boundary.
860    pub morsel_peak_buffered_outputs: usize,
861    /// Highest estimated resident bytes held by those completed outputs.
862    pub morsel_peak_buffered_output_bytes: usize,
863    /// Highest number of out-of-order outputs held by the ordinal merger.
864    pub morsel_peak_reorder_entries: usize,
865    pub output_rows: usize,
866    pub output_payload_bytes: usize,
867    /// Resident memory before execution, when process sampling is supported.
868    pub start_resident_bytes: Option<u64>,
869    /// Process high-water resident memory before execution.
870    pub start_peak_resident_bytes: Option<u64>,
871    /// Resident memory after the output rows have been materialized.
872    pub steady_resident_bytes: Option<u64>,
873    /// Process high-water resident memory at completion.
874    pub peak_resident_bytes: Option<u64>,
875    /// Positive resident-memory delta retained at completion.
876    pub steady_resident_growth_bytes: Option<u64>,
877    /// Positive process high-water delta observed during execution.
878    pub lifetime_peak_resident_growth_bytes: Option<u64>,
879    /// Aggregate page-fault delta when the platform exposes it.
880    pub total_page_faults: Option<u64>,
881    /// Split page-fault deltas only on platforms that expose this distinction.
882    pub minor_page_faults: Option<u64>,
883    pub major_page_faults: Option<u64>,
884}
885
886/// The observed output cardinality for one physical operator.
887#[derive(Debug, Clone, PartialEq, Eq)]
888pub struct OperatorCardinalityProfile {
889    pub operator_id: PhysicalOperatorId,
890    pub operator: PhysicalPlanKind,
891    /// `None` means the operator was not invoked, while `Some(0)` means it ran
892    /// and produced no output rows.
893    pub actual_rows: Option<usize>,
894}
895
896#[derive(Debug, Clone, PartialEq, Eq)]
897pub struct ReadExecutionProfile<TScanPruningReport> {
898    pub max_rows: Option<usize>,
899    pub detection_row_cap: Option<usize>,
900    pub row_limit_enforced_before_output: bool,
901    pub operator_row_cap_enabled: bool,
902    pub operator_cardinality_profiles: Vec<OperatorCardinalityProfile>,
903    pub blocking_operator_kinds: Vec<String>,
904    pub scan_pruning_reports: Vec<TScanPruningReport>,
905    pub vector_execution_reports: Vec<crate::VectorExecutionReport>,
906    pub graph_expansion_reports: Vec<crate::GraphExpansionExecutionReport>,
907    pub blocking_operator_memory_reports: Vec<BlockingOperatorMemoryReport>,
908    pub pipeline_memory_report: PipelineMemoryReport,
909}
910
911impl<TScanPruningReport> ReadExecutionProfile<TScanPruningReport> {
912    pub fn blocking_operator_count(&self) -> usize {
913        self.blocking_operator_kinds.len()
914    }
915}
916
917#[derive(Debug, Clone, PartialEq, Eq)]
918pub struct ProfiledQueryRows<TScanPruningReport> {
919    pub rows: QueryRows,
920    pub profile: ReadExecutionProfile<TScanPruningReport>,
921}
922
923#[derive(Debug, Clone, PartialEq, Eq)]
924pub struct ProfiledQueryStream<TScanPruningReport> {
925    /// True when the physical plan emitted batches directly to the consumer.
926    /// False means an unsupported operator still materialized bindings before
927    /// the bounded consumer boundary.
928    pub fully_streamed: bool,
929    pub profile: ReadExecutionProfile<TScanPruningReport>,
930}
931
932#[cfg(test)]
933mod tests {
934    use super::*;
935    use crate::{
936        BindingSchema, ColumnVector, ColumnarBatch, SlotDescriptor, SlotId, SlotType, Validity,
937    };
938    use hawdb_core::LogicalType;
939    use std::sync::Arc;
940
941    #[test]
942    fn counts_blocking_operator_kinds() {
943        let profile = ReadExecutionProfile::<()> {
944            max_rows: Some(10),
945            detection_row_cap: Some(11),
946            row_limit_enforced_before_output: true,
947            operator_row_cap_enabled: true,
948            operator_cardinality_profiles: Vec::new(),
949            blocking_operator_kinds: vec!["sort".to_string(), "aggregate".to_string()],
950            scan_pruning_reports: Vec::new(),
951            vector_execution_reports: Vec::new(),
952            graph_expansion_reports: Vec::new(),
953            blocking_operator_memory_reports: Vec::new(),
954            pipeline_memory_report: PipelineMemoryReport::default(),
955        };
956        assert_eq!(profile.blocking_operator_count(), 2);
957    }
958
959    #[test]
960    fn borrowed_map_row_materializes_only_on_request() {
961        let row = Row::from([
962            ("id".to_string(), Value::Int(7)),
963            ("content".to_string(), Value::String("payload".into())),
964        ]);
965        let row_ref = RowRef::from(&row);
966
967        assert_eq!(row_ref.get("id"), Some(ValueRef::Int(7)));
968        assert_eq!(row_ref.get("content").unwrap().as_str(), Some("payload"));
969        assert_eq!(row_ref.to_owned_row(), row);
970    }
971
972    #[test]
973    fn schema_bearing_rows_keep_names_once_in_a_flat_value_buffer() {
974        let schema = QuerySchema::try_new(["payload".to_string(), "id".to_string()]).unwrap();
975        let mut builder = QueryRowsBuilder::with_schema(schema, 2);
976        builder
977            .push_values([Value::String("one".to_string()), Value::Int(1)])
978            .unwrap();
979        builder
980            .push_values([Value::String("two".to_string()), Value::Int(2)])
981            .unwrap();
982        let rows = builder.finish();
983
984        assert_eq!(rows.schema().columns(), ["payload", "id"]);
985        assert_eq!(rows.get(1, "payload").unwrap().as_str(), Some("two"));
986        assert_eq!(rows.value(0, 1), Some(ValueRef::Int(1)));
987        assert_eq!(rows.payload_bytes(), "payload".len() + "id".len() + 6 + 16);
988        assert_eq!(rows.len(), 2);
989        assert!(!rows.is_empty());
990        assert_eq!(rows.storage.values.len(), 4);
991        assert_eq!(rows[0]["payload"], Value::String("one".to_string()));
992        assert_eq!(rows.iter().count(), 2);
993        assert_eq!(rows.storage.values.len(), 4);
994    }
995
996    #[test]
997    fn schema_bearing_rows_reject_duplicate_columns_and_width_mismatch() {
998        assert!(QuerySchema::try_new(["id".to_string(), "id".to_string()]).is_err());
999        let schema = QuerySchema::try_new(["id".to_string()]).unwrap();
1000        let mut invalid = QueryRowsBuilder::with_schema(schema, 1);
1001        assert!(invalid.push_values([]).is_err());
1002
1003        let mut left = QueryRowsBuilder::with_schema(
1004            QuerySchema::try_new(["left".to_string(), "right".to_string()]).unwrap(),
1005            1,
1006        );
1007        left.push_values([Value::Int(1), Value::Int(2)]).unwrap();
1008        let left = left.finish();
1009        let mut right = QueryRowsBuilder::with_schema(
1010            QuerySchema::try_new(["right".to_string(), "left".to_string()]).unwrap(),
1011            1,
1012        );
1013        right.push_values([Value::Int(1), Value::Int(2)]).unwrap();
1014        let right = right.finish();
1015        assert_ne!(left, right);
1016    }
1017
1018    #[test]
1019    fn fallible_flat_row_append_rolls_back_partial_values() {
1020        let schema = QuerySchema::try_new(["left".to_string(), "right".to_string()]).unwrap();
1021        let mut builder = QueryRowsBuilder::with_schema(schema, 1);
1022        assert!(builder
1023            .try_push_values([
1024                Ok(Value::Int(1)),
1025                Err(HawDBError::Execution("projection failed".to_string())),
1026            ])
1027            .is_err());
1028        builder.push_values([Value::Int(2), Value::Int(3)]).unwrap();
1029        let rows = builder.finish();
1030
1031        assert_eq!(rows.len(), 1);
1032        assert_eq!(rows.value(0, 0), Some(ValueRef::Int(2)));
1033        assert_eq!(rows.value(0, 1), Some(ValueRef::Int(3)));
1034        assert_eq!(rows.storage.values.len(), 2);
1035    }
1036
1037    #[test]
1038    fn named_row_builder_binds_schema_once_and_moves_values_by_ordinal() {
1039        let mut builder = QueryRowsBuilder::with_row_capacity(2);
1040        builder
1041            .push_named_row(Row::from([
1042                ("id".to_string(), Value::Int(1)),
1043                ("payload".to_string(), Value::String("one".to_string())),
1044            ]))
1045            .unwrap();
1046        builder
1047            .push_named_row(Row::from([
1048                ("id".to_string(), Value::Int(2)),
1049                ("payload".to_string(), Value::String("two".to_string())),
1050            ]))
1051            .unwrap();
1052        let rows = builder.finish();
1053
1054        assert_eq!(rows.schema().columns(), ["id", "payload"]);
1055        assert_eq!(rows.value(1, 0), Some(ValueRef::Int(2)));
1056        assert_eq!(rows.get(0, "payload").unwrap().as_str(), Some("one"));
1057        assert_eq!(rows.storage.values.len(), 4);
1058    }
1059
1060    #[test]
1061    fn named_row_builder_rejects_schema_drift_without_partial_append() {
1062        let mut builder = QueryRowsBuilder::new();
1063        builder
1064            .push_named_row(Row::from([("id".to_string(), Value::Int(1))]))
1065            .unwrap();
1066        assert!(builder
1067            .push_named_row(Row::from([("name".to_string(), Value::Int(2))]))
1068            .is_err());
1069        let rows = builder.finish();
1070
1071        assert_eq!(rows.len(), 1);
1072        assert_eq!(rows.get(0, "id"), Some(ValueRef::Int(1)));
1073    }
1074
1075    #[test]
1076    fn borrowed_columnar_row_pulls_values_without_a_row_map() {
1077        let schema = Arc::new(
1078            BindingSchema::try_new(vec![
1079                SlotDescriptor {
1080                    id: SlotId(0),
1081                    name: "id".into(),
1082                    slot_type: SlotType::NodeId,
1083                },
1084                SlotDescriptor {
1085                    id: SlotId(1),
1086                    name: "content".into(),
1087                    slot_type: SlotType::logical(LogicalType::Text),
1088                },
1089            ])
1090            .unwrap(),
1091        );
1092        let batch = ColumnarBatch::try_new(
1093            schema,
1094            vec![
1095                Arc::new(ColumnVector::node_ids(vec![7])),
1096                Arc::new(ColumnVector::Utf8 {
1097                    values: vec!["payload".to_string()].into(),
1098                    validity: Validity::all(1),
1099                }),
1100            ],
1101        )
1102        .unwrap();
1103        let row_ref = RowRef::from(batch.rows().next().unwrap());
1104
1105        assert_eq!(row_ref.get("id"), Some(ValueRef::Int(7)));
1106        assert_eq!(row_ref.get("content").unwrap().as_str(), Some("payload"));
1107        assert_eq!(
1108            row_ref.to_owned_row(),
1109            Row::from([
1110                ("content".into(), Value::String("payload".into())),
1111                ("id".into(), Value::Int(7)),
1112            ])
1113        );
1114    }
1115}