1use crate::{
2 lakehouse::async_parquet_writer::AsyncParquetWriter, response_writer::Logger, time::TimeRange,
3};
4use anyhow::{Context, Result};
5use chrono::{DateTime, TimeDelta, Utc};
6use datafusion::{
7 arrow::{array::RecordBatch, datatypes::Schema},
8 parquet::{
9 arrow::AsyncArrowWriter,
10 basic::Compression,
11 file::properties::{WriterProperties, WriterVersion},
12 },
13};
14use micromegas_ingestion::data_lake_connection::DataLakeConnection;
15use micromegas_tracing::prelude::*;
16use object_store::ObjectStoreExt;
17use object_store::buffered::BufWriter;
18use sqlx::Row;
19use std::collections::hash_map::DefaultHasher;
20use std::hash::{Hash, Hasher};
21use std::sync::{Arc, atomic::AtomicI64};
22use tokio::sync::mpsc::Receiver;
23
24use super::{partition::Partition, partition_source_data, view::ViewMetadata};
25
26pub async fn add_file_for_cleanup(
31 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
32 file_path: &str,
33 file_size: i64,
34) -> Result<()> {
35 let expiration = Utc::now()
36 + TimeDelta::try_hours(1)
37 .with_context(|| "calculating expiration time for temporary file")?;
38
39 instrument_named!(
40 sqlx::query("INSERT INTO temporary_files VALUES ($1, $2, $3)")
41 .bind(file_path)
42 .bind(file_size)
43 .bind(expiration)
44 .execute(&mut **transaction),
45 "sql_insert_temporary_file"
46 )
47 .await
48 .with_context(|| format!("adding file {file_path} to temporary files for cleanup"))?;
49
50 Ok(())
51}
52
53pub struct PartitionRowSet {
55 pub rows_time_range: TimeRange,
56 pub rows: RecordBatch,
57 pub max_sort_key_time: Option<DateTime<Utc>>,
62}
63
64impl PartitionRowSet {
65 pub fn new(
73 rows_time_range: TimeRange,
74 rows: RecordBatch,
75 max_sort_key_time: Option<DateTime<Utc>>,
76 ) -> Self {
77 Self {
78 rows_time_range,
79 rows,
80 max_sort_key_time,
81 }
82 }
83}
84
85#[span_fn]
86async fn retire_expired_partitions_batch(
87 lake: &DataLakeConnection,
88 expiration: DateTime<Utc>,
89) -> Result<bool> {
90 let batch_size: i32 = 1000;
91 let mut transaction = lake.db_pool.begin().await?;
92 let rows = instrument_named!(
93 sqlx::query(
94 "DELETE FROM lakehouse_partitions
95 WHERE (view_set_name, view_instance_id, begin_insert_time, end_insert_time) IN (
96 SELECT view_set_name, view_instance_id, begin_insert_time, end_insert_time
97 FROM lakehouse_partitions
98 WHERE end_insert_time < $1
99 LIMIT $2
100 )
101 RETURNING file_path, file_size;",
102 )
103 .bind(expiration)
104 .bind(batch_size)
105 .fetch_all(&mut *transaction),
106 "sql_delete_expired_partitions_batch"
107 )
108 .await
109 .with_context(|| "deleting expired partitions batch")?;
110
111 if rows.is_empty() {
112 return Ok(false);
113 }
114 let count = rows.len();
115 for row in &rows {
116 let file_path: Option<String> = row.try_get("file_path")?;
117 let file_size: i64 = row.try_get("file_size")?;
118 if let Some(path) = file_path {
119 debug!("retiring expired partition file {path} ({file_size} bytes)");
120 add_file_for_cleanup(&mut transaction, &path, file_size).await?;
121 }
122 }
123 transaction.commit().await.with_context(|| "commit")?;
124 info!("retired {count} expired partitions");
125 Ok(count == batch_size as usize)
126}
127
128#[span_fn]
129pub async fn retire_expired_partitions(
130 lake: &DataLakeConnection,
131 expiration: DateTime<Utc>,
132) -> Result<()> {
133 while retire_expired_partitions_batch(lake, expiration).await? {}
134 Ok(())
135}
136
137#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142pub enum RetireMatch {
143 Containment,
152 Overlap,
176}
177
178#[expect(clippy::too_many_arguments)]
193pub async fn retire_partitions(
194 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
195 view_set_name: &str,
196 view_instance_id: &str,
197 begin_insert_time: DateTime<Utc>,
198 end_insert_time: DateTime<Utc>,
199 retire_match: RetireMatch,
200 same_run_ranges: &[TimeRange],
201 logger: Arc<dyn Logger>,
202) -> Result<()> {
203 let same_run_begins: Vec<DateTime<Utc>> = same_run_ranges.iter().map(|r| r.begin).collect();
209 let same_run_ends: Vec<DateTime<Utc>> = same_run_ranges.iter().map(|r| r.end).collect();
210
211 let old_partitions = match retire_match {
213 RetireMatch::Containment if begin_insert_time == end_insert_time => {
214 instrument_named!(
216 sqlx::query(
217 "SELECT file_path, file_size
218 FROM lakehouse_partitions
219 WHERE view_set_name = $1
220 AND view_instance_id = $2
221 AND begin_insert_time = $3
222 AND end_insert_time = $3
223 ;",
224 )
225 .bind(view_set_name)
226 .bind(view_instance_id)
227 .bind(begin_insert_time)
228 .fetch_all(&mut **transaction),
229 "sql_select_old_partitions"
230 )
231 .await
232 .with_context(|| "listing old partitions (exact match)")?
233 }
234 RetireMatch::Containment => {
235 instrument_named!(
237 sqlx::query(
238 "SELECT file_path, file_size
239 FROM lakehouse_partitions
240 WHERE view_set_name = $1
241 AND view_instance_id = $2
242 AND begin_insert_time >= $3
243 AND end_insert_time <= $4
244 ;",
245 )
246 .bind(view_set_name)
247 .bind(view_instance_id)
248 .bind(begin_insert_time)
249 .bind(end_insert_time)
250 .fetch_all(&mut **transaction),
251 "sql_select_old_partitions"
252 )
253 .await
254 .with_context(|| "listing old partitions (range)")?
255 }
256 RetireMatch::Overlap => {
257 instrument_named!(
260 sqlx::query(
261 "SELECT file_path, file_size
262 FROM lakehouse_partitions
263 WHERE view_set_name = $1
264 AND view_instance_id = $2
265 AND tstzrange(begin_insert_time, end_insert_time, '[]') && tstzrange($3, $4, '[]')
266 AND NOT EXISTS (
267 SELECT 1 FROM unnest($5::timestamptz[], $6::timestamptz[]) AS same_run(b, e)
268 WHERE same_run.b = begin_insert_time AND same_run.e = end_insert_time
269 )
270 ;",
271 )
272 .bind(view_set_name)
273 .bind(view_instance_id)
274 .bind(begin_insert_time)
275 .bind(end_insert_time)
276 .bind(&same_run_begins)
277 .bind(&same_run_ends)
278 .fetch_all(&mut **transaction),
279 "sql_select_old_partitions"
280 )
281 .await
282 .with_context(|| "listing old partitions (overlap)")?
283 }
284 };
285
286 if !old_partitions.is_empty() {
288 logger
289 .write_log_entry(format!(
290 "[RETIRE_FOUND] view={}/{} time_range=[{}, {}] found_partitions={}",
291 view_set_name,
292 view_instance_id,
293 begin_insert_time,
294 end_insert_time,
295 old_partitions.len()
296 ))
297 .await?;
298 }
299
300 let mut file_paths = Vec::new();
301 for old_part in &old_partitions {
302 let file_path: Option<String> = old_part.try_get("file_path")?;
303 let file_size: i64 = old_part.try_get("file_size")?;
304 if let Some(path) = file_path {
305 logger
306 .write_log_entry(format!(
307 "adding out of date partition {path} to temporary files to be deleted"
308 ))
309 .await?;
310 add_file_for_cleanup(transaction, &path, file_size).await?;
311 file_paths.push(path);
312 }
313 }
314
315 match retire_match {
316 RetireMatch::Containment if begin_insert_time == end_insert_time => {
317 instrument_named!(
319 sqlx::query(
320 "DELETE from lakehouse_partitions
321 WHERE view_set_name = $1
322 AND view_instance_id = $2
323 AND begin_insert_time = $3
324 AND end_insert_time = $3
325 ;",
326 )
327 .bind(view_set_name)
328 .bind(view_instance_id)
329 .bind(begin_insert_time)
330 .execute(&mut **transaction),
331 "sql_delete_old_partitions"
332 )
333 .await
334 .with_context(|| "deleting out of date partitions (exact match)")?
335 }
336 RetireMatch::Containment => {
337 instrument_named!(
339 sqlx::query(
340 "DELETE from lakehouse_partitions
341 WHERE view_set_name = $1
342 AND view_instance_id = $2
343 AND begin_insert_time >= $3
344 AND end_insert_time <= $4
345 ;",
346 )
347 .bind(view_set_name)
348 .bind(view_instance_id)
349 .bind(begin_insert_time)
350 .bind(end_insert_time)
351 .execute(&mut **transaction),
352 "sql_delete_old_partitions"
353 )
354 .await
355 .with_context(|| "deleting out of date partitions (range)")?
356 }
357 RetireMatch::Overlap => {
358 instrument_named!(
361 sqlx::query(
362 "DELETE from lakehouse_partitions
363 WHERE view_set_name = $1
364 AND view_instance_id = $2
365 AND tstzrange(begin_insert_time, end_insert_time, '[]') && tstzrange($3, $4, '[]')
366 AND NOT EXISTS (
367 SELECT 1 FROM unnest($5::timestamptz[], $6::timestamptz[]) AS same_run(b, e)
368 WHERE same_run.b = begin_insert_time AND same_run.e = end_insert_time
369 )
370 ;",
371 )
372 .bind(view_set_name)
373 .bind(view_instance_id)
374 .bind(begin_insert_time)
375 .bind(end_insert_time)
376 .bind(&same_run_begins)
377 .bind(&same_run_ends)
378 .execute(&mut **transaction),
379 "sql_delete_old_partitions"
380 )
381 .await
382 .with_context(|| "deleting out of date partitions (overlap)")?
383 }
384 };
385 Ok(())
386}
387
388fn generate_partition_lock_key(
390 view_set_name: &str,
391 view_instance_id: &str,
392 begin_insert_time: DateTime<Utc>,
393 end_insert_time: DateTime<Utc>,
394) -> i64 {
395 let mut hasher = DefaultHasher::new();
396 view_set_name.hash(&mut hasher);
397 view_instance_id.hash(&mut hasher);
398 begin_insert_time.hash(&mut hasher);
399 end_insert_time.hash(&mut hasher);
400 hasher.finish() as i64
401}
402
403async fn delete_if_orphan(lake: &DataLakeConnection, file_path: &str) -> Result<()> {
410 let referenced = instrument_named!(
411 sqlx::query("SELECT 1 FROM lakehouse_partitions WHERE file_path = $1 LIMIT 1;")
412 .bind(file_path)
413 .fetch_optional(&lake.db_pool),
414 "sql_select_partition_file_referenced"
415 )
416 .await
417 .with_context(|| "checking whether partition file is referenced")?
418 .is_some();
419 if !referenced {
420 let path = object_store::path::Path::from(file_path);
421 lake.blob_storage
422 .inner()
423 .delete(&path)
424 .await
425 .with_context(|| format!("deleting orphaned partition file {file_path}"))?;
426 }
427 Ok(())
428}
429
430pub async fn insert_partition(
439 lake: &DataLakeConnection,
440 partition: &Partition,
441 retire_match: RetireMatch,
442 same_run_ranges: &[TimeRange],
443 logger: Arc<dyn Logger>,
444) -> Result<()> {
445 let result =
446 insert_partition_transaction(lake, partition, retire_match, same_run_ranges, logger).await;
447 if result.is_err()
448 && let Some(file_path) = &partition.file_path
449 {
450 if let Err(cleanup_err) = delete_if_orphan(lake, file_path).await {
454 warn!("delete_if_orphan failed for {file_path}: {cleanup_err}");
455 }
456 }
457 result
458}
459
460async fn insert_partition_transaction(
461 lake: &DataLakeConnection,
462 partition: &Partition,
463 retire_match: RetireMatch,
464 same_run_ranges: &[TimeRange],
465 logger: Arc<dyn Logger>,
466) -> Result<()> {
467 let lock_key = generate_partition_lock_key(
469 &partition.view_metadata.view_set_name,
470 &partition.view_metadata.view_instance_id,
471 partition.begin_insert_time(),
472 partition.end_insert_time(),
473 );
474
475 let mut transaction = lake.db_pool.begin().await?;
476
477 debug!(
478 "[PARTITION_LOCK] view={}/{} time_range=[{}, {}] lock_key={} - acquiring advisory lock",
479 &partition.view_metadata.view_set_name,
480 &partition.view_metadata.view_instance_id,
481 partition.begin_insert_time(),
482 partition.end_insert_time(),
483 lock_key
484 );
485
486 instrument_named!(
499 sqlx::query("SELECT pg_advisory_xact_lock($1);")
500 .bind(lock_key)
501 .execute(&mut *transaction),
502 "sql_advisory_lock"
503 )
504 .await
505 .with_context(|| "acquiring advisory lock")?;
506
507 let source_row_count = partition_source_data::hash_to_object_count(&partition.source_data_hash)
509 .with_context(|| "decoding source_data_hash to row count")?;
510
511 logger
513 .write_log_entry(format!(
514 "[PARTITION_WRITE_START] view={}/{} time_range=[{}, {}] source_rows={} - lock acquired",
515 partition.view_metadata.view_set_name,
516 partition.view_metadata.view_instance_id,
517 partition.begin_insert_time(),
518 partition.end_insert_time(),
519 source_row_count
520 ))
521 .await?;
522
523 retire_partitions(
528 &mut transaction,
529 &partition.view_metadata.view_set_name,
530 &partition.view_metadata.view_instance_id,
531 partition.begin_insert_time(),
532 partition.end_insert_time(),
533 retire_match,
534 same_run_ranges,
535 logger.clone(),
536 )
537 .await
538 .with_context(|| "retire_partitions")?;
539
540 debug!(
541 "[PARTITION_INSERT_ATTEMPT] view={}/{} time_range=[{}, {}] source_rows={} file_path={:?}",
542 &partition.view_metadata.view_set_name,
543 &partition.view_metadata.view_instance_id,
544 partition.begin_insert_time(),
545 partition.end_insert_time(),
546 source_row_count,
547 partition.file_path
548 );
549
550 let insert_result = instrument_named!(
558 sqlx::query(
559 "INSERT INTO lakehouse_partitions (
560 view_set_name,
561 view_instance_id,
562 begin_insert_time,
563 end_insert_time,
564 min_event_time,
565 max_event_time,
566 updated,
567 file_path,
568 file_size,
569 file_schema_hash,
570 source_data_hash,
571 num_rows,
572 partition_format_version,
573 sort_order,
574 max_sort_key_time
575 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, 2, $13, $14);",
576 )
577 .bind(&*partition.view_metadata.view_set_name)
578 .bind(&*partition.view_metadata.view_instance_id)
579 .bind(partition.begin_insert_time())
580 .bind(partition.end_insert_time())
581 .bind(partition.min_event_time())
582 .bind(partition.max_event_time())
583 .bind(partition.updated)
584 .bind(&partition.file_path)
585 .bind(partition.file_size)
586 .bind(&partition.view_metadata.file_schema_hash)
587 .bind(&partition.source_data_hash)
588 .bind(partition.num_rows)
589 .bind(&partition.sort_order)
590 .bind(partition.max_sort_key_time)
591 .execute(&mut *transaction),
592 "sql_insert_partition"
593 )
594 .await;
595
596 match insert_result {
597 Ok(_) => {
598 debug!(
599 "[PARTITION_INSERT_SUCCESS] view={}/{} time_range=[{}, {}] source_rows={}",
600 &partition.view_metadata.view_set_name,
601 &partition.view_metadata.view_instance_id,
602 partition.begin_insert_time(),
603 partition.end_insert_time(),
604 source_row_count
605 );
606 }
607 Err(ref e) => {
608 logger
609 .write_log_entry(format!(
610 "[PARTITION_INSERT_ERROR] view={}/{} time_range=[{}, {}] source_rows={} error={}",
611 partition.view_metadata.view_set_name,
612 partition.view_metadata.view_instance_id,
613 partition.begin_insert_time(),
614 partition.end_insert_time(),
615 source_row_count,
616 e
617 ))
618 .await?;
619 let overlap_detail = e.as_database_error().and_then(|db_err| {
627 (db_err.constraint() == Some("lakehouse_partitions_no_overlap"))
628 .then(|| db_err.to_string())
629 });
630 if let Some(detail) = overlap_detail {
631 anyhow::bail!(
632 "new partition {}/{} [{}, {}] overlaps an existing partition that this \
633 write's retire step ({retire_match:?}) did not replace, so this write \
634 cannot replace it (likely a concurrent materialization or merge). Retire \
635 the conflicting partition (e.g. retire_partition_by_metadata) or align the \
636 requested range/delta, then retry. Postgres detail: {detail}",
637 partition.view_metadata.view_set_name,
638 partition.view_metadata.view_instance_id,
639 partition.begin_insert_time().to_rfc3339(),
640 partition.end_insert_time().to_rfc3339(),
641 );
642 }
643 return Err(insert_result.unwrap_err().into());
644 }
645 };
646
647 transaction.commit().await.with_context(|| "commit")?;
650
651 info!(
652 "[PARTITION_WRITE_COMMIT] view={}/{} time_range=[{}, {}] file_path={:?} - lock released",
653 &partition.view_metadata.view_set_name,
654 &partition.view_metadata.view_instance_id,
655 partition.begin_insert_time(),
656 partition.end_insert_time(),
657 partition.file_path
658 );
659 Ok(())
660}
661
662struct PartitionWriteResult {
664 num_rows: i64,
665 file_path: Option<String>,
666 file_size: i64,
667 event_time_range: Option<TimeRange>,
668 max_sort_key_time: Option<DateTime<Utc>>,
669}
670
671#[derive(Debug)]
675pub struct RowSetTrackingResult {
676 pub event_time_range: Option<TimeRange>,
677 pub max_sort_key_time: Option<DateTime<Utc>>,
678}
679
680fn check_non_nullable_columns(batch: &RecordBatch, file_schema: &Schema, desc: &str) -> Result<()> {
692 for (i, field) in file_schema.fields().iter().enumerate() {
699 if field.is_nullable() {
700 continue;
701 }
702 let Some(column) = batch.columns().get(i) else {
703 continue;
704 };
705 if column.null_count() > 0 {
706 anyhow::bail!(
707 "writing partition {desc}: column {} ({:?}) is declared NOT NULL but the batch \
708 carries {} null value(s)",
709 i,
710 field.name(),
711 column.null_count()
712 );
713 }
714 }
715 Ok(())
716}
717
718pub async fn write_rows_and_track_times(
729 rb_stream: &mut Receiver<Result<PartitionRowSet, anyhow::Error>>,
730 arrow_writer: &mut AsyncArrowWriter<AsyncParquetWriter>,
731 file_schema: &Schema,
732 logger: &Arc<dyn Logger>,
733 desc: &str,
734) -> Result<RowSetTrackingResult> {
735 let mut min_event_time: Option<DateTime<Utc>> = None;
736 let mut max_event_time: Option<DateTime<Utc>> = None;
737 let mut max_sort_key_time: Option<DateTime<Utc>> = None;
738 let mut any_row_set = false;
739 let mut all_some_sort_key_time = true;
740 let mut write_progression = 0;
741
742 while let Some(msg) = rb_stream.recv().await {
743 let row_set = msg?;
744 any_row_set = true;
745 min_event_time = Some(
746 min_event_time
747 .unwrap_or(row_set.rows_time_range.begin)
748 .min(row_set.rows_time_range.begin),
749 );
750 max_event_time = Some(
751 max_event_time
752 .unwrap_or(row_set.rows_time_range.end)
753 .max(row_set.rows_time_range.end),
754 );
755 match row_set.max_sort_key_time {
756 Some(t) => {
757 max_sort_key_time = Some(max_sort_key_time.map_or(t, |running| running.max(t)));
758 }
759 None => {
760 all_some_sort_key_time = false;
761 }
762 }
763 check_non_nullable_columns(&row_set.rows, file_schema, desc)?;
764 arrow_writer
765 .write(&row_set.rows)
766 .await
767 .with_context(|| "arrow_writer.write")?;
768 if arrow_writer.in_progress_size() > 100 * 1024 * 1024 {
769 arrow_writer
770 .flush()
771 .await
772 .with_context(|| "arrow_writer.flush")?;
773 }
774
775 let progression = arrow_writer.bytes_written() / (10 * 1024 * 1024);
777 if progression != write_progression {
778 write_progression = progression;
779 let written = arrow_writer.bytes_written();
780 logger
781 .write_log_entry(format!("{desc}: written {written} bytes"))
782 .await
783 .with_context(|| "writing log entry")?;
784 }
785 }
786
787 let event_time_range = match (min_event_time, max_event_time) {
788 (Some(begin), Some(end)) => Some(TimeRange { begin, end }),
789 _ => None,
790 };
791 let max_sort_key_time = if any_row_set && all_some_sort_key_time {
792 max_sort_key_time
793 } else {
794 None
795 };
796 Ok(RowSetTrackingResult {
797 event_time_range,
798 max_sort_key_time,
799 })
800}
801
802#[expect(clippy::too_many_arguments)]
804async fn finalize_partition_write(
805 event_time_range: Option<TimeRange>,
806 max_sort_key_time: Option<DateTime<Utc>>,
807 arrow_writer: AsyncArrowWriter<AsyncParquetWriter>,
808 file_path: String,
809 byte_counter: &Arc<AtomicI64>,
810 logger: &Arc<dyn Logger>,
811 desc: &str,
812 object_store: Arc<dyn object_store::ObjectStore>,
813) -> Result<PartitionWriteResult> {
814 if let Some(event_time_range) = event_time_range {
815 let close_result = arrow_writer.close().await;
817
818 match close_result {
819 Ok(parquet_metadata) => {
820 let num_rows = parquet_metadata.file_metadata().num_rows();
821
822 if num_rows == 0 {
825 logger
827 .write_log_entry(format!(
828 "created 0-row file, treating as empty partition for {desc}"
829 ))
830 .await
831 .with_context(|| "writing log entry")?;
832
833 let path = object_store::path::Path::from(file_path.as_str());
835 if let Err(delete_err) = object_store.delete(&path).await {
836 warn!("failed to delete empty file {}: {}", file_path, delete_err);
837 }
838
839 return Ok(PartitionWriteResult {
840 num_rows: 0,
841 file_path: None,
842 file_size: 0,
843 event_time_range: None,
844 max_sort_key_time: None,
845 });
846 }
847
848 debug!(
850 "wrote nb_rows={} size={} path={file_path}",
851 num_rows,
852 byte_counter.load(std::sync::atomic::Ordering::Relaxed)
853 );
854 let file_size = byte_counter.load(std::sync::atomic::Ordering::Relaxed);
855 Ok(PartitionWriteResult {
856 num_rows,
857 file_path: Some(file_path),
858 file_size,
859 event_time_range: Some(event_time_range),
860 max_sort_key_time,
861 })
862 }
863 Err(e) => {
864 warn!(
866 "arrow_writer.close failed, attempting to delete partial file: {}",
867 file_path
868 );
869 let path = object_store::path::Path::from(file_path.as_str());
870 if let Err(delete_err) = object_store.delete(&path).await {
871 warn!(
872 "failed to delete partial file {}: {}",
873 file_path, delete_err
874 );
875 }
876 Err(e).with_context(|| "arrow_writer.close")
877 }
878 }
879 } else {
880 drop(arrow_writer);
883
884 logger
885 .write_log_entry(format!("creating empty partition record for {desc}"))
886 .await
887 .with_context(|| "writing log entry")?;
888
889 let path = object_store::path::Path::from(file_path.as_str());
892 let _ = object_store.delete(&path).await;
893
894 Ok(PartitionWriteResult {
895 num_rows: 0,
896 file_path: None,
897 file_size: 0,
898 event_time_range: None,
899 max_sort_key_time: None,
900 })
901 }
902}
903
904#[expect(clippy::too_many_arguments)]
920pub async fn write_partition_from_rows(
921 lake: Arc<DataLakeConnection>,
922 view_metadata: ViewMetadata,
923 file_schema: Arc<Schema>,
924 insert_range: TimeRange,
925 source_data_hash: Vec<u8>,
926 sort_order: Option<Vec<String>>,
927 retire_match: RetireMatch,
928 same_run_ranges: Vec<TimeRange>,
929 mut rb_stream: Receiver<Result<PartitionRowSet, anyhow::Error>>,
930 logger: Arc<dyn Logger>,
931) -> Result<()> {
932 let file_id = uuid::Uuid::new_v4();
933 let file_path = format!(
934 "views/{}/{}/{}/{}_{file_id}.parquet",
935 view_metadata.view_set_name,
936 view_metadata.view_instance_id,
937 insert_range.begin.format("%Y-%m-%d"),
938 insert_range.begin.format("%H-%M-%S")
939 );
940 let byte_counter = Arc::new(AtomicI64::new(0));
941 let object_store_writer = AsyncParquetWriter::new(
942 BufWriter::new(
943 lake.blob_storage.inner(),
944 object_store::path::Path::parse(&file_path).with_context(|| "parsing path")?,
945 )
946 .with_max_concurrency(2),
947 byte_counter.clone(),
948 );
949
950 let props = WriterProperties::builder()
953 .set_writer_version(WriterVersion::PARQUET_2_0)
954 .set_compression(Compression::LZ4_RAW)
955 .set_statistics_enabled(parquet::file::properties::EnabledStatistics::Page)
958 .set_max_row_group_row_count(Some(128 * 1024))
961 .build();
962 let mut arrow_writer =
963 AsyncArrowWriter::try_new(object_store_writer, file_schema.clone(), Some(props))
964 .with_context(|| "allocating async arrow writer")?;
965
966 let desc = format!(
967 "[{}, {}] {} {}",
968 view_metadata.view_set_name,
969 view_metadata.view_instance_id,
970 insert_range.begin.to_rfc3339(),
971 insert_range.end.to_rfc3339()
972 );
973
974 let RowSetTrackingResult {
976 event_time_range,
977 max_sort_key_time,
978 } = match write_rows_and_track_times(
979 &mut rb_stream,
980 &mut arrow_writer,
981 &file_schema,
982 &logger,
983 &desc,
984 )
985 .await
986 {
987 Ok(result) => result,
988 Err(e) => {
989 drop(arrow_writer);
993 warn!(
994 "write_rows_and_track_times failed, attempting to delete partial file: {}",
995 file_path
996 );
997 let path = object_store::path::Path::from(file_path.as_str());
998 if let Err(delete_err) = lake.blob_storage.inner().delete(&path).await {
999 warn!(
1000 "failed to delete partial file {}: {}",
1001 file_path, delete_err
1002 );
1003 }
1004 return Err(e).with_context(|| "write_rows_and_track_times");
1005 }
1006 };
1007
1008 let result = finalize_partition_write(
1010 event_time_range,
1011 max_sort_key_time,
1012 arrow_writer,
1013 file_path,
1014 &byte_counter,
1015 &logger,
1016 &desc,
1017 lake.blob_storage.inner(),
1018 )
1019 .await?;
1020
1021 let warm_file_path = result.file_path.clone();
1023 insert_partition(
1024 &lake,
1025 &Partition {
1026 view_metadata,
1027 insert_time_range: insert_range,
1028 event_time_range: result.event_time_range,
1029 updated: sqlx::types::chrono::Utc::now(),
1030 file_path: result.file_path,
1031 file_size: result.file_size,
1032 source_data_hash,
1033 num_rows: result.num_rows,
1034 sort_order,
1035 max_sort_key_time: result.max_sort_key_time,
1036 },
1037 retire_match,
1038 &same_run_ranges,
1039 logger,
1040 )
1041 .await
1042 .with_context(|| "insert_partition")?;
1043
1044 if let Some(file_path) = &warm_file_path {
1049 lake.warm_object(file_path, result.file_size);
1050 }
1051 Ok(())
1052}