1use 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#[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#[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#[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
255pub 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 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
429pub 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 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#[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 pub candidate_rows: usize,
819 pub replay_rows: usize,
821 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 pub query_memory_budget_bytes: usize,
834 pub query_memory_peak_bytes: usize,
836 pub query_memory_completion_bytes: usize,
838 pub query_memory_account_count: usize,
840 pub intermediate_rows: usize,
842 pub intermediate_payload_bytes: usize,
844 pub peak_batch_rows: usize,
845 pub peak_batch_payload_bytes: usize,
846 pub columnar_batches: usize,
848 pub columnar_input_rows: usize,
850 pub columnar_selected_rows: usize,
852 pub morsel_count: usize,
854 pub morsel_max_admitted_workers: usize,
856 pub morsel_peak_active_workers: usize,
858 pub morsel_peak_buffered_outputs: usize,
861 pub morsel_peak_buffered_output_bytes: usize,
863 pub morsel_peak_reorder_entries: usize,
865 pub output_rows: usize,
866 pub output_payload_bytes: usize,
867 pub start_resident_bytes: Option<u64>,
869 pub start_peak_resident_bytes: Option<u64>,
871 pub steady_resident_bytes: Option<u64>,
873 pub peak_resident_bytes: Option<u64>,
875 pub steady_resident_growth_bytes: Option<u64>,
877 pub lifetime_peak_resident_growth_bytes: Option<u64>,
879 pub total_page_faults: Option<u64>,
881 pub minor_page_faults: Option<u64>,
883 pub major_page_faults: Option<u64>,
884}
885
886#[derive(Debug, Clone, PartialEq, Eq)]
888pub struct OperatorCardinalityProfile {
889 pub operator_id: PhysicalOperatorId,
890 pub operator: PhysicalPlanKind,
891 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 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}