Skip to main content

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}