Skip to main content

metric_engine/
data_region.rs

1// Copyright 2023 Greptime Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use api::v1::SemanticType;
16use common_query::native_histogram::is_native_histogram_value_type;
17use common_telemetry::{debug, info};
18use datatypes::schema::{SkippingIndexOptions, SkippingIndexType};
19use mito2::engine::MitoEngine;
20use snafu::ResultExt;
21use store_api::metadata::ColumnMetadata;
22use store_api::region_engine::RegionEngine;
23use store_api::region_request::{
24    AddColumn, AffectedRows, AlterKind, RegionAlterRequest, RegionRequest,
25};
26use store_api::storage::consts::ReservedColumnId;
27use store_api::storage::{ConcreteDataType, RegionId};
28
29use crate::engine::IndexOptions;
30use crate::error::{
31    AddingFieldColumnSnafu, ColumnTypeMismatchSnafu, ForbiddenPhysicalAlterSnafu,
32    MitoReadOperationSnafu, MitoWriteOperationSnafu, Result, SetSkippingIndexOptionSnafu,
33};
34use crate::metrics::{FORBIDDEN_OPERATION_COUNT, MITO_DDL_DURATION, PHYSICAL_COLUMN_COUNT};
35use crate::utils;
36
37/// This is a generic handler like [MetricEngine](crate::engine::MetricEngine). It
38/// will handle all the data related operations across physical tables. Thus
39/// every operation should be associated to a [RegionId], which is the physical
40/// table id + region sequence. This handler will transform the region group by
41/// itself.
42pub struct DataRegion {
43    mito: MitoEngine,
44}
45
46impl DataRegion {
47    pub fn new(mito: MitoEngine) -> Self {
48        Self { mito }
49    }
50
51    /// Submit an alter request to underlying physical region.
52    ///
53    /// This method will change the nullability of those given columns.
54    /// [SemanticType::Tag] will become nullable column as it's shared between
55    /// logical regions.
56    ///
57    /// Invoker don't need to set up or verify the column id. This method will adjust
58    /// it using underlying schema.
59    ///
60    /// This method will also set the nullable marker to true. All of those change are applies
61    /// to `columns` in-place.
62    pub async fn add_columns(
63        &self,
64        region_id: RegionId,
65        columns: Vec<ColumnMetadata>,
66        index_options: IndexOptions,
67    ) -> Result<()> {
68        // Return early if no new columns are added.
69        if columns.is_empty() {
70            return Ok(());
71        }
72
73        let region_id = utils::to_data_region_id(region_id);
74
75        let num_columns = columns.len();
76        let request = self
77            .assemble_alter_request(region_id, columns, index_options)
78            .await?;
79
80        let _timer = MITO_DDL_DURATION.start_timer();
81
82        let _ = self
83            .mito
84            .handle_request(region_id, request)
85            .await
86            .context(MitoWriteOperationSnafu)?;
87
88        PHYSICAL_COLUMN_COUNT.add(num_columns as _);
89
90        Ok(())
91    }
92
93    /// Generate wrapped [RegionAlterRequest] with given [ColumnMetadata].
94    /// This method will modify `columns` in-place.
95    async fn assemble_alter_request(
96        &self,
97        region_id: RegionId,
98        columns: Vec<ColumnMetadata>,
99        index_options: IndexOptions,
100    ) -> Result<RegionRequest> {
101        // retrieve underlying version
102        let region_metadata = self
103            .mito
104            .get_metadata(region_id)
105            .await
106            .context(MitoReadOperationSnafu)?;
107
108        // find the max column id
109        let new_column_id_start = 1 + region_metadata
110            .column_metadatas
111            .iter()
112            .filter_map(|c| {
113                if ReservedColumnId::is_reserved(c.column_id) {
114                    None
115                } else {
116                    Some(c.column_id)
117                }
118            })
119            .max()
120            .unwrap_or(0);
121
122        // overwrite semantic type
123        let new_columns = columns
124            .into_iter()
125            .enumerate()
126            .map(|(delta, mut c)| {
127                match c.semantic_type {
128                    SemanticType::Tag => {
129                        if !c.column_schema.data_type.is_string() {
130                            return ColumnTypeMismatchSnafu {
131                                expect: ConcreteDataType::string_datatype(),
132                                actual: c.column_schema.data_type.clone(),
133                            }
134                            .fail();
135                        }
136                    }
137                    // Field columns can only be added to the shared physical
138                    // table for native histograms; ordinary metric fields are
139                    // created with the logical table.
140                    SemanticType::Field
141                        if is_native_histogram_value_type(&c.column_schema.data_type) => {}
142                    _ => {
143                        return AddingFieldColumnSnafu {
144                            name: &c.column_schema.name,
145                        }
146                        .fail();
147                    }
148                }
149
150                c.column_id = new_column_id_start + delta as u32;
151                c.column_schema.set_nullable();
152                if c.semantic_type == SemanticType::Tag {
153                    match index_options {
154                        IndexOptions::None => {}
155                        IndexOptions::Inverted => {
156                            c.column_schema.set_inverted_index(true);
157                        }
158                        IndexOptions::Skipping {
159                            granularity,
160                            false_positive_rate,
161                        } => {
162                            c.column_schema
163                                .set_skipping_options(
164                                    &SkippingIndexOptions::new(
165                                        granularity,
166                                        false_positive_rate,
167                                        SkippingIndexType::BloomFilter,
168                                    )
169                                    .context(SetSkippingIndexOptionSnafu)?,
170                                )
171                                .context(SetSkippingIndexOptionSnafu)?;
172                        }
173                    }
174                }
175
176                Ok(AddColumn {
177                    column_metadata: c.clone(),
178                    location: None,
179                })
180            })
181            .collect::<Result<_>>()?;
182
183        debug!("Adding (Column id assigned) columns {new_columns:?} to region {region_id:?}");
184        // assemble alter request
185        let alter_request = RegionRequest::Alter(RegionAlterRequest {
186            kind: AlterKind::AddColumns {
187                columns: new_columns,
188            },
189        });
190
191        Ok(alter_request)
192    }
193
194    pub async fn write_data(
195        &self,
196        region_id: RegionId,
197        request: RegionRequest,
198    ) -> Result<AffectedRows> {
199        let region_id = utils::to_data_region_id(region_id);
200        self.mito
201            .handle_request(region_id, request)
202            .await
203            .context(MitoWriteOperationSnafu)
204            .map(|result| result.affected_rows)
205    }
206
207    pub async fn physical_columns(
208        &self,
209        physical_region_id: RegionId,
210    ) -> Result<Vec<ColumnMetadata>> {
211        let data_region_id = utils::to_data_region_id(physical_region_id);
212        let metadata = self
213            .mito
214            .get_metadata(data_region_id)
215            .await
216            .context(MitoReadOperationSnafu)?;
217        Ok(metadata.column_metadatas.clone())
218    }
219
220    pub async fn alter_region_options(
221        &self,
222        region_id: RegionId,
223        request: RegionAlterRequest,
224    ) -> Result<AffectedRows> {
225        match request.kind {
226            AlterKind::SetRegionOptions { options: _ }
227            | AlterKind::UnsetRegionOptions { keys: _ }
228            | AlterKind::SetIndexes { options: _ }
229            | AlterKind::UnsetIndexes { options: _ }
230            | AlterKind::SyncColumns {
231                column_metadatas: _,
232            } => {
233                let region_id = utils::to_data_region_id(region_id);
234                self.mito
235                    .handle_request(region_id, RegionRequest::Alter(request))
236                    .await
237                    .context(MitoWriteOperationSnafu)
238                    .map(|result| result.affected_rows)
239            }
240            _ => {
241                info!(
242                    "Metric region received alter request {request:?} on physical region {region_id:?}"
243                );
244                FORBIDDEN_OPERATION_COUNT.inc();
245
246                ForbiddenPhysicalAlterSnafu.fail()
247            }
248        }
249    }
250}
251
252#[cfg(test)]
253mod test {
254    use common_query::prelude::{greptime_timestamp, greptime_value};
255    use datatypes::prelude::ConcreteDataType;
256    use datatypes::schema::ColumnSchema;
257
258    use super::*;
259    use crate::test_util::TestEnv;
260
261    #[tokio::test]
262    async fn test_add_columns() {
263        let env = TestEnv::new().await;
264        env.init_metric_region().await;
265
266        let current_version = env
267            .mito()
268            .get_metadata(utils::to_data_region_id(env.default_physical_region_id()))
269            .await
270            .unwrap()
271            .schema_version;
272        // TestEnv will create a logical region which changes the version to 1.
273        assert_eq!(current_version, 1);
274
275        let new_columns = vec![
276            ColumnMetadata {
277                column_id: 0,
278                semantic_type: SemanticType::Tag,
279                column_schema: ColumnSchema::new(
280                    "tag2",
281                    ConcreteDataType::string_datatype(),
282                    false,
283                ),
284            },
285            ColumnMetadata {
286                column_id: 0,
287                semantic_type: SemanticType::Tag,
288                column_schema: ColumnSchema::new(
289                    "tag3",
290                    ConcreteDataType::string_datatype(),
291                    false,
292                ),
293            },
294        ];
295        env.data_region()
296            .add_columns(
297                env.default_physical_region_id(),
298                new_columns,
299                IndexOptions::Inverted,
300            )
301            .await
302            .unwrap();
303
304        let new_metadata = env
305            .mito()
306            .get_metadata(utils::to_data_region_id(env.default_physical_region_id()))
307            .await
308            .unwrap();
309        let column_names = new_metadata
310            .column_metadatas
311            .iter()
312            .map(|c| &c.column_schema.name)
313            .collect::<Vec<_>>();
314        let expected = vec![
315            greptime_timestamp(),
316            greptime_value(),
317            "__table_id",
318            "__tsid",
319            "job",
320            "tag2",
321            "tag3",
322        ];
323        assert_eq!(column_names, expected);
324    }
325
326    // Only string is allowed for tag column
327    #[tokio::test]
328    async fn test_add_invalid_column() {
329        let env = TestEnv::new().await;
330        env.init_metric_region().await;
331
332        let new_columns = vec![ColumnMetadata {
333            column_id: 0,
334            semantic_type: SemanticType::Tag,
335            column_schema: ColumnSchema::new("tag2", ConcreteDataType::int64_datatype(), false),
336        }];
337        let result = env
338            .data_region()
339            .add_columns(
340                env.default_physical_region_id(),
341                new_columns,
342                IndexOptions::Inverted,
343            )
344            .await;
345        assert!(result.is_err());
346    }
347}