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).
30//! * Remote execution — [`RemoteFragmentTransport`] and
31//!   [`RemoteFragmentEndpoint`] use a versioned, bounded Arrow IPC pull
32//!   protocol. One batch per pull provides backpressure; query-scoped
33//!   cancellation and adaptive top-k refill cross the same authenticated
34//!   node-internal RPC carrier.
35//!
36//! # Integration point with DataFusion
37//!
38//! [`DataFusionDistributedPlanner::lower`] converts a real
39//! `datafusion::logical_expr::LogicalPlan` (produced by
40//! [`MongrelSession`](crate::MongrelSession) after catalog/schema resolution)
41//! onto [`LogicalPlanLite`] and hands the tree to [`distribute`]. Supported
42//! operators: TableScan, Projection (column pushdown), Filter (pushdown onto
43//! scans as typed predicate text for worker evaluation), Aggregate, Sort,
44//! Limit, equi-Join, and Union. Every other DataFusion node is rejected with
45//! [`DistributedError::Unsupported`].
46
47use std::cmp::Ordering;
48use std::collections::{BTreeMap, BinaryHeap, HashMap};
49use std::io::Cursor;
50use std::sync::Arc;
51use std::time::{Duration, Instant};
52
53use arrow::array::{
54    Array, ArrayRef, Float64Array, Int64Array, RecordBatch, UInt32Array, UInt64Array,
55};
56use arrow::compute::{concat_batches, interleave, take};
57use arrow::datatypes::{DataType, Field, Schema, SchemaRef};
58use arrow::ipc::reader::StreamReader;
59use arrow::ipc::writer::StreamWriter;
60use arrow::row::{RowConverter, SortField};
61use arrow::util::display::array_value_to_string;
62use bincode::Options;
63use futures::stream::{self, BoxStream, FuturesUnordered, StreamExt};
64use mongreldb_core::{CancellationReason, ExecutionControl, RowId};
65use mongreldb_types::ids::{MetadataVersion, QueryId, TabletId};
66use parking_lot::Mutex;
67use serde::{Deserialize, Serialize};
68
69use crate::query_registry::{CancelOutcome, RegisteredSqlQuery, SqlQueryOptions, SqlQueryRegistry};
70
71/// Small-side byte threshold below which a join is planned as a broadcast
72/// join (spec section 12.10: "broadcast small side").
73pub const DEFAULT_BROADCAST_THRESHOLD_BYTES: u64 = 8 * 1024 * 1024;
74
75/// Default per-fragment maximum spill allowance (spec section 12.10: "every
76/// plan includes ... a maximum spill allowance"). Bound to the core
77/// [`mongreldb_core::SpillManager`] via [`FragmentControl::begin_spill`].
78pub const DEFAULT_MAX_SPILL_BYTES_PER_FRAGMENT: u64 = 256 * 1024 * 1024;
79/// Maximum server-issued authorization envelope forwarded to a worker.
80pub const MAX_FRAGMENT_AUTHORIZATION_CONTEXT_BYTES: usize = 64 * 1024;
81
82/// Rows per `RecordBatch` emitted by coordinator merge operators.
83const COORDINATOR_OUTPUT_BATCH_ROWS: usize = 8_192;
84
85/// Name of the unsigned row-id column every scored (top-k) stream carries.
86/// Tablet scans feeding a distributed top-k must project this column; it is
87/// stripped from the coordinator's final top-k output.
88pub const TOPK_ROWID_COLUMN: &str = "__rowid";
89
90/// Errors raised by distributed planning and coordination.
91#[non_exhaustive]
92#[derive(Debug, thiserror::Error)]
93pub enum DistributedError {
94    /// The locator or metadata has no entry for the table.
95    #[error("unknown table `{0}`")]
96    UnknownTable(String),
97    /// The table's layout has no tablets to scan.
98    #[error("table `{table}` has no tablets in its layout")]
99    EmptyLayout { table: String },
100    /// The input IR or a computed plan is not well-formed.
101    #[error("invalid plan: {0}")]
102    InvalidPlan(String),
103    /// A fragment's resource reservation was denied.
104    #[error("fragment {fragment_id} resource reservation denied: {reason}")]
105    Reservation { fragment_id: u32, reason: String },
106    /// A fragment failed on its worker.
107    #[error("fragment {fragment_id} failed on worker {worker}: {message}")]
108    FragmentExecution {
109        fragment_id: u32,
110        worker: String,
111        message: String,
112    },
113    /// The query was cancelled; carries the first observed reason.
114    #[error("distributed query cancelled: {0:?}")]
115    Cancelled(CancellationReason),
116    /// A groundwork boundary was crossed (documented later-wave work).
117    #[error("unsupported in this wave: {0}")]
118    Unsupported(String),
119    /// Arrow kernel failure inside a merge operator.
120    #[error("arrow error: {0}")]
121    Arrow(String),
122    /// A remote fragment transport call failed.
123    #[error("remote fragment transport error: {0}")]
124    RemoteTransport(String),
125    /// A remote fragment peer violated the versioned wire contract.
126    #[error("remote fragment protocol error: {0}")]
127    RemoteProtocol(String),
128}
129
130/// Result alias for distributed planning and coordination.
131pub type DistributedResult<T> = Result<T, DistributedError>;
132
133impl From<arrow::error::ArrowError> for DistributedError {
134    fn from(error: arrow::error::ArrowError) -> Self {
135        Self::Arrow(error.to_string())
136    }
137}
138
139/// FNV-1a 64-bit hash (same convention as the cluster routing code).
140fn fnv1a64(bytes: &[u8]) -> u64 {
141    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
142    for byte in bytes {
143        hash ^= u64::from(*byte);
144        hash = hash.wrapping_mul(0x0000_0100_0000_01b3);
145    }
146    hash
147}
148
149// ---------------------------------------------------------------------------
150// Cluster metadata traits (the dependency-free seam)
151// ---------------------------------------------------------------------------
152
153/// How one table's rows map onto tablets — the planner's dependency-free
154/// mirror of `mongreldb_cluster::tablet::Partitioning` (spec section 12.2).
155/// The cluster wave adapts the cluster type onto this enum one-to-one.
156#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
157pub enum PartitionSpec {
158    /// Hash of the declared columns into a fixed bucket space.
159    Hash { columns: Vec<String>, buckets: u32 },
160    /// Lexicographic ranges over the declared columns.
161    Range { columns: Vec<String> },
162    /// One bucket space per tenant.
163    Tenant {
164        column: String,
165        buckets_per_tenant: u32,
166    },
167    /// Time-bucketed ranges over the timestamp column.
168    TimeRange { column: String },
169    /// Single-tablet (unpartitioned) table.
170    Unpartitioned,
171}
172
173impl PartitionSpec {
174    /// The declared partition-key columns, in key order.
175    pub fn partition_columns(&self) -> Vec<&str> {
176        match self {
177            Self::Hash { columns, .. } | Self::Range { columns } => {
178                columns.iter().map(String::as_str).collect()
179            }
180            Self::Tenant { column, .. } | Self::TimeRange { column } => vec![column.as_str()],
181            Self::Unpartitioned => Vec::new(),
182        }
183    }
184
185    /// True when two tables partition identically, so equal partition keys
186    /// land on the same tablet and a join on the partition key can be planned
187    /// colocated (spec section 12.10: "colocated join"). Two unpartitioned
188    /// tables are trivially colocated (both live on their single tablet).
189    pub fn colocated_with(&self, other: &PartitionSpec) -> bool {
190        match (self, other) {
191            (Self::Unpartitioned, Self::Unpartitioned) => true,
192            (Self::Unpartitioned, _) | (_, Self::Unpartitioned) => false,
193            _ => self == other,
194        }
195    }
196}
197
198/// Planner statistics for one table.
199#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
200pub struct TableStats {
201    /// Estimated live row count.
202    pub row_count: u64,
203    /// Estimated total size in bytes.
204    pub total_bytes: u64,
205}
206
207/// Resolves which tablets serve one table. Defined here (not in
208/// `mongreldb-cluster`) so the query crate stays free of the cluster
209/// dependency; the cluster wave binds `TabletLayout`/routing metadata onto
210/// this trait.
211pub trait TabletLocator: Send + Sync {
212    /// The tablets serving `table`, in deterministic layout order.
213    fn tablets_for_table(&self, table: &str) -> DistributedResult<Vec<TabletId>>;
214    /// How `table` is partitioned across those tablets.
215    fn partitioning(&self, table: &str) -> DistributedResult<PartitionSpec>;
216}
217
218/// Control-plane metadata the planner pins against (spec section 12.10:
219/// "coordinator plans using metadata version").
220pub trait ClusterMetadata: Send + Sync {
221    /// The control-plane metadata version this plan is pinned to.
222    fn metadata_version(&self) -> MetadataVersion;
223    /// Planner statistics for one table.
224    fn table_stats(&self, table: &str) -> DistributedResult<TableStats>;
225}
226
227// ---------------------------------------------------------------------------
228// Input IR (LogicalPlanLite)
229// ---------------------------------------------------------------------------
230
231/// One join-key equality pair (`left.col = right.col`).
232#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
233pub struct JoinKey {
234    /// Column name on the left input.
235    pub left: String,
236    /// Column name on the right input.
237    pub right: String,
238}
239
240/// One sort key.
241#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
242pub struct SortKey {
243    /// Column name.
244    pub column: String,
245    /// True for `ORDER BY ... DESC`.
246    pub descending: bool,
247}
248
249/// One aggregate expression.
250#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
251pub struct AggregateExpr {
252    /// The aggregate function.
253    pub function: AggregateFunction,
254    /// The aggregated column; `None` only for `COUNT(*)`.
255    pub column: Option<String>,
256}
257
258/// Supported aggregate functions.
259#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
260pub enum AggregateFunction {
261    /// Row count (`column == None`) or non-null count.
262    Count,
263    /// Numeric sum.
264    Sum,
265    /// Numeric minimum.
266    Min,
267    /// Numeric maximum.
268    Max,
269    /// Numeric average.
270    Avg,
271}
272
273impl AggregateFunction {
274    fn name(self) -> &'static str {
275        match self {
276            Self::Count => "count",
277            Self::Sum => "sum",
278            Self::Min => "min",
279            Self::Max => "max",
280            Self::Avg => "avg",
281        }
282    }
283}
284
285/// The simplified logical IR [`distribute`] lowers onto tablets.
286///
287/// This is the seam documented at the module level: the DataFusion
288/// integration wave lowers real DataFusion plans onto this enum; this wave
289/// plans from it directly.
290#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
291pub enum LogicalPlanLite {
292    /// Base-table scan with an optional opaque predicate and projection.
293    Scan {
294        /// Table name.
295        table: String,
296        /// Opaque predicate text (pushed down verbatim; not interpreted in
297        /// this wave).
298        predicate: Option<String>,
299        /// Projected column names; empty = all columns.
300        projection: Vec<String>,
301    },
302    /// Grouped aggregation.
303    Aggregate {
304        /// Input subtree.
305        input: Box<LogicalPlanLite>,
306        /// Group-by columns.
307        group_by: Vec<String>,
308        /// Aggregate expressions.
309        aggregates: Vec<AggregateExpr>,
310    },
311    /// Equi-join.
312    Join {
313        /// Left input subtree.
314        left: Box<LogicalPlanLite>,
315        /// Right input subtree.
316        right: Box<LogicalPlanLite>,
317        /// Equality key pairs.
318        on: Vec<JoinKey>,
319    },
320    /// Sort with an optional limit (`ORDER BY ... [LIMIT k]`).
321    Sort {
322        /// Input subtree.
323        input: Box<LogicalPlanLite>,
324        /// Sort keys, most significant first.
325        keys: Vec<SortKey>,
326        /// Optional limit.
327        limit: Option<usize>,
328    },
329    /// Limit without an ordering.
330    Limit {
331        /// Input subtree.
332        input: Box<LogicalPlanLite>,
333        /// Row limit.
334        limit: usize,
335    },
336    /// Concatenate inputs with identical schemas (`UNION ALL` shape).
337    Union {
338        /// Input subtrees (at least two).
339        inputs: Vec<LogicalPlanLite>,
340    },
341}
342
343// ---------------------------------------------------------------------------
344// Plan model (spec section 12.10)
345// ---------------------------------------------------------------------------
346
347/// Plan-local fragment identifier (index into [`DistributedPlan::fragments`]).
348pub type FragmentId = u32;
349/// Plan-local exchange identifier (index into [`DistributedPlan::exchanges`]).
350pub type ExchangeId = u32;
351
352/// Where one fragment executes.
353#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
354pub enum FragmentAssignment {
355    /// On the worker serving this tablet's data.
356    Tablet(TabletId),
357    /// On the query coordinator (gateway) itself.
358    Coordinator,
359}
360
361/// Which join input is the broadcast (build) side.
362#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
363pub enum BuildSide {
364    /// The left input is broadcast.
365    Left,
366    /// The right input is broadcast.
367    Right,
368}
369
370/// One physical operator inside a fragment (spec section 12.10).
371#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
372pub enum FragmentOperator {
373    /// Scan one tablet's slice of a table.
374    TabletScan {
375        /// Table name.
376        table: String,
377        /// Opaque pushed-down predicate text.
378        predicate: Option<String>,
379        /// Projected columns; empty = all.
380        projection: Vec<String>,
381    },
382    /// Receive one exchange edge from a producer fragment.
383    RemoteExchangeSource {
384        /// The exchange edge this source consumes.
385        exchange: ExchangeId,
386    },
387    /// Emit this fragment's output onto one exchange edge.
388    RemoteExchangeSink {
389        /// The exchange edge this sink feeds.
390        exchange: ExchangeId,
391    },
392    /// Per-tablet partial aggregation (pre-shuffle combine).
393    PartialAggregate {
394        /// Group-by columns.
395        group_by: Vec<String>,
396        /// Aggregate expressions.
397        aggregates: Vec<AggregateExpr>,
398    },
399    /// Coordinator-side combine of partial aggregates.
400    FinalAggregate {
401        /// Group-by columns.
402        group_by: Vec<String>,
403        /// Aggregate expressions.
404        aggregates: Vec<AggregateExpr>,
405    },
406    /// Hash join over colocated inputs (no exchange needed).
407    DistributedHashJoin {
408        /// Equality key pairs.
409        on: Vec<JoinKey>,
410    },
411    /// Join where the small build side is broadcast to every big-side
412    /// fragment.
413    BroadcastJoin {
414        /// Equality key pairs.
415        on: Vec<JoinKey>,
416        /// Which input is broadcast.
417        build_side: BuildSide,
418    },
419    /// Join after both sides are hash-repartitioned on the join keys.
420    RepartitionJoin {
421        /// Equality key pairs.
422        on: Vec<JoinKey>,
423    },
424    /// Producer side: local bounded sort. Coordinator side: deterministic
425    /// k-way merge of sorted streams.
426    MergeSort {
427        /// Sort keys.
428        keys: Vec<SortKey>,
429        /// Optional row limit applied after the sort/merge.
430        limit: Option<usize>,
431    },
432    /// Producer side: bounded local top-k plus tie information. Coordinator
433    /// side: deterministic merge (score desc, tablet asc, [`RowId`] asc) with
434    /// adaptive refill (spec section 12.10).
435    DistributedTopK {
436        /// Number of winners.
437        k: usize,
438        /// The (single, descending) score key.
439        score: SortKey,
440    },
441    /// Row limit (per-fragment locally; global at the coordinator).
442    DistributedLimit {
443        /// Row limit.
444        limit: usize,
445    },
446}
447
448/// How rows move across one exchange edge (spec section 12.10).
449#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
450pub enum ExchangeKind {
451    /// Rows are hash-routed on `keys` across the sibling consumers of the
452    /// producing fragment.
453    HashRepartition {
454        /// Hash key columns (in the producer's output schema).
455        keys: Vec<String>,
456    },
457    /// The producer's full output is replicated to every consumer.
458    Broadcast,
459    /// The producer's output flows to its single consumer.
460    Merge,
461}
462
463/// One exchange edge between two fragments (spec section 12.10).
464#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
465pub struct ExchangeDescriptor {
466    /// Plan-local exchange identifier.
467    pub exchange_id: ExchangeId,
468    /// Producing fragment.
469    pub producer: FragmentId,
470    /// Consuming fragment.
471    pub consumer: FragmentId,
472    /// Row movement kind.
473    pub kind: ExchangeKind,
474    /// FNV-1a fingerprint of the producer's output column names (types join
475    /// the fingerprint with the DataFusion lowering wave).
476    pub schema_fingerprint: u64,
477}
478
479/// One executable unit of a [`DistributedPlan`].
480#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
481pub struct PlanFragment {
482    /// Plan-local fragment identifier.
483    pub fragment_id: FragmentId,
484    /// Where the fragment executes.
485    pub assignment: FragmentAssignment,
486    /// Operators, in execution order (scans/sources first, sink last).
487    pub operators: Vec<FragmentOperator>,
488    /// Estimated output rows (spec section 12.10).
489    pub estimated_rows: u64,
490    /// Estimated output bytes (spec section 12.10).
491    pub estimated_bytes: u64,
492    /// Maximum spill allowance in bytes (spec section 12.10).
493    pub max_spill_bytes: u64,
494}
495
496/// A distributed query plan pinned to one control-plane metadata version
497/// (spec section 12.10).
498#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
499pub struct DistributedPlan {
500    /// The query execution this plan belongs to.
501    pub query_id: QueryId,
502    /// The control-plane metadata version the plan was built against.
503    pub metadata_version: MetadataVersion,
504    /// All fragments; `fragment_id` equals the vector index.
505    pub fragments: Vec<PlanFragment>,
506    /// All exchange edges; `exchange_id` equals the vector index.
507    pub exchanges: Vec<ExchangeDescriptor>,
508}
509
510impl DistributedPlan {
511    /// The fragment with no outgoing exchange (the coordinator root).
512    pub fn root_fragment_id(&self) -> Option<FragmentId> {
513        self.fragments
514            .iter()
515            .map(|fragment| fragment.fragment_id)
516            .find(|id| !self.exchanges.iter().any(|edge| edge.producer == *id))
517    }
518
519    /// Looks up a fragment by id.
520    pub fn fragment(&self, fragment_id: FragmentId) -> Option<&PlanFragment> {
521        self.fragments.get(fragment_id as usize)
522    }
523
524    /// Exchange edges out of one fragment.
525    pub fn exchanges_from(
526        &self,
527        producer: FragmentId,
528    ) -> impl Iterator<Item = &ExchangeDescriptor> {
529        self.exchanges
530            .iter()
531            .filter(move |edge| edge.producer == producer)
532    }
533
534    /// Exchange edges into one fragment, ordered by producer id.
535    pub fn exchanges_into(&self, consumer: FragmentId) -> Vec<&ExchangeDescriptor> {
536        let mut edges: Vec<&ExchangeDescriptor> = self
537            .exchanges
538            .iter()
539            .filter(|edge| edge.consumer == consumer)
540            .collect();
541        edges.sort_by_key(|edge| edge.producer);
542        edges
543    }
544}
545
546// ---------------------------------------------------------------------------
547// Planner
548// ---------------------------------------------------------------------------
549
550/// Everything [`distribute`] needs to build a [`DistributedPlan`].
551#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
552pub struct PlanDescription {
553    /// The query execution id the plan is pinned to.
554    pub query_id: QueryId,
555    /// The logical input tree.
556    pub root: LogicalPlanLite,
557    /// Planner tuning.
558    pub options: PlannerOptions,
559}
560
561/// Planner tuning knobs.
562#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
563pub struct PlannerOptions {
564    /// Small-side byte threshold below which a join is planned as broadcast.
565    pub broadcast_threshold_bytes: u64,
566    /// Per-fragment maximum spill allowance stamped on every fragment.
567    pub max_spill_bytes_per_fragment: u64,
568}
569
570impl Default for PlannerOptions {
571    fn default() -> Self {
572        Self {
573            broadcast_threshold_bytes: DEFAULT_BROADCAST_THRESHOLD_BYTES,
574            max_spill_bytes_per_fragment: DEFAULT_MAX_SPILL_BYTES_PER_FRAGMENT,
575        }
576    }
577}
578
579/// Lowers a [`LogicalPlanLite`] tree onto tablets (spec section 12.10).
580///
581/// Join strategy, in decision order:
582///
583/// 1. **Colocated** — both inputs are scans whose partition specs are
584///    colocated ([`PartitionSpec::colocated_with`]) over the identical tablet
585///    list; the hash join runs fused inside each shared tablet's fragment.
586/// 2. **Broadcast** — the smaller input's estimated bytes fit under
587///    [`PlannerOptions::broadcast_threshold_bytes`]; the small side is
588///    replicated to every big-side fragment.
589/// 3. **Repartition** — otherwise; both sides are hash-repartitioned on their
590///    join keys into fresh join fragments.
591///
592/// `Sort` with a single descending key and a limit plans as
593/// [`FragmentOperator::DistributedTopK`]; any other sort plans as
594/// [`FragmentOperator::MergeSort`]. Every fragment carries estimated
595/// rows/bytes and the configured maximum spill allowance.
596pub fn distribute(
597    description: &PlanDescription,
598    locator: &dyn TabletLocator,
599    metadata: &dyn ClusterMetadata,
600) -> DistributedResult<DistributedPlan> {
601    validate_lite(&description.root)?;
602    let mut planner = Planner {
603        locator,
604        metadata,
605        options: description.options,
606        fragments: Vec::new(),
607        exchanges: Vec::new(),
608    };
609    let stage = planner.plan_node(&description.root)?;
610    let root = match stage.producers.as_slice() {
611        [only]
612            if planner.fragments[*only as usize].assignment == FragmentAssignment::Coordinator =>
613        {
614            *only
615        }
616        _ => {
617            let gather = planner.push_fragment(
618                FragmentAssignment::Coordinator,
619                Vec::new(),
620                stage.estimated_rows,
621                stage.estimated_bytes,
622            );
623            planner.wire(&stage.producers, &[gather], ExchangeKind::Merge);
624            gather
625        }
626    };
627    debug_assert_eq!(
628        planner
629            .fragments
630            .iter()
631            .filter(|fragment| {
632                !planner
633                    .exchanges
634                    .iter()
635                    .any(|edge| edge.producer == fragment.fragment_id)
636            })
637            .count(),
638        1,
639        "exactly one root fragment"
640    );
641    let _ = root;
642    Ok(DistributedPlan {
643        query_id: description.query_id,
644        metadata_version: metadata.metadata_version(),
645        fragments: planner.fragments,
646        exchanges: planner.exchanges,
647    })
648}
649
650/// Validates the input IR before planning.
651fn validate_lite(node: &LogicalPlanLite) -> DistributedResult<()> {
652    match node {
653        LogicalPlanLite::Scan { table, .. } => {
654            if table.is_empty() {
655                return Err(DistributedError::InvalidPlan(
656                    "scan needs a table name".to_owned(),
657                ));
658            }
659        }
660        LogicalPlanLite::Aggregate {
661            input, aggregates, ..
662        } => {
663            for aggregate in aggregates {
664                if aggregate.function != AggregateFunction::Count && aggregate.column.is_none() {
665                    return Err(DistributedError::InvalidPlan(format!(
666                        "{} needs a column",
667                        aggregate.function.name()
668                    )));
669                }
670            }
671            validate_lite(input)?;
672        }
673        LogicalPlanLite::Join { left, right, on } => {
674            if on.is_empty() {
675                return Err(DistributedError::InvalidPlan(
676                    "join needs at least one key".to_owned(),
677                ));
678            }
679            validate_lite(left)?;
680            validate_lite(right)?;
681        }
682        LogicalPlanLite::Sort { input, keys, .. } => {
683            if keys.is_empty() {
684                return Err(DistributedError::InvalidPlan(
685                    "sort needs at least one key".to_owned(),
686                ));
687            }
688            validate_lite(input)?;
689        }
690        LogicalPlanLite::Limit { input, .. } => validate_lite(input)?,
691        LogicalPlanLite::Union { inputs } => {
692            if inputs.len() < 2 {
693                return Err(DistributedError::InvalidPlan(
694                    "union needs at least two inputs".to_owned(),
695                ));
696            }
697            for input in inputs {
698                validate_lite(input)?;
699            }
700        }
701    }
702    Ok(())
703}
704
705/// The planner's view of one planned subtree's output stage.
706struct Stage {
707    /// Fragments producing this stage's output (sinks not yet attached).
708    producers: Vec<FragmentId>,
709    estimated_rows: u64,
710    estimated_bytes: u64,
711    /// Output partitioning, when known (scan stages and colocated joins).
712    partitioning: Option<PartitionSpec>,
713    /// Tablets the stage's fragments run on (empty for coordinator stages).
714    tablets: Vec<TabletId>,
715}
716
717struct Planner<'a> {
718    locator: &'a dyn TabletLocator,
719    metadata: &'a dyn ClusterMetadata,
720    options: PlannerOptions,
721    fragments: Vec<PlanFragment>,
722    exchanges: Vec<ExchangeDescriptor>,
723}
724
725impl Planner<'_> {
726    fn push_fragment(
727        &mut self,
728        assignment: FragmentAssignment,
729        operators: Vec<FragmentOperator>,
730        estimated_rows: u64,
731        estimated_bytes: u64,
732    ) -> FragmentId {
733        let fragment_id = self.fragments.len() as FragmentId;
734        self.fragments.push(PlanFragment {
735            fragment_id,
736            assignment,
737            operators,
738            estimated_rows,
739            estimated_bytes,
740            max_spill_bytes: self.options.max_spill_bytes_per_fragment,
741        });
742        fragment_id
743    }
744
745    /// Connects every producer to every consumer with fresh exchange edges,
746    /// appending the sink operators to producers and source operators to
747    /// consumers (consumers must not have transform operators yet).
748    fn wire(&mut self, producers: &[FragmentId], consumers: &[FragmentId], kind: ExchangeKind) {
749        for &producer in producers {
750            let schema_fingerprint = self.schema_fingerprint(producer);
751            for &consumer in consumers {
752                let exchange_id = self.exchanges.len() as ExchangeId;
753                self.exchanges.push(ExchangeDescriptor {
754                    exchange_id,
755                    producer,
756                    consumer,
757                    kind: kind.clone(),
758                    schema_fingerprint,
759                });
760                self.fragments[producer as usize].operators.push(
761                    FragmentOperator::RemoteExchangeSink {
762                        exchange: exchange_id,
763                    },
764                );
765                self.fragments[consumer as usize].operators.push(
766                    FragmentOperator::RemoteExchangeSource {
767                        exchange: exchange_id,
768                    },
769                );
770            }
771        }
772    }
773
774    /// FNV-1a over the producer fragment's output column names.
775    fn schema_fingerprint(&self, fragment_id: FragmentId) -> u64 {
776        let columns = fragment_output_columns(&self.fragments[fragment_id as usize]);
777        let mut bytes = Vec::new();
778        for column in &columns {
779            bytes.extend_from_slice(&(column.len() as u32).to_le_bytes());
780            bytes.extend_from_slice(column.as_bytes());
781        }
782        fnv1a64(&bytes)
783    }
784
785    fn metadata_stats(&self, table: &str) -> DistributedResult<TableStats> {
786        self.metadata.table_stats(table)
787    }
788
789    fn plan_node(&mut self, node: &LogicalPlanLite) -> DistributedResult<Stage> {
790        match node {
791            LogicalPlanLite::Scan {
792                table,
793                predicate,
794                projection,
795            } => self.plan_scan(table, predicate, projection),
796            LogicalPlanLite::Aggregate {
797                input,
798                group_by,
799                aggregates,
800            } => self.plan_aggregate(input, group_by, aggregates),
801            LogicalPlanLite::Join { left, right, on } => self.plan_join(left, right, on),
802            LogicalPlanLite::Sort { input, keys, limit } => self.plan_sort(input, keys, *limit),
803            LogicalPlanLite::Limit { input, limit } => self.plan_limit(input, *limit),
804            LogicalPlanLite::Union { inputs } => self.plan_union(inputs),
805        }
806    }
807
808    fn plan_scan(
809        &mut self,
810        table: &str,
811        predicate: &Option<String>,
812        projection: &[String],
813    ) -> DistributedResult<Stage> {
814        let tablets = self.locator.tablets_for_table(table)?;
815        if tablets.is_empty() {
816            return Err(DistributedError::EmptyLayout {
817                table: table.to_owned(),
818            });
819        }
820        let spec = self.locator.partitioning(table)?;
821        let stats = self.metadata_stats(table)?;
822        let count = tablets.len() as u64;
823        let per_rows = stats.row_count.div_ceil(count);
824        let per_bytes = stats.total_bytes.div_ceil(count);
825        let mut producers = Vec::with_capacity(tablets.len());
826        for tablet in &tablets {
827            producers.push(self.push_fragment(
828                FragmentAssignment::Tablet(*tablet),
829                vec![FragmentOperator::TabletScan {
830                    table: table.to_owned(),
831                    predicate: predicate.clone(),
832                    projection: projection.to_vec(),
833                }],
834                per_rows,
835                per_bytes,
836            ));
837        }
838        Ok(Stage {
839            producers,
840            estimated_rows: stats.row_count,
841            estimated_bytes: stats.total_bytes,
842            partitioning: Some(spec),
843            tablets,
844        })
845    }
846
847    fn plan_aggregate(
848        &mut self,
849        input: &LogicalPlanLite,
850        group_by: &[String],
851        aggregates: &[AggregateExpr],
852    ) -> DistributedResult<Stage> {
853        let child = self.plan_node(input)?;
854        let estimated_rows = if group_by.is_empty() {
855            1
856        } else {
857            (child.estimated_rows / 2).max(1)
858        };
859        let per_row = child
860            .estimated_bytes
861            .checked_div(child.estimated_rows.max(1))
862            .unwrap_or(0)
863            .max(1);
864        let estimated_bytes = estimated_rows.saturating_mul(per_row);
865        if let [only] = child.producers.as_slice() {
866            if self.fragments[*only as usize].assignment == FragmentAssignment::Coordinator {
867                // Single coordinator producer: combine in place, no shuffle.
868                self.fragments[*only as usize].operators.extend([
869                    FragmentOperator::PartialAggregate {
870                        group_by: group_by.to_vec(),
871                        aggregates: aggregates.to_vec(),
872                    },
873                    FragmentOperator::FinalAggregate {
874                        group_by: group_by.to_vec(),
875                        aggregates: aggregates.to_vec(),
876                    },
877                ]);
878                return Ok(Stage {
879                    estimated_rows,
880                    estimated_bytes,
881                    ..child
882                });
883            }
884        }
885        for &producer in &child.producers {
886            self.fragments[producer as usize]
887                .operators
888                .push(FragmentOperator::PartialAggregate {
889                    group_by: group_by.to_vec(),
890                    aggregates: aggregates.to_vec(),
891                });
892        }
893        let consumer = self.push_fragment(
894            FragmentAssignment::Coordinator,
895            Vec::new(),
896            estimated_rows,
897            estimated_bytes,
898        );
899        let kind = if group_by.is_empty() {
900            ExchangeKind::Merge
901        } else {
902            ExchangeKind::HashRepartition {
903                keys: group_by.to_vec(),
904            }
905        };
906        self.wire(&child.producers, &[consumer], kind);
907        self.fragments[consumer as usize]
908            .operators
909            .push(FragmentOperator::FinalAggregate {
910                group_by: group_by.to_vec(),
911                aggregates: aggregates.to_vec(),
912            });
913        Ok(Stage {
914            producers: vec![consumer],
915            estimated_rows,
916            estimated_bytes,
917            partitioning: None,
918            tablets: Vec::new(),
919        })
920    }
921
922    fn plan_join(
923        &mut self,
924        left: &LogicalPlanLite,
925        right: &LogicalPlanLite,
926        on: &[JoinKey],
927    ) -> DistributedResult<Stage> {
928        // Colocation can be decided from metadata alone (no fragments built).
929        if let (
930            LogicalPlanLite::Scan {
931                table: left_table, ..
932            },
933            LogicalPlanLite::Scan {
934                table: right_table, ..
935            },
936        ) = (left, right)
937        {
938            let left_tablets = self.locator.tablets_for_table(left_table)?;
939            if left_tablets.is_empty() {
940                return Err(DistributedError::EmptyLayout {
941                    table: left_table.clone(),
942                });
943            }
944            let right_tablets = self.locator.tablets_for_table(right_table)?;
945            if right_tablets.is_empty() {
946                return Err(DistributedError::EmptyLayout {
947                    table: right_table.clone(),
948                });
949            }
950            let left_spec = self.locator.partitioning(left_table)?;
951            let right_spec = self.locator.partitioning(right_table)?;
952            if left_spec.colocated_with(&right_spec) && left_tablets == right_tablets {
953                return self.plan_colocated_join(left, right, on, &left_tablets, &left_spec);
954            }
955        }
956        let left_stage = self.plan_node(left)?;
957        let right_stage = self.plan_node(right)?;
958        if left_stage.estimated_bytes.min(right_stage.estimated_bytes)
959            <= self.options.broadcast_threshold_bytes
960        {
961            self.plan_broadcast_join(left_stage, right_stage, on)
962        } else {
963            self.plan_repartition_join(left_stage, right_stage, on)
964        }
965    }
966
967    /// Fuses two colocated scans and the hash join into one fragment per
968    /// shared tablet — no exchange at all (spec section 12.10).
969    fn plan_colocated_join(
970        &mut self,
971        left: &LogicalPlanLite,
972        right: &LogicalPlanLite,
973        on: &[JoinKey],
974        tablets: &[TabletId],
975        spec: &PartitionSpec,
976    ) -> DistributedResult<Stage> {
977        let (
978            LogicalPlanLite::Scan {
979                table: left_table,
980                predicate: left_predicate,
981                projection: left_projection,
982            },
983            LogicalPlanLite::Scan {
984                table: right_table,
985                predicate: right_predicate,
986                projection: right_projection,
987            },
988        ) = (left, right)
989        else {
990            return Err(DistributedError::InvalidPlan(
991                "colocated join needs scan inputs".to_owned(),
992            ));
993        };
994        let left_stats = self.metadata_stats(left_table)?;
995        let right_stats = self.metadata_stats(right_table)?;
996        let estimated_rows = left_stats.row_count.max(right_stats.row_count);
997        let estimated_bytes = left_stats.total_bytes.max(right_stats.total_bytes);
998        let count = tablets.len() as u64;
999        let mut producers = Vec::with_capacity(tablets.len());
1000        for tablet in tablets {
1001            producers.push(self.push_fragment(
1002                FragmentAssignment::Tablet(*tablet),
1003                vec![
1004                    FragmentOperator::TabletScan {
1005                        table: left_table.clone(),
1006                        predicate: left_predicate.clone(),
1007                        projection: left_projection.clone(),
1008                    },
1009                    FragmentOperator::TabletScan {
1010                        table: right_table.clone(),
1011                        predicate: right_predicate.clone(),
1012                        projection: right_projection.clone(),
1013                    },
1014                    FragmentOperator::DistributedHashJoin { on: on.to_vec() },
1015                ],
1016                estimated_rows.div_ceil(count),
1017                estimated_bytes.div_ceil(count),
1018            ));
1019        }
1020        Ok(Stage {
1021            producers,
1022            estimated_rows,
1023            estimated_bytes,
1024            partitioning: Some(spec.clone()),
1025            tablets: tablets.to_vec(),
1026        })
1027    }
1028
1029    /// Replicates the small side to every big-side fragment (spec section
1030    /// 12.10: "broadcast small side").
1031    fn plan_broadcast_join(
1032        &mut self,
1033        left_stage: Stage,
1034        right_stage: Stage,
1035        on: &[JoinKey],
1036    ) -> DistributedResult<Stage> {
1037        let (big, small, build_side) = if right_stage.estimated_bytes <= left_stage.estimated_bytes
1038        {
1039            (left_stage, right_stage, BuildSide::Right)
1040        } else {
1041            (right_stage, left_stage, BuildSide::Left)
1042        };
1043        self.wire(&small.producers, &big.producers, ExchangeKind::Broadcast);
1044        for &producer in &big.producers {
1045            self.fragments[producer as usize]
1046                .operators
1047                .push(FragmentOperator::BroadcastJoin {
1048                    on: on.to_vec(),
1049                    build_side,
1050                });
1051        }
1052        let estimated_rows = big.estimated_rows;
1053        let estimated_bytes = big.estimated_bytes.saturating_add(small.estimated_bytes);
1054        Ok(Stage {
1055            producers: big.producers,
1056            estimated_rows,
1057            estimated_bytes,
1058            partitioning: big.partitioning,
1059            tablets: big.tablets,
1060        })
1061    }
1062
1063    /// Hash-repartitions both inputs on their join keys into fresh join
1064    /// fragments (spec section 12.10: "repartition both sides").
1065    fn plan_repartition_join(
1066        &mut self,
1067        left_stage: Stage,
1068        right_stage: Stage,
1069        on: &[JoinKey],
1070    ) -> DistributedResult<Stage> {
1071        let join_tablets = if left_stage.tablets.len() >= right_stage.tablets.len() {
1072            left_stage.tablets.clone()
1073        } else {
1074            right_stage.tablets.clone()
1075        };
1076        if join_tablets.is_empty() {
1077            return Err(DistributedError::InvalidPlan(
1078                "repartition join needs at least one tablet-backed input".to_owned(),
1079            ));
1080        }
1081        let width = left_stage.producers.len().max(right_stage.producers.len());
1082        let estimated_rows = left_stage.estimated_rows.max(right_stage.estimated_rows);
1083        let estimated_bytes = left_stage
1084            .estimated_bytes
1085            .saturating_add(right_stage.estimated_bytes);
1086        let mut consumers = Vec::with_capacity(width);
1087        for index in 0..width {
1088            consumers.push(self.push_fragment(
1089                FragmentAssignment::Tablet(join_tablets[index % join_tablets.len()]),
1090                Vec::new(),
1091                estimated_rows.div_ceil(width as u64),
1092                estimated_bytes.div_ceil(width as u64),
1093            ));
1094        }
1095        let left_keys: Vec<String> = on.iter().map(|key| key.left.clone()).collect();
1096        let right_keys: Vec<String> = on.iter().map(|key| key.right.clone()).collect();
1097        self.wire(
1098            &left_stage.producers,
1099            &consumers,
1100            ExchangeKind::HashRepartition { keys: left_keys },
1101        );
1102        self.wire(
1103            &right_stage.producers,
1104            &consumers,
1105            ExchangeKind::HashRepartition { keys: right_keys },
1106        );
1107        for &consumer in &consumers {
1108            self.fragments[consumer as usize]
1109                .operators
1110                .push(FragmentOperator::RepartitionJoin { on: on.to_vec() });
1111        }
1112        Ok(Stage {
1113            producers: consumers,
1114            estimated_rows,
1115            estimated_bytes,
1116            partitioning: None,
1117            tablets: join_tablets,
1118        })
1119    }
1120
1121    fn plan_sort(
1122        &mut self,
1123        input: &LogicalPlanLite,
1124        keys: &[SortKey],
1125        limit: Option<usize>,
1126    ) -> DistributedResult<Stage> {
1127        let child = self.plan_node(input)?;
1128        // The distributed top-k shape (spec section 12.10): one descending
1129        // score key plus a limit. Anything else is a merge sort.
1130        let top_k = match (limit, keys) {
1131            (Some(k), [score]) if score.descending => Some((k, score.clone())),
1132            _ => None,
1133        };
1134        let estimated_rows = limit.map_or(child.estimated_rows, |limit| {
1135            child.estimated_rows.min(limit as u64)
1136        });
1137        let estimated_bytes =
1138            scaled_bytes(child.estimated_bytes, child.estimated_rows, estimated_rows);
1139        let local_op = match &top_k {
1140            Some((k, score)) => FragmentOperator::DistributedTopK {
1141                k: *k,
1142                score: score.clone(),
1143            },
1144            None => FragmentOperator::MergeSort {
1145                keys: keys.to_vec(),
1146                limit,
1147            },
1148        };
1149        if let [only] = child.producers.as_slice() {
1150            if self.fragments[*only as usize].assignment == FragmentAssignment::Coordinator {
1151                self.fragments[*only as usize].operators.push(local_op);
1152                return Ok(Stage {
1153                    estimated_rows,
1154                    estimated_bytes,
1155                    ..child
1156                });
1157            }
1158        }
1159        for &producer in &child.producers {
1160            self.fragments[producer as usize]
1161                .operators
1162                .push(local_op.clone());
1163        }
1164        let consumer = self.push_fragment(
1165            FragmentAssignment::Coordinator,
1166            Vec::new(),
1167            estimated_rows,
1168            estimated_bytes,
1169        );
1170        self.wire(&child.producers, &[consumer], ExchangeKind::Merge);
1171        let root_op = match &top_k {
1172            Some((k, score)) => FragmentOperator::DistributedTopK {
1173                k: *k,
1174                score: score.clone(),
1175            },
1176            None => FragmentOperator::MergeSort {
1177                keys: keys.to_vec(),
1178                limit,
1179            },
1180        };
1181        self.fragments[consumer as usize].operators.push(root_op);
1182        Ok(Stage {
1183            producers: vec![consumer],
1184            estimated_rows,
1185            estimated_bytes,
1186            partitioning: None,
1187            tablets: Vec::new(),
1188        })
1189    }
1190
1191    fn plan_limit(&mut self, input: &LogicalPlanLite, limit: usize) -> DistributedResult<Stage> {
1192        let child = self.plan_node(input)?;
1193        let estimated_rows = child.estimated_rows.min(limit as u64);
1194        let estimated_bytes =
1195            scaled_bytes(child.estimated_bytes, child.estimated_rows, estimated_rows);
1196        if let [only] = child.producers.as_slice() {
1197            if self.fragments[*only as usize].assignment == FragmentAssignment::Coordinator {
1198                self.fragments[*only as usize]
1199                    .operators
1200                    .push(FragmentOperator::DistributedLimit { limit });
1201                return Ok(Stage {
1202                    estimated_rows,
1203                    estimated_bytes,
1204                    ..child
1205                });
1206            }
1207        }
1208        for &producer in &child.producers {
1209            self.fragments[producer as usize]
1210                .operators
1211                .push(FragmentOperator::DistributedLimit { limit });
1212        }
1213        let consumer = self.push_fragment(
1214            FragmentAssignment::Coordinator,
1215            Vec::new(),
1216            estimated_rows,
1217            estimated_bytes,
1218        );
1219        self.wire(&child.producers, &[consumer], ExchangeKind::Merge);
1220        self.fragments[consumer as usize]
1221            .operators
1222            .push(FragmentOperator::DistributedLimit { limit });
1223        Ok(Stage {
1224            producers: vec![consumer],
1225            estimated_rows,
1226            estimated_bytes,
1227            partitioning: None,
1228            tablets: Vec::new(),
1229        })
1230    }
1231
1232    fn plan_union(&mut self, inputs: &[LogicalPlanLite]) -> DistributedResult<Stage> {
1233        let mut producers = Vec::new();
1234        let mut estimated_rows = 0u64;
1235        let mut estimated_bytes = 0u64;
1236        for input in inputs {
1237            let stage = self.plan_node(input)?;
1238            producers.extend(stage.producers);
1239            estimated_rows = estimated_rows.saturating_add(stage.estimated_rows);
1240            estimated_bytes = estimated_bytes.saturating_add(stage.estimated_bytes);
1241        }
1242        if producers.is_empty() {
1243            return Err(DistributedError::InvalidPlan(
1244                "union produced no fragments".to_owned(),
1245            ));
1246        }
1247        // All inputs already coordinator-local and single-producer: keep them
1248        // as-is (no extra gather hop).
1249        if producers.len() == 1 {
1250            return Ok(Stage {
1251                producers,
1252                estimated_rows,
1253                estimated_bytes,
1254                partitioning: None,
1255                tablets: Vec::new(),
1256            });
1257        }
1258        let consumer = self.push_fragment(
1259            FragmentAssignment::Coordinator,
1260            Vec::new(),
1261            estimated_rows,
1262            estimated_bytes,
1263        );
1264        self.wire(&producers, &[consumer], ExchangeKind::Merge);
1265        Ok(Stage {
1266            producers: vec![consumer],
1267            estimated_rows,
1268            estimated_bytes,
1269            partitioning: None,
1270            tablets: Vec::new(),
1271        })
1272    }
1273}
1274
1275// ---------------------------------------------------------------------------
1276// DataFusion → DistributedPlan lowering (P0.4)
1277// ---------------------------------------------------------------------------
1278
1279/// Lowers a DataFusion [`datafusion::logical_expr::LogicalPlan`] onto tablets.
1280///
1281/// Conversion is two-phase: the DataFusion tree is reduced to
1282/// [`LogicalPlanLite`], then [`distribute`] places fragments. Filters and
1283/// projections are pushed onto base scans; residual filters that cannot be
1284/// pushed are rejected rather than stringified without worker evaluation.
1285#[derive(Clone, Debug)]
1286pub struct DataFusionDistributedPlanner {
1287    query_id: QueryId,
1288    options: PlannerOptions,
1289}
1290
1291impl DataFusionDistributedPlanner {
1292    /// Planner pinned to one query id with default options.
1293    pub fn new(query_id: QueryId) -> Self {
1294        Self {
1295            query_id,
1296            options: PlannerOptions::default(),
1297        }
1298    }
1299
1300    /// Planner with explicit fragment/join tuning.
1301    pub fn with_options(query_id: QueryId, options: PlannerOptions) -> Self {
1302        Self { query_id, options }
1303    }
1304
1305    /// Query id this planner stamps onto every plan.
1306    pub fn query_id(&self) -> QueryId {
1307        self.query_id
1308    }
1309
1310    /// Lowers `plan` into a [`DistributedPlan`] against the given locator and
1311    /// control-plane metadata.
1312    pub fn lower(
1313        &self,
1314        plan: &datafusion::logical_expr::LogicalPlan,
1315        locator: &dyn TabletLocator,
1316        metadata: &dyn ClusterMetadata,
1317    ) -> DistributedResult<DistributedPlan> {
1318        let root = self.to_lite(plan)?;
1319        distribute(
1320            &PlanDescription {
1321                query_id: self.query_id,
1322                root,
1323                options: self.options,
1324            },
1325            locator,
1326            metadata,
1327        )
1328    }
1329
1330    /// Converts a DataFusion logical plan into the distributed IR without
1331    /// tablet placement. Useful for unit tests and diagnostics.
1332    pub fn to_lite(
1333        &self,
1334        plan: &datafusion::logical_expr::LogicalPlan,
1335    ) -> DistributedResult<LogicalPlanLite> {
1336        lower_datafusion_plan(plan)
1337    }
1338}
1339
1340fn lower_datafusion_plan(
1341    plan: &datafusion::logical_expr::LogicalPlan,
1342) -> DistributedResult<LogicalPlanLite> {
1343    use datafusion::logical_expr::LogicalPlan;
1344
1345    match plan {
1346        LogicalPlan::TableScan(scan) => lower_table_scan(scan),
1347        LogicalPlan::Projection(projection) => lower_projection(projection),
1348        LogicalPlan::Filter(filter) => lower_filter(filter),
1349        LogicalPlan::Aggregate(aggregate) => lower_aggregate(aggregate),
1350        LogicalPlan::Sort(sort) => lower_sort(sort),
1351        LogicalPlan::Limit(limit) => lower_limit(limit),
1352        LogicalPlan::Join(join) => lower_join(join),
1353        LogicalPlan::Union(union) => lower_union(union),
1354        LogicalPlan::SubqueryAlias(alias) => lower_datafusion_plan(alias.input.as_ref()),
1355        other => Err(DistributedError::Unsupported(format!(
1356            "DataFusion operator not supported for distributed planning: {}",
1357            other.display()
1358        ))),
1359    }
1360}
1361
1362fn lower_table_scan(
1363    scan: &datafusion::logical_expr::TableScan,
1364) -> DistributedResult<LogicalPlanLite> {
1365    let table = scan.table_name.table().to_owned();
1366    if table.is_empty() {
1367        return Err(DistributedError::InvalidPlan(
1368            "table scan needs a table name".to_owned(),
1369        ));
1370    }
1371    let projection = if let Some(indices) = &scan.projection {
1372        let schema = scan.source.schema();
1373        let mut columns = Vec::with_capacity(indices.len());
1374        for &idx in indices {
1375            if idx >= schema.fields().len() {
1376                return Err(DistributedError::InvalidPlan(format!(
1377                    "table scan projection index {idx} out of range"
1378                )));
1379            }
1380            columns.push(schema.field(idx).name().clone());
1381        }
1382        columns
1383    } else {
1384        // projected_schema already reflects provider projection when present.
1385        scan.projected_schema
1386            .fields()
1387            .iter()
1388            .map(|field| field.name().clone())
1389            .collect()
1390    };
1391    let predicate = combine_filter_predicates(&scan.filters)?;
1392    let mut root = LogicalPlanLite::Scan {
1393        table,
1394        predicate,
1395        projection,
1396    };
1397    if let Some(fetch) = scan.fetch {
1398        root = LogicalPlanLite::Limit {
1399            input: Box::new(root),
1400            limit: fetch,
1401        };
1402    }
1403    Ok(root)
1404}
1405
1406fn lower_projection(
1407    projection: &datafusion::logical_expr::Projection,
1408) -> DistributedResult<LogicalPlanLite> {
1409    let mut input = lower_datafusion_plan(projection.input.as_ref())?;
1410    let columns = projection_column_names(&projection.expr)?;
1411    // Pure column projections fold into the base scan when possible.
1412    if let Some((predicate, projection_cols)) = scan_fields_mut(&mut input) {
1413        let _ = predicate; // projection does not touch the predicate
1414        if !columns.is_empty() {
1415            if projection_cols.is_empty() {
1416                *projection_cols = columns;
1417            } else {
1418                // Intersect/reorder existing projection by requested names.
1419                let map: HashMap<String, String> = projection_cols
1420                    .iter()
1421                    .map(|name| (name.clone(), name.clone()))
1422                    .collect();
1423                let mut next = Vec::with_capacity(columns.len());
1424                for column in &columns {
1425                    let Some(existing) = map.get(column.as_str()) else {
1426                        return Err(DistributedError::InvalidPlan(format!(
1427                            "projection column `{column}` is not in the scan projection"
1428                        )));
1429                    };
1430                    next.push(existing.clone());
1431                }
1432                *projection_cols = next;
1433            }
1434        }
1435        return Ok(input);
1436    }
1437    // Non-scan inputs (Aggregate / Sort / Limit / Join / Union): treat pure
1438    // column renames as identity. The fragment model has no free-standing
1439    // Projection operator; final output naming is coordinator-side.
1440    if columns.is_empty()
1441        || matches!(
1442            input,
1443            LogicalPlanLite::Aggregate { .. }
1444                | LogicalPlanLite::Sort { .. }
1445                | LogicalPlanLite::Limit { .. }
1446                | LogicalPlanLite::Join { .. }
1447                | LogicalPlanLite::Union { .. }
1448        )
1449    {
1450        return Ok(input);
1451    }
1452    Err(DistributedError::Unsupported(
1453        "projection over a non-scan plan is not supported; push the projection onto the base table"
1454            .to_owned(),
1455    ))
1456}
1457
1458fn lower_filter(filter: &datafusion::logical_expr::Filter) -> DistributedResult<LogicalPlanLite> {
1459    // Require a worker-evaluable predicate shape before accepting it.
1460    assert_filter_evaluable(&filter.predicate)?;
1461    let mut input = lower_datafusion_plan(filter.input.as_ref())?;
1462    let text = filter.predicate.to_string();
1463    if let Some((predicate, _)) = scan_fields_mut(&mut input) {
1464        *predicate = match predicate.take() {
1465            Some(existing) => Some(format!("({existing}) AND ({text})")),
1466            None => Some(text),
1467        };
1468        return Ok(input);
1469    }
1470    Err(DistributedError::Unsupported(
1471        "filter must push down onto a table scan for distributed execution".to_owned(),
1472    ))
1473}
1474
1475fn lower_aggregate(
1476    aggregate: &datafusion::logical_expr::Aggregate,
1477) -> DistributedResult<LogicalPlanLite> {
1478    let input = lower_datafusion_plan(aggregate.input.as_ref())?;
1479    let mut group_by = Vec::with_capacity(aggregate.group_expr.len());
1480    for expr in &aggregate.group_expr {
1481        group_by.push(column_name(expr).ok_or_else(|| {
1482            DistributedError::Unsupported(format!(
1483                "aggregate group-by expression must be a column, got {expr}"
1484            ))
1485        })?);
1486    }
1487    let mut aggregates = Vec::with_capacity(aggregate.aggr_expr.len());
1488    for expr in &aggregate.aggr_expr {
1489        aggregates.push(aggregate_expr_from_df(expr)?);
1490    }
1491    if aggregates.is_empty() {
1492        return Err(DistributedError::InvalidPlan(
1493            "aggregate needs at least one aggregate expression".to_owned(),
1494        ));
1495    }
1496    Ok(LogicalPlanLite::Aggregate {
1497        input: Box::new(input),
1498        group_by,
1499        aggregates,
1500    })
1501}
1502
1503fn lower_sort(sort: &datafusion::logical_expr::Sort) -> DistributedResult<LogicalPlanLite> {
1504    let input = lower_datafusion_plan(sort.input.as_ref())?;
1505    if sort.expr.is_empty() {
1506        return Err(DistributedError::InvalidPlan(
1507            "sort needs at least one key".to_owned(),
1508        ));
1509    }
1510    let mut keys = Vec::with_capacity(sort.expr.len());
1511    for sort_expr in &sort.expr {
1512        let column = column_name(&sort_expr.expr).ok_or_else(|| {
1513            DistributedError::Unsupported(format!(
1514                "sort key must be a column, got {}",
1515                sort_expr.expr
1516            ))
1517        })?;
1518        keys.push(SortKey {
1519            column,
1520            descending: !sort_expr.asc,
1521        });
1522    }
1523    Ok(LogicalPlanLite::Sort {
1524        input: Box::new(input),
1525        keys,
1526        limit: sort.fetch,
1527    })
1528}
1529
1530fn lower_limit(limit: &datafusion::logical_expr::Limit) -> DistributedResult<LogicalPlanLite> {
1531    use datafusion::logical_expr::{FetchType, SkipType};
1532
1533    match limit.get_skip_type() {
1534        Ok(SkipType::Literal(0)) => {}
1535        Ok(SkipType::Literal(skip)) => {
1536            return Err(DistributedError::Unsupported(format!(
1537                "OFFSET {skip} is not supported in distributed planning"
1538            )));
1539        }
1540        Ok(SkipType::UnsupportedExpr) => {
1541            return Err(DistributedError::Unsupported(
1542                "non-literal OFFSET is not supported in distributed planning".to_owned(),
1543            ));
1544        }
1545        Err(error) => {
1546            return Err(DistributedError::InvalidPlan(error.to_string()));
1547        }
1548    }
1549    let fetch = match limit.get_fetch_type() {
1550        Ok(FetchType::Literal(Some(n))) => n,
1551        Ok(FetchType::Literal(None)) => {
1552            // LIMIT with no fetch is a no-op.
1553            return lower_datafusion_plan(limit.input.as_ref());
1554        }
1555        Ok(FetchType::UnsupportedExpr) => {
1556            return Err(DistributedError::Unsupported(
1557                "non-literal LIMIT is not supported in distributed planning".to_owned(),
1558            ));
1559        }
1560        Err(error) => return Err(DistributedError::InvalidPlan(error.to_string())),
1561    };
1562    let input = lower_datafusion_plan(limit.input.as_ref())?;
1563    Ok(LogicalPlanLite::Limit {
1564        input: Box::new(input),
1565        limit: fetch,
1566    })
1567}
1568
1569fn lower_join(join: &datafusion::logical_expr::Join) -> DistributedResult<LogicalPlanLite> {
1570    use datafusion::logical_expr::{JoinConstraint, JoinType};
1571
1572    if join.join_type != JoinType::Inner {
1573        return Err(DistributedError::Unsupported(format!(
1574            "only Inner joins are supported for distributed planning, got {:?}",
1575            join.join_type
1576        )));
1577    }
1578    if join.filter.is_some() {
1579        return Err(DistributedError::Unsupported(
1580            "non-equi join filters are not supported for distributed planning".to_owned(),
1581        ));
1582    }
1583    if !matches!(
1584        join.join_constraint,
1585        JoinConstraint::On | JoinConstraint::Using
1586    ) {
1587        return Err(DistributedError::Unsupported(format!(
1588            "unsupported join constraint {:?}",
1589            join.join_constraint
1590        )));
1591    }
1592    if join.on.is_empty() {
1593        return Err(DistributedError::InvalidPlan(
1594            "join needs at least one key".to_owned(),
1595        ));
1596    }
1597    let left = lower_datafusion_plan(join.left.as_ref())?;
1598    let right = lower_datafusion_plan(join.right.as_ref())?;
1599    let mut on = Vec::with_capacity(join.on.len());
1600    for (left_expr, right_expr) in &join.on {
1601        let left_col = column_name(left_expr).ok_or_else(|| {
1602            DistributedError::Unsupported(format!(
1603                "join key must be a column, got left={left_expr}"
1604            ))
1605        })?;
1606        let right_col = column_name(right_expr).ok_or_else(|| {
1607            DistributedError::Unsupported(format!(
1608                "join key must be a column, got right={right_expr}"
1609            ))
1610        })?;
1611        on.push(JoinKey {
1612            left: left_col,
1613            right: right_col,
1614        });
1615    }
1616    Ok(LogicalPlanLite::Join {
1617        left: Box::new(left),
1618        right: Box::new(right),
1619        on,
1620    })
1621}
1622
1623fn lower_union(union: &datafusion::logical_expr::Union) -> DistributedResult<LogicalPlanLite> {
1624    if union.inputs.len() < 2 {
1625        return Err(DistributedError::InvalidPlan(
1626            "union needs at least two inputs".to_owned(),
1627        ));
1628    }
1629    let mut inputs = Vec::with_capacity(union.inputs.len());
1630    for input in &union.inputs {
1631        inputs.push(lower_datafusion_plan(input.as_ref())?);
1632    }
1633    Ok(LogicalPlanLite::Union { inputs })
1634}
1635
1636/// Mutable access to a base-scan's predicate and projection, including when the
1637/// scan is wrapped by a table-scan `fetch` limit.
1638fn scan_fields_mut(node: &mut LogicalPlanLite) -> Option<(&mut Option<String>, &mut Vec<String>)> {
1639    match node {
1640        LogicalPlanLite::Scan {
1641            predicate,
1642            projection,
1643            ..
1644        } => Some((predicate, projection)),
1645        LogicalPlanLite::Limit { input, .. } => match input.as_mut() {
1646            LogicalPlanLite::Scan {
1647                predicate,
1648                projection,
1649                ..
1650            } => Some((predicate, projection)),
1651            _ => None,
1652        },
1653        _ => None,
1654    }
1655}
1656
1657fn projection_column_names(
1658    exprs: &[datafusion::logical_expr::Expr],
1659) -> DistributedResult<Vec<String>> {
1660    let mut columns = Vec::with_capacity(exprs.len());
1661    for expr in exprs {
1662        if let Some(name) = column_name(expr) {
1663            columns.push(name);
1664            continue;
1665        }
1666        // Unresolved wildcards (or other non-column exprs) are rejected —
1667        // DataFusion should expand them before distributed lowering.
1668        return Err(DistributedError::Unsupported(format!(
1669            "projection expression must be a column (or alias of a column), got {expr}"
1670        )));
1671    }
1672    Ok(columns)
1673}
1674
1675fn column_name(expr: &datafusion::logical_expr::Expr) -> Option<String> {
1676    use datafusion::logical_expr::Expr;
1677    match expr {
1678        Expr::Column(column) => Some(column.name.clone()),
1679        Expr::Alias(alias) => column_name(alias.expr.as_ref()),
1680        _ => None,
1681    }
1682}
1683
1684fn combine_filter_predicates(
1685    filters: &[datafusion::logical_expr::Expr],
1686) -> DistributedResult<Option<String>> {
1687    if filters.is_empty() {
1688        return Ok(None);
1689    }
1690    let mut parts = Vec::with_capacity(filters.len());
1691    for filter in filters {
1692        assert_filter_evaluable(filter)?;
1693        parts.push(format!("({filter})"));
1694    }
1695    Ok(Some(parts.join(" AND ")))
1696}
1697
1698/// Accept only predicates the distributed worker can evaluate without an
1699/// opaque, untyped blob. Column comparisons against literals (and boolean
1700/// combinations of those) are allowed; everything else is rejected.
1701fn assert_filter_evaluable(expr: &datafusion::logical_expr::Expr) -> DistributedResult<()> {
1702    use datafusion::logical_expr::{Expr, Operator};
1703
1704    match expr {
1705        Expr::Column(_) | Expr::Literal(_, _) | Expr::IsNull(_) | Expr::IsNotNull(_) => Ok(()),
1706        Expr::Alias(alias) => assert_filter_evaluable(alias.expr.as_ref()),
1707        Expr::Not(inner) => assert_filter_evaluable(inner.as_ref()),
1708        Expr::BinaryExpr(binary) => match binary.op {
1709            Operator::And
1710            | Operator::Or
1711            | Operator::Eq
1712            | Operator::NotEq
1713            | Operator::Lt
1714            | Operator::LtEq
1715            | Operator::Gt
1716            | Operator::GtEq => {
1717                assert_filter_evaluable(binary.left.as_ref())?;
1718                assert_filter_evaluable(binary.right.as_ref())
1719            }
1720            other => Err(DistributedError::Unsupported(format!(
1721                "filter operator {other:?} is not supported for distributed planning"
1722            ))),
1723        },
1724        Expr::Between(between) => {
1725            assert_filter_evaluable(between.expr.as_ref())?;
1726            assert_filter_evaluable(between.low.as_ref())?;
1727            assert_filter_evaluable(between.high.as_ref())
1728        }
1729        Expr::InList(list) => {
1730            assert_filter_evaluable(list.expr.as_ref())?;
1731            for value in &list.list {
1732                assert_filter_evaluable(value)?;
1733            }
1734            Ok(())
1735        }
1736        Expr::Like(like) => {
1737            assert_filter_evaluable(like.expr.as_ref())?;
1738            assert_filter_evaluable(like.pattern.as_ref())
1739        }
1740        other => Err(DistributedError::Unsupported(format!(
1741            "filter expression is not supported for distributed planning: {other}"
1742        ))),
1743    }
1744}
1745
1746// ---------------------------------------------------------------------------
1747// Public SQL → DistributedPlan entry (P0.4 gateway seam)
1748// ---------------------------------------------------------------------------
1749
1750/// Arrow schemas for tables that DataFusion must resolve while planning SQL
1751/// for distributed placement. Schemas are planning-only (no row data).
1752#[derive(Clone, Debug, Default)]
1753pub struct PlanningTableCatalog {
1754    tables: HashMap<String, SchemaRef>,
1755}
1756
1757impl PlanningTableCatalog {
1758    /// Empty catalog.
1759    pub fn new() -> Self {
1760        Self::default()
1761    }
1762
1763    /// Registers (or replaces) one table's schema for SQL planning.
1764    pub fn insert(&mut self, table: impl Into<String>, schema: SchemaRef) {
1765        self.tables.insert(table.into(), schema);
1766    }
1767
1768    /// Builder-style registration.
1769    pub fn with_table(mut self, table: impl Into<String>, schema: SchemaRef) -> Self {
1770        self.insert(table, schema);
1771        self
1772    }
1773
1774    /// Registered table names (sorted for stable diagnostics).
1775    pub fn table_names(&self) -> Vec<&str> {
1776        let mut names: Vec<&str> = self.tables.keys().map(String::as_str).collect();
1777        names.sort_unstable();
1778        names
1779    }
1780
1781    /// Look up one table schema.
1782    pub fn schema(&self, table: &str) -> Option<&SchemaRef> {
1783        self.tables.get(table)
1784    }
1785}
1786
1787/// Lowers an already-parsed DataFusion logical plan onto tablets.
1788///
1789/// Prefer [`plan_sql_distributed`] when the caller only has SQL text. This
1790/// entry is useful when [`crate::MongrelSession`] (or another frontend) has
1791/// already produced a resolved `LogicalPlan`.
1792pub fn plan_logical_distributed(
1793    plan: &datafusion::logical_expr::LogicalPlan,
1794    locator: &dyn TabletLocator,
1795    metadata: &dyn ClusterMetadata,
1796) -> DistributedResult<DistributedPlan> {
1797    plan_logical_distributed_with_id(plan, QueryId::new_random(), locator, metadata)
1798}
1799
1800/// Same as [`plan_logical_distributed`] but stamps a caller-chosen query id.
1801pub fn plan_logical_distributed_with_id(
1802    plan: &datafusion::logical_expr::LogicalPlan,
1803    query_id: QueryId,
1804    locator: &dyn TabletLocator,
1805    metadata: &dyn ClusterMetadata,
1806) -> DistributedResult<DistributedPlan> {
1807    DataFusionDistributedPlanner::new(query_id).lower(plan, locator, metadata)
1808}
1809
1810/// Public gateway entry: parse `sql` with DataFusion against `catalog`, then
1811/// lower via [`DataFusionDistributedPlanner`] onto `locator`/`metadata`.
1812///
1813/// This is the product seam for cluster SQL planning (P0.4). Callers supply
1814/// planning schemas (column names/types) for every table the SQL references;
1815/// placement uses [`TabletLocator`] / [`ClusterMetadata`], not a standalone
1816/// local catalog scan.
1817pub async fn plan_sql_distributed(
1818    sql: &str,
1819    catalog: &PlanningTableCatalog,
1820    locator: &dyn TabletLocator,
1821    metadata: &dyn ClusterMetadata,
1822) -> DistributedResult<DistributedPlan> {
1823    plan_sql_distributed_with_id(sql, catalog, QueryId::new_random(), locator, metadata).await
1824}
1825
1826/// Same as [`plan_sql_distributed`] but stamps a caller-chosen query id.
1827pub async fn plan_sql_distributed_with_id(
1828    sql: &str,
1829    catalog: &PlanningTableCatalog,
1830    query_id: QueryId,
1831    locator: &dyn TabletLocator,
1832    metadata: &dyn ClusterMetadata,
1833) -> DistributedResult<DistributedPlan> {
1834    let sql = sql.trim();
1835    if sql.is_empty() {
1836        return Err(DistributedError::InvalidPlan(
1837            "SQL statement is empty".to_owned(),
1838        ));
1839    }
1840    let ctx = datafusion::prelude::SessionContext::new();
1841    for (name, schema) in &catalog.tables {
1842        // Empty MemTable: planning needs schema only; workers scan real tablets.
1843        let provider = datafusion::datasource::MemTable::try_new(
1844            Arc::clone(schema),
1845            vec![vec![RecordBatch::new_empty(Arc::clone(schema))]],
1846        )
1847        .map_err(|error| {
1848            DistributedError::InvalidPlan(format!(
1849                "failed to register planning table `{name}`: {error}"
1850            ))
1851        })?;
1852        ctx.register_table(name.as_str(), Arc::new(provider))
1853            .map_err(|error| {
1854                DistributedError::InvalidPlan(format!(
1855                    "failed to register planning table `{name}`: {error}"
1856                ))
1857            })?;
1858    }
1859    let df = ctx.sql(sql).await.map_err(|error| {
1860        DistributedError::InvalidPlan(format!("DataFusion failed to plan SQL: {error}"))
1861    })?;
1862    plan_logical_distributed_with_id(df.logical_plan(), query_id, locator, metadata)
1863}
1864
1865fn aggregate_expr_from_df(
1866    expr: &datafusion::logical_expr::Expr,
1867) -> DistributedResult<AggregateExpr> {
1868    use datafusion::logical_expr::Expr;
1869
1870    let expr = match expr {
1871        Expr::Alias(alias) => alias.expr.as_ref(),
1872        other => other,
1873    };
1874    let Expr::AggregateFunction(agg) = expr else {
1875        return Err(DistributedError::Unsupported(format!(
1876            "aggregate expression must be an aggregate function, got {expr}"
1877        )));
1878    };
1879    let name = agg.func.name().to_ascii_lowercase();
1880    let function = match name.as_str() {
1881        "count" => AggregateFunction::Count,
1882        "sum" => AggregateFunction::Sum,
1883        "min" => AggregateFunction::Min,
1884        "max" => AggregateFunction::Max,
1885        "avg" | "mean" => AggregateFunction::Avg,
1886        other => {
1887            return Err(DistributedError::Unsupported(format!(
1888                "aggregate function `{other}` is not supported for distributed planning"
1889            )));
1890        }
1891    };
1892    let column = match agg.params.args.as_slice() {
1893        [] => None,
1894        // COUNT(*) / COUNT(1) have no column; COUNT(col) keeps the column.
1895        [arg] if function == AggregateFunction::Count => column_name(arg),
1896        [arg] => Some(column_name(arg).ok_or_else(|| {
1897            DistributedError::Unsupported(format!("aggregate argument must be a column, got {arg}"))
1898        })?),
1899        _ => {
1900            return Err(DistributedError::Unsupported(
1901                "multi-argument aggregates are not supported for distributed planning".to_owned(),
1902            ));
1903        }
1904    };
1905    if function != AggregateFunction::Count && column.is_none() {
1906        return Err(DistributedError::InvalidPlan(format!(
1907            "{} needs a column",
1908            function.name()
1909        )));
1910    }
1911    Ok(AggregateExpr { function, column })
1912}
1913
1914/// Scales a byte estimate to a new row estimate.
1915fn scaled_bytes(bytes: u64, rows: u64, new_rows: u64) -> u64 {
1916    if rows == 0 {
1917        return 0;
1918    }
1919    bytes
1920        .saturating_mul(new_rows)
1921        .checked_div(rows)
1922        .unwrap_or(bytes)
1923}
1924
1925/// Derives a fragment's output column names (fingerprint input).
1926fn fragment_output_columns(fragment: &PlanFragment) -> Vec<String> {
1927    let mut columns = Vec::new();
1928    for operator in &fragment.operators {
1929        match operator {
1930            FragmentOperator::TabletScan {
1931                table, projection, ..
1932            } => {
1933                if projection.is_empty() {
1934                    columns = vec![format!("{table}.*")];
1935                } else {
1936                    columns = projection
1937                        .iter()
1938                        .map(|column| format!("{table}.{column}"))
1939                        .collect();
1940                }
1941            }
1942            FragmentOperator::PartialAggregate {
1943                group_by,
1944                aggregates,
1945            } => {
1946                columns = group_by.to_vec();
1947                columns.extend(partial_column_names(aggregates));
1948            }
1949            FragmentOperator::FinalAggregate {
1950                group_by,
1951                aggregates,
1952            } => {
1953                columns = group_by.to_vec();
1954                columns.extend(aggregates.iter().map(aggregate_output_name));
1955            }
1956            FragmentOperator::DistributedHashJoin { .. }
1957            | FragmentOperator::BroadcastJoin { .. }
1958            | FragmentOperator::RepartitionJoin { .. } => {
1959                columns = vec!["*join*".to_owned()];
1960            }
1961            FragmentOperator::RemoteExchangeSource { .. }
1962            | FragmentOperator::RemoteExchangeSink { .. }
1963            | FragmentOperator::MergeSort { .. }
1964            | FragmentOperator::DistributedTopK { .. }
1965            | FragmentOperator::DistributedLimit { .. } => {}
1966        }
1967    }
1968    columns
1969}
1970
1971/// Partial-aggregate output column names (`__partial_0`, and `__partial_i_sum`
1972/// / `__partial_i_count` for averages).
1973fn partial_column_names(aggregates: &[AggregateExpr]) -> Vec<String> {
1974    let mut names = Vec::new();
1975    for (index, aggregate) in aggregates.iter().enumerate() {
1976        if aggregate.function == AggregateFunction::Avg {
1977            names.push(format!("__partial_{index}_sum"));
1978            names.push(format!("__partial_{index}_count"));
1979        } else {
1980            names.push(format!("__partial_{index}"));
1981        }
1982    }
1983    names
1984}
1985
1986/// Final-aggregate output column name (`count_star`, `sum_cost`, ...).
1987fn aggregate_output_name(aggregate: &AggregateExpr) -> String {
1988    format!(
1989        "{}_{}",
1990        aggregate.function.name(),
1991        aggregate.column.as_deref().unwrap_or("star")
1992    )
1993}
1994
1995// ---------------------------------------------------------------------------
1996// Distributed top-k (spec section 12.10): deterministic merge + adaptive refill
1997// ---------------------------------------------------------------------------
1998
1999/// One ranked row in the top-k model: the score plus the exact tie-break
2000/// identity (spec section 12.10: final score descending, tablet id ascending,
2001/// [`RowId`] ascending).
2002#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2003pub struct TopKCandidate {
2004    /// Score mapped onto an order-preserving `u64` key (higher wins).
2005    pub score: u64,
2006    /// The tablet that contributed the row.
2007    pub tablet: TabletId,
2008    /// The row's id within the table.
2009    pub row_id: RowId,
2010}
2011
2012/// The deterministic winner order: score descending, then tablet id
2013/// ascending, then [`RowId`] ascending. Returns [`Ordering::Less`] when `a`
2014/// ranks strictly better than `b`.
2015pub fn topk_cmp(a: &TopKCandidate, b: &TopKCandidate) -> Ordering {
2016    b.score
2017        .cmp(&a.score)
2018        .then_with(|| a.tablet.cmp(&b.tablet))
2019        .then_with(|| a.row_id.cmp(&b.row_id))
2020}
2021
2022/// One tablet's bounded local top-k plus tie information (spec section
2023/// 12.10: "each tablet returns a bounded local top-k plus tie information").
2024#[derive(Clone, Debug)]
2025pub struct TabletTopK {
2026    /// The contributing tablet.
2027    pub tablet: TabletId,
2028    /// Local winners, best first (at most the bounded size).
2029    pub rows: Vec<TopKCandidate>,
2030    /// Upper bound on the score of any row NOT in `rows` (`None` = the tablet
2031    /// is exhausted — every local row has been returned).
2032    pub unseen_bound: Option<u64>,
2033}
2034
2035/// The outcome of one deterministic coordinator merge step.
2036#[derive(Clone, Debug, PartialEq, Eq)]
2037pub struct TopKMerge {
2038    /// The best `k` received candidates, best first.
2039    pub winners: Vec<TopKCandidate>,
2040    /// Tablets that must be refilled before `winners` is provably the exact
2041    /// global top-k (empty = the result is exact).
2042    pub refill: Vec<TabletId>,
2043}
2044
2045/// Deterministically merges bounded local top-ks (spec section 12.10:
2046/// "coordinator merges deterministically").
2047///
2048/// Winners are the best `k` received candidates under [`topk_cmp`]. A tablet
2049/// lands in [`TopKMerge::refill`] when its unseen rows could still displace a
2050/// winner — i.e. when fewer than `k` candidates were received in total and
2051/// the tablet is not exhausted, or when the tablet's most optimistic unseen
2052/// candidate (score = `unseen_bound`, smallest possible [`RowId`]) ranks at
2053/// least as well as the current `k`-th winner. The tie case is deliberately
2054/// conservative (unseen row ids are unknown), so a refill may be requested
2055/// that returns no winning rows; correctness never depends on it.
2056///
2057/// Exactness invariant: when `refill` is empty, `winners` equals the global
2058/// top-`k` of all rows on all tablets. Proof sketch: any unseen row of tablet
2059/// `t` ranks no better than the optimistic candidate `(unseen_bound, t,
2060/// RowId::MIN)` — its score is at most the bound, and at equal score its row
2061/// id is larger — so when the optimistic candidate ranks strictly worse than
2062/// the `k`-th winner, no unseen row of `t` can enter the top-`k`. When `t` is
2063/// exhausted it has no unseen rows at all. With every tablet in one of those
2064/// two states, the received top-`k` is the global top-`k`. When fewer than
2065/// `k` candidates were received, refill is empty iff every tablet is
2066/// exhausted, in which case all rows were seen.
2067pub fn merge_top_k(shards: &[TabletTopK], k: usize) -> TopKMerge {
2068    if k == 0 {
2069        return TopKMerge {
2070            winners: Vec::new(),
2071            refill: Vec::new(),
2072        };
2073    }
2074    let mut received: Vec<TopKCandidate> = shards
2075        .iter()
2076        .flat_map(|shard| shard.rows.iter().copied())
2077        .collect();
2078    received.sort_by(topk_cmp);
2079    received.truncate(k);
2080    let mut refill = Vec::new();
2081    if received.len() < k {
2082        for shard in shards {
2083            if shard.unseen_bound.is_some() {
2084                refill.push(shard.tablet);
2085            }
2086        }
2087    } else {
2088        let threshold = received[k - 1];
2089        for shard in shards {
2090            let Some(bound) = shard.unseen_bound else {
2091                continue;
2092            };
2093            let optimistic = TopKCandidate {
2094                score: bound,
2095                tablet: shard.tablet,
2096                row_id: RowId::MIN,
2097            };
2098            if topk_cmp(&optimistic, &threshold) != Ordering::Greater {
2099                refill.push(shard.tablet);
2100            }
2101        }
2102    }
2103    refill.sort();
2104    refill.dedup();
2105    TopKMerge {
2106        winners: received,
2107        refill,
2108    }
2109}
2110
2111/// Drives [`merge_top_k`] with adaptive refill until the result is provably
2112/// exact (spec section 12.10: "for exact global top-k, use adaptive refill
2113/// when local bounds show unseen rows could still win").
2114///
2115/// `initial` holds each tablet's first bounded contribution. `refill_batch`
2116/// must return the NEXT batch of local winners for one tablet (rows not
2117/// returned before, best first) together with a tightened unseen bound
2118/// (`None` when the tablet is exhausted). Iteration order over tablets is
2119/// sorted by id, so the driver is fully deterministic. Fails when a refill
2120/// makes no progress (no new rows and an unchanged bound), which indicates a
2121/// broken producer contract rather than a planning problem.
2122pub fn exact_top_k(
2123    k: usize,
2124    initial: Vec<TabletTopK>,
2125    mut refill_batch: impl FnMut(TabletId) -> TabletTopK,
2126) -> DistributedResult<Vec<TopKCandidate>> {
2127    let mut shards: BTreeMap<TabletId, TabletTopK> = initial
2128        .into_iter()
2129        .map(|shard| (shard.tablet, shard))
2130        .collect();
2131    loop {
2132        let ordered: Vec<TabletTopK> = shards.values().cloned().collect();
2133        let merge = merge_top_k(&ordered, k);
2134        if merge.refill.is_empty() {
2135            return Ok(merge.winners);
2136        }
2137        for tablet in merge.refill {
2138            let batch = refill_batch(tablet);
2139            let entry = shards.get_mut(&tablet).ok_or_else(|| {
2140                DistributedError::InvalidPlan(format!(
2141                    "top-k refill requested for unknown tablet {tablet}"
2142                ))
2143            })?;
2144            if batch.rows.is_empty() && batch.unseen_bound == entry.unseen_bound {
2145                return Err(DistributedError::InvalidPlan(format!(
2146                    "top-k refill for tablet {tablet} made no progress"
2147                )));
2148            }
2149            entry.rows.extend(batch.rows);
2150            entry.unseen_bound = batch.unseen_bound;
2151        }
2152    }
2153}
2154
2155// ---------------------------------------------------------------------------
2156// Execution skeleton (spec section 12.10)
2157// ---------------------------------------------------------------------------
2158
2159/// Cooperative per-fragment execution control: cancellation/deadline shared
2160/// with the query's [`ExecutionControl`] hierarchy plus the fragment's spill
2161/// allowance.
2162#[derive(Debug, Clone)]
2163pub struct FragmentControl {
2164    /// Cooperative cancellation handle (child of the query control, so a
2165    /// registry cancel fans out to every fragment).
2166    pub control: ExecutionControl,
2167    /// Maximum spill allowance stamped by the planner (spec section 12.10).
2168    pub max_spill_bytes: u64,
2169    /// Opaque server-issued user/session authorization envelope. Worker
2170    /// executors validate it before touching tablet data.
2171    pub authorization_context: Arc<[u8]>,
2172}
2173
2174impl FragmentControl {
2175    /// Open a core spill session for this fragment against `manager`, capped
2176    /// at [`Self::max_spill_bytes`]. Query operators that sort/join/aggregate
2177    /// under pressure call this instead of inventing a second spill path.
2178    pub fn begin_spill(
2179        &self,
2180        manager: &mongreldb_core::SpillManager,
2181        query_id: mongreldb_types::ids::QueryId,
2182    ) -> Result<mongreldb_core::SpillSession, mongreldb_core::SpillError> {
2183        manager.begin_query(query_id, self.max_spill_bytes)
2184    }
2185}
2186
2187/// Tie information for scored (top-k) streams, carried on a producer's
2188/// terminal frame (spec section 12.10: "bounded local top-k plus tie
2189/// information").
2190#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
2191pub enum ScoreBound {
2192    /// Not a scored stream, or the producer cannot report bounds; the
2193    /// coordinator treats such producers as exhausted.
2194    Unknown,
2195    /// Unsent rows may have score keys up to this value.
2196    AtMost(u64),
2197    /// The producer emitted every local row.
2198    Exhausted,
2199}
2200
2201/// One Arrow-ish record-batch frame on a fragment stream.
2202#[derive(Debug, Clone)]
2203pub struct BatchFrame {
2204    /// The record batch payload.
2205    pub batch: RecordBatch,
2206    /// Tie information; meaningful only on a scored stream's terminal frame.
2207    pub score_bound: ScoreBound,
2208}
2209
2210impl BatchFrame {
2211    /// A plain data frame (no tie information).
2212    pub fn data(batch: RecordBatch) -> Self {
2213        Self {
2214            batch,
2215            score_bound: ScoreBound::Unknown,
2216        }
2217    }
2218}
2219
2220/// A fragment's output stream.
2221pub type FragmentStream = BoxStream<'static, DistributedResult<BatchFrame>>;
2222
2223/// The next batch of a tablet's local top-k (adaptive refill, spec section
2224/// 12.10).
2225#[derive(Debug)]
2226pub struct TopKRefill {
2227    /// The next local candidates (rows not returned before, best first),
2228    /// aligned with `payload`'s rows.
2229    pub rows: Vec<TopKCandidate>,
2230    /// The candidates' full row payload (same schema as the stream output).
2231    pub payload: RecordBatch,
2232    /// Tightened unseen-score bound (`None` = tablet exhausted).
2233    pub unseen_bound: Option<u64>,
2234}
2235
2236/// Executes one fragment on its worker. Server/engine bindings install an
2237/// implementation behind [`RemoteFragmentEndpoint`];
2238/// [`InMemoryFragmentExecutor`] remains the deterministic reference executor.
2239#[async_trait::async_trait]
2240pub trait FragmentExecutor: Send + Sync {
2241    /// Runs the fragment. `inputs` carries one resolved stream per
2242    /// [`FragmentOperator::RemoteExchangeSource`] operator, in operator order.
2243    async fn execute(
2244        &self,
2245        fragment: &PlanFragment,
2246        inputs: Vec<FragmentStream>,
2247        control: FragmentControl,
2248    ) -> DistributedResult<FragmentStream>;
2249
2250    /// Returns the next `limit` local top-k candidates after `offset` (the
2251    /// number already returned), with a tightened unseen bound. Executors
2252    /// without scored streams leave the default, which rejects.
2253    fn refill_top_k(
2254        &self,
2255        fragment: &PlanFragment,
2256        offset: usize,
2257        limit: usize,
2258        control: FragmentControl,
2259    ) -> DistributedResult<TopKRefill> {
2260        let _ = (fragment, offset, limit, control);
2261        Err(DistributedError::Unsupported(
2262            "top-k refill is not implemented by this executor".to_owned(),
2263        ))
2264    }
2265}
2266
2267/// Moves fragments, refill, and cancellation between the coordinator and
2268/// workers (spec section 12.10). Implementations include the deterministic
2269/// [`InMemoryTransport`] and the bounded Arrow IPC
2270/// [`RemoteFragmentTransport`].
2271#[async_trait::async_trait]
2272pub trait FragmentTransport: Send + Sync {
2273    /// Starts a fragment on its assigned worker and returns its output
2274    /// stream.
2275    async fn execute_fragment(
2276        &self,
2277        query_id: QueryId,
2278        fragment: &PlanFragment,
2279        inputs: Vec<FragmentStream>,
2280        control: FragmentControl,
2281    ) -> DistributedResult<FragmentStream>;
2282
2283    /// Best-effort cancellation of a running (or abandoned) fragment.
2284    fn cancel_fragment(&self, query_id: QueryId, fragment_id: FragmentId) -> DistributedResult<()>;
2285
2286    /// Fetches the next top-k batch of a fragment's tablet (adaptive
2287    /// refill). Transports without a refill binding leave the default.
2288    async fn refill_top_k(
2289        &self,
2290        query_id: QueryId,
2291        fragment: &PlanFragment,
2292        offset: usize,
2293        limit: usize,
2294        control: FragmentControl,
2295    ) -> DistributedResult<TopKRefill> {
2296        let _ = (query_id, fragment, offset, limit, control);
2297        Err(DistributedError::Unsupported(
2298            "top-k refill over this transport is not bound in this wave".to_owned(),
2299        ))
2300    }
2301}
2302
2303/// In-memory [`FragmentTransport`]: routes fragments to per-tablet executors,
2304/// records starts/cancellations/refills for test introspection, and keeps
2305/// each fragment's [`ExecutionControl`] so cancellations are observable.
2306pub struct InMemoryTransport {
2307    default_executor: Arc<dyn FragmentExecutor>,
2308    executors: parking_lot::RwLock<HashMap<TabletId, Arc<dyn FragmentExecutor>>>,
2309    started: Mutex<Vec<FragmentId>>,
2310    cancelled: Mutex<Vec<FragmentId>>,
2311    controls: Mutex<HashMap<FragmentId, ExecutionControl>>,
2312    refills: Mutex<Vec<(FragmentId, usize, usize)>>,
2313}
2314
2315impl InMemoryTransport {
2316    /// A transport whose every fragment runs on `default_executor`.
2317    pub fn new(default_executor: Arc<dyn FragmentExecutor>) -> Self {
2318        Self {
2319            default_executor,
2320            executors: parking_lot::RwLock::new(HashMap::new()),
2321            started: Mutex::new(Vec::new()),
2322            cancelled: Mutex::new(Vec::new()),
2323            controls: Mutex::new(HashMap::new()),
2324            refills: Mutex::new(Vec::new()),
2325        }
2326    }
2327
2328    /// Pins one tablet to its own executor.
2329    pub fn with_executor(self, tablet: TabletId, executor: Arc<dyn FragmentExecutor>) -> Self {
2330        self.executors.write().insert(tablet, executor);
2331        self
2332    }
2333
2334    fn executor_for(&self, assignment: &FragmentAssignment) -> Arc<dyn FragmentExecutor> {
2335        match assignment {
2336            FragmentAssignment::Tablet(tablet) => self
2337                .executors
2338                .read()
2339                .get(tablet)
2340                .cloned()
2341                .unwrap_or_else(|| Arc::clone(&self.default_executor)),
2342            FragmentAssignment::Coordinator => Arc::clone(&self.default_executor),
2343        }
2344    }
2345
2346    /// Fragments started so far, in start order.
2347    pub fn started_fragments(&self) -> Vec<FragmentId> {
2348        self.started.lock().clone()
2349    }
2350
2351    /// Fragments cancelled so far, in cancel order.
2352    pub fn cancelled_fragments(&self) -> Vec<FragmentId> {
2353        self.cancelled.lock().clone()
2354    }
2355
2356    /// Top-k refills issued so far: `(fragment_id, offset, limit)`.
2357    pub fn refill_log(&self) -> Vec<(FragmentId, usize, usize)> {
2358        self.refills.lock().clone()
2359    }
2360
2361    /// The control handed to a started fragment, for cancellation assertions.
2362    pub fn control_for(&self, fragment_id: FragmentId) -> Option<ExecutionControl> {
2363        self.controls.lock().get(&fragment_id).cloned()
2364    }
2365}
2366
2367#[async_trait::async_trait]
2368impl FragmentTransport for InMemoryTransport {
2369    async fn execute_fragment(
2370        &self,
2371        _query_id: QueryId,
2372        fragment: &PlanFragment,
2373        inputs: Vec<FragmentStream>,
2374        control: FragmentControl,
2375    ) -> DistributedResult<FragmentStream> {
2376        self.started.lock().push(fragment.fragment_id);
2377        self.controls
2378            .lock()
2379            .insert(fragment.fragment_id, control.control.clone());
2380        self.executor_for(&fragment.assignment)
2381            .execute(fragment, inputs, control)
2382            .await
2383    }
2384
2385    fn cancel_fragment(
2386        &self,
2387        _query_id: QueryId,
2388        fragment_id: FragmentId,
2389    ) -> DistributedResult<()> {
2390        self.cancelled.lock().push(fragment_id);
2391        if let Some(control) = self.controls.lock().get(&fragment_id) {
2392            control.cancel(CancellationReason::ClientRequest);
2393        }
2394        Ok(())
2395    }
2396
2397    async fn refill_top_k(
2398        &self,
2399        _query_id: QueryId,
2400        fragment: &PlanFragment,
2401        offset: usize,
2402        limit: usize,
2403        control: FragmentControl,
2404    ) -> DistributedResult<TopKRefill> {
2405        self.refills
2406            .lock()
2407            .push((fragment.fragment_id, offset, limit));
2408        self.executor_for(&fragment.assignment)
2409            .refill_top_k(fragment, offset, limit, control)
2410    }
2411}
2412
2413// ---------------------------------------------------------------------------
2414// Remote Arrow IPC fragment transport
2415// ---------------------------------------------------------------------------
2416
2417/// Version of the private cluster fragment protocol.
2418///
2419/// This is an internal wire generation, not a MongrelDB release or database
2420/// format version. Peers fail closed when it differs.
2421pub const REMOTE_FRAGMENT_PROTOCOL_VERSION: u16 = 1;
2422/// Stable service discriminator inside the cluster's authenticated internal
2423/// RPC multiplexer.
2424pub const REMOTE_FRAGMENT_SERVICE_ID: u32 = 1;
2425
2426/// Default maximum encoded request or response body for one fragment RPC.
2427pub const DEFAULT_REMOTE_FRAGMENT_MESSAGE_BYTES: usize = 16 * 1024 * 1024;
2428
2429/// Default maximum number of fragment streams held by one worker.
2430pub const DEFAULT_REMOTE_FRAGMENT_EXECUTIONS: usize = 1_024;
2431
2432#[derive(Debug, Clone, Serialize, Deserialize)]
2433struct RemoteFragmentEnvelope {
2434    version: u16,
2435    request: RemoteFragmentRequest,
2436}
2437
2438#[derive(Debug, Clone, Serialize, Deserialize)]
2439enum RemoteFragmentRequest {
2440    Start {
2441        query_id: QueryId,
2442        fragment: PlanFragment,
2443        inputs: Vec<Vec<RemoteBatchFrame>>,
2444        max_spill_bytes: u64,
2445        authorization_context: Vec<u8>,
2446        deadline_ms: Option<u64>,
2447    },
2448    Pull {
2449        query_id: QueryId,
2450        fragment_id: FragmentId,
2451    },
2452    Cancel {
2453        query_id: QueryId,
2454        fragment_id: FragmentId,
2455    },
2456    RefillTopK {
2457        query_id: QueryId,
2458        fragment: PlanFragment,
2459        offset: usize,
2460        limit: usize,
2461        authorization_context: Vec<u8>,
2462        deadline_ms: Option<u64>,
2463    },
2464}
2465
2466#[derive(Debug, Clone, Serialize, Deserialize)]
2467struct RemoteFragmentResponseEnvelope {
2468    version: u16,
2469    response: RemoteFragmentResponse,
2470}
2471
2472#[derive(Debug, Clone, Serialize, Deserialize)]
2473enum RemoteFragmentResponse {
2474    Started,
2475    Frame(Option<RemoteBatchFrame>),
2476    Cancelled,
2477    TopKRefill {
2478        rows: Vec<TopKCandidate>,
2479        payload: Vec<u8>,
2480        unseen_bound: Option<u64>,
2481    },
2482    Error(String),
2483}
2484
2485#[derive(Debug, Clone, Serialize, Deserialize)]
2486struct RemoteBatchFrame {
2487    ipc: Vec<u8>,
2488    score_bound: ScoreBound,
2489}
2490
2491type RemoteExecutionKey = (QueryId, FragmentId);
2492
2493struct RemoteExecution {
2494    stream: tokio::sync::Mutex<Option<FragmentStream>>,
2495    control: ExecutionControl,
2496}
2497
2498/// Lifecycle counters for remote fragment workers (P0.4-T6).
2499#[derive(Debug, Default)]
2500pub struct FragmentLifecycleMetrics {
2501    /// Fragment start requests accepted.
2502    pub starts: std::sync::atomic::AtomicU64,
2503    /// Pull frames returned (including end-of-stream).
2504    pub pulls: std::sync::atomic::AtomicU64,
2505    /// Explicit cancel requests that reclaimed a cursor.
2506    pub cancels: std::sync::atomic::AtomicU64,
2507    /// Streams that completed (end-of-stream pull).
2508    pub completes: std::sync::atomic::AtomicU64,
2509    /// Request body bytes handled.
2510    pub bytes_in: std::sync::atomic::AtomicU64,
2511    /// Response body bytes emitted.
2512    pub bytes_out: std::sync::atomic::AtomicU64,
2513}
2514
2515impl FragmentLifecycleMetrics {
2516    /// Snapshot of counters for tests / admin surfaces.
2517    pub fn snapshot(&self) -> FragmentLifecycleSnapshot {
2518        use std::sync::atomic::Ordering::Relaxed;
2519        FragmentLifecycleSnapshot {
2520            starts: self.starts.load(Relaxed),
2521            pulls: self.pulls.load(Relaxed),
2522            cancels: self.cancels.load(Relaxed),
2523            completes: self.completes.load(Relaxed),
2524            bytes_in: self.bytes_in.load(Relaxed),
2525            bytes_out: self.bytes_out.load(Relaxed),
2526            active_executions: 0,
2527        }
2528    }
2529}
2530
2531/// Point-in-time fragment lifecycle view (P0.4-T6).
2532#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
2533pub struct FragmentLifecycleSnapshot {
2534    pub starts: u64,
2535    pub pulls: u64,
2536    pub cancels: u64,
2537    pub completes: u64,
2538    pub bytes_in: u64,
2539    pub bytes_out: u64,
2540    pub active_executions: usize,
2541}
2542
2543/// Worker-side endpoint for the internal fragment protocol.
2544///
2545/// The cluster transport supplies authenticated node-to-node delivery. This
2546/// endpoint owns bounded active cursors and returns one Arrow IPC batch per
2547/// pull. Pull framing provides backpressure without buffering a whole result
2548/// on either peer.
2549pub struct RemoteFragmentEndpoint {
2550    executor: Arc<dyn FragmentExecutor>,
2551    executions: parking_lot::Mutex<HashMap<RemoteExecutionKey, Arc<RemoteExecution>>>,
2552    max_executions: usize,
2553    max_message_bytes: usize,
2554    metrics: FragmentLifecycleMetrics,
2555}
2556
2557impl RemoteFragmentEndpoint {
2558    /// Creates a bounded worker endpoint.
2559    pub fn new(executor: Arc<dyn FragmentExecutor>) -> Self {
2560        Self::with_limits(
2561            executor,
2562            DEFAULT_REMOTE_FRAGMENT_EXECUTIONS,
2563            DEFAULT_REMOTE_FRAGMENT_MESSAGE_BYTES,
2564        )
2565    }
2566
2567    /// Creates a worker endpoint with explicit cursor and frame bounds.
2568    pub fn with_limits(
2569        executor: Arc<dyn FragmentExecutor>,
2570        max_executions: usize,
2571        max_message_bytes: usize,
2572    ) -> Self {
2573        Self {
2574            executor,
2575            executions: parking_lot::Mutex::new(HashMap::new()),
2576            max_executions: max_executions.max(1),
2577            max_message_bytes: max_message_bytes.max(1),
2578            metrics: FragmentLifecycleMetrics::default(),
2579        }
2580    }
2581
2582    /// Number of live remote fragment cursors.
2583    pub fn active_executions(&self) -> usize {
2584        self.executions.lock().len()
2585    }
2586
2587    /// Fragment lifecycle metrics including active cursor count (P0.4-T6).
2588    pub fn lifecycle_metrics(&self) -> FragmentLifecycleSnapshot {
2589        let mut snap = self.metrics.snapshot();
2590        snap.active_executions = self.active_executions();
2591        snap
2592    }
2593
2594    /// Handles one authenticated internal RPC body.
2595    pub async fn handle(&self, bytes: &[u8]) -> DistributedResult<Vec<u8>> {
2596        use std::sync::atomic::Ordering::Relaxed;
2597        self.metrics.bytes_in.fetch_add(bytes.len() as u64, Relaxed);
2598        if bytes.len() > self.max_message_bytes {
2599            return Err(DistributedError::RemoteProtocol(format!(
2600                "fragment request is {} bytes; limit is {}",
2601                bytes.len(),
2602                self.max_message_bytes
2603            )));
2604        }
2605        let envelope: RemoteFragmentEnvelope = decode_remote_wire(bytes, self.max_message_bytes)?;
2606        if envelope.version != REMOTE_FRAGMENT_PROTOCOL_VERSION {
2607            return Err(DistributedError::RemoteProtocol(format!(
2608                "unsupported fragment protocol version {}; supported version is {}",
2609                envelope.version, REMOTE_FRAGMENT_PROTOCOL_VERSION
2610            )));
2611        }
2612        let response = match self.handle_request(envelope.request).await {
2613            Ok(response) => response,
2614            Err(error) => RemoteFragmentResponse::Error(error.to_string()),
2615        };
2616        let encoded = encode_remote_wire(
2617            &RemoteFragmentResponseEnvelope {
2618                version: REMOTE_FRAGMENT_PROTOCOL_VERSION,
2619                response,
2620            },
2621            self.max_message_bytes,
2622        )?;
2623        self.metrics
2624            .bytes_out
2625            .fetch_add(encoded.len() as u64, std::sync::atomic::Ordering::Relaxed);
2626        Ok(encoded)
2627    }
2628
2629    async fn handle_request(
2630        &self,
2631        request: RemoteFragmentRequest,
2632    ) -> DistributedResult<RemoteFragmentResponse> {
2633        match request {
2634            RemoteFragmentRequest::Start {
2635                query_id,
2636                fragment,
2637                inputs,
2638                max_spill_bytes,
2639                authorization_context,
2640                deadline_ms,
2641            } => {
2642                if authorization_context.len() > MAX_FRAGMENT_AUTHORIZATION_CONTEXT_BYTES {
2643                    return Err(DistributedError::RemoteProtocol(format!(
2644                        "fragment authorization context is {} bytes; limit is {}",
2645                        authorization_context.len(),
2646                        MAX_FRAGMENT_AUTHORIZATION_CONTEXT_BYTES
2647                    )));
2648                }
2649                let key = (query_id, fragment.fragment_id);
2650                {
2651                    let executions = self.executions.lock();
2652                    if executions.contains_key(&key) {
2653                        return Err(DistributedError::RemoteProtocol(format!(
2654                            "fragment {} for query {query_id} is already running",
2655                            fragment.fragment_id
2656                        )));
2657                    }
2658                    if executions.len() >= self.max_executions {
2659                        return Err(DistributedError::Reservation {
2660                            fragment_id: fragment.fragment_id,
2661                            reason: format!(
2662                                "worker holds {} remote fragments; limit is {}",
2663                                executions.len(),
2664                                self.max_executions
2665                            ),
2666                        });
2667                    }
2668                }
2669                let inputs = inputs
2670                    .into_iter()
2671                    .map(|frames| {
2672                        frames
2673                            .into_iter()
2674                            .map(|frame| decode_remote_frame(frame, self.max_message_bytes))
2675                            .collect::<DistributedResult<Vec<_>>>()
2676                            .map(|frames| {
2677                                Box::pin(stream::iter(frames.into_iter().map(Ok))) as FragmentStream
2678                            })
2679                    })
2680                    .collect::<DistributedResult<Vec<_>>>()?;
2681                let control = deadline_ms.map_or_else(
2682                    || ExecutionControl::new(None),
2683                    |milliseconds| {
2684                        ExecutionControl::with_timeout(Duration::from_millis(milliseconds))
2685                    },
2686                );
2687                let execution = Arc::new(RemoteExecution {
2688                    stream: tokio::sync::Mutex::new(None),
2689                    control: control.clone(),
2690                });
2691                {
2692                    let mut executions = self.executions.lock();
2693                    if executions.contains_key(&key) {
2694                        return Err(DistributedError::RemoteProtocol(format!(
2695                            "fragment {} for query {query_id} raced another start",
2696                            fragment.fragment_id
2697                        )));
2698                    }
2699                    if executions.len() >= self.max_executions {
2700                        return Err(DistributedError::Reservation {
2701                            fragment_id: fragment.fragment_id,
2702                            reason: "remote fragment limit reached during start".to_owned(),
2703                        });
2704                    }
2705                    executions.insert(key, Arc::clone(&execution));
2706                }
2707                let stream = match self
2708                    .executor
2709                    .execute(
2710                        &fragment,
2711                        inputs,
2712                        FragmentControl {
2713                            control,
2714                            max_spill_bytes,
2715                            authorization_context: authorization_context.into(),
2716                        },
2717                    )
2718                    .await
2719                {
2720                    Ok(stream) => stream,
2721                    Err(error) => {
2722                        self.executions.lock().remove(&key);
2723                        return Err(error);
2724                    }
2725                };
2726                if self.executions.lock().get(&key).is_none() {
2727                    return Err(DistributedError::Cancelled(
2728                        CancellationReason::ClientRequest,
2729                    ));
2730                }
2731                if let Err(error) = checkpoint(&execution.control) {
2732                    self.executions.lock().remove(&key);
2733                    return Err(error);
2734                }
2735                *execution.stream.lock().await = Some(stream);
2736                self.metrics
2737                    .starts
2738                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2739                Ok(RemoteFragmentResponse::Started)
2740            }
2741            RemoteFragmentRequest::Pull {
2742                query_id,
2743                fragment_id,
2744            } => {
2745                let key = (query_id, fragment_id);
2746                let execution = self.executions.lock().get(&key).cloned().ok_or_else(|| {
2747                    DistributedError::RemoteProtocol(format!(
2748                        "fragment {fragment_id} for query {query_id} is not running"
2749                    ))
2750                })?;
2751                let next = {
2752                    checkpoint(&execution.control)?;
2753                    let mut stream = execution.stream.lock().await;
2754                    let stream = stream.as_mut().ok_or_else(|| {
2755                        DistributedError::RemoteProtocol(format!(
2756                            "fragment {fragment_id} for query {query_id} is not ready"
2757                        ))
2758                    })?;
2759                    stream.next().await.transpose()?
2760                };
2761                self.metrics
2762                    .pulls
2763                    .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2764                match next {
2765                    Some(frame) => Ok(RemoteFragmentResponse::Frame(Some(encode_remote_frame(
2766                        &frame,
2767                        self.max_message_bytes,
2768                    )?))),
2769                    None => {
2770                        self.executions.lock().remove(&key);
2771                        self.metrics
2772                            .completes
2773                            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2774                        Ok(RemoteFragmentResponse::Frame(None))
2775                    }
2776                }
2777            }
2778            RemoteFragmentRequest::Cancel {
2779                query_id,
2780                fragment_id,
2781            } => {
2782                if let Some(execution) = self.executions.lock().remove(&(query_id, fragment_id)) {
2783                    execution.control.cancel(CancellationReason::ClientRequest);
2784                    self.metrics
2785                        .cancels
2786                        .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2787                }
2788                Ok(RemoteFragmentResponse::Cancelled)
2789            }
2790            RemoteFragmentRequest::RefillTopK {
2791                query_id: _,
2792                fragment,
2793                offset,
2794                limit,
2795                authorization_context,
2796                deadline_ms,
2797            } => {
2798                if authorization_context.len() > MAX_FRAGMENT_AUTHORIZATION_CONTEXT_BYTES {
2799                    return Err(DistributedError::RemoteProtocol(format!(
2800                        "fragment authorization context is {} bytes; limit is {}",
2801                        authorization_context.len(),
2802                        MAX_FRAGMENT_AUTHORIZATION_CONTEXT_BYTES
2803                    )));
2804                }
2805                let control = deadline_ms.map_or_else(
2806                    || ExecutionControl::new(None),
2807                    |milliseconds| {
2808                        ExecutionControl::with_timeout(Duration::from_millis(milliseconds))
2809                    },
2810                );
2811                let refill = self.executor.refill_top_k(
2812                    &fragment,
2813                    offset,
2814                    limit,
2815                    FragmentControl {
2816                        control,
2817                        max_spill_bytes: fragment.max_spill_bytes,
2818                        authorization_context: authorization_context.into(),
2819                    },
2820                )?;
2821                let payload = encode_record_batch(&refill.payload, self.max_message_bytes)?;
2822                Ok(RemoteFragmentResponse::TopKRefill {
2823                    rows: refill.rows,
2824                    payload,
2825                    unseen_bound: refill.unseen_bound,
2826                })
2827            }
2828        }
2829    }
2830}
2831
2832/// One authenticated request/response carrier for remote fragment bodies.
2833///
2834/// Production implements this over the cluster's node-identity-bound mTLS
2835/// transport. Tests may use [`LoopbackFragmentRpcClient`] to isolate the
2836/// query-side protocol.
2837#[async_trait::async_trait]
2838pub trait FragmentRpcClient: Send + Sync {
2839    /// Performs one bounded internal RPC.
2840    async fn call(&self, request: Vec<u8>) -> DistributedResult<Vec<u8>>;
2841}
2842
2843/// In-process carrier for protocol and endpoint tests.
2844pub struct LoopbackFragmentRpcClient {
2845    endpoint: Arc<RemoteFragmentEndpoint>,
2846}
2847
2848impl LoopbackFragmentRpcClient {
2849    /// Wraps one worker endpoint.
2850    pub fn new(endpoint: Arc<RemoteFragmentEndpoint>) -> Self {
2851        Self { endpoint }
2852    }
2853}
2854
2855#[async_trait::async_trait]
2856impl FragmentRpcClient for LoopbackFragmentRpcClient {
2857    async fn call(&self, request: Vec<u8>) -> DistributedResult<Vec<u8>> {
2858        self.endpoint.handle(&request).await
2859    }
2860}
2861
2862/// Coordinator-side Arrow IPC transport for tablet fragments.
2863///
2864/// Each tablet route names an authenticated RPC carrier. Output is pulled one
2865/// batch at a time, so consumer polling is the backpressure mechanism.
2866pub struct RemoteFragmentTransport {
2867    default_client: Option<Arc<dyn FragmentRpcClient>>,
2868    clients: parking_lot::RwLock<HashMap<TabletId, Arc<dyn FragmentRpcClient>>>,
2869    active: Arc<parking_lot::Mutex<HashMap<RemoteExecutionKey, Arc<dyn FragmentRpcClient>>>>,
2870    max_message_bytes: usize,
2871}
2872
2873impl RemoteFragmentTransport {
2874    /// Creates a transport whose tablets use `default_client`.
2875    pub fn new(default_client: Arc<dyn FragmentRpcClient>) -> Self {
2876        Self::with_message_limit(default_client, DEFAULT_REMOTE_FRAGMENT_MESSAGE_BYTES)
2877    }
2878
2879    /// Creates a transport with an explicit per-call body bound.
2880    pub fn with_message_limit(
2881        default_client: Arc<dyn FragmentRpcClient>,
2882        max_message_bytes: usize,
2883    ) -> Self {
2884        Self {
2885            default_client: Some(default_client),
2886            clients: parking_lot::RwLock::new(HashMap::new()),
2887            active: Arc::new(parking_lot::Mutex::new(HashMap::new())),
2888            max_message_bytes: max_message_bytes.max(1),
2889        }
2890    }
2891
2892    /// Creates a fail-closed transport with no fallback route.
2893    pub fn routed() -> Self {
2894        Self {
2895            default_client: None,
2896            clients: parking_lot::RwLock::new(HashMap::new()),
2897            active: Arc::new(parking_lot::Mutex::new(HashMap::new())),
2898            max_message_bytes: DEFAULT_REMOTE_FRAGMENT_MESSAGE_BYTES,
2899        }
2900    }
2901
2902    /// Routes one tablet through a specific peer carrier.
2903    pub fn with_client(self, tablet: TabletId, client: Arc<dyn FragmentRpcClient>) -> Self {
2904        self.clients.write().insert(tablet, client);
2905        self
2906    }
2907
2908    fn client_for(&self, fragment: &PlanFragment) -> DistributedResult<Arc<dyn FragmentRpcClient>> {
2909        let tablet = tablet_of(fragment)?;
2910        self.clients
2911            .read()
2912            .get(&tablet)
2913            .cloned()
2914            .or_else(|| self.default_client.as_ref().map(Arc::clone))
2915            .ok_or_else(|| {
2916                DistributedError::RemoteTransport(format!(
2917                    "no authenticated fragment route for tablet {tablet}"
2918                ))
2919            })
2920    }
2921
2922    async fn call(
2923        &self,
2924        client: &Arc<dyn FragmentRpcClient>,
2925        request: RemoteFragmentRequest,
2926    ) -> DistributedResult<RemoteFragmentResponse> {
2927        remote_call(client, request, self.max_message_bytes).await
2928    }
2929}
2930
2931struct RemoteStreamState {
2932    client: Arc<dyn FragmentRpcClient>,
2933    key: RemoteExecutionKey,
2934    active: Arc<parking_lot::Mutex<HashMap<RemoteExecutionKey, Arc<dyn FragmentRpcClient>>>>,
2935    max_message_bytes: usize,
2936    complete: bool,
2937    yielded_error: bool,
2938}
2939
2940impl RemoteStreamState {
2941    fn mark_complete(&mut self) {
2942        self.complete = true;
2943    }
2944}
2945
2946impl Drop for RemoteStreamState {
2947    fn drop(&mut self) {
2948        self.active.lock().remove(&self.key);
2949        if self.complete {
2950            return;
2951        }
2952        let request = RemoteFragmentRequest::Cancel {
2953            query_id: self.key.0,
2954            fragment_id: self.key.1,
2955        };
2956        let client = Arc::clone(&self.client);
2957        let max_message_bytes = self.max_message_bytes;
2958        if let Ok(runtime) = tokio::runtime::Handle::try_current() {
2959            runtime.spawn(async move {
2960                let _ = remote_call(&client, request, max_message_bytes).await;
2961            });
2962        }
2963    }
2964}
2965
2966#[async_trait::async_trait]
2967impl FragmentTransport for RemoteFragmentTransport {
2968    async fn execute_fragment(
2969        &self,
2970        query_id: QueryId,
2971        fragment: &PlanFragment,
2972        inputs: Vec<FragmentStream>,
2973        control: FragmentControl,
2974    ) -> DistributedResult<FragmentStream> {
2975        let client = self.client_for(fragment)?;
2976        let mut wire_inputs = Vec::with_capacity(inputs.len());
2977        for input in inputs {
2978            let frames = drain_stream(input, &control.control).await?;
2979            wire_inputs.push(
2980                frames
2981                    .iter()
2982                    .map(|frame| encode_remote_frame(frame, self.max_message_bytes))
2983                    .collect::<DistributedResult<Vec<_>>>()?,
2984            );
2985        }
2986        let key = (query_id, fragment.fragment_id);
2987        self.active.lock().insert(key, Arc::clone(&client));
2988        let start = self.call(
2989            &client,
2990            RemoteFragmentRequest::Start {
2991                query_id,
2992                fragment: fragment.clone(),
2993                inputs: wire_inputs,
2994                max_spill_bytes: control.max_spill_bytes,
2995                authorization_context: control.authorization_context.to_vec(),
2996                deadline_ms: control
2997                    .control
2998                    .remaining_duration()
2999                    .map(|duration| duration.as_millis().min(u128::from(u64::MAX)) as u64),
3000            },
3001        );
3002        let response = tokio::select! {
3003            response = start => response,
3004            _ = control.control.cancelled() => {
3005                let _ = remote_call(
3006                    &client,
3007                    RemoteFragmentRequest::Cancel {
3008                        query_id,
3009                        fragment_id: fragment.fragment_id,
3010                    },
3011                    self.max_message_bytes,
3012                ).await;
3013                Err(DistributedError::Cancelled(control.control.reason()))
3014            }
3015        };
3016        let response = match response {
3017            Ok(response) => response,
3018            Err(error) => {
3019                self.active.lock().remove(&key);
3020                return Err(error);
3021            }
3022        };
3023        match response {
3024            RemoteFragmentResponse::Started => {}
3025            other => {
3026                self.active.lock().remove(&key);
3027                return Err(unexpected_remote_response("Started", &other));
3028            }
3029        }
3030        let state = RemoteStreamState {
3031            client,
3032            key,
3033            active: Arc::clone(&self.active),
3034            max_message_bytes: self.max_message_bytes,
3035            complete: false,
3036            yielded_error: false,
3037        };
3038        let output = stream::unfold(state, |mut state| async move {
3039            if state.complete || state.yielded_error {
3040                return None;
3041            }
3042            let response = remote_call(
3043                &state.client,
3044                RemoteFragmentRequest::Pull {
3045                    query_id: state.key.0,
3046                    fragment_id: state.key.1,
3047                },
3048                state.max_message_bytes,
3049            )
3050            .await;
3051            match response {
3052                Ok(RemoteFragmentResponse::Frame(Some(frame))) => {
3053                    let item = decode_remote_frame(frame, state.max_message_bytes);
3054                    if item.is_err() {
3055                        state.yielded_error = true;
3056                    }
3057                    Some((item, state))
3058                }
3059                Ok(RemoteFragmentResponse::Frame(None)) => {
3060                    state.mark_complete();
3061                    None
3062                }
3063                Ok(other) => {
3064                    state.yielded_error = true;
3065                    Some((Err(unexpected_remote_response("Frame", &other)), state))
3066                }
3067                Err(error) => {
3068                    state.yielded_error = true;
3069                    Some((Err(error), state))
3070                }
3071            }
3072        });
3073        Ok(Box::pin(output))
3074    }
3075
3076    fn cancel_fragment(&self, query_id: QueryId, fragment_id: FragmentId) -> DistributedResult<()> {
3077        let key = (query_id, fragment_id);
3078        let Some(client) = self.active.lock().remove(&key) else {
3079            return Ok(());
3080        };
3081        let max_message_bytes = self.max_message_bytes;
3082        let runtime = tokio::runtime::Handle::try_current().map_err(|error| {
3083            DistributedError::RemoteTransport(format!(
3084                "cannot schedule fragment cancellation outside Tokio: {error}"
3085            ))
3086        })?;
3087        runtime.spawn(async move {
3088            let _ = remote_call(
3089                &client,
3090                RemoteFragmentRequest::Cancel {
3091                    query_id,
3092                    fragment_id,
3093                },
3094                max_message_bytes,
3095            )
3096            .await;
3097        });
3098        Ok(())
3099    }
3100
3101    async fn refill_top_k(
3102        &self,
3103        query_id: QueryId,
3104        fragment: &PlanFragment,
3105        offset: usize,
3106        limit: usize,
3107        control: FragmentControl,
3108    ) -> DistributedResult<TopKRefill> {
3109        let client = self.client_for(fragment)?;
3110        match self
3111            .call(
3112                &client,
3113                RemoteFragmentRequest::RefillTopK {
3114                    query_id,
3115                    fragment: fragment.clone(),
3116                    offset,
3117                    limit,
3118                    authorization_context: control.authorization_context.to_vec(),
3119                    deadline_ms: control
3120                        .control
3121                        .remaining_duration()
3122                        .map(|duration| duration.as_millis().min(u128::from(u64::MAX)) as u64),
3123                },
3124            )
3125            .await?
3126        {
3127            RemoteFragmentResponse::TopKRefill {
3128                rows,
3129                payload,
3130                unseen_bound,
3131            } => Ok(TopKRefill {
3132                rows,
3133                payload: decode_record_batch(&payload, self.max_message_bytes)?,
3134                unseen_bound,
3135            }),
3136            other => Err(unexpected_remote_response("TopKRefill", &other)),
3137        }
3138    }
3139}
3140
3141async fn remote_call(
3142    client: &Arc<dyn FragmentRpcClient>,
3143    request: RemoteFragmentRequest,
3144    max_message_bytes: usize,
3145) -> DistributedResult<RemoteFragmentResponse> {
3146    let request = encode_remote_wire(
3147        &RemoteFragmentEnvelope {
3148            version: REMOTE_FRAGMENT_PROTOCOL_VERSION,
3149            request,
3150        },
3151        max_message_bytes,
3152    )?;
3153    let response = client.call(request).await?;
3154    let envelope: RemoteFragmentResponseEnvelope =
3155        decode_remote_wire(&response, max_message_bytes)?;
3156    if envelope.version != REMOTE_FRAGMENT_PROTOCOL_VERSION {
3157        return Err(DistributedError::RemoteProtocol(format!(
3158            "peer answered with fragment protocol version {}; supported version is {}",
3159            envelope.version, REMOTE_FRAGMENT_PROTOCOL_VERSION
3160        )));
3161    }
3162    match envelope.response {
3163        RemoteFragmentResponse::Error(message) => Err(DistributedError::RemoteTransport(message)),
3164        response => Ok(response),
3165    }
3166}
3167
3168fn remote_wire_options() -> impl Options {
3169    bincode::DefaultOptions::new()
3170        .with_fixint_encoding()
3171        .reject_trailing_bytes()
3172}
3173
3174fn encode_remote_wire<T: Serialize>(
3175    value: &T,
3176    max_message_bytes: usize,
3177) -> DistributedResult<Vec<u8>> {
3178    let bytes = remote_wire_options()
3179        .serialize(value)
3180        .map_err(|error| DistributedError::RemoteProtocol(error.to_string()))?;
3181    if bytes.len() > max_message_bytes {
3182        return Err(DistributedError::RemoteProtocol(format!(
3183            "encoded fragment message is {} bytes; limit is {max_message_bytes}",
3184            bytes.len()
3185        )));
3186    }
3187    Ok(bytes)
3188}
3189
3190fn decode_remote_wire<T: for<'de> Deserialize<'de>>(
3191    bytes: &[u8],
3192    max_message_bytes: usize,
3193) -> DistributedResult<T> {
3194    if bytes.len() > max_message_bytes {
3195        return Err(DistributedError::RemoteProtocol(format!(
3196            "fragment message is {} bytes; limit is {max_message_bytes}",
3197            bytes.len()
3198        )));
3199    }
3200    remote_wire_options()
3201        .with_limit(max_message_bytes as u64)
3202        .deserialize(bytes)
3203        .map_err(|error| DistributedError::RemoteProtocol(error.to_string()))
3204}
3205
3206fn encode_record_batch(
3207    batch: &RecordBatch,
3208    max_message_bytes: usize,
3209) -> DistributedResult<Vec<u8>> {
3210    let mut ipc = Vec::new();
3211    {
3212        let mut writer = StreamWriter::try_new(&mut ipc, &batch.schema())?;
3213        writer.write(batch)?;
3214        writer.finish()?;
3215    }
3216    if ipc.len() > max_message_bytes {
3217        return Err(DistributedError::RemoteProtocol(format!(
3218            "Arrow IPC batch is {} bytes; limit is {max_message_bytes}",
3219            ipc.len()
3220        )));
3221    }
3222    Ok(ipc)
3223}
3224
3225fn decode_record_batch(ipc: &[u8], max_message_bytes: usize) -> DistributedResult<RecordBatch> {
3226    if ipc.len() > max_message_bytes {
3227        return Err(DistributedError::RemoteProtocol(format!(
3228            "Arrow IPC batch is {} bytes; limit is {max_message_bytes}",
3229            ipc.len()
3230        )));
3231    }
3232    let mut reader = StreamReader::try_new(Cursor::new(ipc), None)?;
3233    let batch = reader.next().transpose()?.ok_or_else(|| {
3234        DistributedError::RemoteProtocol("Arrow IPC frame contains no record batch".to_owned())
3235    })?;
3236    if reader.next().transpose()?.is_some() {
3237        return Err(DistributedError::RemoteProtocol(
3238            "Arrow IPC frame contains more than one record batch".to_owned(),
3239        ));
3240    }
3241    Ok(batch)
3242}
3243
3244fn encode_remote_frame(
3245    frame: &BatchFrame,
3246    max_message_bytes: usize,
3247) -> DistributedResult<RemoteBatchFrame> {
3248    Ok(RemoteBatchFrame {
3249        ipc: encode_record_batch(&frame.batch, max_message_bytes)?,
3250        score_bound: frame.score_bound,
3251    })
3252}
3253
3254fn decode_remote_frame(
3255    frame: RemoteBatchFrame,
3256    max_message_bytes: usize,
3257) -> DistributedResult<BatchFrame> {
3258    Ok(BatchFrame {
3259        batch: decode_record_batch(&frame.ipc, max_message_bytes)?,
3260        score_bound: frame.score_bound,
3261    })
3262}
3263
3264fn unexpected_remote_response(expected: &str, actual: &RemoteFragmentResponse) -> DistributedError {
3265    DistributedError::RemoteProtocol(format!(
3266        "expected remote {expected} response, got {actual:?}"
3267    ))
3268}
3269
3270/// Batch source consumed by the reference fragment operator engine.
3271///
3272/// Implementations validate `control.authorization_context` before reading.
3273pub trait FragmentTableSource: Send + Sync {
3274    /// Reads one table slice from a hosted tablet.
3275    fn scan(
3276        &self,
3277        table: &str,
3278        tablet: TabletId,
3279        include_row_id: bool,
3280        control: Option<&FragmentControl>,
3281    ) -> DistributedResult<Vec<RecordBatch>>;
3282
3283    /// Returns the empty-result schema for one table, when known.
3284    fn schema(&self, table: &str, tablet: TabletId) -> DistributedResult<Option<SchemaRef>>;
3285}
3286
3287/// Resolves the local storage owner for one hosted tablet.
3288pub trait FragmentDatabaseProvider: Send + Sync {
3289    /// Returns `None` when this node does not host `tablet`.
3290    fn database(&self, tablet: TabletId) -> Option<Arc<mongreldb_core::Database>>;
3291}
3292
3293/// Validates the forwarded server-issued authorization envelope.
3294pub trait FragmentAuthorizationResolver: Send + Sync {
3295    /// Resolves the exact principal for the local database. `None` is valid
3296    /// only for a credentialless database.
3297    fn resolve(
3298        &self,
3299        database: &mongreldb_core::Database,
3300        context: &[u8],
3301    ) -> DistributedResult<Option<mongreldb_core::Principal>>;
3302}
3303
3304/// Core MVCC-backed tablet source used by remote SQL workers.
3305pub struct CoreFragmentTableSource {
3306    databases: Arc<dyn FragmentDatabaseProvider>,
3307    authorization: Arc<dyn FragmentAuthorizationResolver>,
3308}
3309
3310impl CoreFragmentTableSource {
3311    /// Creates an authorized core-backed source.
3312    pub fn new(
3313        databases: Arc<dyn FragmentDatabaseProvider>,
3314        authorization: Arc<dyn FragmentAuthorizationResolver>,
3315    ) -> Self {
3316        Self {
3317            databases,
3318            authorization,
3319        }
3320    }
3321}
3322
3323impl FragmentTableSource for CoreFragmentTableSource {
3324    fn scan(
3325        &self,
3326        table: &str,
3327        tablet: TabletId,
3328        include_row_id: bool,
3329        control: Option<&FragmentControl>,
3330    ) -> DistributedResult<Vec<RecordBatch>> {
3331        let control = control.ok_or_else(|| {
3332            DistributedError::RemoteProtocol(
3333                "core tablet scan requires fragment authorization control".to_owned(),
3334            )
3335        })?;
3336        checkpoint(&control.control)?;
3337        let database = self.databases.database(tablet).ok_or_else(|| {
3338            DistributedError::RemoteTransport(format!("this node does not host tablet {tablet}"))
3339        })?;
3340        let principal = self
3341            .authorization
3342            .resolve(&database, &control.authorization_context)?;
3343        let schema = database
3344            .table(table)
3345            .map_err(|error| DistributedError::RemoteTransport(error.to_string()))?
3346            .lock()
3347            .schema()
3348            .clone();
3349        let rows = database
3350            .query_as_principal_controlled(
3351                table,
3352                &mongreldb_core::Query {
3353                    conditions: Vec::new(),
3354                    // Fragment scans are an internal streaming input, not a
3355                    // public final-result window. Capping this at
3356                    // MAX_FINAL_LIMIT silently dropped every row after
3357                    // 10,000 before aggregation, sorting, or exchange.
3358                    limit: None,
3359                    offset: 0,
3360                },
3361                None,
3362                principal.as_ref(),
3363                &control.control,
3364            )
3365            .map_err(|error| DistributedError::RemoteTransport(error.to_string()))?;
3366        let batch = crate::arrow_conv::rows_to_batch(&rows, &schema)
3367            .map_err(|error| DistributedError::Arrow(error.to_string()))?;
3368        if !include_row_id {
3369            return Ok(vec![batch]);
3370        }
3371        let mut fields = batch.schema().fields().iter().cloned().collect::<Vec<_>>();
3372        fields.push(Arc::new(Field::new(
3373            TOPK_ROWID_COLUMN,
3374            DataType::UInt64,
3375            false,
3376        )));
3377        let mut columns = batch.columns().to_vec();
3378        columns.push(Arc::new(UInt64Array::from(
3379            rows.iter().map(|row| row.row_id.0).collect::<Vec<_>>(),
3380        )));
3381        let batch = RecordBatch::try_new(Arc::new(Schema::new(fields)), columns)?;
3382        Ok(vec![batch])
3383    }
3384
3385    fn schema(&self, _table: &str, _tablet: TabletId) -> DistributedResult<Option<SchemaRef>> {
3386        // `scan` always returns one (possibly empty) batch with the authorized
3387        // schema, so this fallback is never needed.
3388        Ok(None)
3389    }
3390}
3391
3392/// In-memory per-`(table, tablet)` record-batch source backing
3393/// [`InMemoryFragmentExecutor`].
3394#[derive(Default)]
3395pub struct InMemoryTableStore {
3396    tables: parking_lot::RwLock<HashMap<(String, TabletId), Vec<RecordBatch>>>,
3397    schemas: parking_lot::RwLock<HashMap<String, SchemaRef>>,
3398}
3399
3400impl InMemoryTableStore {
3401    /// An empty store.
3402    pub fn new() -> Self {
3403        Self::default()
3404    }
3405
3406    /// Appends one batch to a tablet of a table, registering the table's
3407    /// schema from the batch on first sight.
3408    pub fn insert(&self, table: &str, tablet: TabletId, batch: RecordBatch) {
3409        self.schemas
3410            .write()
3411            .entry(table.to_owned())
3412            .or_insert_with(|| batch.schema());
3413        self.tables
3414            .write()
3415            .entry((table.to_owned(), tablet))
3416            .or_default()
3417            .push(batch);
3418    }
3419
3420    /// Registers a table schema explicitly (covers tablets with no rows).
3421    pub fn register_schema(&self, table: &str, schema: SchemaRef) {
3422        self.schemas.write().insert(table.to_owned(), schema);
3423    }
3424
3425    /// All batches stored for one tablet of a table (empty when none).
3426    pub fn snapshot(&self, table: &str, tablet: TabletId) -> Vec<RecordBatch> {
3427        self.tables
3428            .read()
3429            .get(&(table.to_owned(), tablet))
3430            .cloned()
3431            .unwrap_or_default()
3432    }
3433
3434    /// The table's registered schema, when known.
3435    pub fn schema(&self, table: &str) -> Option<SchemaRef> {
3436        self.schemas.read().get(table).cloned()
3437    }
3438}
3439
3440impl FragmentTableSource for InMemoryTableStore {
3441    fn scan(
3442        &self,
3443        table: &str,
3444        tablet: TabletId,
3445        _include_row_id: bool,
3446        _control: Option<&FragmentControl>,
3447    ) -> DistributedResult<Vec<RecordBatch>> {
3448        Ok(self.snapshot(table, tablet))
3449    }
3450
3451    fn schema(&self, table: &str, _tablet: TabletId) -> DistributedResult<Option<SchemaRef>> {
3452        Ok(self.schema(table))
3453    }
3454}
3455
3456/// Reference [`FragmentExecutor`] over an [`InMemoryTableStore`]. Interprets
3457/// scans, projections, partial aggregates, local merge sorts, bounded local
3458/// top-ks (with exact tie information), and limits for real; exchange
3459/// sources drain their input streams; join operators reject (their execution
3460/// binding lands with the tablet wave — plan shape is fully tested).
3461pub struct InMemoryFragmentExecutor {
3462    store: Arc<dyn FragmentTableSource>,
3463    /// Wire batch for the local top-k: at most this many rows are emitted
3464    /// before reporting a bound (`None` = emit `k`, which never needs a
3465    /// refill). Smaller values exercise the coordinator's adaptive refill.
3466    topk_emit_batch: Option<usize>,
3467}
3468
3469impl InMemoryFragmentExecutor {
3470    /// An executor over `store` that emits up to `k` top-k rows locally.
3471    pub fn new(store: Arc<InMemoryTableStore>) -> Self {
3472        Self {
3473            store,
3474            topk_emit_batch: None,
3475        }
3476    }
3477
3478    /// Bounds the local top-k emission batch (refill exerciser).
3479    pub fn with_topk_emit_batch(store: Arc<InMemoryTableStore>, batch: usize) -> Self {
3480        Self {
3481            store,
3482            topk_emit_batch: Some(batch),
3483        }
3484    }
3485
3486    /// Uses an arbitrary authorized tablet source with the same operator
3487    /// engine as the deterministic in-memory reference.
3488    pub fn from_source(store: Arc<dyn FragmentTableSource>) -> Self {
3489        Self {
3490            store,
3491            topk_emit_batch: None,
3492        }
3493    }
3494
3495    /// Replays the scan (+ projection) of a fragment from the store.
3496    fn scan_batches(
3497        &self,
3498        fragment: &PlanFragment,
3499        control: Option<&FragmentControl>,
3500    ) -> DistributedResult<Vec<RecordBatch>> {
3501        let tablet = tablet_of(fragment)?;
3502        let scan = fragment
3503            .operators
3504            .iter()
3505            .find_map(|operator| match operator {
3506                FragmentOperator::TabletScan {
3507                    table,
3508                    predicate,
3509                    projection,
3510                } => Some((table, predicate, projection)),
3511                _ => None,
3512            })
3513            .ok_or_else(|| {
3514                DistributedError::InvalidPlan(format!(
3515                    "fragment {} has no tablet scan",
3516                    fragment.fragment_id
3517                ))
3518            })?;
3519        if scan.1.is_some() {
3520            return Err(DistributedError::Unsupported(
3521                "tablet predicate execution is not bound to the fragment operator engine"
3522                    .to_owned(),
3523            ));
3524        }
3525        let include_row_id = fragment
3526            .operators
3527            .iter()
3528            .any(|operator| matches!(operator, FragmentOperator::DistributedTopK { .. }));
3529        let mut batches = self.store.scan(scan.0, tablet, include_row_id, control)?;
3530        if batches.is_empty() {
3531            if let Some(schema) = self.store.schema(scan.0, tablet)? {
3532                batches = vec![RecordBatch::new_empty(schema)];
3533            }
3534        }
3535        if !scan.2.is_empty() {
3536            batches = project_batches(&batches, scan.2)?;
3537        }
3538        Ok(batches)
3539    }
3540}
3541
3542#[async_trait::async_trait]
3543impl FragmentExecutor for InMemoryFragmentExecutor {
3544    async fn execute(
3545        &self,
3546        fragment: &PlanFragment,
3547        inputs: Vec<FragmentStream>,
3548        control: FragmentControl,
3549    ) -> DistributedResult<FragmentStream> {
3550        let mut batches: Vec<RecordBatch> = Vec::new();
3551        let mut inputs = inputs.into_iter();
3552        let mut bound = ScoreBound::Unknown;
3553        for operator in &fragment.operators {
3554            checkpoint(&control.control)?;
3555            match operator {
3556                FragmentOperator::TabletScan { .. } => {
3557                    batches = self.scan_batches(fragment, Some(&control))?;
3558                }
3559                FragmentOperator::RemoteExchangeSource { .. } => {
3560                    let input = inputs.next().ok_or_else(|| {
3561                        DistributedError::InvalidPlan(format!(
3562                            "fragment {} is missing an exchange input stream",
3563                            fragment.fragment_id
3564                        ))
3565                    })?;
3566                    let frames = drain_stream(input, &control.control).await?;
3567                    batches.extend(frames.into_iter().map(|frame| frame.batch));
3568                }
3569                FragmentOperator::PartialAggregate {
3570                    group_by,
3571                    aggregates,
3572                } => {
3573                    batches = vec![partial_aggregate_batches(&batches, group_by, aggregates)?];
3574                }
3575                FragmentOperator::FinalAggregate {
3576                    group_by,
3577                    aggregates,
3578                } => {
3579                    batches = vec![final_aggregate_batches(&batches, group_by, aggregates)?];
3580                }
3581                FragmentOperator::MergeSort { keys, limit } => {
3582                    batches = sort_batches_local(&batches, keys, *limit)?;
3583                }
3584                FragmentOperator::DistributedTopK { k, score } => {
3585                    let tablet = tablet_of(fragment)?;
3586                    let input = prepare_top_k(&batches, &score.column, tablet)?;
3587                    let emit = self.topk_emit_batch.unwrap_or(*k).min(*k);
3588                    let (_rows, payload, next) = input.emit(0, emit);
3589                    batches = vec![payload];
3590                    bound = match next {
3591                        Some(next) => ScoreBound::AtMost(next),
3592                        None => ScoreBound::Exhausted,
3593                    };
3594                }
3595                FragmentOperator::DistributedLimit { limit } => {
3596                    batches = limit_batches(&batches, *limit);
3597                }
3598                FragmentOperator::RemoteExchangeSink { .. } => {
3599                    // Routing is the transport/coordinator's job; the sink is
3600                    // a pass-through here.
3601                }
3602                FragmentOperator::DistributedHashJoin { .. }
3603                | FragmentOperator::BroadcastJoin { .. }
3604                | FragmentOperator::RepartitionJoin { .. } => {
3605                    return Err(DistributedError::Unsupported(
3606                        "join execution binding lands with the tablet wave".to_owned(),
3607                    ));
3608                }
3609            }
3610        }
3611        let mut frames: Vec<BatchFrame> = batches.into_iter().map(BatchFrame::data).collect();
3612        if bound != ScoreBound::Unknown {
3613            if let Some(last) = frames.last_mut() {
3614                last.score_bound = bound;
3615            }
3616        }
3617        Ok(Box::pin(stream::iter(frames.into_iter().map(Ok))))
3618    }
3619
3620    fn refill_top_k(
3621        &self,
3622        fragment: &PlanFragment,
3623        offset: usize,
3624        limit: usize,
3625        control: FragmentControl,
3626    ) -> DistributedResult<TopKRefill> {
3627        let tablet = tablet_of(fragment)?;
3628        let score = fragment
3629            .operators
3630            .iter()
3631            .find_map(|operator| match operator {
3632                FragmentOperator::DistributedTopK { score, .. } => Some(score.clone()),
3633                _ => None,
3634            })
3635            .ok_or_else(|| {
3636                DistributedError::InvalidPlan(format!(
3637                    "fragment {} has no distributed top-k operator",
3638                    fragment.fragment_id
3639                ))
3640            })?;
3641        let batches = self.scan_batches(fragment, Some(&control))?;
3642        let input = prepare_top_k(&batches, &score.column, tablet)?;
3643        let (rows, payload, unseen_bound) = input.emit(offset, limit);
3644        Ok(TopKRefill {
3645            rows,
3646            payload,
3647            unseen_bound,
3648        })
3649    }
3650}
3651
3652// ---------------------------------------------------------------------------
3653// Shared execution helpers and real merge operators
3654// ---------------------------------------------------------------------------
3655
3656/// Maps an [`ExecutionControl`] checkpoint onto a distributed cancellation.
3657fn checkpoint(control: &ExecutionControl) -> DistributedResult<()> {
3658    control
3659        .checkpoint()
3660        .map_err(|_| DistributedError::Cancelled(control.reason()))
3661}
3662
3663/// The tablet a fragment is assigned to (errors for coordinator fragments).
3664fn tablet_of(fragment: &PlanFragment) -> DistributedResult<TabletId> {
3665    match fragment.assignment {
3666        FragmentAssignment::Tablet(tablet) => Ok(tablet),
3667        FragmentAssignment::Coordinator => Err(DistributedError::InvalidPlan(format!(
3668            "fragment {} operator requires a tablet assignment",
3669            fragment.fragment_id
3670        ))),
3671    }
3672}
3673
3674/// Drains a fragment stream with cooperative cancellation.
3675async fn drain_stream(
3676    mut stream: FragmentStream,
3677    control: &ExecutionControl,
3678) -> DistributedResult<Vec<BatchFrame>> {
3679    let mut frames = Vec::new();
3680    while let Some(item) = stream.next().await {
3681        checkpoint(control)?;
3682        frames.push(item?);
3683    }
3684    Ok(frames)
3685}
3686
3687/// Concatenates batches (`None` when the input is empty).
3688fn concat_all(batches: &[RecordBatch]) -> DistributedResult<Option<RecordBatch>> {
3689    let Some(first) = batches.first() else {
3690        return Ok(None);
3691    };
3692    if batches.len() == 1 {
3693        return Ok(Some(first.clone()));
3694    }
3695    Ok(Some(concat_batches(&first.schema(), batches)?))
3696}
3697
3698/// Projects every batch onto the named columns.
3699fn project_batches(
3700    batches: &[RecordBatch],
3701    projection: &[String],
3702) -> DistributedResult<Vec<RecordBatch>> {
3703    batches
3704        .iter()
3705        .map(|batch| {
3706            let schema = batch.schema();
3707            let indexes: Vec<usize> = projection
3708                .iter()
3709                .map(|name| {
3710                    schema.index_of(name).map_err(|_| {
3711                        DistributedError::InvalidPlan(format!(
3712                            "projection column `{name}` not in schema"
3713                        ))
3714                    })
3715                })
3716                .collect::<DistributedResult<Vec<usize>>>()?;
3717            Ok(batch.project(&indexes)?)
3718        })
3719        .collect()
3720}
3721
3722/// Builds a row converter + key column indexes for sort keys.
3723fn row_converter(
3724    schema: &Schema,
3725    keys: &[SortKey],
3726) -> DistributedResult<(RowConverter, Vec<usize>)> {
3727    let mut fields = Vec::with_capacity(keys.len());
3728    let mut indexes = Vec::with_capacity(keys.len());
3729    for key in keys {
3730        let index = schema.index_of(&key.column).map_err(|_| {
3731            DistributedError::InvalidPlan(format!("sort key `{}` not in schema", key.column))
3732        })?;
3733        // Groundwork null semantics: nulls sort first under descending keys
3734        // and last under ascending keys. The DataFusion lowering wave maps
3735        // explicit NULLS FIRST/LAST clauses onto this.
3736        let options = arrow::compute::SortOptions {
3737            descending: key.descending,
3738            nulls_first: key.descending,
3739        };
3740        fields.push(SortField::new_with_options(
3741            schema.field(index).data_type().clone(),
3742            options,
3743        ));
3744        indexes.push(index);
3745    }
3746    Ok((RowConverter::new(fields)?, indexes))
3747}
3748
3749/// Gathers `order`ed rows of one batch into chunked output batches.
3750fn take_rows(batch: &RecordBatch, order: &[usize]) -> DistributedResult<Vec<RecordBatch>> {
3751    let mut out = Vec::new();
3752    for chunk in order.chunks(COORDINATOR_OUTPUT_BATCH_ROWS) {
3753        let indices = UInt32Array::from(
3754            chunk
3755                .iter()
3756                .map(|index| u32::try_from(*index).unwrap_or(u32::MAX))
3757                .collect::<Vec<u32>>(),
3758        );
3759        let mut columns = Vec::with_capacity(batch.num_columns());
3760        for column in batch.columns() {
3761            columns.push(take(column, &indices, None)?);
3762        }
3763        out.push(RecordBatch::try_new(batch.schema(), columns)?);
3764    }
3765    if out.is_empty() {
3766        out.push(RecordBatch::new_empty(batch.schema()));
3767    }
3768    Ok(out)
3769}
3770
3771/// Interleaves rows from several same-schema streams in `order`
3772/// (`(stream, row)` pairs) into chunked output batches.
3773fn emit_interleaved(
3774    streams: &[RecordBatch],
3775    order: &[(usize, usize)],
3776) -> DistributedResult<Vec<RecordBatch>> {
3777    let Some(first) = streams.first() else {
3778        return Ok(Vec::new());
3779    };
3780    if order.is_empty() {
3781        return Ok(vec![RecordBatch::new_empty(first.schema())]);
3782    }
3783    let schema = first.schema();
3784    let mut out = Vec::new();
3785    for chunk in order.chunks(COORDINATOR_OUTPUT_BATCH_ROWS) {
3786        let mut columns = Vec::with_capacity(schema.fields().len());
3787        for column_index in 0..schema.fields().len() {
3788            let refs: Vec<&dyn Array> = streams
3789                .iter()
3790                .map(|batch| batch.column(column_index).as_ref())
3791                .collect();
3792            columns.push(interleave(&refs, chunk)?);
3793        }
3794        out.push(RecordBatch::try_new(schema.clone(), columns)?);
3795    }
3796    Ok(out)
3797}
3798
3799/// Producer-side local sort: full deterministic sort of the (unsorted)
3800/// input, optionally truncated to `limit` rows.
3801fn sort_batches_local(
3802    batches: &[RecordBatch],
3803    keys: &[SortKey],
3804    limit: Option<usize>,
3805) -> DistributedResult<Vec<RecordBatch>> {
3806    let Some(batch) = concat_all(batches)? else {
3807        return Ok(Vec::new());
3808    };
3809    if batch.num_rows() == 0 {
3810        return Ok(vec![RecordBatch::new_empty(batch.schema())]);
3811    }
3812    let (converter, indexes) = row_converter(&batch.schema(), keys)?;
3813    let columns: Vec<ArrayRef> = indexes
3814        .iter()
3815        .map(|index| batch.column(*index).clone())
3816        .collect();
3817    let rows = converter.convert_columns(&columns)?;
3818    let mut order: Vec<usize> = (0..batch.num_rows()).collect();
3819    order.sort_by(|left, right| {
3820        rows.row(*left)
3821            .cmp(&rows.row(*right))
3822            .then_with(|| left.cmp(right))
3823    });
3824    if let Some(limit) = limit {
3825        order.truncate(limit);
3826    }
3827    take_rows(&batch, &order)
3828}
3829
3830/// Coordinator-side merge sort: a deterministic k-way merge over streams
3831/// that are each already sorted on `keys` (ties break by stream index, so
3832/// the result is fully deterministic).
3833fn merge_sorted_streams(
3834    streams: &[RecordBatch],
3835    keys: &[SortKey],
3836    limit: Option<usize>,
3837) -> DistributedResult<Vec<RecordBatch>> {
3838    /// Min-heap entry (via `Reverse`): smallest key, then smallest stream.
3839    struct MergeItem {
3840        key: Vec<u8>,
3841        stream: usize,
3842    }
3843    impl PartialEq for MergeItem {
3844        fn eq(&self, other: &Self) -> bool {
3845            self.key == other.key && self.stream == other.stream
3846        }
3847    }
3848    impl Eq for MergeItem {}
3849    impl PartialOrd for MergeItem {
3850        fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
3851            Some(self.cmp(other))
3852        }
3853    }
3854    impl Ord for MergeItem {
3855        fn cmp(&self, other: &Self) -> Ordering {
3856            self.key
3857                .cmp(&other.key)
3858                .then_with(|| self.stream.cmp(&other.stream))
3859        }
3860    }
3861
3862    let mut per_stream: Vec<Vec<Vec<u8>>> = Vec::with_capacity(streams.len());
3863    for batch in streams {
3864        let (converter, indexes) = row_converter(&batch.schema(), keys)?;
3865        let columns: Vec<ArrayRef> = indexes
3866            .iter()
3867            .map(|index| batch.column(*index).clone())
3868            .collect();
3869        let rows = converter.convert_columns(&columns)?;
3870        per_stream.push(
3871            (0..batch.num_rows())
3872                .map(|row| rows.row(row).as_ref().to_vec())
3873                .collect(),
3874        );
3875    }
3876    let mut cursors: Vec<usize> = vec![0; streams.len()];
3877    let mut heap = BinaryHeap::new();
3878    for (stream, keys) in per_stream.iter().enumerate() {
3879        if let Some(key) = keys.first() {
3880            heap.push(std::cmp::Reverse(MergeItem {
3881                key: key.clone(),
3882                stream,
3883            }));
3884        }
3885    }
3886    let mut order = Vec::new();
3887    while let Some(std::cmp::Reverse(item)) = heap.pop() {
3888        let row = cursors[item.stream];
3889        order.push((item.stream, row));
3890        if limit.is_some_and(|limit| order.len() >= limit) {
3891            break;
3892        }
3893        cursors[item.stream] += 1;
3894        let cursor = cursors[item.stream];
3895        if let Some(key) = per_stream[item.stream].get(cursor) {
3896            heap.push(std::cmp::Reverse(MergeItem {
3897                key: key.clone(),
3898                stream: item.stream,
3899            }));
3900        }
3901    }
3902    emit_interleaved(streams, &order)
3903}
3904
3905/// Truncates batches to `limit` rows, preserving stream order.
3906fn limit_batches(batches: &[RecordBatch], limit: usize) -> Vec<RecordBatch> {
3907    let mut remaining = limit;
3908    let mut out = Vec::new();
3909    for batch in batches {
3910        if remaining == 0 {
3911            break;
3912        }
3913        let rows = batch.num_rows().min(remaining);
3914        if rows == 0 {
3915            continue;
3916        }
3917        out.push(if rows == batch.num_rows() {
3918            batch.clone()
3919        } else {
3920            batch.slice(0, rows)
3921        });
3922        remaining -= rows;
3923    }
3924    out
3925}
3926
3927/// Routes a producer's output across the sibling consumers of a
3928/// hash-repartition boundary: rows whose FNV-1a key hash modulo `width`
3929/// equals `index`.
3930fn repartition_frames(
3931    frames: &[BatchFrame],
3932    keys: &[String],
3933    width: usize,
3934    index: usize,
3935) -> DistributedResult<Vec<BatchFrame>> {
3936    let batches: Vec<RecordBatch> = frames.iter().map(|frame| frame.batch.clone()).collect();
3937    let Some(batch) = concat_all(&batches)? else {
3938        return Ok(Vec::new());
3939    };
3940    if batch.num_rows() == 0 || width <= 1 {
3941        return Ok(vec![BatchFrame::data(batch)]);
3942    }
3943    let schema = batch.schema();
3944    let mut key_columns = Vec::with_capacity(keys.len());
3945    for key in keys {
3946        let column_index = schema.index_of(key).map_err(|_| {
3947            DistributedError::InvalidPlan(format!("repartition key `{key}` not in schema"))
3948        })?;
3949        key_columns.push(batch.column(column_index).clone());
3950    }
3951    let converter = RowConverter::new(
3952        key_columns
3953            .iter()
3954            .map(|column| SortField::new(column.data_type().clone()))
3955            .collect::<Vec<SortField>>(),
3956    )?;
3957    let rows = converter.convert_columns(&key_columns)?;
3958    let mut order = Vec::new();
3959    for row in 0..batch.num_rows() {
3960        let bucket = (fnv1a64(rows.row(row).as_ref()) % width as u64) as usize;
3961        if bucket == index {
3962            order.push(row);
3963        }
3964    }
3965    Ok(take_rows(&batch, &order)?
3966        .into_iter()
3967        .map(BatchFrame::data)
3968        .collect())
3969}
3970
3971/// Maps one numeric score cell onto an order-preserving `u64` key (higher
3972/// sorts better). Null scores map to the minimum key.
3973fn score_key(array: &dyn Array, row: usize) -> DistributedResult<u64> {
3974    if array.is_null(row) {
3975        return Ok(0);
3976    }
3977    match array.data_type() {
3978        DataType::UInt64 => Ok(array
3979            .as_any()
3980            .downcast_ref::<UInt64Array>()
3981            .expect("type checked")
3982            .value(row)),
3983        DataType::Int64 => {
3984            let value = array
3985                .as_any()
3986                .downcast_ref::<Int64Array>()
3987                .expect("type checked")
3988                .value(row);
3989            Ok((value as u64) ^ (1_u64 << 63))
3990        }
3991        DataType::Float64 => {
3992            let bits = array
3993                .as_any()
3994                .downcast_ref::<Float64Array>()
3995                .expect("type checked")
3996                .value(row)
3997                .to_bits();
3998            Ok(if bits & (1_u64 << 63) != 0 {
3999                !bits
4000            } else {
4001                bits | (1_u64 << 63)
4002            })
4003        }
4004        other => Err(DistributedError::Unsupported(format!(
4005            "top-k score column of type {other} is not supported in this wave"
4006        ))),
4007    }
4008}
4009
4010/// Reads the `__rowid` column of a top-k batch.
4011fn row_ids(batch: &RecordBatch) -> DistributedResult<UInt64Array> {
4012    let index = batch.schema().index_of(TOPK_ROWID_COLUMN).map_err(|_| {
4013        DistributedError::InvalidPlan(format!(
4014            "top-k stream is missing the `{TOPK_ROWID_COLUMN}` column"
4015        ))
4016    })?;
4017    let array = batch.column(index);
4018    if array.data_type() != &DataType::UInt64 {
4019        return Err(DistributedError::InvalidPlan(format!(
4020            "`{TOPK_ROWID_COLUMN}` must be UInt64, found {}",
4021            array.data_type()
4022        )));
4023    }
4024    Ok(array
4025        .as_any()
4026        .downcast_ref::<UInt64Array>()
4027        .expect("type checked")
4028        .clone())
4029}
4030
4031/// A top-k input fully prepared for bounded emission: the concatenated
4032/// payload plus the globally sorted candidate list.
4033struct TopKInput {
4034    batch: RecordBatch,
4035    /// Candidates, best first, aligned with `positions`.
4036    candidates: Vec<TopKCandidate>,
4037    /// Row index in `batch` of each candidate.
4038    positions: Vec<usize>,
4039}
4040
4041impl TopKInput {
4042    /// Emits candidates `[offset, offset + limit)`: the candidates
4043    /// themselves, their payload rows, and the tightened unseen bound
4044    /// (`None` when nothing remains).
4045    fn emit(&self, offset: usize, limit: usize) -> (Vec<TopKCandidate>, RecordBatch, Option<u64>) {
4046        let offset = offset.min(self.candidates.len());
4047        let end = (offset + limit).min(self.candidates.len());
4048        let rows = self.candidates[offset..end].to_vec();
4049        let positions = &self.positions[offset..end];
4050        let payload = take_rows(&self.batch, positions)
4051            .unwrap_or_else(|_| vec![RecordBatch::new_empty(self.batch.schema())])
4052            .into_iter()
4053            .next()
4054            .unwrap_or_else(|| RecordBatch::new_empty(self.batch.schema()));
4055        let bound = self.candidates.get(end).map(|candidate| candidate.score);
4056        (rows, payload, bound)
4057    }
4058}
4059
4060/// Builds the sorted candidate list of a top-k input.
4061fn prepare_top_k(
4062    batches: &[RecordBatch],
4063    score_column: &str,
4064    tablet: TabletId,
4065) -> DistributedResult<TopKInput> {
4066    let Some(batch) = concat_all(batches)? else {
4067        return Err(DistributedError::InvalidPlan(
4068            "top-k input has no batches".to_owned(),
4069        ));
4070    };
4071    let schema = batch.schema();
4072    let score_index = schema.index_of(score_column).map_err(|_| {
4073        DistributedError::InvalidPlan(format!("top-k score column `{score_column}` not in schema"))
4074    })?;
4075    let score_array = batch.column(score_index).clone();
4076    let row_id_array = row_ids(&batch)?;
4077    let mut candidates = Vec::with_capacity(batch.num_rows());
4078    for row in 0..batch.num_rows() {
4079        candidates.push(TopKCandidate {
4080            score: score_key(score_array.as_ref(), row)?,
4081            tablet,
4082            row_id: RowId(row_id_array.value(row)),
4083        });
4084    }
4085    let mut positions: Vec<usize> = (0..candidates.len()).collect();
4086    positions.sort_by(|left, right| topk_cmp(&candidates[*left], &candidates[*right]));
4087    let candidates = positions.iter().map(|index| candidates[*index]).collect();
4088    Ok(TopKInput {
4089        batch,
4090        candidates,
4091        positions,
4092    })
4093}
4094
4095// ---------------------------------------------------------------------------
4096// Aggregation combine (partial + final)
4097// ---------------------------------------------------------------------------
4098
4099/// One numeric cell (the groundwork supports Int64 and Float64 aggregate
4100/// inputs).
4101#[derive(Clone, Copy, Debug)]
4102enum AggValue {
4103    I64(i64),
4104    F64(f64),
4105}
4106
4107/// Reads one numeric cell (`None` for nulls).
4108fn numeric_cell(array: &dyn Array, row: usize) -> DistributedResult<Option<AggValue>> {
4109    if array.is_null(row) {
4110        return Ok(None);
4111    }
4112    match array.data_type() {
4113        DataType::Int64 => Ok(Some(AggValue::I64(
4114            array
4115                .as_any()
4116                .downcast_ref::<Int64Array>()
4117                .expect("type checked")
4118                .value(row),
4119        ))),
4120        DataType::Float64 => Ok(Some(AggValue::F64(
4121            array
4122                .as_any()
4123                .downcast_ref::<Float64Array>()
4124                .expect("type checked")
4125                .value(row),
4126        ))),
4127        other => Err(DistributedError::Unsupported(format!(
4128            "aggregate over {other} is not supported in this wave"
4129        ))),
4130    }
4131}
4132
4133/// Per-group accumulator for one aggregate expression.
4134#[derive(Clone, Debug)]
4135enum AggAccum {
4136    Count(i64),
4137    SumI(i128, bool),
4138    SumF(f64, bool),
4139    MinI(i64, bool),
4140    MinF(f64, bool),
4141    MaxI(i64, bool),
4142    MaxF(f64, bool),
4143    Avg { sum: f64, count: i64 },
4144}
4145
4146impl AggAccum {
4147    /// A fresh accumulator for `function` over a value column of type
4148    /// `float` (`true` = Float64, `false` = Int64).
4149    fn fresh(function: AggregateFunction, float: bool) -> Self {
4150        match function {
4151            AggregateFunction::Count => Self::Count(0),
4152            AggregateFunction::Sum if float => Self::SumF(0.0, false),
4153            AggregateFunction::Sum => Self::SumI(0, false),
4154            AggregateFunction::Min if float => Self::MinF(f64::INFINITY, false),
4155            AggregateFunction::Min => Self::MinI(i64::MAX, false),
4156            AggregateFunction::Max if float => Self::MaxF(f64::NEG_INFINITY, false),
4157            AggregateFunction::Max => Self::MaxI(i64::MIN, false),
4158            AggregateFunction::Avg => Self::Avg { sum: 0.0, count: 0 },
4159        }
4160    }
4161
4162    /// Folds one value cell (shared by partial folds and final combines).
4163    fn fold_value(&mut self, cell: Option<AggValue>) {
4164        match (self, cell) {
4165            (Self::SumI(sum, seen), Some(AggValue::I64(value))) => {
4166                *sum += i128::from(value);
4167                *seen = true;
4168            }
4169            (Self::SumF(sum, seen), Some(AggValue::F64(value))) => {
4170                *sum += value;
4171                *seen = true;
4172            }
4173            (Self::MinI(min, seen), Some(AggValue::I64(value))) => {
4174                *min = (*min).min(value);
4175                *seen = true;
4176            }
4177            (Self::MinF(min, seen), Some(AggValue::F64(value))) => {
4178                *min = min.min(value);
4179                *seen = true;
4180            }
4181            (Self::MaxI(max, seen), Some(AggValue::I64(value))) => {
4182                *max = (*max).max(value);
4183                *seen = true;
4184            }
4185            (Self::MaxF(max, seen), Some(AggValue::F64(value))) => {
4186                *max = max.max(value);
4187                *seen = true;
4188            }
4189            (Self::Avg { sum, count }, Some(AggValue::I64(value))) => {
4190                *sum += value as f64;
4191                *count += 1;
4192            }
4193            (Self::Avg { sum, count }, Some(AggValue::F64(value))) => {
4194                *sum += value;
4195                *count += 1;
4196            }
4197            _ => {}
4198        }
4199    }
4200}
4201
4202/// One folded group.
4203struct GroupEntry {
4204    /// Global row (in the concatenated input) whose key columns represent
4205    /// this group in the output.
4206    first_row: usize,
4207    accums: Vec<AggAccum>,
4208}
4209
4210/// Groups in first-seen order (deterministic for a fixed input order).
4211#[derive(Default)]
4212struct GroupFold {
4213    order: Vec<String>,
4214    groups: HashMap<String, GroupEntry>,
4215}
4216
4217impl GroupFold {
4218    fn entry(&mut self, key: String, row: usize, templates: &[AggAccum]) -> &mut GroupEntry {
4219        if !self.groups.contains_key(&key) {
4220            self.order.push(key.clone());
4221            self.groups.insert(
4222                key.clone(),
4223                GroupEntry {
4224                    first_row: row,
4225                    accums: templates.to_vec(),
4226                },
4227            );
4228        }
4229        self.groups.get_mut(&key).expect("inserted above")
4230    }
4231}
4232
4233/// The deterministic group key of one row (display form of the key columns).
4234fn group_key(batch: &RecordBatch, key_indexes: &[usize], row: usize) -> DistributedResult<String> {
4235    let mut parts = Vec::with_capacity(key_indexes.len());
4236    for index in key_indexes {
4237        parts.push(array_value_to_string(batch.column(*index).as_ref(), row)?);
4238    }
4239    Ok(parts.join("\u{1f}"))
4240}
4241
4242/// Resolves group-by column indexes.
4243fn resolve_columns(schema: &Schema, columns: &[String]) -> DistributedResult<Vec<usize>> {
4244    columns
4245        .iter()
4246        .map(|column| {
4247            schema.index_of(column).map_err(|_| {
4248                DistributedError::InvalidPlan(format!("group-by column `{column}` not in schema"))
4249            })
4250        })
4251        .collect()
4252}
4253
4254/// The value-column type marker of one aggregate (`true` = Float64).
4255fn aggregate_float(schema: &Schema, aggregate: &AggregateExpr) -> DistributedResult<bool> {
4256    match &aggregate.column {
4257        None => Ok(false),
4258        Some(column) => {
4259            let index = schema.index_of(column).map_err(|_| {
4260                DistributedError::InvalidPlan(format!("aggregate column `{column}` not in schema"))
4261            })?;
4262            match schema.field(index).data_type() {
4263                DataType::Int64 => Ok(false),
4264                DataType::Float64 => Ok(true),
4265                other => Err(DistributedError::Unsupported(format!(
4266                    "aggregate over {other} is not supported in this wave"
4267                ))),
4268            }
4269        }
4270    }
4271}
4272
4273/// Gathers group key columns at each group's first-seen row.
4274fn take_group_keys(
4275    batch: &RecordBatch,
4276    key_indexes: &[usize],
4277    fold: &GroupFold,
4278) -> DistributedResult<Vec<ArrayRef>> {
4279    if key_indexes.is_empty() {
4280        return Ok(Vec::new());
4281    }
4282    let indices = UInt32Array::from(
4283        fold.order
4284            .iter()
4285            .map(|key| u32::try_from(fold.groups[key].first_row).unwrap_or(u32::MAX))
4286            .collect::<Vec<u32>>(),
4287    );
4288    let mut columns = Vec::with_capacity(key_indexes.len());
4289    for index in key_indexes {
4290        columns.push(take(batch.column(*index), &indices, None)?);
4291    }
4292    Ok(columns)
4293}
4294
4295/// Emits one integer accumulator column (partial or final).
4296fn emit_int_accum(groups: &GroupFold, index: usize) -> DistributedResult<Vec<Option<i64>>> {
4297    let mut values = Vec::with_capacity(groups.order.len());
4298    for key in &groups.order {
4299        let accum = &groups.groups[key].accums[index];
4300        values.push(match accum {
4301            AggAccum::Count(count) => Some(*count),
4302            AggAccum::SumI(sum, seen) => seen
4303                .then(|| {
4304                    i64::try_from(*sum).map_err(|_| {
4305                        DistributedError::Unsupported(
4306                            "integer sum overflow in this wave".to_owned(),
4307                        )
4308                    })
4309                })
4310                .transpose()?,
4311            AggAccum::MinI(value, seen) | AggAccum::MaxI(value, seen) => seen.then_some(*value),
4312            other => {
4313                return Err(DistributedError::InvalidPlan(format!(
4314                    "internal: accumulator {other:?} is not an integer column"
4315                )))
4316            }
4317        });
4318    }
4319    Ok(values)
4320}
4321
4322/// Emits one float accumulator column (partial or final).
4323fn emit_float_accum(groups: &GroupFold, index: usize) -> DistributedResult<Vec<Option<f64>>> {
4324    let mut values = Vec::with_capacity(groups.order.len());
4325    for key in &groups.order {
4326        let accum = &groups.groups[key].accums[index];
4327        values.push(match accum {
4328            AggAccum::SumF(sum, seen) => seen.then_some(*sum),
4329            AggAccum::MinF(value, seen) | AggAccum::MaxF(value, seen) => seen.then_some(*value),
4330            other => {
4331                return Err(DistributedError::InvalidPlan(format!(
4332                    "internal: accumulator {other:?} is not a float column"
4333                )))
4334            }
4335        });
4336    }
4337    Ok(values)
4338}
4339
4340/// Per-tablet partial aggregation (the producer half of the two-phase
4341/// aggregate, spec section 12.10).
4342fn partial_aggregate_batches(
4343    batches: &[RecordBatch],
4344    group_by: &[String],
4345    aggregates: &[AggregateExpr],
4346) -> DistributedResult<RecordBatch> {
4347    let Some(batch) = concat_all(batches)? else {
4348        return Err(DistributedError::InvalidPlan(
4349            "aggregate input has no batches".to_owned(),
4350        ));
4351    };
4352    let schema = batch.schema();
4353    let key_indexes = resolve_columns(&schema, group_by)?;
4354    let mut value_indexes = Vec::with_capacity(aggregates.len());
4355    let mut templates = Vec::with_capacity(aggregates.len());
4356    for aggregate in aggregates {
4357        let float = aggregate_float(&schema, aggregate)?;
4358        value_indexes.push(match &aggregate.column {
4359            Some(column) => Some(schema.index_of(column).map_err(|_| {
4360                DistributedError::InvalidPlan(format!("aggregate column `{column}` not in schema"))
4361            })?),
4362            None => None,
4363        });
4364        templates.push(AggAccum::fresh(aggregate.function, float));
4365    }
4366    let mut fold = GroupFold::default();
4367    for row in 0..batch.num_rows() {
4368        let key = group_key(&batch, &key_indexes, row)?;
4369        let entry = fold.entry(key, row, &templates);
4370        for (index, aggregate) in aggregates.iter().enumerate() {
4371            match aggregate.function {
4372                AggregateFunction::Count => {
4373                    let counted = match value_indexes[index] {
4374                        Some(column) => !batch.column(column).is_null(row),
4375                        None => true,
4376                    };
4377                    if counted {
4378                        let AggAccum::Count(count) = &mut entry.accums[index] else {
4379                            unreachable!("count template");
4380                        };
4381                        *count += 1;
4382                    }
4383                }
4384                _ => {
4385                    let cell = match value_indexes[index] {
4386                        Some(column) => numeric_cell(batch.column(column).as_ref(), row)?,
4387                        None => None,
4388                    };
4389                    entry.accums[index].fold_value(cell);
4390                }
4391            }
4392        }
4393    }
4394    // Emit: key columns at first-seen rows, then partial columns.
4395    let mut columns = take_group_keys(&batch, &key_indexes, &fold)?;
4396    let mut fields: Vec<Field> = key_indexes
4397        .iter()
4398        .map(|index| schema.field(*index).clone())
4399        .collect();
4400    for (index, aggregate) in aggregates.iter().enumerate() {
4401        match aggregate.function {
4402            AggregateFunction::Avg => {
4403                let mut sums = Vec::with_capacity(fold.order.len());
4404                let mut counts = Vec::with_capacity(fold.order.len());
4405                for key in &fold.order {
4406                    let AggAccum::Avg { sum, count } = &fold.groups[key].accums[index] else {
4407                        unreachable!("avg template");
4408                    };
4409                    sums.push(Some(*sum));
4410                    counts.push(Some(*count));
4411                }
4412                fields.push(Field::new(
4413                    format!("__partial_{index}_sum"),
4414                    DataType::Float64,
4415                    true,
4416                ));
4417                columns.push(Arc::new(Float64Array::from(sums)));
4418                fields.push(Field::new(
4419                    format!("__partial_{index}_count"),
4420                    DataType::Int64,
4421                    true,
4422                ));
4423                columns.push(Arc::new(Int64Array::from(counts)));
4424            }
4425            AggregateFunction::Count => {
4426                fields.push(Field::new(
4427                    format!("__partial_{index}"),
4428                    DataType::Int64,
4429                    true,
4430                ));
4431                columns.push(Arc::new(Int64Array::from(emit_int_accum(&fold, index)?)));
4432            }
4433            _ => match &templates[index] {
4434                AggAccum::SumI(..) | AggAccum::MinI(..) | AggAccum::MaxI(..) => {
4435                    fields.push(Field::new(
4436                        format!("__partial_{index}"),
4437                        DataType::Int64,
4438                        true,
4439                    ));
4440                    columns.push(Arc::new(Int64Array::from(emit_int_accum(&fold, index)?)));
4441                }
4442                AggAccum::SumF(..) | AggAccum::MinF(..) | AggAccum::MaxF(..) => {
4443                    fields.push(Field::new(
4444                        format!("__partial_{index}"),
4445                        DataType::Float64,
4446                        true,
4447                    ));
4448                    columns.push(Arc::new(Float64Array::from(emit_float_accum(
4449                        &fold, index,
4450                    )?)));
4451                }
4452                other => {
4453                    return Err(DistributedError::InvalidPlan(format!(
4454                        "internal: unexpected accumulator {other:?}"
4455                    )))
4456                }
4457            },
4458        }
4459    }
4460    Ok(RecordBatch::try_new(Schema::new(fields).into(), columns)?)
4461}
4462
4463/// Coordinator-side combine of partial aggregates into final results (spec
4464/// section 12.10).
4465fn final_aggregate_batches(
4466    batches: &[RecordBatch],
4467    group_by: &[String],
4468    aggregates: &[AggregateExpr],
4469) -> DistributedResult<RecordBatch> {
4470    let Some(batch) = concat_all(batches)? else {
4471        return Err(DistributedError::InvalidPlan(
4472            "aggregate input has no batches".to_owned(),
4473        ));
4474    };
4475    let schema = batch.schema();
4476    let key_indexes = resolve_columns(&schema, group_by)?;
4477    // Resolve the partial columns produced by `partial_aggregate_batches`.
4478    let mut partial_indexes: Vec<(usize, Option<usize>)> = Vec::with_capacity(aggregates.len());
4479    let mut templates = Vec::with_capacity(aggregates.len());
4480    for (index, aggregate) in aggregates.iter().enumerate() {
4481        let value_name = if aggregate.function == AggregateFunction::Avg {
4482            format!("__partial_{index}_sum")
4483        } else {
4484            format!("__partial_{index}")
4485        };
4486        let value_index = schema.index_of(&value_name).map_err(|_| {
4487            DistributedError::InvalidPlan(format!(
4488                "partial aggregate column `{value_name}` not in schema"
4489            ))
4490        })?;
4491        let float = match schema.field(value_index).data_type() {
4492            DataType::Int64 => false,
4493            DataType::Float64 => true,
4494            other => {
4495                return Err(DistributedError::Unsupported(format!(
4496                    "aggregate partial of type {other} is not supported in this wave"
4497                )))
4498            }
4499        };
4500        templates.push(AggAccum::fresh(aggregate.function, float));
4501        let count_index = if aggregate.function == AggregateFunction::Avg {
4502            let count_name = format!("__partial_{index}_count");
4503            Some(schema.index_of(&count_name).map_err(|_| {
4504                DistributedError::InvalidPlan(format!(
4505                    "partial aggregate column `{count_name}` not in schema"
4506                ))
4507            })?)
4508        } else {
4509            None
4510        };
4511        partial_indexes.push((value_index, count_index));
4512    }
4513    let mut fold = GroupFold::default();
4514    for row in 0..batch.num_rows() {
4515        let key = group_key(&batch, &key_indexes, row)?;
4516        let entry = fold.entry(key, row, &templates);
4517        for (index, aggregate) in aggregates.iter().enumerate() {
4518            let (value_index, count_index) = partial_indexes[index];
4519            match aggregate.function {
4520                AggregateFunction::Count => {
4521                    if let Some(AggValue::I64(value)) =
4522                        numeric_cell(batch.column(value_index).as_ref(), row)?
4523                    {
4524                        let AggAccum::Count(count) = &mut entry.accums[index] else {
4525                            unreachable!("count template");
4526                        };
4527                        *count += value;
4528                    }
4529                }
4530                AggregateFunction::Avg => {
4531                    let sum = numeric_cell(batch.column(value_index).as_ref(), row)?;
4532                    let count = match count_index {
4533                        Some(count_index) => numeric_cell(batch.column(count_index).as_ref(), row)?,
4534                        None => None,
4535                    };
4536                    let AggAccum::Avg {
4537                        sum: total,
4538                        count: rows,
4539                    } = &mut entry.accums[index]
4540                    else {
4541                        unreachable!("avg template");
4542                    };
4543                    match sum {
4544                        Some(AggValue::F64(value)) => *total += value,
4545                        Some(AggValue::I64(value)) => *total += value as f64,
4546                        None => {}
4547                    }
4548                    if let Some(AggValue::I64(value)) = count {
4549                        *rows += value;
4550                    }
4551                }
4552                _ => {
4553                    let cell = numeric_cell(batch.column(value_index).as_ref(), row)?;
4554                    entry.accums[index].fold_value(cell);
4555                }
4556            }
4557        }
4558    }
4559    // SQL semantics: an empty input with no group-by still yields one row.
4560    if fold.order.is_empty() && group_by.is_empty() {
4561        fold.entry(String::new(), 0, &templates);
4562    }
4563    // Emit: key columns at first-seen rows, then one column per aggregate.
4564    let mut columns = take_group_keys(&batch, &key_indexes, &fold)?;
4565    let mut fields: Vec<Field> = key_indexes
4566        .iter()
4567        .map(|index| schema.field(*index).clone())
4568        .collect();
4569    for (index, aggregate) in aggregates.iter().enumerate() {
4570        let name = aggregate_output_name(aggregate);
4571        match &templates[index] {
4572            AggAccum::Count(_) | AggAccum::SumI(..) | AggAccum::MinI(..) | AggAccum::MaxI(..) => {
4573                fields.push(Field::new(name, DataType::Int64, true));
4574                columns.push(Arc::new(Int64Array::from(emit_int_accum(&fold, index)?)));
4575            }
4576            AggAccum::SumF(..) | AggAccum::MinF(..) | AggAccum::MaxF(..) => {
4577                fields.push(Field::new(name, DataType::Float64, true));
4578                columns.push(Arc::new(Float64Array::from(emit_float_accum(
4579                    &fold, index,
4580                )?)));
4581            }
4582            AggAccum::Avg { .. } => {
4583                let values: Vec<Option<f64>> = fold
4584                    .order
4585                    .iter()
4586                    .map(|key| match &fold.groups[key].accums[index] {
4587                        AggAccum::Avg { sum, count } => {
4588                            (*count > 0).then_some(*sum / *count as f64)
4589                        }
4590                        _ => unreachable!("avg template"),
4591                    })
4592                    .collect();
4593                fields.push(Field::new(name, DataType::Float64, true));
4594                columns.push(Arc::new(Float64Array::from(values)));
4595            }
4596        }
4597    }
4598    Ok(RecordBatch::try_new(Schema::new(fields).into(), columns)?)
4599}
4600
4601// ---------------------------------------------------------------------------
4602// Coordinator runtime (spec section 12.10)
4603// ---------------------------------------------------------------------------
4604
4605/// Per-query fragment resource ledger. Reservations are RAII: dropping a
4606/// [`ResourcePermit`] releases its share.
4607#[derive(Debug)]
4608pub struct ResourceLedger {
4609    state: Mutex<LedgerState>,
4610    max_fragments: usize,
4611    max_bytes: u64,
4612}
4613
4614#[derive(Default, Debug)]
4615struct LedgerState {
4616    fragments: usize,
4617    bytes: u64,
4618}
4619
4620/// A held resource reservation; released on drop.
4621#[derive(Debug)]
4622pub struct ResourcePermit {
4623    ledger: Arc<ResourceLedger>,
4624    bytes: u64,
4625}
4626
4627impl Drop for ResourcePermit {
4628    fn drop(&mut self) {
4629        let mut state = self.ledger.state.lock();
4630        state.fragments = state.fragments.saturating_sub(1);
4631        state.bytes = state.bytes.saturating_sub(self.bytes);
4632    }
4633}
4634
4635impl ResourceLedger {
4636    /// A ledger admitting at most `max_fragments` concurrent fragments and
4637    /// `max_bytes` total estimated bytes.
4638    pub fn new(max_fragments: usize, max_bytes: u64) -> Self {
4639        Self {
4640            state: Mutex::new(LedgerState::default()),
4641            max_fragments,
4642            max_bytes,
4643        }
4644    }
4645
4646    /// Reserves one fragment's estimated resources (spec section 12.10:
4647    /// "workers reserve resources").
4648    pub fn reserve(self: &Arc<Self>, fragment: &PlanFragment) -> DistributedResult<ResourcePermit> {
4649        let mut state = self.state.lock();
4650        if state.fragments >= self.max_fragments {
4651            return Err(DistributedError::Reservation {
4652                fragment_id: fragment.fragment_id,
4653                reason: format!("fragment concurrency limit {} reached", self.max_fragments),
4654            });
4655        }
4656        if state.bytes.saturating_add(fragment.estimated_bytes) > self.max_bytes {
4657            return Err(DistributedError::Reservation {
4658                fragment_id: fragment.fragment_id,
4659                reason: format!(
4660                    "estimated bytes {} exceed the {} byte budget",
4661                    state.bytes.saturating_add(fragment.estimated_bytes),
4662                    self.max_bytes
4663                ),
4664            });
4665        }
4666        state.fragments += 1;
4667        state.bytes = state.bytes.saturating_add(fragment.estimated_bytes);
4668        Ok(ResourcePermit {
4669            ledger: Arc::clone(self),
4670            bytes: fragment.estimated_bytes,
4671        })
4672    }
4673
4674    /// Currently reserved fragment count.
4675    pub fn reserved_fragments(&self) -> usize {
4676        self.state.lock().fragments
4677    }
4678
4679    /// Currently reserved estimated bytes.
4680    pub fn reserved_bytes(&self) -> u64 {
4681        self.state.lock().bytes
4682    }
4683}
4684
4685/// Worker lease ledger (spec section 12.10: "worker lease expiry cleans
4686/// abandoned fragments"). Workers are keyed by the tablet whose data they
4687/// serve this wave; the node-level binding lands with the transport wave.
4688#[derive(Default)]
4689pub struct LeaseLedger {
4690    leases: Mutex<HashMap<TabletId, Instant>>,
4691}
4692
4693impl LeaseLedger {
4694    /// Renews a worker's lease to `expiry`.
4695    pub fn renew(&self, worker: TabletId, expiry: Instant) {
4696        self.leases.lock().insert(worker, expiry);
4697    }
4698
4699    /// A worker's current lease expiry.
4700    pub fn expiry(&self, worker: &TabletId) -> Option<Instant> {
4701        self.leases.lock().get(worker).copied()
4702    }
4703
4704    /// Removes and returns every worker whose lease expired at or before
4705    /// `now`.
4706    pub fn sweep(&self, now: Instant) -> Vec<TabletId> {
4707        let mut leases = self.leases.lock();
4708        let expired: Vec<TabletId> = leases
4709            .iter()
4710            .filter(|(_, expiry)| **expiry <= now)
4711            .map(|(worker, _)| *worker)
4712            .collect();
4713        for worker in &expired {
4714            leases.remove(worker);
4715        }
4716        expired
4717    }
4718}
4719
4720/// One in-flight fragment of a running query.
4721struct InFlight {
4722    worker: Option<TabletId>,
4723    control: ExecutionControl,
4724    _permit: ResourcePermit,
4725}
4726
4727/// Per-query execution state tracked by the coordinator.
4728struct ExecutionState {
4729    query_id: QueryId,
4730    in_flight: Mutex<HashMap<FragmentId, InFlight>>,
4731}
4732
4733impl ExecutionState {
4734    fn new(query_id: QueryId) -> Self {
4735        Self {
4736            query_id,
4737            in_flight: Mutex::new(HashMap::new()),
4738        }
4739    }
4740}
4741
4742/// One producer stream feeding a coordinator (root) fragment.
4743struct ProducerInput {
4744    fragment_id: FragmentId,
4745    tablet: Option<TabletId>,
4746    frames: Vec<BatchFrame>,
4747}
4748
4749/// The root fragment's working data: either per-producer streams (fresh from
4750/// the exchange edges) or already-combined batches (after a coordinator
4751/// operator ran).
4752enum RootData {
4753    Streams(Vec<ProducerInput>),
4754    Batches(Vec<RecordBatch>),
4755}
4756
4757impl RootData {
4758    /// All payload batches, in stream order.
4759    fn flatten(&self) -> Vec<RecordBatch> {
4760        match self {
4761            Self::Streams(inputs) => inputs
4762                .iter()
4763                .flat_map(|input| input.frames.iter().map(|frame| frame.batch.clone()))
4764                .collect(),
4765            Self::Batches(batches) => batches.clone(),
4766        }
4767    }
4768}
4769
4770/// The query coordinator (spec section 12.10): registers the query with the
4771/// existing [`SqlQueryRegistry`] (so `registry.cancel(...)` reaches every
4772/// fragment through the [`ExecutionControl`] hierarchy), reserves resources
4773/// per fragment, fans out cancellation to every fragment, sweeps expired
4774/// worker leases to clean abandoned fragments, and merges producer streams
4775/// per the exchange descriptors with the real in-memory operators.
4776pub struct Coordinator {
4777    transport: Arc<dyn FragmentTransport>,
4778    registry: Arc<SqlQueryRegistry>,
4779    resources: Arc<ResourceLedger>,
4780    leases: LeaseLedger,
4781    lease_ttl: Duration,
4782    executions: Mutex<HashMap<QueryId, Arc<ExecutionState>>>,
4783}
4784
4785impl Coordinator {
4786    /// A coordinator with default limits (1024 concurrent fragments, 16 GiB
4787    /// of estimated bytes, 30 second worker leases).
4788    pub fn new(transport: Arc<dyn FragmentTransport>, registry: Arc<SqlQueryRegistry>) -> Self {
4789        Self::with_limits(
4790            transport,
4791            registry,
4792            1_024,
4793            16 * 1024 * 1024 * 1024,
4794            Duration::from_secs(30),
4795        )
4796    }
4797
4798    /// A coordinator with explicit resource limits and lease TTL.
4799    pub fn with_limits(
4800        transport: Arc<dyn FragmentTransport>,
4801        registry: Arc<SqlQueryRegistry>,
4802        max_fragments: usize,
4803        max_bytes: u64,
4804        lease_ttl: Duration,
4805    ) -> Self {
4806        Self {
4807            transport,
4808            registry,
4809            resources: Arc::new(ResourceLedger::new(max_fragments, max_bytes)),
4810            leases: LeaseLedger::default(),
4811            lease_ttl,
4812            executions: Mutex::new(HashMap::new()),
4813        }
4814    }
4815
4816    /// The fragment resource ledger (test introspection).
4817    pub fn resources(&self) -> &Arc<ResourceLedger> {
4818        &self.resources
4819    }
4820
4821    /// Executes a distributed plan to completion, returning the root
4822    /// fragment's output batches.
4823    ///
4824    /// The plan's [`QueryId`] is registered with the query registry first;
4825    /// every fragment runs under a child [`ExecutionControl`] of that
4826    /// registration, so a registry-level cancel fans out to all fragments.
4827    /// Fragments execute layer by layer (producers before consumers),
4828    /// concurrently within a layer. The coordinator-local root fragment's
4829    /// merge operators ([`FragmentOperator::MergeSort`],
4830    /// [`FragmentOperator::FinalAggregate`],
4831    /// [`FragmentOperator::DistributedTopK`],
4832    /// [`FragmentOperator::DistributedLimit`]) run over the producer streams
4833    /// for real.
4834    pub async fn execute(&self, plan: &DistributedPlan) -> DistributedResult<Vec<RecordBatch>> {
4835        self.execute_with_authorization(plan, &[]).await
4836    }
4837
4838    /// Executes a plan while forwarding a bounded, server-issued
4839    /// authorization envelope to every tablet worker.
4840    pub async fn execute_with_authorization(
4841        &self,
4842        plan: &DistributedPlan,
4843        authorization_context: &[u8],
4844    ) -> DistributedResult<Vec<RecordBatch>> {
4845        if authorization_context.len() > MAX_FRAGMENT_AUTHORIZATION_CONTEXT_BYTES {
4846            return Err(DistributedError::InvalidPlan(format!(
4847                "fragment authorization context is {} bytes; limit is {}",
4848                authorization_context.len(),
4849                MAX_FRAGMENT_AUTHORIZATION_CONTEXT_BYTES
4850            )));
4851        }
4852        let registry_id = to_registry_query_id(&plan.query_id)?;
4853        let registered = self
4854            .registry
4855            .register(SqlQueryOptions {
4856                query_id: Some(registry_id),
4857                ..Default::default()
4858            })
4859            .map_err(|error| {
4860                DistributedError::InvalidPlan(format!("query registration failed: {error}"))
4861            })?;
4862        let result = self
4863            .execute_registered(plan, &registered, authorization_context.into())
4864            .await;
4865        match &result {
4866            Ok(_) => {
4867                let _ = registered.try_complete();
4868            }
4869            Err(_) => registered.fail(),
4870        }
4871        result
4872    }
4873
4874    /// Cancels a running distributed query: registry cancel (which reaches
4875    /// every fragment control through the parent-child hierarchy) plus an
4876    /// explicit transport cancel per in-flight fragment (spec section 12.10:
4877    /// "cancellation fans out to every fragment"). Returns true when the
4878    /// registry accepted (or already observed) the cancellation.
4879    pub fn cancel_query(&self, query_id: &QueryId) -> DistributedResult<bool> {
4880        let outcome = self.registry.cancel(to_registry_query_id(query_id)?);
4881        let state = self.executions.lock().get(query_id).cloned();
4882        if let Some(state) = state {
4883            let in_flight = state.in_flight.lock();
4884            for (fragment_id, inflight) in in_flight.iter() {
4885                inflight.control.cancel(CancellationReason::ClientRequest);
4886                let _ = self.transport.cancel_fragment(*query_id, *fragment_id);
4887            }
4888        }
4889        Ok(matches!(
4890            outcome,
4891            CancelOutcome::Accepted | CancelOutcome::AlreadyCancelling
4892        ))
4893    }
4894
4895    /// Sweeps expired worker leases and cleans their abandoned in-flight
4896    /// fragments: cancels the fragment control (as
4897    /// [`CancellationReason::ServerShutdown`]), notifies the transport, and
4898    /// releases the resource reservation (spec section 12.10: "worker lease
4899    /// expiry cleans abandoned fragments"). Returns the number of fragments
4900    /// cleaned.
4901    pub fn sweep_expired_leases(&self, now: Instant) -> usize {
4902        let expired = self.leases.sweep(now);
4903        if expired.is_empty() {
4904            return 0;
4905        }
4906        let states: Vec<Arc<ExecutionState>> = self.executions.lock().values().cloned().collect();
4907        let mut cleaned = 0;
4908        for state in states {
4909            let victims: Vec<FragmentId> = {
4910                let in_flight = state.in_flight.lock();
4911                in_flight
4912                    .iter()
4913                    .filter(|(_, inflight)| {
4914                        inflight
4915                            .worker
4916                            .is_some_and(|worker| expired.contains(&worker))
4917                    })
4918                    .map(|(fragment_id, _)| *fragment_id)
4919                    .collect()
4920            };
4921            for fragment_id in victims {
4922                let inflight = state.in_flight.lock().remove(&fragment_id);
4923                if let Some(inflight) = inflight {
4924                    inflight.control.cancel(CancellationReason::ServerShutdown);
4925                    drop(inflight);
4926                    let _ = self.transport.cancel_fragment(state.query_id, fragment_id);
4927                    cleaned += 1;
4928                }
4929            }
4930        }
4931        cleaned
4932    }
4933
4934    async fn execute_registered(
4935        &self,
4936        plan: &DistributedPlan,
4937        registered: &RegisteredSqlQuery,
4938        authorization_context: Arc<[u8]>,
4939    ) -> DistributedResult<Vec<RecordBatch>> {
4940        validate_plan_shape(plan)?;
4941        let root = plan
4942            .root_fragment_id()
4943            .ok_or_else(|| DistributedError::InvalidPlan("plan has no root fragment".to_owned()))?;
4944        let state = Arc::new(ExecutionState::new(plan.query_id));
4945        self.executions
4946            .lock()
4947            .insert(plan.query_id, Arc::clone(&state));
4948        let result = self
4949            .run_plan(plan, root, registered, &state, authorization_context)
4950            .await;
4951        self.executions.lock().remove(&plan.query_id);
4952        result
4953    }
4954
4955    async fn run_plan(
4956        &self,
4957        plan: &DistributedPlan,
4958        root: FragmentId,
4959        registered: &RegisteredSqlQuery,
4960        state: &Arc<ExecutionState>,
4961        authorization_context: Arc<[u8]>,
4962    ) -> DistributedResult<Vec<RecordBatch>> {
4963        let parent = registered.control().clone();
4964        let layers = fragment_layers(plan, root)?;
4965        let mut outputs: HashMap<FragmentId, Vec<BatchFrame>> = HashMap::new();
4966        for layer in &layers {
4967            checkpoint(&parent)?;
4968            let now = Instant::now();
4969            for &fragment_id in layer {
4970                if let FragmentAssignment::Tablet(tablet) =
4971                    plan.fragments[fragment_id as usize].assignment
4972                {
4973                    self.leases.renew(tablet, now + self.lease_ttl);
4974                }
4975            }
4976            let mut tasks = FuturesUnordered::new();
4977            let mut spawn_error: Option<DistributedError> = None;
4978            for &fragment_id in layer {
4979                let fragment = &plan.fragments[fragment_id as usize];
4980                let reserved = match self.resources.reserve(fragment) {
4981                    Ok(permit) => permit,
4982                    Err(error) => {
4983                        spawn_error = Some(error);
4984                        break;
4985                    }
4986                };
4987                let inputs = match build_inputs(plan, fragment_id, &outputs) {
4988                    Ok(inputs) => inputs,
4989                    Err(error) => {
4990                        spawn_error = Some(error);
4991                        break;
4992                    }
4993                };
4994                let control = parent.child_with_deadline(None);
4995                let worker = match fragment.assignment {
4996                    FragmentAssignment::Tablet(tablet) => Some(tablet),
4997                    FragmentAssignment::Coordinator => None,
4998                };
4999                state.in_flight.lock().insert(
5000                    fragment_id,
5001                    InFlight {
5002                        worker,
5003                        control: control.clone(),
5004                        _permit: reserved,
5005                    },
5006                );
5007                let fragment = fragment.clone();
5008                let fragment_control = FragmentControl {
5009                    control,
5010                    max_spill_bytes: fragment.max_spill_bytes,
5011                    authorization_context: Arc::clone(&authorization_context),
5012                };
5013                let transport = Arc::clone(&self.transport);
5014                tasks.push(async move {
5015                    let worker = worker_label(&fragment);
5016                    let drain_control = fragment_control.control.clone();
5017                    let result = async {
5018                        let stream = transport
5019                            .execute_fragment(plan.query_id, &fragment, inputs, fragment_control)
5020                            .await?;
5021                        drain_stream(stream, &drain_control).await
5022                    }
5023                    .await;
5024                    (fragment.fragment_id, worker, result)
5025                });
5026            }
5027            if let Some(error) = spawn_error {
5028                self.abort(plan, state);
5029                while let Some((fragment_id, _, _)) = tasks.next().await {
5030                    state.in_flight.lock().remove(&fragment_id);
5031                }
5032                return Err(error);
5033            }
5034            let mut failure: Option<DistributedError> = None;
5035            while let Some((fragment_id, worker, result)) = tasks.next().await {
5036                state.in_flight.lock().remove(&fragment_id);
5037                match result {
5038                    Ok(frames) => {
5039                        outputs.insert(fragment_id, frames);
5040                    }
5041                    Err(error) => {
5042                        if failure.is_none() {
5043                            failure = Some(match error {
5044                                DistributedError::Cancelled(reason) => {
5045                                    DistributedError::Cancelled(reason)
5046                                }
5047                                other => DistributedError::FragmentExecution {
5048                                    fragment_id,
5049                                    worker,
5050                                    message: other.to_string(),
5051                                },
5052                            });
5053                        }
5054                    }
5055                }
5056            }
5057            if let Some(error) = failure {
5058                self.abort(plan, state);
5059                return Err(error);
5060            }
5061        }
5062        checkpoint(&parent)?;
5063        self.run_root(plan, root, &outputs, &parent, authorization_context)
5064            .await
5065    }
5066
5067    /// Cancels every in-flight fragment control and notifies the transport
5068    /// for every fragment (the abort path's cancellation fan-out).
5069    fn abort(&self, plan: &DistributedPlan, state: &Arc<ExecutionState>) {
5070        {
5071            let in_flight = state.in_flight.lock();
5072            for inflight in in_flight.values() {
5073                inflight.control.cancel(CancellationReason::ClientRequest);
5074            }
5075        }
5076        for fragment in &plan.fragments {
5077            let _ = self
5078                .transport
5079                .cancel_fragment(plan.query_id, fragment.fragment_id);
5080        }
5081    }
5082
5083    /// Runs the coordinator-local root fragment over the producer outputs.
5084    async fn run_root(
5085        &self,
5086        plan: &DistributedPlan,
5087        root: FragmentId,
5088        outputs: &HashMap<FragmentId, Vec<BatchFrame>>,
5089        parent: &ExecutionControl,
5090        authorization_context: Arc<[u8]>,
5091    ) -> DistributedResult<Vec<RecordBatch>> {
5092        let fragment = &plan.fragments[root as usize];
5093        let mut inputs: Vec<ProducerInput> = Vec::new();
5094        for operator in &fragment.operators {
5095            if let FragmentOperator::RemoteExchangeSource { exchange } = operator {
5096                let edge = plan.exchanges.get(*exchange as usize).ok_or_else(|| {
5097                    DistributedError::InvalidPlan(format!("unknown exchange {exchange}"))
5098                })?;
5099                let producer = plan.fragments.get(edge.producer as usize).ok_or_else(|| {
5100                    DistributedError::InvalidPlan(format!(
5101                        "unknown producer fragment {}",
5102                        edge.producer
5103                    ))
5104                })?;
5105                let tablet = match producer.assignment {
5106                    FragmentAssignment::Tablet(tablet) => Some(tablet),
5107                    FragmentAssignment::Coordinator => None,
5108                };
5109                let frames = outputs.get(&edge.producer).cloned().ok_or_else(|| {
5110                    DistributedError::InvalidPlan(format!(
5111                        "missing output of producer fragment {}",
5112                        edge.producer
5113                    ))
5114                })?;
5115                let frames = route_frames(plan, edge, frames)?;
5116                inputs.push(ProducerInput {
5117                    fragment_id: edge.producer,
5118                    tablet,
5119                    frames,
5120                });
5121            }
5122        }
5123        let mut current = RootData::Streams(inputs);
5124        for operator in &fragment.operators {
5125            checkpoint(parent)?;
5126            match operator {
5127                FragmentOperator::RemoteExchangeSource { .. } => {}
5128                FragmentOperator::FinalAggregate {
5129                    group_by,
5130                    aggregates,
5131                } => {
5132                    let batches = current.flatten();
5133                    current = RootData::Batches(vec![final_aggregate_batches(
5134                        &batches, group_by, aggregates,
5135                    )?]);
5136                }
5137                FragmentOperator::MergeSort { keys, limit } => {
5138                    current = RootData::Batches(match &current {
5139                        RootData::Streams(_) => {
5140                            let streams = per_stream_batches(&current);
5141                            merge_sorted_streams(&streams, keys, *limit)?
5142                        }
5143                        RootData::Batches(batches) => sort_batches_local(batches, keys, *limit)?,
5144                    });
5145                }
5146                FragmentOperator::DistributedTopK { k, score } => {
5147                    current = RootData::Batches(
5148                        self.coordinator_top_k(
5149                            plan,
5150                            &current,
5151                            *k,
5152                            score,
5153                            parent,
5154                            &authorization_context,
5155                        )
5156                        .await?,
5157                    );
5158                }
5159                FragmentOperator::DistributedLimit { limit } => {
5160                    let batches = current.flatten();
5161                    current = RootData::Batches(limit_batches(&batches, *limit));
5162                }
5163                other => {
5164                    return Err(DistributedError::Unsupported(format!(
5165                    "root fragment operator {other:?} is not coordinator-executable in this wave"
5166                )))
5167                }
5168            }
5169        }
5170        Ok(current.flatten())
5171    }
5172
5173    /// The coordinator-side deterministic top-k merge with adaptive refill
5174    /// (spec section 12.10): merges every producer's bounded local top-k
5175    /// under the exact tie-break, refilling tablets whose unseen-score bound
5176    /// could still contribute winners through the transport.
5177    async fn coordinator_top_k(
5178        &self,
5179        plan: &DistributedPlan,
5180        data: &RootData,
5181        k: usize,
5182        score: &SortKey,
5183        control: &ExecutionControl,
5184        authorization_context: &[u8],
5185    ) -> DistributedResult<Vec<RecordBatch>> {
5186        // A pre-combined input (chained after another coordinator operator)
5187        // is a single synthetic stream that never refills.
5188        let synthetic;
5189        let inputs: &[ProducerInput] = match data {
5190            RootData::Streams(inputs) => inputs,
5191            RootData::Batches(batches) => {
5192                synthetic = ProducerInput {
5193                    fragment_id: u32::MAX,
5194                    tablet: Some(TabletId::ZERO),
5195                    frames: batches.iter().cloned().map(BatchFrame::data).collect(),
5196                };
5197                std::slice::from_ref(&synthetic)
5198            }
5199        };
5200        let mut all_batches: Vec<RecordBatch> = Vec::new();
5201        let mut batch_tablet: Vec<TabletId> = Vec::new();
5202        let mut shards: BTreeMap<TabletId, TabletTopK> = BTreeMap::new();
5203        let mut fragment_of: HashMap<TabletId, FragmentId> = HashMap::new();
5204        for input in inputs {
5205            let tablet = input.tablet.ok_or_else(|| {
5206                DistributedError::InvalidPlan(
5207                    "distributed top-k inputs must be tablet fragments".to_owned(),
5208                )
5209            })?;
5210            fragment_of.insert(tablet, input.fragment_id);
5211            let mut rows = Vec::new();
5212            let mut bound = None;
5213            for frame in &input.frames {
5214                let batch = &frame.batch;
5215                if batch.num_rows() > 0 {
5216                    let score_index = batch.schema().index_of(&score.column).map_err(|_| {
5217                        DistributedError::InvalidPlan(format!(
5218                            "top-k score column `{}` not in schema",
5219                            score.column
5220                        ))
5221                    })?;
5222                    let score_array = batch.column(score_index).clone();
5223                    let row_id_array = row_ids(batch)?;
5224                    for row in 0..batch.num_rows() {
5225                        rows.push(TopKCandidate {
5226                            score: score_key(score_array.as_ref(), row)?,
5227                            tablet,
5228                            row_id: RowId(row_id_array.value(row)),
5229                        });
5230                    }
5231                }
5232                batch_tablet.push(tablet);
5233                all_batches.push(batch.clone());
5234                match frame.score_bound {
5235                    ScoreBound::AtMost(next) => bound = Some(next),
5236                    ScoreBound::Exhausted => bound = None,
5237                    // No bound reporting: treated as exhausted (documented
5238                    // producer contract).
5239                    ScoreBound::Unknown => {}
5240                }
5241            }
5242            shards.insert(
5243                tablet,
5244                TabletTopK {
5245                    tablet,
5246                    rows,
5247                    unseen_bound: bound,
5248                },
5249            );
5250        }
5251        let winners = loop {
5252            let ordered: Vec<TabletTopK> = shards.values().cloned().collect();
5253            let merge = merge_top_k(&ordered, k);
5254            if merge.refill.is_empty() {
5255                break merge.winners;
5256            }
5257            for tablet in merge.refill {
5258                let fragment_id = fragment_of[&tablet];
5259                let already = shards[&tablet].rows.len();
5260                let refill = self
5261                    .transport
5262                    .refill_top_k(
5263                        plan.query_id,
5264                        &plan.fragments[fragment_id as usize],
5265                        already,
5266                        k,
5267                        FragmentControl {
5268                            control: control.child_with_deadline(None),
5269                            max_spill_bytes: plan.fragments[fragment_id as usize].max_spill_bytes,
5270                            authorization_context: authorization_context.into(),
5271                        },
5272                    )
5273                    .await?;
5274                let entry = shards.get_mut(&tablet).expect("shard exists");
5275                if refill.rows.is_empty() && refill.unseen_bound == entry.unseen_bound {
5276                    return Err(DistributedError::InvalidPlan(format!(
5277                        "top-k refill for fragment {fragment_id} made no progress"
5278                    )));
5279                }
5280                batch_tablet.push(tablet);
5281                all_batches.push(refill.payload);
5282                entry.rows.extend(refill.rows);
5283                entry.unseen_bound = refill.unseen_bound;
5284            }
5285        };
5286        if winners.is_empty() {
5287            return Ok(Vec::new());
5288        }
5289        // Locate every winner's payload row.
5290        let mut locations: HashMap<(TabletId, u64), (usize, usize)> = HashMap::new();
5291        for (batch_index, batch) in all_batches.iter().enumerate() {
5292            if batch.num_rows() == 0 {
5293                continue;
5294            }
5295            let row_id_array = row_ids(batch)?;
5296            let tablet = batch_tablet[batch_index];
5297            for row in 0..batch.num_rows() {
5298                locations.insert((tablet, row_id_array.value(row)), (batch_index, row));
5299            }
5300        }
5301        let mut order = Vec::with_capacity(winners.len());
5302        for winner in &winners {
5303            let location = locations
5304                .get(&(winner.tablet, winner.row_id.0))
5305                .ok_or_else(|| {
5306                    DistributedError::InvalidPlan(format!(
5307                        "top-k winner {:?} has no payload row",
5308                        winner.row_id
5309                    ))
5310                })?;
5311            order.push(*location);
5312        }
5313        let merged = emit_interleaved(&all_batches, &order)?;
5314        // Strip the internal row-id column from the result.
5315        let schema = merged.first().map(|batch| batch.schema()).ok_or_else(|| {
5316            DistributedError::InvalidPlan("top-k merge produced no output".to_owned())
5317        })?;
5318        let keep: Vec<usize> = schema
5319            .fields()
5320            .iter()
5321            .enumerate()
5322            .filter(|(_, field)| field.name() != TOPK_ROWID_COLUMN)
5323            .map(|(index, _)| index)
5324            .collect();
5325        if keep.len() == schema.fields().len() {
5326            return Ok(merged);
5327        }
5328        merged
5329            .iter()
5330            .map(|batch| Ok(batch.project(&keep)?))
5331            .collect()
5332    }
5333}
5334
5335/// One stream's concatenated payload batches (for the k-way merge).
5336fn per_stream_batches(data: &RootData) -> Vec<RecordBatch> {
5337    match data {
5338        RootData::Streams(inputs) => inputs
5339            .iter()
5340            .filter_map(|input| {
5341                let batches: Vec<RecordBatch> = input
5342                    .frames
5343                    .iter()
5344                    .map(|frame| frame.batch.clone())
5345                    .collect();
5346                concat_all(&batches).ok().flatten()
5347            })
5348            .collect(),
5349        RootData::Batches(batches) => batches.clone(),
5350    }
5351}
5352
5353/// A short worker label for fragment errors.
5354fn worker_label(fragment: &PlanFragment) -> String {
5355    match fragment.assignment {
5356        FragmentAssignment::Tablet(tablet) => format!("tablet {tablet}"),
5357        FragmentAssignment::Coordinator => "coordinator".to_owned(),
5358    }
5359}
5360
5361/// Validates the structural invariants execution relies on.
5362fn validate_plan_shape(plan: &DistributedPlan) -> DistributedResult<()> {
5363    if plan.fragments.is_empty() {
5364        return Err(DistributedError::InvalidPlan(
5365            "plan has no fragments".to_owned(),
5366        ));
5367    }
5368    for (index, fragment) in plan.fragments.iter().enumerate() {
5369        if fragment.fragment_id as usize != index {
5370            return Err(DistributedError::InvalidPlan(
5371                "fragment ids must equal their vector index".to_owned(),
5372            ));
5373        }
5374    }
5375    for edge in &plan.exchanges {
5376        if edge.producer as usize >= plan.fragments.len()
5377            || edge.consumer as usize >= plan.fragments.len()
5378        {
5379            return Err(DistributedError::InvalidPlan(format!(
5380                "exchange {} references a missing fragment",
5381                edge.exchange_id
5382            )));
5383        }
5384    }
5385    let roots = plan
5386        .fragments
5387        .iter()
5388        .filter(|fragment| {
5389            !plan
5390                .exchanges
5391                .iter()
5392                .any(|edge| edge.producer == fragment.fragment_id)
5393        })
5394        .count();
5395    if roots != 1 {
5396        return Err(DistributedError::InvalidPlan(format!(
5397            "plan must have exactly one root fragment, found {roots}"
5398        )));
5399    }
5400    if let Some(root) = plan.root_fragment_id() {
5401        for fragment in &plan.fragments {
5402            if fragment.fragment_id != root
5403                && fragment.assignment == FragmentAssignment::Coordinator
5404            {
5405                return Err(DistributedError::InvalidPlan(
5406                    "only the root fragment may be coordinator-assigned".to_owned(),
5407                ));
5408            }
5409        }
5410    }
5411    Ok(())
5412}
5413
5414/// Groups non-root fragments into dependency layers (producers first).
5415fn fragment_layers(
5416    plan: &DistributedPlan,
5417    root: FragmentId,
5418) -> DistributedResult<Vec<Vec<FragmentId>>> {
5419    fn depth(
5420        plan: &DistributedPlan,
5421        fragment: FragmentId,
5422        memo: &mut [Option<usize>],
5423        visiting: &mut [bool],
5424    ) -> DistributedResult<usize> {
5425        if let Some(depth) = memo[fragment as usize] {
5426            return Ok(depth);
5427        }
5428        if visiting[fragment as usize] {
5429            return Err(DistributedError::InvalidPlan(
5430                "exchange graph has a cycle".to_owned(),
5431            ));
5432        }
5433        visiting[fragment as usize] = true;
5434        let mut best = 0;
5435        for edge in plan.exchanges_into(fragment) {
5436            best = best.max(depth(plan, edge.producer, memo, visiting)? + 1);
5437        }
5438        visiting[fragment as usize] = false;
5439        memo[fragment as usize] = Some(best);
5440        Ok(best)
5441    }
5442
5443    let mut memo = vec![None; plan.fragments.len()];
5444    let mut visiting = vec![false; plan.fragments.len()];
5445    let mut layers: Vec<Vec<FragmentId>> = Vec::new();
5446    for fragment in &plan.fragments {
5447        if fragment.fragment_id == root {
5448            continue;
5449        }
5450        let depth = depth(plan, fragment.fragment_id, &mut memo, &mut visiting)?;
5451        if layers.len() <= depth {
5452            layers.resize(depth + 1, Vec::new());
5453        }
5454        layers[depth].push(fragment.fragment_id);
5455    }
5456    Ok(layers)
5457}
5458
5459/// Builds one consumer's input streams from producer outputs, routing each
5460/// edge per its exchange kind.
5461fn build_inputs(
5462    plan: &DistributedPlan,
5463    consumer: FragmentId,
5464    outputs: &HashMap<FragmentId, Vec<BatchFrame>>,
5465) -> DistributedResult<Vec<FragmentStream>> {
5466    let mut inputs = Vec::new();
5467    for edge in plan.exchanges_into(consumer) {
5468        let frames = outputs.get(&edge.producer).cloned().ok_or_else(|| {
5469            DistributedError::InvalidPlan(format!(
5470                "missing output of producer fragment {}",
5471                edge.producer
5472            ))
5473        })?;
5474        let frames = route_frames(plan, edge, frames)?;
5475        inputs.push(Box::pin(stream::iter(frames.into_iter().map(Ok))) as FragmentStream);
5476    }
5477    Ok(inputs)
5478}
5479
5480/// Routes one producer's output frames to one consumer per the edge kind:
5481/// hash-repartitioned rows are split across the producer's sibling edges.
5482fn route_frames(
5483    plan: &DistributedPlan,
5484    edge: &ExchangeDescriptor,
5485    frames: Vec<BatchFrame>,
5486) -> DistributedResult<Vec<BatchFrame>> {
5487    match &edge.kind {
5488        ExchangeKind::Merge | ExchangeKind::Broadcast => Ok(frames),
5489        ExchangeKind::HashRepartition { keys } => {
5490            let mut siblings: Vec<&ExchangeDescriptor> = plan
5491                .exchanges
5492                .iter()
5493                .filter(|candidate| {
5494                    candidate.producer == edge.producer
5495                        && matches!(candidate.kind, ExchangeKind::HashRepartition { .. })
5496                })
5497                .collect();
5498            siblings.sort_by_key(|candidate| candidate.consumer);
5499            let width = siblings.len();
5500            let index = siblings
5501                .iter()
5502                .position(|candidate| candidate.consumer == edge.consumer)
5503                .ok_or_else(|| {
5504                    DistributedError::InvalidPlan(format!(
5505                        "exchange {} is missing from its repartition boundary",
5506                        edge.exchange_id
5507                    ))
5508                })?;
5509            repartition_frames(&frames, keys, width, index)
5510        }
5511    }
5512}
5513
5514/// Bridges a types-level [`QueryId`] onto the query registry's id space.
5515fn to_registry_query_id(query_id: &QueryId) -> DistributedResult<crate::query_registry::QueryId> {
5516    query_id
5517        .to_hex()
5518        .parse()
5519        .map_err(|error| DistributedError::InvalidPlan(format!("query id bridge failed: {error}")))
5520}
5521
5522// ---------------------------------------------------------------------------
5523// Tests
5524// ---------------------------------------------------------------------------
5525
5526#[cfg(test)]
5527mod tests {
5528    use super::*;
5529    use arrow::datatypes::Field;
5530
5531    /// Deterministic tablet id for tests.
5532    fn tablet(n: u8) -> TabletId {
5533        let mut bytes = [0u8; 16];
5534        bytes[15] = n;
5535        TabletId::from_bytes(bytes)
5536    }
5537
5538    /// Seeded RNG (SplitMix64) for reproducible property tests.
5539    struct SplitMix64(u64);
5540
5541    impl SplitMix64 {
5542        fn next(&mut self) -> u64 {
5543            self.0 = self.0.wrapping_add(0x9E37_79B9_7F4A_7C15);
5544            let mut z = self.0;
5545            z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
5546            z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
5547            z ^ (z >> 31)
5548        }
5549
5550        fn below(&mut self, n: u64) -> u64 {
5551            self.next() % n
5552        }
5553    }
5554
5555    #[derive(Default)]
5556    struct StaticLocator {
5557        tablets: HashMap<String, Vec<TabletId>>,
5558        specs: HashMap<String, PartitionSpec>,
5559    }
5560
5561    impl TabletLocator for StaticLocator {
5562        fn tablets_for_table(&self, table: &str) -> DistributedResult<Vec<TabletId>> {
5563            self.tablets
5564                .get(table)
5565                .cloned()
5566                .ok_or_else(|| DistributedError::UnknownTable(table.to_owned()))
5567        }
5568
5569        fn partitioning(&self, table: &str) -> DistributedResult<PartitionSpec> {
5570            self.specs
5571                .get(table)
5572                .cloned()
5573                .ok_or_else(|| DistributedError::UnknownTable(table.to_owned()))
5574        }
5575    }
5576
5577    struct StaticMetadata {
5578        version: MetadataVersion,
5579        stats: HashMap<String, TableStats>,
5580    }
5581
5582    impl ClusterMetadata for StaticMetadata {
5583        fn metadata_version(&self) -> MetadataVersion {
5584            self.version
5585        }
5586
5587        fn table_stats(&self, table: &str) -> DistributedResult<TableStats> {
5588            self.stats
5589                .get(table)
5590                .copied()
5591                .ok_or_else(|| DistributedError::UnknownTable(table.to_owned()))
5592        }
5593    }
5594
5595    /// Builds locator + metadata for a set of tables.
5596    fn world(
5597        entries: &[(&str, &[TabletId], PartitionSpec, TableStats)],
5598    ) -> (StaticLocator, StaticMetadata) {
5599        let mut locator = StaticLocator::default();
5600        let mut stats = HashMap::new();
5601        for (table, tablets, spec, stat) in entries {
5602            locator
5603                .tablets
5604                .insert((*table).to_owned(), tablets.to_vec());
5605            locator.specs.insert((*table).to_owned(), spec.clone());
5606            stats.insert((*table).to_owned(), *stat);
5607        }
5608        (
5609            locator,
5610            StaticMetadata {
5611                version: MetadataVersion::new(7),
5612                stats,
5613            },
5614        )
5615    }
5616
5617    fn scan(table: &str) -> LogicalPlanLite {
5618        LogicalPlanLite::Scan {
5619            table: table.to_owned(),
5620            predicate: None,
5621            projection: Vec::new(),
5622        }
5623    }
5624
5625    fn desc(root: LogicalPlanLite) -> PlanDescription {
5626        PlanDescription {
5627            query_id: QueryId::new_random(),
5628            root,
5629            options: PlannerOptions::default(),
5630        }
5631    }
5632
5633    fn hash_spec(column: &str) -> PartitionSpec {
5634        PartitionSpec::Hash {
5635            columns: vec![column.to_owned()],
5636            buckets: 16,
5637        }
5638    }
5639
5640    fn stats(rows: u64, bytes: u64) -> TableStats {
5641        TableStats {
5642            row_count: rows,
5643            total_bytes: bytes,
5644        }
5645    }
5646
5647    fn i64_batch(columns: &[(&str, Vec<i64>)]) -> RecordBatch {
5648        let schema = Schema::new(
5649            columns
5650                .iter()
5651                .map(|(name, _)| Field::new(*name, DataType::Int64, true))
5652                .collect::<Vec<_>>(),
5653        );
5654        let arrays: Vec<ArrayRef> = columns
5655            .iter()
5656            .map(|(_, values)| Arc::new(Int64Array::from(values.clone())) as ArrayRef)
5657            .collect();
5658        RecordBatch::try_new(schema.into(), arrays).unwrap()
5659    }
5660
5661    fn score_batch(scores: Vec<u64>, row_ids: Vec<u64>, payloads: Vec<i64>) -> RecordBatch {
5662        let schema = Schema::new(vec![
5663            Field::new("score", DataType::UInt64, true),
5664            Field::new(TOPK_ROWID_COLUMN, DataType::UInt64, false),
5665            Field::new("payload", DataType::Int64, true),
5666        ]);
5667        RecordBatch::try_new(
5668            schema.into(),
5669            vec![
5670                Arc::new(UInt64Array::from(scores)),
5671                Arc::new(UInt64Array::from(row_ids)),
5672                Arc::new(Int64Array::from(payloads)),
5673            ],
5674        )
5675        .unwrap()
5676    }
5677
5678    fn collect_i64(batches: &[RecordBatch], column: &str) -> Vec<i64> {
5679        let mut values = Vec::new();
5680        for batch in batches {
5681            let index = batch.schema().index_of(column).unwrap();
5682            let array = batch
5683                .column(index)
5684                .as_any()
5685                .downcast_ref::<Int64Array>()
5686                .unwrap();
5687            for row in 0..batch.num_rows() {
5688                assert!(
5689                    !array.is_null(row),
5690                    "column {column} has an unexpected null"
5691                );
5692                values.push(array.value(row));
5693            }
5694        }
5695        values
5696    }
5697
5698    fn collect_u64(batches: &[RecordBatch], column: &str) -> Vec<u64> {
5699        let mut values = Vec::new();
5700        for batch in batches {
5701            let index = batch.schema().index_of(column).unwrap();
5702            let array = batch
5703                .column(index)
5704                .as_any()
5705                .downcast_ref::<UInt64Array>()
5706                .unwrap();
5707            for row in 0..batch.num_rows() {
5708                values.push(array.value(row));
5709            }
5710        }
5711        values
5712    }
5713
5714    fn collect_f64(batches: &[RecordBatch], column: &str) -> Vec<Option<f64>> {
5715        let mut values = Vec::new();
5716        for batch in batches {
5717            let index = batch.schema().index_of(column).unwrap();
5718            let array = batch
5719                .column(index)
5720                .as_any()
5721                .downcast_ref::<Float64Array>()
5722                .unwrap();
5723            for row in 0..batch.num_rows() {
5724                values.push((!array.is_null(row)).then(|| array.value(row)));
5725            }
5726        }
5727        values
5728    }
5729
5730    fn operators(plan: &DistributedPlan, fragment_id: FragmentId) -> &[FragmentOperator] {
5731        &plan.fragments[fragment_id as usize].operators
5732    }
5733
5734    // -----------------------------------------------------------------------
5735    // Plan shape tests
5736    // -----------------------------------------------------------------------
5737
5738    #[test]
5739    fn scan_plan_shape_estimates_and_spill() {
5740        let tablets = [tablet(1), tablet(2), tablet(3)];
5741        let (locator, metadata) = world(&[(
5742            "t",
5743            &tablets,
5744            hash_spec("id"),
5745            stats(3_000, 3 * 1024 * 1024),
5746        )]);
5747        let description = desc(scan("t"));
5748        let plan = distribute(&description, &locator, &metadata).unwrap();
5749        assert_eq!(plan.query_id, description.query_id);
5750        assert_eq!(plan.metadata_version, MetadataVersion::new(7));
5751        assert_eq!(
5752            plan.fragments.len(),
5753            4,
5754            "3 scan fragments + coordinator gather"
5755        );
5756        for (index, id) in tablets.iter().enumerate() {
5757            let fragment = &plan.fragments[index];
5758            assert_eq!(fragment.assignment, FragmentAssignment::Tablet(*id));
5759            assert!(
5760                matches!(
5761                    operators(&plan, fragment.fragment_id)[0],
5762                    FragmentOperator::TabletScan { .. }
5763                ),
5764                "fragment {index} starts with a tablet scan"
5765            );
5766            assert!(
5767                matches!(
5768                    operators(&plan, fragment.fragment_id).last().unwrap(),
5769                    FragmentOperator::RemoteExchangeSink { .. }
5770                ),
5771                "fragment {index} ends with a sink"
5772            );
5773            assert_eq!(fragment.estimated_rows, 1_000);
5774            assert_eq!(fragment.estimated_bytes, 1024 * 1024);
5775            assert_eq!(
5776                fragment.max_spill_bytes,
5777                DEFAULT_MAX_SPILL_BYTES_PER_FRAGMENT
5778            );
5779        }
5780        let root = plan.root_fragment_id().unwrap();
5781        assert_eq!(root, 3);
5782        assert_eq!(
5783            plan.fragments[3].assignment,
5784            FragmentAssignment::Coordinator
5785        );
5786        assert_eq!(plan.exchanges.len(), 3);
5787        for edge in &plan.exchanges {
5788            assert_eq!(edge.kind, ExchangeKind::Merge);
5789            assert_eq!(edge.consumer, root);
5790            assert_eq!(
5791                edge.schema_fingerprint,
5792                plan.exchanges[0].schema_fingerprint
5793            );
5794        }
5795        // The configured spill allowance is stamped on every fragment.
5796        let mut custom = desc(scan("t"));
5797        custom.options.max_spill_bytes_per_fragment = 1_234;
5798        let plan = distribute(&custom, &locator, &metadata).unwrap();
5799        assert!(plan
5800            .fragments
5801            .iter()
5802            .all(|fragment| fragment.max_spill_bytes == 1_234));
5803    }
5804
5805    #[test]
5806    fn fragment_control_begin_spill_opens_core_spill_session() {
5807        use mongreldb_core::ExecutionControl;
5808        use mongreldb_types::ids::QueryId;
5809
5810        let dir = tempfile::tempdir().unwrap();
5811        // Real shipped path: Database::create owns the SpillManager.
5812        let db = mongreldb_core::Database::create(dir.path()).unwrap();
5813        let control = FragmentControl {
5814            control: ExecutionControl::new(None),
5815            max_spill_bytes: 64 * 1024,
5816            authorization_context: Arc::from([]),
5817        };
5818        let session = control
5819            .begin_spill(db.spill_manager(), QueryId::from_bytes([0xAB; 16]))
5820            .expect("begin_spill binds planner allowance to core SpillManager");
5821        let stats = db.spill_manager().stats();
5822        assert_eq!(
5823            stats.global_budget_bytes,
5824            db.spill_manager().config().global_bytes
5825        );
5826        drop(session);
5827    }
5828
5829    #[test]
5830    fn aggregate_plan_shape_grouped_and_ungrouped() {
5831        let tablets = [tablet(1), tablet(2), tablet(3)];
5832        let (locator, metadata) = world(&[("t", &tablets, hash_spec("id"), stats(900, 900_000))]);
5833        let aggregates = vec![
5834            AggregateExpr {
5835                function: AggregateFunction::Count,
5836                column: None,
5837            },
5838            AggregateExpr {
5839                function: AggregateFunction::Sum,
5840                column: Some("v".to_owned()),
5841            },
5842        ];
5843        // Grouped: partial on producers, hash-repartition exchange, final at
5844        // the coordinator.
5845        let grouped = LogicalPlanLite::Aggregate {
5846            input: Box::new(scan("t")),
5847            group_by: vec!["g".to_owned()],
5848            aggregates: aggregates.clone(),
5849        };
5850        let plan = distribute(&desc(grouped), &locator, &metadata).unwrap();
5851        assert_eq!(plan.fragments.len(), 4);
5852        for index in 0..3 {
5853            assert!(matches!(
5854                operators(&plan, index as FragmentId)[1],
5855                FragmentOperator::PartialAggregate { .. }
5856            ));
5857        }
5858        assert_eq!(plan.exchanges.len(), 3);
5859        for edge in &plan.exchanges {
5860            assert_eq!(
5861                edge.kind,
5862                ExchangeKind::HashRepartition {
5863                    keys: vec!["g".to_owned()]
5864                }
5865            );
5866        }
5867        let root = plan.root_fragment_id().unwrap();
5868        assert!(matches!(
5869            operators(&plan, root).last().unwrap(),
5870            FragmentOperator::FinalAggregate { .. }
5871        ));
5872        // Ungrouped: merge exchange, single-row estimate.
5873        let ungrouped = LogicalPlanLite::Aggregate {
5874            input: Box::new(scan("t")),
5875            group_by: Vec::new(),
5876            aggregates: aggregates[..1].to_vec(),
5877        };
5878        let plan = distribute(&desc(ungrouped), &locator, &metadata).unwrap();
5879        assert!(plan
5880            .exchanges
5881            .iter()
5882            .all(|edge| edge.kind == ExchangeKind::Merge));
5883        let root = plan.root_fragment_id().unwrap();
5884        assert_eq!(plan.fragments[root as usize].estimated_rows, 1);
5885    }
5886
5887    #[test]
5888    fn colocated_join_plan_fuses_scans_without_exchange() {
5889        let tablets = [tablet(1), tablet(2), tablet(3)];
5890        let (locator, metadata) = world(&[
5891            (
5892                "a",
5893                &tablets,
5894                hash_spec("id"),
5895                stats(6_000, 6 * 1024 * 1024),
5896            ),
5897            (
5898                "b",
5899                &tablets,
5900                hash_spec("id"),
5901                stats(3_000, 3 * 1024 * 1024),
5902            ),
5903        ]);
5904        let join = LogicalPlanLite::Join {
5905            left: Box::new(scan("a")),
5906            right: Box::new(scan("b")),
5907            on: vec![JoinKey {
5908                left: "id".to_owned(),
5909                right: "id".to_owned(),
5910            }],
5911        };
5912        let plan = distribute(&desc(join), &locator, &metadata).unwrap();
5913        assert_eq!(plan.fragments.len(), 4, "3 fused fragments + gather");
5914        for index in 0..3 {
5915            let ops = operators(&plan, index as FragmentId);
5916            assert!(
5917                matches!(&ops[0], FragmentOperator::TabletScan { table, .. } if table == "a"),
5918                "left scan first: {ops:?}"
5919            );
5920            assert!(
5921                matches!(&ops[1], FragmentOperator::TabletScan { table, .. } if table == "b"),
5922                "right scan second: {ops:?}"
5923            );
5924            assert!(
5925                matches!(&ops[2], FragmentOperator::DistributedHashJoin { .. }),
5926                "fused hash join: {ops:?}"
5927            );
5928        }
5929        assert!(
5930            plan.exchanges
5931                .iter()
5932                .all(|edge| edge.kind == ExchangeKind::Merge),
5933            "a colocated join has no shuffle: {:?}",
5934            plan.exchanges
5935        );
5936        assert_eq!(plan.exchanges.len(), 3, "only the gather edges remain");
5937    }
5938
5939    #[test]
5940    fn broadcast_join_plan_replicates_the_small_side() {
5941        let big_tablets = [tablet(1), tablet(2), tablet(3)];
5942        let small_tablets = [tablet(4), tablet(5)];
5943        let (locator, metadata) = world(&[
5944            (
5945                "big",
5946                &big_tablets,
5947                hash_spec("id"),
5948                stats(8_000, 64 * 1024 * 1024),
5949            ),
5950            (
5951                "small",
5952                &small_tablets,
5953                hash_spec("key"),
5954                stats(100, 1024 * 1024),
5955            ),
5956        ]);
5957        let join = LogicalPlanLite::Join {
5958            left: Box::new(scan("big")),
5959            right: Box::new(scan("small")),
5960            on: vec![JoinKey {
5961                left: "id".to_owned(),
5962                right: "key".to_owned(),
5963            }],
5964        };
5965        let plan = distribute(&desc(join), &locator, &metadata).unwrap();
5966        // 3 big + 2 small + gather.
5967        assert_eq!(plan.fragments.len(), 6);
5968        let root = plan.root_fragment_id().unwrap();
5969        let broadcast_edges: Vec<&ExchangeDescriptor> = plan
5970            .exchanges
5971            .iter()
5972            .filter(|edge| edge.kind == ExchangeKind::Broadcast)
5973            .collect();
5974        assert_eq!(
5975            broadcast_edges.len(),
5976            6,
5977            "each small producer x each big fragment"
5978        );
5979        for edge in &broadcast_edges {
5980            assert!(edge.producer >= 3, "small side produces the broadcast");
5981            assert!(edge.consumer < 3, "big side consumes the broadcast");
5982        }
5983        for index in 0..3 {
5984            let ops = operators(&plan, index as FragmentId);
5985            let last_transform = ops
5986                .iter()
5987                .rfind(|op| !matches!(op, FragmentOperator::RemoteExchangeSink { .. }))
5988                .unwrap();
5989            assert!(matches!(
5990                last_transform,
5991                FragmentOperator::BroadcastJoin {
5992                    build_side: BuildSide::Right,
5993                    ..
5994                }
5995            ));
5996            let sources = ops
5997                .iter()
5998                .filter(|op| matches!(op, FragmentOperator::RemoteExchangeSource { .. }))
5999                .count();
6000            assert_eq!(sources, 2, "one source per small producer");
6001        }
6002        assert!(plan
6003            .exchanges
6004            .iter()
6005            .filter(|edge| edge.consumer == root)
6006            .all(|edge| edge.kind == ExchangeKind::Merge && edge.producer < 3));
6007    }
6008
6009    #[test]
6010    fn repartition_join_plan_shuffles_both_sides() {
6011        let left_tablets = [tablet(1), tablet(2)];
6012        let right_tablets = [tablet(3), tablet(4), tablet(5)];
6013        let (locator, metadata) = world(&[
6014            (
6015                "l",
6016                &left_tablets,
6017                hash_spec("a"),
6018                stats(8_000, 64 * 1024 * 1024),
6019            ),
6020            (
6021                "r",
6022                &right_tablets,
6023                hash_spec("b"),
6024                stats(9_000, 96 * 1024 * 1024),
6025            ),
6026        ]);
6027        let join = LogicalPlanLite::Join {
6028            left: Box::new(scan("l")),
6029            right: Box::new(scan("r")),
6030            on: vec![JoinKey {
6031                left: "a".to_owned(),
6032                right: "b".to_owned(),
6033            }],
6034        };
6035        let plan = distribute(&desc(join), &locator, &metadata).unwrap();
6036        // 2 left scans + 3 right scans + 3 join fragments + gather.
6037        assert_eq!(plan.fragments.len(), 9);
6038        let root = plan.root_fragment_id().unwrap();
6039        let shuffles: Vec<&ExchangeDescriptor> = plan
6040            .exchanges
6041            .iter()
6042            .filter(|edge| matches!(edge.kind, ExchangeKind::HashRepartition { .. }))
6043            .collect();
6044        assert_eq!(shuffles.len(), 2 * 3 + 3 * 3);
6045        let left_keys: Vec<&ExchangeDescriptor> = shuffles
6046            .iter()
6047            .filter(|edge| edge.producer < 2)
6048            .copied()
6049            .collect();
6050        assert!(
6051            left_keys
6052                .iter()
6053                .all(|edge| matches!(&edge.kind, ExchangeKind::HashRepartition { keys } if keys == &vec!["a".to_owned()]))
6054        );
6055        for join_fragment in 5..8 {
6056            let fragment = &plan.fragments[join_fragment];
6057            assert_eq!(
6058                fragment.assignment,
6059                FragmentAssignment::Tablet(right_tablets[(join_fragment - 5) % 3]),
6060                "join fragments round-robin over the larger side's tablets"
6061            );
6062            let ops = operators(&plan, fragment.fragment_id);
6063            let sources = ops
6064                .iter()
6065                .filter(|op| matches!(op, FragmentOperator::RemoteExchangeSource { .. }))
6066                .count();
6067            assert_eq!(sources, 5, "2 left + 3 right sources: {ops:?}");
6068            let last_transform = ops
6069                .iter()
6070                .rfind(|op| !matches!(op, FragmentOperator::RemoteExchangeSink { .. }))
6071                .unwrap();
6072            assert!(matches!(
6073                last_transform,
6074                FragmentOperator::RepartitionJoin { .. }
6075            ));
6076        }
6077        assert!(plan
6078            .exchanges
6079            .iter()
6080            .filter(|edge| edge.consumer == root)
6081            .all(|edge| edge.kind == ExchangeKind::Merge));
6082    }
6083
6084    #[test]
6085    fn sort_and_limit_plan_shapes() {
6086        let tablets = [tablet(1), tablet(2), tablet(3)];
6087        let (locator, metadata) = world(&[("t", &tablets, hash_spec("id"), stats(900, 900_000))]);
6088
6089        // Single descending key + limit: distributed top-k on both sides.
6090        let topk = LogicalPlanLite::Sort {
6091            input: Box::new(scan("t")),
6092            keys: vec![SortKey {
6093                column: "score".to_owned(),
6094                descending: true,
6095            }],
6096            limit: Some(10),
6097        };
6098        let plan = distribute(&desc(topk), &locator, &metadata).unwrap();
6099        for index in 0..3 {
6100            assert!(matches!(
6101                operators(&plan, index as FragmentId)[1],
6102                FragmentOperator::DistributedTopK { k: 10, .. }
6103            ));
6104        }
6105        let root = plan.root_fragment_id().unwrap();
6106        assert!(matches!(
6107            operators(&plan, root).last().unwrap(),
6108            FragmentOperator::DistributedTopK { k: 10, .. }
6109        ));
6110        assert!(plan
6111            .exchanges
6112            .iter()
6113            .all(|edge| edge.kind == ExchangeKind::Merge));
6114
6115        // Plain sort: merge sort on both sides.
6116        let sort = LogicalPlanLite::Sort {
6117            input: Box::new(scan("t")),
6118            keys: vec![SortKey {
6119                column: "x".to_owned(),
6120                descending: false,
6121            }],
6122            limit: None,
6123        };
6124        let plan = distribute(&desc(sort), &locator, &metadata).unwrap();
6125        for index in 0..3 {
6126            assert!(matches!(
6127                operators(&plan, index as FragmentId)[1],
6128                FragmentOperator::MergeSort { limit: None, .. }
6129            ));
6130        }
6131        let root = plan.root_fragment_id().unwrap();
6132        assert!(matches!(
6133            operators(&plan, root).last().unwrap(),
6134            FragmentOperator::MergeSort { limit: None, .. }
6135        ));
6136
6137        // Multi-key sort with a limit is NOT a top-k (single descending key
6138        // only): merge sort with the limit pushed down.
6139        let multi = LogicalPlanLite::Sort {
6140            input: Box::new(scan("t")),
6141            keys: vec![
6142                SortKey {
6143                    column: "a".to_owned(),
6144                    descending: false,
6145                },
6146                SortKey {
6147                    column: "b".to_owned(),
6148                    descending: true,
6149                },
6150            ],
6151            limit: Some(5),
6152        };
6153        let plan = distribute(&desc(multi), &locator, &metadata).unwrap();
6154        for index in 0..3 {
6155            assert!(matches!(
6156                operators(&plan, index as FragmentId)[1],
6157                FragmentOperator::MergeSort { limit: Some(5), .. }
6158            ));
6159        }
6160        let root = plan.root_fragment_id().unwrap();
6161        assert!(matches!(
6162            operators(&plan, root).last().unwrap(),
6163            FragmentOperator::MergeSort { limit: Some(5), .. }
6164        ));
6165
6166        // Bare limit: pushed down and re-applied at the coordinator.
6167        let limit = LogicalPlanLite::Limit {
6168            input: Box::new(scan("t")),
6169            limit: 7,
6170        };
6171        let plan = distribute(&desc(limit), &locator, &metadata).unwrap();
6172        for index in 0..3 {
6173            assert!(matches!(
6174                operators(&plan, index as FragmentId)[1],
6175                FragmentOperator::DistributedLimit { limit: 7 }
6176            ));
6177        }
6178        let root = plan.root_fragment_id().unwrap();
6179        assert!(matches!(
6180            operators(&plan, root).last().unwrap(),
6181            FragmentOperator::DistributedLimit { limit: 7 }
6182        ));
6183
6184        // A limit over a scalar aggregate stays in the one coordinator
6185        // fragment (no extra hop).
6186        let chained = LogicalPlanLite::Limit {
6187            input: Box::new(LogicalPlanLite::Aggregate {
6188                input: Box::new(scan("t")),
6189                group_by: Vec::new(),
6190                aggregates: vec![AggregateExpr {
6191                    function: AggregateFunction::Count,
6192                    column: None,
6193                }],
6194            }),
6195            limit: 5,
6196        };
6197        let plan = distribute(&desc(chained), &locator, &metadata).unwrap();
6198        assert_eq!(plan.fragments.len(), 4);
6199        let root = plan.root_fragment_id().unwrap();
6200        let ops = operators(&plan, root);
6201        assert!(matches!(
6202            ops[ops.len() - 2],
6203            FragmentOperator::FinalAggregate { .. }
6204        ));
6205        assert!(matches!(
6206            ops[ops.len() - 1],
6207            FragmentOperator::DistributedLimit { limit: 5 }
6208        ));
6209    }
6210
6211    #[test]
6212    fn planner_rejects_invalid_inputs() {
6213        let tablets = [tablet(1)];
6214        let (locator, metadata) = world(&[("t", &tablets, hash_spec("id"), stats(10, 100))]);
6215        // Unknown table.
6216        let error = distribute(&desc(scan("nope")), &locator, &metadata).unwrap_err();
6217        assert!(
6218            matches!(error, DistributedError::UnknownTable(_)),
6219            "{error}"
6220        );
6221        // Empty layout.
6222        let (locator, metadata) = world(&[("e", &[], hash_spec("id"), stats(0, 0))]);
6223        let error = distribute(&desc(scan("e")), &locator, &metadata).unwrap_err();
6224        assert!(
6225            matches!(error, DistributedError::EmptyLayout { .. }),
6226            "{error}"
6227        );
6228        let (locator, metadata) = world(&[("t", &tablets, hash_spec("id"), stats(10, 100))]);
6229        // Join without keys.
6230        let join = LogicalPlanLite::Join {
6231            left: Box::new(scan("t")),
6232            right: Box::new(scan("t")),
6233            on: Vec::new(),
6234        };
6235        let error = distribute(&desc(join), &locator, &metadata).unwrap_err();
6236        assert!(matches!(error, DistributedError::InvalidPlan(_)), "{error}");
6237        // Sum without a column.
6238        let aggregate = LogicalPlanLite::Aggregate {
6239            input: Box::new(scan("t")),
6240            group_by: Vec::new(),
6241            aggregates: vec![AggregateExpr {
6242                function: AggregateFunction::Sum,
6243                column: None,
6244            }],
6245        };
6246        let error = distribute(&desc(aggregate), &locator, &metadata).unwrap_err();
6247        assert!(matches!(error, DistributedError::InvalidPlan(_)), "{error}");
6248        // Sort without keys.
6249        let sort = LogicalPlanLite::Sort {
6250            input: Box::new(scan("t")),
6251            keys: Vec::new(),
6252            limit: None,
6253        };
6254        let error = distribute(&desc(sort), &locator, &metadata).unwrap_err();
6255        assert!(matches!(error, DistributedError::InvalidPlan(_)), "{error}");
6256    }
6257
6258    // -----------------------------------------------------------------------
6259    // Distributed top-k: pure merge + adaptive refill
6260    // -----------------------------------------------------------------------
6261
6262    fn candidate(score: u64, tablet_n: u8, row_id: u64) -> TopKCandidate {
6263        TopKCandidate {
6264            score,
6265            tablet: tablet(tablet_n),
6266            row_id: RowId(row_id),
6267        }
6268    }
6269
6270    #[test]
6271    fn topk_tie_break_is_exact() {
6272        // Final score descending, tablet id ascending, RowId ascending.
6273        let mut rows = vec![
6274            candidate(5, 2, 0),
6275            candidate(5, 1, 2),
6276            candidate(7, 3, 0),
6277            candidate(5, 1, 0),
6278            candidate(3, 1, 0),
6279        ];
6280        rows.sort_by(topk_cmp);
6281        assert_eq!(
6282            rows,
6283            vec![
6284                candidate(7, 3, 0),
6285                candidate(5, 1, 0),
6286                candidate(5, 1, 2),
6287                candidate(5, 2, 0),
6288                candidate(3, 1, 0),
6289            ]
6290        );
6291    }
6292
6293    #[test]
6294    fn merge_top_k_needs_no_refill_when_tablets_are_exhausted() {
6295        let shards = vec![
6296            TabletTopK {
6297                tablet: tablet(1),
6298                rows: vec![candidate(100, 1, 0), candidate(90, 1, 1)],
6299                unseen_bound: None,
6300            },
6301            TabletTopK {
6302                tablet: tablet(2),
6303                rows: vec![candidate(96, 2, 0)],
6304                unseen_bound: None,
6305            },
6306        ];
6307        let merge = merge_top_k(&shards, 2);
6308        assert_eq!(
6309            merge.winners,
6310            vec![candidate(100, 1, 0), candidate(96, 2, 0)]
6311        );
6312        assert!(merge.refill.is_empty());
6313    }
6314
6315    #[test]
6316    fn merge_top_k_refill_rules_follow_the_bounds() {
6317        // Bound below the k-th winner: no refill possible.
6318        let shards = vec![
6319            TabletTopK {
6320                tablet: tablet(1),
6321                rows: vec![candidate(100, 1, 0), candidate(90, 1, 1)],
6322                unseen_bound: Some(95),
6323            },
6324            TabletTopK {
6325                tablet: tablet(2),
6326                rows: vec![candidate(96, 2, 0)],
6327                unseen_bound: None,
6328            },
6329        ];
6330        let merge = merge_top_k(&shards, 2);
6331        assert!(
6332            merge.refill.is_empty(),
6333            "unseen scores of tablet 1 are at most 95 < 96: {:?}",
6334            merge.refill
6335        );
6336        // Bound above the k-th winner: refill required.
6337        let shards = vec![
6338            TabletTopK {
6339                tablet: tablet(1),
6340                rows: vec![candidate(100, 1, 0), candidate(90, 1, 1)],
6341                unseen_bound: Some(97),
6342            },
6343            TabletTopK {
6344                tablet: tablet(2),
6345                rows: vec![candidate(96, 2, 0)],
6346                unseen_bound: None,
6347            },
6348        ];
6349        let merge = merge_top_k(&shards, 2);
6350        assert_eq!(merge.refill, vec![tablet(1)]);
6351        // Tie at the k-th winner's score with a better tablet id: the
6352        // conservative tie case refills.
6353        let shards = vec![
6354            TabletTopK {
6355                tablet: tablet(1),
6356                rows: vec![candidate(100, 1, 0)],
6357                unseen_bound: Some(96),
6358            },
6359            TabletTopK {
6360                tablet: tablet(2),
6361                rows: vec![candidate(96, 2, 9)],
6362                unseen_bound: None,
6363            },
6364        ];
6365        let merge = merge_top_k(&shards, 2);
6366        assert_eq!(
6367            merge.refill,
6368            vec![tablet(1)],
6369            "(96, tablet 1, min row id) ranks better than (96, tablet 2, 9)"
6370        );
6371        // Fewer than k candidates with unseen rows: refill.
6372        let shards = vec![
6373            TabletTopK {
6374                tablet: tablet(1),
6375                rows: vec![candidate(5, 1, 0)],
6376                unseen_bound: Some(1),
6377            },
6378            TabletTopK {
6379                tablet: tablet(2),
6380                rows: Vec::new(),
6381                unseen_bound: None,
6382            },
6383        ];
6384        let merge = merge_top_k(&shards, 3);
6385        assert_eq!(merge.winners, vec![candidate(5, 1, 0)]);
6386        assert_eq!(merge.refill, vec![tablet(1)]);
6387        // k = 0 is trivially exact.
6388        let merge = merge_top_k(&shards, 0);
6389        assert!(merge.winners.is_empty() && merge.refill.is_empty());
6390    }
6391
6392    #[test]
6393    fn exact_top_k_fills_winners_via_adaptive_refill() {
6394        // Tablet 1 holds 3 of the true top-3 but only emits 2 up front.
6395        let tablet_one_rows = vec![
6396            candidate(100, 1, 0),
6397            candidate(99, 1, 1),
6398            candidate(98, 1, 2),
6399            candidate(97, 1, 3),
6400        ];
6401        let shards = vec![
6402            TabletTopK {
6403                tablet: tablet(1),
6404                rows: tablet_one_rows[..2].to_vec(),
6405                unseen_bound: Some(98),
6406            },
6407            TabletTopK {
6408                tablet: tablet(2),
6409                rows: vec![candidate(96, 2, 0), candidate(95, 2, 1)],
6410                unseen_bound: None,
6411            },
6412        ];
6413        let remaining = tablet_one_rows.clone();
6414        let result = exact_top_k(3, shards, move |tablet_id| {
6415            assert_eq!(tablet_id, tablet(1));
6416            TabletTopK {
6417                tablet: tablet_id,
6418                rows: remaining[2..].to_vec(),
6419                unseen_bound: None,
6420            }
6421        })
6422        .unwrap();
6423        assert_eq!(
6424            result,
6425            vec![
6426                candidate(100, 1, 0),
6427                candidate(99, 1, 1),
6428                candidate(98, 1, 2),
6429            ]
6430        );
6431    }
6432
6433    #[test]
6434    fn exact_top_k_rejects_a_stuck_refill() {
6435        let shards = vec![TabletTopK {
6436            tablet: tablet(1),
6437            rows: Vec::new(),
6438            unseen_bound: Some(10),
6439        }];
6440        let error = exact_top_k(1, shards, |tablet| TabletTopK {
6441            tablet,
6442            rows: Vec::new(),
6443            unseen_bound: Some(10),
6444        })
6445        .unwrap_err();
6446        assert!(matches!(error, DistributedError::InvalidPlan(_)), "{error}");
6447    }
6448
6449    #[test]
6450    fn exact_top_k_matches_single_node_oracle_across_1000_sharded_datasets() {
6451        let mut total_refills = 0usize;
6452        for seed in 0..1_000u64 {
6453            let mut rng = SplitMix64(seed.wrapping_mul(0x9E37_79B9_7F4A_7C15).wrapping_add(1));
6454            let tablet_count = 1 + rng.below(8) as usize;
6455            let k = rng.below(30) as usize;
6456            let batch = 1 + rng.below(4) as usize;
6457            let mut oracle_rows = Vec::new();
6458            let mut per_tablet: HashMap<TabletId, Vec<TopKCandidate>> = HashMap::new();
6459            for index in 0..tablet_count {
6460                let tablet = tablet(index as u8 + 1);
6461                let row_count = rng.below(40) as usize;
6462                let mut rows: Vec<TopKCandidate> = (0..row_count)
6463                    .map(|row| TopKCandidate {
6464                        score: rng.below(12),
6465                        tablet,
6466                        row_id: RowId(row as u64),
6467                    })
6468                    .collect();
6469                rows.sort_by(topk_cmp);
6470                oracle_rows.extend(rows.iter().copied());
6471                per_tablet.insert(tablet, rows);
6472            }
6473            let mut oracle = oracle_rows;
6474            oracle.sort_by(topk_cmp);
6475            oracle.truncate(k);
6476            let contribution = |tablet: TabletId, offset: usize| -> TabletTopK {
6477                let rows = &per_tablet[&tablet];
6478                let start = offset.min(rows.len());
6479                let end = (offset + batch).min(rows.len());
6480                TabletTopK {
6481                    tablet,
6482                    rows: rows[start..end].to_vec(),
6483                    unseen_bound: rows.get(end).map(|candidate| candidate.score),
6484                }
6485            };
6486            let initial: Vec<TabletTopK> = (0..tablet_count)
6487                .map(|index| contribution(tablet(index as u8 + 1), 0))
6488                .collect();
6489            let mut returned: HashMap<TabletId, usize> = (0..tablet_count)
6490                .map(|index| (tablet(index as u8 + 1), batch))
6491                .collect();
6492            let result = exact_top_k(k, initial, |tablet| {
6493                total_refills += 1;
6494                let offset = returned[&tablet];
6495                returned.insert(tablet, offset + batch);
6496                contribution(tablet, offset)
6497            })
6498            .unwrap_or_else(|error| panic!("seed {seed}: {error}"));
6499            assert_eq!(result, oracle, "seed {seed}: k={k} batch={batch}");
6500        }
6501        assert!(
6502            total_refills > 0,
6503            "the property run must actually exercise adaptive refill"
6504        );
6505    }
6506
6507    // -----------------------------------------------------------------------
6508    // Execution skeleton: merge operators, cancellation, leases
6509    // -----------------------------------------------------------------------
6510
6511    /// Builds a coordinator + in-memory transport + registry over one
6512    /// executor.
6513    fn coordinator_with(
6514        executor: Arc<dyn FragmentExecutor>,
6515    ) -> (
6516        Arc<Coordinator>,
6517        Arc<InMemoryTransport>,
6518        Arc<SqlQueryRegistry>,
6519    ) {
6520        let transport = Arc::new(InMemoryTransport::new(executor));
6521        let registry = Arc::new(SqlQueryRegistry::default());
6522        let coordinator = Arc::new(Coordinator::new(
6523            Arc::clone(&transport) as Arc<dyn FragmentTransport>,
6524            Arc::clone(&registry),
6525        ));
6526        (coordinator, transport, registry)
6527    }
6528
6529    /// An executor that signals arrival and then waits for cancellation —
6530    /// the fixture for cancellation/lease tests.
6531    struct BlockingExecutor {
6532        barrier: Arc<tokio::sync::Barrier>,
6533    }
6534
6535    #[async_trait::async_trait]
6536    impl FragmentExecutor for BlockingExecutor {
6537        async fn execute(
6538            &self,
6539            _fragment: &PlanFragment,
6540            _inputs: Vec<FragmentStream>,
6541            control: FragmentControl,
6542        ) -> DistributedResult<FragmentStream> {
6543            self.barrier.wait().await;
6544            control.control.cancelled().await;
6545            Err(DistributedError::Cancelled(control.control.reason()))
6546        }
6547    }
6548
6549    /// A scan-only plan over `table` with 3 tablets, suitable for
6550    /// cancellation fixtures.
6551    fn scan_plan(table: &str) -> (DistributedPlan, Vec<TabletId>) {
6552        let tablets = [tablet(1), tablet(2), tablet(3)];
6553        let (locator, metadata) = world(&[(
6554            table,
6555            &tablets,
6556            hash_spec("id"),
6557            stats(3_000, 3 * 1024 * 1024),
6558        )]);
6559        let plan = distribute(&desc(scan(table)), &locator, &metadata).unwrap();
6560        (plan, tablets.to_vec())
6561    }
6562
6563    #[tokio::test]
6564    async fn scan_gather_returns_all_rows() {
6565        let store = Arc::new(InMemoryTableStore::new());
6566        let tablets = [tablet(1), tablet(2), tablet(3)];
6567        store.insert("t", tablets[0], i64_batch(&[("v", vec![1, 2])]));
6568        store.insert("t", tablets[1], i64_batch(&[("v", vec![3])]));
6569        // Tablet 3 intentionally holds no rows.
6570        let (plan, _) = scan_plan("t");
6571        let (coordinator, _, _) = coordinator_with(Arc::new(InMemoryFragmentExecutor::new(store)));
6572        let batches = coordinator.execute(&plan).await.unwrap();
6573        let mut values = collect_i64(&batches, "v");
6574        values.sort_unstable();
6575        assert_eq!(values, vec![1, 2, 3]);
6576    }
6577
6578    #[tokio::test]
6579    async fn remote_arrow_ipc_transport_gathers_with_pull_backpressure() {
6580        let store = Arc::new(InMemoryTableStore::new());
6581        let tablets = [tablet(1), tablet(2), tablet(3)];
6582        store.insert("t", tablets[0], i64_batch(&[("v", vec![1, 2])]));
6583        store.insert("t", tablets[1], i64_batch(&[("v", vec![3])]));
6584        let executor: Arc<dyn FragmentExecutor> = Arc::new(InMemoryFragmentExecutor::new(store));
6585        let endpoint = Arc::new(RemoteFragmentEndpoint::new(executor));
6586        let client: Arc<dyn FragmentRpcClient> =
6587            Arc::new(LoopbackFragmentRpcClient::new(Arc::clone(&endpoint)));
6588        let transport: Arc<dyn FragmentTransport> = Arc::new(RemoteFragmentTransport::new(client));
6589        let coordinator = Coordinator::new(transport, Arc::new(SqlQueryRegistry::default()));
6590        let (plan, _) = scan_plan("t");
6591
6592        let batches = coordinator.execute(&plan).await.unwrap();
6593        let mut values = collect_i64(&batches, "v");
6594        values.sort_unstable();
6595        assert_eq!(values, vec![1, 2, 3]);
6596        assert_eq!(
6597            endpoint.active_executions(),
6598            0,
6599            "terminal pulls release every worker cursor"
6600        );
6601        // P0.4-T6: fragment lifecycle metrics observe starts/pulls/completes and bytes.
6602        let metrics = endpoint.lifecycle_metrics();
6603        assert!(metrics.starts >= 1, "starts={}", metrics.starts);
6604        assert!(metrics.pulls >= 1, "pulls={}", metrics.pulls);
6605        assert!(metrics.completes >= 1, "completes={}", metrics.completes);
6606        assert!(metrics.bytes_in > 0, "bytes_in={}", metrics.bytes_in);
6607        assert!(metrics.bytes_out > 0, "bytes_out={}", metrics.bytes_out);
6608        assert_eq!(metrics.active_executions, 0);
6609    }
6610
6611    #[tokio::test]
6612    async fn merge_sort_matches_single_node_sort() {
6613        let store = Arc::new(InMemoryTableStore::new());
6614        let tablets = [tablet(1), tablet(2), tablet(3)];
6615        store.insert("s", tablets[0], i64_batch(&[("x", vec![5, 1, 9])]));
6616        store.insert("s", tablets[1], i64_batch(&[("x", vec![3, 7])]));
6617        store.insert("s", tablets[2], i64_batch(&[("x", vec![8, 2, 6, 4])]));
6618        let (locator, metadata) = world(&[("s", &tablets, hash_spec("id"), stats(9, 900))]);
6619        let (coordinator, _, _) = coordinator_with(Arc::new(InMemoryFragmentExecutor::new(store)));
6620
6621        // Full merge sort.
6622        let sort = LogicalPlanLite::Sort {
6623            input: Box::new(scan("s")),
6624            keys: vec![SortKey {
6625                column: "x".to_owned(),
6626                descending: false,
6627            }],
6628            limit: None,
6629        };
6630        let plan = distribute(&desc(sort), &locator, &metadata).unwrap();
6631        let batches = coordinator.execute(&plan).await.unwrap();
6632        assert_eq!(collect_i64(&batches, "x"), vec![1, 2, 3, 4, 5, 6, 7, 8, 9]);
6633
6634        // Bounded merge sort.
6635        let sort = LogicalPlanLite::Sort {
6636            input: Box::new(scan("s")),
6637            keys: vec![SortKey {
6638                column: "x".to_owned(),
6639                descending: false,
6640            }],
6641            limit: Some(4),
6642        };
6643        let plan = distribute(&desc(sort), &locator, &metadata).unwrap();
6644        let batches = coordinator.execute(&plan).await.unwrap();
6645        assert_eq!(collect_i64(&batches, "x"), vec![1, 2, 3, 4]);
6646    }
6647
6648    #[tokio::test]
6649    async fn final_aggregate_matches_single_node_aggregate() {
6650        let store = Arc::new(InMemoryTableStore::new());
6651        let tablets = [tablet(1), tablet(2), tablet(3)];
6652        store.insert(
6653            "t",
6654            tablets[0],
6655            i64_batch(&[("g", vec![1, 2, 1, 3]), ("v", vec![10, 20, 30, 40])]),
6656        );
6657        store.insert(
6658            "t",
6659            tablets[1],
6660            i64_batch(&[("g", vec![2, 1, 3]), ("v", vec![50, 60, 70])]),
6661        );
6662        store.insert(
6663            "t",
6664            tablets[2],
6665            i64_batch(&[("g", vec![3, 2]), ("v", vec![80, 90])]),
6666        );
6667        let (locator, metadata) = world(&[("t", &tablets, hash_spec("id"), stats(9, 900))]);
6668        let (coordinator, _, _) = coordinator_with(Arc::new(InMemoryFragmentExecutor::new(store)));
6669
6670        let aggregates = vec![
6671            AggregateExpr {
6672                function: AggregateFunction::Count,
6673                column: None,
6674            },
6675            AggregateExpr {
6676                function: AggregateFunction::Sum,
6677                column: Some("v".to_owned()),
6678            },
6679            AggregateExpr {
6680                function: AggregateFunction::Min,
6681                column: Some("v".to_owned()),
6682            },
6683            AggregateExpr {
6684                function: AggregateFunction::Max,
6685                column: Some("v".to_owned()),
6686            },
6687            AggregateExpr {
6688                function: AggregateFunction::Avg,
6689                column: Some("v".to_owned()),
6690            },
6691        ];
6692        let grouped = LogicalPlanLite::Aggregate {
6693            input: Box::new(scan("t")),
6694            group_by: vec!["g".to_owned()],
6695            aggregates,
6696        };
6697        let plan = distribute(&desc(grouped), &locator, &metadata).unwrap();
6698        let batches = coordinator.execute(&plan).await.unwrap();
6699        let groups = collect_i64(&batches, "g");
6700        let counts = collect_i64(&batches, "count_star");
6701        let sums = collect_i64(&batches, "sum_v");
6702        let mins = collect_i64(&batches, "min_v");
6703        let maxs = collect_i64(&batches, "max_v");
6704        let avgs = collect_f64(&batches, "avg_v");
6705        // Single-node oracle, group order-independent.
6706        let mut oracle: HashMap<i64, (i64, i64, i64, i64)> = HashMap::new();
6707        for (g, v) in [
6708            (1, 10),
6709            (2, 20),
6710            (1, 30),
6711            (3, 40),
6712            (2, 50),
6713            (1, 60),
6714            (3, 70),
6715            (3, 80),
6716            (2, 90),
6717        ] {
6718            let entry = oracle.entry(g).or_insert((0, 0, i64::MAX, i64::MIN));
6719            entry.0 += 1;
6720            entry.1 += v;
6721            entry.2 = entry.2.min(v);
6722            entry.3 = entry.3.max(v);
6723        }
6724        assert_eq!(groups.len(), 3);
6725        for index in 0..groups.len() {
6726            let (count, sum, min, max) = oracle[&groups[index]];
6727            assert_eq!(counts[index], count, "count for g={}", groups[index]);
6728            assert_eq!(sums[index], sum, "sum for g={}", groups[index]);
6729            assert_eq!(mins[index], min, "min for g={}", groups[index]);
6730            assert_eq!(maxs[index], max, "max for g={}", groups[index]);
6731            let expected_avg = sum as f64 / count as f64;
6732            let actual_avg = avgs[index].expect("avg is set");
6733            assert!(
6734                (actual_avg - expected_avg).abs() < 1e-12,
6735                "avg for g={}: {actual_avg} vs {expected_avg}",
6736                groups[index]
6737            );
6738        }
6739
6740        // Scalar aggregate: one row over all tablets.
6741        let scalar = LogicalPlanLite::Aggregate {
6742            input: Box::new(scan("t")),
6743            group_by: Vec::new(),
6744            aggregates: vec![
6745                AggregateExpr {
6746                    function: AggregateFunction::Count,
6747                    column: None,
6748                },
6749                AggregateExpr {
6750                    function: AggregateFunction::Sum,
6751                    column: Some("v".to_owned()),
6752                },
6753            ],
6754        };
6755        let plan = distribute(&desc(scalar), &locator, &metadata).unwrap();
6756        let batches = coordinator.execute(&plan).await.unwrap();
6757        assert_eq!(collect_i64(&batches, "count_star"), vec![9]);
6758        assert_eq!(collect_i64(&batches, "sum_v"), vec![450]);
6759    }
6760
6761    #[tokio::test]
6762    async fn distributed_top_k_matches_oracle_with_and_without_refill() {
6763        // Heavy score ties across three tablets; payloads mirror row ids so
6764        // the exact tie order is observable after `__rowid` is stripped.
6765        let store = Arc::new(InMemoryTableStore::new());
6766        let tablets = [tablet(1), tablet(2), tablet(3)];
6767        store.insert(
6768            "k",
6769            tablets[0],
6770            score_batch(vec![100, 90, 80, 70], vec![0, 1, 2, 3], vec![0, 1, 2, 3]),
6771        );
6772        store.insert(
6773            "k",
6774            tablets[1],
6775            score_batch(vec![90, 80, 60], vec![0, 1, 2], vec![0, 1, 2]),
6776        );
6777        store.insert(
6778            "k",
6779            tablets[2],
6780            score_batch(vec![90, 80, 50], vec![0, 1, 2], vec![0, 1, 2]),
6781        );
6782        let (locator, metadata) = world(&[("k", &tablets, hash_spec("id"), stats(10, 1_000))]);
6783        let topk = LogicalPlanLite::Sort {
6784            input: Box::new(scan("k")),
6785            keys: vec![SortKey {
6786                column: "score".to_owned(),
6787                descending: true,
6788            }],
6789            limit: Some(5),
6790        };
6791        // Single-node oracle under the exact tie-break.
6792        let mut oracle: Vec<TopKCandidate> = Vec::new();
6793        for (tablet_n, rows) in [
6794            (1u8, vec![(100u64, 0u64), (90, 1), (80, 2), (70, 3)]),
6795            (2u8, vec![(90, 0), (80, 1), (60, 2)]),
6796            (3u8, vec![(90, 0), (80, 1), (50, 2)]),
6797        ] {
6798            for (score, row_id) in rows {
6799                oracle.push(candidate(score, tablet_n, row_id));
6800            }
6801        }
6802        oracle.sort_by(topk_cmp);
6803        oracle.truncate(5);
6804        let expected: Vec<(u64, i64)> = oracle
6805            .iter()
6806            .map(|winner| (winner.score, winner.row_id.0 as i64))
6807            .collect();
6808
6809        let run = async |emit_batch: Option<usize>| {
6810            let executor = match emit_batch {
6811                Some(batch) => InMemoryFragmentExecutor::with_topk_emit_batch(store.clone(), batch),
6812                None => InMemoryFragmentExecutor::new(store.clone()),
6813            };
6814            let (coordinator, transport, _) = coordinator_with(Arc::new(executor));
6815            let plan = distribute(&desc(topk.clone()), &locator, &metadata).unwrap();
6816            let batches = coordinator.execute(&plan).await.unwrap();
6817            (batches, transport)
6818        };
6819
6820        // Default emission (up to k rows): exact without refills.
6821        let (batches, transport) = run(None).await;
6822        let scores = collect_u64(&batches, "score");
6823        let payloads = collect_i64(&batches, "payload");
6824        let actual: Vec<(u64, i64)> = scores.into_iter().zip(payloads).collect();
6825        assert_eq!(actual, expected);
6826        assert!(
6827            transport.refill_log().is_empty(),
6828            "exact local top-k never needs refill"
6829        );
6830        assert!(
6831            batches
6832                .iter()
6833                .all(|batch| batch.schema().index_of(TOPK_ROWID_COLUMN).is_err()),
6834            "the internal row-id column is stripped"
6835        );
6836
6837        // Bounded emission (2 rows per round): the coordinator must refill
6838        // and still match the oracle exactly.
6839        let (batches, transport) = run(Some(2)).await;
6840        let scores = collect_u64(&batches, "score");
6841        let payloads = collect_i64(&batches, "payload");
6842        let actual: Vec<(u64, i64)> = scores.into_iter().zip(payloads).collect();
6843        assert_eq!(actual, expected);
6844        assert!(
6845            !transport.refill_log().is_empty(),
6846            "bounded emission must trigger adaptive refill"
6847        );
6848
6849        // The same bounded producer over the remote protocol must cross the
6850        // refill RPC and preserve the exact winners.
6851        let executor: Arc<dyn FragmentExecutor> = Arc::new(
6852            InMemoryFragmentExecutor::with_topk_emit_batch(Arc::clone(&store), 2),
6853        );
6854        let endpoint = Arc::new(RemoteFragmentEndpoint::new(executor));
6855        let client: Arc<dyn FragmentRpcClient> =
6856            Arc::new(LoopbackFragmentRpcClient::new(Arc::clone(&endpoint)));
6857        let coordinator = Coordinator::new(
6858            Arc::new(RemoteFragmentTransport::new(client)),
6859            Arc::new(SqlQueryRegistry::default()),
6860        );
6861        let plan = distribute(&desc(topk), &locator, &metadata).unwrap();
6862        let batches = coordinator.execute(&plan).await.unwrap();
6863        let actual = collect_u64(&batches, "score")
6864            .into_iter()
6865            .zip(collect_i64(&batches, "payload"))
6866            .collect::<Vec<_>>();
6867        assert_eq!(actual, expected);
6868        assert_eq!(endpoint.active_executions(), 0);
6869    }
6870
6871    #[tokio::test]
6872    async fn cancellation_fans_out_to_every_fragment() {
6873        let barrier = Arc::new(tokio::sync::Barrier::new(4));
6874        let executor = Arc::new(BlockingExecutor {
6875            barrier: Arc::clone(&barrier),
6876        });
6877        let (coordinator, transport, _registry) = coordinator_with(executor);
6878        let (plan, _) = scan_plan("t");
6879        let task = {
6880            let coordinator = Arc::clone(&coordinator);
6881            let plan = plan.clone();
6882            tokio::spawn(async move { coordinator.execute(&plan).await })
6883        };
6884        tokio::time::timeout(Duration::from_secs(30), barrier.wait())
6885            .await
6886            .expect("all fragments started");
6887        assert!(coordinator.cancel_query(&plan.query_id).unwrap());
6888        let result = task.await.unwrap();
6889        assert!(
6890            matches!(
6891                result,
6892                Err(DistributedError::Cancelled(
6893                    CancellationReason::ClientRequest
6894                ))
6895            ),
6896            "cancellation surfaces as ClientRequest: {result:?}"
6897        );
6898        let cancelled = transport.cancelled_fragments();
6899        for fragment_id in 0..3 {
6900            assert!(
6901                cancelled.contains(&fragment_id),
6902                "fragment {fragment_id} received the transport cancel: {cancelled:?}"
6903            );
6904            let control = transport.control_for(fragment_id).unwrap();
6905            assert_eq!(control.reason(), CancellationReason::ClientRequest);
6906        }
6907    }
6908
6909    #[tokio::test]
6910    async fn remote_cancellation_reaches_worker_during_fragment_start() {
6911        let barrier = Arc::new(tokio::sync::Barrier::new(4));
6912        let executor: Arc<dyn FragmentExecutor> = Arc::new(BlockingExecutor {
6913            barrier: Arc::clone(&barrier),
6914        });
6915        let endpoint = Arc::new(RemoteFragmentEndpoint::new(executor));
6916        let client: Arc<dyn FragmentRpcClient> =
6917            Arc::new(LoopbackFragmentRpcClient::new(Arc::clone(&endpoint)));
6918        let coordinator = Arc::new(Coordinator::new(
6919            Arc::new(RemoteFragmentTransport::new(client)),
6920            Arc::new(SqlQueryRegistry::default()),
6921        ));
6922        let (plan, _) = scan_plan("t");
6923        let task = {
6924            let coordinator = Arc::clone(&coordinator);
6925            let plan = plan.clone();
6926            tokio::spawn(async move { coordinator.execute(&plan).await })
6927        };
6928        tokio::time::timeout(Duration::from_secs(30), barrier.wait())
6929            .await
6930            .expect("all remote workers entered fragment start");
6931        assert!(coordinator.cancel_query(&plan.query_id).unwrap());
6932        let result = tokio::time::timeout(Duration::from_secs(30), task)
6933            .await
6934            .expect("remote cancellation must not hang")
6935            .unwrap();
6936        assert!(matches!(result, Err(DistributedError::Cancelled(_))));
6937        assert_eq!(endpoint.active_executions(), 0);
6938    }
6939
6940    #[tokio::test]
6941    async fn registry_cancel_reaches_all_fragments() {
6942        let barrier = Arc::new(tokio::sync::Barrier::new(4));
6943        let executor = Arc::new(BlockingExecutor {
6944            barrier: Arc::clone(&barrier),
6945        });
6946        let (coordinator, transport, registry) = coordinator_with(executor);
6947        let (plan, _) = scan_plan("t");
6948        let task = {
6949            let coordinator = Arc::clone(&coordinator);
6950            let plan = plan.clone();
6951            tokio::spawn(async move { coordinator.execute(&plan).await })
6952        };
6953        tokio::time::timeout(Duration::from_secs(30), barrier.wait())
6954            .await
6955            .expect("all fragments started");
6956        // The existing registry cancel path — this is the wiring proof.
6957        let bridged: crate::query_registry::QueryId = plan.query_id.to_hex().parse().unwrap();
6958        assert_eq!(registry.cancel(bridged), CancelOutcome::Accepted);
6959        let result = task.await.unwrap();
6960        assert!(
6961            matches!(result, Err(DistributedError::Cancelled(_))),
6962            "registry cancellation aborts the distributed query: {result:?}"
6963        );
6964        for fragment_id in 0..3 {
6965            let control = transport.control_for(fragment_id).unwrap();
6966            assert_eq!(
6967                control.reason(),
6968                CancellationReason::ClientRequest,
6969                "fragment {fragment_id} observed the registry cancel"
6970            );
6971        }
6972    }
6973
6974    #[tokio::test]
6975    async fn lease_expiry_reclaims_abandoned_fragments() {
6976        let barrier = Arc::new(tokio::sync::Barrier::new(4));
6977        let executor = Arc::new(BlockingExecutor {
6978            barrier: Arc::clone(&barrier),
6979        });
6980        let transport = Arc::new(InMemoryTransport::new(executor));
6981        let registry = Arc::new(SqlQueryRegistry::default());
6982        let coordinator = Arc::new(Coordinator::with_limits(
6983            Arc::clone(&transport) as Arc<dyn FragmentTransport>,
6984            registry,
6985            1_024,
6986            16 * 1024 * 1024 * 1024,
6987            Duration::from_secs(30),
6988        ));
6989        let (plan, _) = scan_plan("t");
6990        let task = {
6991            let coordinator = Arc::clone(&coordinator);
6992            let plan = plan.clone();
6993            tokio::spawn(async move { coordinator.execute(&plan).await })
6994        };
6995        tokio::time::timeout(Duration::from_secs(30), barrier.wait())
6996            .await
6997            .expect("all fragments started");
6998        assert_eq!(coordinator.resources().reserved_fragments(), 3);
6999        // All worker leases expired an hour from now's perspective.
7000        let cleaned = coordinator.sweep_expired_leases(Instant::now() + Duration::from_secs(3_600));
7001        assert_eq!(cleaned, 3, "every abandoned fragment is reclaimed");
7002        assert_eq!(
7003            coordinator.resources().reserved_fragments(),
7004            0,
7005            "reclaimed fragments release their reservations"
7006        );
7007        for fragment_id in 0..3 {
7008            let control = transport.control_for(fragment_id).unwrap();
7009            assert_eq!(
7010                control.reason(),
7011                CancellationReason::ServerShutdown,
7012                "fragment {fragment_id} observes the worker loss"
7013            );
7014        }
7015        let result = task.await.unwrap();
7016        assert!(
7017            matches!(
7018                result,
7019                Err(DistributedError::Cancelled(
7020                    CancellationReason::ServerShutdown
7021                ))
7022            ),
7023            "the query fails with the worker-loss reason: {result:?}"
7024        );
7025    }
7026
7027    #[test]
7028    fn resource_reservation_denies_then_releases() {
7029        let ledger = Arc::new(ResourceLedger::new(1, u64::MAX));
7030        let fragment = PlanFragment {
7031            fragment_id: 0,
7032            assignment: FragmentAssignment::Coordinator,
7033            operators: Vec::new(),
7034            estimated_rows: 1,
7035            estimated_bytes: 100,
7036            max_spill_bytes: 0,
7037        };
7038        let permit = ledger.reserve(&fragment).unwrap();
7039        assert_eq!(ledger.reserved_fragments(), 1);
7040        assert_eq!(ledger.reserved_bytes(), 100);
7041        let denied = ledger.reserve(&fragment).unwrap_err();
7042        assert!(
7043            matches!(denied, DistributedError::Reservation { fragment_id: 0, .. }),
7044            "{denied}"
7045        );
7046        drop(permit);
7047        assert_eq!(ledger.reserved_fragments(), 0);
7048        assert_eq!(ledger.reserved_bytes(), 0);
7049        ledger.reserve(&fragment).unwrap();
7050        // Byte budget enforcement.
7051        let tight = Arc::new(ResourceLedger::new(8, 50));
7052        let denied = tight.reserve(&fragment).unwrap_err();
7053        assert!(
7054            matches!(denied, DistributedError::Reservation { .. }),
7055            "{denied}"
7056        );
7057    }
7058
7059    #[test]
7060    fn repartition_routes_every_row_exactly_once() {
7061        let batch = i64_batch(&[("k", (0..100).collect()), ("v", (0..100).collect())]);
7062        let frames = vec![BatchFrame::data(batch)];
7063        let keys = vec!["k".to_owned()];
7064        let mut partitions = Vec::new();
7065        for index in 0..3 {
7066            partitions.push(repartition_frames(&frames, &keys, 3, index).unwrap());
7067        }
7068        let total: usize = partitions
7069            .iter()
7070            .map(|frames| {
7071                frames
7072                    .iter()
7073                    .map(|frame| frame.batch.num_rows())
7074                    .sum::<usize>()
7075            })
7076            .sum();
7077        assert_eq!(total, 100, "every row lands in exactly one partition");
7078        // Re-routing a partition keeps all of its rows in the same partition
7079        // (disjointness proof by idempotence).
7080        for (index, partition) in partitions.iter().enumerate() {
7081            for other in 0..3 {
7082                let rerouted = repartition_frames(partition, &keys, 3, other).unwrap();
7083                let rows: usize = rerouted.iter().map(|frame| frame.batch.num_rows()).sum();
7084                if other == index {
7085                    let expected: usize =
7086                        partition.iter().map(|frame| frame.batch.num_rows()).sum();
7087                    assert_eq!(rows, expected, "partition {index} rows stay in {index}");
7088                } else {
7089                    assert_eq!(rows, 0, "partition {index} leaks rows into {other}");
7090                }
7091            }
7092        }
7093    }
7094
7095    #[test]
7096    fn score_key_mapping_preserves_order() {
7097        let ints = Int64Array::from(vec![i64::MIN, -5, 0, 5, i64::MAX]);
7098        let keys: Vec<u64> = (0..5).map(|row| score_key(&ints, row).unwrap()).collect();
7099        let mut sorted = keys.clone();
7100        sorted.sort_unstable();
7101        assert_eq!(keys, sorted, "int64 mapping is order-preserving");
7102        let floats = Float64Array::from(vec![f64::MIN, -1.5, 0.0, 2.5, f64::MAX]);
7103        let keys: Vec<u64> = (0..5).map(|row| score_key(&floats, row).unwrap()).collect();
7104        let mut sorted = keys.clone();
7105        sorted.sort_unstable();
7106        assert_eq!(keys, sorted, "float64 mapping is order-preserving");
7107    }
7108
7109    // -----------------------------------------------------------------------
7110    // DataFusionDistributedPlanner (P0.4)
7111    // -----------------------------------------------------------------------
7112
7113    fn df_table_source(
7114        columns: &[(&str, DataType)],
7115    ) -> Arc<dyn datafusion::logical_expr::TableSource> {
7116        use datafusion::logical_expr::logical_plan::builder::LogicalTableSource;
7117        let fields = columns
7118            .iter()
7119            .map(|(name, dtype)| Field::new(*name, dtype.clone(), true))
7120            .collect::<Vec<_>>();
7121        Arc::new(LogicalTableSource::new(Arc::new(Schema::new(fields))))
7122    }
7123
7124    fn df_scan(table: &str, columns: &[(&str, DataType)]) -> datafusion::logical_expr::LogicalPlan {
7125        use datafusion::logical_expr::LogicalPlanBuilder;
7126        LogicalPlanBuilder::scan(table, df_table_source(columns), None)
7127            .unwrap()
7128            .build()
7129            .unwrap()
7130    }
7131
7132    #[test]
7133    fn datafusion_planner_lowers_scan_filter_agg_sort_limit() {
7134        use datafusion::functions_aggregate::expr_fn::sum;
7135        use datafusion::logical_expr::{col, lit, LogicalPlanBuilder};
7136
7137        let plan = LogicalPlanBuilder::from(df_scan(
7138            "orders",
7139            &[("region", DataType::Utf8), ("amount", DataType::Int64)],
7140        ))
7141        .filter(col("amount").gt(lit(10i64)))
7142        .unwrap()
7143        .aggregate(vec![col("region")], vec![sum(col("amount"))])
7144        .unwrap()
7145        .sort(vec![col("region").sort(true, true)])
7146        .unwrap()
7147        .limit(0, Some(5))
7148        .unwrap()
7149        .build()
7150        .unwrap();
7151
7152        let planner = DataFusionDistributedPlanner::new(QueryId::new_random());
7153        let lite = planner.to_lite(&plan).unwrap();
7154        match &lite {
7155            LogicalPlanLite::Limit { limit, input } => {
7156                assert_eq!(*limit, 5);
7157                match input.as_ref() {
7158                    LogicalPlanLite::Sort { keys, input, .. } => {
7159                        assert_eq!(keys.len(), 1);
7160                        assert!(!keys[0].descending);
7161                        match input.as_ref() {
7162                            LogicalPlanLite::Aggregate {
7163                                group_by,
7164                                aggregates,
7165                                input,
7166                            } => {
7167                                assert_eq!(group_by, &vec!["region".to_owned()]);
7168                                assert_eq!(aggregates.len(), 1);
7169                                assert_eq!(aggregates[0].function, AggregateFunction::Sum);
7170                                match input.as_ref() {
7171                                    LogicalPlanLite::Scan {
7172                                        table, predicate, ..
7173                                    } => {
7174                                        assert_eq!(table, "orders");
7175                                        assert!(
7176                                            predicate
7177                                                .as_ref()
7178                                                .is_some_and(|p| p.contains("amount")),
7179                                            "predicate={predicate:?}"
7180                                        );
7181                                    }
7182                                    other => panic!("expected scan under aggregate, got {other:?}"),
7183                                }
7184                            }
7185                            other => panic!("expected aggregate under sort, got {other:?}"),
7186                        }
7187                    }
7188                    other => panic!("expected sort under limit, got {other:?}"),
7189                }
7190            }
7191            other => panic!("expected limit root, got {other:?}"),
7192        }
7193
7194        let t1 = TabletId::from_bytes([1; 16]);
7195        let t2 = TabletId::from_bytes([2; 16]);
7196        let (locator, metadata) = world(&[(
7197            "orders",
7198            &[t1, t2],
7199            hash_spec("region"),
7200            stats(1_000, 64_000),
7201        )]);
7202        let distributed = planner.lower(&plan, &locator, &metadata).unwrap();
7203        assert!(!distributed.fragments.is_empty());
7204        assert!(distributed.root_fragment_id().is_some());
7205        assert!(distributed.fragments.iter().any(|fragment| fragment
7206            .operators
7207            .iter()
7208            .any(|op| matches!(op, FragmentOperator::TabletScan { .. }))));
7209    }
7210
7211    #[test]
7212    fn datafusion_planner_lowers_join_and_union() {
7213        use datafusion::logical_expr::{JoinType, LogicalPlanBuilder};
7214
7215        let left = df_scan(
7216            "orders",
7217            &[("id", DataType::Int64), ("uid", DataType::Int64)],
7218        );
7219        let right = df_scan(
7220            "users",
7221            &[("id", DataType::Int64), ("name", DataType::Utf8)],
7222        );
7223        let join = LogicalPlanBuilder::from(left)
7224            .join(
7225                LogicalPlanBuilder::from(right).build().unwrap(),
7226                JoinType::Inner,
7227                (vec!["uid"], vec!["id"]),
7228                None,
7229            )
7230            .unwrap()
7231            .build()
7232            .unwrap();
7233
7234        let planner = DataFusionDistributedPlanner::new(QueryId::new_random());
7235        let lite = planner.to_lite(&join).unwrap();
7236        match lite {
7237            LogicalPlanLite::Join { on, left, right } => {
7238                assert_eq!(on.len(), 1);
7239                assert_eq!(on[0].left, "uid");
7240                assert_eq!(on[0].right, "id");
7241                assert!(
7242                    matches!(left.as_ref(), LogicalPlanLite::Scan { table, .. } if table == "orders")
7243                );
7244                assert!(
7245                    matches!(right.as_ref(), LogicalPlanLite::Scan { table, .. } if table == "users")
7246                );
7247            }
7248            other => panic!("expected join, got {other:?}"),
7249        }
7250
7251        let a = df_scan("a", &[("x", DataType::Int64)]);
7252        let b = df_scan("b", &[("x", DataType::Int64)]);
7253        let union = LogicalPlanBuilder::from(a)
7254            .union(LogicalPlanBuilder::from(b).build().unwrap())
7255            .unwrap()
7256            .build()
7257            .unwrap();
7258        let lite = planner.to_lite(&union).unwrap();
7259        match lite {
7260            LogicalPlanLite::Union { inputs } => {
7261                assert_eq!(inputs.len(), 2);
7262            }
7263            other => panic!("expected union, got {other:?}"),
7264        }
7265
7266        let t = TabletId::from_bytes([9; 16]);
7267        let (locator, metadata) = world(&[
7268            ("a", &[t], PartitionSpec::Unpartitioned, stats(10, 100)),
7269            ("b", &[t], PartitionSpec::Unpartitioned, stats(10, 100)),
7270            ("orders", &[t], hash_spec("uid"), stats(100, 1_000)),
7271            ("users", &[t], hash_spec("id"), stats(100, 1_000)),
7272        ]);
7273        planner.lower(&join, &locator, &metadata).unwrap();
7274        planner.lower(&union, &locator, &metadata).unwrap();
7275    }
7276
7277    #[test]
7278    fn datafusion_planner_rejects_unsupported_operators() {
7279        use datafusion::logical_expr::{col, lit, LogicalPlanBuilder};
7280
7281        let plan = LogicalPlanBuilder::from(df_scan("t", &[("v", DataType::Int64)]))
7282            .filter(col("v").gt(lit(1i64)))
7283            .unwrap()
7284            .distinct()
7285            .unwrap()
7286            .build()
7287            .unwrap();
7288        let planner = DataFusionDistributedPlanner::new(QueryId::new_random());
7289        let err = planner.to_lite(&plan).unwrap_err();
7290        assert!(
7291            matches!(err, DistributedError::Unsupported(_)),
7292            "distinct must be rejected: {err:?}"
7293        );
7294
7295        // VALUES is not a tablet scan and must be rejected.
7296        let values = LogicalPlanBuilder::values(vec![vec![lit(1i64)]])
7297            .unwrap()
7298            .build()
7299            .unwrap();
7300        let err = planner.to_lite(&values).unwrap_err();
7301        assert!(
7302            matches!(err, DistributedError::Unsupported(_)),
7303            "values must be rejected: {err:?}"
7304        );
7305    }
7306
7307    #[test]
7308    fn logical_plan_lite_union_plans_to_coordinator_gather() {
7309        let t1 = TabletId::from_bytes([1; 16]);
7310        let t2 = TabletId::from_bytes([2; 16]);
7311        let (locator, metadata) = world(&[
7312            ("a", &[t1], PartitionSpec::Unpartitioned, stats(10, 100)),
7313            ("b", &[t2], PartitionSpec::Unpartitioned, stats(20, 200)),
7314        ]);
7315        let plan = distribute(
7316            &desc(LogicalPlanLite::Union {
7317                inputs: vec![scan("a"), scan("b")],
7318            }),
7319            &locator,
7320            &metadata,
7321        )
7322        .unwrap();
7323        assert!(plan.root_fragment_id().is_some());
7324        assert!(plan
7325            .fragments
7326            .iter()
7327            .any(|f| f.assignment == FragmentAssignment::Coordinator));
7328        assert_eq!(
7329            plan.fragments
7330                .iter()
7331                .filter(|f| matches!(f.assignment, FragmentAssignment::Tablet(_)))
7332                .count(),
7333            2
7334        );
7335    }
7336
7337    #[tokio::test]
7338    async fn plan_sql_distributed_public_entry_lowers_real_sql() {
7339        // Public entry must exist and perform real DataFusion parse + lower.
7340        let schema = Arc::new(Schema::new(vec![
7341            Field::new("region", DataType::Utf8, true),
7342            Field::new("amount", DataType::Int64, true),
7343        ]));
7344        let catalog = PlanningTableCatalog::new().with_table("orders", schema);
7345        let t1 = TabletId::from_bytes([1; 16]);
7346        let t2 = TabletId::from_bytes([2; 16]);
7347        let (locator, metadata) = world(&[(
7348            "orders",
7349            &[t1, t2],
7350            hash_spec("region"),
7351            stats(1_000, 64_000),
7352        )]);
7353
7354        // Keep the SQL shape within the supported lower surface (scan+filter+
7355        // limit). Aggregate/join paths are covered by DataFusion plan unit tests.
7356        let plan = plan_sql_distributed(
7357            "SELECT region, amount FROM orders WHERE amount > 10 LIMIT 5",
7358            &catalog,
7359            &locator,
7360            &metadata,
7361        )
7362        .await
7363        .expect("plan_sql_distributed must lower real SQL");
7364
7365        assert!(!plan.fragments.is_empty());
7366        assert!(plan.root_fragment_id().is_some());
7367        assert!(
7368            plan.fragments.iter().any(|fragment| fragment
7369                .operators
7370                .iter()
7371                .any(|op| matches!(op, FragmentOperator::TabletScan { table, .. } if table == "orders"))),
7372            "expected tablet scan of orders: {plan:?}"
7373        );
7374        assert_eq!(plan.metadata_version, metadata.metadata_version());
7375
7376        // plan_logical_distributed is the same seam when a DF plan is already
7377        // available (e.g. from MongrelSession).
7378        let ctx = datafusion::prelude::SessionContext::new();
7379        let provider = datafusion::datasource::MemTable::try_new(
7380            Arc::clone(catalog.schema("orders").unwrap()),
7381            vec![vec![RecordBatch::new_empty(Arc::clone(
7382                catalog.schema("orders").unwrap(),
7383            ))]],
7384        )
7385        .unwrap();
7386        ctx.register_table("orders", Arc::new(provider)).unwrap();
7387        let df = ctx
7388            .sql("SELECT region FROM orders WHERE amount > 0")
7389            .await
7390            .unwrap();
7391        let from_logical =
7392            plan_logical_distributed(df.logical_plan(), &locator, &metadata).unwrap();
7393        assert!(!from_logical.fragments.is_empty());
7394    }
7395
7396    #[tokio::test]
7397    async fn plan_sql_distributed_rejects_unknown_table() {
7398        let catalog = PlanningTableCatalog::new();
7399        let (locator, metadata) = world(&[]);
7400        let err = plan_sql_distributed("SELECT 1 FROM missing", &catalog, &locator, &metadata)
7401            .await
7402            .unwrap_err();
7403        assert!(
7404            matches!(err, DistributedError::InvalidPlan(_)),
7405            "unknown table must fail planning: {err:?}"
7406        );
7407    }
7408}