Skip to main content

datafusion_physical_plan/operator_statistics/
mod.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Pluggable statistics propagation for physical plans.
19//!
20//! This module provides an extensible mechanism for computing statistics
21//! on [`ExecutionPlan`] nodes, following the chain of responsibility pattern
22//! similar to `RelationPlanner` for SQL parsing.
23//!
24//! # Overview
25//!
26//! The default implementation delegates to each operator's built-in
27//! `partition_statistics`. Users can register custom [`StatisticsProvider`]
28//! implementations to:
29//!
30//! 1. Provide statistics for custom [`ExecutionPlan`] implementations
31//! 2. Override default estimation with advanced approaches (e.g., histograms)
32//! 3. Plug in domain-specific knowledge for better cardinality estimation
33//!
34//! # Architecture
35//!
36//! - [`StatisticsProvider`]: Chain element that computes statistics for specific operators
37//! - [`StatisticsRegistry`]: Chains providers, lives in SessionState
38//! - [`ExtendedStatistics`]: Statistics with type-safe custom extensions
39//!
40//! # Built-in Providers
41//!
42//! The following providers are included and can be registered in this order:
43//!
44//! 1. [`FilterStatisticsProvider`] - selectivity-based filter estimation
45//! 2. [`ProjectionStatisticsProvider`] - column mapping through projections
46//! 3. [`PassthroughStatisticsProvider`] - passthrough for cardinality-preserving operators
47//! 4. [`AggregateStatisticsProvider`] - NDV-based GROUP BY cardinality estimation
48//! 5. [`JoinStatisticsProvider`] - NDV-based join output estimation (hash, sort-merge, cross)
49//! 6. [`LimitStatisticsProvider`] - caps output at the fetch limit (local and global)
50//! 7. [`UnionStatisticsProvider`] - sums input row counts
51//! 8. [`DefaultStatisticsProvider`] - fallback to `partition_statistics(None)`
52//!
53//! # Relationship to [#20184](https://github.com/apache/datafusion/issues/20184)
54//!
55//! This module performs its own bottom-up tree walk in [`StatisticsRegistry::compute`],
56//! separate from the walk optimizer rules do via `transform_up`. This means existing
57//! rules that call `partition_statistics` directly bypass the registry.
58//!
59//! [#20184](https://github.com/apache/datafusion/issues/20184) adds a `child_stats`
60//! parameter to `partition_statistics`. Once it lands, the registry can feed enriched
61//! **base** [`Statistics`] into operators' built-in `partition_statistics` calls,
62//! removing redundancy for the base-stats path (row counts, column stats). However,
63//! the separate registry walk is still required for [`ExtendedStatistics`] extension
64//! propagation: `partition_statistics` returns `Arc<Statistics>`, so extensions
65//! (histograms, sketches, etc.) are stripped at that boundary and can only flow
66//! through the registry walk.
67//!
68//! If [`Statistics`] itself were extended to carry a type-erased extension map
69//! (similar to [`ExtendedStatistics`]), the registry walk could be dropped entirely:
70//! extensions would flow naturally through `partition_statistics(child_stats)` and
71//! the registry would become a pure chain-of-responsibility on top of the existing
72//! traversal with no separate walk needed.
73//!
74//! # Example
75//!
76//! ```ignore
77//! use datafusion_physical_plan::operator_statistics::*;
78//!
79//! // Create registry with default provider
80//! let mut registry = StatisticsRegistry::new();
81//!
82//! // Register custom provider (higher priority)
83//! registry.register(Arc::new(MyHistogramProvider));
84//!
85//! // Compute statistics through the chain
86//! let stats = registry.compute(plan.as_ref())?;
87//! ```
88
89use std::fmt::{self, Debug};
90use std::sync::Arc;
91
92use datafusion_common::extensions::Extensions;
93use datafusion_common::stats::Precision;
94use datafusion_common::{Result, Statistics};
95
96use crate::ExecutionPlan;
97use crate::statistics::{StatisticsArgs, StatisticsContext};
98
99// ============================================================================
100// ExtendedStatistics: Statistics with type-safe extensions
101// ============================================================================
102
103/// Statistics with support for custom extensions.
104///
105/// Wraps the standard [`Statistics`] and adds a type-erased extension map
106/// for custom statistics like histograms, sketches, or domain-specific metadata.
107///
108/// # Example
109///
110/// ```ignore
111/// // Define a custom statistics extension
112/// #[derive(Debug, Clone)]
113/// struct HistogramStats {
114///     buckets: Vec<(i64, i64, usize)>, // (min, max, count)
115/// }
116///
117/// // Set extension in a planner
118/// let mut stats = ExtendedStatistics::from(base_stats);
119/// stats.set_extension(HistogramStats { buckets: vec![] });
120///
121/// // Retrieve in a consumer
122/// if let Some(hist) = stats.get_extension::<HistogramStats>() {
123///     // Use histogram for better estimation
124/// }
125/// ```
126#[derive(Debug, Clone, Default)]
127pub struct ExtendedStatistics {
128    /// Standard statistics (num_rows, byte_size, column stats)
129    base: Arc<Statistics>,
130    /// Type-erased extensions for custom statistics
131    extensions: Extensions,
132}
133
134impl ExtendedStatistics {
135    /// Create new ExtendedStatistics wrapping owned statistics.
136    pub fn new(base: Statistics) -> Self {
137        Self {
138            base: Arc::new(base),
139            extensions: Extensions::new(),
140        }
141    }
142
143    /// Create new ExtendedStatistics from an [`Arc<Statistics>`].
144    pub fn new_arc(base: Arc<Statistics>) -> Self {
145        Self {
146            base,
147            extensions: Extensions::new(),
148        }
149    }
150
151    /// Returns a reference to the base [`Statistics`].
152    pub fn base(&self) -> &Statistics {
153        &self.base
154    }
155
156    /// Returns a reference to the underlying [`Arc<Statistics>`].
157    pub fn base_arc(&self) -> &Arc<Statistics> {
158        &self.base
159    }
160
161    /// Get a reference to a custom statistics extension by type.
162    pub fn get_extension<T: 'static + Send + Sync>(&self) -> Option<&T> {
163        self.extensions.get::<T>()
164    }
165
166    /// Set a custom statistics extension.
167    pub fn set_extension<T: 'static + Send + Sync>(&mut self, value: T) {
168        self.extensions.insert(value);
169    }
170
171    /// Check if an extension of the given type exists.
172    pub fn has_extension<T: 'static + Send + Sync>(&self) -> bool {
173        self.extensions.contains::<T>()
174    }
175
176    /// Merge extensions from another ExtendedStatistics (other's extensions take precedence).
177    pub fn merge_extensions(&mut self, other: &ExtendedStatistics) {
178        self.extensions.merge(&other.extensions);
179    }
180}
181
182impl From<Statistics> for ExtendedStatistics {
183    fn from(base: Statistics) -> Self {
184        Self::new(base)
185    }
186}
187
188impl From<Arc<Statistics>> for ExtendedStatistics {
189    fn from(base: Arc<Statistics>) -> Self {
190        Self::new_arc(base)
191    }
192}
193
194impl From<ExtendedStatistics> for Statistics {
195    fn from(extended: ExtendedStatistics) -> Self {
196        Arc::unwrap_or_clone(extended.base)
197    }
198}
199
200// ============================================================================
201// StatisticsProvider trait and registry
202// ============================================================================
203
204/// Result of attempting to compute statistics with a [`StatisticsProvider`].
205#[derive(Debug)]
206pub enum StatisticsResult {
207    /// Statistics were computed by this provider
208    Computed(ExtendedStatistics),
209    /// This provider doesn't handle this operator; delegate to next in chain
210    Delegate,
211}
212
213/// Customize statistics computation for [`ExecutionPlan`] nodes.
214///
215/// Implementations can handle specific operator types or override default
216/// estimation logic. The chain of providers is traversed until one returns
217/// [`StatisticsResult::Computed`].
218///
219/// # Implementing a Custom Provider
220///
221/// ```ignore
222/// #[derive(Debug)]
223/// struct MyStatisticsProvider;
224///
225/// impl StatisticsProvider for MyStatisticsProvider {
226///     fn compute_statistics(
227///         &self,
228///         plan: &dyn ExecutionPlan,
229///         child_stats: &[ExtendedStatistics],
230///     ) -> Result<StatisticsResult> {
231///         if let Some(my_exec) = plan.downcast_ref::<MyCustomExec>() {
232///             // Custom logic for MyCustomExec
233///             Ok(StatisticsResult::Computed(/* ... */))
234///         } else {
235///             // Let next provider handle it
236///             Ok(StatisticsResult::Delegate)
237///         }
238///     }
239/// }
240/// ```
241pub trait StatisticsProvider: Debug + Send + Sync {
242    /// Compute statistics for an [`ExecutionPlan`] node.
243    ///
244    /// # Arguments
245    /// * `plan` - The execution plan node to compute statistics for
246    /// * `child_stats` - Extended statistics already computed for child nodes,
247    ///   in the same order as `plan.children()`. Empty for leaf nodes.
248    ///
249    /// # Returns
250    /// * `StatisticsResult::Computed(stats)` - Short-circuits the chain
251    /// * `StatisticsResult::Delegate` - Passes to next provider in chain
252    fn compute_statistics(
253        &self,
254        plan: &dyn ExecutionPlan,
255        child_stats: &[ExtendedStatistics],
256    ) -> Result<StatisticsResult>;
257}
258
259/// Default statistics provider that delegates to each operator's built-in
260/// `partition_statistics` implementation.
261#[derive(Debug, Default)]
262pub struct DefaultStatisticsProvider;
263
264impl StatisticsProvider for DefaultStatisticsProvider {
265    fn compute_statistics(
266        &self,
267        plan: &dyn ExecutionPlan,
268        _child_stats: &[ExtendedStatistics],
269    ) -> Result<StatisticsResult> {
270        let base = StatisticsContext::new().compute(plan, &StatisticsArgs::new())?;
271        Ok(StatisticsResult::Computed(ExtendedStatistics::new_arc(
272            base,
273        )))
274    }
275}
276
277/// Registry that chains [`StatisticsProvider`] implementations.
278///
279/// The registry is a stateless provider chain: it holds no mutable state
280/// and is cheaply `Clone`able / `Send` / `Sync`.
281#[derive(Clone)]
282pub struct StatisticsRegistry {
283    providers: Vec<Arc<dyn StatisticsProvider>>,
284}
285
286impl Debug for StatisticsRegistry {
287    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288        write!(f, "StatisticsRegistry({} providers)", self.providers.len())
289    }
290}
291
292impl Default for StatisticsRegistry {
293    fn default() -> Self {
294        Self::new()
295    }
296}
297
298impl StatisticsRegistry {
299    /// Create a new empty registry.
300    ///
301    /// With no providers, `compute()` falls back to each plan node's
302    /// built-in `partition_statistics()`. Register providers to enhance
303    /// statistics (e.g., inject NDV, use histograms).
304    pub fn new() -> Self {
305        Self {
306            providers: Vec::new(),
307        }
308    }
309
310    /// Create a registry with the given provider chain.
311    pub fn with_providers(providers: Vec<Arc<dyn StatisticsProvider>>) -> Self {
312        Self { providers }
313    }
314
315    /// Create a registry pre-loaded with the standard built-in providers.
316    ///
317    /// Provider order (first match wins):
318    /// 1. [`FilterStatisticsProvider`]
319    /// 2. [`ProjectionStatisticsProvider`]
320    /// 3. [`PassthroughStatisticsProvider`]
321    /// 4. [`AggregateStatisticsProvider`]
322    /// 5. [`JoinStatisticsProvider`]
323    /// 6. [`LimitStatisticsProvider`]
324    /// 7. [`UnionStatisticsProvider`]
325    /// 8. [`DefaultStatisticsProvider`]
326    pub fn default_with_builtin_providers() -> Self {
327        Self::with_providers(vec![
328            Arc::new(FilterStatisticsProvider),
329            Arc::new(ProjectionStatisticsProvider),
330            Arc::new(PassthroughStatisticsProvider),
331            Arc::new(AggregateStatisticsProvider),
332            Arc::new(JoinStatisticsProvider),
333            Arc::new(LimitStatisticsProvider),
334            Arc::new(UnionStatisticsProvider),
335            Arc::new(DefaultStatisticsProvider),
336        ])
337    }
338
339    /// Register a provider at the front of the chain (higher priority).
340    pub fn register(&mut self, provider: Arc<dyn StatisticsProvider>) {
341        self.providers.insert(0, provider);
342    }
343
344    /// Returns the current provider chain.
345    pub fn providers(&self) -> &[Arc<dyn StatisticsProvider>] {
346        &self.providers
347    }
348
349    /// Compute extended statistics for a plan through the provider chain.
350    ///
351    /// Performs a bottom-up tree walk: child statistics are computed recursively
352    /// and passed to providers, mirroring how `partition_statistics` composes
353    /// operators. Once [#20184](https://github.com/apache/datafusion/issues/20184)
354    /// lands, the registry can feed enriched base stats directly into
355    /// `partition_statistics(child_stats)`, removing the need for a separate walk.
356    ///
357    /// If no providers are registered, falls back to the plan's built-in
358    /// `partition_statistics(None)` with no overhead.
359    pub fn compute(&self, plan: &dyn ExecutionPlan) -> Result<ExtendedStatistics> {
360        // Fast path: no providers registered, skip the walk entirely
361        if self.providers.is_empty() {
362            let base = StatisticsContext::new().compute(plan, &StatisticsArgs::new())?;
363            return Ok(ExtendedStatistics::new_arc(base));
364        }
365
366        let children = plan.children();
367
368        // For leaf nodes, try providers with empty child stats.
369        // For non-leaf nodes, recursively compute enhanced child stats first.
370        let child_stats: Vec<ExtendedStatistics> = if children.is_empty() {
371            Vec::new()
372        } else {
373            children
374                .iter()
375                .map(|child| self.compute(child.as_ref()))
376                .collect::<Result<Vec<_>>>()?
377        };
378
379        for provider in &self.providers {
380            match provider.compute_statistics(plan, &child_stats)? {
381                StatisticsResult::Computed(stats) => return Ok(stats),
382                StatisticsResult::Delegate => continue,
383            }
384        }
385        // Fallback: use plan's built-in stats
386        let base = StatisticsContext::new().compute(plan, &StatisticsArgs::new())?;
387        Ok(ExtendedStatistics::new_arc(base))
388    }
389
390    /// Compute statistics and return only the base Statistics (no extensions).
391    ///
392    /// Convenience method for callers that don't need extensions.
393    pub fn compute_base(&self, plan: &dyn ExecutionPlan) -> Result<Statistics> {
394        Ok(self.compute(plan)?.base().clone())
395    }
396}
397
398// ============================================================================
399// Statistics Utility Functions
400// ============================================================================
401
402/// Estimate the number of distinct values when sampling from a population.
403///
404/// Given a domain with `domain_size` distinct values and `num_selected` rows
405/// sampled/filtered from it, estimates how many distinct values will appear
406/// in the sample.
407///
408/// Uses the formula: `Expected distinct = N * [1 - (1 - 1/N)^n]`
409///
410/// # References
411///
412/// Based on Calcite's `RelMdUtil.numDistinctVals()`:
413/// <https://github.com/apache/calcite/blob/main/core/src/main/java/org/apache/calcite/rel/metadata/RelMdUtil.java>
414pub fn num_distinct_vals(domain_size: usize, num_selected: usize) -> usize {
415    if domain_size == 0 || num_selected == 0 {
416        return 0;
417    }
418
419    if num_selected >= domain_size {
420        return domain_size;
421    }
422
423    let n = domain_size as f64;
424    let k = num_selected as f64;
425
426    // For large n, (1-1/n).powf(k) loses precision because the base is near
427    // 1.0; use the equivalent exp(-k/n) form which is numerically stable.
428    // Threshold matches Calcite's RelMdUtil.numDistinctVals().
429    let expected = if domain_size > 1000 {
430        n * (1.0 - (-k / n).exp())
431    } else {
432        n * (1.0 - (1.0 - 1.0 / n).powf(k))
433    };
434
435    let result = expected.round() as usize;
436    result.clamp(1, domain_size)
437}
438
439/// Estimate NDV after applying a selectivity factor (filtering).
440///
441/// When filtering rows, each distinct value has multiple rows. If a value
442/// appears `k` times, the probability it survives the filter is `1 - (1-s)^k`
443/// where `s` is the selectivity.
444///
445/// Assuming uniform distribution (each value appears `rows/ndv` times):
446/// ```text
447/// NDV_after ~ NDV_before * [1 - (1 - selectivity)^(rows/NDV)]
448/// ```
449pub fn ndv_after_selectivity(
450    original_ndv: usize,
451    original_rows: usize,
452    selectivity: f64,
453) -> usize {
454    if selectivity <= 0.0 || original_ndv == 0 || original_rows == 0 {
455        return 0;
456    }
457    if selectivity >= 1.0 {
458        return original_ndv;
459    }
460
461    let ndv = original_ndv as f64;
462    let rows = original_rows as f64;
463
464    let rows_per_value = rows / ndv;
465    let survival_prob = 1.0 - (1.0 - selectivity).powf(rows_per_value);
466    let expected_ndv = ndv * survival_prob;
467
468    (expected_ndv.round() as usize).clamp(1, original_ndv)
469}
470
471/// Rescale `total_byte_size` proportionally after overriding `num_rows`.
472///
473/// When a provider replaces `num_rows` but keeps the rest of the stats from
474/// `partition_statistics`, the original `total_byte_size` becomes inconsistent.
475/// This function adjusts it by the ratio `new_rows / old_rows`, preserving the
476/// average bytes-per-row from the original estimate.
477fn rescale_byte_size(stats: &mut Statistics, new_num_rows: Precision<usize>) {
478    let old_rows = stats.num_rows;
479    stats.num_rows = new_num_rows;
480    stats.total_byte_size = match (old_rows, new_num_rows, stats.total_byte_size) {
481        (Precision::Exact(old), Precision::Exact(new), Precision::Exact(bytes))
482            if old > 0 =>
483        {
484            Precision::Exact((bytes as f64 * new as f64 / old as f64).round() as usize)
485        }
486        _ => match (
487            old_rows.get_value(),
488            new_num_rows.get_value(),
489            stats.total_byte_size.get_value(),
490        ) {
491            (Some(&old), Some(&new), Some(&bytes)) if old > 0 => Precision::Inexact(
492                (bytes as f64 * new as f64 / old as f64).round() as usize,
493            ),
494            _ => stats.total_byte_size,
495        },
496    };
497}
498
499/// Fetches base statistics from the operator's built-in `partition_statistics`,
500/// overrides `num_rows` with the registry-computed estimate, and rescales
501/// `total_byte_size` proportionally.
502///
503/// Used by providers that compute a better row count but cannot yet propagate
504/// column-level stats (NDV, min/max) through the operator — pending #20184.
505fn computed_with_row_count(
506    plan: &dyn ExecutionPlan,
507    num_rows: Precision<usize>,
508) -> Result<StatisticsResult> {
509    let mut base = Arc::unwrap_or_clone(
510        StatisticsContext::new().compute(plan, &StatisticsArgs::new())?,
511    );
512    rescale_byte_size(&mut base, num_rows);
513    Ok(StatisticsResult::Computed(ExtendedStatistics::new(base)))
514}
515
516/// Statistics provider for [`FilterExec`](crate::filter::FilterExec) that uses
517/// pre-computed enhanced child statistics from the registry walk.
518///
519/// Unlike the default provider (which calls `partition_statistics` and gets raw
520/// child stats), this provider receives enhanced child stats that may include
521/// NDV overrides injected at the scan level. It applies the same selectivity
522/// estimation logic as `FilterExec::statistics_helper`, then additionally
523/// adjusts each column's `distinct_count` using [`ndv_after_selectivity`] based
524/// on the computed selectivity ratio.
525#[derive(Debug, Default)]
526pub struct FilterStatisticsProvider;
527
528impl StatisticsProvider for FilterStatisticsProvider {
529    fn compute_statistics(
530        &self,
531        plan: &dyn ExecutionPlan,
532        child_stats: &[ExtendedStatistics],
533    ) -> Result<StatisticsResult> {
534        use crate::filter::FilterExec;
535
536        let Some(filter) = plan.downcast_ref::<FilterExec>() else {
537            return Ok(StatisticsResult::Delegate);
538        };
539        if child_stats.is_empty() {
540            return Ok(StatisticsResult::Delegate);
541        }
542
543        let input_stats = (*child_stats[0].base).clone();
544        let input_rows = input_stats.num_rows;
545        let mut stats = FilterExec::statistics_helper(
546            &filter.input().schema(),
547            input_stats,
548            filter.predicate(),
549            filter.default_selectivity(),
550            // TODO: pass filter.expression_analyzer_registry() once #21122 lands
551        )?;
552
553        // Adjust distinct_count for each column using the selectivity ratio
554        // via the probabilistic survival model from
555        // ndv_after_selectivity to account for rows removed by the filter.
556        if let (Some(&orig_rows), Some(&filtered_rows)) =
557            (input_rows.get_value(), stats.num_rows.get_value())
558            && orig_rows > 0
559            && filtered_rows < orig_rows
560        {
561            let selectivity = filtered_rows as f64 / orig_rows as f64;
562            for col_stat in &mut stats.column_statistics {
563                if let Some(&ndv) = col_stat.distinct_count.get_value() {
564                    let adjusted = ndv_after_selectivity(ndv, orig_rows, selectivity);
565                    col_stat.distinct_count = Precision::Inexact(adjusted);
566                }
567            }
568        }
569
570        let stats = stats.project(filter.projection().as_ref());
571        Ok(StatisticsResult::Computed(ExtendedStatistics::new(stats)))
572    }
573}
574
575/// Statistics provider for [`ProjectionExec`](crate::projection::ProjectionExec)
576/// that uses pre-computed enhanced child statistics from the registry walk.
577///
578/// Maps enhanced child column statistics to output columns based on the
579/// projection expressions, preserving NDV and other statistics through
580/// column references.
581#[derive(Debug, Default)]
582pub struct ProjectionStatisticsProvider;
583
584impl StatisticsProvider for ProjectionStatisticsProvider {
585    fn compute_statistics(
586        &self,
587        plan: &dyn ExecutionPlan,
588        child_stats: &[ExtendedStatistics],
589    ) -> Result<StatisticsResult> {
590        use crate::projection::ProjectionExec;
591
592        let Some(proj) = plan.downcast_ref::<ProjectionExec>() else {
593            return Ok(StatisticsResult::Delegate);
594        };
595        if child_stats.is_empty() {
596            return Ok(StatisticsResult::Delegate);
597        }
598
599        let input_stats = (*child_stats[0].base).clone();
600        let output_schema = proj.schema();
601        // TODO: pass proj.expression_analyzer_registry() once #21122 lands,
602        // so expression-level NDV/min/max feeds into projected column stats.
603        let stats = proj
604            .projection_expr()
605            .project_statistics(input_stats, &output_schema)?;
606        Ok(StatisticsResult::Computed(ExtendedStatistics::new(stats)))
607    }
608}
609
610/// Statistics provider for single-input operators with
611/// [`CardinalityEffect::Equal`](crate::execution_plan::CardinalityEffect::Equal).
612///
613/// These operators (Sort, Repartition, CoalescePartitions, etc.) don't
614/// transform statistics, so we pass through the enhanced child stats directly.
615/// This avoids the fallback calling `partition_statistics(None)` which would
616/// trigger a redundant internal recursion with raw (non-enhanced) stats.
617#[derive(Debug, Default)]
618pub struct PassthroughStatisticsProvider;
619
620impl StatisticsProvider for PassthroughStatisticsProvider {
621    fn compute_statistics(
622        &self,
623        plan: &dyn ExecutionPlan,
624        child_stats: &[ExtendedStatistics],
625    ) -> Result<StatisticsResult> {
626        use crate::execution_plan::CardinalityEffect;
627
628        if child_stats.len() != 1
629            || !matches!(plan.cardinality_effect(), CardinalityEffect::Equal)
630        {
631            return Ok(StatisticsResult::Delegate);
632        }
633
634        // Only pass through when the schema is unchanged (same column count).
635        // Operators like WindowAggExec preserve row count but add columns;
636        // passing through child stats would produce wrong column_statistics.
637        let input_cols = child_stats[0].base.column_statistics.len();
638        let output_cols = plan.schema().fields().len();
639        if input_cols != output_cols {
640            return Ok(StatisticsResult::Delegate);
641        }
642
643        Ok(StatisticsResult::Computed(child_stats[0].clone()))
644    }
645}
646
647/// Statistics provider for [`AggregateExec`](crate::aggregates::AggregateExec)
648/// that estimates output cardinality from the NDV of GROUP BY columns.
649///
650/// For each GROUP BY column, looks up `distinct_count` from the enhanced
651/// child statistics. The estimated output rows is the product of all
652/// column NDVs, capped at the input row count. This assumes independence
653/// between columns, so correlated columns (e.g., `city` and `state`) will
654/// produce overestimates.
655///
656/// For GROUPING SETS / CUBE / ROLLUP, delegates to the built-in
657/// `partition_statistics`, which handles per-set NDV estimation correctly.
658///
659/// Delegates when:
660/// - The plan is not an `AggregateExec`
661/// - The aggregate is `Partial` (per-partition, not bounded by global NDV)
662/// - GROUP BY is empty (scalar aggregate)
663/// - Any GROUP BY expression is not a simple column reference
664/// - Any GROUP BY column lacks NDV information
665#[derive(Debug, Default)]
666pub struct AggregateStatisticsProvider;
667
668impl StatisticsProvider for AggregateStatisticsProvider {
669    fn compute_statistics(
670        &self,
671        plan: &dyn ExecutionPlan,
672        child_stats: &[ExtendedStatistics],
673    ) -> Result<StatisticsResult> {
674        use crate::aggregates::AggregateExec;
675        use datafusion_physical_expr::expressions::Column;
676
677        use crate::aggregates::AggregateMode;
678
679        let Some(agg) = plan.downcast_ref::<AggregateExec>() else {
680            return Ok(StatisticsResult::Delegate);
681        };
682
683        // Partial aggregates produce per-partition groups, not bounded by
684        // global NDV; delegate to the built-in estimate for those.
685        if matches!(agg.mode(), AggregateMode::Partial) {
686            return Ok(StatisticsResult::Delegate);
687        }
688
689        if child_stats.is_empty() || agg.group_expr().expr().is_empty() {
690            return Ok(StatisticsResult::Delegate);
691        }
692
693        let input_stats = &child_stats[0].base;
694
695        // Compute NDV product of GROUP BY columns
696        let mut ndv_product: Option<usize> = None;
697        for (expr, _) in agg.group_expr().expr().iter() {
698            let Some(col) = expr.downcast_ref::<Column>() else {
699                return Ok(StatisticsResult::Delegate);
700            };
701            let Some(&ndv) = input_stats
702                .column_statistics
703                .get(col.index())
704                .and_then(|s| s.distinct_count.get_value())
705            else {
706                return Ok(StatisticsResult::Delegate);
707            };
708            if ndv == 0 {
709                return Ok(StatisticsResult::Delegate);
710            }
711            ndv_product = Some(match ndv_product {
712                Some(prev) => prev.saturating_mul(ndv),
713                None => ndv,
714            });
715        }
716
717        let Some(product) = ndv_product else {
718            return Ok(StatisticsResult::Delegate);
719        };
720
721        // For CUBE/ROLLUP/GROUPING SETS (multiple grouping sets), delegate to
722        // the built-in estimate, which handles per-set NDV estimation correctly.
723        if agg.group_expr().groups().len() > 1 {
724            return Ok(StatisticsResult::Delegate);
725        }
726
727        // Cap at input rows
728        let estimate = match input_stats.num_rows.get_value() {
729            Some(&rows) => product.min(rows),
730            None => product,
731        };
732
733        let num_rows = Precision::Inexact(estimate);
734
735        computed_with_row_count(plan, num_rows)
736    }
737}
738
739/// Statistics provider for equi-joins (hash join, sort-merge join) and cross joins.
740///
741/// For equi-joins, estimates output cardinality as
742/// `left_rows * right_rows / product(max(left_ndv_i, right_ndv_i))`
743/// across all join key columns (assuming independence between keys),
744/// falling back to the Cartesian product when any key lacks NDV on both sides.
745/// For cross joins, uses the exact Cartesian product.
746///
747/// The base inner-join estimate is then adjusted for the join type:
748/// - Semi joins: capped at the preserved-side row count
749/// - Anti joins: preserved-side minus matched rows (clamped to 0)
750/// - Left/Right outer: at least as many rows as the preserved side
751/// - Full outer: at least `left + right - inner_estimate`
752/// - Left mark: exactly `left_rows` (one output row per left row)
753///
754/// Delegates when:
755/// - The plan is not a supported join type
756/// - Either input lacks row count information
757#[derive(Debug, Default)]
758pub struct JoinStatisticsProvider;
759
760impl StatisticsProvider for JoinStatisticsProvider {
761    fn compute_statistics(
762        &self,
763        plan: &dyn ExecutionPlan,
764        child_stats: &[ExtendedStatistics],
765    ) -> Result<StatisticsResult> {
766        use crate::joins::{CrossJoinExec, HashJoinExec, SortMergeJoinExec};
767        use datafusion_common::JoinType;
768        use datafusion_physical_expr::expressions::Column;
769
770        if child_stats.len() < 2 {
771            return Ok(StatisticsResult::Delegate);
772        }
773
774        let left = &child_stats[0].base;
775        let right = &child_stats[1].base;
776
777        let (Some(&left_rows), Some(&right_rows)) =
778            (left.num_rows.get_value(), right.num_rows.get_value())
779        else {
780            return Ok(StatisticsResult::Delegate);
781        };
782
783        use crate::joins::JoinOnRef;
784
785        /// Estimate equi-join output using NDV of join key columns:
786        ///   left_rows * right_rows / product(max(left_ndv_i, right_ndv_i))
787        /// Falls back to Cartesian product if any key lacks NDV on both sides.
788        fn equi_join_estimate(
789            on: JoinOnRef,
790            left: &Statistics,
791            right: &Statistics,
792            left_rows: usize,
793            right_rows: usize,
794        ) -> usize {
795            if on.is_empty() {
796                return left_rows.saturating_mul(right_rows);
797            }
798            let mut ndv_divisor: usize = 1;
799            for (left_key, right_key) in on {
800                let left_ndv = left_key
801                    .downcast_ref::<Column>()
802                    .and_then(|c| left.column_statistics.get(c.index()))
803                    .and_then(|s| s.distinct_count.get_value().copied());
804                let right_ndv = right_key
805                    .downcast_ref::<Column>()
806                    .and_then(|c| right.column_statistics.get(c.index()))
807                    .and_then(|s| s.distinct_count.get_value().copied());
808                match (left_ndv, right_ndv) {
809                    (Some(l), Some(r)) if l > 0 && r > 0 => {
810                        ndv_divisor = ndv_divisor.saturating_mul(l.max(r));
811                    }
812                    _ => return left_rows.saturating_mul(right_rows),
813                }
814            }
815            let max_rows = left_rows.saturating_mul(right_rows);
816            max_rows.checked_div(ndv_divisor).unwrap_or(max_rows)
817        }
818
819        let (inner_estimate, is_exact_cartesian, join_type) = if let Some(hash_join) =
820            plan.downcast_ref::<HashJoinExec>()
821        {
822            let est =
823                equi_join_estimate(hash_join.on(), left, right, left_rows, right_rows);
824            (est, false, *hash_join.join_type())
825        } else if let Some(smj) = plan.downcast_ref::<SortMergeJoinExec>() {
826            let est = equi_join_estimate(smj.on(), left, right, left_rows, right_rows);
827            (est, false, smj.join_type())
828        } else if plan.downcast_ref::<CrossJoinExec>().is_some() {
829            let both_exact = left.num_rows.is_exact().unwrap_or(false)
830                && right.num_rows.is_exact().unwrap_or(false);
831            (
832                left_rows.saturating_mul(right_rows),
833                both_exact,
834                JoinType::Inner,
835            )
836        } else {
837            return Ok(StatisticsResult::Delegate);
838        };
839
840        // Apply join-type-aware cardinality bounds
841        let estimated = match join_type {
842            JoinType::Inner => inner_estimate,
843            JoinType::Left => inner_estimate.max(left_rows),
844            JoinType::Right => inner_estimate.max(right_rows),
845            JoinType::Full => {
846                // At least left + right - matched, but never less than inner
847                let outer_bound = left_rows
848                    .saturating_add(right_rows)
849                    .saturating_sub(inner_estimate);
850                inner_estimate.max(outer_bound)
851            }
852            JoinType::LeftSemi => inner_estimate.min(left_rows),
853            JoinType::RightSemi => inner_estimate.min(right_rows),
854            JoinType::LeftAnti => left_rows.saturating_sub(inner_estimate.min(left_rows)),
855            JoinType::RightAnti => {
856                right_rows.saturating_sub(inner_estimate.min(right_rows))
857            }
858            JoinType::LeftMark => left_rows,
859            JoinType::RightMark => right_rows,
860        };
861
862        // NL join inner with exact inputs is an exact Cartesian product;
863        // NDV-based estimates are inherently inexact.
864        let num_rows = if is_exact_cartesian && join_type == JoinType::Inner {
865            Precision::Exact(estimated)
866        } else {
867            Precision::Inexact(estimated)
868        };
869
870        computed_with_row_count(plan, num_rows)
871    }
872}
873
874/// Statistics provider for [`LocalLimitExec`](crate::limit::LocalLimitExec) and
875/// [`GlobalLimitExec`](crate::limit::GlobalLimitExec).
876///
877/// Caps output row count at the limit value, accounting for any leading skip offset
878/// in `GlobalLimitExec`.
879#[derive(Debug, Default)]
880pub struct LimitStatisticsProvider;
881
882impl StatisticsProvider for LimitStatisticsProvider {
883    fn compute_statistics(
884        &self,
885        plan: &dyn ExecutionPlan,
886        child_stats: &[ExtendedStatistics],
887    ) -> Result<StatisticsResult> {
888        use crate::limit::{GlobalLimitExec, LocalLimitExec};
889
890        if child_stats.is_empty() {
891            return Ok(StatisticsResult::Delegate);
892        }
893
894        let (skip, fetch) = if let Some(limit) = plan.downcast_ref::<LocalLimitExec>() {
895            (0usize, Some(limit.fetch()))
896        } else if let Some(limit) = plan.downcast_ref::<GlobalLimitExec>() {
897            (limit.skip(), limit.fetch())
898        } else {
899            return Ok(StatisticsResult::Delegate);
900        };
901
902        let num_rows = match child_stats[0].base.num_rows {
903            Precision::Exact(rows) => {
904                let available = rows.saturating_sub(skip);
905                Precision::Exact(fetch.map_or(available, |f| available.min(f)))
906            }
907            Precision::Inexact(rows) => {
908                let available = rows.saturating_sub(skip);
909                match fetch {
910                    Some(f) => Precision::Inexact(available.min(f)),
911                    None => Precision::Inexact(available),
912                }
913            }
914            Precision::Absent => match fetch {
915                Some(f) => Precision::Inexact(f),
916                None => Precision::Absent,
917            },
918        };
919
920        computed_with_row_count(plan, num_rows)
921    }
922}
923
924/// Statistics provider for [`UnionExec`](crate::union::UnionExec).
925///
926/// Sums row counts across all inputs.
927#[derive(Debug, Default)]
928pub struct UnionStatisticsProvider;
929
930impl StatisticsProvider for UnionStatisticsProvider {
931    fn compute_statistics(
932        &self,
933        plan: &dyn ExecutionPlan,
934        child_stats: &[ExtendedStatistics],
935    ) -> Result<StatisticsResult> {
936        use crate::union::UnionExec;
937
938        if plan.downcast_ref::<UnionExec>().is_none() {
939            return Ok(StatisticsResult::Delegate);
940        }
941
942        let total = child_stats.iter().try_fold(
943            Precision::Exact(0usize),
944            |acc, s| -> Result<Precision<usize>> {
945                Ok(match (acc, s.base.num_rows) {
946                    (Precision::Absent, _) | (_, Precision::Absent) => Precision::Absent,
947                    (Precision::Exact(a), Precision::Exact(b)) => {
948                        Precision::Exact(a.saturating_add(b))
949                    }
950                    (Precision::Inexact(a), Precision::Exact(b))
951                    | (Precision::Exact(a), Precision::Inexact(b))
952                    | (Precision::Inexact(a), Precision::Inexact(b)) => {
953                        Precision::Inexact(a.saturating_add(b))
954                    }
955                })
956            },
957        )?;
958
959        computed_with_row_count(plan, total)
960    }
961}
962
963type ProviderFn = dyn Fn(&dyn ExecutionPlan, &[ExtendedStatistics]) -> Result<StatisticsResult>
964    + Send
965    + Sync;
966
967/// A [`StatisticsProvider`] backed by a user-supplied closure.
968///
969/// Useful for injecting custom statistics in tests or for cardinality feedback
970/// pipelines where real runtime statistics need to override plan estimates.
971/// The closure receives the current plan node and its children's enhanced
972/// statistics, returning a [`StatisticsResult`].
973///
974/// To distinguish between multiple nodes of the same type (e.g., two
975/// `FilterExec` nodes), match on structural properties like the input schema's
976/// column names, number of columns, or child row counts.
977///
978/// # Example
979///
980/// ```rust,ignore (requires crate-internal imports)
981/// let provider = ClosureStatisticsProvider::new(|plan, child_stats| {
982///     if plan.downcast_ref::<FilterExec>().is_some() {
983///         Ok(StatisticsResult::Computed(ExtendedStatistics::from(Statistics {
984///             num_rows: Precision::Inexact(42),
985///             ..Statistics::new_unknown(plan.schema().as_ref())
986///         })))
987///     } else {
988///         Ok(StatisticsResult::Delegate)
989///     }
990/// });
991/// ```
992pub struct ClosureStatisticsProvider {
993    f: Box<ProviderFn>,
994}
995
996impl ClosureStatisticsProvider {
997    /// Create a new provider from a closure.
998    pub fn new(
999        f: impl Fn(&dyn ExecutionPlan, &[ExtendedStatistics]) -> Result<StatisticsResult>
1000        + Send
1001        + Sync
1002        + 'static,
1003    ) -> Self {
1004        Self { f: Box::new(f) }
1005    }
1006}
1007
1008impl Debug for ClosureStatisticsProvider {
1009    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1010        write!(f, "ClosureStatisticsProvider")
1011    }
1012}
1013
1014impl StatisticsProvider for ClosureStatisticsProvider {
1015    fn compute_statistics(
1016        &self,
1017        plan: &dyn ExecutionPlan,
1018        child_stats: &[ExtendedStatistics],
1019    ) -> Result<StatisticsResult> {
1020        (self.f)(plan, child_stats)
1021    }
1022}
1023
1024#[cfg(test)]
1025mod tests {
1026    use super::*;
1027    use crate::filter::FilterExec;
1028    use crate::projection::ProjectionExec;
1029    use crate::statistics::StatisticsArgs;
1030    use crate::{
1031        ChildrenPropertiesMode, DisplayAs, DisplayFormatType, PlanProperties,
1032        ReplaceChildrenOptions,
1033    };
1034    use arrow::datatypes::{DataType, Field, Schema};
1035    use datafusion_common::stats::Precision;
1036    use datafusion_common::{ColumnStatistics, ScalarValue};
1037    use datafusion_expr::Operator;
1038    use datafusion_physical_expr::PhysicalExpr;
1039    use datafusion_physical_expr::expressions::{BinaryExpr, Column, Literal, col, lit};
1040    use datafusion_physical_expr::{EquivalenceProperties, Partitioning};
1041    use std::fmt;
1042
1043    use crate::execution_plan::{Boundedness, EmissionType};
1044    use datafusion_common::tree_node::TreeNodeRecursion;
1045
1046    fn make_schema() -> Arc<Schema> {
1047        Arc::new(Schema::new(vec![
1048            Field::new("a", DataType::Int32, false),
1049            Field::new("b", DataType::Int32, false),
1050        ]))
1051    }
1052
1053    #[derive(Debug)]
1054    struct MockSourceExec {
1055        schema: Arc<Schema>,
1056        stats: Statistics,
1057        cache: Arc<PlanProperties>,
1058    }
1059
1060    impl MockSourceExec {
1061        fn new(schema: Arc<Schema>, num_rows: Precision<usize>) -> Self {
1062            let num_cols = schema.fields().len();
1063            Self::with_column_stats(
1064                schema,
1065                num_rows,
1066                vec![ColumnStatistics::new_unknown(); num_cols],
1067            )
1068        }
1069
1070        fn with_column_stats(
1071            schema: Arc<Schema>,
1072            num_rows: Precision<usize>,
1073            column_statistics: Vec<ColumnStatistics>,
1074        ) -> Self {
1075            let eq_properties = EquivalenceProperties::new(Arc::clone(&schema));
1076            let cache = Arc::new(PlanProperties::new(
1077                eq_properties,
1078                Partitioning::UnknownPartitioning(1),
1079                EmissionType::Incremental,
1080                Boundedness::Bounded,
1081            ));
1082            Self {
1083                schema,
1084                stats: Statistics {
1085                    num_rows,
1086                    total_byte_size: Precision::Absent,
1087                    column_statistics,
1088                },
1089                cache,
1090            }
1091        }
1092    }
1093
1094    impl DisplayAs for MockSourceExec {
1095        fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
1096            write!(f, "MockSourceExec")
1097        }
1098    }
1099
1100    impl ExecutionPlan for MockSourceExec {
1101        fn name(&self) -> &str {
1102            "MockSourceExec"
1103        }
1104
1105        fn schema(&self) -> Arc<Schema> {
1106            Arc::clone(&self.schema)
1107        }
1108
1109        fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1110            vec![]
1111        }
1112
1113        fn replace_children(
1114            self: Arc<Self>,
1115            _: Vec<Arc<dyn ExecutionPlan>>,
1116            _: ReplaceChildrenOptions,
1117        ) -> Result<Arc<dyn ExecutionPlan>> {
1118            Ok(self)
1119        }
1120
1121        fn with_new_children(
1122            self: Arc<Self>,
1123            children: Vec<Arc<dyn ExecutionPlan>>,
1124        ) -> Result<Arc<dyn ExecutionPlan>> {
1125            self.replace_children(
1126                children,
1127                ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
1128            )
1129        }
1130
1131        fn properties(&self) -> &Arc<PlanProperties> {
1132            &self.cache
1133        }
1134
1135        fn apply_expressions(
1136            &self,
1137            _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
1138        ) -> Result<TreeNodeRecursion> {
1139            Ok(TreeNodeRecursion::Continue)
1140        }
1141
1142        fn execute(
1143            &self,
1144            _partition: usize,
1145            _context: Arc<datafusion_execution::TaskContext>,
1146        ) -> Result<crate::SendableRecordBatchStream> {
1147            unimplemented!()
1148        }
1149
1150        fn statistics_from_inputs(
1151            &self,
1152            _input_stats: &[Arc<Statistics>],
1153            _args: &StatisticsArgs,
1154        ) -> Result<Arc<Statistics>> {
1155            Ok(Arc::new(self.stats.clone()))
1156        }
1157    }
1158
1159    fn make_source(num_rows: usize) -> Arc<dyn ExecutionPlan> {
1160        Arc::new(MockSourceExec::new(
1161            make_schema(),
1162            Precision::Exact(num_rows),
1163        ))
1164    }
1165
1166    #[test]
1167    fn test_default_provider() -> Result<()> {
1168        let engine = StatisticsRegistry::new();
1169        let source = make_source(1000);
1170
1171        let stats = engine.compute(source.as_ref())?;
1172        assert!(matches!(stats.base.num_rows, Precision::Exact(1000)));
1173        Ok(())
1174    }
1175
1176    #[test]
1177    fn test_custom_chain_configuration() -> Result<()> {
1178        let source = make_source(1000);
1179
1180        // Test with_providers: fully custom chain (no default)
1181        let custom_only =
1182            StatisticsRegistry::with_providers(vec![Arc::new(CustomStatisticsProvider)]);
1183        // CustomStatisticsProvider only handles CustomExec, delegates for others
1184        // With no default provider, filter returns fallback statistics
1185        let filter: Arc<dyn ExecutionPlan> =
1186            Arc::new(FilterExec::try_new(lit(true), Arc::clone(&source))?);
1187        let stats = custom_only.compute(filter.as_ref())?;
1188        // Falls back to plan.statistics() since no provider handles it
1189        assert!(stats.base.num_rows.get_value().is_some());
1190
1191        // Test with_providers: custom provider + built-in fallback
1192        let with_override =
1193            StatisticsRegistry::with_providers(vec![Arc::new(OverrideFilterProvider {
1194                fixed_selectivity: 0.25,
1195            })
1196                as Arc<dyn StatisticsProvider>]);
1197        // OverrideFilterProvider handles filters, built-in fallback handles the rest
1198        let stats = with_override.compute(filter.as_ref())?;
1199        assert!(matches!(stats.base.num_rows, Precision::Inexact(250)));
1200
1201        // Verify chain inspection
1202        assert_eq!(with_override.providers().len(), 1);
1203
1204        Ok(())
1205    }
1206
1207    #[derive(Debug)]
1208    struct CustomExec {
1209        input: Arc<dyn ExecutionPlan>,
1210    }
1211
1212    impl DisplayAs for CustomExec {
1213        fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
1214            write!(f, "CustomExec")
1215        }
1216    }
1217
1218    impl ExecutionPlan for CustomExec {
1219        fn name(&self) -> &str {
1220            "CustomExec"
1221        }
1222
1223        fn schema(&self) -> Arc<Schema> {
1224            self.input.schema()
1225        }
1226
1227        fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
1228            vec![&self.input]
1229        }
1230
1231        fn replace_children(
1232            self: Arc<Self>,
1233            children: Vec<Arc<dyn ExecutionPlan>>,
1234            _: ReplaceChildrenOptions,
1235        ) -> Result<Arc<dyn ExecutionPlan>> {
1236            Ok(Arc::new(CustomExec {
1237                input: Arc::clone(&children[0]),
1238            }))
1239        }
1240
1241        fn with_new_children(
1242            self: Arc<Self>,
1243            children: Vec<Arc<dyn ExecutionPlan>>,
1244        ) -> Result<Arc<dyn ExecutionPlan>> {
1245            self.replace_children(
1246                children,
1247                ReplaceChildrenOptions::new(ChildrenPropertiesMode::Recompute),
1248            )
1249        }
1250
1251        fn properties(&self) -> &Arc<PlanProperties> {
1252            self.input.properties()
1253        }
1254
1255        fn apply_expressions(
1256            &self,
1257            _f: &mut dyn FnMut(&Arc<dyn PhysicalExpr>) -> Result<TreeNodeRecursion>,
1258        ) -> Result<TreeNodeRecursion> {
1259            Ok(TreeNodeRecursion::Continue)
1260        }
1261
1262        fn execute(
1263            &self,
1264            _partition: usize,
1265            _context: Arc<datafusion_execution::TaskContext>,
1266        ) -> Result<crate::SendableRecordBatchStream> {
1267            unimplemented!()
1268        }
1269    }
1270
1271    #[derive(Debug)]
1272    struct CustomStatisticsProvider;
1273
1274    impl StatisticsProvider for CustomStatisticsProvider {
1275        fn compute_statistics(
1276            &self,
1277            plan: &dyn ExecutionPlan,
1278            child_stats: &[ExtendedStatistics],
1279        ) -> Result<StatisticsResult> {
1280            if plan.downcast_ref::<CustomExec>().is_some() {
1281                Ok(StatisticsResult::Computed(child_stats[0].clone()))
1282            } else {
1283                Ok(StatisticsResult::Delegate)
1284            }
1285        }
1286    }
1287
1288    #[test]
1289    fn test_custom_provider_for_custom_exec() -> Result<()> {
1290        let mut engine = StatisticsRegistry::new();
1291        engine.register(Arc::new(CustomStatisticsProvider));
1292
1293        let source = make_source(1000);
1294        let custom: Arc<dyn ExecutionPlan> = Arc::new(CustomExec { input: source });
1295
1296        let stats = engine.compute(custom.as_ref())?;
1297        assert!(matches!(stats.base.num_rows, Precision::Exact(1000)));
1298        Ok(())
1299    }
1300
1301    #[derive(Debug)]
1302    struct OverrideFilterProvider {
1303        fixed_selectivity: f64,
1304    }
1305
1306    impl StatisticsProvider for OverrideFilterProvider {
1307        fn compute_statistics(
1308            &self,
1309            plan: &dyn ExecutionPlan,
1310            child_stats: &[ExtendedStatistics],
1311        ) -> Result<StatisticsResult> {
1312            if plan.downcast_ref::<FilterExec>().is_some() {
1313                if let Some(&input_rows) = child_stats[0].base.num_rows.get_value() {
1314                    let estimated = (input_rows as f64 * self.fixed_selectivity) as usize;
1315                    Ok(StatisticsResult::Computed(ExtendedStatistics::from(
1316                        Statistics {
1317                            num_rows: Precision::Inexact(estimated),
1318                            total_byte_size: Precision::Absent,
1319                            column_statistics: child_stats[0]
1320                                .base
1321                                .column_statistics
1322                                .clone(),
1323                        },
1324                    )))
1325                } else {
1326                    Ok(StatisticsResult::Delegate)
1327                }
1328            } else {
1329                Ok(StatisticsResult::Delegate)
1330            }
1331        }
1332    }
1333
1334    #[test]
1335    fn test_override_builtin_operator() -> Result<()> {
1336        let mut engine = StatisticsRegistry::new();
1337        engine.register(Arc::new(OverrideFilterProvider {
1338            fixed_selectivity: 0.1,
1339        }));
1340
1341        let source = make_source(1000);
1342        let filter: Arc<dyn ExecutionPlan> =
1343            Arc::new(FilterExec::try_new(lit(true), source)?);
1344
1345        let stats = engine.compute(filter.as_ref())?;
1346        assert!(matches!(stats.base.num_rows, Precision::Inexact(100)));
1347        Ok(())
1348    }
1349
1350    #[test]
1351    fn test_filter_statistics_propagation() -> Result<()> {
1352        let engine = StatisticsRegistry::new();
1353        let source = make_source(1000);
1354        let predicate = lit(true);
1355        let filter: Arc<dyn ExecutionPlan> =
1356            Arc::new(FilterExec::try_new(predicate, source)?);
1357
1358        let stats = engine.compute(filter.as_ref())?;
1359        assert!(stats.base.num_rows.get_value().unwrap_or(&0) <= &1000);
1360        Ok(())
1361    }
1362
1363    #[test]
1364    fn test_filter_adjusts_ndv_by_selectivity() -> Result<()> {
1365        use datafusion_common::ScalarValue;
1366        use datafusion_expr::Operator;
1367        use datafusion_physical_expr::expressions::{
1368            BinaryExpr, Column as PhysColumn, Literal,
1369        };
1370
1371        // Source: 1000 rows, NDV(a)=1000 (unique), NDV(b)=800 (near-unique)
1372        // With NDV close to num_rows, each value has ~1.25 rows, so filtering
1373        // visibly reduces the number of surviving distinct values.
1374        let schema = make_schema(); // "a" Int32, "b" Int32
1375        let col_stats = vec![
1376            {
1377                let mut cs = ColumnStatistics::new_unknown();
1378                cs.distinct_count = Precision::Exact(1000);
1379                cs.min_value = Precision::Exact(ScalarValue::Int32(Some(1)));
1380                cs.max_value = Precision::Exact(ScalarValue::Int32(Some(1000)));
1381                cs
1382            },
1383            {
1384                let mut cs = ColumnStatistics::new_unknown();
1385                cs.distinct_count = Precision::Exact(800);
1386                cs.min_value = Precision::Exact(ScalarValue::Int32(Some(1)));
1387                cs.max_value = Precision::Exact(ScalarValue::Int32(Some(800)));
1388                cs
1389            },
1390        ];
1391        let source: Arc<dyn ExecutionPlan> = Arc::new(MockSourceExec::with_column_stats(
1392            schema,
1393            Precision::Exact(1000),
1394            col_stats,
1395        ));
1396
1397        // Filter: a > 900 (selectivity ~10%, keeps values 901-1000)
1398        let predicate: Arc<dyn PhysicalExpr> = Arc::new(BinaryExpr::new(
1399            Arc::new(PhysColumn::new("a", 0)),
1400            Operator::Gt,
1401            Arc::new(Literal::new(ScalarValue::Int32(Some(900)))),
1402        ));
1403        let filter: Arc<dyn ExecutionPlan> =
1404            Arc::new(FilterExec::try_new(predicate, source)?);
1405
1406        let registry = StatisticsRegistry::with_providers(vec![
1407            Arc::new(FilterStatisticsProvider),
1408            Arc::new(DefaultStatisticsProvider),
1409        ]);
1410        let stats = registry.compute(filter.as_ref())?;
1411
1412        let output_ndv_a = stats.base.column_statistics[0]
1413            .distinct_count
1414            .get_value()
1415            .copied()
1416            .unwrap_or(0);
1417        let output_ndv_b = stats.base.column_statistics[1]
1418            .distinct_count
1419            .get_value()
1420            .copied()
1421            .unwrap_or(0);
1422
1423        // NDV(a): interval analysis narrows to [901,1000] -> ~100 distinct values
1424        assert!(
1425            output_ndv_a <= 100,
1426            "Expected NDV(a) <= 100 after filter, got {output_ndv_a}"
1427        );
1428        // NDV(b): not in predicate, but selectivity ~10% with 1.25 rows/value
1429        // means many distinct values are lost. ndv_after_selectivity(800, 1000, 0.1)
1430        // gives ~76. Significantly less than the original 800.
1431        assert!(
1432            output_ndv_b < 200,
1433            "Expected NDV(b) < 200 after filter, got {output_ndv_b}"
1434        );
1435        Ok(())
1436    }
1437
1438    #[test]
1439    fn test_projection_statistics_propagation() -> Result<()> {
1440        let engine = StatisticsRegistry::new();
1441        let source = make_source(1000);
1442        let schema = make_schema();
1443        let proj: Arc<dyn ExecutionPlan> = Arc::new(ProjectionExec::try_new(
1444            vec![(col("a", &schema)?, "a".to_string())],
1445            source,
1446        )?);
1447
1448        let stats = engine.compute(proj.as_ref())?;
1449        assert!(matches!(stats.base.num_rows, Precision::Exact(1000)));
1450        Ok(())
1451    }
1452
1453    #[test]
1454    fn test_passthrough_statistics_propagation() -> Result<()> {
1455        use crate::coalesce_partitions::CoalescePartitionsExec;
1456
1457        let engine = StatisticsRegistry::new();
1458        let source = make_source(1000);
1459        let coalesce: Arc<dyn ExecutionPlan> =
1460            Arc::new(CoalescePartitionsExec::new(source));
1461
1462        let stats = engine.compute(coalesce.as_ref())?;
1463        // PassthroughStatisticsProvider should propagate child row count unchanged
1464        assert_eq!(stats.base.num_rows, Precision::Exact(1000));
1465        Ok(())
1466    }
1467
1468    #[test]
1469    fn test_chain_priority() -> Result<()> {
1470        let mut engine = StatisticsRegistry::new();
1471        engine.register(Arc::new(OverrideFilterProvider {
1472            fixed_selectivity: 0.5,
1473        }));
1474        engine.register(Arc::new(CustomStatisticsProvider));
1475
1476        let source = make_source(1000);
1477
1478        // CustomExec handled by CustomStatisticsProvider
1479        let custom: Arc<dyn ExecutionPlan> = Arc::new(CustomExec {
1480            input: Arc::clone(&source),
1481        });
1482        let stats = engine.compute(custom.as_ref())?;
1483        assert!(matches!(stats.base.num_rows, Precision::Exact(1000)));
1484
1485        // FilterExec: CustomStatisticsProvider delegates, OverrideFilterProvider handles
1486        let filter: Arc<dyn ExecutionPlan> =
1487            Arc::new(FilterExec::try_new(lit(true), source)?);
1488        let stats = engine.compute(filter.as_ref())?;
1489        assert!(matches!(stats.base.num_rows, Precision::Inexact(500)));
1490
1491        Ok(())
1492    }
1493
1494    // =========================================================================
1495    // num_distinct_vals Utility Tests
1496    // =========================================================================
1497
1498    #[test]
1499    fn test_num_distinct_vals_basic() {
1500        assert_eq!(num_distinct_vals(0, 100), 0);
1501        assert_eq!(num_distinct_vals(100, 0), 0);
1502        assert_eq!(num_distinct_vals(100, 100), 100);
1503        assert_eq!(num_distinct_vals(100, 200), 100);
1504
1505        let ndv = num_distinct_vals(1000, 100);
1506        assert!((90..=100).contains(&ndv), "Expected ~95, got {ndv}");
1507
1508        let ndv = num_distinct_vals(1000, 500);
1509        assert!((350..=450).contains(&ndv), "Expected ~393, got {ndv}");
1510
1511        let ndv = num_distinct_vals(1_000_000, 10_000);
1512        assert!((9900..=10000).contains(&ndv), "Expected ~9950, got {ndv}");
1513
1514        let ndv = num_distinct_vals(1_000_000, 100);
1515        assert!((99..=100).contains(&ndv), "Expected ~100, got {ndv}");
1516    }
1517
1518    #[test]
1519    fn test_num_distinct_vals_small_domain() {
1520        let ndv = num_distinct_vals(10, 5);
1521        assert!((3..=5).contains(&ndv), "Expected ~4, got {ndv}");
1522
1523        assert_eq!(num_distinct_vals(10, 20), 10);
1524        assert_eq!(num_distinct_vals(10, 1), 1);
1525    }
1526
1527    #[test]
1528    fn test_ndv_after_selectivity() {
1529        let ndv = ndv_after_selectivity(1000, 10000, 0.1);
1530        assert!((600..=700).contains(&ndv), "Expected ~632, got {ndv}");
1531
1532        let ndv = ndv_after_selectivity(1000, 10000, 0.01);
1533        assert!((90..=100).contains(&ndv), "Expected ~95, got {ndv}");
1534
1535        assert_eq!(ndv_after_selectivity(1000, 10000, 0.0), 0);
1536        assert_eq!(ndv_after_selectivity(1000, 10000, 1.0), 1000);
1537        assert_eq!(ndv_after_selectivity(0, 10000, 0.5), 0);
1538    }
1539
1540    // =========================================================================
1541    // AggregateStatisticsProvider tests
1542    // =========================================================================
1543
1544    use crate::aggregates::{AggregateExec, AggregateMode, PhysicalGroupBy};
1545
1546    fn make_source_with_ndv(
1547        num_rows: usize,
1548        col_ndvs: Vec<Option<usize>>,
1549    ) -> Arc<dyn ExecutionPlan> {
1550        let fields: Vec<Field> = col_ndvs
1551            .iter()
1552            .enumerate()
1553            .map(|(i, _)| Field::new(format!("c{i}"), DataType::Int32, false))
1554            .collect();
1555        let schema = Arc::new(Schema::new(fields));
1556        let col_stats = col_ndvs
1557            .into_iter()
1558            .map(|ndv| {
1559                let mut cs = ColumnStatistics::new_unknown();
1560                if let Some(n) = ndv {
1561                    cs.distinct_count = Precision::Exact(n);
1562                }
1563                cs
1564            })
1565            .collect();
1566        Arc::new(MockSourceExec::with_column_stats(
1567            schema,
1568            Precision::Exact(num_rows),
1569            col_stats,
1570        ))
1571    }
1572
1573    fn make_aggregate(
1574        input: Arc<dyn ExecutionPlan>,
1575        group_by: PhysicalGroupBy,
1576    ) -> Result<Arc<dyn ExecutionPlan>> {
1577        Ok(Arc::new(AggregateExec::try_new(
1578            AggregateMode::Single,
1579            group_by,
1580            vec![],
1581            vec![],
1582            Arc::clone(&input),
1583            input.schema(),
1584        )?))
1585    }
1586
1587    #[test]
1588    fn test_aggregate_provider_with_ndv() -> Result<()> {
1589        let source = make_source_with_ndv(100, vec![Some(10)]);
1590        let group_by = PhysicalGroupBy::new_single(vec![(
1591            Arc::new(Column::new("c0", 0)),
1592            "c0".to_string(),
1593        )]);
1594        let agg = make_aggregate(source, group_by)?;
1595
1596        let registry = StatisticsRegistry::with_providers(vec![
1597            Arc::new(AggregateStatisticsProvider),
1598            Arc::new(DefaultStatisticsProvider),
1599        ]);
1600        let stats = registry.compute(agg.as_ref())?;
1601        assert_eq!(stats.base.num_rows, Precision::Inexact(10));
1602        Ok(())
1603    }
1604
1605    #[test]
1606    fn test_aggregate_provider_multi_column() -> Result<()> {
1607        let source = make_source_with_ndv(1000, vec![Some(10), Some(5)]);
1608        let group_by = PhysicalGroupBy::new_single(vec![
1609            (Arc::new(Column::new("c0", 0)), "c0".to_string()),
1610            (Arc::new(Column::new("c1", 1)), "c1".to_string()),
1611        ]);
1612        let agg = make_aggregate(source, group_by)?;
1613
1614        let registry = StatisticsRegistry::with_providers(vec![
1615            Arc::new(AggregateStatisticsProvider),
1616            Arc::new(DefaultStatisticsProvider),
1617        ]);
1618        let stats = registry.compute(agg.as_ref())?;
1619        // 10 * 5 = 50
1620        assert_eq!(stats.base.num_rows, Precision::Inexact(50));
1621        Ok(())
1622    }
1623
1624    #[test]
1625    fn test_aggregate_provider_caps_at_input_rows() -> Result<()> {
1626        // NDV product (100 * 100 = 10_000) exceeds input rows (500)
1627        let source = make_source_with_ndv(500, vec![Some(100), Some(100)]);
1628        let group_by = PhysicalGroupBy::new_single(vec![
1629            (Arc::new(Column::new("c0", 0)), "c0".to_string()),
1630            (Arc::new(Column::new("c1", 1)), "c1".to_string()),
1631        ]);
1632        let agg = make_aggregate(source, group_by)?;
1633
1634        let registry = StatisticsRegistry::with_providers(vec![
1635            Arc::new(AggregateStatisticsProvider),
1636            Arc::new(DefaultStatisticsProvider),
1637        ]);
1638        let stats = registry.compute(agg.as_ref())?;
1639        assert_eq!(stats.base.num_rows, Precision::Inexact(500));
1640        Ok(())
1641    }
1642
1643    #[test]
1644    fn test_aggregate_provider_no_ndv_delegates() -> Result<()> {
1645        // No NDV on the GROUP BY column
1646        let source = make_source_with_ndv(100, vec![None]);
1647        let group_by = PhysicalGroupBy::new_single(vec![(
1648            Arc::new(Column::new("c0", 0)),
1649            "c0".to_string(),
1650        )]);
1651        let agg = make_aggregate(source, group_by)?;
1652
1653        let registry = StatisticsRegistry::with_providers(vec![
1654            Arc::new(AggregateStatisticsProvider),
1655            Arc::new(DefaultStatisticsProvider),
1656        ]);
1657        let stats = registry.compute(agg.as_ref())?;
1658        // Delegates to DefaultStatisticsProvider, which calls partition_statistics
1659        assert!(
1660            stats.base.num_rows.get_value().is_some()
1661                || matches!(stats.base.num_rows, Precision::Absent)
1662        );
1663        Ok(())
1664    }
1665
1666    #[test]
1667    fn test_aggregate_provider_non_column_expr_delegates() -> Result<()> {
1668        let source = make_source_with_ndv(100, vec![Some(10), Some(5)]);
1669        // GROUP BY an expression (c0 + c1), not a simple column ref
1670        let expr: Arc<dyn PhysicalExpr> = Arc::new(BinaryExpr::new(
1671            Arc::new(Column::new("c0", 0)),
1672            Operator::Plus,
1673            Arc::new(Column::new("c1", 1)),
1674        ));
1675        let group_by = PhysicalGroupBy::new_single(vec![(expr, "sum".to_string())]);
1676        let agg = make_aggregate(source, group_by)?;
1677
1678        let registry = StatisticsRegistry::with_providers(vec![
1679            Arc::new(AggregateStatisticsProvider),
1680            Arc::new(DefaultStatisticsProvider),
1681        ]);
1682        let stats = registry.compute(agg.as_ref())?;
1683        // Should delegate (expression is not a Column)
1684        assert!(
1685            stats.base.num_rows.get_value().is_some()
1686                || matches!(stats.base.num_rows, Precision::Absent)
1687        );
1688        Ok(())
1689    }
1690
1691    #[test]
1692    fn test_aggregate_provider_grouping_sets() -> Result<()> {
1693        let source = make_source_with_ndv(1000, vec![Some(10), Some(5)]);
1694        // GROUPING SETS: (c0, c1), (c0), (c1) -> 3 groups
1695        let group_by = PhysicalGroupBy::new(
1696            vec![
1697                (Arc::new(Column::new("c0", 0)), "c0".to_string()),
1698                (Arc::new(Column::new("c1", 1)), "c1".to_string()),
1699            ],
1700            vec![
1701                (
1702                    Arc::new(Literal::new(ScalarValue::Int32(None))),
1703                    "c0".to_string(),
1704                ),
1705                (
1706                    Arc::new(Literal::new(ScalarValue::Int32(None))),
1707                    "c1".to_string(),
1708                ),
1709            ],
1710            vec![
1711                vec![false, true],  // (c0, NULL) - group by c0 only
1712                vec![true, false],  // (NULL, c1) - group by c1 only
1713                vec![false, false], // (c0, c1)   - group by both
1714            ],
1715            true,
1716        );
1717        let agg = make_aggregate(source, group_by)?;
1718
1719        let registry = StatisticsRegistry::with_providers(vec![
1720            Arc::new(AggregateStatisticsProvider),
1721            Arc::new(DefaultStatisticsProvider),
1722        ]);
1723        let stats = registry.compute(agg.as_ref())?;
1724        // Multiple grouping sets: provider delegates to DefaultStatisticsProvider,
1725        // which calls the built-in partition_statistics for correct per-set
1726        // NDV estimation. The exact value depends on the built-in implementation.
1727        assert!(
1728            stats.base.num_rows.get_value().is_some()
1729                || matches!(stats.base.num_rows, Precision::Absent)
1730        );
1731        Ok(())
1732    }
1733
1734    #[test]
1735    fn test_aggregate_provider_partial_delegates() -> Result<()> {
1736        // Partial aggregates produce per-partition groups; the provider
1737        // should delegate rather than applying global NDV bounds.
1738        let source = make_source_with_ndv(100, vec![Some(10)]);
1739        let group_by = PhysicalGroupBy::new_single(vec![(
1740            Arc::new(Column::new("c0", 0)),
1741            "c0".to_string(),
1742        )]);
1743        let agg: Arc<dyn ExecutionPlan> = Arc::new(AggregateExec::try_new(
1744            AggregateMode::Partial,
1745            group_by,
1746            vec![],
1747            vec![],
1748            Arc::clone(&source),
1749            source.schema(),
1750        )?);
1751
1752        let registry = StatisticsRegistry::with_providers(vec![
1753            Arc::new(AggregateStatisticsProvider),
1754            Arc::new(DefaultStatisticsProvider),
1755        ]);
1756        let stats = registry.compute(agg.as_ref())?;
1757        // Should fall through to DefaultStatisticsProvider (partition_statistics).
1758        // The exact value depends on the built-in implementation.
1759        assert!(
1760            stats.base.num_rows.get_value().is_some()
1761                || matches!(stats.base.num_rows, Precision::Absent)
1762        );
1763        Ok(())
1764    }
1765
1766    // =========================================================================
1767    // JoinStatisticsProvider tests
1768    // =========================================================================
1769
1770    use crate::joins::{HashJoinExec, PartitionMode};
1771    use datafusion_common::{JoinType, NullEquality};
1772
1773    fn make_source_with_ndv_2col(
1774        num_rows: usize,
1775        ndv_a: Option<usize>,
1776    ) -> Arc<dyn ExecutionPlan> {
1777        let schema = make_schema(); // "a" Int32, "b" Int32
1778        let col_stats = vec![
1779            {
1780                let mut cs = ColumnStatistics::new_unknown();
1781                if let Some(n) = ndv_a {
1782                    cs.distinct_count = Precision::Exact(n);
1783                }
1784                cs
1785            },
1786            ColumnStatistics::new_unknown(),
1787        ];
1788        Arc::new(MockSourceExec::with_column_stats(
1789            schema,
1790            Precision::Exact(num_rows),
1791            col_stats,
1792        ))
1793    }
1794
1795    fn make_hash_join(
1796        left: Arc<dyn ExecutionPlan>,
1797        right: Arc<dyn ExecutionPlan>,
1798    ) -> Result<Arc<dyn ExecutionPlan>> {
1799        let _schema = make_schema();
1800        let on: crate::joins::JoinOn = vec![(
1801            Arc::new(Column::new("a", 0)) as Arc<dyn PhysicalExpr>,
1802            Arc::new(Column::new("a", 0)) as Arc<dyn PhysicalExpr>,
1803        )];
1804        Ok(Arc::new(HashJoinExec::try_new(
1805            left,
1806            right,
1807            on,
1808            None,
1809            &JoinType::Inner,
1810            None,
1811            PartitionMode::CollectLeft,
1812            NullEquality::NullEqualsNull,
1813            false,
1814        )?))
1815    }
1816
1817    #[test]
1818    fn test_join_provider_with_ndv() -> Result<()> {
1819        // left: 1000 rows, NDV(a)=100; right: 500 rows, NDV(a)=50
1820        // expected = 1000 * 500 / max(100, 50) = 5000
1821        let left = make_source_with_ndv_2col(1000, Some(100));
1822        let right = make_source_with_ndv_2col(500, Some(50));
1823        let join = make_hash_join(left, right)?;
1824
1825        let registry = StatisticsRegistry::with_providers(vec![
1826            Arc::new(JoinStatisticsProvider),
1827            Arc::new(DefaultStatisticsProvider),
1828        ]);
1829        let stats = registry.compute(join.as_ref())?;
1830        assert_eq!(stats.base.num_rows, Precision::Inexact(5000));
1831        Ok(())
1832    }
1833
1834    #[test]
1835    fn test_join_provider_uses_actual_key_column_ndv() -> Result<()> {
1836        // Join on column "b" (index 1), NDV only set on "b", not "a".
1837        // Old first()-based code would look up column 0 (a), find no NDV,
1838        // and fall back to Cartesian product. The fix looks up column 1 (b).
1839        // left: 1000 rows, NDV(b)=50; right: 500 rows, NDV(b)=25
1840        // expected = 1000 * 500 / max(50, 25) = 10000
1841        let schema = make_schema(); // "a" Int32, "b" Int32
1842        let make_source_ndv_b =
1843            |num_rows: usize, ndv_b: usize| -> Arc<dyn ExecutionPlan> {
1844                let col_stats = vec![
1845                    ColumnStatistics::new_unknown(), // "a": no NDV
1846                    {
1847                        let mut cs = ColumnStatistics::new_unknown();
1848                        cs.distinct_count = Precision::Exact(ndv_b);
1849                        cs
1850                    },
1851                ];
1852                Arc::new(MockSourceExec::with_column_stats(
1853                    Arc::clone(&schema),
1854                    Precision::Exact(num_rows),
1855                    col_stats,
1856                ))
1857            };
1858
1859        let left = make_source_ndv_b(1000, 50);
1860        let right = make_source_ndv_b(500, 25);
1861
1862        // Join on column "b" (index 1)
1863        let on: crate::joins::JoinOn = vec![(
1864            Arc::new(Column::new("b", 1)) as Arc<dyn PhysicalExpr>,
1865            Arc::new(Column::new("b", 1)) as Arc<dyn PhysicalExpr>,
1866        )];
1867        let join: Arc<dyn ExecutionPlan> = Arc::new(HashJoinExec::try_new(
1868            left,
1869            right,
1870            on,
1871            None,
1872            &JoinType::Inner,
1873            None,
1874            PartitionMode::CollectLeft,
1875            NullEquality::NullEqualsNull,
1876            false,
1877        )?);
1878
1879        let registry = StatisticsRegistry::with_providers(vec![
1880            Arc::new(JoinStatisticsProvider),
1881            Arc::new(DefaultStatisticsProvider),
1882        ]);
1883        let stats = registry.compute(join.as_ref())?;
1884        assert_eq!(stats.base.num_rows, Precision::Inexact(10_000));
1885        Ok(())
1886    }
1887
1888    #[test]
1889    fn test_join_provider_multi_key_ndv() -> Result<()> {
1890        // Multi-key join: ON a.a = b.a AND a.b = b.b
1891        // left: 1000 rows, NDV(a)=100, NDV(b)=20
1892        // right: 500 rows, NDV(a)=50, NDV(b)=10
1893        // expected = 1000 * 500 / (max(100,50) * max(20,10)) = 500000 / 2000 = 250
1894        let schema = make_schema(); // "a" Int32, "b" Int32
1895        let make_source_2ndv =
1896            |num_rows: usize, ndv_a: usize, ndv_b: usize| -> Arc<dyn ExecutionPlan> {
1897                let col_stats = vec![
1898                    {
1899                        let mut cs = ColumnStatistics::new_unknown();
1900                        cs.distinct_count = Precision::Exact(ndv_a);
1901                        cs
1902                    },
1903                    {
1904                        let mut cs = ColumnStatistics::new_unknown();
1905                        cs.distinct_count = Precision::Exact(ndv_b);
1906                        cs
1907                    },
1908                ];
1909                Arc::new(MockSourceExec::with_column_stats(
1910                    Arc::clone(&schema),
1911                    Precision::Exact(num_rows),
1912                    col_stats,
1913                ))
1914            };
1915
1916        let left = make_source_2ndv(1000, 100, 20);
1917        let right = make_source_2ndv(500, 50, 10);
1918
1919        let on: crate::joins::JoinOn = vec![
1920            (
1921                Arc::new(Column::new("a", 0)) as Arc<dyn PhysicalExpr>,
1922                Arc::new(Column::new("a", 0)) as Arc<dyn PhysicalExpr>,
1923            ),
1924            (
1925                Arc::new(Column::new("b", 1)) as Arc<dyn PhysicalExpr>,
1926                Arc::new(Column::new("b", 1)) as Arc<dyn PhysicalExpr>,
1927            ),
1928        ];
1929        let join: Arc<dyn ExecutionPlan> = Arc::new(HashJoinExec::try_new(
1930            left,
1931            right,
1932            on,
1933            None,
1934            &JoinType::Inner,
1935            None,
1936            PartitionMode::CollectLeft,
1937            NullEquality::NullEqualsNull,
1938            false,
1939        )?);
1940
1941        let registry = StatisticsRegistry::with_providers(vec![
1942            Arc::new(JoinStatisticsProvider),
1943            Arc::new(DefaultStatisticsProvider),
1944        ]);
1945        let stats = registry.compute(join.as_ref())?;
1946        assert_eq!(stats.base.num_rows, Precision::Inexact(250));
1947        Ok(())
1948    }
1949
1950    #[test]
1951    fn test_join_provider_fallback_cartesian() -> Result<()> {
1952        // No NDV available -> Cartesian product estimate
1953        let left = make_source_with_ndv_2col(100, None);
1954        let right = make_source_with_ndv_2col(200, None);
1955        let join = make_hash_join(left, right)?;
1956
1957        let registry = StatisticsRegistry::with_providers(vec![
1958            Arc::new(JoinStatisticsProvider),
1959            Arc::new(DefaultStatisticsProvider),
1960        ]);
1961        let stats = registry.compute(join.as_ref())?;
1962        assert_eq!(stats.base.num_rows, Precision::Inexact(20_000));
1963        Ok(())
1964    }
1965
1966    #[test]
1967    fn test_nl_join_delegates() -> Result<()> {
1968        use crate::joins::NestedLoopJoinExec;
1969
1970        // NL join delegates to the built-in (NestedLoopJoinExec may have an
1971        // arbitrary JoinFilter, so the provider cannot safely assume Cartesian).
1972        let left = make_source(100);
1973        let right = make_source(200);
1974        let join: Arc<dyn ExecutionPlan> = Arc::new(NestedLoopJoinExec::try_new(
1975            left,
1976            right,
1977            None,
1978            &JoinType::Inner,
1979            None,
1980        )?);
1981
1982        let registry = StatisticsRegistry::with_providers(vec![
1983            Arc::new(JoinStatisticsProvider),
1984            Arc::new(DefaultStatisticsProvider),
1985        ]);
1986        let stats = registry.compute(join.as_ref())?;
1987        // Provider delegates; result comes from built-in partition_statistics.
1988        assert!(
1989            stats.base.num_rows.get_value().is_some()
1990                || matches!(stats.base.num_rows, Precision::Absent)
1991        );
1992        Ok(())
1993    }
1994
1995    fn make_hash_join_typed(
1996        left: Arc<dyn ExecutionPlan>,
1997        right: Arc<dyn ExecutionPlan>,
1998        join_type: JoinType,
1999    ) -> Result<Arc<dyn ExecutionPlan>> {
2000        let on: crate::joins::JoinOn = vec![(
2001            Arc::new(Column::new("a", 0)) as Arc<dyn PhysicalExpr>,
2002            Arc::new(Column::new("a", 0)) as Arc<dyn PhysicalExpr>,
2003        )];
2004        Ok(Arc::new(HashJoinExec::try_new(
2005            left,
2006            right,
2007            on,
2008            None,
2009            &join_type,
2010            None,
2011            PartitionMode::CollectLeft,
2012            NullEquality::NullEqualsNull,
2013            false,
2014        )?))
2015    }
2016
2017    fn compute_join_rows(
2018        left_rows: usize,
2019        left_ndv: Option<usize>,
2020        right_rows: usize,
2021        right_ndv: Option<usize>,
2022        join_type: JoinType,
2023    ) -> Result<Precision<usize>> {
2024        let left = make_source_with_ndv_2col(left_rows, left_ndv);
2025        let right = make_source_with_ndv_2col(right_rows, right_ndv);
2026        let join = make_hash_join_typed(left, right, join_type)?;
2027        let registry = StatisticsRegistry::with_providers(vec![
2028            Arc::new(JoinStatisticsProvider),
2029            Arc::new(DefaultStatisticsProvider),
2030        ]);
2031        Ok(registry.compute(join.as_ref())?.base.num_rows)
2032    }
2033
2034    #[test]
2035    fn test_join_provider_left_outer() -> Result<()> {
2036        // left=1000, right=500, NDV(a)=100/50
2037        // inner estimate = 1000*500/100 = 5000, already >= left_rows
2038        // Left outer: max(5000, 1000) = 5000
2039        assert_eq!(
2040            compute_join_rows(1000, Some(100), 500, Some(50), JoinType::Left)?,
2041            Precision::Inexact(5000)
2042        );
2043        // Small inner estimate: left=1000, right=10, NDV=100/100
2044        // inner = 1000*10/100 = 100, left outer = max(100, 1000) = 1000
2045        assert_eq!(
2046            compute_join_rows(1000, Some(100), 10, Some(100), JoinType::Left)?,
2047            Precision::Inexact(1000)
2048        );
2049        Ok(())
2050    }
2051
2052    #[test]
2053    fn test_join_provider_right_outer() -> Result<()> {
2054        // inner = 1000*10/100 = 100, right outer = max(100, 10) = 100
2055        assert_eq!(
2056            compute_join_rows(1000, Some(100), 10, Some(100), JoinType::Right)?,
2057            Precision::Inexact(100)
2058        );
2059        // inner = 10*1000/100 = 100, right outer = max(100, 1000) = 1000
2060        assert_eq!(
2061            compute_join_rows(10, Some(100), 1000, Some(100), JoinType::Right)?,
2062            Precision::Inexact(1000)
2063        );
2064        Ok(())
2065    }
2066
2067    #[test]
2068    fn test_join_provider_semi_join() -> Result<()> {
2069        // inner = 5000, left semi = min(5000, 1000) = 1000
2070        assert_eq!(
2071            compute_join_rows(1000, Some(100), 500, Some(50), JoinType::LeftSemi)?,
2072            Precision::Inexact(1000)
2073        );
2074        // inner = 5000, right semi = min(5000, 500) = 500
2075        assert_eq!(
2076            compute_join_rows(1000, Some(100), 500, Some(50), JoinType::RightSemi)?,
2077            Precision::Inexact(500)
2078        );
2079        // Cartesian fallback (no NDV): inner = 1000*500 = 500000,
2080        // left semi = min(500000, 1000) = 1000 (selectivity = 1.0)
2081        assert_eq!(
2082            compute_join_rows(1000, None, 500, None, JoinType::LeftSemi)?,
2083            Precision::Inexact(1000)
2084        );
2085        Ok(())
2086    }
2087
2088    #[test]
2089    fn test_join_provider_anti_join() -> Result<()> {
2090        // inner = 1000*10/100 = 100, left anti = 1000 - min(100, 1000) = 900
2091        assert_eq!(
2092            compute_join_rows(1000, Some(100), 10, Some(100), JoinType::LeftAnti)?,
2093            Precision::Inexact(900)
2094        );
2095        // inner = 5000, right anti = 500 - min(5000, 500) = 0
2096        assert_eq!(
2097            compute_join_rows(1000, Some(100), 500, Some(50), JoinType::RightAnti)?,
2098            Precision::Inexact(0)
2099        );
2100        Ok(())
2101    }
2102
2103    // =========================================================================
2104    // CrossJoinExec tests (handled by JoinStatisticsProvider)
2105    // =========================================================================
2106
2107    #[test]
2108    fn test_cross_join_provider_exact() -> Result<()> {
2109        use crate::joins::CrossJoinExec;
2110        let left = make_source(100);
2111        let right = make_source(200);
2112        let join: Arc<dyn ExecutionPlan> = Arc::new(CrossJoinExec::new(left, right));
2113
2114        let registry = StatisticsRegistry::with_providers(vec![
2115            Arc::new(JoinStatisticsProvider),
2116            Arc::new(DefaultStatisticsProvider),
2117        ]);
2118        let stats = registry.compute(join.as_ref())?;
2119        // Both inputs have Exact row counts -> result is also Exact
2120        assert_eq!(stats.base.num_rows, Precision::Exact(20_000));
2121        Ok(())
2122    }
2123
2124    // =========================================================================
2125    // LimitStatisticsProvider tests
2126    // =========================================================================
2127
2128    use crate::limit::{GlobalLimitExec, LocalLimitExec};
2129
2130    #[test]
2131    fn test_limit_provider_caps_output() -> Result<()> {
2132        // input > fetch -> capped at fetch
2133        let source = make_source(1000);
2134        let limit: Arc<dyn ExecutionPlan> = Arc::new(LocalLimitExec::new(source, 100));
2135
2136        let registry = StatisticsRegistry::with_providers(vec![
2137            Arc::new(LimitStatisticsProvider),
2138            Arc::new(DefaultStatisticsProvider),
2139        ]);
2140        let stats = registry.compute(limit.as_ref())?;
2141        assert_eq!(stats.base.num_rows, Precision::Exact(100));
2142        Ok(())
2143    }
2144
2145    #[test]
2146    fn test_limit_provider_input_smaller_than_fetch() -> Result<()> {
2147        // input < fetch -> output = input
2148        let source = make_source(50);
2149        let limit: Arc<dyn ExecutionPlan> = Arc::new(LocalLimitExec::new(source, 200));
2150
2151        let registry = StatisticsRegistry::with_providers(vec![
2152            Arc::new(LimitStatisticsProvider),
2153            Arc::new(DefaultStatisticsProvider),
2154        ]);
2155        let stats = registry.compute(limit.as_ref())?;
2156        assert_eq!(stats.base.num_rows, Precision::Exact(50));
2157        Ok(())
2158    }
2159
2160    #[test]
2161    fn test_global_limit_provider_skip_and_fetch() -> Result<()> {
2162        // 1000 rows, skip 200, fetch 100 -> exactly 100
2163        let source = make_source(1000);
2164        let limit: Arc<dyn ExecutionPlan> =
2165            Arc::new(GlobalLimitExec::new(source, 200, Some(100)));
2166
2167        let registry = StatisticsRegistry::with_providers(vec![
2168            Arc::new(LimitStatisticsProvider),
2169            Arc::new(DefaultStatisticsProvider),
2170        ]);
2171        let stats = registry.compute(limit.as_ref())?;
2172        assert_eq!(stats.base.num_rows, Precision::Exact(100));
2173        Ok(())
2174    }
2175
2176    #[test]
2177    fn test_global_limit_provider_skip_exceeds_rows() -> Result<()> {
2178        // 100 rows, skip 200 -> 0 rows (skip > available)
2179        let source = make_source(100);
2180        let limit: Arc<dyn ExecutionPlan> =
2181            Arc::new(GlobalLimitExec::new(source, 200, Some(50)));
2182
2183        let registry = StatisticsRegistry::with_providers(vec![
2184            Arc::new(LimitStatisticsProvider),
2185            Arc::new(DefaultStatisticsProvider),
2186        ]);
2187        let stats = registry.compute(limit.as_ref())?;
2188        assert_eq!(stats.base.num_rows, Precision::Exact(0));
2189        Ok(())
2190    }
2191
2192    #[test]
2193    fn test_limit_provider_inexact_input() -> Result<()> {
2194        // Inexact(1000) with fetch=100: result must stay Inexact, not Exact,
2195        // because the actual row count could be less than 100.
2196        let source = make_source_with_precision(Precision::Inexact(1000));
2197        let limit: Arc<dyn ExecutionPlan> = Arc::new(LocalLimitExec::new(source, 100));
2198
2199        let registry = StatisticsRegistry::with_providers(vec![
2200            Arc::new(LimitStatisticsProvider),
2201            Arc::new(DefaultStatisticsProvider),
2202        ]);
2203        let stats = registry.compute(limit.as_ref())?;
2204        assert_eq!(stats.base.num_rows, Precision::Inexact(100));
2205        Ok(())
2206    }
2207
2208    // =========================================================================
2209    // UnionStatisticsProvider tests
2210    // =========================================================================
2211
2212    use crate::union::UnionExec;
2213
2214    fn make_source_with_precision(num_rows: Precision<usize>) -> Arc<dyn ExecutionPlan> {
2215        Arc::new(MockSourceExec::new(make_schema(), num_rows))
2216    }
2217
2218    #[test]
2219    fn test_union_provider_sums_rows() -> Result<()> {
2220        let union = UnionExec::try_new(vec![make_source(300), make_source(700)])?;
2221
2222        let registry = StatisticsRegistry::with_providers(vec![
2223            Arc::new(UnionStatisticsProvider),
2224            Arc::new(DefaultStatisticsProvider),
2225        ]);
2226        let stats = registry.compute(union.as_ref())?;
2227        assert_eq!(stats.base.num_rows, Precision::Exact(1000));
2228        Ok(())
2229    }
2230
2231    #[test]
2232    fn test_union_provider_three_inputs() -> Result<()> {
2233        let union = UnionExec::try_new(vec![
2234            make_source(100),
2235            make_source(200),
2236            make_source(300),
2237        ])?;
2238
2239        let registry = StatisticsRegistry::with_providers(vec![
2240            Arc::new(UnionStatisticsProvider),
2241            Arc::new(DefaultStatisticsProvider),
2242        ]);
2243        let stats = registry.compute(union.as_ref())?;
2244        assert_eq!(stats.base.num_rows, Precision::Exact(600));
2245        Ok(())
2246    }
2247
2248    #[test]
2249    fn test_union_provider_absent_propagates() -> Result<()> {
2250        // One input with unknown row count -> result must be Absent, not Inexact(300)
2251        let union = UnionExec::try_new(vec![
2252            make_source(300),
2253            make_source_with_precision(Precision::Absent),
2254        ])?;
2255
2256        let registry = StatisticsRegistry::with_providers(vec![
2257            Arc::new(UnionStatisticsProvider),
2258            Arc::new(DefaultStatisticsProvider),
2259        ]);
2260        let stats = registry.compute(union.as_ref())?;
2261        assert_eq!(stats.base.num_rows, Precision::Absent);
2262        Ok(())
2263    }
2264
2265    // =========================================================================
2266    // ClosureStatisticsProvider tests
2267    // =========================================================================
2268
2269    #[test]
2270    fn test_closure_provider_basic() -> Result<()> {
2271        // Override all FilterExec stats with a fixed row count
2272        let provider = ClosureStatisticsProvider::new(|plan, _child_stats| {
2273            if plan.downcast_ref::<FilterExec>().is_some() {
2274                Ok(StatisticsResult::Computed(ExtendedStatistics::from(
2275                    Statistics {
2276                        num_rows: Precision::Inexact(42),
2277                        total_byte_size: Precision::Absent,
2278                        column_statistics: vec![],
2279                    },
2280                )))
2281            } else {
2282                Ok(StatisticsResult::Delegate)
2283            }
2284        });
2285
2286        let registry = StatisticsRegistry::with_providers(vec![
2287            Arc::new(provider),
2288            Arc::new(DefaultStatisticsProvider),
2289        ]);
2290
2291        let source = make_source(1000);
2292        let filter: Arc<dyn ExecutionPlan> =
2293            Arc::new(FilterExec::try_new(lit(true), source)?);
2294        let stats = registry.compute(filter.as_ref())?;
2295        assert_eq!(stats.base.num_rows, Precision::Inexact(42));
2296        Ok(())
2297    }
2298
2299    #[test]
2300    fn test_closure_provider_distinguishes_nodes_by_child_stats() -> Result<()> {
2301        // Two FilterExec nodes with different input sizes.
2302        // The closure uses the child row count as a proxy to distinguish them,
2303        // which mirrors the cardinality feedback use case where you match a
2304        // runtime-observed count to the right node in the plan tree.
2305        let provider = ClosureStatisticsProvider::new(|plan, child_stats| {
2306            if plan.downcast_ref::<FilterExec>().is_none() {
2307                return Ok(StatisticsResult::Delegate);
2308            }
2309            match child_stats[0].base.num_rows.get_value().copied() {
2310                Some(500) => Ok(StatisticsResult::Computed(ExtendedStatistics::from(
2311                    Statistics {
2312                        num_rows: Precision::Inexact(100),
2313                        total_byte_size: Precision::Absent,
2314                        column_statistics: vec![],
2315                    },
2316                ))),
2317                Some(200) => Ok(StatisticsResult::Computed(ExtendedStatistics::from(
2318                    Statistics {
2319                        num_rows: Precision::Inexact(50),
2320                        total_byte_size: Precision::Absent,
2321                        column_statistics: vec![],
2322                    },
2323                ))),
2324                _ => Ok(StatisticsResult::Delegate),
2325            }
2326        });
2327
2328        let registry = StatisticsRegistry::with_providers(vec![Arc::new(provider)]);
2329
2330        let filter_a: Arc<dyn ExecutionPlan> =
2331            Arc::new(FilterExec::try_new(lit(true), make_source(500))?);
2332        let filter_b: Arc<dyn ExecutionPlan> =
2333            Arc::new(FilterExec::try_new(lit(true), make_source(200))?);
2334
2335        let stats_a = registry.compute(filter_a.as_ref())?;
2336        let stats_b = registry.compute(filter_b.as_ref())?;
2337
2338        assert_eq!(stats_a.base.num_rows, Precision::Inexact(100));
2339        assert_eq!(stats_b.base.num_rows, Precision::Inexact(50));
2340        Ok(())
2341    }
2342}