micromegas_analytics/lakehouse/view.rs
1use super::{
2 batch_update::PartitionCreationStrategy,
3 dataframe_time_bounds::DataFrameTimeBounds,
4 lakehouse_context::LakehouseContext,
5 materialized_view::MaterializedView,
6 merge::{MergeQueryResult, PartitionMerger, QueryMerger},
7 partition::Partition,
8 partition_cache::PartitionCache,
9 session_configurator::NoOpSessionConfigurator,
10 view_factory::ViewFactory,
11};
12use crate::{response_writer::Logger, time::TimeRange};
13use anyhow::Result;
14use async_trait::async_trait;
15use chrono::{DateTime, TimeDelta, Utc};
16use datafusion::{arrow::datatypes::Schema, logical_expr::Expr, prelude::*, sql::TableReference};
17use micromegas_ingestion::data_lake_connection::DataLakeConnection;
18use std::fmt::Debug;
19use std::sync::Arc;
20
21/// A trait for defining a partition specification.
22#[async_trait]
23pub trait PartitionSpec: Send + Sync + Debug {
24 /// Returns true if the partition is empty.
25 fn is_empty(&self) -> bool;
26 /// Returns a hash of the source data.
27 fn get_source_data_hash(&self) -> Vec<u8>;
28 /// Writes the partition to the data lake.
29 async fn write(&self, lake: Arc<DataLakeConnection>, logger: Arc<dyn Logger>) -> Result<()>;
30}
31
32/// Metadata about a view.
33#[derive(Debug, Clone)]
34pub struct ViewMetadata {
35 pub view_set_name: Arc<String>,
36 pub view_instance_id: Arc<String>,
37 pub file_schema_hash: Vec<u8>,
38}
39
40/// A column an ordering is expressed over (ascending unless `descending`).
41#[derive(Clone, Debug)]
42pub struct ScanSortColumn {
43 pub column: Arc<String>,
44 pub descending: bool,
45}
46
47/// A trait for defining a view.
48#[async_trait]
49pub trait View: std::fmt::Debug + Send + Sync {
50 /// name of the table from the user's perspective
51 fn get_view_set_name(&self) -> Arc<String>;
52
53 /// get_view_instance_id can be a process_id, a stream_id or 'global'.
54 fn get_view_instance_id(&self) -> Arc<String>;
55
56 /// make_batch_partition_spec determines what should be found in an up to date partition.
57 /// The resulting PartitionSpec can be used to validate existing partitions are create a new one.
58 async fn make_batch_partition_spec(
59 &self,
60 lakehouse: Arc<LakehouseContext>,
61 existing_partitions: Arc<PartitionCache>,
62 insert_range: TimeRange,
63 ) -> Result<Arc<dyn PartitionSpec>>;
64
65 /// get_file_schema_hash returns a hash (can be a version number, version string, etc.) that allows
66 /// to identify out of date partitions.
67 fn get_file_schema_hash(&self) -> Vec<u8>;
68
69 /// get_file_schema returns the schema of the partition file in object storage
70 fn get_file_schema(&self) -> Arc<Schema>;
71
72 /// jit_update creates or updates process-specific partitions before a query
73 async fn jit_update(
74 &self,
75 lakehouse: Arc<LakehouseContext>,
76 query_range: Option<TimeRange>,
77 ) -> Result<()>;
78
79 /// make_time_filter returns a set of expressions that will filter out the rows of the partition
80 /// outside the time range requested.
81 fn make_time_filter(&self, _begin: DateTime<Utc>, _end: DateTime<Utc>) -> Result<Vec<Expr>>;
82
83 // a view must provide a way to compute the time bounds of a DataFrame corresponding to its schema
84 fn get_time_bounds(&self) -> Arc<dyn DataFrameTimeBounds>;
85
86 /// register the table in the SessionContext
87 async fn register_table(&self, ctx: &SessionContext, table: MaterializedView) -> Result<()> {
88 let view_set_name = self.get_view_set_name().to_string();
89 ctx.register_table(
90 TableReference::Bare {
91 table: view_set_name.into(),
92 },
93 Arc::new(table),
94 )?;
95 Ok(())
96 }
97
98 async fn merge_partitions(
99 &self,
100 lakehouse: Arc<LakehouseContext>,
101 partitions_to_merge: Arc<Vec<Partition>>,
102 partitions_all_views: Arc<PartitionCache>,
103 insert_range: TimeRange,
104 ) -> Result<MergeQueryResult> {
105 let merge_query = Arc::new(String::from("SELECT * FROM source;"));
106 let empty_view_factory = Arc::new(ViewFactory::new(vec![]));
107 let merger = QueryMerger::new(
108 empty_view_factory,
109 Arc::new(NoOpSessionConfigurator),
110 self.get_file_schema(),
111 merge_query,
112 );
113 merger
114 .execute_merge_query(
115 lakehouse,
116 partitions_to_merge,
117 partitions_all_views,
118 insert_range,
119 )
120 .await
121 }
122
123 /// Returns the sort guarantee a merge of `partitions_to_merge` will actually produce, to be
124 /// recorded as the resulting partition's `Partition::sort_order` (see
125 /// `merge::create_merged_partition`).
126 ///
127 /// This is a distinct concept from `get_scan_output_ordering()`: that one is a trusted
128 /// scan-ordering declaration consumed during physical planning, while this one is a record of
129 /// what a specific merge actually produced, computed purely from the input partitions (before
130 /// `merge_partitions` runs) and independent of whether DataFusion's elision optimization
131 /// happened to succeed for this particular run.
132 ///
133 /// Default: `None` -- no guarantee recorded, ignoring the argument.
134 fn get_merged_partition_sort_order(
135 &self,
136 _partitions_to_merge: &[Partition],
137 ) -> Option<Vec<String>> {
138 None
139 }
140
141 /// tells the daemon which view should be materialized and in what order
142 fn get_update_group(&self) -> Option<i32>;
143
144 /// allow the view to subdivide the requested partition
145 fn get_max_partition_time_delta(&self, _strategy: &PartitionCreationStrategy) -> TimeDelta {
146 TimeDelta::days(1)
147 }
148
149 /// Declares an ordering the view's partition scan *already* emits, letting DataFusion
150 /// elide redundant `Sort` nodes for queries that `ORDER BY` these columns.
151 ///
152 /// Returning a non-empty ordering is a correctness contract the view must guarantee:
153 /// - rows within each partition file are already sorted by these columns, AND
154 /// - the leading column is the view's min-event-time column, and partition event-time
155 /// ranges are non-overlapping (so files concatenate in globally-sorted order).
156 ///
157 /// For `ThreadSpansView`, the non-overlapping-ranges half of this contract rests on JIT
158 /// partitions being sliced in event-time order, which in turn assumes a stream's blocks are
159 /// registered in event-time order — an assumption documented but not enforced (see
160 /// `thread_spans_view.rs`). If that assumption is ever violated, output would be silently
161 /// mis-ordered rather than re-sorted, since no `Sort` node remains once this ordering is
162 /// declared.
163 ///
164 /// Default: empty (no declared ordering — DataFusion sorts as usual).
165 fn get_scan_output_ordering(&self) -> Vec<ScanSortColumn> {
166 vec![]
167 }
168}
169
170impl dyn View {
171 pub fn get_meta(&self) -> ViewMetadata {
172 ViewMetadata {
173 view_set_name: self.get_view_set_name(),
174 view_instance_id: self.get_view_instance_id(),
175 file_schema_hash: self.get_file_schema_hash(),
176 }
177 }
178}