1use super::{
2 lakehouse_context::LakehouseContext,
3 partition::Partition,
4 partition_cache::{PartitionCache, QueryPartitionProvider},
5 partition_source_data::hash_to_object_count,
6 partitioned_execution_plan::{
7 ScanOrdering, assert_ordering_satisfied, assert_single_partition, sort_exprs,
8 },
9 partitioned_table_provider::PartitionedTableProvider,
10 query::make_session_context,
11 read_scope::CallerContext,
12 session_configurator::SessionConfigurator,
13 view::{ScanSortColumn, View},
14 view_factory::ViewFactory,
15 write_partition::{PartitionRowSet, RetireMatch, write_partition_from_rows},
16};
17use crate::{response_writer::Logger, time::TimeRange};
18use anyhow::{Context, Result};
19use async_trait::async_trait;
20use datafusion::{
21 arrow::datatypes::Schema,
22 common::TableReference,
23 execution::SendableRecordBatchStream,
24 physical_plan::{displayable, execute_stream},
25 prelude::*,
26};
27use futures::stream::StreamExt;
28use micromegas_tracing::prelude::*;
29use std::fmt::Debug;
30use std::sync::Arc;
31use std::time::Instant;
32use xxhash_rust::xxh32::xxh32;
33
34pub async fn make_merge_session_context(
50 lakehouse: Arc<LakehouseContext>,
51 part_provider: Arc<dyn QueryPartitionProvider>,
52 query_range: Option<TimeRange>,
53 view_factory: Arc<ViewFactory>,
54 configurator: Arc<dyn SessionConfigurator>,
55 caller: CallerContext,
56) -> Result<SessionContext> {
57 let ctx = make_session_context(
58 lakehouse,
59 part_provider,
60 query_range,
61 view_factory,
62 configurator,
63 caller,
64 )
65 .await?;
66 ctx.state_ref()
67 .write()
68 .config_mut()
69 .options_mut()
70 .optimizer
71 .repartition_file_scans = false;
72 Ok(ctx)
73}
74
75pub struct MergeQueryResult {
77 pub stream: SendableRecordBatchStream,
79 pub ordering_honored: bool,
91}
92
93#[async_trait]
95pub trait PartitionMerger: Send + Sync + Debug {
96 async fn execute_merge_query(
98 &self,
99 lakehouse: Arc<LakehouseContext>,
100 partitions_to_merge: Arc<Vec<Partition>>,
101 partitions_all_views: Arc<PartitionCache>,
102 insert_range: TimeRange,
103 ) -> Result<MergeQueryResult>;
104}
105
106#[derive(Debug)]
108pub struct QueryMerger {
109 view_factory: Arc<ViewFactory>,
110 session_configurator: Arc<dyn SessionConfigurator>,
111 file_schema: Arc<Schema>,
112 query: Arc<String>,
113 merge_scan_ordering: ScanOrdering,
114}
115
116impl QueryMerger {
117 pub fn new(
118 view_factory: Arc<ViewFactory>,
119 session_configurator: Arc<dyn SessionConfigurator>,
120 file_schema: Arc<Schema>,
121 query: Arc<String>,
122 ) -> Self {
123 Self {
124 view_factory,
125 session_configurator,
126 file_schema,
127 query,
128 merge_scan_ordering: ScanOrdering::Unordered,
129 }
130 }
131
132 pub fn with_merge_scan_ordering(mut self, ordering: ScanOrdering) -> Self {
137 self.merge_scan_ordering = ordering;
138 self
139 }
140
141 async fn execute_concatenated_merge(
154 &self,
155 ctx: &SessionContext,
156 declared: bool,
157 insert_range: TimeRange,
158 ) -> Result<MergeQueryResult> {
159 let df = ctx.sql(&self.query).await?;
160 let task_ctx = Arc::new(df.task_ctx());
161 let plan = df
162 .create_physical_plan()
163 .await
164 .with_context(|| "creating physical plan for merge query")?;
165
166 if declared {
167 assert_single_partition(
168 &plan,
169 &format!("merge query {:?}", self.query),
170 insert_range,
171 "executing it would coalesce partitions and destroy the declared ordering. This \
172 likely means repartition_file_scans did not take effect.",
173 )?;
174 }
175
176 let ordering_honored = if declared {
177 let plan_str = displayable(plan.as_ref()).indent(true).to_string();
178 let honored =
179 !plan_str.contains("SortExec") && !plan_str.contains("SortPreservingMergeExec");
180 if !honored {
181 warn!(
182 "merge query {:?} (insert_range=[{}, {}]) did not elide its declared \
183 ordering -- the merge will still produce a correctly ordered result, but \
184 it will buffer in memory instead of streaming. Plan:\n{plan_str}",
185 self.query,
186 insert_range.begin.to_rfc3339(),
187 insert_range.end.to_rfc3339()
188 );
189 }
190 honored
191 } else {
192 true
193 };
194
195 let stream =
196 execute_stream(plan, task_ctx).with_context(|| "executing merge query plan")?;
197 Ok(MergeQueryResult {
198 stream,
199 ordering_honored,
200 })
201 }
202
203 async fn execute_sorted_merge(
211 &self,
212 ctx: &SessionContext,
213 columns: &[ScanSortColumn],
214 insert_range: TimeRange,
215 ) -> Result<MergeQueryResult> {
216 {
217 let state = ctx.state_ref();
218 let mut state = state.write();
219 let optimizer = &mut state.config_mut().options_mut().optimizer;
220 optimizer.enable_round_robin_repartition = false;
221 optimizer.repartition_aggregations = false;
222 optimizer.prefer_existing_sort = true;
223 optimizer.repartition_joins = false;
224 }
225
226 let df = ctx.sql(&self.query).await?;
227 let df = df.sort(sort_exprs(columns))?;
228 let task_ctx = Arc::new(df.task_ctx());
229 let plan = df
230 .create_physical_plan()
231 .await
232 .with_context(|| "creating physical plan for per-file merge query")?;
233
234 assert_single_partition(
236 &plan,
237 &format!("merge query {:?}", self.query),
238 insert_range,
239 "executing it would coalesce partitions and destroy the declared ordering. This \
240 likely means repartition_aggregations or enable_round_robin_repartition did not \
241 take effect, or the merge query is missing a collapsing operator (e.g. a \
242 SortPreservingMergeExec) to bring the per-file partitions back to one.",
243 )?;
244
245 assert_ordering_satisfied(
251 &plan,
252 columns,
253 "per-file merge",
254 &format!("merge query {:?}", self.query),
255 insert_range,
256 &format!(
257 "the declared per-file merge columns {columns:?}; refusing to record a false \
258 sort_order guarantee."
259 ),
260 )?;
261
262 let plan_str = displayable(plan.as_ref()).indent(true).to_string();
266 let ordering_honored = !plan_str.contains("SortExec");
267 if !ordering_honored {
268 warn!(
269 "merge query {:?} (insert_range=[{}, {}]) did not elide its declared per-file \
270 ordering -- the merge will still produce a correctly ordered result, but it will \
271 buffer in memory instead of streaming. Plan:\n{plan_str}",
272 self.query,
273 insert_range.begin.to_rfc3339(),
274 insert_range.end.to_rfc3339()
275 );
276 }
277
278 let stream = execute_stream(plan, task_ctx)
279 .with_context(|| "executing per-file merge query plan")?;
280 Ok(MergeQueryResult {
281 stream,
282 ordering_honored,
283 })
284 }
285}
286
287#[async_trait]
288impl PartitionMerger for QueryMerger {
289 async fn execute_merge_query(
290 &self,
291 lakehouse: Arc<LakehouseContext>,
292 partitions_to_merge: Arc<Vec<Partition>>,
293 partitions_all_views: Arc<PartitionCache>,
294 insert_range: TimeRange,
295 ) -> Result<MergeQueryResult> {
296 let reader_factory = lakehouse.reader_factory().clone();
297 let ctx = make_merge_session_context(
298 lakehouse.clone(),
299 partitions_all_views,
300 Some(insert_range),
301 self.view_factory.clone(),
302 self.session_configurator.clone(),
303 CallerContext::maintenance(),
304 )
305 .await?;
306 let src_table = PartitionedTableProvider::with_scan_ordering(
307 self.file_schema.clone(),
308 reader_factory,
309 partitions_to_merge,
310 self.merge_scan_ordering.clone(),
311 );
312 ctx.register_table(
313 TableReference::Bare {
314 table: "source".into(),
315 },
316 Arc::new(src_table),
317 )?;
318
319 match &self.merge_scan_ordering {
320 ScanOrdering::PerFile { columns } => {
322 self.execute_sorted_merge(&ctx, columns, insert_range).await
323 }
324 other @ (ScanOrdering::Unordered | ScanOrdering::Concatenated { .. }) => {
330 self.execute_concatenated_merge(
331 &ctx,
332 other.declares_concatenated_ordering(),
333 insert_range,
334 )
335 .await
336 }
337 }
338 }
339}
340
341fn partition_set_stats(
342 view: Arc<dyn View>,
343 filtered_partitions: &[Partition],
344) -> Result<(i64, i64)> {
345 let mut sum_size: i64 = 0;
346 let mut source_hash: i64 = 0;
347 let latest_file_schema_hash = view.get_file_schema_hash();
348 for p in filtered_partitions {
349 source_hash = if p.source_data_hash.len() == std::mem::size_of::<i64>() {
352 source_hash + hash_to_object_count(&p.source_data_hash)?
353 } else {
354 xxh32(&p.source_data_hash, source_hash as u32).into()
356 };
357
358 sum_size += p.file_size;
359
360 if p.view_metadata.file_schema_hash != latest_file_schema_hash {
361 anyhow::bail!(
362 "incompatible file schema with [{},{}]",
363 p.begin_insert_time().to_rfc3339(),
364 p.end_insert_time().to_rfc3339()
365 );
366 }
367 }
368 Ok((sum_size, source_hash))
369}
370
371pub async fn create_merged_partition(
373 partitions_to_merge: Arc<PartitionCache>,
374 partitions_all_views: Arc<PartitionCache>,
375 lakehouse: Arc<LakehouseContext>,
376 view: Arc<dyn View>,
377 insert_range: TimeRange,
378 logger: Arc<dyn Logger>,
379) -> Result<()> {
380 let view_set_name = &view.get_view_set_name();
381 let view_instance_id = &view.get_view_instance_id();
382 let desc = format!(
383 "[{}, {}] {view_set_name} {view_instance_id}",
384 insert_range.begin.to_rfc3339(),
385 insert_range.end.to_rfc3339()
386 );
387 let mut filtered_partitions = partitions_to_merge
390 .filter_inside_range(view_set_name, view_instance_id, insert_range)
391 .partitions;
392 if filtered_partitions.len() != partitions_to_merge.len() {
393 warn!("partitions_to_merge was not filtered properly");
394 }
395 if filtered_partitions.len() < 2 {
396 logger
397 .write_log_entry(format!("{desc}: not enough partitions to merge"))
398 .await
399 .with_context(|| "writing log")?;
400 return Ok(());
401 }
402 let (sum_size, source_hash) = partition_set_stats(view.clone(), &filtered_partitions)
403 .with_context(|| "partition_set_stats")?;
404 logger
405 .write_log_entry(format!(
406 "{desc}: merging {} partitions sum_size={sum_size}",
407 filtered_partitions.len()
408 ))
409 .await
410 .with_context(|| "write_log_entry")?;
411 let merge_start = Instant::now();
412 filtered_partitions.sort_by_key(|p| p.begin_insert_time());
413 let merged_sort_order = view.get_merged_partition_sort_order(&filtered_partitions);
415 let merge_result = view
416 .merge_partitions(
417 lakehouse.clone(),
418 Arc::new(filtered_partitions),
419 partitions_all_views,
420 insert_range,
421 )
422 .await
423 .with_context(|| "view.merge_partitions")?;
424 let mut merged_stream = merge_result.stream;
427 let (tx, rx) = tokio::sync::mpsc::channel(1);
428 let view_copy = view.clone();
429 let lake = lakehouse.lake().clone();
430 let join_handle = spawn_with_context(write_partition_from_rows(
431 lake,
432 view_copy.get_meta(),
433 view_copy.get_file_schema(),
434 insert_range,
435 source_hash.to_le_bytes().to_vec(),
436 merged_sort_order,
437 RetireMatch::Containment,
438 Vec::new(),
439 rx,
440 logger.clone(),
441 ));
442 let compute_time_bounds = view.get_time_bounds();
443 let ctx =
444 SessionContext::new_with_config_rt(SessionConfig::default(), lakehouse.runtime().clone());
445 let stream_result: Result<()> = async {
446 while let Some(rb_res) = merged_stream.next().await {
447 let rb = rb_res.with_context(|| "receiving record_batch from stream")?;
448 let event_time_range = compute_time_bounds
449 .get_time_bounds(ctx.read_batch(rb.clone()).with_context(|| "read_batch")?)
450 .await?;
451 tx.send(Ok(PartitionRowSet::new(event_time_range, rb, None)))
452 .await
453 .with_context(|| "sending partition row set")?;
454 }
455 Ok(())
456 }
457 .await;
458
459 match stream_result {
460 Ok(()) => {
461 drop(tx);
462 join_handle.await??;
463 logger
464 .write_log_entry(format!(
465 "{desc}: merge completed in {:.3}s",
466 merge_start.elapsed().as_secs_f64()
467 ))
468 .await
469 .with_context(|| "write_log_entry")?;
470 Ok(())
471 }
472 Err(e) => {
473 warn!("aborting merge partition write for {desc}: {e:?}");
474 let _ = tx.send(Err(anyhow::anyhow!("merge stream aborted"))).await;
475 drop(tx);
476 match join_handle.await {
477 Ok(Ok(())) => {}
478 Ok(Err(writer_err)) => {
479 debug!("merge writer task error during abort: {writer_err:?}");
480 }
481 Err(join_err) => {
482 warn!("merge writer task panicked during abort: {join_err:?}");
483 }
484 }
485 Err(e)
486 }
487 }
488}