micromegas_analytics/lakehouse/
list_partitions_table_function.rs1use super::audience_guard::{AudienceGuard, IdKind};
2use super::read_scope::ReadScope;
3use crate::sql_arrow_bridge::rows_to_record_batch;
4use async_trait::async_trait;
5use datafusion::arrow::array::RecordBatch;
6use datafusion::arrow::datatypes::DataType;
7use datafusion::arrow::datatypes::Field;
8use datafusion::arrow::datatypes::Schema;
9use datafusion::arrow::datatypes::SchemaRef;
10use datafusion::arrow::datatypes::TimeUnit;
11use datafusion::catalog::Session;
12use datafusion::catalog::TableFunctionArgs;
13use datafusion::catalog::TableFunctionImpl;
14use datafusion::catalog::TableProvider;
15use datafusion::datasource::TableType;
16use datafusion::datasource::memory::{DataSourceExec, MemorySourceConfig};
17use datafusion::error::DataFusionError;
18use datafusion::physical_plan::ExecutionPlan;
19use datafusion::prelude::Expr;
20use micromegas_ingestion::data_lake_connection::DataLakeConnection;
21use micromegas_tracing::prelude::*;
22use sqlx::Row;
23use std::collections::HashSet;
24use std::sync::Arc;
25use uuid::Uuid;
26
27const SELECT_PARTITION_COLUMNS: &str = "SELECT view_set_name,
30 view_instance_id,
31 begin_insert_time,
32 end_insert_time,
33 min_event_time,
34 max_event_time,
35 updated,
36 file_path,
37 file_size,
38 file_schema_hash,
39 source_data_hash,
40 num_rows,
41 partition_format_version,
42 sort_order,
43 max_sort_key_time";
44
45#[derive(Debug)]
47pub struct ListPartitionsTableFunction {
48 lake: Arc<DataLakeConnection>,
49 guard: Arc<AudienceGuard>,
50}
51
52impl ListPartitionsTableFunction {
53 pub fn new(lake: Arc<DataLakeConnection>, guard: Arc<AudienceGuard>) -> Self {
54 Self { lake, guard }
55 }
56}
57
58impl TableFunctionImpl for ListPartitionsTableFunction {
59 fn call_with_args(
60 &self,
61 _args: TableFunctionArgs,
62 ) -> datafusion::error::Result<Arc<dyn TableProvider>> {
63 Ok(Arc::new(ListPartitionsTableProvider {
64 lake: self.lake.clone(),
65 guard: self.guard.clone(),
66 }))
67 }
68}
69
70#[derive(Debug)]
72pub struct ListPartitionsTableProvider {
73 pub lake: Arc<DataLakeConnection>,
74 guard: Arc<AudienceGuard>,
75}
76
77#[async_trait]
78impl TableProvider for ListPartitionsTableProvider {
79 fn schema(&self) -> SchemaRef {
80 Arc::new(Schema::new(vec![
81 Field::new("view_set_name", DataType::Utf8, false),
82 Field::new("view_instance_id", DataType::Utf8, false),
83 Field::new(
84 "begin_insert_time",
85 DataType::Timestamp(TimeUnit::Nanosecond, Some("+00:00".into())),
86 false,
87 ),
88 Field::new(
89 "end_insert_time",
90 DataType::Timestamp(TimeUnit::Nanosecond, Some("+00:00".into())),
91 false,
92 ),
93 Field::new(
94 "min_event_time",
95 DataType::Timestamp(TimeUnit::Nanosecond, Some("+00:00".into())),
96 true,
97 ),
98 Field::new(
99 "max_event_time",
100 DataType::Timestamp(TimeUnit::Nanosecond, Some("+00:00".into())),
101 true,
102 ),
103 Field::new(
104 "updated",
105 DataType::Timestamp(TimeUnit::Nanosecond, Some("+00:00".into())),
106 false,
107 ),
108 Field::new("file_path", DataType::Utf8, true),
109 Field::new("file_size", DataType::Int64, false),
110 Field::new("file_schema_hash", DataType::Binary, false),
111 Field::new("source_data_hash", DataType::Binary, false),
112 Field::new("num_rows", DataType::Int64, false),
113 Field::new("partition_format_version", DataType::Int32, false),
114 Field::new(
115 "sort_order",
116 DataType::List(Arc::new(Field::new("tag", DataType::Utf8, false))),
117 true,
118 ),
119 Field::new(
120 "max_sort_key_time",
121 DataType::Timestamp(TimeUnit::Nanosecond, Some("+00:00".into())),
122 true,
123 ),
124 ]))
125 }
126
127 fn table_type(&self) -> TableType {
128 TableType::Temporary
129 }
130
131 async fn scan(
132 &self,
133 _state: &dyn Session,
134 projection: Option<&Vec<usize>>,
135 _filters: &[Expr],
136 limit: Option<usize>,
137 ) -> datafusion::error::Result<Arc<dyn ExecutionPlan>> {
138 let restricted = *self.guard.read_scope() != ReadScope::All;
144
145 let limit_clause = match (restricted, limit) {
153 (false, Some(n)) => format!("\n LIMIT {n}"),
154 _ => String::new(),
155 };
156 let query = format!("{SELECT_PARTITION_COLUMNS} FROM lakehouse_partitions{limit_clause};");
165
166 let rows = instrument_named!(
167 sqlx::query(&query).fetch_all(&self.lake.db_pool),
168 "sql_select_list_partitions"
169 )
170 .await
171 .map_err(|e| DataFusionError::External(e.into()))?;
172
173 let rows = if restricted {
174 self.filter_rows(rows, limit).await?
175 } else {
176 rows
177 };
178 let rb = if rows.is_empty() {
187 RecordBatch::new_empty(self.schema())
188 } else {
189 rows_to_record_batch(&rows).map_err(|e| DataFusionError::External(e.into()))?
190 };
191
192 let source = MemorySourceConfig::try_new(
193 &[vec![rb]],
194 self.schema(),
195 projection.map(|v| v.to_owned()),
196 )?;
197 Ok(DataSourceExec::from_data_source(source))
198 }
199}
200
201impl ListPartitionsTableProvider {
202 const RESOLVE_CHUNK_ROWS: usize = 1_000;
210
211 async fn filter_rows(
220 &self,
221 rows: Vec<sqlx::postgres::PgRow>,
222 limit: Option<usize>,
223 ) -> datafusion::error::Result<Vec<sqlx::postgres::PgRow>> {
224 let mut kept = Vec::new();
225 let mut rows_iter = rows.into_iter();
226 loop {
227 let chunk: Vec<_> = (&mut rows_iter).take(Self::RESOLVE_CHUNK_ROWS).collect();
228 if chunk.is_empty() {
229 break;
230 }
231 let mut candidate_ids: Vec<Uuid> = Vec::with_capacity(chunk.len());
232 let mut seen_ids: HashSet<Uuid> = HashSet::with_capacity(chunk.len());
233 let mut row_ids: Vec<Option<Uuid>> = Vec::with_capacity(chunk.len());
234 for row in &chunk {
235 let view_instance_id: &str = row
236 .try_get("view_instance_id")
237 .map_err(|e| DataFusionError::External(e.into()))?;
238 let id = Uuid::parse_str(view_instance_id).ok();
239 if let Some(id) = id
240 && seen_ids.insert(id)
241 {
242 candidate_ids.push(id);
243 }
244 row_ids.push(id);
245 }
246 let readable = self
247 .guard
248 .readable_ids(&candidate_ids, IdKind::ProcessOrStream)
249 .await?;
250 for (row, id) in chunk.into_iter().zip(row_ids) {
251 let keep = match id {
252 Some(uuid) => readable.contains(&uuid),
253 None => {
254 let view_instance_id: &str = row
255 .try_get("view_instance_id")
256 .map_err(|e| DataFusionError::External(e.into()))?;
257 let view_set_name: &str = row
258 .try_get("view_set_name")
259 .map_err(|e| DataFusionError::External(e.into()))?;
260 view_instance_id == "global"
261 && self.guard.global_rows_visible(view_set_name)
262 }
263 };
264 if keep {
265 kept.push(row);
266 if let Some(n) = limit
267 && kept.len() >= n
268 {
269 return Ok(kept);
270 }
271 }
272 }
273 }
274 Ok(kept)
275 }
276}