Skip to main content

mongreldb_query/
distributed.rs

1//! Distributed SQL plan groundwork (Stage 3J, spec section 12.10).
2//!
3//! This module is the planning and coordination seam for tablet-distributed
4//! queries. It deliberately depends on small local metadata traits
5//! ([`TabletLocator`], [`ClusterMetadata`]) instead of `mongreldb-cluster`, so
6//! the query crate stays free of the cluster dependency; the cluster wave
7//! adapts `mongreldb_cluster::tablet` routing metadata onto these traits.
8//!
9//! # What lives here
10//!
11//! * [`DistributedPlan`] — the plan structure from spec section 12.10: pinned
12//!   [`QueryId`] + [`MetadataVersion`], [`PlanFragment`]s with tablet
13//!   assignments, and [`ExchangeDescriptor`] edges between them.
14//! * [`distribute`] — the planner. It lowers a [`LogicalPlanLite`] tree (the
15//!   simplified input IR) onto tablets: colocated joins when both sides share
16//!   partitioning and layout, broadcast joins when the small side fits under a
17//!   byte threshold, repartition joins otherwise. Every fragment carries
18//!   estimated rows/bytes and a maximum spill allowance (spec section 12.10).
19//! * Distributed top-k — [`merge_top_k`] / [`exact_top_k`] are pure functions
20//!   implementing the deterministic coordinator merge (final score descending,
21//!   tablet id ascending, [`RowId`] ascending) with adaptive refill when a
22//!   tablet's unseen-score bound shows it could still contribute winners.
23//! * Execution skeleton — [`FragmentExecutor`], [`FragmentTransport`], and the
24//!   [`Coordinator`] runtime: per-fragment resource reservation, cancellation
25//!   fan-out wired to the existing [`SqlQueryRegistry`](crate::SqlQueryRegistry),
26//!   worker lease expiry that cleans abandoned fragments, and real in-memory
27//!   merge operators (k-way [`MergeSort`](FragmentOperator::MergeSort),
28//!   [`FinalAggregate`](FragmentOperator::FinalAggregate) combine, distributed
29//!   top-k) used until the remote Arrow IPC transport lands.
30//!
31//! # Integration point with DataFusion
32//!
33//! `LogicalPlanLite` is intentionally minimal. The DataFusion integration wave
34//! lowers `datafusion::logical_expr::LogicalPlan` (produced by
35//! [`MongrelSession`](crate::MongrelSession) after catalog/schema resolution)
36//! onto `LogicalPlanLite` — scans of `MongrelScanExec`-backed tables become
37//! [`LogicalPlanLite::Scan`], aggregates/joins/sorts/limits map one-to-one —
38//! and hands the tree to [`distribute`]. Full DataFusion plan conversion is
39//! explicitly out of scope for this wave.
40
41use 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
61/// Small-side byte threshold below which a join is planned as a broadcast
62/// join (spec section 12.10: "broadcast small side").
63pub const DEFAULT_BROADCAST_THRESHOLD_BYTES: u64 = 8 * 1024 * 1024;
64
65/// Default per-fragment maximum spill allowance (spec section 12.10: "every
66/// plan includes ... a maximum spill allowance"). Bound to the core
67/// [`mongreldb_core::SpillManager`] via [`FragmentControl::begin_spill`].
68pub const DEFAULT_MAX_SPILL_BYTES_PER_FRAGMENT: u64 = 256 * 1024 * 1024;
69
70/// Rows per `RecordBatch` emitted by coordinator merge operators.
71const COORDINATOR_OUTPUT_BATCH_ROWS: usize = 8_192;
72
73/// Name of the unsigned row-id column every scored (top-k) stream carries.
74/// Tablet scans feeding a distributed top-k must project this column; it is
75/// stripped from the coordinator's final top-k output.
76pub const TOPK_ROWID_COLUMN: &str = "__rowid";
77
78/// Errors raised by distributed planning and coordination.
79#[non_exhaustive]
80#[derive(Debug, thiserror::Error)]
81pub enum DistributedError {
82    /// The locator or metadata has no entry for the table.
83    #[error("unknown table `{0}`")]
84    UnknownTable(String),
85    /// The table's layout has no tablets to scan.
86    #[error("table `{table}` has no tablets in its layout")]
87    EmptyLayout { table: String },
88    /// The input IR or a computed plan is not well-formed.
89    #[error("invalid plan: {0}")]
90    InvalidPlan(String),
91    /// A fragment's resource reservation was denied.
92    #[error("fragment {fragment_id} resource reservation denied: {reason}")]
93    Reservation { fragment_id: u32, reason: String },
94    /// A fragment failed on its worker.
95    #[error("fragment {fragment_id} failed on worker {worker}: {message}")]
96    FragmentExecution {
97        fragment_id: u32,
98        worker: String,
99        message: String,
100    },
101    /// The query was cancelled; carries the first observed reason.
102    #[error("distributed query cancelled: {0:?}")]
103    Cancelled(CancellationReason),
104    /// A groundwork boundary was crossed (documented later-wave work).
105    #[error("unsupported in this wave: {0}")]
106    Unsupported(String),
107    /// Arrow kernel failure inside a merge operator.
108    #[error("arrow error: {0}")]
109    Arrow(String),
110}
111
112/// Result alias for distributed planning and coordination.
113pub 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
121/// FNV-1a 64-bit hash (same convention as the cluster routing code).
122fn 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// ---------------------------------------------------------------------------
132// Cluster metadata traits (the dependency-free seam)
133// ---------------------------------------------------------------------------
134
135/// How one table's rows map onto tablets — the planner's dependency-free
136/// mirror of `mongreldb_cluster::tablet::Partitioning` (spec section 12.2).
137/// The cluster wave adapts the cluster type onto this enum one-to-one.
138#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
139pub enum PartitionSpec {
140    /// Hash of the declared columns into a fixed bucket space.
141    Hash { columns: Vec<String>, buckets: u32 },
142    /// Lexicographic ranges over the declared columns.
143    Range { columns: Vec<String> },
144    /// One bucket space per tenant.
145    Tenant {
146        column: String,
147        buckets_per_tenant: u32,
148    },
149    /// Time-bucketed ranges over the timestamp column.
150    TimeRange { column: String },
151    /// Single-tablet (unpartitioned) table.
152    Unpartitioned,
153}
154
155impl PartitionSpec {
156    /// The declared partition-key columns, in key order.
157    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    /// True when two tables partition identically, so equal partition keys
168    /// land on the same tablet and a join on the partition key can be planned
169    /// colocated (spec section 12.10: "colocated join"). Two unpartitioned
170    /// tables are trivially colocated (both live on their single tablet).
171    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/// Planner statistics for one table.
181#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
182pub struct TableStats {
183    /// Estimated live row count.
184    pub row_count: u64,
185    /// Estimated total size in bytes.
186    pub total_bytes: u64,
187}
188
189/// Resolves which tablets serve one table. Defined here (not in
190/// `mongreldb-cluster`) so the query crate stays free of the cluster
191/// dependency; the cluster wave binds `TabletLayout`/routing metadata onto
192/// this trait.
193pub trait TabletLocator: Send + Sync {
194    /// The tablets serving `table`, in deterministic layout order.
195    fn tablets_for_table(&self, table: &str) -> DistributedResult<Vec<TabletId>>;
196    /// How `table` is partitioned across those tablets.
197    fn partitioning(&self, table: &str) -> DistributedResult<PartitionSpec>;
198}
199
200/// Control-plane metadata the planner pins against (spec section 12.10:
201/// "coordinator plans using metadata version").
202pub trait ClusterMetadata: Send + Sync {
203    /// The control-plane metadata version this plan is pinned to.
204    fn metadata_version(&self) -> MetadataVersion;
205    /// Planner statistics for one table.
206    fn table_stats(&self, table: &str) -> DistributedResult<TableStats>;
207}
208
209// ---------------------------------------------------------------------------
210// Input IR (LogicalPlanLite)
211// ---------------------------------------------------------------------------
212
213/// One join-key equality pair (`left.col = right.col`).
214#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
215pub struct JoinKey {
216    /// Column name on the left input.
217    pub left: String,
218    /// Column name on the right input.
219    pub right: String,
220}
221
222/// One sort key.
223#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
224pub struct SortKey {
225    /// Column name.
226    pub column: String,
227    /// True for `ORDER BY ... DESC`.
228    pub descending: bool,
229}
230
231/// One aggregate expression.
232#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
233pub struct AggregateExpr {
234    /// The aggregate function.
235    pub function: AggregateFunction,
236    /// The aggregated column; `None` only for `COUNT(*)`.
237    pub column: Option<String>,
238}
239
240/// Supported aggregate functions.
241#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
242pub enum AggregateFunction {
243    /// Row count (`column == None`) or non-null count.
244    Count,
245    /// Numeric sum.
246    Sum,
247    /// Numeric minimum.
248    Min,
249    /// Numeric maximum.
250    Max,
251    /// Numeric average.
252    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/// The simplified logical IR [`distribute`] lowers onto tablets.
268///
269/// This is the seam documented at the module level: the DataFusion
270/// integration wave lowers real DataFusion plans onto this enum; this wave
271/// plans from it directly.
272#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
273pub enum LogicalPlanLite {
274    /// Base-table scan with an optional opaque predicate and projection.
275    Scan {
276        /// Table name.
277        table: String,
278        /// Opaque predicate text (pushed down verbatim; not interpreted in
279        /// this wave).
280        predicate: Option<String>,
281        /// Projected column names; empty = all columns.
282        projection: Vec<String>,
283    },
284    /// Grouped aggregation.
285    Aggregate {
286        /// Input subtree.
287        input: Box<LogicalPlanLite>,
288        /// Group-by columns.
289        group_by: Vec<String>,
290        /// Aggregate expressions.
291        aggregates: Vec<AggregateExpr>,
292    },
293    /// Equi-join.
294    Join {
295        /// Left input subtree.
296        left: Box<LogicalPlanLite>,
297        /// Right input subtree.
298        right: Box<LogicalPlanLite>,
299        /// Equality key pairs.
300        on: Vec<JoinKey>,
301    },
302    /// Sort with an optional limit (`ORDER BY ... [LIMIT k]`).
303    Sort {
304        /// Input subtree.
305        input: Box<LogicalPlanLite>,
306        /// Sort keys, most significant first.
307        keys: Vec<SortKey>,
308        /// Optional limit.
309        limit: Option<usize>,
310    },
311    /// Limit without an ordering.
312    Limit {
313        /// Input subtree.
314        input: Box<LogicalPlanLite>,
315        /// Row limit.
316        limit: usize,
317    },
318}
319
320// ---------------------------------------------------------------------------
321// Plan model (spec section 12.10)
322// ---------------------------------------------------------------------------
323
324/// Plan-local fragment identifier (index into [`DistributedPlan::fragments`]).
325pub type FragmentId = u32;
326/// Plan-local exchange identifier (index into [`DistributedPlan::exchanges`]).
327pub type ExchangeId = u32;
328
329/// Where one fragment executes.
330#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
331pub enum FragmentAssignment {
332    /// On the worker serving this tablet's data.
333    Tablet(TabletId),
334    /// On the query coordinator (gateway) itself.
335    Coordinator,
336}
337
338/// Which join input is the broadcast (build) side.
339#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
340pub enum BuildSide {
341    /// The left input is broadcast.
342    Left,
343    /// The right input is broadcast.
344    Right,
345}
346
347/// One physical operator inside a fragment (spec section 12.10).
348#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
349pub enum FragmentOperator {
350    /// Scan one tablet's slice of a table.
351    TabletScan {
352        /// Table name.
353        table: String,
354        /// Opaque pushed-down predicate text.
355        predicate: Option<String>,
356        /// Projected columns; empty = all.
357        projection: Vec<String>,
358    },
359    /// Receive one exchange edge from a producer fragment.
360    RemoteExchangeSource {
361        /// The exchange edge this source consumes.
362        exchange: ExchangeId,
363    },
364    /// Emit this fragment's output onto one exchange edge.
365    RemoteExchangeSink {
366        /// The exchange edge this sink feeds.
367        exchange: ExchangeId,
368    },
369    /// Per-tablet partial aggregation (pre-shuffle combine).
370    PartialAggregate {
371        /// Group-by columns.
372        group_by: Vec<String>,
373        /// Aggregate expressions.
374        aggregates: Vec<AggregateExpr>,
375    },
376    /// Coordinator-side combine of partial aggregates.
377    FinalAggregate {
378        /// Group-by columns.
379        group_by: Vec<String>,
380        /// Aggregate expressions.
381        aggregates: Vec<AggregateExpr>,
382    },
383    /// Hash join over colocated inputs (no exchange needed).
384    DistributedHashJoin {
385        /// Equality key pairs.
386        on: Vec<JoinKey>,
387    },
388    /// Join where the small build side is broadcast to every big-side
389    /// fragment.
390    BroadcastJoin {
391        /// Equality key pairs.
392        on: Vec<JoinKey>,
393        /// Which input is broadcast.
394        build_side: BuildSide,
395    },
396    /// Join after both sides are hash-repartitioned on the join keys.
397    RepartitionJoin {
398        /// Equality key pairs.
399        on: Vec<JoinKey>,
400    },
401    /// Producer side: local bounded sort. Coordinator side: deterministic
402    /// k-way merge of sorted streams.
403    MergeSort {
404        /// Sort keys.
405        keys: Vec<SortKey>,
406        /// Optional row limit applied after the sort/merge.
407        limit: Option<usize>,
408    },
409    /// Producer side: bounded local top-k plus tie information. Coordinator
410    /// side: deterministic merge (score desc, tablet asc, [`RowId`] asc) with
411    /// adaptive refill (spec section 12.10).
412    DistributedTopK {
413        /// Number of winners.
414        k: usize,
415        /// The (single, descending) score key.
416        score: SortKey,
417    },
418    /// Row limit (per-fragment locally; global at the coordinator).
419    DistributedLimit {
420        /// Row limit.
421        limit: usize,
422    },
423}
424
425/// How rows move across one exchange edge (spec section 12.10).
426#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
427pub enum ExchangeKind {
428    /// Rows are hash-routed on `keys` across the sibling consumers of the
429    /// producing fragment.
430    HashRepartition {
431        /// Hash key columns (in the producer's output schema).
432        keys: Vec<String>,
433    },
434    /// The producer's full output is replicated to every consumer.
435    Broadcast,
436    /// The producer's output flows to its single consumer.
437    Merge,
438}
439
440/// One exchange edge between two fragments (spec section 12.10).
441#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
442pub struct ExchangeDescriptor {
443    /// Plan-local exchange identifier.
444    pub exchange_id: ExchangeId,
445    /// Producing fragment.
446    pub producer: FragmentId,
447    /// Consuming fragment.
448    pub consumer: FragmentId,
449    /// Row movement kind.
450    pub kind: ExchangeKind,
451    /// FNV-1a fingerprint of the producer's output column names (types join
452    /// the fingerprint with the DataFusion lowering wave).
453    pub schema_fingerprint: u64,
454}
455
456/// One executable unit of a [`DistributedPlan`].
457#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
458pub struct PlanFragment {
459    /// Plan-local fragment identifier.
460    pub fragment_id: FragmentId,
461    /// Where the fragment executes.
462    pub assignment: FragmentAssignment,
463    /// Operators, in execution order (scans/sources first, sink last).
464    pub operators: Vec<FragmentOperator>,
465    /// Estimated output rows (spec section 12.10).
466    pub estimated_rows: u64,
467    /// Estimated output bytes (spec section 12.10).
468    pub estimated_bytes: u64,
469    /// Maximum spill allowance in bytes (spec section 12.10).
470    pub max_spill_bytes: u64,
471}
472
473/// A distributed query plan pinned to one control-plane metadata version
474/// (spec section 12.10).
475#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
476pub struct DistributedPlan {
477    /// The query execution this plan belongs to.
478    pub query_id: QueryId,
479    /// The control-plane metadata version the plan was built against.
480    pub metadata_version: MetadataVersion,
481    /// All fragments; `fragment_id` equals the vector index.
482    pub fragments: Vec<PlanFragment>,
483    /// All exchange edges; `exchange_id` equals the vector index.
484    pub exchanges: Vec<ExchangeDescriptor>,
485}
486
487impl DistributedPlan {
488    /// The fragment with no outgoing exchange (the coordinator root).
489    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    /// Looks up a fragment by id.
497    pub fn fragment(&self, fragment_id: FragmentId) -> Option<&PlanFragment> {
498        self.fragments.get(fragment_id as usize)
499    }
500
501    /// Exchange edges out of one fragment.
502    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    /// Exchange edges into one fragment, ordered by producer id.
512    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// ---------------------------------------------------------------------------
524// Planner
525// ---------------------------------------------------------------------------
526
527/// Everything [`distribute`] needs to build a [`DistributedPlan`].
528#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
529pub struct PlanDescription {
530    /// The query execution id the plan is pinned to.
531    pub query_id: QueryId,
532    /// The logical input tree.
533    pub root: LogicalPlanLite,
534    /// Planner tuning.
535    pub options: PlannerOptions,
536}
537
538/// Planner tuning knobs.
539#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
540pub struct PlannerOptions {
541    /// Small-side byte threshold below which a join is planned as broadcast.
542    pub broadcast_threshold_bytes: u64,
543    /// Per-fragment maximum spill allowance stamped on every fragment.
544    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
556/// Lowers a [`LogicalPlanLite`] tree onto tablets (spec section 12.10).
557///
558/// Join strategy, in decision order:
559///
560/// 1. **Colocated** — both inputs are scans whose partition specs are
561///    colocated ([`PartitionSpec::colocated_with`]) over the identical tablet
562///    list; the hash join runs fused inside each shared tablet's fragment.
563/// 2. **Broadcast** — the smaller input's estimated bytes fit under
564///    [`PlannerOptions::broadcast_threshold_bytes`]; the small side is
565///    replicated to every big-side fragment.
566/// 3. **Repartition** — otherwise; both sides are hash-repartitioned on their
567///    join keys into fresh join fragments.
568///
569/// `Sort` with a single descending key and a limit plans as
570/// [`FragmentOperator::DistributedTopK`]; any other sort plans as
571/// [`FragmentOperator::MergeSort`]. Every fragment carries estimated
572/// rows/bytes and the configured maximum spill allowance.
573pub 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
627/// Validates the input IR before planning.
628fn 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
672/// The planner's view of one planned subtree's output stage.
673struct Stage {
674    /// Fragments producing this stage's output (sinks not yet attached).
675    producers: Vec<FragmentId>,
676    estimated_rows: u64,
677    estimated_bytes: u64,
678    /// Output partitioning, when known (scan stages and colocated joins).
679    partitioning: Option<PartitionSpec>,
680    /// Tablets the stage's fragments run on (empty for coordinator stages).
681    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    /// Connects every producer to every consumer with fresh exchange edges,
713    /// appending the sink operators to producers and source operators to
714    /// consumers (consumers must not have transform operators yet).
715    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    /// FNV-1a over the producer fragment's output column names.
742    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                // Single coordinator producer: combine in place, no shuffle.
834                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        // Colocation can be decided from metadata alone (no fragments built).
895        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    /// Fuses two colocated scans and the hash join into one fragment per
934    /// shared tablet — no exchange at all (spec section 12.10).
935    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    /// Replicates the small side to every big-side fragment (spec section
996    /// 12.10: "broadcast small side").
997    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    /// Hash-repartitions both inputs on their join keys into fresh join
1030    /// fragments (spec section 12.10: "repartition both sides").
1031    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        // The distributed top-k shape (spec section 12.10): one descending
1095        // score key plus a limit. Anything else is a merge sort.
1096        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
1199/// Scales a byte estimate to a new row estimate.
1200fn 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
1210/// Derives a fragment's output column names (fingerprint input).
1211fn 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
1256/// Partial-aggregate output column names (`__partial_0`, and `__partial_i_sum`
1257/// / `__partial_i_count` for averages).
1258fn 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
1271/// Final-aggregate output column name (`count_star`, `sum_cost`, ...).
1272fn 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// ---------------------------------------------------------------------------
1281// Distributed top-k (spec section 12.10): deterministic merge + adaptive refill
1282// ---------------------------------------------------------------------------
1283
1284/// One ranked row in the top-k model: the score plus the exact tie-break
1285/// identity (spec section 12.10: final score descending, tablet id ascending,
1286/// [`RowId`] ascending).
1287#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1288pub struct TopKCandidate {
1289    /// Score mapped onto an order-preserving `u64` key (higher wins).
1290    pub score: u64,
1291    /// The tablet that contributed the row.
1292    pub tablet: TabletId,
1293    /// The row's id within the table.
1294    pub row_id: RowId,
1295}
1296
1297/// The deterministic winner order: score descending, then tablet id
1298/// ascending, then [`RowId`] ascending. Returns [`Ordering::Less`] when `a`
1299/// ranks strictly better than `b`.
1300pub 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/// One tablet's bounded local top-k plus tie information (spec section
1308/// 12.10: "each tablet returns a bounded local top-k plus tie information").
1309#[derive(Clone, Debug)]
1310pub struct TabletTopK {
1311    /// The contributing tablet.
1312    pub tablet: TabletId,
1313    /// Local winners, best first (at most the bounded size).
1314    pub rows: Vec<TopKCandidate>,
1315    /// Upper bound on the score of any row NOT in `rows` (`None` = the tablet
1316    /// is exhausted — every local row has been returned).
1317    pub unseen_bound: Option<u64>,
1318}
1319
1320/// The outcome of one deterministic coordinator merge step.
1321#[derive(Clone, Debug, PartialEq, Eq)]
1322pub struct TopKMerge {
1323    /// The best `k` received candidates, best first.
1324    pub winners: Vec<TopKCandidate>,
1325    /// Tablets that must be refilled before `winners` is provably the exact
1326    /// global top-k (empty = the result is exact).
1327    pub refill: Vec<TabletId>,
1328}
1329
1330/// Deterministically merges bounded local top-ks (spec section 12.10:
1331/// "coordinator merges deterministically").
1332///
1333/// Winners are the best `k` received candidates under [`topk_cmp`]. A tablet
1334/// lands in [`TopKMerge::refill`] when its unseen rows could still displace a
1335/// winner — i.e. when fewer than `k` candidates were received in total and
1336/// the tablet is not exhausted, or when the tablet's most optimistic unseen
1337/// candidate (score = `unseen_bound`, smallest possible [`RowId`]) ranks at
1338/// least as well as the current `k`-th winner. The tie case is deliberately
1339/// conservative (unseen row ids are unknown), so a refill may be requested
1340/// that returns no winning rows; correctness never depends on it.
1341///
1342/// Exactness invariant: when `refill` is empty, `winners` equals the global
1343/// top-`k` of all rows on all tablets. Proof sketch: any unseen row of tablet
1344/// `t` ranks no better than the optimistic candidate `(unseen_bound, t,
1345/// RowId::MIN)` — its score is at most the bound, and at equal score its row
1346/// id is larger — so when the optimistic candidate ranks strictly worse than
1347/// the `k`-th winner, no unseen row of `t` can enter the top-`k`. When `t` is
1348/// exhausted it has no unseen rows at all. With every tablet in one of those
1349/// two states, the received top-`k` is the global top-`k`. When fewer than
1350/// `k` candidates were received, refill is empty iff every tablet is
1351/// exhausted, in which case all rows were seen.
1352pub 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
1396/// Drives [`merge_top_k`] with adaptive refill until the result is provably
1397/// exact (spec section 12.10: "for exact global top-k, use adaptive refill
1398/// when local bounds show unseen rows could still win").
1399///
1400/// `initial` holds each tablet's first bounded contribution. `refill_batch`
1401/// must return the NEXT batch of local winners for one tablet (rows not
1402/// returned before, best first) together with a tightened unseen bound
1403/// (`None` when the tablet is exhausted). Iteration order over tablets is
1404/// sorted by id, so the driver is fully deterministic. Fails when a refill
1405/// makes no progress (no new rows and an unchanged bound), which indicates a
1406/// broken producer contract rather than a planning problem.
1407pub 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// ---------------------------------------------------------------------------
1441// Execution skeleton (spec section 12.10)
1442// ---------------------------------------------------------------------------
1443
1444/// Cooperative per-fragment execution control: cancellation/deadline shared
1445/// with the query's [`ExecutionControl`] hierarchy plus the fragment's spill
1446/// allowance.
1447#[derive(Debug, Clone)]
1448pub struct FragmentControl {
1449    /// Cooperative cancellation handle (child of the query control, so a
1450    /// registry cancel fans out to every fragment).
1451    pub control: ExecutionControl,
1452    /// Maximum spill allowance stamped by the planner (spec section 12.10).
1453    pub max_spill_bytes: u64,
1454}
1455
1456impl FragmentControl {
1457    /// Open a core spill session for this fragment against `manager`, capped
1458    /// at [`Self::max_spill_bytes`]. Query operators that sort/join/aggregate
1459    /// under pressure call this instead of inventing a second spill path.
1460    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/// Tie information for scored (top-k) streams, carried on a producer's
1470/// terminal frame (spec section 12.10: "bounded local top-k plus tie
1471/// information").
1472#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1473pub enum ScoreBound {
1474    /// Not a scored stream, or the producer cannot report bounds; the
1475    /// coordinator treats such producers as exhausted.
1476    Unknown,
1477    /// Unsent rows may have score keys up to this value.
1478    AtMost(u64),
1479    /// The producer emitted every local row.
1480    Exhausted,
1481}
1482
1483/// One Arrow-ish record-batch frame on a fragment stream.
1484#[derive(Debug, Clone)]
1485pub struct BatchFrame {
1486    /// The record batch payload.
1487    pub batch: RecordBatch,
1488    /// Tie information; meaningful only on a scored stream's terminal frame.
1489    pub score_bound: ScoreBound,
1490}
1491
1492impl BatchFrame {
1493    /// A plain data frame (no tie information).
1494    pub fn data(batch: RecordBatch) -> Self {
1495        Self {
1496            batch,
1497            score_bound: ScoreBound::Unknown,
1498        }
1499    }
1500}
1501
1502/// A fragment's output stream.
1503pub type FragmentStream = BoxStream<'static, DistributedResult<BatchFrame>>;
1504
1505/// The next batch of a tablet's local top-k (adaptive refill, spec section
1506/// 12.10).
1507#[derive(Debug)]
1508pub struct TopKRefill {
1509    /// The next local candidates (rows not returned before, best first),
1510    /// aligned with `payload`'s rows.
1511    pub rows: Vec<TopKCandidate>,
1512    /// The candidates' full row payload (same schema as the stream output).
1513    pub payload: RecordBatch,
1514    /// Tightened unseen-score bound (`None` = tablet exhausted).
1515    pub unseen_bound: Option<u64>,
1516}
1517
1518/// Executes one fragment on its worker. The real binding runs tablet
1519/// fragments against a `ClusterReplica` core in a later wave; this wave ships
1520/// [`InMemoryFragmentExecutor`] as the reference implementation.
1521#[async_trait::async_trait]
1522pub trait FragmentExecutor: Send + Sync {
1523    /// Runs the fragment. `inputs` carries one resolved stream per
1524    /// [`FragmentOperator::RemoteExchangeSource`] operator, in operator order.
1525    async fn execute(
1526        &self,
1527        fragment: &PlanFragment,
1528        inputs: Vec<FragmentStream>,
1529        control: FragmentControl,
1530    ) -> DistributedResult<FragmentStream>;
1531
1532    /// Returns the next `limit` local top-k candidates after `offset` (the
1533    /// number already returned), with a tightened unseen bound. Executors
1534    /// without scored streams leave the default, which rejects.
1535    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/// Moves fragments and cancellation between the coordinator and workers
1549/// (spec section 12.10). The Arrow IPC transport with backpressure is the
1550/// remote-transport wave's job; this wave ships the in-memory implementation.
1551#[async_trait::async_trait]
1552pub trait FragmentTransport: Send + Sync {
1553    /// Starts a fragment on its assigned worker and returns its output
1554    /// stream.
1555    async fn execute_fragment(
1556        &self,
1557        fragment: &PlanFragment,
1558        inputs: Vec<FragmentStream>,
1559        control: FragmentControl,
1560    ) -> DistributedResult<FragmentStream>;
1561
1562    /// Best-effort cancellation of a running (or abandoned) fragment.
1563    fn cancel_fragment(&self, fragment_id: FragmentId) -> DistributedResult<()>;
1564
1565    /// Fetches the next top-k batch of a fragment's tablet (adaptive
1566    /// refill). Transports without a refill binding leave the default.
1567    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
1580/// In-memory [`FragmentTransport`]: routes fragments to per-tablet executors,
1581/// records starts/cancellations/refills for test introspection, and keeps
1582/// each fragment's [`ExecutionControl`] so cancellations are observable.
1583pub 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    /// A transport whose every fragment runs on `default_executor`.
1594    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    /// Pins one tablet to its own executor.
1606    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    /// Fragments started so far, in start order.
1624    pub fn started_fragments(&self) -> Vec<FragmentId> {
1625        self.started.lock().clone()
1626    }
1627
1628    /// Fragments cancelled so far, in cancel order.
1629    pub fn cancelled_fragments(&self) -> Vec<FragmentId> {
1630        self.cancelled.lock().clone()
1631    }
1632
1633    /// Top-k refills issued so far: `(fragment_id, offset, limit)`.
1634    pub fn refill_log(&self) -> Vec<(FragmentId, usize, usize)> {
1635        self.refills.lock().clone()
1636    }
1637
1638    /// The control handed to a started fragment, for cancellation assertions.
1639    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/// In-memory per-`(table, tablet)` record-batch store backing
1684/// [`InMemoryFragmentExecutor`].
1685#[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    /// An empty store.
1693    pub fn new() -> Self {
1694        Self::default()
1695    }
1696
1697    /// Appends one batch to a tablet of a table, registering the table's
1698    /// schema from the batch on first sight.
1699    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    /// Registers a table schema explicitly (covers tablets with no rows).
1712    pub fn register_schema(&self, table: &str, schema: SchemaRef) {
1713        self.schemas.write().insert(table.to_owned(), schema);
1714    }
1715
1716    /// All batches stored for one tablet of a table (empty when none).
1717    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    /// The table's registered schema, when known.
1726    pub fn schema(&self, table: &str) -> Option<SchemaRef> {
1727        self.schemas.read().get(table).cloned()
1728    }
1729}
1730
1731/// Reference [`FragmentExecutor`] over an [`InMemoryTableStore`]. Interprets
1732/// scans, projections, partial aggregates, local merge sorts, bounded local
1733/// top-ks (with exact tie information), and limits for real; exchange
1734/// sources drain their input streams; join operators reject (their execution
1735/// binding lands with the tablet wave — plan shape is fully tested).
1736pub struct InMemoryFragmentExecutor {
1737    store: Arc<InMemoryTableStore>,
1738    /// Wire batch for the local top-k: at most this many rows are emitted
1739    /// before reporting a bound (`None` = emit `k`, which never needs a
1740    /// refill). Smaller values exercise the coordinator's adaptive refill.
1741    topk_emit_batch: Option<usize>,
1742}
1743
1744impl InMemoryFragmentExecutor {
1745    /// An executor over `store` that emits up to `k` top-k rows locally.
1746    pub fn new(store: Arc<InMemoryTableStore>) -> Self {
1747        Self {
1748            store,
1749            topk_emit_batch: None,
1750        }
1751    }
1752
1753    /// Bounds the local top-k emission batch (refill exerciser).
1754    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    /// Replays the scan (+ projection) of a fragment from the store.
1762    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                    // Routing is the transport/coordinator's job; the sink is
1850                    // a pass-through here.
1851                }
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
1901// ---------------------------------------------------------------------------
1902// Shared execution helpers and real merge operators
1903// ---------------------------------------------------------------------------
1904
1905/// Maps an [`ExecutionControl`] checkpoint onto a distributed cancellation.
1906fn checkpoint(control: &ExecutionControl) -> DistributedResult<()> {
1907    control
1908        .checkpoint()
1909        .map_err(|_| DistributedError::Cancelled(control.reason()))
1910}
1911
1912/// The tablet a fragment is assigned to (errors for coordinator fragments).
1913fn 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
1923/// Drains a fragment stream with cooperative cancellation.
1924async 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
1936/// Concatenates batches (`None` when the input is empty).
1937fn 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
1947/// Projects every batch onto the named columns.
1948fn 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
1971/// Builds a row converter + key column indexes for sort keys.
1972fn 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        // Groundwork null semantics: nulls sort first under descending keys
1983        // and last under ascending keys. The DataFusion lowering wave maps
1984        // explicit NULLS FIRST/LAST clauses onto this.
1985        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
1998/// Gathers `order`ed rows of one batch into chunked output batches.
1999fn 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
2020/// Interleaves rows from several same-schema streams in `order`
2021/// (`(stream, row)` pairs) into chunked output batches.
2022fn 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
2048/// Producer-side local sort: full deterministic sort of the (unsorted)
2049/// input, optionally truncated to `limit` rows.
2050fn 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
2079/// Coordinator-side merge sort: a deterministic k-way merge over streams
2080/// that are each already sorted on `keys` (ties break by stream index, so
2081/// the result is fully deterministic).
2082fn merge_sorted_streams(
2083    streams: &[RecordBatch],
2084    keys: &[SortKey],
2085    limit: Option<usize>,
2086) -> DistributedResult<Vec<RecordBatch>> {
2087    /// Min-heap entry (via `Reverse`): smallest key, then smallest stream.
2088    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
2154/// Truncates batches to `limit` rows, preserving stream order.
2155fn 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
2176/// Routes a producer's output across the sibling consumers of a
2177/// hash-repartition boundary: rows whose FNV-1a key hash modulo `width`
2178/// equals `index`.
2179fn 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
2220/// Maps one numeric score cell onto an order-preserving `u64` key (higher
2221/// sorts better). Null scores map to the minimum key.
2222fn 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
2259/// Reads the `__rowid` column of a top-k batch.
2260fn 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
2280/// A top-k input fully prepared for bounded emission: the concatenated
2281/// payload plus the globally sorted candidate list.
2282struct TopKInput {
2283    batch: RecordBatch,
2284    /// Candidates, best first, aligned with `positions`.
2285    candidates: Vec<TopKCandidate>,
2286    /// Row index in `batch` of each candidate.
2287    positions: Vec<usize>,
2288}
2289
2290impl TopKInput {
2291    /// Emits candidates `[offset, offset + limit)`: the candidates
2292    /// themselves, their payload rows, and the tightened unseen bound
2293    /// (`None` when nothing remains).
2294    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
2309/// Builds the sorted candidate list of a top-k input.
2310fn 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// ---------------------------------------------------------------------------
2345// Aggregation combine (partial + final)
2346// ---------------------------------------------------------------------------
2347
2348/// One numeric cell (the groundwork supports Int64 and Float64 aggregate
2349/// inputs).
2350#[derive(Clone, Copy, Debug)]
2351enum AggValue {
2352    I64(i64),
2353    F64(f64),
2354}
2355
2356/// Reads one numeric cell (`None` for nulls).
2357fn 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/// Per-group accumulator for one aggregate expression.
2383#[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    /// A fresh accumulator for `function` over a value column of type
2397    /// `float` (`true` = Float64, `false` = Int64).
2398    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    /// Folds one value cell (shared by partial folds and final combines).
2412    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
2451/// One folded group.
2452struct GroupEntry {
2453    /// Global row (in the concatenated input) whose key columns represent
2454    /// this group in the output.
2455    first_row: usize,
2456    accums: Vec<AggAccum>,
2457}
2458
2459/// Groups in first-seen order (deterministic for a fixed input order).
2460#[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
2482/// The deterministic group key of one row (display form of the key columns).
2483fn 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
2491/// Resolves group-by column indexes.
2492fn 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
2503/// The value-column type marker of one aggregate (`true` = Float64).
2504fn 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
2522/// Gathers group key columns at each group's first-seen row.
2523fn 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
2544/// Emits one integer accumulator column (partial or final).
2545fn 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
2571/// Emits one float accumulator column (partial or final).
2572fn 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
2589/// Per-tablet partial aggregation (the producer half of the two-phase
2590/// aggregate, spec section 12.10).
2591fn 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    // Emit: key columns at first-seen rows, then partial columns.
2644    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
2712/// Coordinator-side combine of partial aggregates into final results (spec
2713/// section 12.10).
2714fn 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    // Resolve the partial columns produced by `partial_aggregate_batches`.
2727    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    // SQL semantics: an empty input with no group-by still yields one row.
2809    if fold.order.is_empty() && group_by.is_empty() {
2810        fold.entry(String::new(), 0, &templates);
2811    }
2812    // Emit: key columns at first-seen rows, then one column per aggregate.
2813    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// ---------------------------------------------------------------------------
2851// Coordinator runtime (spec section 12.10)
2852// ---------------------------------------------------------------------------
2853
2854/// Per-query fragment resource ledger. Reservations are RAII: dropping a
2855/// [`ResourcePermit`] releases its share.
2856#[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/// A held resource reservation; released on drop.
2870#[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    /// A ledger admitting at most `max_fragments` concurrent fragments and
2886    /// `max_bytes` total estimated bytes.
2887    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    /// Reserves one fragment's estimated resources (spec section 12.10:
2896    /// "workers reserve resources").
2897    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    /// Currently reserved fragment count.
2924    pub fn reserved_fragments(&self) -> usize {
2925        self.state.lock().fragments
2926    }
2927
2928    /// Currently reserved estimated bytes.
2929    pub fn reserved_bytes(&self) -> u64 {
2930        self.state.lock().bytes
2931    }
2932}
2933
2934/// Worker lease ledger (spec section 12.10: "worker lease expiry cleans
2935/// abandoned fragments"). Workers are keyed by the tablet whose data they
2936/// serve this wave; the node-level binding lands with the transport wave.
2937#[derive(Default)]
2938pub struct LeaseLedger {
2939    leases: Mutex<HashMap<TabletId, Instant>>,
2940}
2941
2942impl LeaseLedger {
2943    /// Renews a worker's lease to `expiry`.
2944    pub fn renew(&self, worker: TabletId, expiry: Instant) {
2945        self.leases.lock().insert(worker, expiry);
2946    }
2947
2948    /// A worker's current lease expiry.
2949    pub fn expiry(&self, worker: &TabletId) -> Option<Instant> {
2950        self.leases.lock().get(worker).copied()
2951    }
2952
2953    /// Removes and returns every worker whose lease expired at or before
2954    /// `now`.
2955    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
2969/// One in-flight fragment of a running query.
2970struct InFlight {
2971    worker: Option<TabletId>,
2972    control: ExecutionControl,
2973    _permit: ResourcePermit,
2974}
2975
2976/// Per-query execution state tracked by the coordinator.
2977#[derive(Default)]
2978struct ExecutionState {
2979    in_flight: Mutex<HashMap<FragmentId, InFlight>>,
2980}
2981
2982/// One producer stream feeding a coordinator (root) fragment.
2983struct ProducerInput {
2984    fragment_id: FragmentId,
2985    tablet: Option<TabletId>,
2986    frames: Vec<BatchFrame>,
2987}
2988
2989/// The root fragment's working data: either per-producer streams (fresh from
2990/// the exchange edges) or already-combined batches (after a coordinator
2991/// operator ran).
2992enum RootData {
2993    Streams(Vec<ProducerInput>),
2994    Batches(Vec<RecordBatch>),
2995}
2996
2997impl RootData {
2998    /// All payload batches, in stream order.
2999    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
3010/// The query coordinator (spec section 12.10): registers the query with the
3011/// existing [`SqlQueryRegistry`] (so `registry.cancel(...)` reaches every
3012/// fragment through the [`ExecutionControl`] hierarchy), reserves resources
3013/// per fragment, fans out cancellation to every fragment, sweeps expired
3014/// worker leases to clean abandoned fragments, and merges producer streams
3015/// per the exchange descriptors with the real in-memory operators.
3016pub 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    /// A coordinator with default limits (1024 concurrent fragments, 16 GiB
3027    /// of estimated bytes, 30 second worker leases).
3028    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    /// A coordinator with explicit resource limits and lease TTL.
3039    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    /// The fragment resource ledger (test introspection).
3057    pub fn resources(&self) -> &Arc<ResourceLedger> {
3058        &self.resources
3059    }
3060
3061    /// Executes a distributed plan to completion, returning the root
3062    /// fragment's output batches.
3063    ///
3064    /// The plan's [`QueryId`] is registered with the query registry first;
3065    /// every fragment runs under a child [`ExecutionControl`] of that
3066    /// registration, so a registry-level cancel fans out to all fragments.
3067    /// Fragments execute layer by layer (producers before consumers),
3068    /// concurrently within a layer. The coordinator-local root fragment's
3069    /// merge operators ([`FragmentOperator::MergeSort`],
3070    /// [`FragmentOperator::FinalAggregate`],
3071    /// [`FragmentOperator::DistributedTopK`],
3072    /// [`FragmentOperator::DistributedLimit`]) run over the producer streams
3073    /// for real.
3074    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, &registered).await;
3086        match &result {
3087            Ok(_) => {
3088                let _ = registered.try_complete();
3089            }
3090            Err(_) => registered.fail(),
3091        }
3092        result
3093    }
3094
3095    /// Cancels a running distributed query: registry cancel (which reaches
3096    /// every fragment control through the parent-child hierarchy) plus an
3097    /// explicit transport cancel per in-flight fragment (spec section 12.10:
3098    /// "cancellation fans out to every fragment"). Returns true when the
3099    /// registry accepted (or already observed) the cancellation.
3100    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    /// Sweeps expired worker leases and cleans their abandoned in-flight
3117    /// fragments: cancels the fragment control (as
3118    /// [`CancellationReason::ServerShutdown`]), notifies the transport, and
3119    /// releases the resource reservation (spec section 12.10: "worker lease
3120    /// expiry cleans abandoned fragments"). Returns the number of fragments
3121    /// cleaned.
3122    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    /// Cancels every in-flight fragment control and notifies the transport
3283    /// for every fragment (the abort path's cancellation fan-out).
3284    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    /// Runs the coordinator-local root fragment over the producer outputs.
3297    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 &current {
3351                        RootData::Streams(_) => {
3352                            let streams = per_stream_batches(&current);
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, &current, *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    /// The coordinator-side deterministic top-k merge with adaptive refill
3376    /// (spec section 12.10): merges every producer's bounded local top-k
3377    /// under the exact tie-break, refilling tablets whose unseen-score bound
3378    /// could still contribute winners through the transport.
3379    fn coordinator_top_k(
3380        &self,
3381        plan: &DistributedPlan,
3382        data: &RootData,
3383        k: usize,
3384        score: &SortKey,
3385    ) -> DistributedResult<Vec<RecordBatch>> {
3386        // A pre-combined input (chained after another coordinator operator)
3387        // is a single synthetic stream that never refills.
3388        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                    // No bound reporting: treated as exhausted (documented
3438                    // producer contract).
3439                    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        // Locate every winner's payload row.
3481        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        // Strip the internal row-id column from the result.
3506        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
3526/// One stream's concatenated payload batches (for the k-way merge).
3527fn 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
3544/// A short worker label for fragment errors.
3545fn 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
3552/// Validates the structural invariants execution relies on.
3553fn 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
3605/// Groups non-root fragments into dependency layers (producers first).
3606fn 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
3650/// Builds one consumer's input streams from producer outputs, routing each
3651/// edge per its exchange kind.
3652fn 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
3671/// Routes one producer's output frames to one consumer per the edge kind:
3672/// hash-repartitioned rows are split across the producer's sibling edges.
3673fn 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
3705/// Bridges a types-level [`QueryId`] onto the query registry's id space.
3706fn 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// ---------------------------------------------------------------------------
3714// Tests
3715// ---------------------------------------------------------------------------
3716
3717#[cfg(test)]
3718mod tests {
3719    use super::*;
3720    use arrow::datatypes::Field;
3721
3722    /// Deterministic tablet id for tests.
3723    fn tablet(n: u8) -> TabletId {
3724        let mut bytes = [0u8; 16];
3725        bytes[15] = n;
3726        TabletId::from_bytes(bytes)
3727    }
3728
3729    /// Seeded RNG (SplitMix64) for reproducible property tests.
3730    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    /// Builds locator + metadata for a set of tables.
3787    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    // -----------------------------------------------------------------------
3926    // Plan shape tests
3927    // -----------------------------------------------------------------------
3928
3929    #[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        // The configured spill allowance is stamped on every fragment.
3987        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        // Real shipped path: Database::create owns the SpillManager.
4003        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        // Grouped: partial on producers, hash-repartition exchange, final at
4034        // the coordinator.
4035        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        // Ungrouped: merge exchange, single-row estimate.
4063        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        // 3 big + 2 small + gather.
4157        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        // 2 left scans + 3 right scans + 3 join fragments + gather.
4227        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        // Single descending key + limit: distributed top-k on both sides.
4280        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        // Plain sort: merge sort on both sides.
4306        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        // Multi-key sort with a limit is NOT a top-k (single descending key
4328        // only): merge sort with the limit pushed down.
4329        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        // Bare limit: pushed down and re-applied at the coordinator.
4357        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        // A limit over a scalar aggregate stays in the one coordinator
4375        // fragment (no extra hop).
4376        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        // Unknown table.
4406        let error = distribute(&desc(scan("nope")), &locator, &metadata).unwrap_err();
4407        assert!(
4408            matches!(error, DistributedError::UnknownTable(_)),
4409            "{error}"
4410        );
4411        // Empty layout.
4412        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        // Join without keys.
4420        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        // Sum without a column.
4428        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        // Sort without keys.
4439        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    // -----------------------------------------------------------------------
4449    // Distributed top-k: pure merge + adaptive refill
4450    // -----------------------------------------------------------------------
4451
4452    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        // Final score descending, tablet id ascending, RowId ascending.
4463        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        // Bound below the k-th winner: no refill possible.
4508        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        // Bound above the k-th winner: refill required.
4527        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        // Tie at the k-th winner's score with a better tablet id: the
4542        // conservative tie case refills.
4543        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        // Fewer than k candidates with unseen rows: refill.
4562        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        // k = 0 is trivially exact.
4578        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        // Tablet 1 holds 3 of the true top-3 but only emits 2 up front.
4585        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    // -----------------------------------------------------------------------
4698    // Execution skeleton: merge operators, cancellation, leases
4699    // -----------------------------------------------------------------------
4700
4701    /// Builds a coordinator + in-memory transport + registry over one
4702    /// executor.
4703    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(&registry),
4715        ));
4716        (coordinator, transport, registry)
4717    }
4718
4719    /// An executor that signals arrival and then waits for cancellation —
4720    /// the fixture for cancellation/lease tests.
4721    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    /// A scan-only plan over `table` with 3 tablets, suitable for
4740    /// cancellation fixtures.
4741    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        // Tablet 3 intentionally holds no rows.
4760        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        // Full merge sort.
4779        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        // Bounded merge sort.
4792        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        // Single-node oracle, group order-independent.
4863        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        // Scalar aggregate: one row over all tablets.
4898        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        // Heavy score ties across three tablets; payloads mirror row ids so
4921        // the exact tie order is observable after `__rowid` is stripped.
4922        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        // Single-node oracle under the exact tie-break.
4949        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        // Default emission (up to k rows): exact without refills.
4978        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        // Bounded emission (2 rows per round): the coordinator must refill
4995        // and still match the oracle exactly.
4996        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        // The existing registry cancel path — this is the wiring proof.
5062        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        // All worker leases expired an hour from now's perspective.
5105        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        // Byte budget enforcement.
5156        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        // Re-routing a partition keeps all of its rows in the same partition
5184        // (disjointness proof by idempotence).
5185        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}