micromegas_analytics/lakehouse/
batch_partition_merger.rs1use super::{
2 lakehouse_context::LakehouseContext,
3 merge::{MergeQueryResult, PartitionMerger},
4 partition::Partition,
5 partition_cache::PartitionCache,
6 session_configurator::SessionConfigurator,
7 view_factory::ViewFactory,
8};
9use crate::{
10 lakehouse::{
11 partitioned_table_provider::PartitionedTableProvider, query::make_session_context,
12 },
13 time::{TimeRange, datetime_to_scalar},
14};
15use anyhow::Result;
16use async_trait::async_trait;
17use chrono::{DateTime, TimeDelta, Utc};
18use datafusion::{
19 arrow::datatypes::Schema, error::DataFusionError,
20 physical_plan::stream::RecordBatchReceiverStreamBuilder, sql::TableReference,
21};
22use futures::TryStreamExt;
23use futures::{StreamExt, stream};
24use micromegas_tracing::prelude::*;
25use std::sync::Arc;
26
27struct PartitionStats {
29 pub num_rows: i64,
30 pub min_event_time: DateTime<Utc>,
31 pub max_event_time: DateTime<Utc>,
32}
33
34fn compute_partition_stats(partitions: &[Partition]) -> Result<PartitionStats> {
35 let non_empty: Vec<_> = partitions.iter().filter(|p| !p.is_empty()).collect();
37
38 if non_empty.is_empty() {
39 anyhow::bail!(
40 "compute_partition_stats given only empty partitions (should be filtered at caller)"
41 );
42 }
43
44 let first = non_empty.first().unwrap();
45 let first_event_range = first
46 .event_time_range
47 .ok_or_else(|| anyhow::anyhow!("non-empty partition has no event_time_range"))?;
48
49 let state = PartitionStats {
50 num_rows: first.num_rows,
51 min_event_time: first_event_range.begin,
52 max_event_time: first_event_range.end,
53 };
54
55 non_empty
56 .iter()
57 .skip(1)
58 .try_fold(state, |state, part| -> Result<PartitionStats> {
59 let event_range = part
60 .event_time_range
61 .ok_or_else(|| anyhow::anyhow!("non-empty partition has no event_time_range"))?;
62 Ok(PartitionStats {
63 num_rows: state.num_rows + part.num_rows,
64 min_event_time: state.min_event_time.min(event_range.begin),
65 max_event_time: state.max_event_time.max(event_range.end),
66 })
67 })
68}
69
70#[derive(Debug)]
73pub struct BatchPartitionMerger {
74 file_schema: Arc<Schema>,
76 view_factory: Arc<ViewFactory>,
78 session_configurator: Arc<dyn SessionConfigurator>,
80 merge_batch_query: String,
82 approx_nb_rows_per_batch: i64,
84}
85
86impl BatchPartitionMerger {
87 pub fn new(
88 file_schema: Arc<Schema>,
89 view_factory: Arc<ViewFactory>,
90 session_configurator: Arc<dyn SessionConfigurator>,
91 merge_batch_query: String,
92 approx_nb_rows_per_batch: i64,
93 ) -> Self {
94 Self {
95 file_schema,
96 view_factory,
97 session_configurator,
98 merge_batch_query,
99 approx_nb_rows_per_batch,
100 }
101 }
102}
103
104#[async_trait]
105impl PartitionMerger for BatchPartitionMerger {
106 #[span_fn]
107 async fn execute_merge_query(
108 &self,
109 lakehouse: Arc<LakehouseContext>,
110 partitions_to_merge: Arc<Vec<Partition>>,
111 partitions_all_views: Arc<PartitionCache>,
112 insert_range: TimeRange,
113 ) -> Result<MergeQueryResult> {
114 info!("execute_merge_query");
115
116 if partitions_to_merge.iter().all(|p| p.is_empty()) {
118 debug!("all partitions are empty, returning empty stream");
119 let builder = RecordBatchReceiverStreamBuilder::new(self.file_schema.clone(), 1);
120 return Ok(MergeQueryResult {
121 stream: builder.build(),
122 ordering_honored: true,
123 });
124 }
125
126 let stats = compute_partition_stats(partitions_to_merge.as_ref())?;
127 let nb_batches = ((stats.num_rows / self.approx_nb_rows_per_batch) + 1) as i32;
128 let batch_time_delta = ((stats.max_event_time - stats.min_event_time) / nb_batches)
129 + TimeDelta::nanoseconds(1);
130
131 let file_schema = self.file_schema.clone();
132 let reader_factory = lakehouse.reader_factory().clone();
133 let ctx = make_session_context(
134 lakehouse.clone(),
135 partitions_all_views,
136 Some(insert_range),
137 self.view_factory.clone(),
138 self.session_configurator.clone(),
139 true,
140 )
141 .await?;
142 let src_table =
143 PartitionedTableProvider::new(file_schema, reader_factory, partitions_to_merge);
144 ctx.register_table(
145 TableReference::Bare {
146 table: "source".into(),
147 },
148 Arc::new(src_table),
149 )?;
150 let df_template = ctx.sql(&self.merge_batch_query).await.map_err(|e| {
151 DataFusionError::Execution(format!("building template for merge query: {e:?}"))
152 })?;
153
154 let mut builder = RecordBatchReceiverStreamBuilder::new(self.file_schema.clone(), 10);
155 let sender = builder.tx();
156 builder.spawn(async move {
157 let mut streams_stream = stream::iter(0..nb_batches)
158 .map(|i| {
159 let begin = stats.min_event_time + (batch_time_delta * i);
160 let end = begin + batch_time_delta;
161 debug!("merging batch {begin} {end}");
162 df_template
163 .clone()
164 .with_param_values(vec![
165 ("begin", datetime_to_scalar(begin)),
166 ("end", datetime_to_scalar(end)),
167 ])
168 .map(|df| async {
169 spawn_with_context(df.execute_stream())
170 .await
171 .map_err(|e| DataFusionError::External(e.into()))
172 })
173 })
174 .try_buffered(2);
175
176 while let Some(stream_res) = streams_stream.next().await {
177 let mut merge_stream = stream_res??;
178 let sender = sender.clone();
179 while let Some(rb_res) = merge_stream.next().await {
180 if let Err(e) = sender.send(rb_res).await {
181 error!("sending record batch: {e:?}");
182 }
183 }
184 }
185 Ok(())
186 });
187 debug!("building merge stream");
188 Ok(MergeQueryResult {
189 stream: builder.build(),
190 ordering_honored: true,
191 })
192 }
193}