timeseries_table_format/datafusion/
ts_table_provider.rs1mod segment_pruning;
2#[cfg(test)]
3mod tests;
4mod timestamp_pruning;
5
6use crate::storage::file_size;
7
8use std::collections::HashSet;
9use std::path::Path;
10use std::sync::Arc;
11
12use arrow::datatypes::{DataType, SchemaRef};
13use async_trait::async_trait;
14
15use datafusion::catalog::Session;
16use datafusion::catalog::TableProvider;
17use datafusion::common::DFSchema;
18
19use datafusion::datasource::listing::PartitionedFile;
20use datafusion::datasource::physical_plan::FileScanConfigBuilder;
21use datafusion::datasource::physical_plan::ParquetSource;
22use datafusion::datasource::source::DataSourceExec;
23use datafusion::error::{DataFusionError, Result as DFResult};
24use datafusion::execution::object_store::ObjectStoreUrl;
25
26use datafusion::logical_expr::{Expr, Operator};
27
28use datafusion::logical_expr::TableProviderFilterPushDown;
29
30use crate::metadata::index::{IndexKind, IndexSpec};
31use crate::table::TimeSeriesTable;
32use crate::transaction_log::SegmentMeta;
33use crate::transaction_log::TableState;
34use datafusion::logical_expr::utils::{conjunction, expr_to_columns};
35use datafusion::physical_expr::PhysicalExpr;
36use datafusion::physical_plan::ExecutionPlan;
37use datafusion::physical_plan::expressions::lit;
38use datafusion::scalar::ScalarValue;
39use tokio::sync::RwLock;
40
41#[derive(Debug)]
46pub struct TsTableProvider {
47 table: Arc<TimeSeriesTable>,
48 schema: SchemaRef,
49 cache: RwLock<Cache>,
50
51 object_store_url: ObjectStoreUrl,
52}
53
54#[derive(Debug)]
55struct Cache {
56 version: Option<u64>,
57 state: Option<TableState>,
58}
59
60fn df_external<E>(e: E) -> DataFusionError
62where
63 E: std::error::Error + Send + Sync + 'static,
64{
65 DataFusionError::External(Box::new(e))
66}
67
68fn metadata_pruning_expr(
69 expr: &Expr,
70 index: &IndexSpec,
71 schema: &SchemaRef,
72) -> DFResult<Option<Expr>> {
73 if is_exact_entity_equality(expr, index, schema) {
74 return Ok(Some(expr.clone()));
75 }
76
77 if let Expr::BinaryExpr(binary) = expr
78 && matches!(binary.op, Operator::And | Operator::Or)
79 {
80 let left = metadata_pruning_expr(&binary.left, index, schema)?;
81 let right = metadata_pruning_expr(&binary.right, index, schema)?;
82 return Ok(if binary.op == Operator::And {
83 match (left, right) {
84 (Some(left), Some(right)) => Some(left.and(right)),
85 (left @ Some(_), None) | (None, left @ Some(_)) => left,
86 (None, None) => None,
87 }
88 } else {
89 left.zip(right).map(|(left, right)| left.or(right))
90 });
91 }
92
93 let mut columns = HashSet::new();
94 expr_to_columns(expr, &mut columns)?;
95 Ok(
96 (!columns.is_empty() && columns.iter().all(|column| column.name == index.column))
97 .then(|| expr.clone()),
98 )
99}
100
101fn is_exact_entity_equality(expr: &Expr, index: &IndexSpec, schema: &SchemaRef) -> bool {
102 let Expr::BinaryExpr(binary) = expr else {
103 return false;
104 };
105 if binary.op != Operator::Eq {
106 return false;
107 }
108 let (column, literal) = match (&*binary.left, &*binary.right) {
109 (Expr::Column(column), Expr::Literal(literal, _))
110 | (Expr::Literal(literal, _), Expr::Column(column)) => (column, literal),
111 _ => return false,
112 };
113 entity_literal_matches(column.name.as_str(), literal, index, schema)
114}
115
116fn entity_literal_matches(
117 column: &str,
118 literal: &ScalarValue,
119 index: &IndexSpec,
120 schema: &SchemaRef,
121) -> bool {
122 if !index.entity_columns.iter().any(|entity| entity == column) {
123 return false;
124 }
125 let Ok(field) = schema.field_with_name(column) else {
126 return false;
127 };
128 matches!(
129 (field.data_type(), literal),
130 (DataType::Utf8, ScalarValue::Utf8(Some(_)))
131 | (DataType::LargeUtf8, ScalarValue::LargeUtf8(Some(_)))
132 | (DataType::Int32, ScalarValue::Int32(Some(_)))
133 | (DataType::Int64, ScalarValue::Int64(Some(_)))
134 | (DataType::UInt64, ScalarValue::UInt64(Some(_)))
135 )
136}
137
138impl TsTableProvider {
139 pub fn try_new(table: Arc<TimeSeriesTable>) -> DFResult<Self> {
141 let schema = table
144 .state()
145 .table_meta
146 .arrow_schema_ref()
147 .map_err(df_external)?;
148
149 let object_store_url =
150 ObjectStoreUrl::parse(table.location().object_store_url()).map_err(df_external)?;
151 let state = table.state().clone();
152
153 Ok(Self {
154 table,
155 schema,
156 cache: RwLock::new(Cache {
157 version: Some(state.version),
158 state: Some(state),
159 }),
160 object_store_url,
161 })
162 }
163
164 async fn latest_state(&self) -> DFResult<TableState> {
165 let current_version = self.table.current_version().await.map_err(df_external)?;
166
167 {
169 let cache = self.cache.read().await;
170 if cache.version == Some(current_version)
171 && let Some(st) = cache.state.clone()
172 {
173 return Ok(st);
174 }
175 }
176
177 let state = self.table.load_latest_state().await.map_err(df_external)?;
179 let mut cache = self.cache.write().await;
180 cache.version = Some(state.version);
181 cache.state = Some(state.clone());
182 Ok(state)
183 }
184
185 async fn segment_file_size(&self, seg: &SegmentMeta) -> datafusion::error::Result<u64> {
186 if let Some(sz) = seg.file_size {
187 return Ok(sz);
188 }
189
190 let sz = file_size(self.table.location().storage(), Path::new(&seg.path))
191 .await
192 .map_err(|source| {
193 df_external(source).context(format!(
194 "missing segment file size; failed to inspect {}",
195 seg.path
196 ))
197 })?;
198 Ok(sz)
199 }
200
201 fn index_column_name(&self) -> &str {
203 self.table.index_spec().column.as_str()
204 }
205
206 fn prune_segments_by_metadata<'a>(
207 &self,
208 segments: Vec<&'a SegmentMeta>,
209 metadata_filters: &[Expr],
210 pruning_predicate: &Arc<dyn PhysicalExpr>,
211 ) -> DFResult<Vec<&'a SegmentMeta>> {
212 if metadata_filters.is_empty() {
213 return Ok(segments);
214 }
215
216 segment_pruning::prune_segments(
217 &self.schema,
218 self.table.index_spec(),
219 segments,
220 pruning_predicate,
221 )
222 }
223}
224
225#[async_trait]
226impl TableProvider for TsTableProvider {
227 fn schema(&self) -> arrow::datatypes::SchemaRef {
228 Arc::clone(&self.schema)
229 }
230
231 fn table_type(&self) -> datafusion::datasource::TableType {
232 datafusion::datasource::TableType::Base
233 }
234
235 fn supports_filters_pushdown(
236 &self,
237 filters: &[&Expr],
238 ) -> DFResult<Vec<TableProviderFilterPushDown>> {
239 Ok(vec![TableProviderFilterPushDown::Inexact; filters.len()])
242 }
243
244 #[tracing::instrument(
245 name = "table.scan.plan",
246 level = "debug",
247 skip_all,
248 fields(
249 snapshot_version = tracing::field::Empty,
250 total_candidate_segments = tracing::field::Empty,
251 selected_segments = tracing::field::Empty,
252 pruned_segments = tracing::field::Empty,
253 filter_count = filters.len(),
254 projection_column_count = tracing::field::Empty,
255 limit = tracing::field::Empty
256 )
257 )]
258 async fn scan(
259 &self,
260 state: &dyn Session,
261 projection: Option<&Vec<usize>>,
262 filters: &[Expr], limit: Option<usize>,
264 ) -> DFResult<Arc<dyn ExecutionPlan>> {
265 let span = tracing::Span::current();
266 if let Some(projection) = projection {
267 span.record("projection_column_count", projection.len());
268 }
269 if let Some(limit) = limit {
270 span.record("limit", limit);
271 }
272
273 let snapshot = self.latest_state().await?;
275 span.record("snapshot_version", snapshot.version);
276
277 for segment in snapshot.segments.values() {
278 segment
279 .validate_bounds(&self.table.index_spec().kind)
280 .map_err(df_external)?;
281 }
282
283 let segments = snapshot.segments_sorted_by_index().map_err(df_external)?;
284 let total_candidate_segments = segments.len();
285 span.record("total_candidate_segments", total_candidate_segments);
286
287 let df_schema = DFSchema::try_from(self.schema().as_ref().clone())?;
288 let exact_predicate = conjunction(filters.to_vec())
289 .map(|p| state.create_physical_expr(p, &df_schema))
290 .transpose()?
291 .unwrap_or_else(|| lit(true));
292
293 let metadata_filters = filters
294 .iter()
295 .map(|filter| metadata_pruning_expr(filter, self.table.index_spec(), &self.schema))
296 .collect::<DFResult<Vec<_>>>()?
297 .into_iter()
298 .flatten()
299 .collect::<Vec<_>>();
300
301 let pruning_filters = if matches!(self.table.index_spec().kind, IndexKind::Timestamp { .. })
302 {
303 let index_column = self.index_column_name();
304 let index_type = self.schema.field_with_name(index_column)?.data_type();
305 metadata_filters
306 .iter()
307 .cloned()
308 .map(|filter| {
309 timestamp_pruning::normalize_timestamp_predicate(
310 filter,
311 index_column,
312 index_type,
313 )
314 })
315 .collect::<DFResult<Vec<_>>>()?
316 } else {
317 metadata_filters.clone()
318 };
319 let pruning_predicate = if pruning_filters.as_slice() == filters {
320 Arc::clone(&exact_predicate)
321 } else {
322 conjunction(pruning_filters)
323 .map(|p| state.create_physical_expr(p, &df_schema))
324 .transpose()?
325 .unwrap_or_else(|| lit(true))
326 };
327
328 let parquet_source = Arc::new(
330 ParquetSource::new(Arc::clone(&self.schema))
331 .with_predicate(Arc::clone(&exact_predicate)),
332 );
333
334 let mut builder = FileScanConfigBuilder::new(self.object_store_url.clone(), parquet_source)
335 .with_projection_indices(projection.cloned())?
336 .with_limit(limit);
337
338 let selected =
339 self.prune_segments_by_metadata(segments, &metadata_filters, &pruning_predicate)?;
340 span.record("selected_segments", selected.len());
341 span.record(
342 "pruned_segments",
343 total_candidate_segments.saturating_sub(selected.len()),
344 );
345 for seg in selected {
346 let file_size = self.segment_file_size(seg).await?;
347 let location = self
348 .table
349 .location()
350 .object_store_path(Path::new(&seg.path))
351 .map_err(df_external)?;
352 let pf = PartitionedFile::new(location.as_ref(), file_size);
353
354 builder = builder.with_file(pf);
355 }
356
357 let plan = DataSourceExec::from_data_source(builder.build());
359 Ok(plan)
360 }
361}