1mod aggregate;
10mod display;
11mod expand;
12mod group;
13mod tree;
14
15use std::{
16 cmp::Ordering,
17 panic,
18 sync::{
19 Arc, OnceLock,
20 atomic::{AtomicBool, AtomicUsize, Ordering as AtomicOrdering},
21 },
22 thread,
23};
24
25use rustc_hash::{FxHashMap, FxHashSet};
26use serde::{Deserialize, Serialize};
27
28pub use self::{
29 aggregate::{Aggregate, AggregateFn},
30 display::{
31 AggregateRow, DisplayRow, ExpandDepth, Expansion, GroupId, GroupInfo, GroupPath, GroupTable,
32 },
33 group::{
34 AggregateOrder, EveryGroupFooter, GroupBy, GroupFooter, GroupKey, GroupOrder,
35 GroupOrderContext, KeyOrder, NoGroupFooter,
36 },
37 tree::{
38 Branch, TreeFilter, TreeFilterContext, TreeFilterRule, TreeSpec, TreeTotals,
39 TreeTotalsContext, TreeTotalsRule,
40 },
41};
42use self::{
43 display::{AggregateValues, Lines, SummaryStamps},
44 group::GroupSpec,
45};
46use crate::{
47 model::{CellValue, ColumnSet, GridColumns, GridRow},
48 quick_filter::{QuickFilter, QuickFilterPass},
49 store::{RowId, StoreSnapshot},
50};
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(rename_all = "lowercase")]
56pub enum SortDirection {
57 Ascending,
59 Descending,
61}
62
63type Filter<R> = Arc<dyn Fn(&R) -> bool + Send + Sync>;
64
65pub struct ViewSpec<R: GridRow> {
70 sort: Vec<(R::Column, SortDirection)>,
71 filter: Option<(ColumnSet, Filter<R>)>,
72 quick_filter: Option<QuickFilter>,
73 structure: Structure<R>,
76 group_order: Option<Arc<dyn GroupOrder<R>>>,
78 group_footer: Option<Arc<dyn GroupFooter>>,
80 aggregates: Option<Arc<[(R::Column, Aggregate<R>)]>>,
83 expansion: Expansion<R::Key>,
85}
86
87pub(crate) enum Structure<R: GridRow> {
89 Flat,
91 Groups(GroupSpec<R>),
93 Tree(TreeSpec<R>),
95}
96
97impl<R: GridRow> Clone for Structure<R> {
98 fn clone(&self) -> Self {
99 match self {
100 Self::Flat => Self::Flat,
101 Self::Groups(groups) => Self::Groups(groups.clone()),
102 Self::Tree(tree) => Self::Tree(tree.clone()),
103 }
104 }
105}
106
107impl<R: GridRow> Clone for ViewSpec<R> {
108 fn clone(&self) -> Self {
109 Self {
110 sort: self.sort.clone(),
111 filter: self.filter.clone(),
112 quick_filter: self.quick_filter.clone(),
113 structure: self.structure.clone(),
114 group_order: self.group_order.clone(),
115 group_footer: self.group_footer.clone(),
116 aggregates: self.aggregates.clone(),
117 expansion: self.expansion.clone(),
118 }
119 }
120}
121
122impl<R: GridRow> Default for ViewSpec<R> {
123 fn default() -> Self {
124 Self {
125 sort: Vec::new(),
126 filter: None,
127 quick_filter: None,
128 structure: Structure::Flat,
129 group_order: None,
130 group_footer: None,
131 aggregates: None,
132 expansion: Expansion::default(),
133 }
134 }
135}
136
137impl<R: GridRow> ViewSpec<R> {
138 pub fn new() -> Self {
140 Self::default()
141 }
142
143 pub fn with_sort(mut self, sort: impl IntoIterator<Item = (R::Column, SortDirection)>) -> Self {
146 self.sort = sort.into_iter().collect();
147 self
148 }
149
150 pub fn with_filter(
155 mut self,
156 depends_on: ColumnSet,
157 filter: impl Fn(&R) -> bool + Send + Sync + 'static,
158 ) -> Self {
159 self.filter = Some((depends_on, Arc::new(filter)));
160 self
161 }
162
163 pub fn with_quick_filter(mut self, filter: Option<QuickFilter>) -> Self {
167 self.quick_filter = filter;
168 self
169 }
170
171 pub fn sort(&self) -> &[(R::Column, SortDirection)] {
173 &self.sort
174 }
175
176 pub fn quick_filter(&self) -> Option<&QuickFilter> {
178 self.quick_filter.as_ref()
179 }
180
181 pub fn dependencies(&self) -> ColumnSet {
191 let mut columns = ColumnSet::EMPTY;
192 for (column, _) in &self.sort {
193 columns.insert(column.ix());
194 }
195 if let Some((depends_on, _)) = &self.filter {
196 columns.union_with(*depends_on);
197 }
198 if let Some(quick_filter) = &self.quick_filter {
199 columns.union_with(quick_filter.columns());
200 }
201 if let Structure::Groups(groups) = &self.structure {
202 for level in groups.levels() {
203 match (level.as_column(), level.as_key()) {
204 (Some(column), _) => columns.insert(column.ix()),
205 (None, Some(key)) => columns.union_with(key.depends_on()),
206 (None, None) => {}
207 }
208 }
209 let order_columns = self.group_order().depends_on();
211 columns.union_with(order_columns);
212 for (column, aggregate) in self.aggregates() {
213 if order_columns.contains(column.ix()) {
214 columns.union_with(aggregate.depends_on(*column));
215 }
216 }
217 }
218 columns
219 }
220
221 pub(crate) fn structure(&self) -> &Structure<R> {
223 &self.structure
224 }
225}
226
227#[derive(Debug, Default)]
236pub struct Projection {
237 store_version: u64,
238 rows: Vec<RowId>,
241 row_to_order: Vec<u32>,
245 shared_order: FxHashMap<RowId, u32>,
249 row_count: usize,
251 lines: Lines,
253 totals: AggregateValues,
255 summary_stamps: Option<Box<SummaryStamps>>,
258}
259
260impl Projection {
261 fn new<R: GridRow>(snapshot: &StoreSnapshot<R>, rows: Vec<RowId>) -> Self {
264 let (row_to_order, shared_order) =
265 places(rows.iter().copied().enumerate(), snapshot.capacity());
266 Self {
267 store_version: snapshot.version(),
268 row_count: rows.len(),
269 rows,
270 row_to_order,
271 shared_order,
272 lines: Lines::Flat,
273 totals: AggregateValues::default(),
274 summary_stamps: None,
275 }
276 }
277
278 pub(crate) fn duplicate(&self) -> Self {
281 Self {
282 store_version: self.store_version,
283 rows: self.rows.clone(),
284 row_to_order: self.row_to_order.clone(),
285 shared_order: self.shared_order.clone(),
286 row_count: self.row_count,
287 lines: self.lines.clone(),
288 totals: self.totals.clone(),
289 summary_stamps: self.summary_stamps.clone(),
290 }
291 }
292
293 pub fn store_version(&self) -> u64 {
295 self.store_version
296 }
297
298 pub fn rows(&self) -> &[RowId] {
302 &self.rows
303 }
304
305 pub fn contains(&self, id: RowId) -> bool {
309 self.order_ix(id).is_some()
310 }
311}
312
313pub trait ProjectionEngine<R: GridRow>: Send + Sync + 'static {
316 fn rebuild(&self, snapshot: &StoreSnapshot<R>, spec: &ViewSpec<R>) -> Projection;
318
319 fn compact(&self, previous: &Projection, snapshot: &StoreSnapshot<R>) -> Projection {
326 if previous.is_tree() {
327 tree::compact(previous, snapshot)
328 } else {
329 display::compact(previous, snapshot)
330 }
331 }
332
333 fn aggregate(
338 &self,
339 previous: &Projection,
340 snapshot: &StoreSnapshot<R>,
341 spec: &ViewSpec<R>,
342 ) -> Projection {
343 if previous.is_tree() {
344 tree::refresh(previous, snapshot, spec)
345 } else {
346 aggregate::refresh(previous, snapshot, spec)
347 }
348 }
349
350 fn expand(
353 &self,
354 previous: &Projection,
355 snapshot: &StoreSnapshot<R>,
356 spec: &ViewSpec<R>,
357 ) -> Projection {
358 if previous.is_tree() {
359 tree::relayout(previous, snapshot, spec)
360 } else {
361 expand::relayout(previous, snapshot, spec)
362 }
363 }
364}
365
366#[derive(Clone, Copy, Debug, Default)]
376pub struct Rebuild {
377 max_threads: Option<usize>,
379}
380
381impl Rebuild {
382 pub fn with_max_threads(mut self, threads: usize) -> Self {
392 self.max_threads = Some(threads.max(1));
393 self
394 }
395
396 pub fn max_threads(&self) -> usize {
400 if cfg!(target_family = "wasm") {
401 1
403 } else {
404 self.max_threads.unwrap_or_else(default_thread_budget)
405 }
406 }
407
408 fn rebuild_flat<R: GridRow>(
412 &self,
413 snapshot: &StoreSnapshot<R>,
414 spec: &ViewSpec<R>,
415 ) -> Projection {
416 let parallelism = Parallelism::for_view(snapshot.len(), spec, self.max_threads());
417 let (rows, fading) = project(snapshot, spec, parallelism);
418 let live = rows.len() - fading;
419 let projection = Projection::new(snapshot, rows).with_row_count(live);
420 let aggregates = aggregate::ViewAggregates::new(spec);
421 if aggregates.is_empty() {
422 return projection;
423 }
424 let (totals, _) = aggregate::flat_totals(
425 &aggregates,
426 projection
427 .rows
428 .iter()
429 .filter_map(|id| snapshot.row(*id).map(|row| &**row)),
430 );
431 projection.with_totals(AggregateValues::new(aggregates.columns(), totals))
432 }
433}
434
435fn directed(ordering: Ordering, direction: SortDirection) -> Ordering {
436 match direction {
437 SortDirection::Ascending => ordering,
438 SortDirection::Descending => ordering.reverse(),
439 }
440}
441
442impl<R: GridRow> ProjectionEngine<R> for Rebuild {
443 fn rebuild(&self, snapshot: &StoreSnapshot<R>, spec: &ViewSpec<R>) -> Projection {
444 match spec.structure() {
445 Structure::Flat => self.rebuild_flat(snapshot, spec),
446 Structure::Groups(groups) => group::rebuild(self, snapshot, spec, groups),
447 Structure::Tree(tree) => tree::rebuild(self, snapshot, spec, tree),
448 }
449 }
450}
451
452const MIN_ROWS_PER_THREAD: usize = 8 * 1024;
454const CHUNK_ROWS: usize = 1024;
457const RESERVED_CORES: usize = 2;
459const MAX_THREADS: usize = 8;
462
463#[derive(Clone, Copy, Debug)]
465struct Parallelism {
466 threads: usize,
468 chunk_rows: usize,
470}
471
472impl Parallelism {
473 fn for_view<R: GridRow>(rows: usize, spec: &ViewSpec<R>, budget: usize) -> Self {
477 let has_work =
478 !spec.sort.is_empty() || spec.filter.is_some() || spec.quick_filter.is_some();
479 if has_work {
480 Self::for_rows(rows, budget)
481 } else {
482 Self {
483 threads: 1,
484 chunk_rows: CHUNK_ROWS,
485 }
486 }
487 }
488
489 fn for_rows(rows: usize, budget: usize) -> Self {
492 Self {
493 threads: budget.min(rows / MIN_ROWS_PER_THREAD).max(1),
494 chunk_rows: CHUNK_ROWS,
495 }
496 }
497}
498
499fn default_thread_budget() -> usize {
502 static BUDGET: OnceLock<usize> = OnceLock::new();
503 *BUDGET.get_or_init(|| {
504 thread::available_parallelism().map_or(1, |cores| {
505 cores
506 .get()
507 .saturating_sub(RESERVED_CORES)
508 .clamp(1, MAX_THREADS)
509 })
510 })
511}
512
513fn project<R: GridRow>(
516 snapshot: &StoreSnapshot<R>,
517 spec: &ViewSpec<R>,
518 parallelism: Parallelism,
519) -> (Vec<RowId>, usize) {
520 match spec.sort.split_first() {
521 None => sorted_rows(
523 snapshot,
524 spec,
525 parallelism,
526 |id, _, seq| (seq, id),
527 |a, b| a.0.cmp(&b.0),
528 |&(_, id)| id,
529 ),
530 Some((&(first, first_direction), rest)) => sorted_rows(
533 snapshot,
534 spec,
535 parallelism,
536 |id, row, seq| (row.value(first), seq, id, row),
537 move |a, b| compare_sorted(a, b, first_direction, rest),
538 |&(_, _, id, _)| id,
539 ),
540 }
541}
542
543type SortEntry<'a, R> = (CellValue<'a>, u64, RowId, &'a Arc<R>);
545
546#[inline(always)]
550fn compare_sorted<R: GridRow>(
551 a: &SortEntry<'_, R>,
552 b: &SortEntry<'_, R>,
553 first_direction: SortDirection,
554 rest: &[(R::Column, SortDirection)],
555) -> Ordering {
556 directed(a.0.cmp_total(&b.0), first_direction)
557 .then_with(|| {
558 rest.iter()
559 .map(|&(column, direction)| {
560 directed(a.3.value(column).cmp_total(&b.3.value(column)), direction)
561 })
562 .find(|ordering| ordering.is_ne())
563 .unwrap_or(Ordering::Equal)
564 })
565 .then_with(|| a.1.cmp(&b.1))
566}
567
568fn sorted_rows<'a, R: GridRow, E: Copy + Send + Sync, T>(
575 snapshot: &'a StoreSnapshot<R>,
576 spec: &ViewSpec<R>,
577 parallelism: Parallelism,
578 entry: impl Fn(RowId, &'a Arc<R>, u64) -> E + Sync,
579 compare: impl Fn(&E, &E) -> Ordering + Sync,
580 row_id: impl Fn(&E) -> T,
581) -> (Vec<T>, usize) {
582 let mut fading = fading_entries(snapshot, spec, &entry);
583 let fading_count = fading.len();
584 if parallelism.threads <= 1 {
585 let mut visible = VisibleRows::new(spec);
586 let mut entries: Vec<E> = snapshot
587 .iter()
588 .filter(|(_, row)| visible.contains(row))
589 .map(|(id, row)| entry(id, row, snapshot.seq(id)))
590 .collect();
591 entries.append(&mut fading);
592 entries.sort_unstable_by(|a, b| compare(a, b));
593 let rows = entries.into_iter().map(|entry| row_id(&entry)).collect();
594 return (rows, fading_count);
595 }
596
597 let rows: Vec<(RowId, &Arc<R>)> = snapshot.iter().collect();
598 let next_chunk = AtomicUsize::new(0);
599 let fading_taken = AtomicBool::new(fading.is_empty());
602 let sorted_run = || {
605 let mut visible = VisibleRows::new(spec);
606 let mut entries = Vec::new();
607 loop {
608 let start = next_chunk.fetch_add(parallelism.chunk_rows, AtomicOrdering::Relaxed);
609 if start >= rows.len() {
610 break;
611 }
612 let chunk = &rows[start..rows.len().min(start + parallelism.chunk_rows)];
613 entries.extend(
614 chunk
615 .iter()
616 .filter(|(_, row)| visible.contains(row))
617 .map(|&(id, row)| entry(id, row, snapshot.seq(id))),
618 );
619 }
620 if !fading_taken.swap(true, AtomicOrdering::Relaxed) {
621 entries.extend_from_slice(&fading);
622 }
623 entries.sort_unstable_by(|a, b| compare(a, b));
624 entries
625 };
626 let mut runs = run_on_threads(&vec![&sorted_run; parallelism.threads]);
627 runs.retain(|run| !run.is_empty());
628
629 while runs.len() > 2 {
632 let merges: Vec<_> = runs
633 .chunks(2)
634 .map(|pair| {
635 let compare = &compare;
636 move || match pair {
637 [first, second] => {
638 let mut merged = Vec::with_capacity(first.len() + second.len());
639 merge(first, second, compare, |entry| *entry, &mut merged);
640 merged
641 }
642 _ => pair.concat(),
643 }
644 })
645 .collect();
646 runs = run_on_threads(&merges);
647 }
648 let mut rows = Vec::with_capacity(runs.iter().map(Vec::len).sum());
649 match runs.as_slice() {
650 [first, second] => merge(first, second, &compare, row_id, &mut rows),
651 runs => rows.extend(runs.iter().flatten().map(row_id)),
652 }
653 (rows, fading_count)
654}
655
656fn fading_entries<'a, R: GridRow, E>(
660 snapshot: &'a StoreSnapshot<R>,
661 spec: &ViewSpec<R>,
662 entry: &impl Fn(RowId, &'a Arc<R>, u64) -> E,
663) -> Vec<E> {
664 let mut fading = snapshot.fading_rows().peekable();
665 if fading.peek().is_none() {
666 return Vec::new();
667 }
668 let mut visible = VisibleRows::new(spec);
669 fading
670 .filter(|(_, row, ..)| visible.contains(row))
671 .map(|(id, row, _, seq)| entry(id, row, seq))
672 .collect()
673}
674
675fn places(
679 rows: impl Iterator<Item = (usize, RowId)>,
680 capacity: usize,
681) -> (Vec<u32>, FxHashMap<RowId, u32>) {
682 let mut by_slot = vec![u32::MAX; capacity];
683 let mut shared = FxHashMap::default();
684 for (place, id) in rows {
685 match by_slot.get_mut(id.ix()) {
686 Some(slot) if *slot == u32::MAX => *slot = place as u32,
687 _ => {
688 shared.insert(id, place as u32);
689 }
690 }
691 }
692 (by_slot, shared)
693}
694
695#[inline]
697fn shared_place(shared: &FxHashMap<RowId, u32>, id: RowId) -> Option<usize> {
698 if shared.is_empty() {
699 None
700 } else {
701 shared.get(&id).map(|&place| place as usize)
702 }
703}
704
705#[derive(Default)]
710struct FadingIds {
711 slots: Vec<u64>,
712 ids: FxHashSet<RowId>,
713}
714
715impl FadingIds {
716 fn new<R: GridRow>(snapshot: &StoreSnapshot<R>) -> Self {
717 let mut fading = Self::default();
718 for (id, ..) in snapshot.fading_rows() {
719 if fading.slots.is_empty() {
720 fading.slots = vec![0; snapshot.capacity().div_ceil(64)];
721 }
722 if let Some(word) = fading.slots.get_mut(id.ix() / 64) {
723 *word |= 1 << (id.ix() % 64);
724 }
725 fading.ids.insert(id);
726 }
727 fading
728 }
729
730 #[inline]
732 fn contains(&self, id: RowId) -> bool {
733 self.slots
734 .get(id.ix() / 64)
735 .is_some_and(|word| word & (1 << (id.ix() % 64)) != 0)
736 && self.ids.contains(&id)
737 }
738
739 fn is_empty(&self) -> bool {
741 self.ids.is_empty()
742 }
743}
744
745fn merge<E, T>(
748 first: &[E],
749 second: &[E],
750 compare: impl Fn(&E, &E) -> Ordering,
751 map: impl Fn(&E) -> T,
752 out: &mut Vec<T>,
753) {
754 let (mut first_ix, mut second_ix) = (0, 0);
755 while let (Some(a), Some(b)) = (first.get(first_ix), second.get(second_ix)) {
756 if compare(b, a).is_lt() {
757 out.push(map(b));
758 second_ix += 1;
759 } else {
760 out.push(map(a));
761 first_ix += 1;
762 }
763 }
764 out.extend(first[first_ix..].iter().map(&map));
765 out.extend(second[second_ix..].iter().map(&map));
766}
767
768fn run_on_threads<T: Send>(jobs: &[impl FnOnce() -> T + Copy + Send]) -> Vec<T> {
773 let Some((&first, rest)) = jobs.split_first() else {
774 return Vec::new();
775 };
776 thread::scope(|scope| {
777 let started: Vec<_> = rest
778 .iter()
779 .map(|&job| {
780 thread::Builder::new()
781 .name("live-grid-rebuild".into())
782 .spawn_scoped(scope, job)
783 .map_err(|_| job)
784 })
785 .collect();
786 let mut results = Vec::with_capacity(jobs.len());
787 results.push(first());
788 for job in started {
789 results.push(match job {
790 Ok(thread) => thread
791 .join()
792 .unwrap_or_else(|panic| panic::resume_unwind(panic)),
793 Err(job) => job(),
794 });
795 }
796 results
797 })
798}
799
800struct VisibleRows<'a, R: GridRow> {
803 filter: Option<&'a Filter<R>>,
804 quick_filter: Option<QuickFilterPass<'a, R>>,
805}
806
807impl<'a, R: GridRow> VisibleRows<'a, R> {
808 fn new(spec: &'a ViewSpec<R>) -> Self {
809 Self {
810 filter: spec.filter.as_ref().map(|(_, filter)| filter),
811 quick_filter: spec.quick_filter.as_ref().map(QuickFilterPass::new),
813 }
814 }
815
816 fn contains(&mut self, row: &'a R) -> bool {
817 self.filter.is_none_or(|filter| filter(row))
818 && self
819 .quick_filter
820 .as_mut()
821 .is_none_or(|quick_filter| quick_filter.matches(row))
822 }
823}
824
825#[cfg(test)]
826mod tests {
827 use std::time::{Duration, Instant};
828
829 use super::*;
830 use crate::{
831 highlight::HighlightPolicy,
832 model::{ColumnId, ColumnIx, OwnedValue},
833 quick_filter::ContainsAllTerms,
834 store::{Ingest, Op},
835 };
836
837 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
838 enum Column {
839 Id,
840 Name,
841 Price,
842 }
843
844 impl GridColumns for Column {
845 fn all() -> &'static [Self] {
846 &[Self::Id, Self::Name, Self::Price]
847 }
848
849 fn id(self) -> &'static str {
850 match self {
851 Self::Id => "id",
852 Self::Name => "name",
853 Self::Price => "price",
854 }
855 }
856
857 fn ix(self) -> ColumnIx {
858 ColumnIx::new(self as u16)
859 }
860 }
861
862 struct Row;
863
864 impl GridRow for Row {
865 type Key = u32;
866 type Column = Column;
867
868 fn key(&self) -> &u32 {
869 &0
870 }
871
872 fn value(&self, _column: Column) -> CellValue<'_> {
873 CellValue::Empty
874 }
875 }
876
877 #[test]
878 fn test_quick_filter_dependencies() {
879 let mut name = ColumnSet::EMPTY;
880 name.insert(Column::Name.ix());
881 let quick_filter = QuickFilter::new("sber", name, &ContainsAllTerms);
882 let spec = ViewSpec::<Row>::new()
883 .with_sort([(Column::Price, SortDirection::Ascending)])
884 .with_quick_filter(quick_filter);
885 assert_eq!(spec.quick_filter().map(QuickFilter::text), Some("sber"));
886 let dependencies = spec.dependencies();
887 assert!(dependencies.contains(Column::Name.ix()));
888 assert!(dependencies.contains(Column::Price.ix()));
889 assert!(!dependencies.contains(Column::Id.ix()));
890
891 let off = spec.with_quick_filter(None);
893 assert!(off.quick_filter().is_none());
894 assert!(!off.dependencies().contains(Column::Name.ix()));
895 assert!(off.dependencies().contains(Column::Price.ix()));
896 }
897
898 #[test]
899 fn test_sort_direction_serde() {
900 for (direction, json) in [
902 (SortDirection::Ascending, r#""ascending""#),
903 (SortDirection::Descending, r#""descending""#),
904 ] {
905 assert_eq!(serde_json::to_string(&direction).unwrap(), json);
906 assert_eq!(
907 serde_json::from_str::<SortDirection>(json).unwrap(),
908 direction
909 );
910 }
911 assert!(serde_json::from_str::<SortDirection>(r#""Ascending""#).is_err());
912 }
913
914 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
915 enum TradeColumn {
916 Id,
917 Group,
918 Price,
919 Name,
920 Flag,
921 Mixed,
922 }
923
924 impl GridColumns for TradeColumn {
925 fn all() -> &'static [Self] {
926 &[
927 Self::Id,
928 Self::Group,
929 Self::Price,
930 Self::Name,
931 Self::Flag,
932 Self::Mixed,
933 ]
934 }
935
936 fn id(self) -> &'static str {
937 match self {
938 Self::Id => "id",
939 Self::Group => "group",
940 Self::Price => "price",
941 Self::Name => "name",
942 Self::Flag => "flag",
943 Self::Mixed => "mixed",
944 }
945 }
946
947 fn ix(self) -> ColumnIx {
948 ColumnIx::new(self as u16)
949 }
950 }
951
952 struct Trade {
955 id: u32,
956 group: i64,
957 price: f64,
958 name: String,
959 flag: Option<bool>,
960 mixed: u8,
962 }
963
964 impl Trade {
965 fn random(rng: &mut fastrand::Rng, id: u32) -> Self {
966 const NAME_CHARS: &[char] = &['a', 'B', 'я', 'Ж', '1'];
967 Self {
968 id,
969 group: rng.i64(0..4),
970 price: [0.5, 1.0, 1.5, f64::NAN, -0.0][rng.usize(..5)],
971 name: (0..rng.usize(0..=2))
972 .map(|_| NAME_CHARS[rng.usize(..NAME_CHARS.len())])
973 .collect(),
974 flag: [None, Some(false), Some(true)][rng.usize(..3)],
975 mixed: rng.u8(0..8),
976 }
977 }
978 }
979
980 impl GridRow for Trade {
981 type Key = u32;
982 type Column = TradeColumn;
983
984 fn key(&self) -> &u32 {
985 &self.id
986 }
987
988 fn value(&self, column: TradeColumn) -> CellValue<'_> {
989 match column {
990 TradeColumn::Id => CellValue::Int(self.id.into()),
991 TradeColumn::Group => CellValue::Int(self.group),
992 TradeColumn::Price => CellValue::Float(self.price),
993 TradeColumn::Name => CellValue::Text(&self.name),
994 TradeColumn::Flag => self.flag.map_or(CellValue::Empty, CellValue::Bool),
995 TradeColumn::Mixed => match self.mixed {
996 0 => CellValue::Empty,
997 1 => CellValue::Bool(true),
998 2 | 3 => CellValue::Int(i64::from(self.mixed)),
999 4 | 5 => CellValue::Float(f64::from(self.mixed) / 2.0),
1000 _ => CellValue::Text(if self.mixed == 6 { "x" } else { "Y" }),
1001 },
1002 }
1003 }
1004 }
1005
1006 fn random_spec(rng: &mut fastrand::Rng) -> ViewSpec<Trade> {
1007 let columns = TradeColumn::all();
1008 let direction = |rng: &mut fastrand::Rng| {
1009 [SortDirection::Ascending, SortDirection::Descending][rng.usize(..2)]
1010 };
1011 let sort: Vec<_> = (0..rng.usize(0..=3))
1012 .map(|_| (columns[rng.usize(..columns.len())], direction(rng)))
1013 .collect();
1014 let mut quick_columns = ColumnSet::EMPTY;
1015 for column in columns {
1016 if rng.bool() {
1017 quick_columns.insert(column.ix());
1018 }
1019 }
1020 let text = ["", "a", "я 1", "b ж", ".5", "1 a"][rng.usize(..6)];
1021 let spec = ViewSpec::new()
1022 .with_sort(sort)
1023 .with_quick_filter(QuickFilter::new(text, quick_columns, &ContainsAllTerms));
1024 if rng.bool() {
1025 spec.with_filter(ColumnSet::first(columns.len()), |trade: &Trade| {
1026 trade.group != 1
1027 })
1028 } else {
1029 spec
1030 }
1031 }
1032
1033 fn policy(fade: bool) -> HighlightPolicy {
1036 if fade {
1037 HighlightPolicy::default().with_removal_fade(Duration::from_secs(3_600))
1038 } else {
1039 HighlightPolicy::default()
1040 }
1041 }
1042
1043 #[test]
1044 fn test_parallel_rebuild_matches_sequential() {
1045 let epoch = Instant::now();
1049 let mut fading = 0;
1050 for seed in 0..48 {
1051 let mut rng = fastrand::Rng::with_seed(seed);
1052 let mut ingest = Ingest::<Trade>::new(policy(seed % 2 == 1), epoch);
1053 let count = [0, 1, 2, 7, 100, 1_000, 3_000][rng.usize(..7)];
1054 ingest.apply(
1055 (0..count).map(|id| Op::UpsertSilent(Arc::new(Trade::random(&mut rng, id)))),
1056 epoch,
1057 );
1058 ingest.apply(
1061 (0..count / 3).map(|_| Op::Remove(rng.u32(..count.max(1)))),
1062 epoch,
1063 );
1064 ingest.apply(
1065 (count..count + count / 4)
1066 .map(|id| Op::UpsertSilent(Arc::new(Trade::random(&mut rng, id)))),
1067 epoch,
1068 );
1069 let snapshot = ingest.snapshot();
1070 for _ in 0..8 {
1071 let spec = random_spec(&mut rng);
1072 let sequential = project(
1073 &snapshot,
1074 &spec,
1075 Parallelism {
1076 threads: 1,
1077 chunk_rows: CHUNK_ROWS,
1078 },
1079 );
1080 fading += sequential.1;
1081 let parallelism = Parallelism {
1082 threads: rng.usize(2..=7),
1083 chunk_rows: rng.usize(1..=97),
1084 };
1085 assert_eq!(
1086 project(&snapshot, &spec, parallelism),
1087 sequential,
1088 "seed {seed}, {count} rows, {parallelism:?}, sort {:?}, quick filter {:?}",
1089 spec.sort(),
1090 spec.quick_filter().map(QuickFilter::text),
1091 );
1092 }
1093 }
1094 assert!(fading > 1_000, "{fading} fading rows");
1095 }
1096
1097 #[test]
1098 fn test_parallelism_for_view() {
1099 let sorted =
1100 ViewSpec::<Trade>::new().with_sort([(TradeColumn::Name, SortDirection::Ascending)]);
1101 let budget = Rebuild::default().max_threads();
1102 assert_eq!(
1104 Parallelism::for_view(MIN_ROWS_PER_THREAD - 1, &sorted, budget).threads,
1105 1
1106 );
1107 assert_eq!(
1108 Parallelism::for_view(1 << 20, &ViewSpec::<Trade>::new(), budget).threads,
1109 1
1110 );
1111 let threads = Parallelism::for_view(1 << 20, &sorted, budget).threads;
1114 assert_eq!(threads, default_thread_budget());
1115 assert!((1..=MAX_THREADS).contains(&threads));
1116 if let Ok(cores) = thread::available_parallelism() {
1117 assert!(threads == 1 || threads + RESERVED_CORES <= cores.get());
1118 }
1119 for (rows, threads) in [
1121 (MIN_ROWS_PER_THREAD * 3 - 1, 2),
1122 (MIN_ROWS_PER_THREAD * 3, 3),
1123 (MIN_ROWS_PER_THREAD * 40, 12),
1124 ] {
1125 assert_eq!(
1126 Parallelism::for_view(rows, &sorted, 12).threads,
1127 threads,
1128 "{rows} rows"
1129 );
1130 }
1131 }
1132
1133 #[test]
1134 fn test_rebuild_max_threads() {
1135 assert_eq!(Rebuild::default().max_threads(), default_thread_budget());
1136 assert_eq!(Rebuild::default().with_max_threads(1).max_threads(), 1);
1137 assert_eq!(
1138 Rebuild::default().with_max_threads(0).max_threads(),
1139 1,
1140 "at least one"
1141 );
1142 assert_eq!(
1144 Rebuild::default()
1145 .with_max_threads(MAX_THREADS * 2)
1146 .max_threads(),
1147 MAX_THREADS * 2
1148 );
1149 }
1150
1151 #[test]
1152 fn test_parallel_rebuild_resumes_panic() {
1153 let epoch = Instant::now();
1156 let mut rng = fastrand::Rng::with_seed(1);
1157 let mut ingest = Ingest::<Trade>::new(HighlightPolicy::default(), epoch);
1158 ingest.apply(
1159 (0..2_000).map(|id| Op::UpsertSilent(Arc::new(Trade::random(&mut rng, id)))),
1160 epoch,
1161 );
1162 let snapshot = ingest.snapshot();
1163 let spec = ViewSpec::new().with_filter(ColumnSet::first(1), |trade: &Trade| {
1164 assert_ne!(trade.id, 1_500, "filter failed");
1165 true
1166 });
1167 let parallelism = Parallelism {
1168 threads: 4,
1169 chunk_rows: 16,
1170 };
1171 let panic = panic::catch_unwind(panic::AssertUnwindSafe(|| {
1172 project(&snapshot, &spec, parallelism)
1173 }))
1174 .expect_err("the filter panics");
1175 let message = panic
1176 .downcast_ref::<String>()
1177 .map(String::as_str)
1178 .unwrap_or_default();
1179 assert!(message.contains("filter failed"), "{message}");
1180 }
1181
1182 fn random_snapshot(rng: &mut fastrand::Rng, count: u32) -> Arc<StoreSnapshot<Trade>> {
1185 random_snapshot_with(rng, count, false)
1186 }
1187
1188 fn random_snapshot_with(
1191 rng: &mut fastrand::Rng,
1192 count: u32,
1193 fade: bool,
1194 ) -> Arc<StoreSnapshot<Trade>> {
1195 let epoch = Instant::now();
1196 let mut ingest = Ingest::<Trade>::new(policy(fade), epoch);
1197 ingest.apply(
1198 (0..count).map(|id| Op::UpsertSilent(Arc::new(Trade::random(rng, id)))),
1199 epoch,
1200 );
1201 ingest.apply(
1202 (0..count / 3).map(|_| Op::Remove(rng.u32(..count.max(1)))),
1203 epoch,
1204 );
1205 ingest.apply(
1206 (count..count + count / 4).map(|id| Op::UpsertSilent(Arc::new(Trade::random(rng, id)))),
1207 epoch,
1208 );
1209 ingest.snapshot()
1210 }
1211
1212 #[test]
1213 fn test_flat_projection_readers() {
1214 let engine = Rebuild::default().with_max_threads(1);
1215 for seed in 0..24 {
1216 let mut rng = fastrand::Rng::with_seed(seed);
1217 let count = [0, 1, 7, 100, 300][rng.usize(..5)];
1218 let snapshot = random_snapshot(&mut rng, count);
1219 let spec = random_spec(&mut rng);
1220 let projection = engine.rebuild(&snapshot, &spec);
1221 let context = format!("seed {seed}, {count} rows");
1222
1223 assert_eq!(projection.len(), projection.rows().len(), "{context}");
1225 assert_eq!(projection.row_count(), projection.len(), "{context}");
1226 assert_eq!(projection.is_empty(), projection.rows().is_empty());
1227 for (ix, &id) in projection.rows().iter().enumerate() {
1228 assert_eq!(projection.row(ix), Some(id), "{context}");
1229 assert_eq!(
1230 projection.line(ix),
1231 Some(DisplayRow::Data { row: id, depth: 0 }),
1232 "{context}"
1233 );
1234 assert_eq!(projection.display_ix(id), Some(ix), "{context}");
1235 assert_eq!(projection.order_ix(id), Some(ix), "{context}");
1236 assert!(projection.contains(id), "{context}");
1237 }
1238 assert_eq!(projection.line(projection.len()), None);
1239 assert_eq!(projection.row(projection.len()), None);
1240 for (id, _) in snapshot.iter() {
1242 let shown = projection.rows().contains(&id);
1243 assert_eq!(projection.contains(id), shown, "{context}");
1244 assert_eq!(projection.order_ix(id).is_some(), shown, "{context}");
1245 assert_eq!(projection.branch(id), None, "{context}");
1246 }
1247 assert!(!projection.is_grouped() && !projection.is_tree());
1248 assert!(projection.groups().is_empty());
1249 assert_eq!(projection.groups().iter().count(), 0);
1250 assert_eq!(projection.group_line(GroupId::new(0)), None);
1251 assert!(projection.totals().is_empty());
1252 assert_eq!(projection.totals().columns(), ColumnSet::EMPTY);
1253 assert_eq!(projection.totals().get(TradeColumn::Price.ix()), None);
1254 assert_eq!(projection.totals().row_count(), projection.row_count());
1255 let expanded: Vec<DisplayRow> = projection.expanded_lines().collect();
1256 let lines: Vec<DisplayRow> = (0..projection.len())
1257 .filter_map(|ix| projection.line(ix))
1258 .collect();
1259 assert_eq!(expanded, lines, "{context}");
1260 assert_eq!(
1261 projection.shown_rows().collect::<Vec<_>>(),
1262 projection.rows(),
1263 "{context}"
1264 );
1265
1266 let aggregated = engine.aggregate(&projection, &snapshot, &spec);
1269 assert_eq!(aggregated.rows(), projection.rows(), "{context}");
1270 assert_eq!(aggregated.store_version(), snapshot.version());
1271 let expanded = engine.expand(&projection, &snapshot, &spec);
1272 assert_eq!(expanded.rows(), projection.rows(), "{context}");
1273 assert_eq!(expanded.len(), projection.len(), "{context}");
1274 assert_eq!(expanded.store_version(), projection.store_version());
1275 let compacted = engine.compact(&projection, &snapshot);
1276 assert_eq!(compacted.rows(), projection.rows(), "{context}");
1277 }
1278 }
1279
1280 #[test]
1281 fn test_grouped_rows_follow_the_level_sort() {
1282 let engine = Rebuild::default().with_max_threads(1);
1285 for seed in 0..12 {
1286 let mut rng = fastrand::Rng::with_seed(seed);
1287 let snapshot = random_snapshot(&mut rng, 200);
1288 let spec = random_spec(&mut rng);
1289 let direction = [SortDirection::Ascending, SortDirection::Descending][rng.usize(..2)];
1290 let level =
1291 [TradeColumn::Group, TradeColumn::Mixed, TradeColumn::Price][rng.usize(..3)];
1292 let grouped = engine.rebuild(
1293 &snapshot,
1294 &spec
1295 .clone()
1296 .with_group_by([GroupBy::column(level).with_direction(direction)]),
1297 );
1298 let sort: Vec<_> = std::iter::once((level, direction))
1299 .chain(spec.sort().iter().copied())
1300 .collect();
1301 let flat = engine.rebuild(&snapshot, &spec.clone().with_sort(sort));
1302 assert_eq!(grouped.rows(), flat.rows(), "seed {seed}");
1303 assert!(grouped.is_grouped() && !grouped.is_tree());
1304 assert_eq!(grouped.row_count(), flat.row_count());
1305 }
1306 }
1307
1308 struct OrderedSum;
1311
1312 impl AggregateFn<Trade> for OrderedSum {
1313 fn aggregate(&self, _: TradeColumn, rows: &mut dyn Iterator<Item = &Trade>) -> OwnedValue {
1314 OwnedValue::Float(rows.map(|trade| trade.price * 0.1).sum())
1315 }
1316 }
1317
1318 fn random_grouped_spec(rng: &mut fastrand::Rng) -> ViewSpec<Trade> {
1321 let direction = |rng: &mut fastrand::Rng| {
1322 [SortDirection::Ascending, SortDirection::Descending][rng.usize(..2)]
1323 };
1324 let letter = || {
1325 GroupKey::new("letter", ColumnSet::first(4), |trade: &Trade| {
1326 CellValue::Text(trade.name.get(..1).unwrap_or_default())
1327 })
1328 };
1329 let levels: Vec<GroupBy<Trade>> = (0..rng.usize(1..=3))
1330 .map(|_| {
1331 let level = match rng.u8(0..5) {
1332 0 => GroupBy::column(TradeColumn::Group),
1333 1 => GroupBy::column(TradeColumn::Flag),
1334 2 => GroupBy::column(TradeColumn::Mixed),
1335 3 => GroupBy::column(TradeColumn::Price),
1336 _ => GroupBy::key(letter()),
1337 };
1338 level.with_direction(direction(rng))
1339 })
1340 .collect();
1341 let spec = random_spec(rng).with_group_by(levels).with_aggregates([
1342 (TradeColumn::Price, Aggregate::Sum),
1343 (TradeColumn::Group, Aggregate::Avg),
1344 (TradeColumn::Name, Aggregate::Min),
1345 (TradeColumn::Mixed, Aggregate::Max),
1346 (TradeColumn::Flag, Aggregate::Count),
1347 (TradeColumn::Id, Aggregate::Custom(Arc::new(OrderedSum))),
1348 ]);
1349 let spec = if rng.bool() {
1350 spec.with_group_order(AggregateOrder::new(TradeColumn::Price, direction(rng)))
1351 } else {
1352 spec
1353 };
1354 if rng.bool() {
1355 spec.with_group_footer(EveryGroupFooter)
1356 } else {
1357 spec
1358 }
1359 }
1360
1361 fn assert_same_projection(a: &Projection, b: &Projection, context: &str) {
1363 let same = |a: &OwnedValue, b: &OwnedValue| {
1364 std::mem::discriminant(a) == std::mem::discriminant(b) && a == b
1365 };
1366 let same_row = |a: AggregateRow<'_>, b: AggregateRow<'_>| {
1367 a.columns() == b.columns()
1368 && a.row_count() == b.row_count()
1369 && TradeColumn::all().iter().all(|column| {
1370 match (a.get(column.ix()), b.get(column.ix())) {
1371 (Some(a), Some(b)) => same(a, b),
1372 (a, b) => a.is_none() && b.is_none(),
1373 }
1374 })
1375 };
1376 assert_eq!(a.rows(), b.rows(), "{context}");
1377 assert_eq!(a.len(), b.len(), "{context}");
1378 for ix in 0..a.len() {
1379 assert_eq!(a.line(ix), b.line(ix), "{context}, line {ix}");
1380 }
1381 assert_eq!(a.row_count(), b.row_count(), "{context}");
1382 assert!(same_row(a.totals(), b.totals()), "{context}: totals");
1383 assert_eq!(a.groups().len(), b.groups().len(), "{context}");
1384 for (a, b) in a.groups().iter().zip(b.groups().iter()) {
1385 assert_eq!(a.path(), b.path(), "{context}");
1386 assert_eq!(a.parent(), b.parent(), "{context}");
1387 assert_eq!(a.rows(), b.rows(), "{context}");
1388 assert_eq!(a.row_count(), b.row_count(), "{context}");
1389 assert_eq!(
1390 (a.is_expanded(), a.has_footer()),
1391 (b.is_expanded(), b.has_footer()),
1392 "{context}"
1393 );
1394 assert!(same_row(a.aggregates(), b.aggregates()), "{context}: {a:?}");
1395 }
1396 }
1397
1398 #[test]
1399 fn test_grouped_rebuild_is_thread_independent() {
1400 let mut fading = 0;
1405 for seed in 0..48 {
1406 let mut rng = fastrand::Rng::with_seed(seed);
1407 let count = [0, 1, 2, 7, 100, 1_000, 3_000][rng.usize(..7)];
1408 let snapshot = random_snapshot_with(&mut rng, count, seed % 2 == 1);
1409 for _ in 0..4 {
1410 let spec = random_grouped_spec(&mut rng);
1411 let Structure::Groups(grouping) = spec.structure() else {
1412 unreachable!("a grouped view");
1413 };
1414 let sequential = group::rebuild_with(
1415 &snapshot,
1416 &spec,
1417 grouping,
1418 Parallelism {
1419 threads: 1,
1420 chunk_rows: CHUNK_ROWS,
1421 },
1422 );
1423 let parallelism = Parallelism {
1424 threads: rng.usize(2..=7),
1425 chunk_rows: rng.usize(1..=97),
1426 };
1427 let parallel = group::rebuild_with(&snapshot, &spec, grouping, parallelism);
1428 assert_same_projection(
1429 ¶llel,
1430 &sequential,
1431 &format!("seed {seed}, {count} rows, {parallelism:?}"),
1432 );
1433 fading += sequential.rows().len() - sequential.row_count();
1434 for (order_ix, &id) in parallel.rows().iter().enumerate() {
1435 assert_eq!(parallel.order_ix(id), Some(order_ix), "seed {seed}");
1436 }
1437 }
1438 }
1439 assert!(fading > 1_000, "{fading} fading rows");
1440 }
1441
1442 #[test]
1443 fn test_group_key_panic_resumes() {
1444 let mut rng = fastrand::Rng::with_seed(2);
1447 let snapshot = random_snapshot(&mut rng, 3_000);
1448 let spec = ViewSpec::new().with_group_by([GroupBy::key(GroupKey::new(
1449 "checked",
1450 ColumnSet::first(1),
1451 |trade: &Trade| {
1452 assert_ne!(trade.id, 1_500, "group key failed");
1453 CellValue::Int(trade.group)
1454 },
1455 ))]);
1456 let Structure::Groups(grouping) = spec.structure() else {
1457 unreachable!("a grouped view");
1458 };
1459 let parallelism = Parallelism {
1460 threads: 4,
1461 chunk_rows: 16,
1462 };
1463 let panic = panic::catch_unwind(panic::AssertUnwindSafe(|| {
1464 group::rebuild_with(&snapshot, &spec, grouping, parallelism)
1465 }))
1466 .expect_err("the group key panics");
1467 let message = panic
1468 .downcast_ref::<String>()
1469 .map(String::as_str)
1470 .unwrap_or_default();
1471 assert!(message.contains("group key failed"), "{message}");
1472 }
1473
1474 #[test]
1475 fn test_dependencies_with_groups() {
1476 let mut expected = ColumnSet::EMPTY;
1477 expected.insert(TradeColumn::Price.ix());
1478 let sorted =
1479 ViewSpec::<Trade>::new().with_sort([(TradeColumn::Price, SortDirection::Ascending)]);
1480 assert_eq!(sorted.dependencies(), expected);
1481 let mut key_columns = ColumnSet::EMPTY;
1483 key_columns.insert(TradeColumn::Name.ix());
1484 let grouped = sorted
1485 .clone()
1486 .with_group_by([
1487 GroupBy::column(TradeColumn::Group),
1488 GroupBy::key(GroupKey::new("letter", key_columns, |trade: &Trade| {
1489 CellValue::Text(trade.name.get(..1).unwrap_or_default())
1490 })),
1491 ])
1492 .with_aggregates([(TradeColumn::Mixed, Aggregate::Sum)])
1493 .with_group_order(AggregateOrder::new(
1494 TradeColumn::Flag,
1495 SortDirection::Ascending,
1496 ));
1497 for column in [TradeColumn::Group, TradeColumn::Name, TradeColumn::Flag] {
1498 expected.insert(column.ix());
1499 }
1500 assert_eq!(grouped.dependencies(), expected);
1501 assert!(!grouped.dependencies().contains(TradeColumn::Mixed.ix()));
1503 assert!(
1504 grouped
1505 .aggregate_columns()
1506 .contains(TradeColumn::Mixed.ix())
1507 );
1508 let flat = grouped.with_group_by([]);
1510 let mut price = ColumnSet::EMPTY;
1511 price.insert(TradeColumn::Price.ix());
1512 assert_eq!(flat.dependencies(), price);
1513 }
1514
1515 struct Weighted;
1517
1518 impl AggregateFn<Trade> for Weighted {
1519 fn aggregate(&self, _: TradeColumn, _: &mut dyn Iterator<Item = &Trade>) -> OwnedValue {
1520 OwnedValue::Empty
1521 }
1522
1523 fn depends_on(&self, column: TradeColumn) -> ColumnSet {
1524 let mut columns = ColumnSet::EMPTY;
1525 columns.insert(column.ix());
1526 columns.insert(TradeColumn::Group.ix());
1527 columns
1528 }
1529
1530 fn name(&self) -> &str {
1531 "Weighted"
1532 }
1533 }
1534
1535 #[test]
1536 fn test_view_spec_structure() {
1537 let by_group = GroupBy::column(TradeColumn::Group);
1538 let by_letter = GroupBy::key(
1539 GroupKey::new("letter", ColumnSet::first(4), |trade: &Trade| {
1540 CellValue::Text(trade.name.get(..1).unwrap_or_default())
1541 })
1542 .with_title("First letter"),
1543 )
1544 .with_direction(SortDirection::Descending);
1545 let spec = ViewSpec::<Trade>::new()
1546 .with_aggregates([
1547 (TradeColumn::Price, Aggregate::Sum),
1548 (TradeColumn::Id, Aggregate::Count),
1549 (TradeColumn::Price, Aggregate::Max),
1550 ])
1551 .with_group_by([by_group.clone(), by_letter, by_group.clone()]);
1552
1553 let levels: Vec<ColumnId> = spec.group_by().iter().map(GroupBy::id).collect();
1555 assert_eq!(levels, [ColumnId::from("group"), ColumnId::from("letter")]);
1556 assert_eq!(spec.group_by()[0].as_column(), Some(TradeColumn::Group));
1557 assert_eq!(spec.group_by()[1].as_column(), None);
1558 let key = spec.group_by()[1].as_key().expect("a key level");
1559 assert_eq!((key.id(), key.title()), ("letter", "First letter"));
1560 assert_eq!(spec.group_by()[1].direction(), SortDirection::Descending);
1561 assert!(spec.tree().is_none());
1562 let aggregates: Vec<(TradeColumn, String)> = spec
1564 .aggregates()
1565 .iter()
1566 .map(|(column, aggregate)| (*column, format!("{aggregate:?}")))
1567 .collect();
1568 assert_eq!(
1569 aggregates,
1570 [
1571 (TradeColumn::Price, "Max".to_owned()),
1572 (TradeColumn::Id, "Count".to_owned())
1573 ]
1574 );
1575
1576 let tree = spec
1579 .clone()
1580 .with_tree(TreeSpec::new().with_column(TradeColumn::Name));
1581 assert!(tree.group_by().is_empty());
1582 assert_eq!(
1583 tree.tree().and_then(TreeSpec::column),
1584 Some(TradeColumn::Name)
1585 );
1586 assert!(matches!(
1587 tree.tree().unwrap().filter(),
1588 TreeFilter::KeepAncestors
1589 ));
1590 assert!(matches!(tree.tree().unwrap().totals(), TreeTotals::Leaves));
1591 assert_eq!(tree.aggregates().len(), 2);
1592 let regrouped = tree.with_group_by([by_group]);
1593 assert!(regrouped.tree().is_none());
1594 assert_eq!(regrouped.group_by().len(), 1);
1595 let flat = regrouped.with_group_by([]);
1597 assert!(flat.group_by().is_empty() && flat.tree().is_none());
1598 assert!(matches!(flat.structure(), Structure::Flat));
1599 assert_eq!(flat.aggregates().len(), 2);
1600
1601 let mut expected = ColumnSet::EMPTY;
1604 expected.insert(TradeColumn::Price.ix());
1605 expected.insert(TradeColumn::Id.ix());
1606 assert_eq!(flat.aggregate_columns(), expected);
1607 let custom =
1608 flat.with_aggregates([(TradeColumn::Price, Aggregate::Custom(Arc::new(Weighted)))]);
1609 let mut expected = ColumnSet::EMPTY;
1610 expected.insert(TradeColumn::Price.ix());
1611 expected.insert(TradeColumn::Group.ix());
1612 assert_eq!(custom.aggregate_columns(), expected);
1613 assert_eq!(
1614 format!("{:?}", custom.aggregates()[0].1),
1615 r#"Custom("Weighted")"#
1616 );
1617 assert!(ViewSpec::<Trade>::new().aggregates().is_empty());
1618 assert!(ViewSpec::<Trade>::new().aggregate_columns().is_empty());
1619
1620 let a = GroupPath::new([(ColumnId::from("group"), OwnedValue::Int(1))]);
1622 let b = GroupPath::new([(ColumnId::from("group"), OwnedValue::Int(2))]);
1623 let values = [OwnedValue::Float(2.0)];
1624 let low = [OwnedValue::Float(1.0)];
1625 let mut price = ColumnSet::EMPTY;
1626 price.insert(TradeColumn::Price.ix());
1627 let first = GroupOrderContext::new(&a, 3, AggregateRow::new(price, &values, 3));
1628 let second = GroupOrderContext::new(&b, 1, AggregateRow::new(price, &low, 1));
1629 let spec = ViewSpec::<Trade>::new();
1630 assert_eq!(spec.group_order().compare(&first, &second), Ordering::Equal);
1631 assert!(spec.group_order().depends_on().is_empty());
1632 let by_sum = spec.with_group_order(AggregateOrder::new(
1633 TradeColumn::Price,
1634 SortDirection::Descending,
1635 ));
1636 assert_eq!(
1637 by_sum.group_order().compare(&first, &second),
1638 Ordering::Less
1639 );
1640 assert_eq!(by_sum.group_order().depends_on(), price);
1641 assert_eq!(first.value(), &OwnedValue::Int(1));
1642 assert_eq!((first.depth(), first.row_count()), (0, 3));
1643 assert_eq!(first.path(), &a);
1644 assert!(by_sum.expansion() == &Expansion::default());
1645 }
1646}