micromegas_analytics/lakehouse/
sql_partition_spec.rs1use super::{
2 dataframe_time_bounds::DataFrameTimeBounds,
3 partitioned_execution_plan::{assert_ordering_satisfied, assert_single_partition, sort_exprs},
4 view::{PartitionSpec, ScanSortColumn, ViewMetadata},
5 write_partition::write_partition_from_rows,
6};
7use crate::{
8 dfext::typed_column::typed_column_by_name,
9 lakehouse::write_partition::{PartitionRowSet, RetireMatch},
10 record_batch_transformer::RecordBatchTransformer,
11 response_writer::Logger,
12 time::TimeRange,
13};
14use anyhow::{Context, Result};
15use async_trait::async_trait;
16use datafusion::{
17 arrow::{
18 array::{Int64Array, RecordBatch},
19 datatypes::Schema,
20 },
21 execution::{SendableRecordBatchStream, TaskContext},
22 physical_plan::{ExecutionPlan, execute_stream},
23 prelude::*,
24};
25use futures::StreamExt;
26use micromegas_ingestion::data_lake_connection::DataLakeConnection;
27use micromegas_tracing::prelude::*;
28use std::sync::Arc;
29
30pub struct SqlPartitionSpec {
32 ctx: SessionContext,
33 transformer: Arc<dyn RecordBatchTransformer>,
34 compute_time_bounds: Arc<dyn DataFrameTimeBounds>,
35 schema: Arc<Schema>,
36 extract_query: String,
37 view_metadata: ViewMetadata,
38 insert_range: TimeRange,
39 record_count: i64,
40 sort_order: Option<Vec<String>>,
46}
47
48impl SqlPartitionSpec {
49 #[expect(clippy::too_many_arguments)]
50 pub fn new(
51 ctx: SessionContext,
52 transformer: Arc<dyn RecordBatchTransformer>,
53 compute_time_bounds: Arc<dyn DataFrameTimeBounds>,
54 schema: Arc<Schema>,
55 extract_query: String,
56 view_metadata: ViewMetadata,
57 insert_range: TimeRange,
58 record_count: i64,
59 sort_order: Option<Vec<String>>,
60 ) -> Self {
61 Self {
62 ctx,
63 transformer,
64 compute_time_bounds,
65 schema,
66 extract_query,
67 view_metadata,
68 insert_range,
69 record_count,
70 sort_order,
71 }
72 }
73
74 async fn execute_extract_query(&self, df: DataFrame) -> Result<SendableRecordBatchStream> {
80 let columns: Option<Vec<ScanSortColumn>> = self.sort_order.as_ref().map(|sort_order| {
81 sort_order
82 .iter()
83 .map(|c| ScanSortColumn {
84 column: Arc::new(c.clone()),
85 descending: false,
86 })
87 .collect()
88 });
89 let subject = format!("extract query for {}", self.view_metadata.view_set_name);
90 let (plan, task_ctx) =
91 plan_sorted_extract(df, columns.as_deref(), &subject, self.insert_range).await?;
92 execute_stream(plan, task_ctx).with_context(|| "executing extract query plan")
93 }
94}
95
96pub async fn plan_sorted_extract(
107 df: DataFrame,
108 sort_order: Option<&[ScanSortColumn]>,
109 subject: &str,
110 insert_range: TimeRange,
111) -> Result<(Arc<dyn ExecutionPlan>, Arc<TaskContext>)> {
112 let Some(columns) = sort_order else {
113 let task_ctx = Arc::new(df.task_ctx());
114 let plan = df
115 .create_physical_plan()
116 .await
117 .with_context(|| "creating physical plan for extract query")?;
118 return Ok((plan, task_ctx));
119 };
120
121 let df = df.sort(sort_exprs(columns))?;
122 let task_ctx = Arc::new(df.task_ctx());
123 let plan = df
124 .create_physical_plan()
125 .await
126 .with_context(|| "creating physical plan for extract query")?;
127
128 assert_single_partition(
129 &plan,
130 subject,
131 insert_range,
132 "a declared sort_order requires a single-partition, globally-ordered output.",
133 )?;
134
135 assert_ordering_satisfied(
136 &plan,
137 columns,
138 "extract-query",
139 subject,
140 insert_range,
141 &format!(
142 "the declared sort_order {columns:?} even after applying it as a DataFrame::sort; \
143 this indicates the physical plan silently dropped or reordered the applied sort, \
144 not an author mistake."
145 ),
146 )?;
147
148 Ok((plan, task_ctx))
149}
150
151impl std::fmt::Debug for SqlPartitionSpec {
152 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153 write!(f, "SqlPartitionSpec")
154 }
155}
156
157#[async_trait]
158impl PartitionSpec for SqlPartitionSpec {
159 fn is_empty(&self) -> bool {
160 self.record_count < 1
161 }
162
163 fn get_source_data_hash(&self) -> Vec<u8> {
164 self.record_count.to_le_bytes().to_vec()
165 }
166
167 async fn write(&self, lake: Arc<DataLakeConnection>, logger: Arc<dyn Logger>) -> Result<()> {
168 let desc = format!(
171 "[{}, {}] {} {}",
172 self.view_metadata.view_set_name,
173 self.view_metadata.view_instance_id,
174 self.insert_range.begin.to_rfc3339(),
175 self.insert_range.end.to_rfc3339()
176 );
177 logger.write_log_entry(format!("writing {desc}")).await?;
178 let df = self.ctx.sql(&self.extract_query).await?;
179 let mut stream = self.execute_extract_query(df).await?;
180
181 let (tx, rx) = tokio::sync::mpsc::channel(1);
182 let join_handle = spawn_with_context(write_partition_from_rows(
183 lake.clone(),
184 self.view_metadata.clone(),
185 self.schema.clone(),
186 self.insert_range,
187 self.get_source_data_hash(),
188 self.sort_order.clone(),
189 RetireMatch::Containment,
190 Vec::new(),
191 rx,
192 logger.clone(),
193 ));
194
195 while let Some(rb_res) = stream.next().await {
196 let rb = self.transformer.transform(rb_res?).await?;
197 let event_time_range = self
198 .compute_time_bounds
199 .get_time_bounds(self.ctx.read_batch(rb.clone())?)
200 .await?;
201 tx.send(Ok(PartitionRowSet::new(event_time_range, rb, None)))
202 .await?;
203 }
204 drop(tx);
205 join_handle.await??;
206 Ok(())
207 }
208}
209
210#[expect(clippy::too_many_arguments)]
214pub async fn fetch_sql_partition_spec(
215 ctx: SessionContext,
216 transformer: Arc<dyn RecordBatchTransformer>,
217 compute_time_bounds: Arc<dyn DataFrameTimeBounds>,
218 schema: Arc<Schema>,
219 count_src_sql: String,
220 extract_query: String,
221 view_metadata: ViewMetadata,
222 insert_range: TimeRange,
223 sort_order: Option<Vec<String>>,
224) -> Result<SqlPartitionSpec> {
225 let df = ctx.sql(&count_src_sql).await?;
226 let batches: Vec<RecordBatch> = df.collect().await?;
227 if batches.len() != 1 {
228 anyhow::bail!("fetch_sql_partition_spec: query should return a single batch");
229 }
230 let rb = &batches[0];
231 let count_column: &Int64Array = typed_column_by_name(rb, "count")?;
232 if count_column.len() != 1 {
233 anyhow::bail!("fetch_sql_partition_spec: query should return a single row");
234 }
235 let count = count_column.value(0);
236 if count > 0 {
237 trace!(
238 "fetch_sql_partition_spec for view {}, count={count}",
239 &*view_metadata.view_set_name
240 );
241 }
242 Ok(SqlPartitionSpec::new(
243 ctx,
244 transformer,
245 compute_time_bounds,
246 schema,
247 extract_query,
248 view_metadata,
249 insert_range,
250 count,
251 sort_order,
252 ))
253}