1use std::cmp::Ordering;
42use std::collections::{BTreeMap, BinaryHeap, HashMap};
43use std::sync::Arc;
44use std::time::{Duration, Instant};
45
46use arrow::array::{
47 Array, ArrayRef, Float64Array, Int64Array, RecordBatch, UInt32Array, UInt64Array,
48};
49use arrow::compute::{concat_batches, interleave, take};
50use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
51use arrow::row::{RowConverter, SortField};
52use arrow::util::display::array_value_to_string;
53use futures::stream::{self, BoxStream, FuturesUnordered, StreamExt};
54use mongreldb_core::{CancellationReason, ExecutionControl, RowId};
55use mongreldb_types::ids::{MetadataVersion, QueryId, TabletId};
56use parking_lot::Mutex;
57use serde::{Deserialize, Serialize};
58
59use crate::query_registry::{CancelOutcome, RegisteredSqlQuery, SqlQueryOptions, SqlQueryRegistry};
60
61pub const DEFAULT_BROADCAST_THRESHOLD_BYTES: u64 = 8 * 1024 * 1024;
64
65pub const DEFAULT_MAX_SPILL_BYTES_PER_FRAGMENT: u64 = 256 * 1024 * 1024;
69
70const COORDINATOR_OUTPUT_BATCH_ROWS: usize = 8_192;
72
73pub const TOPK_ROWID_COLUMN: &str = "__rowid";
77
78#[non_exhaustive]
80#[derive(Debug, thiserror::Error)]
81pub enum DistributedError {
82 #[error("unknown table `{0}`")]
84 UnknownTable(String),
85 #[error("table `{table}` has no tablets in its layout")]
87 EmptyLayout { table: String },
88 #[error("invalid plan: {0}")]
90 InvalidPlan(String),
91 #[error("fragment {fragment_id} resource reservation denied: {reason}")]
93 Reservation { fragment_id: u32, reason: String },
94 #[error("fragment {fragment_id} failed on worker {worker}: {message}")]
96 FragmentExecution {
97 fragment_id: u32,
98 worker: String,
99 message: String,
100 },
101 #[error("distributed query cancelled: {0:?}")]
103 Cancelled(CancellationReason),
104 #[error("unsupported in this wave: {0}")]
106 Unsupported(String),
107 #[error("arrow error: {0}")]
109 Arrow(String),
110}
111
112pub type DistributedResult<T> = Result<T, DistributedError>;
114
115impl From<arrow::error::ArrowError> for DistributedError {
116 fn from(error: arrow::error::ArrowError) -> Self {
117 Self::Arrow(error.to_string())
118 }
119}
120
121fn fnv1a64(bytes: &[u8]) -> u64 {
123 let mut hash = 0xcbf2_9ce4_8422_2325_u64;
124 for byte in bytes {
125 hash ^= u64::from(*byte);
126 hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
127 }
128 hash
129}
130
131#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
139pub enum PartitionSpec {
140 Hash { columns: Vec<String>, buckets: u32 },
142 Range { columns: Vec<String> },
144 Tenant {
146 column: String,
147 buckets_per_tenant: u32,
148 },
149 TimeRange { column: String },
151 Unpartitioned,
153}
154
155impl PartitionSpec {
156 pub fn partition_columns(&self) -> Vec<&str> {
158 match self {
159 Self::Hash { columns, .. } | Self::Range { columns } => {
160 columns.iter().map(String::as_str).collect()
161 }
162 Self::Tenant { column, .. } | Self::TimeRange { column } => vec![column.as_str()],
163 Self::Unpartitioned => Vec::new(),
164 }
165 }
166
167 pub fn colocated_with(&self, other: &PartitionSpec) -> bool {
172 match (self, other) {
173 (Self::Unpartitioned, Self::Unpartitioned) => true,
174 (Self::Unpartitioned, _) | (_, Self::Unpartitioned) => false,
175 _ => self == other,
176 }
177 }
178}
179
180#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
182pub struct TableStats {
183 pub row_count: u64,
185 pub total_bytes: u64,
187}
188
189pub trait TabletLocator: Send + Sync {
194 fn tablets_for_table(&self, table: &str) -> DistributedResult<Vec<TabletId>>;
196 fn partitioning(&self, table: &str) -> DistributedResult<PartitionSpec>;
198}
199
200pub trait ClusterMetadata: Send + Sync {
203 fn metadata_version(&self) -> MetadataVersion;
205 fn table_stats(&self, table: &str) -> DistributedResult<TableStats>;
207}
208
209#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
215pub struct JoinKey {
216 pub left: String,
218 pub right: String,
220}
221
222#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
224pub struct SortKey {
225 pub column: String,
227 pub descending: bool,
229}
230
231#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
233pub struct AggregateExpr {
234 pub function: AggregateFunction,
236 pub column: Option<String>,
238}
239
240#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
242pub enum AggregateFunction {
243 Count,
245 Sum,
247 Min,
249 Max,
251 Avg,
253}
254
255impl AggregateFunction {
256 fn name(self) -> &'static str {
257 match self {
258 Self::Count => "count",
259 Self::Sum => "sum",
260 Self::Min => "min",
261 Self::Max => "max",
262 Self::Avg => "avg",
263 }
264 }
265}
266
267#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
273pub enum LogicalPlanLite {
274 Scan {
276 table: String,
278 predicate: Option<String>,
281 projection: Vec<String>,
283 },
284 Aggregate {
286 input: Box<LogicalPlanLite>,
288 group_by: Vec<String>,
290 aggregates: Vec<AggregateExpr>,
292 },
293 Join {
295 left: Box<LogicalPlanLite>,
297 right: Box<LogicalPlanLite>,
299 on: Vec<JoinKey>,
301 },
302 Sort {
304 input: Box<LogicalPlanLite>,
306 keys: Vec<SortKey>,
308 limit: Option<usize>,
310 },
311 Limit {
313 input: Box<LogicalPlanLite>,
315 limit: usize,
317 },
318}
319
320pub type FragmentId = u32;
326pub type ExchangeId = u32;
328
329#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
331pub enum FragmentAssignment {
332 Tablet(TabletId),
334 Coordinator,
336}
337
338#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
340pub enum BuildSide {
341 Left,
343 Right,
345}
346
347#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
349pub enum FragmentOperator {
350 TabletScan {
352 table: String,
354 predicate: Option<String>,
356 projection: Vec<String>,
358 },
359 RemoteExchangeSource {
361 exchange: ExchangeId,
363 },
364 RemoteExchangeSink {
366 exchange: ExchangeId,
368 },
369 PartialAggregate {
371 group_by: Vec<String>,
373 aggregates: Vec<AggregateExpr>,
375 },
376 FinalAggregate {
378 group_by: Vec<String>,
380 aggregates: Vec<AggregateExpr>,
382 },
383 DistributedHashJoin {
385 on: Vec<JoinKey>,
387 },
388 BroadcastJoin {
391 on: Vec<JoinKey>,
393 build_side: BuildSide,
395 },
396 RepartitionJoin {
398 on: Vec<JoinKey>,
400 },
401 MergeSort {
404 keys: Vec<SortKey>,
406 limit: Option<usize>,
408 },
409 DistributedTopK {
413 k: usize,
415 score: SortKey,
417 },
418 DistributedLimit {
420 limit: usize,
422 },
423}
424
425#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
427pub enum ExchangeKind {
428 HashRepartition {
431 keys: Vec<String>,
433 },
434 Broadcast,
436 Merge,
438}
439
440#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
442pub struct ExchangeDescriptor {
443 pub exchange_id: ExchangeId,
445 pub producer: FragmentId,
447 pub consumer: FragmentId,
449 pub kind: ExchangeKind,
451 pub schema_fingerprint: u64,
454}
455
456#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
458pub struct PlanFragment {
459 pub fragment_id: FragmentId,
461 pub assignment: FragmentAssignment,
463 pub operators: Vec<FragmentOperator>,
465 pub estimated_rows: u64,
467 pub estimated_bytes: u64,
469 pub max_spill_bytes: u64,
471}
472
473#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
476pub struct DistributedPlan {
477 pub query_id: QueryId,
479 pub metadata_version: MetadataVersion,
481 pub fragments: Vec<PlanFragment>,
483 pub exchanges: Vec<ExchangeDescriptor>,
485}
486
487impl DistributedPlan {
488 pub fn root_fragment_id(&self) -> Option<FragmentId> {
490 self.fragments
491 .iter()
492 .map(|fragment| fragment.fragment_id)
493 .find(|id| !self.exchanges.iter().any(|edge| edge.producer == *id))
494 }
495
496 pub fn fragment(&self, fragment_id: FragmentId) -> Option<&PlanFragment> {
498 self.fragments.get(fragment_id as usize)
499 }
500
501 pub fn exchanges_from(
503 &self,
504 producer: FragmentId,
505 ) -> impl Iterator<Item = &ExchangeDescriptor> {
506 self.exchanges
507 .iter()
508 .filter(move |edge| edge.producer == producer)
509 }
510
511 pub fn exchanges_into(&self, consumer: FragmentId) -> Vec<&ExchangeDescriptor> {
513 let mut edges: Vec<&ExchangeDescriptor> = self
514 .exchanges
515 .iter()
516 .filter(|edge| edge.consumer == consumer)
517 .collect();
518 edges.sort_by_key(|edge| edge.producer);
519 edges
520 }
521}
522
523#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
529pub struct PlanDescription {
530 pub query_id: QueryId,
532 pub root: LogicalPlanLite,
534 pub options: PlannerOptions,
536}
537
538#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
540pub struct PlannerOptions {
541 pub broadcast_threshold_bytes: u64,
543 pub max_spill_bytes_per_fragment: u64,
545}
546
547impl Default for PlannerOptions {
548 fn default() -> Self {
549 Self {
550 broadcast_threshold_bytes: DEFAULT_BROADCAST_THRESHOLD_BYTES,
551 max_spill_bytes_per_fragment: DEFAULT_MAX_SPILL_BYTES_PER_FRAGMENT,
552 }
553 }
554}
555
556pub fn distribute(
574 description: &PlanDescription,
575 locator: &dyn TabletLocator,
576 metadata: &dyn ClusterMetadata,
577) -> DistributedResult<DistributedPlan> {
578 validate_lite(&description.root)?;
579 let mut planner = Planner {
580 locator,
581 metadata,
582 options: description.options,
583 fragments: Vec::new(),
584 exchanges: Vec::new(),
585 };
586 let stage = planner.plan_node(&description.root)?;
587 let root = match stage.producers.as_slice() {
588 [only]
589 if planner.fragments[*only as usize].assignment == FragmentAssignment::Coordinator =>
590 {
591 *only
592 }
593 _ => {
594 let gather = planner.push_fragment(
595 FragmentAssignment::Coordinator,
596 Vec::new(),
597 stage.estimated_rows,
598 stage.estimated_bytes,
599 );
600 planner.wire(&stage.producers, &[gather], ExchangeKind::Merge);
601 gather
602 }
603 };
604 debug_assert_eq!(
605 planner
606 .fragments
607 .iter()
608 .filter(|fragment| {
609 !planner
610 .exchanges
611 .iter()
612 .any(|edge| edge.producer == fragment.fragment_id)
613 })
614 .count(),
615 1,
616 "exactly one root fragment"
617 );
618 let _ = root;
619 Ok(DistributedPlan {
620 query_id: description.query_id,
621 metadata_version: metadata.metadata_version(),
622 fragments: planner.fragments,
623 exchanges: planner.exchanges,
624 })
625}
626
627fn validate_lite(node: &LogicalPlanLite) -> DistributedResult<()> {
629 match node {
630 LogicalPlanLite::Scan { table, .. } => {
631 if table.is_empty() {
632 return Err(DistributedError::InvalidPlan(
633 "scan needs a table name".to_owned(),
634 ));
635 }
636 }
637 LogicalPlanLite::Aggregate {
638 input, aggregates, ..
639 } => {
640 for aggregate in aggregates {
641 if aggregate.function != AggregateFunction::Count && aggregate.column.is_none() {
642 return Err(DistributedError::InvalidPlan(format!(
643 "{} needs a column",
644 aggregate.function.name()
645 )));
646 }
647 }
648 validate_lite(input)?;
649 }
650 LogicalPlanLite::Join { left, right, on } => {
651 if on.is_empty() {
652 return Err(DistributedError::InvalidPlan(
653 "join needs at least one key".to_owned(),
654 ));
655 }
656 validate_lite(left)?;
657 validate_lite(right)?;
658 }
659 LogicalPlanLite::Sort { input, keys, .. } => {
660 if keys.is_empty() {
661 return Err(DistributedError::InvalidPlan(
662 "sort needs at least one key".to_owned(),
663 ));
664 }
665 validate_lite(input)?;
666 }
667 LogicalPlanLite::Limit { input, .. } => validate_lite(input)?,
668 }
669 Ok(())
670}
671
672struct Stage {
674 producers: Vec<FragmentId>,
676 estimated_rows: u64,
677 estimated_bytes: u64,
678 partitioning: Option<PartitionSpec>,
680 tablets: Vec<TabletId>,
682}
683
684struct Planner<'a> {
685 locator: &'a dyn TabletLocator,
686 metadata: &'a dyn ClusterMetadata,
687 options: PlannerOptions,
688 fragments: Vec<PlanFragment>,
689 exchanges: Vec<ExchangeDescriptor>,
690}
691
692impl Planner<'_> {
693 fn push_fragment(
694 &mut self,
695 assignment: FragmentAssignment,
696 operators: Vec<FragmentOperator>,
697 estimated_rows: u64,
698 estimated_bytes: u64,
699 ) -> FragmentId {
700 let fragment_id = self.fragments.len() as FragmentId;
701 self.fragments.push(PlanFragment {
702 fragment_id,
703 assignment,
704 operators,
705 estimated_rows,
706 estimated_bytes,
707 max_spill_bytes: self.options.max_spill_bytes_per_fragment,
708 });
709 fragment_id
710 }
711
712 fn wire(&mut self, producers: &[FragmentId], consumers: &[FragmentId], kind: ExchangeKind) {
716 for &producer in producers {
717 let schema_fingerprint = self.schema_fingerprint(producer);
718 for &consumer in consumers {
719 let exchange_id = self.exchanges.len() as ExchangeId;
720 self.exchanges.push(ExchangeDescriptor {
721 exchange_id,
722 producer,
723 consumer,
724 kind: kind.clone(),
725 schema_fingerprint,
726 });
727 self.fragments[producer as usize].operators.push(
728 FragmentOperator::RemoteExchangeSink {
729 exchange: exchange_id,
730 },
731 );
732 self.fragments[consumer as usize].operators.push(
733 FragmentOperator::RemoteExchangeSource {
734 exchange: exchange_id,
735 },
736 );
737 }
738 }
739 }
740
741 fn schema_fingerprint(&self, fragment_id: FragmentId) -> u64 {
743 let columns = fragment_output_columns(&self.fragments[fragment_id as usize]);
744 let mut bytes = Vec::new();
745 for column in &columns {
746 bytes.extend_from_slice(&(column.len() as u32).to_le_bytes());
747 bytes.extend_from_slice(column.as_bytes());
748 }
749 fnv1a64(&bytes)
750 }
751
752 fn metadata_stats(&self, table: &str) -> DistributedResult<TableStats> {
753 self.metadata.table_stats(table)
754 }
755
756 fn plan_node(&mut self, node: &LogicalPlanLite) -> DistributedResult<Stage> {
757 match node {
758 LogicalPlanLite::Scan {
759 table,
760 predicate,
761 projection,
762 } => self.plan_scan(table, predicate, projection),
763 LogicalPlanLite::Aggregate {
764 input,
765 group_by,
766 aggregates,
767 } => self.plan_aggregate(input, group_by, aggregates),
768 LogicalPlanLite::Join { left, right, on } => self.plan_join(left, right, on),
769 LogicalPlanLite::Sort { input, keys, limit } => self.plan_sort(input, keys, *limit),
770 LogicalPlanLite::Limit { input, limit } => self.plan_limit(input, *limit),
771 }
772 }
773
774 fn plan_scan(
775 &mut self,
776 table: &str,
777 predicate: &Option<String>,
778 projection: &[String],
779 ) -> DistributedResult<Stage> {
780 let tablets = self.locator.tablets_for_table(table)?;
781 if tablets.is_empty() {
782 return Err(DistributedError::EmptyLayout {
783 table: table.to_owned(),
784 });
785 }
786 let spec = self.locator.partitioning(table)?;
787 let stats = self.metadata_stats(table)?;
788 let count = tablets.len() as u64;
789 let per_rows = stats.row_count.div_ceil(count);
790 let per_bytes = stats.total_bytes.div_ceil(count);
791 let mut producers = Vec::with_capacity(tablets.len());
792 for tablet in &tablets {
793 producers.push(self.push_fragment(
794 FragmentAssignment::Tablet(*tablet),
795 vec![FragmentOperator::TabletScan {
796 table: table.to_owned(),
797 predicate: predicate.clone(),
798 projection: projection.to_vec(),
799 }],
800 per_rows,
801 per_bytes,
802 ));
803 }
804 Ok(Stage {
805 producers,
806 estimated_rows: stats.row_count,
807 estimated_bytes: stats.total_bytes,
808 partitioning: Some(spec),
809 tablets,
810 })
811 }
812
813 fn plan_aggregate(
814 &mut self,
815 input: &LogicalPlanLite,
816 group_by: &[String],
817 aggregates: &[AggregateExpr],
818 ) -> DistributedResult<Stage> {
819 let child = self.plan_node(input)?;
820 let estimated_rows = if group_by.is_empty() {
821 1
822 } else {
823 (child.estimated_rows / 2).max(1)
824 };
825 let per_row = child
826 .estimated_bytes
827 .checked_div(child.estimated_rows.max(1))
828 .unwrap_or(0)
829 .max(1);
830 let estimated_bytes = estimated_rows.saturating_mul(per_row);
831 if let [only] = child.producers.as_slice() {
832 if self.fragments[*only as usize].assignment == FragmentAssignment::Coordinator {
833 self.fragments[*only as usize].operators.extend([
835 FragmentOperator::PartialAggregate {
836 group_by: group_by.to_vec(),
837 aggregates: aggregates.to_vec(),
838 },
839 FragmentOperator::FinalAggregate {
840 group_by: group_by.to_vec(),
841 aggregates: aggregates.to_vec(),
842 },
843 ]);
844 return Ok(Stage {
845 estimated_rows,
846 estimated_bytes,
847 ..child
848 });
849 }
850 }
851 for &producer in &child.producers {
852 self.fragments[producer as usize]
853 .operators
854 .push(FragmentOperator::PartialAggregate {
855 group_by: group_by.to_vec(),
856 aggregates: aggregates.to_vec(),
857 });
858 }
859 let consumer = self.push_fragment(
860 FragmentAssignment::Coordinator,
861 Vec::new(),
862 estimated_rows,
863 estimated_bytes,
864 );
865 let kind = if group_by.is_empty() {
866 ExchangeKind::Merge
867 } else {
868 ExchangeKind::HashRepartition {
869 keys: group_by.to_vec(),
870 }
871 };
872 self.wire(&child.producers, &[consumer], kind);
873 self.fragments[consumer as usize]
874 .operators
875 .push(FragmentOperator::FinalAggregate {
876 group_by: group_by.to_vec(),
877 aggregates: aggregates.to_vec(),
878 });
879 Ok(Stage {
880 producers: vec![consumer],
881 estimated_rows,
882 estimated_bytes,
883 partitioning: None,
884 tablets: Vec::new(),
885 })
886 }
887
888 fn plan_join(
889 &mut self,
890 left: &LogicalPlanLite,
891 right: &LogicalPlanLite,
892 on: &[JoinKey],
893 ) -> DistributedResult<Stage> {
894 if let (
896 LogicalPlanLite::Scan {
897 table: left_table, ..
898 },
899 LogicalPlanLite::Scan {
900 table: right_table, ..
901 },
902 ) = (left, right)
903 {
904 let left_tablets = self.locator.tablets_for_table(left_table)?;
905 if left_tablets.is_empty() {
906 return Err(DistributedError::EmptyLayout {
907 table: left_table.clone(),
908 });
909 }
910 let right_tablets = self.locator.tablets_for_table(right_table)?;
911 if right_tablets.is_empty() {
912 return Err(DistributedError::EmptyLayout {
913 table: right_table.clone(),
914 });
915 }
916 let left_spec = self.locator.partitioning(left_table)?;
917 let right_spec = self.locator.partitioning(right_table)?;
918 if left_spec.colocated_with(&right_spec) && left_tablets == right_tablets {
919 return self.plan_colocated_join(left, right, on, &left_tablets, &left_spec);
920 }
921 }
922 let left_stage = self.plan_node(left)?;
923 let right_stage = self.plan_node(right)?;
924 if left_stage.estimated_bytes.min(right_stage.estimated_bytes)
925 <= self.options.broadcast_threshold_bytes
926 {
927 self.plan_broadcast_join(left_stage, right_stage, on)
928 } else {
929 self.plan_repartition_join(left_stage, right_stage, on)
930 }
931 }
932
933 fn plan_colocated_join(
936 &mut self,
937 left: &LogicalPlanLite,
938 right: &LogicalPlanLite,
939 on: &[JoinKey],
940 tablets: &[TabletId],
941 spec: &PartitionSpec,
942 ) -> DistributedResult<Stage> {
943 let (
944 LogicalPlanLite::Scan {
945 table: left_table,
946 predicate: left_predicate,
947 projection: left_projection,
948 },
949 LogicalPlanLite::Scan {
950 table: right_table,
951 predicate: right_predicate,
952 projection: right_projection,
953 },
954 ) = (left, right)
955 else {
956 return Err(DistributedError::InvalidPlan(
957 "colocated join needs scan inputs".to_owned(),
958 ));
959 };
960 let left_stats = self.metadata_stats(left_table)?;
961 let right_stats = self.metadata_stats(right_table)?;
962 let estimated_rows = left_stats.row_count.max(right_stats.row_count);
963 let estimated_bytes = left_stats.total_bytes.max(right_stats.total_bytes);
964 let count = tablets.len() as u64;
965 let mut producers = Vec::with_capacity(tablets.len());
966 for tablet in tablets {
967 producers.push(self.push_fragment(
968 FragmentAssignment::Tablet(*tablet),
969 vec![
970 FragmentOperator::TabletScan {
971 table: left_table.clone(),
972 predicate: left_predicate.clone(),
973 projection: left_projection.clone(),
974 },
975 FragmentOperator::TabletScan {
976 table: right_table.clone(),
977 predicate: right_predicate.clone(),
978 projection: right_projection.clone(),
979 },
980 FragmentOperator::DistributedHashJoin { on: on.to_vec() },
981 ],
982 estimated_rows.div_ceil(count),
983 estimated_bytes.div_ceil(count),
984 ));
985 }
986 Ok(Stage {
987 producers,
988 estimated_rows,
989 estimated_bytes,
990 partitioning: Some(spec.clone()),
991 tablets: tablets.to_vec(),
992 })
993 }
994
995 fn plan_broadcast_join(
998 &mut self,
999 left_stage: Stage,
1000 right_stage: Stage,
1001 on: &[JoinKey],
1002 ) -> DistributedResult<Stage> {
1003 let (big, small, build_side) = if right_stage.estimated_bytes <= left_stage.estimated_bytes
1004 {
1005 (left_stage, right_stage, BuildSide::Right)
1006 } else {
1007 (right_stage, left_stage, BuildSide::Left)
1008 };
1009 self.wire(&small.producers, &big.producers, ExchangeKind::Broadcast);
1010 for &producer in &big.producers {
1011 self.fragments[producer as usize]
1012 .operators
1013 .push(FragmentOperator::BroadcastJoin {
1014 on: on.to_vec(),
1015 build_side,
1016 });
1017 }
1018 let estimated_rows = big.estimated_rows;
1019 let estimated_bytes = big.estimated_bytes.saturating_add(small.estimated_bytes);
1020 Ok(Stage {
1021 producers: big.producers,
1022 estimated_rows,
1023 estimated_bytes,
1024 partitioning: big.partitioning,
1025 tablets: big.tablets,
1026 })
1027 }
1028
1029 fn plan_repartition_join(
1032 &mut self,
1033 left_stage: Stage,
1034 right_stage: Stage,
1035 on: &[JoinKey],
1036 ) -> DistributedResult<Stage> {
1037 let join_tablets = if left_stage.tablets.len() >= right_stage.tablets.len() {
1038 left_stage.tablets.clone()
1039 } else {
1040 right_stage.tablets.clone()
1041 };
1042 if join_tablets.is_empty() {
1043 return Err(DistributedError::InvalidPlan(
1044 "repartition join needs at least one tablet-backed input".to_owned(),
1045 ));
1046 }
1047 let width = left_stage.producers.len().max(right_stage.producers.len());
1048 let estimated_rows = left_stage.estimated_rows.max(right_stage.estimated_rows);
1049 let estimated_bytes = left_stage
1050 .estimated_bytes
1051 .saturating_add(right_stage.estimated_bytes);
1052 let mut consumers = Vec::with_capacity(width);
1053 for index in 0..width {
1054 consumers.push(self.push_fragment(
1055 FragmentAssignment::Tablet(join_tablets[index % join_tablets.len()]),
1056 Vec::new(),
1057 estimated_rows.div_ceil(width as u64),
1058 estimated_bytes.div_ceil(width as u64),
1059 ));
1060 }
1061 let left_keys: Vec<String> = on.iter().map(|key| key.left.clone()).collect();
1062 let right_keys: Vec<String> = on.iter().map(|key| key.right.clone()).collect();
1063 self.wire(
1064 &left_stage.producers,
1065 &consumers,
1066 ExchangeKind::HashRepartition { keys: left_keys },
1067 );
1068 self.wire(
1069 &right_stage.producers,
1070 &consumers,
1071 ExchangeKind::HashRepartition { keys: right_keys },
1072 );
1073 for &consumer in &consumers {
1074 self.fragments[consumer as usize]
1075 .operators
1076 .push(FragmentOperator::RepartitionJoin { on: on.to_vec() });
1077 }
1078 Ok(Stage {
1079 producers: consumers,
1080 estimated_rows,
1081 estimated_bytes,
1082 partitioning: None,
1083 tablets: join_tablets,
1084 })
1085 }
1086
1087 fn plan_sort(
1088 &mut self,
1089 input: &LogicalPlanLite,
1090 keys: &[SortKey],
1091 limit: Option<usize>,
1092 ) -> DistributedResult<Stage> {
1093 let child = self.plan_node(input)?;
1094 let top_k = match (limit, keys) {
1097 (Some(k), [score]) if score.descending => Some((k, score.clone())),
1098 _ => None,
1099 };
1100 let estimated_rows = limit.map_or(child.estimated_rows, |limit| {
1101 child.estimated_rows.min(limit as u64)
1102 });
1103 let estimated_bytes =
1104 scaled_bytes(child.estimated_bytes, child.estimated_rows, estimated_rows);
1105 let local_op = match &top_k {
1106 Some((k, score)) => FragmentOperator::DistributedTopK {
1107 k: *k,
1108 score: score.clone(),
1109 },
1110 None => FragmentOperator::MergeSort {
1111 keys: keys.to_vec(),
1112 limit,
1113 },
1114 };
1115 if let [only] = child.producers.as_slice() {
1116 if self.fragments[*only as usize].assignment == FragmentAssignment::Coordinator {
1117 self.fragments[*only as usize].operators.push(local_op);
1118 return Ok(Stage {
1119 estimated_rows,
1120 estimated_bytes,
1121 ..child
1122 });
1123 }
1124 }
1125 for &producer in &child.producers {
1126 self.fragments[producer as usize]
1127 .operators
1128 .push(local_op.clone());
1129 }
1130 let consumer = self.push_fragment(
1131 FragmentAssignment::Coordinator,
1132 Vec::new(),
1133 estimated_rows,
1134 estimated_bytes,
1135 );
1136 self.wire(&child.producers, &[consumer], ExchangeKind::Merge);
1137 let root_op = match &top_k {
1138 Some((k, score)) => FragmentOperator::DistributedTopK {
1139 k: *k,
1140 score: score.clone(),
1141 },
1142 None => FragmentOperator::MergeSort {
1143 keys: keys.to_vec(),
1144 limit,
1145 },
1146 };
1147 self.fragments[consumer as usize].operators.push(root_op);
1148 Ok(Stage {
1149 producers: vec![consumer],
1150 estimated_rows,
1151 estimated_bytes,
1152 partitioning: None,
1153 tablets: Vec::new(),
1154 })
1155 }
1156
1157 fn plan_limit(&mut self, input: &LogicalPlanLite, limit: usize) -> DistributedResult<Stage> {
1158 let child = self.plan_node(input)?;
1159 let estimated_rows = child.estimated_rows.min(limit as u64);
1160 let estimated_bytes =
1161 scaled_bytes(child.estimated_bytes, child.estimated_rows, estimated_rows);
1162 if let [only] = child.producers.as_slice() {
1163 if self.fragments[*only as usize].assignment == FragmentAssignment::Coordinator {
1164 self.fragments[*only as usize]
1165 .operators
1166 .push(FragmentOperator::DistributedLimit { limit });
1167 return Ok(Stage {
1168 estimated_rows,
1169 estimated_bytes,
1170 ..child
1171 });
1172 }
1173 }
1174 for &producer in &child.producers {
1175 self.fragments[producer as usize]
1176 .operators
1177 .push(FragmentOperator::DistributedLimit { limit });
1178 }
1179 let consumer = self.push_fragment(
1180 FragmentAssignment::Coordinator,
1181 Vec::new(),
1182 estimated_rows,
1183 estimated_bytes,
1184 );
1185 self.wire(&child.producers, &[consumer], ExchangeKind::Merge);
1186 self.fragments[consumer as usize]
1187 .operators
1188 .push(FragmentOperator::DistributedLimit { limit });
1189 Ok(Stage {
1190 producers: vec![consumer],
1191 estimated_rows,
1192 estimated_bytes,
1193 partitioning: None,
1194 tablets: Vec::new(),
1195 })
1196 }
1197}
1198
1199fn scaled_bytes(bytes: u64, rows: u64, new_rows: u64) -> u64 {
1201 if rows == 0 {
1202 return 0;
1203 }
1204 bytes
1205 .saturating_mul(new_rows)
1206 .checked_div(rows)
1207 .unwrap_or(bytes)
1208}
1209
1210fn fragment_output_columns(fragment: &PlanFragment) -> Vec<String> {
1212 let mut columns = Vec::new();
1213 for operator in &fragment.operators {
1214 match operator {
1215 FragmentOperator::TabletScan {
1216 table, projection, ..
1217 } => {
1218 if projection.is_empty() {
1219 columns = vec![format!("{table}.*")];
1220 } else {
1221 columns = projection
1222 .iter()
1223 .map(|column| format!("{table}.{column}"))
1224 .collect();
1225 }
1226 }
1227 FragmentOperator::PartialAggregate {
1228 group_by,
1229 aggregates,
1230 } => {
1231 columns = group_by.to_vec();
1232 columns.extend(partial_column_names(aggregates));
1233 }
1234 FragmentOperator::FinalAggregate {
1235 group_by,
1236 aggregates,
1237 } => {
1238 columns = group_by.to_vec();
1239 columns.extend(aggregates.iter().map(aggregate_output_name));
1240 }
1241 FragmentOperator::DistributedHashJoin { .. }
1242 | FragmentOperator::BroadcastJoin { .. }
1243 | FragmentOperator::RepartitionJoin { .. } => {
1244 columns = vec!["*join*".to_owned()];
1245 }
1246 FragmentOperator::RemoteExchangeSource { .. }
1247 | FragmentOperator::RemoteExchangeSink { .. }
1248 | FragmentOperator::MergeSort { .. }
1249 | FragmentOperator::DistributedTopK { .. }
1250 | FragmentOperator::DistributedLimit { .. } => {}
1251 }
1252 }
1253 columns
1254}
1255
1256fn partial_column_names(aggregates: &[AggregateExpr]) -> Vec<String> {
1259 let mut names = Vec::new();
1260 for (index, aggregate) in aggregates.iter().enumerate() {
1261 if aggregate.function == AggregateFunction::Avg {
1262 names.push(format!("__partial_{index}_sum"));
1263 names.push(format!("__partial_{index}_count"));
1264 } else {
1265 names.push(format!("__partial_{index}"));
1266 }
1267 }
1268 names
1269}
1270
1271fn aggregate_output_name(aggregate: &AggregateExpr) -> String {
1273 format!(
1274 "{}_{}",
1275 aggregate.function.name(),
1276 aggregate.column.as_deref().unwrap_or("star")
1277 )
1278}
1279
1280#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1288pub struct TopKCandidate {
1289 pub score: u64,
1291 pub tablet: TabletId,
1293 pub row_id: RowId,
1295}
1296
1297pub fn topk_cmp(a: &TopKCandidate, b: &TopKCandidate) -> Ordering {
1301 b.score
1302 .cmp(&a.score)
1303 .then_with(|| a.tablet.cmp(&b.tablet))
1304 .then_with(|| a.row_id.cmp(&b.row_id))
1305}
1306
1307#[derive(Clone, Debug)]
1310pub struct TabletTopK {
1311 pub tablet: TabletId,
1313 pub rows: Vec<TopKCandidate>,
1315 pub unseen_bound: Option<u64>,
1318}
1319
1320#[derive(Clone, Debug, PartialEq, Eq)]
1322pub struct TopKMerge {
1323 pub winners: Vec<TopKCandidate>,
1325 pub refill: Vec<TabletId>,
1328}
1329
1330pub fn merge_top_k(shards: &[TabletTopK], k: usize) -> TopKMerge {
1353 if k == 0 {
1354 return TopKMerge {
1355 winners: Vec::new(),
1356 refill: Vec::new(),
1357 };
1358 }
1359 let mut received: Vec<TopKCandidate> = shards
1360 .iter()
1361 .flat_map(|shard| shard.rows.iter().copied())
1362 .collect();
1363 received.sort_by(topk_cmp);
1364 received.truncate(k);
1365 let mut refill = Vec::new();
1366 if received.len() < k {
1367 for shard in shards {
1368 if shard.unseen_bound.is_some() {
1369 refill.push(shard.tablet);
1370 }
1371 }
1372 } else {
1373 let threshold = received[k - 1];
1374 for shard in shards {
1375 let Some(bound) = shard.unseen_bound else {
1376 continue;
1377 };
1378 let optimistic = TopKCandidate {
1379 score: bound,
1380 tablet: shard.tablet,
1381 row_id: RowId::MIN,
1382 };
1383 if topk_cmp(&optimistic, &threshold) != Ordering::Greater {
1384 refill.push(shard.tablet);
1385 }
1386 }
1387 }
1388 refill.sort();
1389 refill.dedup();
1390 TopKMerge {
1391 winners: received,
1392 refill,
1393 }
1394}
1395
1396pub fn exact_top_k(
1408 k: usize,
1409 initial: Vec<TabletTopK>,
1410 mut refill_batch: impl FnMut(TabletId) -> TabletTopK,
1411) -> DistributedResult<Vec<TopKCandidate>> {
1412 let mut shards: BTreeMap<TabletId, TabletTopK> = initial
1413 .into_iter()
1414 .map(|shard| (shard.tablet, shard))
1415 .collect();
1416 loop {
1417 let ordered: Vec<TabletTopK> = shards.values().cloned().collect();
1418 let merge = merge_top_k(&ordered, k);
1419 if merge.refill.is_empty() {
1420 return Ok(merge.winners);
1421 }
1422 for tablet in merge.refill {
1423 let batch = refill_batch(tablet);
1424 let entry = shards.get_mut(&tablet).ok_or_else(|| {
1425 DistributedError::InvalidPlan(format!(
1426 "top-k refill requested for unknown tablet {tablet}"
1427 ))
1428 })?;
1429 if batch.rows.is_empty() && batch.unseen_bound == entry.unseen_bound {
1430 return Err(DistributedError::InvalidPlan(format!(
1431 "top-k refill for tablet {tablet} made no progress"
1432 )));
1433 }
1434 entry.rows.extend(batch.rows);
1435 entry.unseen_bound = batch.unseen_bound;
1436 }
1437 }
1438}
1439
1440#[derive(Debug, Clone)]
1448pub struct FragmentControl {
1449 pub control: ExecutionControl,
1452 pub max_spill_bytes: u64,
1454}
1455
1456impl FragmentControl {
1457 pub fn begin_spill(
1461 &self,
1462 manager: &mongreldb_core::SpillManager,
1463 query_id: mongreldb_types::ids::QueryId,
1464 ) -> Result<mongreldb_core::SpillSession, mongreldb_core::SpillError> {
1465 manager.begin_query(query_id, self.max_spill_bytes)
1466 }
1467}
1468
1469#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1473pub enum ScoreBound {
1474 Unknown,
1477 AtMost(u64),
1479 Exhausted,
1481}
1482
1483#[derive(Debug, Clone)]
1485pub struct BatchFrame {
1486 pub batch: RecordBatch,
1488 pub score_bound: ScoreBound,
1490}
1491
1492impl BatchFrame {
1493 pub fn data(batch: RecordBatch) -> Self {
1495 Self {
1496 batch,
1497 score_bound: ScoreBound::Unknown,
1498 }
1499 }
1500}
1501
1502pub type FragmentStream = BoxStream<'static, DistributedResult<BatchFrame>>;
1504
1505#[derive(Debug)]
1508pub struct TopKRefill {
1509 pub rows: Vec<TopKCandidate>,
1512 pub payload: RecordBatch,
1514 pub unseen_bound: Option<u64>,
1516}
1517
1518#[async_trait::async_trait]
1522pub trait FragmentExecutor: Send + Sync {
1523 async fn execute(
1526 &self,
1527 fragment: &PlanFragment,
1528 inputs: Vec<FragmentStream>,
1529 control: FragmentControl,
1530 ) -> DistributedResult<FragmentStream>;
1531
1532 fn refill_top_k(
1536 &self,
1537 fragment: &PlanFragment,
1538 offset: usize,
1539 limit: usize,
1540 ) -> DistributedResult<TopKRefill> {
1541 let _ = (fragment, offset, limit);
1542 Err(DistributedError::Unsupported(
1543 "top-k refill is not implemented by this executor".to_owned(),
1544 ))
1545 }
1546}
1547
1548#[async_trait::async_trait]
1552pub trait FragmentTransport: Send + Sync {
1553 async fn execute_fragment(
1556 &self,
1557 fragment: &PlanFragment,
1558 inputs: Vec<FragmentStream>,
1559 control: FragmentControl,
1560 ) -> DistributedResult<FragmentStream>;
1561
1562 fn cancel_fragment(&self, fragment_id: FragmentId) -> DistributedResult<()>;
1564
1565 fn refill_top_k(
1568 &self,
1569 fragment: &PlanFragment,
1570 offset: usize,
1571 limit: usize,
1572 ) -> DistributedResult<TopKRefill> {
1573 let _ = (fragment, offset, limit);
1574 Err(DistributedError::Unsupported(
1575 "top-k refill over this transport is not bound in this wave".to_owned(),
1576 ))
1577 }
1578}
1579
1580pub struct InMemoryTransport {
1584 default_executor: Arc<dyn FragmentExecutor>,
1585 executors: parking_lot::RwLock<HashMap<TabletId, Arc<dyn FragmentExecutor>>>,
1586 started: Mutex<Vec<FragmentId>>,
1587 cancelled: Mutex<Vec<FragmentId>>,
1588 controls: Mutex<HashMap<FragmentId, ExecutionControl>>,
1589 refills: Mutex<Vec<(FragmentId, usize, usize)>>,
1590}
1591
1592impl InMemoryTransport {
1593 pub fn new(default_executor: Arc<dyn FragmentExecutor>) -> Self {
1595 Self {
1596 default_executor,
1597 executors: parking_lot::RwLock::new(HashMap::new()),
1598 started: Mutex::new(Vec::new()),
1599 cancelled: Mutex::new(Vec::new()),
1600 controls: Mutex::new(HashMap::new()),
1601 refills: Mutex::new(Vec::new()),
1602 }
1603 }
1604
1605 pub fn with_executor(self, tablet: TabletId, executor: Arc<dyn FragmentExecutor>) -> Self {
1607 self.executors.write().insert(tablet, executor);
1608 self
1609 }
1610
1611 fn executor_for(&self, assignment: &FragmentAssignment) -> Arc<dyn FragmentExecutor> {
1612 match assignment {
1613 FragmentAssignment::Tablet(tablet) => self
1614 .executors
1615 .read()
1616 .get(tablet)
1617 .cloned()
1618 .unwrap_or_else(|| Arc::clone(&self.default_executor)),
1619 FragmentAssignment::Coordinator => Arc::clone(&self.default_executor),
1620 }
1621 }
1622
1623 pub fn started_fragments(&self) -> Vec<FragmentId> {
1625 self.started.lock().clone()
1626 }
1627
1628 pub fn cancelled_fragments(&self) -> Vec<FragmentId> {
1630 self.cancelled.lock().clone()
1631 }
1632
1633 pub fn refill_log(&self) -> Vec<(FragmentId, usize, usize)> {
1635 self.refills.lock().clone()
1636 }
1637
1638 pub fn control_for(&self, fragment_id: FragmentId) -> Option<ExecutionControl> {
1640 self.controls.lock().get(&fragment_id).cloned()
1641 }
1642}
1643
1644#[async_trait::async_trait]
1645impl FragmentTransport for InMemoryTransport {
1646 async fn execute_fragment(
1647 &self,
1648 fragment: &PlanFragment,
1649 inputs: Vec<FragmentStream>,
1650 control: FragmentControl,
1651 ) -> DistributedResult<FragmentStream> {
1652 self.started.lock().push(fragment.fragment_id);
1653 self.controls
1654 .lock()
1655 .insert(fragment.fragment_id, control.control.clone());
1656 self.executor_for(&fragment.assignment)
1657 .execute(fragment, inputs, control)
1658 .await
1659 }
1660
1661 fn cancel_fragment(&self, fragment_id: FragmentId) -> DistributedResult<()> {
1662 self.cancelled.lock().push(fragment_id);
1663 if let Some(control) = self.controls.lock().get(&fragment_id) {
1664 control.cancel(CancellationReason::ClientRequest);
1665 }
1666 Ok(())
1667 }
1668
1669 fn refill_top_k(
1670 &self,
1671 fragment: &PlanFragment,
1672 offset: usize,
1673 limit: usize,
1674 ) -> DistributedResult<TopKRefill> {
1675 self.refills
1676 .lock()
1677 .push((fragment.fragment_id, offset, limit));
1678 self.executor_for(&fragment.assignment)
1679 .refill_top_k(fragment, offset, limit)
1680 }
1681}
1682
1683#[derive(Default)]
1686pub struct InMemoryTableStore {
1687 tables: parking_lot::RwLock<HashMap<(String, TabletId), Vec<RecordBatch>>>,
1688 schemas: parking_lot::RwLock<HashMap<String, SchemaRef>>,
1689}
1690
1691impl InMemoryTableStore {
1692 pub fn new() -> Self {
1694 Self::default()
1695 }
1696
1697 pub fn insert(&self, table: &str, tablet: TabletId, batch: RecordBatch) {
1700 self.schemas
1701 .write()
1702 .entry(table.to_owned())
1703 .or_insert_with(|| batch.schema());
1704 self.tables
1705 .write()
1706 .entry((table.to_owned(), tablet))
1707 .or_default()
1708 .push(batch);
1709 }
1710
1711 pub fn register_schema(&self, table: &str, schema: SchemaRef) {
1713 self.schemas.write().insert(table.to_owned(), schema);
1714 }
1715
1716 pub fn snapshot(&self, table: &str, tablet: TabletId) -> Vec<RecordBatch> {
1718 self.tables
1719 .read()
1720 .get(&(table.to_owned(), tablet))
1721 .cloned()
1722 .unwrap_or_default()
1723 }
1724
1725 pub fn schema(&self, table: &str) -> Option<SchemaRef> {
1727 self.schemas.read().get(table).cloned()
1728 }
1729}
1730
1731pub struct InMemoryFragmentExecutor {
1737 store: Arc<InMemoryTableStore>,
1738 topk_emit_batch: Option<usize>,
1742}
1743
1744impl InMemoryFragmentExecutor {
1745 pub fn new(store: Arc<InMemoryTableStore>) -> Self {
1747 Self {
1748 store,
1749 topk_emit_batch: None,
1750 }
1751 }
1752
1753 pub fn with_topk_emit_batch(store: Arc<InMemoryTableStore>, batch: usize) -> Self {
1755 Self {
1756 store,
1757 topk_emit_batch: Some(batch),
1758 }
1759 }
1760
1761 fn scan_batches(&self, fragment: &PlanFragment) -> DistributedResult<Vec<RecordBatch>> {
1763 let tablet = tablet_of(fragment)?;
1764 let scan = fragment
1765 .operators
1766 .iter()
1767 .find_map(|operator| match operator {
1768 FragmentOperator::TabletScan {
1769 table, projection, ..
1770 } => Some((table, projection)),
1771 _ => None,
1772 })
1773 .ok_or_else(|| {
1774 DistributedError::InvalidPlan(format!(
1775 "fragment {} has no tablet scan",
1776 fragment.fragment_id
1777 ))
1778 })?;
1779 let mut batches = self.store.snapshot(scan.0, tablet);
1780 if batches.is_empty() {
1781 if let Some(schema) = self.store.schema(scan.0) {
1782 batches = vec![RecordBatch::new_empty(schema)];
1783 }
1784 }
1785 if !scan.1.is_empty() {
1786 batches = project_batches(&batches, scan.1)?;
1787 }
1788 Ok(batches)
1789 }
1790}
1791
1792#[async_trait::async_trait]
1793impl FragmentExecutor for InMemoryFragmentExecutor {
1794 async fn execute(
1795 &self,
1796 fragment: &PlanFragment,
1797 inputs: Vec<FragmentStream>,
1798 control: FragmentControl,
1799 ) -> DistributedResult<FragmentStream> {
1800 let mut batches: Vec<RecordBatch> = Vec::new();
1801 let mut inputs = inputs.into_iter();
1802 let mut bound = ScoreBound::Unknown;
1803 for operator in &fragment.operators {
1804 checkpoint(&control.control)?;
1805 match operator {
1806 FragmentOperator::TabletScan { .. } => {
1807 batches = self.scan_batches(fragment)?;
1808 }
1809 FragmentOperator::RemoteExchangeSource { .. } => {
1810 let input = inputs.next().ok_or_else(|| {
1811 DistributedError::InvalidPlan(format!(
1812 "fragment {} is missing an exchange input stream",
1813 fragment.fragment_id
1814 ))
1815 })?;
1816 let frames = drain_stream(input, &control.control).await?;
1817 batches.extend(frames.into_iter().map(|frame| frame.batch));
1818 }
1819 FragmentOperator::PartialAggregate {
1820 group_by,
1821 aggregates,
1822 } => {
1823 batches = vec![partial_aggregate_batches(&batches, group_by, aggregates)?];
1824 }
1825 FragmentOperator::FinalAggregate {
1826 group_by,
1827 aggregates,
1828 } => {
1829 batches = vec![final_aggregate_batches(&batches, group_by, aggregates)?];
1830 }
1831 FragmentOperator::MergeSort { keys, limit } => {
1832 batches = sort_batches_local(&batches, keys, *limit)?;
1833 }
1834 FragmentOperator::DistributedTopK { k, score } => {
1835 let tablet = tablet_of(fragment)?;
1836 let input = prepare_top_k(&batches, &score.column, tablet)?;
1837 let emit = self.topk_emit_batch.unwrap_or(*k).min(*k);
1838 let (_rows, payload, next) = input.emit(0, emit);
1839 batches = vec![payload];
1840 bound = match next {
1841 Some(next) => ScoreBound::AtMost(next),
1842 None => ScoreBound::Exhausted,
1843 };
1844 }
1845 FragmentOperator::DistributedLimit { limit } => {
1846 batches = limit_batches(&batches, *limit);
1847 }
1848 FragmentOperator::RemoteExchangeSink { .. } => {
1849 }
1852 FragmentOperator::DistributedHashJoin { .. }
1853 | FragmentOperator::BroadcastJoin { .. }
1854 | FragmentOperator::RepartitionJoin { .. } => {
1855 return Err(DistributedError::Unsupported(
1856 "join execution binding lands with the tablet wave".to_owned(),
1857 ));
1858 }
1859 }
1860 }
1861 let mut frames: Vec<BatchFrame> = batches.into_iter().map(BatchFrame::data).collect();
1862 if bound != ScoreBound::Unknown {
1863 if let Some(last) = frames.last_mut() {
1864 last.score_bound = bound;
1865 }
1866 }
1867 Ok(Box::pin(stream::iter(frames.into_iter().map(Ok))))
1868 }
1869
1870 fn refill_top_k(
1871 &self,
1872 fragment: &PlanFragment,
1873 offset: usize,
1874 limit: usize,
1875 ) -> DistributedResult<TopKRefill> {
1876 let tablet = tablet_of(fragment)?;
1877 let score = fragment
1878 .operators
1879 .iter()
1880 .find_map(|operator| match operator {
1881 FragmentOperator::DistributedTopK { score, .. } => Some(score.clone()),
1882 _ => None,
1883 })
1884 .ok_or_else(|| {
1885 DistributedError::InvalidPlan(format!(
1886 "fragment {} has no distributed top-k operator",
1887 fragment.fragment_id
1888 ))
1889 })?;
1890 let batches = self.scan_batches(fragment)?;
1891 let input = prepare_top_k(&batches, &score.column, tablet)?;
1892 let (rows, payload, unseen_bound) = input.emit(offset, limit);
1893 Ok(TopKRefill {
1894 rows,
1895 payload,
1896 unseen_bound,
1897 })
1898 }
1899}
1900
1901fn checkpoint(control: &ExecutionControl) -> DistributedResult<()> {
1907 control
1908 .checkpoint()
1909 .map_err(|_| DistributedError::Cancelled(control.reason()))
1910}
1911
1912fn tablet_of(fragment: &PlanFragment) -> DistributedResult<TabletId> {
1914 match fragment.assignment {
1915 FragmentAssignment::Tablet(tablet) => Ok(tablet),
1916 FragmentAssignment::Coordinator => Err(DistributedError::InvalidPlan(format!(
1917 "fragment {} operator requires a tablet assignment",
1918 fragment.fragment_id
1919 ))),
1920 }
1921}
1922
1923async fn drain_stream(
1925 mut stream: FragmentStream,
1926 control: &ExecutionControl,
1927) -> DistributedResult<Vec<BatchFrame>> {
1928 let mut frames = Vec::new();
1929 while let Some(item) = stream.next().await {
1930 checkpoint(control)?;
1931 frames.push(item?);
1932 }
1933 Ok(frames)
1934}
1935
1936fn concat_all(batches: &[RecordBatch]) -> DistributedResult<Option<RecordBatch>> {
1938 let Some(first) = batches.first() else {
1939 return Ok(None);
1940 };
1941 if batches.len() == 1 {
1942 return Ok(Some(first.clone()));
1943 }
1944 Ok(Some(concat_batches(&first.schema(), batches)?))
1945}
1946
1947fn project_batches(
1949 batches: &[RecordBatch],
1950 projection: &[String],
1951) -> DistributedResult<Vec<RecordBatch>> {
1952 batches
1953 .iter()
1954 .map(|batch| {
1955 let schema = batch.schema();
1956 let indexes: Vec<usize> = projection
1957 .iter()
1958 .map(|name| {
1959 schema.index_of(name).map_err(|_| {
1960 DistributedError::InvalidPlan(format!(
1961 "projection column `{name}` not in schema"
1962 ))
1963 })
1964 })
1965 .collect::<DistributedResult<Vec<usize>>>()?;
1966 Ok(batch.project(&indexes)?)
1967 })
1968 .collect()
1969}
1970
1971fn row_converter(
1973 schema: &Schema,
1974 keys: &[SortKey],
1975) -> DistributedResult<(RowConverter, Vec<usize>)> {
1976 let mut fields = Vec::with_capacity(keys.len());
1977 let mut indexes = Vec::with_capacity(keys.len());
1978 for key in keys {
1979 let index = schema.index_of(&key.column).map_err(|_| {
1980 DistributedError::InvalidPlan(format!("sort key `{}` not in schema", key.column))
1981 })?;
1982 let options = arrow::compute::SortOptions {
1986 descending: key.descending,
1987 nulls_first: key.descending,
1988 };
1989 fields.push(SortField::new_with_options(
1990 schema.field(index).data_type().clone(),
1991 options,
1992 ));
1993 indexes.push(index);
1994 }
1995 Ok((RowConverter::new(fields)?, indexes))
1996}
1997
1998fn take_rows(batch: &RecordBatch, order: &[usize]) -> DistributedResult<Vec<RecordBatch>> {
2000 let mut out = Vec::new();
2001 for chunk in order.chunks(COORDINATOR_OUTPUT_BATCH_ROWS) {
2002 let indices = UInt32Array::from(
2003 chunk
2004 .iter()
2005 .map(|index| u32::try_from(*index).unwrap_or(u32::MAX))
2006 .collect::<Vec<u32>>(),
2007 );
2008 let mut columns = Vec::with_capacity(batch.num_columns());
2009 for column in batch.columns() {
2010 columns.push(take(column, &indices, None)?);
2011 }
2012 out.push(RecordBatch::try_new(batch.schema(), columns)?);
2013 }
2014 if out.is_empty() {
2015 out.push(RecordBatch::new_empty(batch.schema()));
2016 }
2017 Ok(out)
2018}
2019
2020fn emit_interleaved(
2023 streams: &[RecordBatch],
2024 order: &[(usize, usize)],
2025) -> DistributedResult<Vec<RecordBatch>> {
2026 let Some(first) = streams.first() else {
2027 return Ok(Vec::new());
2028 };
2029 if order.is_empty() {
2030 return Ok(vec![RecordBatch::new_empty(first.schema())]);
2031 }
2032 let schema = first.schema();
2033 let mut out = Vec::new();
2034 for chunk in order.chunks(COORDINATOR_OUTPUT_BATCH_ROWS) {
2035 let mut columns = Vec::with_capacity(schema.fields().len());
2036 for column_index in 0..schema.fields().len() {
2037 let refs: Vec<&dyn Array> = streams
2038 .iter()
2039 .map(|batch| batch.column(column_index).as_ref())
2040 .collect();
2041 columns.push(interleave(&refs, chunk)?);
2042 }
2043 out.push(RecordBatch::try_new(schema.clone(), columns)?);
2044 }
2045 Ok(out)
2046}
2047
2048fn sort_batches_local(
2051 batches: &[RecordBatch],
2052 keys: &[SortKey],
2053 limit: Option<usize>,
2054) -> DistributedResult<Vec<RecordBatch>> {
2055 let Some(batch) = concat_all(batches)? else {
2056 return Ok(Vec::new());
2057 };
2058 if batch.num_rows() == 0 {
2059 return Ok(vec![RecordBatch::new_empty(batch.schema())]);
2060 }
2061 let (converter, indexes) = row_converter(&batch.schema(), keys)?;
2062 let columns: Vec<ArrayRef> = indexes
2063 .iter()
2064 .map(|index| batch.column(*index).clone())
2065 .collect();
2066 let rows = converter.convert_columns(&columns)?;
2067 let mut order: Vec<usize> = (0..batch.num_rows()).collect();
2068 order.sort_by(|left, right| {
2069 rows.row(*left)
2070 .cmp(&rows.row(*right))
2071 .then_with(|| left.cmp(right))
2072 });
2073 if let Some(limit) = limit {
2074 order.truncate(limit);
2075 }
2076 take_rows(&batch, &order)
2077}
2078
2079fn merge_sorted_streams(
2083 streams: &[RecordBatch],
2084 keys: &[SortKey],
2085 limit: Option<usize>,
2086) -> DistributedResult<Vec<RecordBatch>> {
2087 struct MergeItem {
2089 key: Vec<u8>,
2090 stream: usize,
2091 }
2092 impl PartialEq for MergeItem {
2093 fn eq(&self, other: &Self) -> bool {
2094 self.key == other.key && self.stream == other.stream
2095 }
2096 }
2097 impl Eq for MergeItem {}
2098 impl PartialOrd for MergeItem {
2099 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
2100 Some(self.cmp(other))
2101 }
2102 }
2103 impl Ord for MergeItem {
2104 fn cmp(&self, other: &Self) -> Ordering {
2105 self.key
2106 .cmp(&other.key)
2107 .then_with(|| self.stream.cmp(&other.stream))
2108 }
2109 }
2110
2111 let mut per_stream: Vec<Vec<Vec<u8>>> = Vec::with_capacity(streams.len());
2112 for batch in streams {
2113 let (converter, indexes) = row_converter(&batch.schema(), keys)?;
2114 let columns: Vec<ArrayRef> = indexes
2115 .iter()
2116 .map(|index| batch.column(*index).clone())
2117 .collect();
2118 let rows = converter.convert_columns(&columns)?;
2119 per_stream.push(
2120 (0..batch.num_rows())
2121 .map(|row| rows.row(row).as_ref().to_vec())
2122 .collect(),
2123 );
2124 }
2125 let mut cursors: Vec<usize> = vec![0; streams.len()];
2126 let mut heap = BinaryHeap::new();
2127 for (stream, keys) in per_stream.iter().enumerate() {
2128 if let Some(key) = keys.first() {
2129 heap.push(std::cmp::Reverse(MergeItem {
2130 key: key.clone(),
2131 stream,
2132 }));
2133 }
2134 }
2135 let mut order = Vec::new();
2136 while let Some(std::cmp::Reverse(item)) = heap.pop() {
2137 let row = cursors[item.stream];
2138 order.push((item.stream, row));
2139 if limit.is_some_and(|limit| order.len() >= limit) {
2140 break;
2141 }
2142 cursors[item.stream] += 1;
2143 let cursor = cursors[item.stream];
2144 if let Some(key) = per_stream[item.stream].get(cursor) {
2145 heap.push(std::cmp::Reverse(MergeItem {
2146 key: key.clone(),
2147 stream: item.stream,
2148 }));
2149 }
2150 }
2151 emit_interleaved(streams, &order)
2152}
2153
2154fn limit_batches(batches: &[RecordBatch], limit: usize) -> Vec<RecordBatch> {
2156 let mut remaining = limit;
2157 let mut out = Vec::new();
2158 for batch in batches {
2159 if remaining == 0 {
2160 break;
2161 }
2162 let rows = batch.num_rows().min(remaining);
2163 if rows == 0 {
2164 continue;
2165 }
2166 out.push(if rows == batch.num_rows() {
2167 batch.clone()
2168 } else {
2169 batch.slice(0, rows)
2170 });
2171 remaining -= rows;
2172 }
2173 out
2174}
2175
2176fn repartition_frames(
2180 frames: &[BatchFrame],
2181 keys: &[String],
2182 width: usize,
2183 index: usize,
2184) -> DistributedResult<Vec<BatchFrame>> {
2185 let batches: Vec<RecordBatch> = frames.iter().map(|frame| frame.batch.clone()).collect();
2186 let Some(batch) = concat_all(&batches)? else {
2187 return Ok(Vec::new());
2188 };
2189 if batch.num_rows() == 0 || width <= 1 {
2190 return Ok(vec![BatchFrame::data(batch)]);
2191 }
2192 let schema = batch.schema();
2193 let mut key_columns = Vec::with_capacity(keys.len());
2194 for key in keys {
2195 let column_index = schema.index_of(key).map_err(|_| {
2196 DistributedError::InvalidPlan(format!("repartition key `{key}` not in schema"))
2197 })?;
2198 key_columns.push(batch.column(column_index).clone());
2199 }
2200 let converter = RowConverter::new(
2201 key_columns
2202 .iter()
2203 .map(|column| SortField::new(column.data_type().clone()))
2204 .collect::<Vec<SortField>>(),
2205 )?;
2206 let rows = converter.convert_columns(&key_columns)?;
2207 let mut order = Vec::new();
2208 for row in 0..batch.num_rows() {
2209 let bucket = (fnv1a64(rows.row(row).as_ref()) % width as u64) as usize;
2210 if bucket == index {
2211 order.push(row);
2212 }
2213 }
2214 Ok(take_rows(&batch, &order)?
2215 .into_iter()
2216 .map(BatchFrame::data)
2217 .collect())
2218}
2219
2220fn score_key(array: &dyn Array, row: usize) -> DistributedResult<u64> {
2223 if array.is_null(row) {
2224 return Ok(0);
2225 }
2226 match array.data_type() {
2227 DataType::UInt64 => Ok(array
2228 .as_any()
2229 .downcast_ref::<UInt64Array>()
2230 .expect("type checked")
2231 .value(row)),
2232 DataType::Int64 => {
2233 let value = array
2234 .as_any()
2235 .downcast_ref::<Int64Array>()
2236 .expect("type checked")
2237 .value(row);
2238 Ok((value as u64) ^ (1_u64 << 63))
2239 }
2240 DataType::Float64 => {
2241 let bits = array
2242 .as_any()
2243 .downcast_ref::<Float64Array>()
2244 .expect("type checked")
2245 .value(row)
2246 .to_bits();
2247 Ok(if bits & (1_u64 << 63) != 0 {
2248 !bits
2249 } else {
2250 bits | (1_u64 << 63)
2251 })
2252 }
2253 other => Err(DistributedError::Unsupported(format!(
2254 "top-k score column of type {other} is not supported in this wave"
2255 ))),
2256 }
2257}
2258
2259fn row_ids(batch: &RecordBatch) -> DistributedResult<UInt64Array> {
2261 let index = batch.schema().index_of(TOPK_ROWID_COLUMN).map_err(|_| {
2262 DistributedError::InvalidPlan(format!(
2263 "top-k stream is missing the `{TOPK_ROWID_COLUMN}` column"
2264 ))
2265 })?;
2266 let array = batch.column(index);
2267 if array.data_type() != &DataType::UInt64 {
2268 return Err(DistributedError::InvalidPlan(format!(
2269 "`{TOPK_ROWID_COLUMN}` must be UInt64, found {}",
2270 array.data_type()
2271 )));
2272 }
2273 Ok(array
2274 .as_any()
2275 .downcast_ref::<UInt64Array>()
2276 .expect("type checked")
2277 .clone())
2278}
2279
2280struct TopKInput {
2283 batch: RecordBatch,
2284 candidates: Vec<TopKCandidate>,
2286 positions: Vec<usize>,
2288}
2289
2290impl TopKInput {
2291 fn emit(&self, offset: usize, limit: usize) -> (Vec<TopKCandidate>, RecordBatch, Option<u64>) {
2295 let offset = offset.min(self.candidates.len());
2296 let end = (offset + limit).min(self.candidates.len());
2297 let rows = self.candidates[offset..end].to_vec();
2298 let positions = &self.positions[offset..end];
2299 let payload = take_rows(&self.batch, positions)
2300 .unwrap_or_else(|_| vec![RecordBatch::new_empty(self.batch.schema())])
2301 .into_iter()
2302 .next()
2303 .unwrap_or_else(|| RecordBatch::new_empty(self.batch.schema()));
2304 let bound = self.candidates.get(end).map(|candidate| candidate.score);
2305 (rows, payload, bound)
2306 }
2307}
2308
2309fn prepare_top_k(
2311 batches: &[RecordBatch],
2312 score_column: &str,
2313 tablet: TabletId,
2314) -> DistributedResult<TopKInput> {
2315 let Some(batch) = concat_all(batches)? else {
2316 return Err(DistributedError::InvalidPlan(
2317 "top-k input has no batches".to_owned(),
2318 ));
2319 };
2320 let schema = batch.schema();
2321 let score_index = schema.index_of(score_column).map_err(|_| {
2322 DistributedError::InvalidPlan(format!("top-k score column `{score_column}` not in schema"))
2323 })?;
2324 let score_array = batch.column(score_index).clone();
2325 let row_id_array = row_ids(&batch)?;
2326 let mut candidates = Vec::with_capacity(batch.num_rows());
2327 for row in 0..batch.num_rows() {
2328 candidates.push(TopKCandidate {
2329 score: score_key(score_array.as_ref(), row)?,
2330 tablet,
2331 row_id: RowId(row_id_array.value(row)),
2332 });
2333 }
2334 let mut positions: Vec<usize> = (0..candidates.len()).collect();
2335 positions.sort_by(|left, right| topk_cmp(&candidates[*left], &candidates[*right]));
2336 let candidates = positions.iter().map(|index| candidates[*index]).collect();
2337 Ok(TopKInput {
2338 batch,
2339 candidates,
2340 positions,
2341 })
2342}
2343
2344#[derive(Clone, Copy, Debug)]
2351enum AggValue {
2352 I64(i64),
2353 F64(f64),
2354}
2355
2356fn numeric_cell(array: &dyn Array, row: usize) -> DistributedResult<Option<AggValue>> {
2358 if array.is_null(row) {
2359 return Ok(None);
2360 }
2361 match array.data_type() {
2362 DataType::Int64 => Ok(Some(AggValue::I64(
2363 array
2364 .as_any()
2365 .downcast_ref::<Int64Array>()
2366 .expect("type checked")
2367 .value(row),
2368 ))),
2369 DataType::Float64 => Ok(Some(AggValue::F64(
2370 array
2371 .as_any()
2372 .downcast_ref::<Float64Array>()
2373 .expect("type checked")
2374 .value(row),
2375 ))),
2376 other => Err(DistributedError::Unsupported(format!(
2377 "aggregate over {other} is not supported in this wave"
2378 ))),
2379 }
2380}
2381
2382#[derive(Clone, Debug)]
2384enum AggAccum {
2385 Count(i64),
2386 SumI(i128, bool),
2387 SumF(f64, bool),
2388 MinI(i64, bool),
2389 MinF(f64, bool),
2390 MaxI(i64, bool),
2391 MaxF(f64, bool),
2392 Avg { sum: f64, count: i64 },
2393}
2394
2395impl AggAccum {
2396 fn fresh(function: AggregateFunction, float: bool) -> Self {
2399 match function {
2400 AggregateFunction::Count => Self::Count(0),
2401 AggregateFunction::Sum if float => Self::SumF(0.0, false),
2402 AggregateFunction::Sum => Self::SumI(0, false),
2403 AggregateFunction::Min if float => Self::MinF(f64::INFINITY, false),
2404 AggregateFunction::Min => Self::MinI(i64::MAX, false),
2405 AggregateFunction::Max if float => Self::MaxF(f64::NEG_INFINITY, false),
2406 AggregateFunction::Max => Self::MaxI(i64::MIN, false),
2407 AggregateFunction::Avg => Self::Avg { sum: 0.0, count: 0 },
2408 }
2409 }
2410
2411 fn fold_value(&mut self, cell: Option<AggValue>) {
2413 match (self, cell) {
2414 (Self::SumI(sum, seen), Some(AggValue::I64(value))) => {
2415 *sum += i128::from(value);
2416 *seen = true;
2417 }
2418 (Self::SumF(sum, seen), Some(AggValue::F64(value))) => {
2419 *sum += value;
2420 *seen = true;
2421 }
2422 (Self::MinI(min, seen), Some(AggValue::I64(value))) => {
2423 *min = (*min).min(value);
2424 *seen = true;
2425 }
2426 (Self::MinF(min, seen), Some(AggValue::F64(value))) => {
2427 *min = min.min(value);
2428 *seen = true;
2429 }
2430 (Self::MaxI(max, seen), Some(AggValue::I64(value))) => {
2431 *max = (*max).max(value);
2432 *seen = true;
2433 }
2434 (Self::MaxF(max, seen), Some(AggValue::F64(value))) => {
2435 *max = max.max(value);
2436 *seen = true;
2437 }
2438 (Self::Avg { sum, count }, Some(AggValue::I64(value))) => {
2439 *sum += value as f64;
2440 *count += 1;
2441 }
2442 (Self::Avg { sum, count }, Some(AggValue::F64(value))) => {
2443 *sum += value;
2444 *count += 1;
2445 }
2446 _ => {}
2447 }
2448 }
2449}
2450
2451struct GroupEntry {
2453 first_row: usize,
2456 accums: Vec<AggAccum>,
2457}
2458
2459#[derive(Default)]
2461struct GroupFold {
2462 order: Vec<String>,
2463 groups: HashMap<String, GroupEntry>,
2464}
2465
2466impl GroupFold {
2467 fn entry(&mut self, key: String, row: usize, templates: &[AggAccum]) -> &mut GroupEntry {
2468 if !self.groups.contains_key(&key) {
2469 self.order.push(key.clone());
2470 self.groups.insert(
2471 key.clone(),
2472 GroupEntry {
2473 first_row: row,
2474 accums: templates.to_vec(),
2475 },
2476 );
2477 }
2478 self.groups.get_mut(&key).expect("inserted above")
2479 }
2480}
2481
2482fn group_key(batch: &RecordBatch, key_indexes: &[usize], row: usize) -> DistributedResult<String> {
2484 let mut parts = Vec::with_capacity(key_indexes.len());
2485 for index in key_indexes {
2486 parts.push(array_value_to_string(batch.column(*index).as_ref(), row)?);
2487 }
2488 Ok(parts.join("\u{1f}"))
2489}
2490
2491fn resolve_columns(schema: &Schema, columns: &[String]) -> DistributedResult<Vec<usize>> {
2493 columns
2494 .iter()
2495 .map(|column| {
2496 schema.index_of(column).map_err(|_| {
2497 DistributedError::InvalidPlan(format!("group-by column `{column}` not in schema"))
2498 })
2499 })
2500 .collect()
2501}
2502
2503fn aggregate_float(schema: &Schema, aggregate: &AggregateExpr) -> DistributedResult<bool> {
2505 match &aggregate.column {
2506 None => Ok(false),
2507 Some(column) => {
2508 let index = schema.index_of(column).map_err(|_| {
2509 DistributedError::InvalidPlan(format!("aggregate column `{column}` not in schema"))
2510 })?;
2511 match schema.field(index).data_type() {
2512 DataType::Int64 => Ok(false),
2513 DataType::Float64 => Ok(true),
2514 other => Err(DistributedError::Unsupported(format!(
2515 "aggregate over {other} is not supported in this wave"
2516 ))),
2517 }
2518 }
2519 }
2520}
2521
2522fn take_group_keys(
2524 batch: &RecordBatch,
2525 key_indexes: &[usize],
2526 fold: &GroupFold,
2527) -> DistributedResult<Vec<ArrayRef>> {
2528 if key_indexes.is_empty() {
2529 return Ok(Vec::new());
2530 }
2531 let indices = UInt32Array::from(
2532 fold.order
2533 .iter()
2534 .map(|key| u32::try_from(fold.groups[key].first_row).unwrap_or(u32::MAX))
2535 .collect::<Vec<u32>>(),
2536 );
2537 let mut columns = Vec::with_capacity(key_indexes.len());
2538 for index in key_indexes {
2539 columns.push(take(batch.column(*index), &indices, None)?);
2540 }
2541 Ok(columns)
2542}
2543
2544fn emit_int_accum(groups: &GroupFold, index: usize) -> DistributedResult<Vec<Option<i64>>> {
2546 let mut values = Vec::with_capacity(groups.order.len());
2547 for key in &groups.order {
2548 let accum = &groups.groups[key].accums[index];
2549 values.push(match accum {
2550 AggAccum::Count(count) => Some(*count),
2551 AggAccum::SumI(sum, seen) => seen
2552 .then(|| {
2553 i64::try_from(*sum).map_err(|_| {
2554 DistributedError::Unsupported(
2555 "integer sum overflow in this wave".to_owned(),
2556 )
2557 })
2558 })
2559 .transpose()?,
2560 AggAccum::MinI(value, seen) | AggAccum::MaxI(value, seen) => seen.then_some(*value),
2561 other => {
2562 return Err(DistributedError::InvalidPlan(format!(
2563 "internal: accumulator {other:?} is not an integer column"
2564 )))
2565 }
2566 });
2567 }
2568 Ok(values)
2569}
2570
2571fn emit_float_accum(groups: &GroupFold, index: usize) -> DistributedResult<Vec<Option<f64>>> {
2573 let mut values = Vec::with_capacity(groups.order.len());
2574 for key in &groups.order {
2575 let accum = &groups.groups[key].accums[index];
2576 values.push(match accum {
2577 AggAccum::SumF(sum, seen) => seen.then_some(*sum),
2578 AggAccum::MinF(value, seen) | AggAccum::MaxF(value, seen) => seen.then_some(*value),
2579 other => {
2580 return Err(DistributedError::InvalidPlan(format!(
2581 "internal: accumulator {other:?} is not a float column"
2582 )))
2583 }
2584 });
2585 }
2586 Ok(values)
2587}
2588
2589fn partial_aggregate_batches(
2592 batches: &[RecordBatch],
2593 group_by: &[String],
2594 aggregates: &[AggregateExpr],
2595) -> DistributedResult<RecordBatch> {
2596 let Some(batch) = concat_all(batches)? else {
2597 return Err(DistributedError::InvalidPlan(
2598 "aggregate input has no batches".to_owned(),
2599 ));
2600 };
2601 let schema = batch.schema();
2602 let key_indexes = resolve_columns(&schema, group_by)?;
2603 let mut value_indexes = Vec::with_capacity(aggregates.len());
2604 let mut templates = Vec::with_capacity(aggregates.len());
2605 for aggregate in aggregates {
2606 let float = aggregate_float(&schema, aggregate)?;
2607 value_indexes.push(match &aggregate.column {
2608 Some(column) => Some(schema.index_of(column).map_err(|_| {
2609 DistributedError::InvalidPlan(format!("aggregate column `{column}` not in schema"))
2610 })?),
2611 None => None,
2612 });
2613 templates.push(AggAccum::fresh(aggregate.function, float));
2614 }
2615 let mut fold = GroupFold::default();
2616 for row in 0..batch.num_rows() {
2617 let key = group_key(&batch, &key_indexes, row)?;
2618 let entry = fold.entry(key, row, &templates);
2619 for (index, aggregate) in aggregates.iter().enumerate() {
2620 match aggregate.function {
2621 AggregateFunction::Count => {
2622 let counted = match value_indexes[index] {
2623 Some(column) => !batch.column(column).is_null(row),
2624 None => true,
2625 };
2626 if counted {
2627 let AggAccum::Count(count) = &mut entry.accums[index] else {
2628 unreachable!("count template");
2629 };
2630 *count += 1;
2631 }
2632 }
2633 _ => {
2634 let cell = match value_indexes[index] {
2635 Some(column) => numeric_cell(batch.column(column).as_ref(), row)?,
2636 None => None,
2637 };
2638 entry.accums[index].fold_value(cell);
2639 }
2640 }
2641 }
2642 }
2643 let mut columns = take_group_keys(&batch, &key_indexes, &fold)?;
2645 let mut fields: Vec<Field> = key_indexes
2646 .iter()
2647 .map(|index| schema.field(*index).clone())
2648 .collect();
2649 for (index, aggregate) in aggregates.iter().enumerate() {
2650 match aggregate.function {
2651 AggregateFunction::Avg => {
2652 let mut sums = Vec::with_capacity(fold.order.len());
2653 let mut counts = Vec::with_capacity(fold.order.len());
2654 for key in &fold.order {
2655 let AggAccum::Avg { sum, count } = &fold.groups[key].accums[index] else {
2656 unreachable!("avg template");
2657 };
2658 sums.push(Some(*sum));
2659 counts.push(Some(*count));
2660 }
2661 fields.push(Field::new(
2662 format!("__partial_{index}_sum"),
2663 DataType::Float64,
2664 true,
2665 ));
2666 columns.push(Arc::new(Float64Array::from(sums)));
2667 fields.push(Field::new(
2668 format!("__partial_{index}_count"),
2669 DataType::Int64,
2670 true,
2671 ));
2672 columns.push(Arc::new(Int64Array::from(counts)));
2673 }
2674 AggregateFunction::Count => {
2675 fields.push(Field::new(
2676 format!("__partial_{index}"),
2677 DataType::Int64,
2678 true,
2679 ));
2680 columns.push(Arc::new(Int64Array::from(emit_int_accum(&fold, index)?)));
2681 }
2682 _ => match &templates[index] {
2683 AggAccum::SumI(..) | AggAccum::MinI(..) | AggAccum::MaxI(..) => {
2684 fields.push(Field::new(
2685 format!("__partial_{index}"),
2686 DataType::Int64,
2687 true,
2688 ));
2689 columns.push(Arc::new(Int64Array::from(emit_int_accum(&fold, index)?)));
2690 }
2691 AggAccum::SumF(..) | AggAccum::MinF(..) | AggAccum::MaxF(..) => {
2692 fields.push(Field::new(
2693 format!("__partial_{index}"),
2694 DataType::Float64,
2695 true,
2696 ));
2697 columns.push(Arc::new(Float64Array::from(emit_float_accum(
2698 &fold, index,
2699 )?)));
2700 }
2701 other => {
2702 return Err(DistributedError::InvalidPlan(format!(
2703 "internal: unexpected accumulator {other:?}"
2704 )))
2705 }
2706 },
2707 }
2708 }
2709 Ok(RecordBatch::try_new(Schema::new(fields).into(), columns)?)
2710}
2711
2712fn final_aggregate_batches(
2715 batches: &[RecordBatch],
2716 group_by: &[String],
2717 aggregates: &[AggregateExpr],
2718) -> DistributedResult<RecordBatch> {
2719 let Some(batch) = concat_all(batches)? else {
2720 return Err(DistributedError::InvalidPlan(
2721 "aggregate input has no batches".to_owned(),
2722 ));
2723 };
2724 let schema = batch.schema();
2725 let key_indexes = resolve_columns(&schema, group_by)?;
2726 let mut partial_indexes: Vec<(usize, Option<usize>)> = Vec::with_capacity(aggregates.len());
2728 let mut templates = Vec::with_capacity(aggregates.len());
2729 for (index, aggregate) in aggregates.iter().enumerate() {
2730 let value_name = if aggregate.function == AggregateFunction::Avg {
2731 format!("__partial_{index}_sum")
2732 } else {
2733 format!("__partial_{index}")
2734 };
2735 let value_index = schema.index_of(&value_name).map_err(|_| {
2736 DistributedError::InvalidPlan(format!(
2737 "partial aggregate column `{value_name}` not in schema"
2738 ))
2739 })?;
2740 let float = match schema.field(value_index).data_type() {
2741 DataType::Int64 => false,
2742 DataType::Float64 => true,
2743 other => {
2744 return Err(DistributedError::Unsupported(format!(
2745 "aggregate partial of type {other} is not supported in this wave"
2746 )))
2747 }
2748 };
2749 templates.push(AggAccum::fresh(aggregate.function, float));
2750 let count_index = if aggregate.function == AggregateFunction::Avg {
2751 let count_name = format!("__partial_{index}_count");
2752 Some(schema.index_of(&count_name).map_err(|_| {
2753 DistributedError::InvalidPlan(format!(
2754 "partial aggregate column `{count_name}` not in schema"
2755 ))
2756 })?)
2757 } else {
2758 None
2759 };
2760 partial_indexes.push((value_index, count_index));
2761 }
2762 let mut fold = GroupFold::default();
2763 for row in 0..batch.num_rows() {
2764 let key = group_key(&batch, &key_indexes, row)?;
2765 let entry = fold.entry(key, row, &templates);
2766 for (index, aggregate) in aggregates.iter().enumerate() {
2767 let (value_index, count_index) = partial_indexes[index];
2768 match aggregate.function {
2769 AggregateFunction::Count => {
2770 if let Some(AggValue::I64(value)) =
2771 numeric_cell(batch.column(value_index).as_ref(), row)?
2772 {
2773 let AggAccum::Count(count) = &mut entry.accums[index] else {
2774 unreachable!("count template");
2775 };
2776 *count += value;
2777 }
2778 }
2779 AggregateFunction::Avg => {
2780 let sum = numeric_cell(batch.column(value_index).as_ref(), row)?;
2781 let count = match count_index {
2782 Some(count_index) => numeric_cell(batch.column(count_index).as_ref(), row)?,
2783 None => None,
2784 };
2785 let AggAccum::Avg {
2786 sum: total,
2787 count: rows,
2788 } = &mut entry.accums[index]
2789 else {
2790 unreachable!("avg template");
2791 };
2792 match sum {
2793 Some(AggValue::F64(value)) => *total += value,
2794 Some(AggValue::I64(value)) => *total += value as f64,
2795 None => {}
2796 }
2797 if let Some(AggValue::I64(value)) = count {
2798 *rows += value;
2799 }
2800 }
2801 _ => {
2802 let cell = numeric_cell(batch.column(value_index).as_ref(), row)?;
2803 entry.accums[index].fold_value(cell);
2804 }
2805 }
2806 }
2807 }
2808 if fold.order.is_empty() && group_by.is_empty() {
2810 fold.entry(String::new(), 0, &templates);
2811 }
2812 let mut columns = take_group_keys(&batch, &key_indexes, &fold)?;
2814 let mut fields: Vec<Field> = key_indexes
2815 .iter()
2816 .map(|index| schema.field(*index).clone())
2817 .collect();
2818 for (index, aggregate) in aggregates.iter().enumerate() {
2819 let name = aggregate_output_name(aggregate);
2820 match &templates[index] {
2821 AggAccum::Count(_) | AggAccum::SumI(..) | AggAccum::MinI(..) | AggAccum::MaxI(..) => {
2822 fields.push(Field::new(name, DataType::Int64, true));
2823 columns.push(Arc::new(Int64Array::from(emit_int_accum(&fold, index)?)));
2824 }
2825 AggAccum::SumF(..) | AggAccum::MinF(..) | AggAccum::MaxF(..) => {
2826 fields.push(Field::new(name, DataType::Float64, true));
2827 columns.push(Arc::new(Float64Array::from(emit_float_accum(
2828 &fold, index,
2829 )?)));
2830 }
2831 AggAccum::Avg { .. } => {
2832 let values: Vec<Option<f64>> = fold
2833 .order
2834 .iter()
2835 .map(|key| match &fold.groups[key].accums[index] {
2836 AggAccum::Avg { sum, count } => {
2837 (*count > 0).then_some(*sum / *count as f64)
2838 }
2839 _ => unreachable!("avg template"),
2840 })
2841 .collect();
2842 fields.push(Field::new(name, DataType::Float64, true));
2843 columns.push(Arc::new(Float64Array::from(values)));
2844 }
2845 }
2846 }
2847 Ok(RecordBatch::try_new(Schema::new(fields).into(), columns)?)
2848}
2849
2850#[derive(Debug)]
2857pub struct ResourceLedger {
2858 state: Mutex<LedgerState>,
2859 max_fragments: usize,
2860 max_bytes: u64,
2861}
2862
2863#[derive(Default, Debug)]
2864struct LedgerState {
2865 fragments: usize,
2866 bytes: u64,
2867}
2868
2869#[derive(Debug)]
2871pub struct ResourcePermit {
2872 ledger: Arc<ResourceLedger>,
2873 bytes: u64,
2874}
2875
2876impl Drop for ResourcePermit {
2877 fn drop(&mut self) {
2878 let mut state = self.ledger.state.lock();
2879 state.fragments = state.fragments.saturating_sub(1);
2880 state.bytes = state.bytes.saturating_sub(self.bytes);
2881 }
2882}
2883
2884impl ResourceLedger {
2885 pub fn new(max_fragments: usize, max_bytes: u64) -> Self {
2888 Self {
2889 state: Mutex::new(LedgerState::default()),
2890 max_fragments,
2891 max_bytes,
2892 }
2893 }
2894
2895 pub fn reserve(self: &Arc<Self>, fragment: &PlanFragment) -> DistributedResult<ResourcePermit> {
2898 let mut state = self.state.lock();
2899 if state.fragments >= self.max_fragments {
2900 return Err(DistributedError::Reservation {
2901 fragment_id: fragment.fragment_id,
2902 reason: format!("fragment concurrency limit {} reached", self.max_fragments),
2903 });
2904 }
2905 if state.bytes.saturating_add(fragment.estimated_bytes) > self.max_bytes {
2906 return Err(DistributedError::Reservation {
2907 fragment_id: fragment.fragment_id,
2908 reason: format!(
2909 "estimated bytes {} exceed the {} byte budget",
2910 state.bytes.saturating_add(fragment.estimated_bytes),
2911 self.max_bytes
2912 ),
2913 });
2914 }
2915 state.fragments += 1;
2916 state.bytes = state.bytes.saturating_add(fragment.estimated_bytes);
2917 Ok(ResourcePermit {
2918 ledger: Arc::clone(self),
2919 bytes: fragment.estimated_bytes,
2920 })
2921 }
2922
2923 pub fn reserved_fragments(&self) -> usize {
2925 self.state.lock().fragments
2926 }
2927
2928 pub fn reserved_bytes(&self) -> u64 {
2930 self.state.lock().bytes
2931 }
2932}
2933
2934#[derive(Default)]
2938pub struct LeaseLedger {
2939 leases: Mutex<HashMap<TabletId, Instant>>,
2940}
2941
2942impl LeaseLedger {
2943 pub fn renew(&self, worker: TabletId, expiry: Instant) {
2945 self.leases.lock().insert(worker, expiry);
2946 }
2947
2948 pub fn expiry(&self, worker: &TabletId) -> Option<Instant> {
2950 self.leases.lock().get(worker).copied()
2951 }
2952
2953 pub fn sweep(&self, now: Instant) -> Vec<TabletId> {
2956 let mut leases = self.leases.lock();
2957 let expired: Vec<TabletId> = leases
2958 .iter()
2959 .filter(|(_, expiry)| **expiry <= now)
2960 .map(|(worker, _)| *worker)
2961 .collect();
2962 for worker in &expired {
2963 leases.remove(worker);
2964 }
2965 expired
2966 }
2967}
2968
2969struct InFlight {
2971 worker: Option<TabletId>,
2972 control: ExecutionControl,
2973 _permit: ResourcePermit,
2974}
2975
2976#[derive(Default)]
2978struct ExecutionState {
2979 in_flight: Mutex<HashMap<FragmentId, InFlight>>,
2980}
2981
2982struct ProducerInput {
2984 fragment_id: FragmentId,
2985 tablet: Option<TabletId>,
2986 frames: Vec<BatchFrame>,
2987}
2988
2989enum RootData {
2993 Streams(Vec<ProducerInput>),
2994 Batches(Vec<RecordBatch>),
2995}
2996
2997impl RootData {
2998 fn flatten(&self) -> Vec<RecordBatch> {
3000 match self {
3001 Self::Streams(inputs) => inputs
3002 .iter()
3003 .flat_map(|input| input.frames.iter().map(|frame| frame.batch.clone()))
3004 .collect(),
3005 Self::Batches(batches) => batches.clone(),
3006 }
3007 }
3008}
3009
3010pub struct Coordinator {
3017 transport: Arc<dyn FragmentTransport>,
3018 registry: Arc<SqlQueryRegistry>,
3019 resources: Arc<ResourceLedger>,
3020 leases: LeaseLedger,
3021 lease_ttl: Duration,
3022 executions: Mutex<HashMap<QueryId, Arc<ExecutionState>>>,
3023}
3024
3025impl Coordinator {
3026 pub fn new(transport: Arc<dyn FragmentTransport>, registry: Arc<SqlQueryRegistry>) -> Self {
3029 Self::with_limits(
3030 transport,
3031 registry,
3032 1_024,
3033 16 * 1024 * 1024 * 1024,
3034 Duration::from_secs(30),
3035 )
3036 }
3037
3038 pub fn with_limits(
3040 transport: Arc<dyn FragmentTransport>,
3041 registry: Arc<SqlQueryRegistry>,
3042 max_fragments: usize,
3043 max_bytes: u64,
3044 lease_ttl: Duration,
3045 ) -> Self {
3046 Self {
3047 transport,
3048 registry,
3049 resources: Arc::new(ResourceLedger::new(max_fragments, max_bytes)),
3050 leases: LeaseLedger::default(),
3051 lease_ttl,
3052 executions: Mutex::new(HashMap::new()),
3053 }
3054 }
3055
3056 pub fn resources(&self) -> &Arc<ResourceLedger> {
3058 &self.resources
3059 }
3060
3061 pub async fn execute(&self, plan: &DistributedPlan) -> DistributedResult<Vec<RecordBatch>> {
3075 let registry_id = to_registry_query_id(&plan.query_id)?;
3076 let registered = self
3077 .registry
3078 .register(SqlQueryOptions {
3079 query_id: Some(registry_id),
3080 ..Default::default()
3081 })
3082 .map_err(|error| {
3083 DistributedError::InvalidPlan(format!("query registration failed: {error}"))
3084 })?;
3085 let result = self.execute_registered(plan, ®istered).await;
3086 match &result {
3087 Ok(_) => {
3088 let _ = registered.try_complete();
3089 }
3090 Err(_) => registered.fail(),
3091 }
3092 result
3093 }
3094
3095 pub fn cancel_query(&self, query_id: &QueryId) -> DistributedResult<bool> {
3101 let outcome = self.registry.cancel(to_registry_query_id(query_id)?);
3102 let state = self.executions.lock().get(query_id).cloned();
3103 if let Some(state) = state {
3104 let in_flight = state.in_flight.lock();
3105 for (fragment_id, inflight) in in_flight.iter() {
3106 inflight.control.cancel(CancellationReason::ClientRequest);
3107 let _ = self.transport.cancel_fragment(*fragment_id);
3108 }
3109 }
3110 Ok(matches!(
3111 outcome,
3112 CancelOutcome::Accepted | CancelOutcome::AlreadyCancelling
3113 ))
3114 }
3115
3116 pub fn sweep_expired_leases(&self, now: Instant) -> usize {
3123 let expired = self.leases.sweep(now);
3124 if expired.is_empty() {
3125 return 0;
3126 }
3127 let states: Vec<Arc<ExecutionState>> = self.executions.lock().values().cloned().collect();
3128 let mut cleaned = 0;
3129 for state in states {
3130 let victims: Vec<FragmentId> = {
3131 let in_flight = state.in_flight.lock();
3132 in_flight
3133 .iter()
3134 .filter(|(_, inflight)| {
3135 inflight
3136 .worker
3137 .is_some_and(|worker| expired.contains(&worker))
3138 })
3139 .map(|(fragment_id, _)| *fragment_id)
3140 .collect()
3141 };
3142 for fragment_id in victims {
3143 let inflight = state.in_flight.lock().remove(&fragment_id);
3144 if let Some(inflight) = inflight {
3145 inflight.control.cancel(CancellationReason::ServerShutdown);
3146 drop(inflight);
3147 let _ = self.transport.cancel_fragment(fragment_id);
3148 cleaned += 1;
3149 }
3150 }
3151 }
3152 cleaned
3153 }
3154
3155 async fn execute_registered(
3156 &self,
3157 plan: &DistributedPlan,
3158 registered: &RegisteredSqlQuery,
3159 ) -> DistributedResult<Vec<RecordBatch>> {
3160 validate_plan_shape(plan)?;
3161 let root = plan
3162 .root_fragment_id()
3163 .ok_or_else(|| DistributedError::InvalidPlan("plan has no root fragment".to_owned()))?;
3164 let state = Arc::new(ExecutionState::default());
3165 self.executions
3166 .lock()
3167 .insert(plan.query_id, Arc::clone(&state));
3168 let result = self.run_plan(plan, root, registered, &state).await;
3169 self.executions.lock().remove(&plan.query_id);
3170 result
3171 }
3172
3173 async fn run_plan(
3174 &self,
3175 plan: &DistributedPlan,
3176 root: FragmentId,
3177 registered: &RegisteredSqlQuery,
3178 state: &Arc<ExecutionState>,
3179 ) -> DistributedResult<Vec<RecordBatch>> {
3180 let parent = registered.control().clone();
3181 let layers = fragment_layers(plan, root)?;
3182 let mut outputs: HashMap<FragmentId, Vec<BatchFrame>> = HashMap::new();
3183 for layer in &layers {
3184 checkpoint(&parent)?;
3185 let now = Instant::now();
3186 for &fragment_id in layer {
3187 if let FragmentAssignment::Tablet(tablet) =
3188 plan.fragments[fragment_id as usize].assignment
3189 {
3190 self.leases.renew(tablet, now + self.lease_ttl);
3191 }
3192 }
3193 let mut tasks = FuturesUnordered::new();
3194 let mut spawn_error: Option<DistributedError> = None;
3195 for &fragment_id in layer {
3196 let fragment = &plan.fragments[fragment_id as usize];
3197 let reserved = match self.resources.reserve(fragment) {
3198 Ok(permit) => permit,
3199 Err(error) => {
3200 spawn_error = Some(error);
3201 break;
3202 }
3203 };
3204 let inputs = match build_inputs(plan, fragment_id, &outputs) {
3205 Ok(inputs) => inputs,
3206 Err(error) => {
3207 spawn_error = Some(error);
3208 break;
3209 }
3210 };
3211 let control = parent.child_with_deadline(None);
3212 let worker = match fragment.assignment {
3213 FragmentAssignment::Tablet(tablet) => Some(tablet),
3214 FragmentAssignment::Coordinator => None,
3215 };
3216 state.in_flight.lock().insert(
3217 fragment_id,
3218 InFlight {
3219 worker,
3220 control: control.clone(),
3221 _permit: reserved,
3222 },
3223 );
3224 let fragment = fragment.clone();
3225 let fragment_control = FragmentControl {
3226 control,
3227 max_spill_bytes: fragment.max_spill_bytes,
3228 };
3229 let transport = Arc::clone(&self.transport);
3230 tasks.push(async move {
3231 let worker = worker_label(&fragment);
3232 let drain_control = fragment_control.control.clone();
3233 let result = async {
3234 let stream = transport
3235 .execute_fragment(&fragment, inputs, fragment_control)
3236 .await?;
3237 drain_stream(stream, &drain_control).await
3238 }
3239 .await;
3240 (fragment.fragment_id, worker, result)
3241 });
3242 }
3243 if let Some(error) = spawn_error {
3244 self.abort(plan, state);
3245 while let Some((fragment_id, _, _)) = tasks.next().await {
3246 state.in_flight.lock().remove(&fragment_id);
3247 }
3248 return Err(error);
3249 }
3250 let mut failure: Option<DistributedError> = None;
3251 while let Some((fragment_id, worker, result)) = tasks.next().await {
3252 state.in_flight.lock().remove(&fragment_id);
3253 match result {
3254 Ok(frames) => {
3255 outputs.insert(fragment_id, frames);
3256 }
3257 Err(error) => {
3258 if failure.is_none() {
3259 failure = Some(match error {
3260 DistributedError::Cancelled(reason) => {
3261 DistributedError::Cancelled(reason)
3262 }
3263 other => DistributedError::FragmentExecution {
3264 fragment_id,
3265 worker,
3266 message: other.to_string(),
3267 },
3268 });
3269 }
3270 }
3271 }
3272 }
3273 if let Some(error) = failure {
3274 self.abort(plan, state);
3275 return Err(error);
3276 }
3277 }
3278 checkpoint(&parent)?;
3279 self.run_root(plan, root, &outputs, &parent).await
3280 }
3281
3282 fn abort(&self, plan: &DistributedPlan, state: &Arc<ExecutionState>) {
3285 {
3286 let in_flight = state.in_flight.lock();
3287 for inflight in in_flight.values() {
3288 inflight.control.cancel(CancellationReason::ClientRequest);
3289 }
3290 }
3291 for fragment in &plan.fragments {
3292 let _ = self.transport.cancel_fragment(fragment.fragment_id);
3293 }
3294 }
3295
3296 async fn run_root(
3298 &self,
3299 plan: &DistributedPlan,
3300 root: FragmentId,
3301 outputs: &HashMap<FragmentId, Vec<BatchFrame>>,
3302 parent: &ExecutionControl,
3303 ) -> DistributedResult<Vec<RecordBatch>> {
3304 let fragment = &plan.fragments[root as usize];
3305 let mut inputs: Vec<ProducerInput> = Vec::new();
3306 for operator in &fragment.operators {
3307 if let FragmentOperator::RemoteExchangeSource { exchange } = operator {
3308 let edge = plan.exchanges.get(*exchange as usize).ok_or_else(|| {
3309 DistributedError::InvalidPlan(format!("unknown exchange {exchange}"))
3310 })?;
3311 let producer = plan.fragments.get(edge.producer as usize).ok_or_else(|| {
3312 DistributedError::InvalidPlan(format!(
3313 "unknown producer fragment {}",
3314 edge.producer
3315 ))
3316 })?;
3317 let tablet = match producer.assignment {
3318 FragmentAssignment::Tablet(tablet) => Some(tablet),
3319 FragmentAssignment::Coordinator => None,
3320 };
3321 let frames = outputs.get(&edge.producer).cloned().ok_or_else(|| {
3322 DistributedError::InvalidPlan(format!(
3323 "missing output of producer fragment {}",
3324 edge.producer
3325 ))
3326 })?;
3327 let frames = route_frames(plan, edge, frames)?;
3328 inputs.push(ProducerInput {
3329 fragment_id: edge.producer,
3330 tablet,
3331 frames,
3332 });
3333 }
3334 }
3335 let mut current = RootData::Streams(inputs);
3336 for operator in &fragment.operators {
3337 checkpoint(parent)?;
3338 match operator {
3339 FragmentOperator::RemoteExchangeSource { .. } => {}
3340 FragmentOperator::FinalAggregate {
3341 group_by,
3342 aggregates,
3343 } => {
3344 let batches = current.flatten();
3345 current = RootData::Batches(vec![final_aggregate_batches(
3346 &batches, group_by, aggregates,
3347 )?]);
3348 }
3349 FragmentOperator::MergeSort { keys, limit } => {
3350 current = RootData::Batches(match ¤t {
3351 RootData::Streams(_) => {
3352 let streams = per_stream_batches(¤t);
3353 merge_sorted_streams(&streams, keys, *limit)?
3354 }
3355 RootData::Batches(batches) => sort_batches_local(batches, keys, *limit)?,
3356 });
3357 }
3358 FragmentOperator::DistributedTopK { k, score } => {
3359 current = RootData::Batches(self.coordinator_top_k(plan, ¤t, *k, score)?);
3360 }
3361 FragmentOperator::DistributedLimit { limit } => {
3362 let batches = current.flatten();
3363 current = RootData::Batches(limit_batches(&batches, *limit));
3364 }
3365 other => {
3366 return Err(DistributedError::Unsupported(format!(
3367 "root fragment operator {other:?} is not coordinator-executable in this wave"
3368 )))
3369 }
3370 }
3371 }
3372 Ok(current.flatten())
3373 }
3374
3375 fn coordinator_top_k(
3380 &self,
3381 plan: &DistributedPlan,
3382 data: &RootData,
3383 k: usize,
3384 score: &SortKey,
3385 ) -> DistributedResult<Vec<RecordBatch>> {
3386 let synthetic;
3389 let inputs: &[ProducerInput] = match data {
3390 RootData::Streams(inputs) => inputs,
3391 RootData::Batches(batches) => {
3392 synthetic = ProducerInput {
3393 fragment_id: u32::MAX,
3394 tablet: Some(TabletId::ZERO),
3395 frames: batches.iter().cloned().map(BatchFrame::data).collect(),
3396 };
3397 std::slice::from_ref(&synthetic)
3398 }
3399 };
3400 let mut all_batches: Vec<RecordBatch> = Vec::new();
3401 let mut batch_tablet: Vec<TabletId> = Vec::new();
3402 let mut shards: BTreeMap<TabletId, TabletTopK> = BTreeMap::new();
3403 let mut fragment_of: HashMap<TabletId, FragmentId> = HashMap::new();
3404 for input in inputs {
3405 let tablet = input.tablet.ok_or_else(|| {
3406 DistributedError::InvalidPlan(
3407 "distributed top-k inputs must be tablet fragments".to_owned(),
3408 )
3409 })?;
3410 fragment_of.insert(tablet, input.fragment_id);
3411 let mut rows = Vec::new();
3412 let mut bound = None;
3413 for frame in &input.frames {
3414 let batch = &frame.batch;
3415 if batch.num_rows() > 0 {
3416 let score_index = batch.schema().index_of(&score.column).map_err(|_| {
3417 DistributedError::InvalidPlan(format!(
3418 "top-k score column `{}` not in schema",
3419 score.column
3420 ))
3421 })?;
3422 let score_array = batch.column(score_index).clone();
3423 let row_id_array = row_ids(batch)?;
3424 for row in 0..batch.num_rows() {
3425 rows.push(TopKCandidate {
3426 score: score_key(score_array.as_ref(), row)?,
3427 tablet,
3428 row_id: RowId(row_id_array.value(row)),
3429 });
3430 }
3431 }
3432 batch_tablet.push(tablet);
3433 all_batches.push(batch.clone());
3434 match frame.score_bound {
3435 ScoreBound::AtMost(next) => bound = Some(next),
3436 ScoreBound::Exhausted => bound = None,
3437 ScoreBound::Unknown => {}
3440 }
3441 }
3442 shards.insert(
3443 tablet,
3444 TabletTopK {
3445 tablet,
3446 rows,
3447 unseen_bound: bound,
3448 },
3449 );
3450 }
3451 let winners = loop {
3452 let ordered: Vec<TabletTopK> = shards.values().cloned().collect();
3453 let merge = merge_top_k(&ordered, k);
3454 if merge.refill.is_empty() {
3455 break merge.winners;
3456 }
3457 for tablet in merge.refill {
3458 let fragment_id = fragment_of[&tablet];
3459 let already = shards[&tablet].rows.len();
3460 let refill = self.transport.refill_top_k(
3461 &plan.fragments[fragment_id as usize],
3462 already,
3463 k,
3464 )?;
3465 let entry = shards.get_mut(&tablet).expect("shard exists");
3466 if refill.rows.is_empty() && refill.unseen_bound == entry.unseen_bound {
3467 return Err(DistributedError::InvalidPlan(format!(
3468 "top-k refill for fragment {fragment_id} made no progress"
3469 )));
3470 }
3471 batch_tablet.push(tablet);
3472 all_batches.push(refill.payload);
3473 entry.rows.extend(refill.rows);
3474 entry.unseen_bound = refill.unseen_bound;
3475 }
3476 };
3477 if winners.is_empty() {
3478 return Ok(Vec::new());
3479 }
3480 let mut locations: HashMap<(TabletId, u64), (usize, usize)> = HashMap::new();
3482 for (batch_index, batch) in all_batches.iter().enumerate() {
3483 if batch.num_rows() == 0 {
3484 continue;
3485 }
3486 let row_id_array = row_ids(batch)?;
3487 let tablet = batch_tablet[batch_index];
3488 for row in 0..batch.num_rows() {
3489 locations.insert((tablet, row_id_array.value(row)), (batch_index, row));
3490 }
3491 }
3492 let mut order = Vec::with_capacity(winners.len());
3493 for winner in &winners {
3494 let location = locations
3495 .get(&(winner.tablet, winner.row_id.0))
3496 .ok_or_else(|| {
3497 DistributedError::InvalidPlan(format!(
3498 "top-k winner {:?} has no payload row",
3499 winner.row_id
3500 ))
3501 })?;
3502 order.push(*location);
3503 }
3504 let merged = emit_interleaved(&all_batches, &order)?;
3505 let schema = merged.first().map(|batch| batch.schema()).ok_or_else(|| {
3507 DistributedError::InvalidPlan("top-k merge produced no output".to_owned())
3508 })?;
3509 let keep: Vec<usize> = schema
3510 .fields()
3511 .iter()
3512 .enumerate()
3513 .filter(|(_, field)| field.name() != TOPK_ROWID_COLUMN)
3514 .map(|(index, _)| index)
3515 .collect();
3516 if keep.len() == schema.fields().len() {
3517 return Ok(merged);
3518 }
3519 merged
3520 .iter()
3521 .map(|batch| Ok(batch.project(&keep)?))
3522 .collect()
3523 }
3524}
3525
3526fn per_stream_batches(data: &RootData) -> Vec<RecordBatch> {
3528 match data {
3529 RootData::Streams(inputs) => inputs
3530 .iter()
3531 .filter_map(|input| {
3532 let batches: Vec<RecordBatch> = input
3533 .frames
3534 .iter()
3535 .map(|frame| frame.batch.clone())
3536 .collect();
3537 concat_all(&batches).ok().flatten()
3538 })
3539 .collect(),
3540 RootData::Batches(batches) => batches.clone(),
3541 }
3542}
3543
3544fn worker_label(fragment: &PlanFragment) -> String {
3546 match fragment.assignment {
3547 FragmentAssignment::Tablet(tablet) => format!("tablet {tablet}"),
3548 FragmentAssignment::Coordinator => "coordinator".to_owned(),
3549 }
3550}
3551
3552fn validate_plan_shape(plan: &DistributedPlan) -> DistributedResult<()> {
3554 if plan.fragments.is_empty() {
3555 return Err(DistributedError::InvalidPlan(
3556 "plan has no fragments".to_owned(),
3557 ));
3558 }
3559 for (index, fragment) in plan.fragments.iter().enumerate() {
3560 if fragment.fragment_id as usize != index {
3561 return Err(DistributedError::InvalidPlan(
3562 "fragment ids must equal their vector index".to_owned(),
3563 ));
3564 }
3565 }
3566 for edge in &plan.exchanges {
3567 if edge.producer as usize >= plan.fragments.len()
3568 || edge.consumer as usize >= plan.fragments.len()
3569 {
3570 return Err(DistributedError::InvalidPlan(format!(
3571 "exchange {} references a missing fragment",
3572 edge.exchange_id
3573 )));
3574 }
3575 }
3576 let roots = plan
3577 .fragments
3578 .iter()
3579 .filter(|fragment| {
3580 !plan
3581 .exchanges
3582 .iter()
3583 .any(|edge| edge.producer == fragment.fragment_id)
3584 })
3585 .count();
3586 if roots != 1 {
3587 return Err(DistributedError::InvalidPlan(format!(
3588 "plan must have exactly one root fragment, found {roots}"
3589 )));
3590 }
3591 if let Some(root) = plan.root_fragment_id() {
3592 for fragment in &plan.fragments {
3593 if fragment.fragment_id != root
3594 && fragment.assignment == FragmentAssignment::Coordinator
3595 {
3596 return Err(DistributedError::InvalidPlan(
3597 "only the root fragment may be coordinator-assigned".to_owned(),
3598 ));
3599 }
3600 }
3601 }
3602 Ok(())
3603}
3604
3605fn fragment_layers(
3607 plan: &DistributedPlan,
3608 root: FragmentId,
3609) -> DistributedResult<Vec<Vec<FragmentId>>> {
3610 fn depth(
3611 plan: &DistributedPlan,
3612 fragment: FragmentId,
3613 memo: &mut [Option<usize>],
3614 visiting: &mut [bool],
3615 ) -> DistributedResult<usize> {
3616 if let Some(depth) = memo[fragment as usize] {
3617 return Ok(depth);
3618 }
3619 if visiting[fragment as usize] {
3620 return Err(DistributedError::InvalidPlan(
3621 "exchange graph has a cycle".to_owned(),
3622 ));
3623 }
3624 visiting[fragment as usize] = true;
3625 let mut best = 0;
3626 for edge in plan.exchanges_into(fragment) {
3627 best = best.max(depth(plan, edge.producer, memo, visiting)? + 1);
3628 }
3629 visiting[fragment as usize] = false;
3630 memo[fragment as usize] = Some(best);
3631 Ok(best)
3632 }
3633
3634 let mut memo = vec![None; plan.fragments.len()];
3635 let mut visiting = vec![false; plan.fragments.len()];
3636 let mut layers: Vec<Vec<FragmentId>> = Vec::new();
3637 for fragment in &plan.fragments {
3638 if fragment.fragment_id == root {
3639 continue;
3640 }
3641 let depth = depth(plan, fragment.fragment_id, &mut memo, &mut visiting)?;
3642 if layers.len() <= depth {
3643 layers.resize(depth + 1, Vec::new());
3644 }
3645 layers[depth].push(fragment.fragment_id);
3646 }
3647 Ok(layers)
3648}
3649
3650fn build_inputs(
3653 plan: &DistributedPlan,
3654 consumer: FragmentId,
3655 outputs: &HashMap<FragmentId, Vec<BatchFrame>>,
3656) -> DistributedResult<Vec<FragmentStream>> {
3657 let mut inputs = Vec::new();
3658 for edge in plan.exchanges_into(consumer) {
3659 let frames = outputs.get(&edge.producer).cloned().ok_or_else(|| {
3660 DistributedError::InvalidPlan(format!(
3661 "missing output of producer fragment {}",
3662 edge.producer
3663 ))
3664 })?;
3665 let frames = route_frames(plan, edge, frames)?;
3666 inputs.push(Box::pin(stream::iter(frames.into_iter().map(Ok))) as FragmentStream);
3667 }
3668 Ok(inputs)
3669}
3670
3671fn route_frames(
3674 plan: &DistributedPlan,
3675 edge: &ExchangeDescriptor,
3676 frames: Vec<BatchFrame>,
3677) -> DistributedResult<Vec<BatchFrame>> {
3678 match &edge.kind {
3679 ExchangeKind::Merge | ExchangeKind::Broadcast => Ok(frames),
3680 ExchangeKind::HashRepartition { keys } => {
3681 let mut siblings: Vec<&ExchangeDescriptor> = plan
3682 .exchanges
3683 .iter()
3684 .filter(|candidate| {
3685 candidate.producer == edge.producer
3686 && matches!(candidate.kind, ExchangeKind::HashRepartition { .. })
3687 })
3688 .collect();
3689 siblings.sort_by_key(|candidate| candidate.consumer);
3690 let width = siblings.len();
3691 let index = siblings
3692 .iter()
3693 .position(|candidate| candidate.consumer == edge.consumer)
3694 .ok_or_else(|| {
3695 DistributedError::InvalidPlan(format!(
3696 "exchange {} is missing from its repartition boundary",
3697 edge.exchange_id
3698 ))
3699 })?;
3700 repartition_frames(&frames, keys, width, index)
3701 }
3702 }
3703}
3704
3705fn to_registry_query_id(query_id: &QueryId) -> DistributedResult<crate::query_registry::QueryId> {
3707 query_id
3708 .to_hex()
3709 .parse()
3710 .map_err(|error| DistributedError::InvalidPlan(format!("query id bridge failed: {error}")))
3711}
3712
3713#[cfg(test)]
3718mod tests {
3719 use super::*;
3720 use arrow::datatypes::Field;
3721
3722 fn tablet(n: u8) -> TabletId {
3724 let mut bytes = [0u8; 16];
3725 bytes[15] = n;
3726 TabletId::from_bytes(bytes)
3727 }
3728
3729 struct SplitMix64(u64);
3731
3732 impl SplitMix64 {
3733 fn next(&mut self) -> u64 {
3734 self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
3735 let mut z = self.0;
3736 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
3737 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
3738 z ^ (z >> 31)
3739 }
3740
3741 fn below(&mut self, n: u64) -> u64 {
3742 self.next() % n
3743 }
3744 }
3745
3746 #[derive(Default)]
3747 struct StaticLocator {
3748 tablets: HashMap<String, Vec<TabletId>>,
3749 specs: HashMap<String, PartitionSpec>,
3750 }
3751
3752 impl TabletLocator for StaticLocator {
3753 fn tablets_for_table(&self, table: &str) -> DistributedResult<Vec<TabletId>> {
3754 self.tablets
3755 .get(table)
3756 .cloned()
3757 .ok_or_else(|| DistributedError::UnknownTable(table.to_owned()))
3758 }
3759
3760 fn partitioning(&self, table: &str) -> DistributedResult<PartitionSpec> {
3761 self.specs
3762 .get(table)
3763 .cloned()
3764 .ok_or_else(|| DistributedError::UnknownTable(table.to_owned()))
3765 }
3766 }
3767
3768 struct StaticMetadata {
3769 version: MetadataVersion,
3770 stats: HashMap<String, TableStats>,
3771 }
3772
3773 impl ClusterMetadata for StaticMetadata {
3774 fn metadata_version(&self) -> MetadataVersion {
3775 self.version
3776 }
3777
3778 fn table_stats(&self, table: &str) -> DistributedResult<TableStats> {
3779 self.stats
3780 .get(table)
3781 .copied()
3782 .ok_or_else(|| DistributedError::UnknownTable(table.to_owned()))
3783 }
3784 }
3785
3786 fn world(
3788 entries: &[(&str, &[TabletId], PartitionSpec, TableStats)],
3789 ) -> (StaticLocator, StaticMetadata) {
3790 let mut locator = StaticLocator::default();
3791 let mut stats = HashMap::new();
3792 for (table, tablets, spec, stat) in entries {
3793 locator
3794 .tablets
3795 .insert((*table).to_owned(), tablets.to_vec());
3796 locator.specs.insert((*table).to_owned(), spec.clone());
3797 stats.insert((*table).to_owned(), *stat);
3798 }
3799 (
3800 locator,
3801 StaticMetadata {
3802 version: MetadataVersion::new(7),
3803 stats,
3804 },
3805 )
3806 }
3807
3808 fn scan(table: &str) -> LogicalPlanLite {
3809 LogicalPlanLite::Scan {
3810 table: table.to_owned(),
3811 predicate: None,
3812 projection: Vec::new(),
3813 }
3814 }
3815
3816 fn desc(root: LogicalPlanLite) -> PlanDescription {
3817 PlanDescription {
3818 query_id: QueryId::new_random(),
3819 root,
3820 options: PlannerOptions::default(),
3821 }
3822 }
3823
3824 fn hash_spec(column: &str) -> PartitionSpec {
3825 PartitionSpec::Hash {
3826 columns: vec![column.to_owned()],
3827 buckets: 16,
3828 }
3829 }
3830
3831 fn stats(rows: u64, bytes: u64) -> TableStats {
3832 TableStats {
3833 row_count: rows,
3834 total_bytes: bytes,
3835 }
3836 }
3837
3838 fn i64_batch(columns: &[(&str, Vec<i64>)]) -> RecordBatch {
3839 let schema = Schema::new(
3840 columns
3841 .iter()
3842 .map(|(name, _)| Field::new(*name, DataType::Int64, true))
3843 .collect::<Vec<_>>(),
3844 );
3845 let arrays: Vec<ArrayRef> = columns
3846 .iter()
3847 .map(|(_, values)| Arc::new(Int64Array::from(values.clone())) as ArrayRef)
3848 .collect();
3849 RecordBatch::try_new(schema.into(), arrays).unwrap()
3850 }
3851
3852 fn score_batch(scores: Vec<u64>, row_ids: Vec<u64>, payloads: Vec<i64>) -> RecordBatch {
3853 let schema = Schema::new(vec![
3854 Field::new("score", DataType::UInt64, true),
3855 Field::new(TOPK_ROWID_COLUMN, DataType::UInt64, false),
3856 Field::new("payload", DataType::Int64, true),
3857 ]);
3858 RecordBatch::try_new(
3859 schema.into(),
3860 vec![
3861 Arc::new(UInt64Array::from(scores)),
3862 Arc::new(UInt64Array::from(row_ids)),
3863 Arc::new(Int64Array::from(payloads)),
3864 ],
3865 )
3866 .unwrap()
3867 }
3868
3869 fn collect_i64(batches: &[RecordBatch], column: &str) -> Vec<i64> {
3870 let mut values = Vec::new();
3871 for batch in batches {
3872 let index = batch.schema().index_of(column).unwrap();
3873 let array = batch
3874 .column(index)
3875 .as_any()
3876 .downcast_ref::<Int64Array>()
3877 .unwrap();
3878 for row in 0..batch.num_rows() {
3879 assert!(
3880 !array.is_null(row),
3881 "column {column} has an unexpected null"
3882 );
3883 values.push(array.value(row));
3884 }
3885 }
3886 values
3887 }
3888
3889 fn collect_u64(batches: &[RecordBatch], column: &str) -> Vec<u64> {
3890 let mut values = Vec::new();
3891 for batch in batches {
3892 let index = batch.schema().index_of(column).unwrap();
3893 let array = batch
3894 .column(index)
3895 .as_any()
3896 .downcast_ref::<UInt64Array>()
3897 .unwrap();
3898 for row in 0..batch.num_rows() {
3899 values.push(array.value(row));
3900 }
3901 }
3902 values
3903 }
3904
3905 fn collect_f64(batches: &[RecordBatch], column: &str) -> Vec<Option<f64>> {
3906 let mut values = Vec::new();
3907 for batch in batches {
3908 let index = batch.schema().index_of(column).unwrap();
3909 let array = batch
3910 .column(index)
3911 .as_any()
3912 .downcast_ref::<Float64Array>()
3913 .unwrap();
3914 for row in 0..batch.num_rows() {
3915 values.push((!array.is_null(row)).then(|| array.value(row)));
3916 }
3917 }
3918 values
3919 }
3920
3921 fn operators(plan: &DistributedPlan, fragment_id: FragmentId) -> &[FragmentOperator] {
3922 &plan.fragments[fragment_id as usize].operators
3923 }
3924
3925 #[test]
3930 fn scan_plan_shape_estimates_and_spill() {
3931 let tablets = [tablet(1), tablet(2), tablet(3)];
3932 let (locator, metadata) = world(&[(
3933 "t",
3934 &tablets,
3935 hash_spec("id"),
3936 stats(3_000, 3 * 1024 * 1024),
3937 )]);
3938 let description = desc(scan("t"));
3939 let plan = distribute(&description, &locator, &metadata).unwrap();
3940 assert_eq!(plan.query_id, description.query_id);
3941 assert_eq!(plan.metadata_version, MetadataVersion::new(7));
3942 assert_eq!(
3943 plan.fragments.len(),
3944 4,
3945 "3 scan fragments + coordinator gather"
3946 );
3947 for (index, id) in tablets.iter().enumerate() {
3948 let fragment = &plan.fragments[index];
3949 assert_eq!(fragment.assignment, FragmentAssignment::Tablet(*id));
3950 assert!(
3951 matches!(
3952 operators(&plan, fragment.fragment_id)[0],
3953 FragmentOperator::TabletScan { .. }
3954 ),
3955 "fragment {index} starts with a tablet scan"
3956 );
3957 assert!(
3958 matches!(
3959 operators(&plan, fragment.fragment_id).last().unwrap(),
3960 FragmentOperator::RemoteExchangeSink { .. }
3961 ),
3962 "fragment {index} ends with a sink"
3963 );
3964 assert_eq!(fragment.estimated_rows, 1_000);
3965 assert_eq!(fragment.estimated_bytes, 1024 * 1024);
3966 assert_eq!(
3967 fragment.max_spill_bytes,
3968 DEFAULT_MAX_SPILL_BYTES_PER_FRAGMENT
3969 );
3970 }
3971 let root = plan.root_fragment_id().unwrap();
3972 assert_eq!(root, 3);
3973 assert_eq!(
3974 plan.fragments[3].assignment,
3975 FragmentAssignment::Coordinator
3976 );
3977 assert_eq!(plan.exchanges.len(), 3);
3978 for edge in &plan.exchanges {
3979 assert_eq!(edge.kind, ExchangeKind::Merge);
3980 assert_eq!(edge.consumer, root);
3981 assert_eq!(
3982 edge.schema_fingerprint,
3983 plan.exchanges[0].schema_fingerprint
3984 );
3985 }
3986 let mut custom = desc(scan("t"));
3988 custom.options.max_spill_bytes_per_fragment = 1_234;
3989 let plan = distribute(&custom, &locator, &metadata).unwrap();
3990 assert!(plan
3991 .fragments
3992 .iter()
3993 .all(|fragment| fragment.max_spill_bytes == 1_234));
3994 }
3995
3996 #[test]
3997 fn fragment_control_begin_spill_opens_core_spill_session() {
3998 use mongreldb_core::ExecutionControl;
3999 use mongreldb_types::ids::QueryId;
4000
4001 let dir = tempfile::tempdir().unwrap();
4002 let db = mongreldb_core::Database::create(dir.path()).unwrap();
4004 let control = FragmentControl {
4005 control: ExecutionControl::new(None),
4006 max_spill_bytes: 64 * 1024,
4007 };
4008 let session = control
4009 .begin_spill(db.spill_manager(), QueryId::from_bytes([0xAB; 16]))
4010 .expect("begin_spill binds planner allowance to core SpillManager");
4011 let stats = db.spill_manager().stats();
4012 assert_eq!(
4013 stats.global_budget_bytes,
4014 db.spill_manager().config().global_bytes
4015 );
4016 drop(session);
4017 }
4018
4019 #[test]
4020 fn aggregate_plan_shape_grouped_and_ungrouped() {
4021 let tablets = [tablet(1), tablet(2), tablet(3)];
4022 let (locator, metadata) = world(&[("t", &tablets, hash_spec("id"), stats(900, 900_000))]);
4023 let aggregates = vec![
4024 AggregateExpr {
4025 function: AggregateFunction::Count,
4026 column: None,
4027 },
4028 AggregateExpr {
4029 function: AggregateFunction::Sum,
4030 column: Some("v".to_owned()),
4031 },
4032 ];
4033 let grouped = LogicalPlanLite::Aggregate {
4036 input: Box::new(scan("t")),
4037 group_by: vec!["g".to_owned()],
4038 aggregates: aggregates.clone(),
4039 };
4040 let plan = distribute(&desc(grouped), &locator, &metadata).unwrap();
4041 assert_eq!(plan.fragments.len(), 4);
4042 for index in 0..3 {
4043 assert!(matches!(
4044 operators(&plan, index as FragmentId)[1],
4045 FragmentOperator::PartialAggregate { .. }
4046 ));
4047 }
4048 assert_eq!(plan.exchanges.len(), 3);
4049 for edge in &plan.exchanges {
4050 assert_eq!(
4051 edge.kind,
4052 ExchangeKind::HashRepartition {
4053 keys: vec!["g".to_owned()]
4054 }
4055 );
4056 }
4057 let root = plan.root_fragment_id().unwrap();
4058 assert!(matches!(
4059 operators(&plan, root).last().unwrap(),
4060 FragmentOperator::FinalAggregate { .. }
4061 ));
4062 let ungrouped = LogicalPlanLite::Aggregate {
4064 input: Box::new(scan("t")),
4065 group_by: Vec::new(),
4066 aggregates: aggregates[..1].to_vec(),
4067 };
4068 let plan = distribute(&desc(ungrouped), &locator, &metadata).unwrap();
4069 assert!(plan
4070 .exchanges
4071 .iter()
4072 .all(|edge| edge.kind == ExchangeKind::Merge));
4073 let root = plan.root_fragment_id().unwrap();
4074 assert_eq!(plan.fragments[root as usize].estimated_rows, 1);
4075 }
4076
4077 #[test]
4078 fn colocated_join_plan_fuses_scans_without_exchange() {
4079 let tablets = [tablet(1), tablet(2), tablet(3)];
4080 let (locator, metadata) = world(&[
4081 (
4082 "a",
4083 &tablets,
4084 hash_spec("id"),
4085 stats(6_000, 6 * 1024 * 1024),
4086 ),
4087 (
4088 "b",
4089 &tablets,
4090 hash_spec("id"),
4091 stats(3_000, 3 * 1024 * 1024),
4092 ),
4093 ]);
4094 let join = LogicalPlanLite::Join {
4095 left: Box::new(scan("a")),
4096 right: Box::new(scan("b")),
4097 on: vec![JoinKey {
4098 left: "id".to_owned(),
4099 right: "id".to_owned(),
4100 }],
4101 };
4102 let plan = distribute(&desc(join), &locator, &metadata).unwrap();
4103 assert_eq!(plan.fragments.len(), 4, "3 fused fragments + gather");
4104 for index in 0..3 {
4105 let ops = operators(&plan, index as FragmentId);
4106 assert!(
4107 matches!(&ops[0], FragmentOperator::TabletScan { table, .. } if table == "a"),
4108 "left scan first: {ops:?}"
4109 );
4110 assert!(
4111 matches!(&ops[1], FragmentOperator::TabletScan { table, .. } if table == "b"),
4112 "right scan second: {ops:?}"
4113 );
4114 assert!(
4115 matches!(&ops[2], FragmentOperator::DistributedHashJoin { .. }),
4116 "fused hash join: {ops:?}"
4117 );
4118 }
4119 assert!(
4120 plan.exchanges
4121 .iter()
4122 .all(|edge| edge.kind == ExchangeKind::Merge),
4123 "a colocated join has no shuffle: {:?}",
4124 plan.exchanges
4125 );
4126 assert_eq!(plan.exchanges.len(), 3, "only the gather edges remain");
4127 }
4128
4129 #[test]
4130 fn broadcast_join_plan_replicates_the_small_side() {
4131 let big_tablets = [tablet(1), tablet(2), tablet(3)];
4132 let small_tablets = [tablet(4), tablet(5)];
4133 let (locator, metadata) = world(&[
4134 (
4135 "big",
4136 &big_tablets,
4137 hash_spec("id"),
4138 stats(8_000, 64 * 1024 * 1024),
4139 ),
4140 (
4141 "small",
4142 &small_tablets,
4143 hash_spec("key"),
4144 stats(100, 1024 * 1024),
4145 ),
4146 ]);
4147 let join = LogicalPlanLite::Join {
4148 left: Box::new(scan("big")),
4149 right: Box::new(scan("small")),
4150 on: vec![JoinKey {
4151 left: "id".to_owned(),
4152 right: "key".to_owned(),
4153 }],
4154 };
4155 let plan = distribute(&desc(join), &locator, &metadata).unwrap();
4156 assert_eq!(plan.fragments.len(), 6);
4158 let root = plan.root_fragment_id().unwrap();
4159 let broadcast_edges: Vec<&ExchangeDescriptor> = plan
4160 .exchanges
4161 .iter()
4162 .filter(|edge| edge.kind == ExchangeKind::Broadcast)
4163 .collect();
4164 assert_eq!(
4165 broadcast_edges.len(),
4166 6,
4167 "each small producer x each big fragment"
4168 );
4169 for edge in &broadcast_edges {
4170 assert!(edge.producer >= 3, "small side produces the broadcast");
4171 assert!(edge.consumer < 3, "big side consumes the broadcast");
4172 }
4173 for index in 0..3 {
4174 let ops = operators(&plan, index as FragmentId);
4175 let last_transform = ops
4176 .iter()
4177 .rfind(|op| !matches!(op, FragmentOperator::RemoteExchangeSink { .. }))
4178 .unwrap();
4179 assert!(matches!(
4180 last_transform,
4181 FragmentOperator::BroadcastJoin {
4182 build_side: BuildSide::Right,
4183 ..
4184 }
4185 ));
4186 let sources = ops
4187 .iter()
4188 .filter(|op| matches!(op, FragmentOperator::RemoteExchangeSource { .. }))
4189 .count();
4190 assert_eq!(sources, 2, "one source per small producer");
4191 }
4192 assert!(plan
4193 .exchanges
4194 .iter()
4195 .filter(|edge| edge.consumer == root)
4196 .all(|edge| edge.kind == ExchangeKind::Merge && edge.producer < 3));
4197 }
4198
4199 #[test]
4200 fn repartition_join_plan_shuffles_both_sides() {
4201 let left_tablets = [tablet(1), tablet(2)];
4202 let right_tablets = [tablet(3), tablet(4), tablet(5)];
4203 let (locator, metadata) = world(&[
4204 (
4205 "l",
4206 &left_tablets,
4207 hash_spec("a"),
4208 stats(8_000, 64 * 1024 * 1024),
4209 ),
4210 (
4211 "r",
4212 &right_tablets,
4213 hash_spec("b"),
4214 stats(9_000, 96 * 1024 * 1024),
4215 ),
4216 ]);
4217 let join = LogicalPlanLite::Join {
4218 left: Box::new(scan("l")),
4219 right: Box::new(scan("r")),
4220 on: vec![JoinKey {
4221 left: "a".to_owned(),
4222 right: "b".to_owned(),
4223 }],
4224 };
4225 let plan = distribute(&desc(join), &locator, &metadata).unwrap();
4226 assert_eq!(plan.fragments.len(), 9);
4228 let root = plan.root_fragment_id().unwrap();
4229 let shuffles: Vec<&ExchangeDescriptor> = plan
4230 .exchanges
4231 .iter()
4232 .filter(|edge| matches!(edge.kind, ExchangeKind::HashRepartition { .. }))
4233 .collect();
4234 assert_eq!(shuffles.len(), 2 * 3 + 3 * 3);
4235 let left_keys: Vec<&ExchangeDescriptor> = shuffles
4236 .iter()
4237 .filter(|edge| edge.producer < 2)
4238 .copied()
4239 .collect();
4240 assert!(
4241 left_keys
4242 .iter()
4243 .all(|edge| matches!(&edge.kind, ExchangeKind::HashRepartition { keys } if keys == &vec!["a".to_owned()]))
4244 );
4245 for join_fragment in 5..8 {
4246 let fragment = &plan.fragments[join_fragment];
4247 assert_eq!(
4248 fragment.assignment,
4249 FragmentAssignment::Tablet(right_tablets[(join_fragment - 5) % 3]),
4250 "join fragments round-robin over the larger side's tablets"
4251 );
4252 let ops = operators(&plan, fragment.fragment_id);
4253 let sources = ops
4254 .iter()
4255 .filter(|op| matches!(op, FragmentOperator::RemoteExchangeSource { .. }))
4256 .count();
4257 assert_eq!(sources, 5, "2 left + 3 right sources: {ops:?}");
4258 let last_transform = ops
4259 .iter()
4260 .rfind(|op| !matches!(op, FragmentOperator::RemoteExchangeSink { .. }))
4261 .unwrap();
4262 assert!(matches!(
4263 last_transform,
4264 FragmentOperator::RepartitionJoin { .. }
4265 ));
4266 }
4267 assert!(plan
4268 .exchanges
4269 .iter()
4270 .filter(|edge| edge.consumer == root)
4271 .all(|edge| edge.kind == ExchangeKind::Merge));
4272 }
4273
4274 #[test]
4275 fn sort_and_limit_plan_shapes() {
4276 let tablets = [tablet(1), tablet(2), tablet(3)];
4277 let (locator, metadata) = world(&[("t", &tablets, hash_spec("id"), stats(900, 900_000))]);
4278
4279 let topk = LogicalPlanLite::Sort {
4281 input: Box::new(scan("t")),
4282 keys: vec![SortKey {
4283 column: "score".to_owned(),
4284 descending: true,
4285 }],
4286 limit: Some(10),
4287 };
4288 let plan = distribute(&desc(topk), &locator, &metadata).unwrap();
4289 for index in 0..3 {
4290 assert!(matches!(
4291 operators(&plan, index as FragmentId)[1],
4292 FragmentOperator::DistributedTopK { k: 10, .. }
4293 ));
4294 }
4295 let root = plan.root_fragment_id().unwrap();
4296 assert!(matches!(
4297 operators(&plan, root).last().unwrap(),
4298 FragmentOperator::DistributedTopK { k: 10, .. }
4299 ));
4300 assert!(plan
4301 .exchanges
4302 .iter()
4303 .all(|edge| edge.kind == ExchangeKind::Merge));
4304
4305 let sort = LogicalPlanLite::Sort {
4307 input: Box::new(scan("t")),
4308 keys: vec![SortKey {
4309 column: "x".to_owned(),
4310 descending: false,
4311 }],
4312 limit: None,
4313 };
4314 let plan = distribute(&desc(sort), &locator, &metadata).unwrap();
4315 for index in 0..3 {
4316 assert!(matches!(
4317 operators(&plan, index as FragmentId)[1],
4318 FragmentOperator::MergeSort { limit: None, .. }
4319 ));
4320 }
4321 let root = plan.root_fragment_id().unwrap();
4322 assert!(matches!(
4323 operators(&plan, root).last().unwrap(),
4324 FragmentOperator::MergeSort { limit: None, .. }
4325 ));
4326
4327 let multi = LogicalPlanLite::Sort {
4330 input: Box::new(scan("t")),
4331 keys: vec![
4332 SortKey {
4333 column: "a".to_owned(),
4334 descending: false,
4335 },
4336 SortKey {
4337 column: "b".to_owned(),
4338 descending: true,
4339 },
4340 ],
4341 limit: Some(5),
4342 };
4343 let plan = distribute(&desc(multi), &locator, &metadata).unwrap();
4344 for index in 0..3 {
4345 assert!(matches!(
4346 operators(&plan, index as FragmentId)[1],
4347 FragmentOperator::MergeSort { limit: Some(5), .. }
4348 ));
4349 }
4350 let root = plan.root_fragment_id().unwrap();
4351 assert!(matches!(
4352 operators(&plan, root).last().unwrap(),
4353 FragmentOperator::MergeSort { limit: Some(5), .. }
4354 ));
4355
4356 let limit = LogicalPlanLite::Limit {
4358 input: Box::new(scan("t")),
4359 limit: 7,
4360 };
4361 let plan = distribute(&desc(limit), &locator, &metadata).unwrap();
4362 for index in 0..3 {
4363 assert!(matches!(
4364 operators(&plan, index as FragmentId)[1],
4365 FragmentOperator::DistributedLimit { limit: 7 }
4366 ));
4367 }
4368 let root = plan.root_fragment_id().unwrap();
4369 assert!(matches!(
4370 operators(&plan, root).last().unwrap(),
4371 FragmentOperator::DistributedLimit { limit: 7 }
4372 ));
4373
4374 let chained = LogicalPlanLite::Limit {
4377 input: Box::new(LogicalPlanLite::Aggregate {
4378 input: Box::new(scan("t")),
4379 group_by: Vec::new(),
4380 aggregates: vec![AggregateExpr {
4381 function: AggregateFunction::Count,
4382 column: None,
4383 }],
4384 }),
4385 limit: 5,
4386 };
4387 let plan = distribute(&desc(chained), &locator, &metadata).unwrap();
4388 assert_eq!(plan.fragments.len(), 4);
4389 let root = plan.root_fragment_id().unwrap();
4390 let ops = operators(&plan, root);
4391 assert!(matches!(
4392 ops[ops.len() - 2],
4393 FragmentOperator::FinalAggregate { .. }
4394 ));
4395 assert!(matches!(
4396 ops[ops.len() - 1],
4397 FragmentOperator::DistributedLimit { limit: 5 }
4398 ));
4399 }
4400
4401 #[test]
4402 fn planner_rejects_invalid_inputs() {
4403 let tablets = [tablet(1)];
4404 let (locator, metadata) = world(&[("t", &tablets, hash_spec("id"), stats(10, 100))]);
4405 let error = distribute(&desc(scan("nope")), &locator, &metadata).unwrap_err();
4407 assert!(
4408 matches!(error, DistributedError::UnknownTable(_)),
4409 "{error}"
4410 );
4411 let (locator, metadata) = world(&[("e", &[], hash_spec("id"), stats(0, 0))]);
4413 let error = distribute(&desc(scan("e")), &locator, &metadata).unwrap_err();
4414 assert!(
4415 matches!(error, DistributedError::EmptyLayout { .. }),
4416 "{error}"
4417 );
4418 let (locator, metadata) = world(&[("t", &tablets, hash_spec("id"), stats(10, 100))]);
4419 let join = LogicalPlanLite::Join {
4421 left: Box::new(scan("t")),
4422 right: Box::new(scan("t")),
4423 on: Vec::new(),
4424 };
4425 let error = distribute(&desc(join), &locator, &metadata).unwrap_err();
4426 assert!(matches!(error, DistributedError::InvalidPlan(_)), "{error}");
4427 let aggregate = LogicalPlanLite::Aggregate {
4429 input: Box::new(scan("t")),
4430 group_by: Vec::new(),
4431 aggregates: vec![AggregateExpr {
4432 function: AggregateFunction::Sum,
4433 column: None,
4434 }],
4435 };
4436 let error = distribute(&desc(aggregate), &locator, &metadata).unwrap_err();
4437 assert!(matches!(error, DistributedError::InvalidPlan(_)), "{error}");
4438 let sort = LogicalPlanLite::Sort {
4440 input: Box::new(scan("t")),
4441 keys: Vec::new(),
4442 limit: None,
4443 };
4444 let error = distribute(&desc(sort), &locator, &metadata).unwrap_err();
4445 assert!(matches!(error, DistributedError::InvalidPlan(_)), "{error}");
4446 }
4447
4448 fn candidate(score: u64, tablet_n: u8, row_id: u64) -> TopKCandidate {
4453 TopKCandidate {
4454 score,
4455 tablet: tablet(tablet_n),
4456 row_id: RowId(row_id),
4457 }
4458 }
4459
4460 #[test]
4461 fn topk_tie_break_is_exact() {
4462 let mut rows = vec![
4464 candidate(5, 2, 0),
4465 candidate(5, 1, 2),
4466 candidate(7, 3, 0),
4467 candidate(5, 1, 0),
4468 candidate(3, 1, 0),
4469 ];
4470 rows.sort_by(topk_cmp);
4471 assert_eq!(
4472 rows,
4473 vec![
4474 candidate(7, 3, 0),
4475 candidate(5, 1, 0),
4476 candidate(5, 1, 2),
4477 candidate(5, 2, 0),
4478 candidate(3, 1, 0),
4479 ]
4480 );
4481 }
4482
4483 #[test]
4484 fn merge_top_k_needs_no_refill_when_tablets_are_exhausted() {
4485 let shards = vec![
4486 TabletTopK {
4487 tablet: tablet(1),
4488 rows: vec![candidate(100, 1, 0), candidate(90, 1, 1)],
4489 unseen_bound: None,
4490 },
4491 TabletTopK {
4492 tablet: tablet(2),
4493 rows: vec![candidate(96, 2, 0)],
4494 unseen_bound: None,
4495 },
4496 ];
4497 let merge = merge_top_k(&shards, 2);
4498 assert_eq!(
4499 merge.winners,
4500 vec![candidate(100, 1, 0), candidate(96, 2, 0)]
4501 );
4502 assert!(merge.refill.is_empty());
4503 }
4504
4505 #[test]
4506 fn merge_top_k_refill_rules_follow_the_bounds() {
4507 let shards = vec![
4509 TabletTopK {
4510 tablet: tablet(1),
4511 rows: vec![candidate(100, 1, 0), candidate(90, 1, 1)],
4512 unseen_bound: Some(95),
4513 },
4514 TabletTopK {
4515 tablet: tablet(2),
4516 rows: vec![candidate(96, 2, 0)],
4517 unseen_bound: None,
4518 },
4519 ];
4520 let merge = merge_top_k(&shards, 2);
4521 assert!(
4522 merge.refill.is_empty(),
4523 "unseen scores of tablet 1 are at most 95 < 96: {:?}",
4524 merge.refill
4525 );
4526 let shards = vec![
4528 TabletTopK {
4529 tablet: tablet(1),
4530 rows: vec![candidate(100, 1, 0), candidate(90, 1, 1)],
4531 unseen_bound: Some(97),
4532 },
4533 TabletTopK {
4534 tablet: tablet(2),
4535 rows: vec![candidate(96, 2, 0)],
4536 unseen_bound: None,
4537 },
4538 ];
4539 let merge = merge_top_k(&shards, 2);
4540 assert_eq!(merge.refill, vec![tablet(1)]);
4541 let shards = vec![
4544 TabletTopK {
4545 tablet: tablet(1),
4546 rows: vec![candidate(100, 1, 0)],
4547 unseen_bound: Some(96),
4548 },
4549 TabletTopK {
4550 tablet: tablet(2),
4551 rows: vec![candidate(96, 2, 9)],
4552 unseen_bound: None,
4553 },
4554 ];
4555 let merge = merge_top_k(&shards, 2);
4556 assert_eq!(
4557 merge.refill,
4558 vec![tablet(1)],
4559 "(96, tablet 1, min row id) ranks better than (96, tablet 2, 9)"
4560 );
4561 let shards = vec![
4563 TabletTopK {
4564 tablet: tablet(1),
4565 rows: vec![candidate(5, 1, 0)],
4566 unseen_bound: Some(1),
4567 },
4568 TabletTopK {
4569 tablet: tablet(2),
4570 rows: Vec::new(),
4571 unseen_bound: None,
4572 },
4573 ];
4574 let merge = merge_top_k(&shards, 3);
4575 assert_eq!(merge.winners, vec![candidate(5, 1, 0)]);
4576 assert_eq!(merge.refill, vec![tablet(1)]);
4577 let merge = merge_top_k(&shards, 0);
4579 assert!(merge.winners.is_empty() && merge.refill.is_empty());
4580 }
4581
4582 #[test]
4583 fn exact_top_k_fills_winners_via_adaptive_refill() {
4584 let tablet_one_rows = vec![
4586 candidate(100, 1, 0),
4587 candidate(99, 1, 1),
4588 candidate(98, 1, 2),
4589 candidate(97, 1, 3),
4590 ];
4591 let shards = vec![
4592 TabletTopK {
4593 tablet: tablet(1),
4594 rows: tablet_one_rows[..2].to_vec(),
4595 unseen_bound: Some(98),
4596 },
4597 TabletTopK {
4598 tablet: tablet(2),
4599 rows: vec![candidate(96, 2, 0), candidate(95, 2, 1)],
4600 unseen_bound: None,
4601 },
4602 ];
4603 let remaining = tablet_one_rows.clone();
4604 let result = exact_top_k(3, shards, move |tablet_id| {
4605 assert_eq!(tablet_id, tablet(1));
4606 TabletTopK {
4607 tablet: tablet_id,
4608 rows: remaining[2..].to_vec(),
4609 unseen_bound: None,
4610 }
4611 })
4612 .unwrap();
4613 assert_eq!(
4614 result,
4615 vec![
4616 candidate(100, 1, 0),
4617 candidate(99, 1, 1),
4618 candidate(98, 1, 2),
4619 ]
4620 );
4621 }
4622
4623 #[test]
4624 fn exact_top_k_rejects_a_stuck_refill() {
4625 let shards = vec![TabletTopK {
4626 tablet: tablet(1),
4627 rows: Vec::new(),
4628 unseen_bound: Some(10),
4629 }];
4630 let error = exact_top_k(1, shards, |tablet| TabletTopK {
4631 tablet,
4632 rows: Vec::new(),
4633 unseen_bound: Some(10),
4634 })
4635 .unwrap_err();
4636 assert!(matches!(error, DistributedError::InvalidPlan(_)), "{error}");
4637 }
4638
4639 #[test]
4640 fn exact_top_k_matches_single_node_oracle_across_1000_sharded_datasets() {
4641 let mut total_refills = 0usize;
4642 for seed in 0..1_000u64 {
4643 let mut rng = SplitMix64(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15).wrapping_add(1));
4644 let tablet_count = 1 + rng.below(8) as usize;
4645 let k = rng.below(30) as usize;
4646 let batch = 1 + rng.below(4) as usize;
4647 let mut oracle_rows = Vec::new();
4648 let mut per_tablet: HashMap<TabletId, Vec<TopKCandidate>> = HashMap::new();
4649 for index in 0..tablet_count {
4650 let tablet = tablet(index as u8 + 1);
4651 let row_count = rng.below(40) as usize;
4652 let mut rows: Vec<TopKCandidate> = (0..row_count)
4653 .map(|row| TopKCandidate {
4654 score: rng.below(12),
4655 tablet,
4656 row_id: RowId(row as u64),
4657 })
4658 .collect();
4659 rows.sort_by(topk_cmp);
4660 oracle_rows.extend(rows.iter().copied());
4661 per_tablet.insert(tablet, rows);
4662 }
4663 let mut oracle = oracle_rows;
4664 oracle.sort_by(topk_cmp);
4665 oracle.truncate(k);
4666 let contribution = |tablet: TabletId, offset: usize| -> TabletTopK {
4667 let rows = &per_tablet[&tablet];
4668 let start = offset.min(rows.len());
4669 let end = (offset + batch).min(rows.len());
4670 TabletTopK {
4671 tablet,
4672 rows: rows[start..end].to_vec(),
4673 unseen_bound: rows.get(end).map(|candidate| candidate.score),
4674 }
4675 };
4676 let initial: Vec<TabletTopK> = (0..tablet_count)
4677 .map(|index| contribution(tablet(index as u8 + 1), 0))
4678 .collect();
4679 let mut returned: HashMap<TabletId, usize> = (0..tablet_count)
4680 .map(|index| (tablet(index as u8 + 1), batch))
4681 .collect();
4682 let result = exact_top_k(k, initial, |tablet| {
4683 total_refills += 1;
4684 let offset = returned[&tablet];
4685 returned.insert(tablet, offset + batch);
4686 contribution(tablet, offset)
4687 })
4688 .unwrap_or_else(|error| panic!("seed {seed}: {error}"));
4689 assert_eq!(result, oracle, "seed {seed}: k={k} batch={batch}");
4690 }
4691 assert!(
4692 total_refills > 0,
4693 "the property run must actually exercise adaptive refill"
4694 );
4695 }
4696
4697 fn coordinator_with(
4704 executor: Arc<dyn FragmentExecutor>,
4705 ) -> (
4706 Arc<Coordinator>,
4707 Arc<InMemoryTransport>,
4708 Arc<SqlQueryRegistry>,
4709 ) {
4710 let transport = Arc::new(InMemoryTransport::new(executor));
4711 let registry = Arc::new(SqlQueryRegistry::default());
4712 let coordinator = Arc::new(Coordinator::new(
4713 Arc::clone(&transport) as Arc<dyn FragmentTransport>,
4714 Arc::clone(®istry),
4715 ));
4716 (coordinator, transport, registry)
4717 }
4718
4719 struct BlockingExecutor {
4722 barrier: Arc<tokio::sync::Barrier>,
4723 }
4724
4725 #[async_trait::async_trait]
4726 impl FragmentExecutor for BlockingExecutor {
4727 async fn execute(
4728 &self,
4729 _fragment: &PlanFragment,
4730 _inputs: Vec<FragmentStream>,
4731 control: FragmentControl,
4732 ) -> DistributedResult<FragmentStream> {
4733 self.barrier.wait().await;
4734 control.control.cancelled().await;
4735 Err(DistributedError::Cancelled(control.control.reason()))
4736 }
4737 }
4738
4739 fn scan_plan(table: &str) -> (DistributedPlan, Vec<TabletId>) {
4742 let tablets = [tablet(1), tablet(2), tablet(3)];
4743 let (locator, metadata) = world(&[(
4744 table,
4745 &tablets,
4746 hash_spec("id"),
4747 stats(3_000, 3 * 1024 * 1024),
4748 )]);
4749 let plan = distribute(&desc(scan(table)), &locator, &metadata).unwrap();
4750 (plan, tablets.to_vec())
4751 }
4752
4753 #[tokio::test]
4754 async fn scan_gather_returns_all_rows() {
4755 let store = Arc::new(InMemoryTableStore::new());
4756 let tablets = [tablet(1), tablet(2), tablet(3)];
4757 store.insert("t", tablets[0], i64_batch(&[("v", vec![1, 2])]));
4758 store.insert("t", tablets[1], i64_batch(&[("v", vec![3])]));
4759 let (plan, _) = scan_plan("t");
4761 let (coordinator, _, _) = coordinator_with(Arc::new(InMemoryFragmentExecutor::new(store)));
4762 let batches = coordinator.execute(&plan).await.unwrap();
4763 let mut values = collect_i64(&batches, "v");
4764 values.sort_unstable();
4765 assert_eq!(values, vec![1, 2, 3]);
4766 }
4767
4768 #[tokio::test]
4769 async fn merge_sort_matches_single_node_sort() {
4770 let store = Arc::new(InMemoryTableStore::new());
4771 let tablets = [tablet(1), tablet(2), tablet(3)];
4772 store.insert("s", tablets[0], i64_batch(&[("x", vec![5, 1, 9])]));
4773 store.insert("s", tablets[1], i64_batch(&[("x", vec![3, 7])]));
4774 store.insert("s", tablets[2], i64_batch(&[("x", vec![8, 2, 6, 4])]));
4775 let (locator, metadata) = world(&[("s", &tablets, hash_spec("id"), stats(9, 900))]);
4776 let (coordinator, _, _) = coordinator_with(Arc::new(InMemoryFragmentExecutor::new(store)));
4777
4778 let sort = LogicalPlanLite::Sort {
4780 input: Box::new(scan("s")),
4781 keys: vec![SortKey {
4782 column: "x".to_owned(),
4783 descending: false,
4784 }],
4785 limit: None,
4786 };
4787 let plan = distribute(&desc(sort), &locator, &metadata).unwrap();
4788 let batches = coordinator.execute(&plan).await.unwrap();
4789 assert_eq!(collect_i64(&batches, "x"), vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
4790
4791 let sort = LogicalPlanLite::Sort {
4793 input: Box::new(scan("s")),
4794 keys: vec![SortKey {
4795 column: "x".to_owned(),
4796 descending: false,
4797 }],
4798 limit: Some(4),
4799 };
4800 let plan = distribute(&desc(sort), &locator, &metadata).unwrap();
4801 let batches = coordinator.execute(&plan).await.unwrap();
4802 assert_eq!(collect_i64(&batches, "x"), vec![1, 2, 3, 4]);
4803 }
4804
4805 #[tokio::test]
4806 async fn final_aggregate_matches_single_node_aggregate() {
4807 let store = Arc::new(InMemoryTableStore::new());
4808 let tablets = [tablet(1), tablet(2), tablet(3)];
4809 store.insert(
4810 "t",
4811 tablets[0],
4812 i64_batch(&[("g", vec![1, 2, 1, 3]), ("v", vec![10, 20, 30, 40])]),
4813 );
4814 store.insert(
4815 "t",
4816 tablets[1],
4817 i64_batch(&[("g", vec![2, 1, 3]), ("v", vec![50, 60, 70])]),
4818 );
4819 store.insert(
4820 "t",
4821 tablets[2],
4822 i64_batch(&[("g", vec![3, 2]), ("v", vec![80, 90])]),
4823 );
4824 let (locator, metadata) = world(&[("t", &tablets, hash_spec("id"), stats(9, 900))]);
4825 let (coordinator, _, _) = coordinator_with(Arc::new(InMemoryFragmentExecutor::new(store)));
4826
4827 let aggregates = vec![
4828 AggregateExpr {
4829 function: AggregateFunction::Count,
4830 column: None,
4831 },
4832 AggregateExpr {
4833 function: AggregateFunction::Sum,
4834 column: Some("v".to_owned()),
4835 },
4836 AggregateExpr {
4837 function: AggregateFunction::Min,
4838 column: Some("v".to_owned()),
4839 },
4840 AggregateExpr {
4841 function: AggregateFunction::Max,
4842 column: Some("v".to_owned()),
4843 },
4844 AggregateExpr {
4845 function: AggregateFunction::Avg,
4846 column: Some("v".to_owned()),
4847 },
4848 ];
4849 let grouped = LogicalPlanLite::Aggregate {
4850 input: Box::new(scan("t")),
4851 group_by: vec!["g".to_owned()],
4852 aggregates,
4853 };
4854 let plan = distribute(&desc(grouped), &locator, &metadata).unwrap();
4855 let batches = coordinator.execute(&plan).await.unwrap();
4856 let groups = collect_i64(&batches, "g");
4857 let counts = collect_i64(&batches, "count_star");
4858 let sums = collect_i64(&batches, "sum_v");
4859 let mins = collect_i64(&batches, "min_v");
4860 let maxs = collect_i64(&batches, "max_v");
4861 let avgs = collect_f64(&batches, "avg_v");
4862 let mut oracle: HashMap<i64, (i64, i64, i64, i64)> = HashMap::new();
4864 for (g, v) in [
4865 (1, 10),
4866 (2, 20),
4867 (1, 30),
4868 (3, 40),
4869 (2, 50),
4870 (1, 60),
4871 (3, 70),
4872 (3, 80),
4873 (2, 90),
4874 ] {
4875 let entry = oracle.entry(g).or_insert((0, 0, i64::MAX, i64::MIN));
4876 entry.0 += 1;
4877 entry.1 += v;
4878 entry.2 = entry.2.min(v);
4879 entry.3 = entry.3.max(v);
4880 }
4881 assert_eq!(groups.len(), 3);
4882 for index in 0..groups.len() {
4883 let (count, sum, min, max) = oracle[&groups[index]];
4884 assert_eq!(counts[index], count, "count for g={}", groups[index]);
4885 assert_eq!(sums[index], sum, "sum for g={}", groups[index]);
4886 assert_eq!(mins[index], min, "min for g={}", groups[index]);
4887 assert_eq!(maxs[index], max, "max for g={}", groups[index]);
4888 let expected_avg = sum as f64 / count as f64;
4889 let actual_avg = avgs[index].expect("avg is set");
4890 assert!(
4891 (actual_avg - expected_avg).abs() < 1e-12,
4892 "avg for g={}: {actual_avg} vs {expected_avg}",
4893 groups[index]
4894 );
4895 }
4896
4897 let scalar = LogicalPlanLite::Aggregate {
4899 input: Box::new(scan("t")),
4900 group_by: Vec::new(),
4901 aggregates: vec![
4902 AggregateExpr {
4903 function: AggregateFunction::Count,
4904 column: None,
4905 },
4906 AggregateExpr {
4907 function: AggregateFunction::Sum,
4908 column: Some("v".to_owned()),
4909 },
4910 ],
4911 };
4912 let plan = distribute(&desc(scalar), &locator, &metadata).unwrap();
4913 let batches = coordinator.execute(&plan).await.unwrap();
4914 assert_eq!(collect_i64(&batches, "count_star"), vec![9]);
4915 assert_eq!(collect_i64(&batches, "sum_v"), vec![450]);
4916 }
4917
4918 #[tokio::test]
4919 async fn distributed_top_k_matches_oracle_with_and_without_refill() {
4920 let store = Arc::new(InMemoryTableStore::new());
4923 let tablets = [tablet(1), tablet(2), tablet(3)];
4924 store.insert(
4925 "k",
4926 tablets[0],
4927 score_batch(vec![100, 90, 80, 70], vec![0, 1, 2, 3], vec![0, 1, 2, 3]),
4928 );
4929 store.insert(
4930 "k",
4931 tablets[1],
4932 score_batch(vec![90, 80, 60], vec![0, 1, 2], vec![0, 1, 2]),
4933 );
4934 store.insert(
4935 "k",
4936 tablets[2],
4937 score_batch(vec![90, 80, 50], vec![0, 1, 2], vec![0, 1, 2]),
4938 );
4939 let (locator, metadata) = world(&[("k", &tablets, hash_spec("id"), stats(10, 1_000))]);
4940 let topk = LogicalPlanLite::Sort {
4941 input: Box::new(scan("k")),
4942 keys: vec![SortKey {
4943 column: "score".to_owned(),
4944 descending: true,
4945 }],
4946 limit: Some(5),
4947 };
4948 let mut oracle: Vec<TopKCandidate> = Vec::new();
4950 for (tablet_n, rows) in [
4951 (1u8, vec![(100u64, 0u64), (90, 1), (80, 2), (70, 3)]),
4952 (2u8, vec![(90, 0), (80, 1), (60, 2)]),
4953 (3u8, vec![(90, 0), (80, 1), (50, 2)]),
4954 ] {
4955 for (score, row_id) in rows {
4956 oracle.push(candidate(score, tablet_n, row_id));
4957 }
4958 }
4959 oracle.sort_by(topk_cmp);
4960 oracle.truncate(5);
4961 let expected: Vec<(u64, i64)> = oracle
4962 .iter()
4963 .map(|winner| (winner.score, winner.row_id.0 as i64))
4964 .collect();
4965
4966 let run = async |emit_batch: Option<usize>| {
4967 let executor = match emit_batch {
4968 Some(batch) => InMemoryFragmentExecutor::with_topk_emit_batch(store.clone(), batch),
4969 None => InMemoryFragmentExecutor::new(store.clone()),
4970 };
4971 let (coordinator, transport, _) = coordinator_with(Arc::new(executor));
4972 let plan = distribute(&desc(topk.clone()), &locator, &metadata).unwrap();
4973 let batches = coordinator.execute(&plan).await.unwrap();
4974 (batches, transport)
4975 };
4976
4977 let (batches, transport) = run(None).await;
4979 let scores = collect_u64(&batches, "score");
4980 let payloads = collect_i64(&batches, "payload");
4981 let actual: Vec<(u64, i64)> = scores.into_iter().zip(payloads).collect();
4982 assert_eq!(actual, expected);
4983 assert!(
4984 transport.refill_log().is_empty(),
4985 "exact local top-k never needs refill"
4986 );
4987 assert!(
4988 batches
4989 .iter()
4990 .all(|batch| batch.schema().index_of(TOPK_ROWID_COLUMN).is_err()),
4991 "the internal row-id column is stripped"
4992 );
4993
4994 let (batches, transport) = run(Some(2)).await;
4997 let scores = collect_u64(&batches, "score");
4998 let payloads = collect_i64(&batches, "payload");
4999 let actual: Vec<(u64, i64)> = scores.into_iter().zip(payloads).collect();
5000 assert_eq!(actual, expected);
5001 assert!(
5002 !transport.refill_log().is_empty(),
5003 "bounded emission must trigger adaptive refill"
5004 );
5005 }
5006
5007 #[tokio::test]
5008 async fn cancellation_fans_out_to_every_fragment() {
5009 let barrier = Arc::new(tokio::sync::Barrier::new(4));
5010 let executor = Arc::new(BlockingExecutor {
5011 barrier: Arc::clone(&barrier),
5012 });
5013 let (coordinator, transport, _registry) = coordinator_with(executor);
5014 let (plan, _) = scan_plan("t");
5015 let task = {
5016 let coordinator = Arc::clone(&coordinator);
5017 let plan = plan.clone();
5018 tokio::spawn(async move { coordinator.execute(&plan).await })
5019 };
5020 tokio::time::timeout(Duration::from_secs(30), barrier.wait())
5021 .await
5022 .expect("all fragments started");
5023 assert!(coordinator.cancel_query(&plan.query_id).unwrap());
5024 let result = task.await.unwrap();
5025 assert!(
5026 matches!(
5027 result,
5028 Err(DistributedError::Cancelled(
5029 CancellationReason::ClientRequest
5030 ))
5031 ),
5032 "cancellation surfaces as ClientRequest: {result:?}"
5033 );
5034 let cancelled = transport.cancelled_fragments();
5035 for fragment_id in 0..3 {
5036 assert!(
5037 cancelled.contains(&fragment_id),
5038 "fragment {fragment_id} received the transport cancel: {cancelled:?}"
5039 );
5040 let control = transport.control_for(fragment_id).unwrap();
5041 assert_eq!(control.reason(), CancellationReason::ClientRequest);
5042 }
5043 }
5044
5045 #[tokio::test]
5046 async fn registry_cancel_reaches_all_fragments() {
5047 let barrier = Arc::new(tokio::sync::Barrier::new(4));
5048 let executor = Arc::new(BlockingExecutor {
5049 barrier: Arc::clone(&barrier),
5050 });
5051 let (coordinator, transport, registry) = coordinator_with(executor);
5052 let (plan, _) = scan_plan("t");
5053 let task = {
5054 let coordinator = Arc::clone(&coordinator);
5055 let plan = plan.clone();
5056 tokio::spawn(async move { coordinator.execute(&plan).await })
5057 };
5058 tokio::time::timeout(Duration::from_secs(30), barrier.wait())
5059 .await
5060 .expect("all fragments started");
5061 let bridged: crate::query_registry::QueryId = plan.query_id.to_hex().parse().unwrap();
5063 assert_eq!(registry.cancel(bridged), CancelOutcome::Accepted);
5064 let result = task.await.unwrap();
5065 assert!(
5066 matches!(result, Err(DistributedError::Cancelled(_))),
5067 "registry cancellation aborts the distributed query: {result:?}"
5068 );
5069 for fragment_id in 0..3 {
5070 let control = transport.control_for(fragment_id).unwrap();
5071 assert_eq!(
5072 control.reason(),
5073 CancellationReason::ClientRequest,
5074 "fragment {fragment_id} observed the registry cancel"
5075 );
5076 }
5077 }
5078
5079 #[tokio::test]
5080 async fn lease_expiry_reclaims_abandoned_fragments() {
5081 let barrier = Arc::new(tokio::sync::Barrier::new(4));
5082 let executor = Arc::new(BlockingExecutor {
5083 barrier: Arc::clone(&barrier),
5084 });
5085 let transport = Arc::new(InMemoryTransport::new(executor));
5086 let registry = Arc::new(SqlQueryRegistry::default());
5087 let coordinator = Arc::new(Coordinator::with_limits(
5088 Arc::clone(&transport) as Arc<dyn FragmentTransport>,
5089 registry,
5090 1_024,
5091 16 * 1024 * 1024 * 1024,
5092 Duration::from_secs(30),
5093 ));
5094 let (plan, _) = scan_plan("t");
5095 let task = {
5096 let coordinator = Arc::clone(&coordinator);
5097 let plan = plan.clone();
5098 tokio::spawn(async move { coordinator.execute(&plan).await })
5099 };
5100 tokio::time::timeout(Duration::from_secs(30), barrier.wait())
5101 .await
5102 .expect("all fragments started");
5103 assert_eq!(coordinator.resources().reserved_fragments(), 3);
5104 let cleaned = coordinator.sweep_expired_leases(Instant::now() + Duration::from_secs(3_600));
5106 assert_eq!(cleaned, 3, "every abandoned fragment is reclaimed");
5107 assert_eq!(
5108 coordinator.resources().reserved_fragments(),
5109 0,
5110 "reclaimed fragments release their reservations"
5111 );
5112 for fragment_id in 0..3 {
5113 let control = transport.control_for(fragment_id).unwrap();
5114 assert_eq!(
5115 control.reason(),
5116 CancellationReason::ServerShutdown,
5117 "fragment {fragment_id} observes the worker loss"
5118 );
5119 }
5120 let result = task.await.unwrap();
5121 assert!(
5122 matches!(
5123 result,
5124 Err(DistributedError::Cancelled(
5125 CancellationReason::ServerShutdown
5126 ))
5127 ),
5128 "the query fails with the worker-loss reason: {result:?}"
5129 );
5130 }
5131
5132 #[test]
5133 fn resource_reservation_denies_then_releases() {
5134 let ledger = Arc::new(ResourceLedger::new(1, u64::MAX));
5135 let fragment = PlanFragment {
5136 fragment_id: 0,
5137 assignment: FragmentAssignment::Coordinator,
5138 operators: Vec::new(),
5139 estimated_rows: 1,
5140 estimated_bytes: 100,
5141 max_spill_bytes: 0,
5142 };
5143 let permit = ledger.reserve(&fragment).unwrap();
5144 assert_eq!(ledger.reserved_fragments(), 1);
5145 assert_eq!(ledger.reserved_bytes(), 100);
5146 let denied = ledger.reserve(&fragment).unwrap_err();
5147 assert!(
5148 matches!(denied, DistributedError::Reservation { fragment_id: 0, .. }),
5149 "{denied}"
5150 );
5151 drop(permit);
5152 assert_eq!(ledger.reserved_fragments(), 0);
5153 assert_eq!(ledger.reserved_bytes(), 0);
5154 ledger.reserve(&fragment).unwrap();
5155 let tight = Arc::new(ResourceLedger::new(8, 50));
5157 let denied = tight.reserve(&fragment).unwrap_err();
5158 assert!(
5159 matches!(denied, DistributedError::Reservation { .. }),
5160 "{denied}"
5161 );
5162 }
5163
5164 #[test]
5165 fn repartition_routes_every_row_exactly_once() {
5166 let batch = i64_batch(&[("k", (0..100).collect()), ("v", (0..100).collect())]);
5167 let frames = vec![BatchFrame::data(batch)];
5168 let keys = vec!["k".to_owned()];
5169 let mut partitions = Vec::new();
5170 for index in 0..3 {
5171 partitions.push(repartition_frames(&frames, &keys, 3, index).unwrap());
5172 }
5173 let total: usize = partitions
5174 .iter()
5175 .map(|frames| {
5176 frames
5177 .iter()
5178 .map(|frame| frame.batch.num_rows())
5179 .sum::<usize>()
5180 })
5181 .sum();
5182 assert_eq!(total, 100, "every row lands in exactly one partition");
5183 for (index, partition) in partitions.iter().enumerate() {
5186 for other in 0..3 {
5187 let rerouted = repartition_frames(partition, &keys, 3, other).unwrap();
5188 let rows: usize = rerouted.iter().map(|frame| frame.batch.num_rows()).sum();
5189 if other == index {
5190 let expected: usize =
5191 partition.iter().map(|frame| frame.batch.num_rows()).sum();
5192 assert_eq!(rows, expected, "partition {index} rows stay in {index}");
5193 } else {
5194 assert_eq!(rows, 0, "partition {index} leaks rows into {other}");
5195 }
5196 }
5197 }
5198 }
5199
5200 #[test]
5201 fn score_key_mapping_preserves_order() {
5202 let ints = Int64Array::from(vec![i64::MIN, -5, 0, 5, i64::MAX]);
5203 let keys: Vec<u64> = (0..5).map(|row| score_key(&ints, row).unwrap()).collect();
5204 let mut sorted = keys.clone();
5205 sorted.sort_unstable();
5206 assert_eq!(keys, sorted, "int64 mapping is order-preserving");
5207 let floats = Float64Array::from(vec![f64::MIN, -1.5, 0.0, 2.5, f64::MAX]);
5208 let keys: Vec<u64> = (0..5).map(|row| score_key(&floats, row).unwrap()).collect();
5209 let mut sorted = keys.clone();
5210 sorted.sort_unstable();
5211 assert_eq!(keys, sorted, "float64 mapping is order-preserving");
5212 }
5213}