1use std::collections::HashMap;
20use std::fmt;
21use std::sync::Arc;
22
23use arrow_schema::SchemaRef;
24use async_trait::async_trait;
25use datafusion::catalog::{ScanArgs, ScanResult, Session};
26use datafusion::common::config::ConfigOptions;
27use datafusion::common::stats::Precision;
28use datafusion::common::{Constraints, Result, Statistics};
29use datafusion::datasource::{TableProvider, TableType};
30use datafusion::execution::{SendableRecordBatchStream, TaskContext};
31use datafusion::logical_expr::{Expr, TableProviderFilterPushDown};
32use datafusion::physical_expr::PhysicalExpr;
33use datafusion::physical_plan::execution_plan::CardinalityEffect;
34use datafusion::physical_plan::filter_pushdown::{
35 ChildPushdownResult, FilterDescription, FilterPushdownPhase, FilterPushdownPropagation,
36};
37use datafusion::physical_plan::{DisplayAs, DisplayFormatType, ExecutionPlan, PlanProperties};
38
39#[derive(Debug)]
45pub struct KeyStatisticsProvider {
46 inner: Arc<dyn TableProvider>,
47 distinct: HashMap<String, usize>,
48}
49
50impl KeyStatisticsProvider {
51 pub fn new(inner: Arc<dyn TableProvider>, distinct: HashMap<String, usize>) -> Self {
52 Self { inner, distinct }
53 }
54}
55
56#[async_trait]
57impl TableProvider for KeyStatisticsProvider {
58 fn schema(&self) -> SchemaRef {
59 self.inner.schema()
60 }
61
62 fn constraints(&self) -> Option<&Constraints> {
63 self.inner.constraints()
64 }
65
66 fn table_type(&self) -> TableType {
67 self.inner.table_type()
68 }
69
70 async fn scan(
71 &self,
72 state: &dyn Session,
73 projection: Option<&Vec<usize>>,
74 filters: &[Expr],
75 limit: Option<usize>,
76 ) -> Result<Arc<dyn ExecutionPlan>> {
77 let plan = self.inner.scan(state, projection, filters, limit).await?;
78 let divisor = equality_divisor(filters, &self.distinct);
79 Ok(KeyStatisticsExec::wrap(plan, &self.distinct, divisor))
80 }
81
82 async fn scan_with_args<'a>(
83 &self,
84 state: &dyn Session,
85 args: ScanArgs<'a>,
86 ) -> Result<ScanResult> {
87 let divisor = equality_divisor(args.filters().unwrap_or_default(), &self.distinct);
88 let plan = self.inner.scan_with_args(state, args).await?.into_inner();
89 Ok(ScanResult::new(KeyStatisticsExec::wrap(
90 plan,
91 &self.distinct,
92 divisor,
93 )))
94 }
95
96 fn supports_filters_pushdown(
97 &self,
98 filters: &[&Expr],
99 ) -> Result<Vec<TableProviderFilterPushDown>> {
100 self.inner.supports_filters_pushdown(filters)
101 }
102
103 fn statistics(&self) -> Option<Statistics> {
104 self.inner.statistics()
105 }
106}
107
108fn equality_divisor(filters: &[Expr], distinct: &HashMap<String, usize>) -> usize {
115 use datafusion::logical_expr::utils::split_conjunction;
116 use datafusion::logical_expr::{BinaryExpr, Operator};
117 let mut divisor = 1usize;
118 for conjunct in filters.iter().flat_map(split_conjunction) {
119 let Expr::BinaryExpr(BinaryExpr {
120 left,
121 op: Operator::Eq,
122 right,
123 }) = conjunct
124 else {
125 continue;
126 };
127 let column = match (left.as_ref(), right.as_ref()) {
128 (Expr::Column(c), Expr::Literal(..)) | (Expr::Literal(..), Expr::Column(c)) => c,
129 _ => continue,
130 };
131 if let Some(&n) = distinct.get(&column.name) {
132 divisor = divisor.saturating_mul(n.max(1));
133 }
134 }
135 divisor
136}
137
138#[derive(Debug)]
140pub struct KeyStatisticsExec {
141 input: Arc<dyn ExecutionPlan>,
142 distinct: Arc<HashMap<String, usize>>,
145 rows_divisor: usize,
147}
148
149impl KeyStatisticsExec {
150 fn wrap(
152 input: Arc<dyn ExecutionPlan>,
153 distinct: &HashMap<String, usize>,
154 rows_divisor: usize,
155 ) -> Arc<dyn ExecutionPlan> {
156 let schema = input.schema();
157 let relevant: HashMap<String, usize> = distinct
158 .iter()
159 .filter(|(name, _)| schema.field_with_name(name).is_ok())
160 .map(|(name, n)| (name.clone(), *n))
161 .collect();
162 if relevant.is_empty() && rows_divisor == 1 {
163 return input;
164 }
165 Arc::new(Self {
166 input,
167 distinct: Arc::new(relevant),
168 rows_divisor,
169 })
170 }
171}
172
173impl DisplayAs for KeyStatisticsExec {
174 fn fmt_as(&self, _t: DisplayFormatType, f: &mut fmt::Formatter) -> fmt::Result {
175 let mut columns: Vec<_> = self.distinct.iter().collect();
176 columns.sort();
177 let shown: Vec<String> = columns.iter().map(|(c, n)| format!("{c}={n}")).collect();
178 write!(f, "KeyStatisticsExec: distinct=[{}]", shown.join(", "))?;
179 if self.rows_divisor > 1 {
180 write!(f, ", rows/{}", self.rows_divisor)?;
181 }
182 Ok(())
183 }
184}
185
186impl ExecutionPlan for KeyStatisticsExec {
187 fn name(&self) -> &str {
188 "KeyStatisticsExec"
189 }
190
191 fn properties(&self) -> &Arc<PlanProperties> {
192 self.input.properties()
193 }
194
195 fn maintains_input_order(&self) -> Vec<bool> {
196 vec![true]
197 }
198
199 fn children(&self) -> Vec<&Arc<dyn ExecutionPlan>> {
200 vec![&self.input]
201 }
202
203 fn with_new_children(
204 self: Arc<Self>,
205 mut children: Vec<Arc<dyn ExecutionPlan>>,
206 ) -> Result<Arc<dyn ExecutionPlan>> {
207 Ok(Self::wrap(
208 children.swap_remove(0),
209 &self.distinct,
210 self.rows_divisor,
211 ))
212 }
213
214 fn execute(
215 &self,
216 partition: usize,
217 context: Arc<TaskContext>,
218 ) -> Result<SendableRecordBatchStream> {
219 self.input.execute(partition, context)
220 }
221
222 fn partition_statistics(&self, partition: Option<usize>) -> Result<Arc<Statistics>> {
223 let mut stats = Statistics::clone(self.input.partition_statistics(partition)?.as_ref());
224 if self.rows_divisor > 1 {
225 if let Some(&n) = stats.num_rows.get_value() {
226 stats.num_rows = Precision::Inexact(n.div_ceil(self.rows_divisor));
227 stats.total_byte_size = stats.total_byte_size.to_inexact();
228 }
229 }
230 let schema = self.input.schema();
231 for (field, column) in schema
232 .fields()
233 .iter()
234 .zip(stats.column_statistics.iter_mut())
235 {
236 if let Some(&n) = self.distinct.get(field.name()) {
237 column.distinct_count = Precision::Inexact(n);
240 }
241 }
242 Ok(Arc::new(stats))
243 }
244
245 fn supports_limit_pushdown(&self) -> bool {
246 true
247 }
248
249 fn cardinality_effect(&self) -> CardinalityEffect {
250 CardinalityEffect::Equal
251 }
252
253 fn gather_filters_for_pushdown(
254 &self,
255 _phase: FilterPushdownPhase,
256 parent_filters: Vec<Arc<dyn PhysicalExpr>>,
257 _config: &ConfigOptions,
258 ) -> Result<FilterDescription> {
259 FilterDescription::from_children(parent_filters, &self.children())
260 }
261
262 fn handle_child_pushdown_result(
263 &self,
264 _phase: FilterPushdownPhase,
265 child_pushdown_result: ChildPushdownResult,
266 _config: &ConfigOptions,
267 ) -> Result<FilterPushdownPropagation<Arc<dyn ExecutionPlan>>> {
268 Ok(FilterPushdownPropagation::if_all(child_pushdown_result))
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use super::*;
275 use arrow_array::{Int64Array, RecordBatch};
276 use arrow_schema::{DataType, Field, Schema};
277 use datafusion::datasource::MemTable;
278 use datafusion::prelude::SessionContext;
279
280 #[tokio::test]
281 async fn scan_reports_distinct_counts_for_named_columns_only() {
282 let schema = Arc::new(Schema::new(vec![
283 Field::new("src", DataType::Int64, false),
284 Field::new("dst", DataType::Int64, false),
285 ]));
286 let batch = RecordBatch::try_new(
287 schema.clone(),
288 vec![
289 Arc::new(Int64Array::from(vec![1, 1, 2, 3])),
290 Arc::new(Int64Array::from(vec![7, 8, 7, 7])),
291 ],
292 )
293 .unwrap();
294 let inner = Arc::new(MemTable::try_new(schema, vec![vec![batch]]).unwrap());
295 let table = KeyStatisticsProvider::new(inner, HashMap::from([("dst".to_string(), 2)]));
296 let ctx = SessionContext::new();
297 let plan = table.scan(&ctx.state(), None, &[], None).await.unwrap();
298 let stats = plan.partition_statistics(None).unwrap();
299 assert_eq!(
300 stats.column_statistics[1].distinct_count,
301 Precision::Inexact(2)
302 );
303 assert_eq!(stats.column_statistics[0].distinct_count, Precision::Absent);
304
305 let plan = table
307 .scan(&ctx.state(), Some(&vec![0]), &[], None)
308 .await
309 .unwrap();
310 assert_ne!(plan.name(), "KeyStatisticsExec");
311
312 use datafusion::prelude::{col, lit};
315 let filters = [col("dst").eq(lit(7_i64)), col("src").gt(lit(1_i64))];
316 let plan = table
317 .scan(&ctx.state(), None, &filters, None)
318 .await
319 .unwrap();
320 let stats = plan.partition_statistics(None).unwrap();
321 assert_eq!(stats.num_rows, Precision::Inexact(2));
322 }
323}