Skip to main content

store_api/
region_request.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 std::collections::HashMap;
16use std::fmt::{self, Display};
17use std::time::Duration;
18
19use api::helper::{ColumnDataTypeWrapper, from_pb_time_ranges, from_pb_time_unit};
20use api::v1::add_column_location::LocationType;
21use api::v1::column_def::{
22    as_fulltext_option_analyzer, as_fulltext_option_backend, as_skipping_index_type,
23};
24use api::v1::region::bulk_insert_request::Body;
25use api::v1::region::{
26    AlterRequest, AlterRequests, BuildIndexRequest, BulkInsertRequest,
27    CleanUpRequest as PbCleanUpRequest, CloseRequest, CompactRequest, CreateRequest,
28    CreateRequests, DeleteRequests, DropRequest, DropRequests, FlushRequest, InsertRequests,
29    OpenRequest, TruncateRequest, alter_request, build_index_request, compact_request,
30    region_request, truncate_request,
31};
32use api::v1::{
33    self, Analyzer, ArrowIpc, FulltextBackend as PbFulltextBackend, Option as PbOption, Rows,
34    SemanticType, SkippingIndexType as PbSkippingIndexType, WriteHint,
35};
36use arrow_schema::extension::ExtensionType;
37pub use common_base::AffectedRows;
38use common_base::readable_size::ReadableSize;
39use common_grpc::flight::FlightDecoder;
40use common_recordbatch::DfRecordBatch;
41use common_time::range::TimestampRange;
42use common_time::{TimeToLive, Timestamp};
43use datatypes::error::time_index_not_widening_error;
44use datatypes::extension::json::Json2ExtensionType;
45use datatypes::json::{JsonSettings, JsonTypeHint};
46use datatypes::prelude::ConcreteDataType;
47use datatypes::schema::{FulltextOptions, SkippingIndexOptions};
48use num_enum::TryFromPrimitive;
49use serde::{Deserialize, Deserializer, Serialize, Serializer};
50use snafu::{OptionExt, ResultExt, ensure};
51use strum::{AsRefStr, IntoStaticStr};
52
53use crate::logstore::entry;
54use crate::metadata::{
55    ColumnMetadata, ConvertTimeRangesSnafu, DecodeProtoSnafu, FlightCodecSnafu,
56    InvalidIndexOptionSnafu, InvalidRawRegionRequestSnafu, InvalidRegionRequestSnafu,
57    InvalidSetRegionOptionRequestSnafu, InvalidUnsetRegionOptionRequestSnafu, MetadataError,
58    RegionMetadata, Result, UnexpectedSnafu,
59};
60use crate::metric_engine_consts::PHYSICAL_TABLE_METADATA_KEY;
61use crate::metrics;
62use crate::mito_engine_options::{
63    APPEND_MODE_KEY, AUTO_FLUSH_INTERVAL_KEY, MAX_ROW_GROUP_ROW_COUNT,
64    MAX_ROW_GROUP_ROW_COUNT_LIMIT, PRESERVE_ROW_SEQUENCE, SKIP_WAL_KEY, SST_FORMAT_KEY, TTL_KEY,
65    TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER, TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM,
66    TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER, TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM,
67    TWCS_MAX_OUTPUT_FILE_SIZE, TWCS_TIME_WINDOW, TWCS_TRIGGER_FILE_NUM, WRITE_BUFFER_SIZE_KEY,
68};
69use crate::path_utils::table_dir;
70use crate::storage::{ColumnId, RegionId};
71
72/// The type of path to generate.
73#[derive(Debug, Clone, Copy, PartialEq, TryFromPrimitive)]
74#[repr(u8)]
75pub enum PathType {
76    /// A bare path - the original path of an engine.
77    ///
78    /// The path prefix is `{table_dir}/{table_id}_{region_sequence}/`.
79    Bare,
80    /// A path for the data region of a metric engine table.
81    ///
82    /// The path prefix is `{table_dir}/{table_id}_{region_sequence}/data/`.
83    Data,
84    /// A path for the metadata region of a metric engine table.
85    ///
86    /// The path prefix is `{table_dir}/{table_id}_{region_sequence}/metadata/`.
87    Metadata,
88}
89
90#[derive(Debug, IntoStaticStr)]
91pub enum BatchRegionDdlRequest {
92    Create(Vec<(RegionId, RegionCreateRequest)>),
93    Drop(Vec<(RegionId, RegionDropRequest)>),
94    Alter(Vec<(RegionId, RegionAlterRequest)>),
95}
96
97impl BatchRegionDdlRequest {
98    /// Converts [Body](region_request::Body) to [`BatchRegionDdlRequest`].
99    pub fn try_from_request_body(body: region_request::Body) -> Result<Option<Self>> {
100        match body {
101            region_request::Body::Creates(creates) => {
102                let requests = creates
103                    .requests
104                    .into_iter()
105                    .map(parse_region_create)
106                    .collect::<Result<Vec<_>>>()?;
107                Ok(Some(Self::Create(requests)))
108            }
109            region_request::Body::Drops(drops) => {
110                let requests = drops
111                    .requests
112                    .into_iter()
113                    .map(parse_region_drop)
114                    .collect::<Result<Vec<_>>>()?;
115                Ok(Some(Self::Drop(requests)))
116            }
117            region_request::Body::Alters(alters) => {
118                let requests = alters
119                    .requests
120                    .into_iter()
121                    .map(parse_region_alter)
122                    .collect::<Result<Vec<_>>>()?;
123                Ok(Some(Self::Alter(requests)))
124            }
125            _ => Ok(None),
126        }
127    }
128
129    pub fn request_type(&self) -> &'static str {
130        self.into()
131    }
132
133    pub fn into_region_requests(self) -> Vec<(RegionId, RegionRequest)> {
134        match self {
135            Self::Create(requests) => requests
136                .into_iter()
137                .map(|(region_id, request)| (region_id, RegionRequest::Create(request)))
138                .collect(),
139            Self::Drop(requests) => requests
140                .into_iter()
141                .map(|(region_id, request)| (region_id, RegionRequest::Drop(request)))
142                .collect(),
143            Self::Alter(requests) => requests
144                .into_iter()
145                .map(|(region_id, request)| (region_id, RegionRequest::Alter(request)))
146                .collect(),
147        }
148    }
149}
150
151#[derive(Debug, IntoStaticStr)]
152pub enum RegionRequest {
153    Put(RegionPutRequest),
154    Delete(RegionDeleteRequest),
155    Create(RegionCreateRequest),
156    Drop(RegionDropRequest),
157    Open(RegionOpenRequest),
158    CleanUp(RegionCleanUpRequest),
159    Close(RegionCloseRequest),
160    Alter(RegionAlterRequest),
161    Flush(RegionFlushRequest),
162    Compact(RegionCompactRequest),
163    BuildIndex(RegionBuildIndexRequest),
164    Truncate(RegionTruncateRequest),
165    Catchup(RegionCatchupRequest),
166    BulkInserts(RegionBulkInsertsRequest),
167    EnterStaging(EnterStagingRequest),
168    ApplyStagingManifest(ApplyStagingManifestRequest),
169}
170
171impl RegionRequest {
172    /// Convert [Body](region_request::Body) to a group of [RegionRequest] with region id.
173    /// Inserts/Deletes request might become multiple requests. Others are one-to-one.
174    pub fn try_from_request_body(body: region_request::Body) -> Result<Vec<(RegionId, Self)>> {
175        match body {
176            region_request::Body::Inserts(inserts) => make_region_puts(inserts),
177            region_request::Body::Deletes(deletes) => make_region_deletes(deletes),
178            region_request::Body::Create(create) => make_region_create(create),
179            region_request::Body::Drop(drop) => make_region_drop(drop),
180            region_request::Body::Open(open) => make_region_open(open),
181            region_request::Body::CleanUp(clean_up) => make_region_clean_up(clean_up),
182            region_request::Body::Close(close) => make_region_close(close),
183            region_request::Body::Alter(alter) => make_region_alter(alter),
184            region_request::Body::Flush(flush) => make_region_flush(flush),
185            region_request::Body::Compact(compact) => make_region_compact(compact),
186            region_request::Body::BuildIndex(index) => make_region_build_index(index),
187            region_request::Body::Truncate(truncate) => make_region_truncate(truncate),
188            region_request::Body::Creates(creates) => make_region_creates(creates),
189            region_request::Body::Drops(drops) => make_region_drops(drops),
190            region_request::Body::Alters(alters) => make_region_alters(alters),
191            region_request::Body::BulkInsert(bulk) => make_region_bulk_inserts(bulk),
192            region_request::Body::Sync(_) => UnexpectedSnafu {
193                reason: "Sync request should be handled separately by RegionServer",
194            }
195            .fail(),
196            region_request::Body::ListMetadata(_) => UnexpectedSnafu {
197                reason: "ListMetadata request should be handled separately by RegionServer",
198            }
199            .fail(),
200            region_request::Body::RemoteDynFilter(_) => UnexpectedSnafu {
201                reason: "RemoteDynFilter request should be handled separately by RegionServer",
202            }
203            .fail(),
204            region_request::Body::ApplyStagingManifest(apply) => {
205                make_region_apply_staging_manifest(apply)
206            }
207        }
208    }
209
210    /// Returns the type name of the request.
211    pub fn request_type(&self) -> &'static str {
212        self.into()
213    }
214}
215
216fn make_region_puts(inserts: InsertRequests) -> Result<Vec<(RegionId, RegionRequest)>> {
217    let requests = inserts
218        .requests
219        .into_iter()
220        .filter_map(|r| {
221            let region_id = r.region_id.into();
222            r.rows.map(|rows| {
223                (
224                    region_id,
225                    RegionRequest::Put(RegionPutRequest {
226                        rows,
227                        hint: None,
228                        skip_wal: r.skip_wal,
229                        partition_expr_version: r.partition_expr_version.map(|v| v.value),
230                    }),
231                )
232            })
233        })
234        .collect();
235    Ok(requests)
236}
237
238fn make_region_deletes(deletes: DeleteRequests) -> Result<Vec<(RegionId, RegionRequest)>> {
239    let requests = deletes
240        .requests
241        .into_iter()
242        .filter_map(|r| {
243            let region_id = r.region_id.into();
244            r.rows.map(|rows| {
245                (
246                    region_id,
247                    RegionRequest::Delete(RegionDeleteRequest {
248                        rows,
249                        hint: None,
250                        partition_expr_version: r.partition_expr_version.map(|v| v.value),
251                    }),
252                )
253            })
254        })
255        .collect();
256    Ok(requests)
257}
258
259fn parse_region_create(create: CreateRequest) -> Result<(RegionId, RegionCreateRequest)> {
260    let column_metadatas = create
261        .column_defs
262        .into_iter()
263        .map(ColumnMetadata::try_from_column_def)
264        .collect::<Result<Vec<_>>>()?;
265    let region_id = RegionId::from(create.region_id);
266    let table_dir = table_dir(&create.path, region_id.table_id());
267    let partition_expr_json = create.partition.as_ref().map(|p| p.expression.clone());
268    Ok((
269        region_id,
270        RegionCreateRequest {
271            engine: create.engine,
272            column_metadatas,
273            primary_key: create.primary_key,
274            options: create.options,
275            table_dir,
276            path_type: PathType::Bare,
277            partition_expr_json,
278            requirements: create
279                .requirements
280                .map(RegionRequirements::from)
281                .unwrap_or_default(),
282        },
283    ))
284}
285
286fn make_region_create(create: CreateRequest) -> Result<Vec<(RegionId, RegionRequest)>> {
287    let (region_id, request) = parse_region_create(create)?;
288    Ok(vec![(region_id, RegionRequest::Create(request))])
289}
290
291fn make_region_creates(creates: CreateRequests) -> Result<Vec<(RegionId, RegionRequest)>> {
292    let mut requests = Vec::with_capacity(creates.requests.len());
293    for create in creates.requests {
294        requests.extend(make_region_create(create)?);
295    }
296    Ok(requests)
297}
298
299fn parse_region_drop(drop: DropRequest) -> Result<(RegionId, RegionDropRequest)> {
300    let region_id = drop.region_id.into();
301    Ok((
302        region_id,
303        RegionDropRequest {
304            fast_path: drop.fast_path,
305            force: drop.force,
306            partial_drop: drop.partial_drop,
307        },
308    ))
309}
310
311fn make_region_drop(drop: DropRequest) -> Result<Vec<(RegionId, RegionRequest)>> {
312    let (region_id, request) = parse_region_drop(drop)?;
313    Ok(vec![(region_id, RegionRequest::Drop(request))])
314}
315
316fn make_region_drops(drops: DropRequests) -> Result<Vec<(RegionId, RegionRequest)>> {
317    let mut requests = Vec::with_capacity(drops.requests.len());
318    for drop in drops.requests {
319        requests.extend(make_region_drop(drop)?);
320    }
321    Ok(requests)
322}
323
324fn make_region_open(open: OpenRequest) -> Result<Vec<(RegionId, RegionRequest)>> {
325    let region_id = RegionId::from(open.region_id);
326    let table_dir = table_dir(&open.path, region_id.table_id());
327    Ok(vec![(
328        region_id,
329        RegionRequest::Open(RegionOpenRequest {
330            engine: open.engine,
331            table_dir,
332            path_type: PathType::Bare,
333            options: open.options,
334            skip_wal_replay: false,
335            checkpoint: None,
336            requirements: Default::default(),
337        }),
338    )])
339}
340
341fn make_region_clean_up(clean_up: PbCleanUpRequest) -> Result<Vec<(RegionId, RegionRequest)>> {
342    let region_id = RegionId::from(clean_up.region_id);
343    let table_dir = table_dir(&clean_up.path, region_id.table_id());
344    Ok(vec![(
345        region_id,
346        RegionRequest::CleanUp(RegionCleanUpRequest {
347            engine: clean_up.engine,
348            table_dir,
349            path_type: PathType::Bare,
350            options: clean_up.options,
351        }),
352    )])
353}
354
355fn make_region_close(close: CloseRequest) -> Result<Vec<(RegionId, RegionRequest)>> {
356    let region_id = close.region_id.into();
357    Ok(vec![(
358        region_id,
359        RegionRequest::Close(RegionCloseRequest {
360            flush_on_close: close.flush_on_close,
361        }),
362    )])
363}
364
365fn parse_region_alter(alter: AlterRequest) -> Result<(RegionId, RegionAlterRequest)> {
366    let region_id = alter.region_id.into();
367    let request = RegionAlterRequest::try_from(alter)?;
368    Ok((region_id, request))
369}
370
371fn make_region_alter(alter: AlterRequest) -> Result<Vec<(RegionId, RegionRequest)>> {
372    let (region_id, request) = parse_region_alter(alter)?;
373    Ok(vec![(region_id, RegionRequest::Alter(request))])
374}
375
376fn make_region_alters(alters: AlterRequests) -> Result<Vec<(RegionId, RegionRequest)>> {
377    let mut requests = Vec::with_capacity(alters.requests.len());
378    for alter in alters.requests {
379        requests.extend(make_region_alter(alter)?);
380    }
381    Ok(requests)
382}
383
384fn make_region_flush(flush: FlushRequest) -> Result<Vec<(RegionId, RegionRequest)>> {
385    let region_id = flush.region_id.into();
386    Ok(vec![(
387        region_id,
388        RegionRequest::Flush(RegionFlushRequest::default()),
389    )])
390}
391
392fn make_region_compact(compact: CompactRequest) -> Result<Vec<(RegionId, RegionRequest)>> {
393    let region_id = compact.region_id.into();
394    let options = compact
395        .options
396        .unwrap_or(compact_request::Options::Regular(Default::default()));
397    // Convert parallelism: a value of 0 indicates no specific parallelism requested (None)
398    let parallelism = if compact.parallelism == 0 {
399        None
400    } else {
401        Some(compact.parallelism)
402    };
403    let time_range = compact
404        .time_range
405        .map(|range| {
406            let time_unit = v1::TimeUnit::try_from(range.time_unit).map_err(|_| {
407                InvalidRegionRequestSnafu {
408                    region_id,
409                    err: format!("invalid compaction time unit: {}", range.time_unit),
410                }
411                .build()
412            })?;
413            let time_unit = from_pb_time_unit(time_unit);
414            let start = Timestamp::new(range.start, time_unit);
415            let end = Timestamp::new(range.end, time_unit);
416            ensure!(
417                start < end,
418                InvalidRegionRequestSnafu {
419                    region_id,
420                    err: format!(
421                        "compaction start time must be earlier than end time: {} >= {}",
422                        range.start, range.end
423                    ),
424                }
425            );
426            TimestampRange::new(start, end).with_context(|| InvalidRegionRequestSnafu {
427                region_id,
428                err: "invalid compaction time range".to_string(),
429            })
430        })
431        .transpose()?;
432    Ok(vec![(
433        region_id,
434        RegionRequest::Compact(RegionCompactRequest {
435            options,
436            parallelism,
437            time_range,
438        }),
439    )])
440}
441
442fn make_region_build_index(index: BuildIndexRequest) -> Result<Vec<(RegionId, RegionRequest)>> {
443    let region_id = index.region_id.into();
444    Ok(vec![(
445        region_id,
446        RegionRequest::BuildIndex(RegionBuildIndexRequest {
447            options: index.options,
448        }),
449    )])
450}
451
452fn make_region_truncate(truncate: TruncateRequest) -> Result<Vec<(RegionId, RegionRequest)>> {
453    let region_id = truncate.region_id.into();
454    match truncate.kind {
455        None => InvalidRawRegionRequestSnafu {
456            err: "missing kind in TruncateRequest".to_string(),
457        }
458        .fail(),
459        Some(truncate_request::Kind::All(_)) => Ok(vec![(
460            region_id,
461            RegionRequest::Truncate(RegionTruncateRequest::All),
462        )]),
463        Some(truncate_request::Kind::TimeRanges(time_ranges)) => {
464            let time_ranges = from_pb_time_ranges(time_ranges).context(ConvertTimeRangesSnafu)?;
465
466            Ok(vec![(
467                region_id,
468                RegionRequest::Truncate(RegionTruncateRequest::ByTimeRanges { time_ranges }),
469            )])
470        }
471        Some(truncate_request::Kind::Unflushed(_)) => Ok(vec![(
472            region_id,
473            RegionRequest::Truncate(RegionTruncateRequest::Unflushed),
474        )]),
475    }
476}
477
478/// Convert [BulkInsertRequest] to [RegionRequest] and group by [RegionId].
479fn make_region_bulk_inserts(request: BulkInsertRequest) -> Result<Vec<(RegionId, RegionRequest)>> {
480    let region_id = request.region_id.into();
481    let skip_wal = request.skip_wal;
482    let partition_expr_version = request.partition_expr_version.map(|v| v.value);
483    let aligned_schema_version = request.aligned_schema_version.map(|v| v.schema_version);
484    let Some(Body::ArrowIpc(request)) = request.body else {
485        return Ok(vec![]);
486    };
487
488    let decoder_timer = metrics::CONVERT_REGION_BULK_REQUEST
489        .with_label_values(&["decode"])
490        .start_timer();
491    let mut decoder =
492        FlightDecoder::try_from_schema_bytes(&request.schema).context(FlightCodecSnafu)?;
493    let payload = decoder
494        .try_decode_record_batch(&request.data_header, &request.payload)
495        .context(FlightCodecSnafu)?;
496    decoder_timer.observe_duration();
497    Ok(vec![(
498        region_id,
499        RegionRequest::BulkInserts(RegionBulkInsertsRequest {
500            region_id,
501            payload,
502            raw_data: request,
503            skip_wal,
504            partition_expr_version,
505            aligned_schema_version,
506        }),
507    )])
508}
509
510fn make_region_apply_staging_manifest(
511    api::v1::region::ApplyStagingManifestRequest {
512        region_id,
513        partition_expr,
514        central_region_id,
515        manifest_path,
516    }: api::v1::region::ApplyStagingManifestRequest,
517) -> Result<Vec<(RegionId, RegionRequest)>> {
518    let region_id = region_id.into();
519    Ok(vec![(
520        region_id,
521        RegionRequest::ApplyStagingManifest(ApplyStagingManifestRequest {
522            partition_expr,
523            central_region_id: central_region_id.into(),
524            manifest_path,
525        }),
526    )])
527}
528
529/// Request to put data into a region.
530#[derive(Debug)]
531pub struct RegionPutRequest {
532    /// Rows to put.
533    pub rows: Rows,
534    /// Write hint.
535    pub hint: Option<WriteHint>,
536    /// Skip WAL for this insert without changing region options.
537    /// Metadata writes must not inherit this option from user inserts.
538    pub skip_wal: bool,
539    /// Partition expression version for the region.
540    pub partition_expr_version: Option<u64>,
541}
542
543/// Request to delete data from a region.
544#[derive(Debug)]
545pub struct RegionDeleteRequest {
546    /// Keys to rows to delete.
547    ///
548    /// Each row only contains primary key columns and a time index column.
549    pub rows: Rows,
550    /// Write hint.
551    pub hint: Option<WriteHint>,
552    /// Partition expression version for the region.
553    pub partition_expr_version: Option<u64>,
554}
555
556#[derive(Debug, Clone)]
557pub struct RegionCreateRequest {
558    /// Region engine name
559    pub engine: String,
560    /// Columns in this region.
561    pub column_metadatas: Vec<ColumnMetadata>,
562    /// Columns in the primary key.
563    pub primary_key: Vec<ColumnId>,
564    /// Options of the created region.
565    pub options: HashMap<String, String>,
566    /// Directory for table's data home. Usually is composed by catalog and table id
567    pub table_dir: String,
568    /// Path type for generating paths
569    pub path_type: PathType,
570    /// Partition expression JSON from table metadata. Set to empty string for a region without partition.
571    /// `Option` to keep compatibility with old clients.
572    pub partition_expr_json: Option<String>,
573    /// Requirements for creating the region.
574    pub requirements: RegionRequirements,
575}
576
577impl RegionCreateRequest {
578    /// Checks whether the request is valid, returns an error if it is invalid.
579    pub fn validate(&self) -> Result<()> {
580        // time index must exist
581        ensure!(
582            self.column_metadatas
583                .iter()
584                .any(|x| x.semantic_type == SemanticType::Timestamp),
585            InvalidRegionRequestSnafu {
586                region_id: RegionId::new(0, 0),
587                err: "missing timestamp column in create region request".to_string(),
588            }
589        );
590
591        // build column id to indices
592        let mut column_id_to_indices = HashMap::with_capacity(self.column_metadatas.len());
593        for (i, c) in self.column_metadatas.iter().enumerate() {
594            if let Some(previous) = column_id_to_indices.insert(c.column_id, i) {
595                return InvalidRegionRequestSnafu {
596                    region_id: RegionId::new(0, 0),
597                    err: format!(
598                        "duplicate column id {} (at position {} and {}) in create region request",
599                        c.column_id, previous, i
600                    ),
601                }
602                .fail();
603            }
604        }
605
606        // primary key must exist
607        for column_id in &self.primary_key {
608            ensure!(
609                column_id_to_indices.contains_key(column_id),
610                InvalidRegionRequestSnafu {
611                    region_id: RegionId::new(0, 0),
612                    err: format!(
613                        "missing primary key column {} in create region request",
614                        column_id
615                    ),
616                }
617            );
618        }
619
620        Ok(())
621    }
622
623    /// Returns true when the region belongs to the metric engine's physical table.
624    pub fn is_physical_table(&self) -> bool {
625        self.options.contains_key(PHYSICAL_TABLE_METADATA_KEY)
626    }
627}
628
629#[derive(Debug, Clone)]
630pub struct RegionDropRequest {
631    /// Enables fast-path drop optimizations for logical regions.
632    /// Only applicable to the Metric Engine; ignored by others.
633    pub fast_path: bool,
634
635    /// Forces the drop of a physical region and all its associated logical regions.
636    /// Only relevant for physical regions managed by the Metric Engine.
637    pub force: bool,
638
639    /// If true, indicates that only a portion of the region is being dropped, and files may still be referenced by other regions.
640    /// This is used to prevent deletion of files that are still in use by other regions.
641    pub partial_drop: bool,
642}
643
644/// Requirements for a region request.
645#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
646#[serde(default)]
647pub struct RegionRequirements {
648    /// Whether the region data must be backed by object storage.
649    pub object_storage: bool,
650}
651
652impl RegionRequirements {
653    /// Returns empty requirements.
654    pub fn empty() -> Self {
655        Self::default()
656    }
657
658    /// Returns requirements for object storage.
659    pub fn object_storage() -> Self {
660        Self {
661            object_storage: true,
662        }
663    }
664}
665
666impl From<api::v1::region::RegionRequirements> for RegionRequirements {
667    fn from(value: api::v1::region::RegionRequirements) -> Self {
668        Self {
669            object_storage: value.object_storage,
670        }
671    }
672}
673
674impl From<RegionRequirements> for api::v1::region::RegionRequirements {
675    fn from(value: RegionRequirements) -> Self {
676        Self {
677            object_storage: value.object_storage,
678        }
679    }
680}
681
682/// Open region request.
683#[derive(Debug, Clone)]
684pub struct RegionOpenRequest {
685    /// Region engine name
686    pub engine: String,
687    /// Directory for table's data home. Usually is composed by catalog and table id
688    pub table_dir: String,
689    /// Path type for generating paths
690    pub path_type: PathType,
691    /// Options of the opened region.
692    pub options: HashMap<String, String>,
693    /// To skip replaying the WAL.
694    pub skip_wal_replay: bool,
695    /// Replay checkpoint.
696    pub checkpoint: Option<ReplayCheckpoint>,
697    /// Requirements for opening the region.
698    pub requirements: RegionRequirements,
699}
700
701#[derive(Debug, Clone, Copy, PartialEq, Eq)]
702pub struct ReplayCheckpoint {
703    pub entry_id: u64,
704    pub metadata_entry_id: Option<u64>,
705}
706
707impl RegionOpenRequest {
708    /// Returns true when the region belongs to the metric engine's physical table.
709    pub fn is_physical_table(&self) -> bool {
710        self.options.contains_key(PHYSICAL_TABLE_METADATA_KEY)
711    }
712}
713
714/// Offline region cleanup request.
715#[derive(Debug, Clone)]
716pub struct RegionCleanUpRequest {
717    /// Region engine name
718    pub engine: String,
719    /// Directory for table's data home. Usually is composed by catalog and table id
720    pub table_dir: String,
721    /// Path type for generating paths
722    pub path_type: PathType,
723    /// Options of the cleaned region.
724    pub options: HashMap<String, String>,
725}
726
727impl RegionCleanUpRequest {
728    /// Returns true when the region belongs to the metric engine's physical table.
729    pub fn is_physical_table(&self) -> bool {
730        self.options.contains_key(PHYSICAL_TABLE_METADATA_KEY)
731    }
732}
733
734/// Close region request.
735#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
736pub struct RegionCloseRequest {
737    /// Whether to flush the region before closing it.
738    pub flush_on_close: bool,
739}
740
741/// Alter metadata of a region.
742#[derive(Debug, PartialEq, Eq, Clone)]
743pub struct RegionAlterRequest {
744    /// Kind of alteration to do.
745    pub kind: AlterKind,
746}
747
748impl RegionAlterRequest {
749    /// Checks whether the request is valid, returns an error if it is invalid.
750    pub fn validate(&self, metadata: &RegionMetadata) -> Result<()> {
751        self.kind.validate(metadata)?;
752
753        Ok(())
754    }
755
756    /// Returns true if we need to apply the request to the region.
757    ///
758    /// The `request` should be valid.
759    pub fn need_alter(&self, metadata: &RegionMetadata) -> bool {
760        debug_assert!(self.validate(metadata).is_ok());
761        self.kind.need_alter(metadata)
762    }
763}
764
765impl TryFrom<AlterRequest> for RegionAlterRequest {
766    type Error = MetadataError;
767
768    fn try_from(value: AlterRequest) -> Result<Self> {
769        let kind = value.kind.context(InvalidRawRegionRequestSnafu {
770            err: "missing kind in AlterRequest",
771        })?;
772
773        let kind = AlterKind::try_from(kind)?;
774        Ok(RegionAlterRequest { kind })
775    }
776}
777
778/// Kind of the alteration.
779#[derive(Debug, PartialEq, Eq, Clone, AsRefStr)]
780pub enum AlterKind {
781    /// Add columns to the region.
782    AddColumns {
783        /// Columns to add.
784        columns: Vec<AddColumn>,
785    },
786    /// Drop columns from the region, only fields are allowed to drop.
787    DropColumns {
788        /// Name of columns to drop.
789        names: Vec<String>,
790    },
791    /// Change columns datatype of the region. Field columns can change to any
792    /// Arrow-castable type; the time index column only supports widening its
793    /// timestamp unit (e.g. `TimestampMillisecond -> TimestampMicrosecond`),
794    /// which is lossless for values that fit the target unit's `i64` range.
795    ModifyColumnTypes {
796        /// Columns to change.
797        columns: Vec<ModifyColumnType>,
798    },
799    /// Set JSON2 settings of a region column.
800    SetJsonSettings {
801        /// Column name.
802        column_name: String,
803        /// Target JSON2 settings.
804        settings: JsonSettings,
805    },
806    /// Set region options.
807    SetRegionOptions { options: Vec<SetRegionOption> },
808    /// Unset region options.
809    UnsetRegionOptions { keys: Vec<UnsetRegionOption> },
810    /// Set index options.
811    SetIndexes { options: Vec<SetIndexOption> },
812    /// Unset index options.
813    UnsetIndexes { options: Vec<UnsetIndexOption> },
814    /// Drop column default value.
815    DropDefaults {
816        /// Name of columns to drop.
817        names: Vec<String>,
818    },
819    /// Set column default value.
820    SetDefaults {
821        /// Columns to change.
822        columns: Vec<SetDefault>,
823    },
824    /// Sync column metadatas.
825    SyncColumns {
826        column_metadatas: Vec<ColumnMetadata>,
827    },
828}
829#[derive(Debug, PartialEq, Eq, Clone)]
830pub struct SetDefault {
831    pub name: String,
832    pub default_constraint: Vec<u8>,
833}
834
835#[derive(Debug, PartialEq, Eq, Clone)]
836pub enum SetIndexOption {
837    Fulltext {
838        column_name: String,
839        options: FulltextOptions,
840    },
841    Inverted {
842        column_name: String,
843    },
844    Skipping {
845        column_name: String,
846        options: SkippingIndexOptions,
847    },
848}
849
850impl SetIndexOption {
851    /// Returns the column name of the index option.
852    pub fn column_name(&self) -> &String {
853        match self {
854            SetIndexOption::Fulltext { column_name, .. } => column_name,
855            SetIndexOption::Inverted { column_name } => column_name,
856            SetIndexOption::Skipping { column_name, .. } => column_name,
857        }
858    }
859
860    /// Returns true if the index option is fulltext.
861    pub fn is_fulltext(&self) -> bool {
862        match self {
863            SetIndexOption::Fulltext { .. } => true,
864            SetIndexOption::Inverted { .. } => false,
865            SetIndexOption::Skipping { .. } => false,
866        }
867    }
868}
869
870impl TryFrom<v1::SetIndex> for SetIndexOption {
871    type Error = MetadataError;
872
873    fn try_from(value: v1::SetIndex) -> Result<Self> {
874        let option = value.options.context(InvalidRawRegionRequestSnafu {
875            err: "missing options in SetIndex",
876        })?;
877
878        let opt = match option {
879            v1::set_index::Options::Fulltext(x) => SetIndexOption::Fulltext {
880                column_name: x.column_name.clone(),
881                options: FulltextOptions::new(
882                    x.enable,
883                    as_fulltext_option_analyzer(
884                        Analyzer::try_from(x.analyzer).context(DecodeProtoSnafu)?,
885                    ),
886                    x.case_sensitive,
887                    as_fulltext_option_backend(
888                        PbFulltextBackend::try_from(x.backend).context(DecodeProtoSnafu)?,
889                    ),
890                    x.granularity as u32,
891                    x.false_positive_rate,
892                )
893                .context(InvalidIndexOptionSnafu)?,
894            },
895            v1::set_index::Options::Inverted(i) => SetIndexOption::Inverted {
896                column_name: i.column_name,
897            },
898            v1::set_index::Options::Skipping(s) => SetIndexOption::Skipping {
899                column_name: s.column_name,
900                options: SkippingIndexOptions::new(
901                    s.granularity as u32,
902                    s.false_positive_rate,
903                    as_skipping_index_type(
904                        PbSkippingIndexType::try_from(s.skipping_index_type)
905                            .context(DecodeProtoSnafu)?,
906                    ),
907                )
908                .context(InvalidIndexOptionSnafu)?,
909            },
910        };
911
912        Ok(opt)
913    }
914}
915
916#[derive(Debug, PartialEq, Eq, Clone)]
917pub enum UnsetIndexOption {
918    Fulltext { column_name: String },
919    Inverted { column_name: String },
920    Skipping { column_name: String },
921}
922
923impl UnsetIndexOption {
924    pub fn column_name(&self) -> &String {
925        match self {
926            UnsetIndexOption::Fulltext { column_name } => column_name,
927            UnsetIndexOption::Inverted { column_name } => column_name,
928            UnsetIndexOption::Skipping { column_name } => column_name,
929        }
930    }
931
932    pub fn is_fulltext(&self) -> bool {
933        match self {
934            UnsetIndexOption::Fulltext { .. } => true,
935            UnsetIndexOption::Inverted { .. } => false,
936            UnsetIndexOption::Skipping { .. } => false,
937        }
938    }
939}
940
941impl TryFrom<v1::UnsetIndex> for UnsetIndexOption {
942    type Error = MetadataError;
943
944    fn try_from(value: v1::UnsetIndex) -> Result<Self> {
945        let option = value.options.context(InvalidRawRegionRequestSnafu {
946            err: "missing options in UnsetIndex",
947        })?;
948
949        let opt = match option {
950            v1::unset_index::Options::Fulltext(f) => UnsetIndexOption::Fulltext {
951                column_name: f.column_name,
952            },
953            v1::unset_index::Options::Inverted(i) => UnsetIndexOption::Inverted {
954                column_name: i.column_name,
955            },
956            v1::unset_index::Options::Skipping(s) => UnsetIndexOption::Skipping {
957                column_name: s.column_name,
958            },
959        };
960
961        Ok(opt)
962    }
963}
964
965impl AlterKind {
966    /// Returns an error if the alter kind is invalid.
967    ///
968    /// It allows adding column if not exists and dropping column if exists.
969    pub fn validate(&self, metadata: &RegionMetadata) -> Result<()> {
970        match self {
971            AlterKind::AddColumns { columns } => {
972                for col_to_add in columns {
973                    col_to_add.validate(metadata)?;
974                }
975            }
976            AlterKind::DropColumns { names } => {
977                for name in names {
978                    Self::validate_column_to_drop(name, metadata)?;
979                }
980            }
981            AlterKind::ModifyColumnTypes { columns } => {
982                for col_to_change in columns {
983                    col_to_change.validate(metadata)?;
984                }
985            }
986            AlterKind::SetJsonSettings { column_name, .. } => {
987                Self::validate_set_json_settings(column_name, metadata)?
988            }
989            AlterKind::SetRegionOptions { .. } => {}
990            AlterKind::UnsetRegionOptions { .. } => {}
991            AlterKind::SetIndexes { options } => {
992                for option in options {
993                    Self::validate_column_alter_index_option(
994                        option.column_name(),
995                        metadata,
996                        option.is_fulltext(),
997                    )?;
998                }
999            }
1000            AlterKind::UnsetIndexes { options } => {
1001                for option in options {
1002                    Self::validate_column_alter_index_option(
1003                        option.column_name(),
1004                        metadata,
1005                        option.is_fulltext(),
1006                    )?;
1007                }
1008            }
1009            AlterKind::DropDefaults { names } => {
1010                names
1011                    .iter()
1012                    .try_for_each(|name| Self::validate_column_existence(name, metadata))?;
1013            }
1014            AlterKind::SetDefaults { columns } => {
1015                columns
1016                    .iter()
1017                    .try_for_each(|col| Self::validate_column_existence(&col.name, metadata))?;
1018            }
1019            AlterKind::SyncColumns { column_metadatas } => {
1020                let new_primary_keys = column_metadatas
1021                    .iter()
1022                    .filter(|c| c.semantic_type == SemanticType::Tag)
1023                    .map(|c| (c.column_schema.name.as_str(), c.column_id))
1024                    .collect::<HashMap<_, _>>();
1025
1026                let old_primary_keys = metadata
1027                    .column_metadatas
1028                    .iter()
1029                    .filter(|c| c.semantic_type == SemanticType::Tag)
1030                    .map(|c| (c.column_schema.name.as_str(), c.column_id));
1031
1032                for (name, id) in old_primary_keys {
1033                    let primary_key =
1034                        new_primary_keys
1035                            .get(name)
1036                            .with_context(|| InvalidRegionRequestSnafu {
1037                                region_id: metadata.region_id,
1038                                err: format!("column {} is not a primary key", name),
1039                            })?;
1040
1041                    ensure!(
1042                        *primary_key == id,
1043                        InvalidRegionRequestSnafu {
1044                            region_id: metadata.region_id,
1045                            err: format!(
1046                                "column with same name {} has different id, existing: {}, got: {}",
1047                                name, id, primary_key
1048                            ),
1049                        }
1050                    );
1051                }
1052
1053                let new_ts_column = column_metadatas
1054                    .iter()
1055                    .find(|c| c.semantic_type == SemanticType::Timestamp)
1056                    .map(|c| (c.column_schema.name.as_str(), c.column_id))
1057                    .context(InvalidRegionRequestSnafu {
1058                        region_id: metadata.region_id,
1059                        err: "timestamp column not found",
1060                    })?;
1061
1062                // Safety: timestamp column must exist.
1063                let old_ts_column = metadata
1064                    .column_metadatas
1065                    .iter()
1066                    .find(|c| c.semantic_type == SemanticType::Timestamp)
1067                    .map(|c| (c.column_schema.name.as_str(), c.column_id))
1068                    .unwrap();
1069
1070                ensure!(
1071                    new_ts_column == old_ts_column,
1072                    InvalidRegionRequestSnafu {
1073                        region_id: metadata.region_id,
1074                        err: format!(
1075                            "timestamp column {} has different id, existing: {}, got: {}",
1076                            old_ts_column.0, old_ts_column.1, new_ts_column.1
1077                        ),
1078                    }
1079                );
1080            }
1081        }
1082        Ok(())
1083    }
1084
1085    /// Returns true if we need to apply the alteration to the region.
1086    pub fn need_alter(&self, metadata: &RegionMetadata) -> bool {
1087        debug_assert!(self.validate(metadata).is_ok());
1088        match self {
1089            AlterKind::AddColumns { columns } => columns
1090                .iter()
1091                .any(|col_to_add| col_to_add.need_alter(metadata)),
1092            AlterKind::DropColumns { names } => names
1093                .iter()
1094                .any(|name| metadata.column_by_name(name).is_some()),
1095            AlterKind::ModifyColumnTypes { columns } => columns
1096                .iter()
1097                .any(|col_to_change| col_to_change.need_alter(metadata)),
1098            AlterKind::SetJsonSettings {
1099                column_name,
1100                settings,
1101            } => metadata.column_by_name(column_name).is_some_and(|col| {
1102                col.column_schema
1103                    .extension_type::<Json2ExtensionType>()
1104                    .ok()
1105                    .flatten()
1106                    .is_none_or(|extension| {
1107                        !extension.metadata().json_settings().equivalent(settings)
1108                    })
1109            }),
1110            AlterKind::SetRegionOptions { .. } => true,
1111            AlterKind::UnsetRegionOptions { .. } => true,
1112            AlterKind::SetIndexes { options, .. } => options
1113                .iter()
1114                .any(|option| metadata.column_by_name(option.column_name()).is_some()),
1115            AlterKind::UnsetIndexes { options } => options
1116                .iter()
1117                .any(|option| metadata.column_by_name(option.column_name()).is_some()),
1118            AlterKind::DropDefaults { names } => names
1119                .iter()
1120                .any(|name| metadata.column_by_name(name).is_some()),
1121
1122            AlterKind::SetDefaults { columns } => columns
1123                .iter()
1124                .any(|x| metadata.column_by_name(&x.name).is_some()),
1125            AlterKind::SyncColumns { column_metadatas } => {
1126                metadata.column_metadatas != *column_metadatas
1127            }
1128        }
1129    }
1130
1131    /// Returns an error if the column to drop is invalid.
1132    fn validate_column_to_drop(name: &str, metadata: &RegionMetadata) -> Result<()> {
1133        let Some(column) = metadata.column_by_name(name) else {
1134            return Ok(());
1135        };
1136        ensure!(
1137            column.semantic_type == SemanticType::Field,
1138            InvalidRegionRequestSnafu {
1139                region_id: metadata.region_id,
1140                err: format!("column {} is not a field and could not be dropped", name),
1141            }
1142        );
1143        Ok(())
1144    }
1145
1146    /// Returns an error if the column's alter index option is invalid.
1147    fn validate_column_alter_index_option(
1148        column_name: &String,
1149        metadata: &RegionMetadata,
1150        is_fulltext: bool,
1151    ) -> Result<()> {
1152        let column = metadata
1153            .column_by_name(column_name)
1154            .context(InvalidRegionRequestSnafu {
1155                region_id: metadata.region_id,
1156                err: format!("column {} not found", column_name),
1157            })?;
1158
1159        if is_fulltext {
1160            ensure!(
1161                column.column_schema.data_type.is_string(),
1162                InvalidRegionRequestSnafu {
1163                    region_id: metadata.region_id,
1164                    err: format!(
1165                        "cannot change alter index options for non-string column {}",
1166                        column_name
1167                    ),
1168                }
1169            );
1170        }
1171
1172        Ok(())
1173    }
1174
1175    /// Returns an error if the column isn't exist.
1176    fn validate_column_existence(column_name: &String, metadata: &RegionMetadata) -> Result<()> {
1177        metadata
1178            .column_by_name(column_name)
1179            .context(InvalidRegionRequestSnafu {
1180                region_id: metadata.region_id,
1181                err: format!("column {} not found", column_name),
1182            })?;
1183
1184        Ok(())
1185    }
1186
1187    fn validate_set_json_settings(col_name: &String, metadata: &RegionMetadata) -> Result<()> {
1188        let region_id = metadata.region_id;
1189
1190        let col = metadata
1191            .column_by_name(col_name)
1192            .with_context(|| InvalidRegionRequestSnafu {
1193                region_id,
1194                err: format!("column {} not found", col_name),
1195            })?;
1196
1197        ensure!(
1198            col.semantic_type == SemanticType::Field,
1199            InvalidRegionRequestSnafu {
1200                region_id,
1201                err: format!("column {} is not a field column", col_name),
1202            }
1203        );
1204        ensure!(
1205            col.column_schema.data_type.is_json2(),
1206            InvalidRegionRequestSnafu {
1207                region_id,
1208                err: format!("column {} is not a JSON2 column", col_name),
1209            }
1210        );
1211
1212        Ok(())
1213    }
1214}
1215
1216impl TryFrom<alter_request::Kind> for AlterKind {
1217    type Error = MetadataError;
1218
1219    fn try_from(kind: alter_request::Kind) -> Result<Self> {
1220        let alter_kind = match kind {
1221            alter_request::Kind::AddColumns(x) => {
1222                let columns = x
1223                    .add_columns
1224                    .into_iter()
1225                    .map(|x| x.try_into())
1226                    .collect::<Result<Vec<_>>>()?;
1227                AlterKind::AddColumns { columns }
1228            }
1229            alter_request::Kind::ModifyColumnTypes(x) => {
1230                let columns = x
1231                    .modify_column_types
1232                    .into_iter()
1233                    .map(|x| x.into())
1234                    .collect::<Vec<_>>();
1235                AlterKind::ModifyColumnTypes { columns }
1236            }
1237            alter_request::Kind::SetJsonSettings(x) => {
1238                let settings = x.settings.context(InvalidRawRegionRequestSnafu {
1239                    err: "missing settings in SetJsonSettings",
1240                })?;
1241                AlterKind::SetJsonSettings {
1242                    column_name: x.column_name,
1243                    settings: json_settings_from_proto(settings)?,
1244                }
1245            }
1246            alter_request::Kind::DropColumns(x) => {
1247                let names = x.drop_columns.into_iter().map(|x| x.name).collect();
1248                AlterKind::DropColumns { names }
1249            }
1250            alter_request::Kind::SetTableOptions(options) => AlterKind::SetRegionOptions {
1251                options: options
1252                    .table_options
1253                    .iter()
1254                    .map(TryFrom::try_from)
1255                    .collect::<Result<Vec<_>>>()?,
1256            },
1257            alter_request::Kind::UnsetTableOptions(options) => AlterKind::UnsetRegionOptions {
1258                keys: options
1259                    .keys
1260                    .iter()
1261                    .map(|key| UnsetRegionOption::try_from(key.as_str()))
1262                    .collect::<Result<Vec<_>>>()?,
1263            },
1264            alter_request::Kind::SetIndex(o) => AlterKind::SetIndexes {
1265                options: vec![SetIndexOption::try_from(o)?],
1266            },
1267            alter_request::Kind::UnsetIndex(o) => AlterKind::UnsetIndexes {
1268                options: vec![UnsetIndexOption::try_from(o)?],
1269            },
1270            alter_request::Kind::SetIndexes(o) => AlterKind::SetIndexes {
1271                options: o
1272                    .set_indexes
1273                    .into_iter()
1274                    .map(SetIndexOption::try_from)
1275                    .collect::<Result<Vec<_>>>()?,
1276            },
1277            alter_request::Kind::UnsetIndexes(o) => AlterKind::UnsetIndexes {
1278                options: o
1279                    .unset_indexes
1280                    .into_iter()
1281                    .map(UnsetIndexOption::try_from)
1282                    .collect::<Result<Vec<_>>>()?,
1283            },
1284            alter_request::Kind::DropDefaults(x) => AlterKind::DropDefaults {
1285                names: x.drop_defaults.into_iter().map(|x| x.column_name).collect(),
1286            },
1287            alter_request::Kind::SetDefaults(x) => AlterKind::SetDefaults {
1288                columns: x
1289                    .set_defaults
1290                    .into_iter()
1291                    .map(|x| {
1292                        Ok(SetDefault {
1293                            name: x.column_name,
1294                            default_constraint: x.default_constraint.clone(),
1295                        })
1296                    })
1297                    .collect::<Result<Vec<_>>>()?,
1298            },
1299            alter_request::Kind::SyncColumns(x) => AlterKind::SyncColumns {
1300                column_metadatas: x
1301                    .column_defs
1302                    .into_iter()
1303                    .map(ColumnMetadata::try_from_column_def)
1304                    .collect::<Result<Vec<_>>>()?,
1305            },
1306        };
1307
1308        Ok(alter_kind)
1309    }
1310}
1311
1312/// Adds a column.
1313#[derive(Debug, PartialEq, Eq, Clone)]
1314pub struct AddColumn {
1315    /// Metadata of the column to add.
1316    pub column_metadata: ColumnMetadata,
1317    /// Location to add the column. If location is None, the region adds
1318    /// the column to the last.
1319    pub location: Option<AddColumnLocation>,
1320}
1321
1322impl AddColumn {
1323    /// Returns an error if the column to add is invalid.
1324    ///
1325    /// It allows adding existing columns. However, the existing column must have the same metadata
1326    /// and the location must be None.
1327    pub fn validate(&self, metadata: &RegionMetadata) -> Result<()> {
1328        ensure!(
1329            self.column_metadata.column_schema.is_nullable()
1330                || self
1331                    .column_metadata
1332                    .column_schema
1333                    .default_constraint()
1334                    .is_some(),
1335            InvalidRegionRequestSnafu {
1336                region_id: metadata.region_id,
1337                err: format!(
1338                    "no default value for column {}",
1339                    self.column_metadata.column_schema.name
1340                ),
1341            }
1342        );
1343
1344        if let Some(existing_column) =
1345            metadata.column_by_name(&self.column_metadata.column_schema.name)
1346        {
1347            // If the column already exists.
1348            ensure!(
1349                *existing_column == self.column_metadata,
1350                InvalidRegionRequestSnafu {
1351                    region_id: metadata.region_id,
1352                    err: format!(
1353                        "column {} already exists with different metadata, existing: {:?}, got: {:?}",
1354                        self.column_metadata.column_schema.name,
1355                        existing_column,
1356                        self.column_metadata,
1357                    ),
1358                }
1359            );
1360            ensure!(
1361                self.location.is_none(),
1362                InvalidRegionRequestSnafu {
1363                    region_id: metadata.region_id,
1364                    err: format!(
1365                        "column {} already exists, but location is specified",
1366                        self.column_metadata.column_schema.name
1367                    ),
1368                }
1369            );
1370        }
1371
1372        if let Some(existing_column) = metadata.column_by_id(self.column_metadata.column_id) {
1373            // Ensures the existing column has the same name.
1374            ensure!(
1375                existing_column.column_schema.name == self.column_metadata.column_schema.name,
1376                InvalidRegionRequestSnafu {
1377                    region_id: metadata.region_id,
1378                    err: format!(
1379                        "column id {} already exists with different name {}",
1380                        self.column_metadata.column_id, existing_column.column_schema.name
1381                    ),
1382                }
1383            );
1384        }
1385
1386        Ok(())
1387    }
1388
1389    /// Returns true if no column to add to the region.
1390    pub fn need_alter(&self, metadata: &RegionMetadata) -> bool {
1391        debug_assert!(self.validate(metadata).is_ok());
1392        metadata
1393            .column_by_name(&self.column_metadata.column_schema.name)
1394            .is_none()
1395    }
1396}
1397
1398impl TryFrom<v1::region::AddColumn> for AddColumn {
1399    type Error = MetadataError;
1400
1401    fn try_from(add_column: v1::region::AddColumn) -> Result<Self> {
1402        let column_def = add_column
1403            .column_def
1404            .context(InvalidRawRegionRequestSnafu {
1405                err: "missing column_def in AddColumn",
1406            })?;
1407
1408        let column_metadata = ColumnMetadata::try_from_column_def(column_def)?;
1409        let location = add_column
1410            .location
1411            .map(AddColumnLocation::try_from)
1412            .transpose()?;
1413
1414        Ok(AddColumn {
1415            column_metadata,
1416            location,
1417        })
1418    }
1419}
1420
1421/// Location to add a column.
1422#[derive(Debug, PartialEq, Eq, Clone)]
1423pub enum AddColumnLocation {
1424    /// Add the column to the first position of columns.
1425    First,
1426    /// Add the column after specific column.
1427    After {
1428        /// Add the column after this column.
1429        column_name: String,
1430    },
1431}
1432
1433impl TryFrom<v1::AddColumnLocation> for AddColumnLocation {
1434    type Error = MetadataError;
1435
1436    fn try_from(location: v1::AddColumnLocation) -> Result<Self> {
1437        let location_type = LocationType::try_from(location.location_type)
1438            .map_err(|e| InvalidRawRegionRequestSnafu { err: e.to_string() }.build())?;
1439        let add_column_location = match location_type {
1440            LocationType::First => AddColumnLocation::First,
1441            LocationType::After => AddColumnLocation::After {
1442                column_name: location.after_column_name,
1443            },
1444        };
1445
1446        Ok(add_column_location)
1447    }
1448}
1449
1450/// Change a column's datatype.
1451#[derive(Debug, PartialEq, Eq, Clone)]
1452pub struct ModifyColumnType {
1453    /// Schema of the column to modify.
1454    pub column_name: String,
1455    /// Column will be changed to this type.
1456    pub target_type: ConcreteDataType,
1457}
1458
1459impl ModifyColumnType {
1460    /// Returns an error if the column's datatype to change is invalid.
1461    pub fn validate(&self, metadata: &RegionMetadata) -> Result<()> {
1462        let column_meta = metadata
1463            .column_by_name(&self.column_name)
1464            .with_context(|| InvalidRegionRequestSnafu {
1465                region_id: metadata.region_id,
1466                err: format!("column {} not found", self.column_name),
1467            })?;
1468
1469        match column_meta.semantic_type {
1470            SemanticType::Field => {
1471                ensure!(
1472                    column_meta
1473                        .column_schema
1474                        .data_type
1475                        .can_arrow_type_cast_to(&self.target_type),
1476                    InvalidRegionRequestSnafu {
1477                        region_id: metadata.region_id,
1478                        err: format!(
1479                            "column '{}' cannot be cast automatically to type '{}'",
1480                            self.column_name, self.target_type
1481                        ),
1482                    }
1483                );
1484            }
1485            // The time index column only supports widening its timestamp
1486            // unit; historical SST data is cast to the new unit on read.
1487            // A same-type change validates so a retried alter procedure is a
1488            // no-op instead of failing the retry forever.
1489            SemanticType::Timestamp => {
1490                ensure!(
1491                    column_meta.column_schema.data_type == self.target_type
1492                        || column_meta
1493                            .column_schema
1494                            .data_type
1495                            .is_timestamp_unit_widening_to(&self.target_type),
1496                    InvalidRegionRequestSnafu {
1497                        region_id: metadata.region_id,
1498                        err: time_index_not_widening_error(
1499                            &column_meta.column_schema.name,
1500                            &column_meta.column_schema.data_type,
1501                            &self.target_type,
1502                        ),
1503                    }
1504                );
1505            }
1506            SemanticType::Tag => {
1507                return InvalidRegionRequestSnafu {
1508                    region_id: metadata.region_id,
1509                    err: format!(
1510                        "tag column '{}' cannot change type, it is part of the primary key",
1511                        self.column_name
1512                    ),
1513                }
1514                .fail();
1515            }
1516        }
1517
1518        Ok(())
1519    }
1520
1521    /// Returns true if no column's datatype to change to the region.
1522    /// A column already in the target type needs no alteration, so a retried
1523    /// alter is a successful no-op.
1524    pub fn need_alter(&self, metadata: &RegionMetadata) -> bool {
1525        debug_assert!(self.validate(metadata).is_ok());
1526        metadata
1527            .column_by_name(&self.column_name)
1528            .is_some_and(|column| column.column_schema.data_type != self.target_type)
1529    }
1530}
1531
1532impl From<v1::ModifyColumnType> for ModifyColumnType {
1533    fn from(modify_column_type: v1::ModifyColumnType) -> Self {
1534        let target_type = ColumnDataTypeWrapper::new(
1535            modify_column_type.target_type(),
1536            modify_column_type.target_type_extension,
1537        )
1538        .into();
1539
1540        ModifyColumnType {
1541            column_name: modify_column_type.column_name,
1542            target_type,
1543        }
1544    }
1545}
1546
1547fn json_settings_from_proto(settings: v1::JsonSettings) -> Result<JsonSettings> {
1548    let type_hints = settings
1549        .type_hints
1550        .into_iter()
1551        .map(|hint| {
1552            let wrapper = ColumnDataTypeWrapper::try_new(hint.data_type, hint.datatype_extension)
1553                .map_err(|err| {
1554                InvalidRawRegionRequestSnafu {
1555                    err: err.to_string(),
1556                }
1557                .build()
1558            })?;
1559            let data_type = ConcreteDataType::from(wrapper);
1560
1561            Ok(JsonTypeHint {
1562                path: hint.path,
1563                data_type,
1564                inverted_index: false,
1565            })
1566        })
1567        .collect::<Result<Vec<_>>>()?;
1568
1569    JsonSettings::try_new(type_hints, settings.max_auto_expanded_paths).map_err(|err| {
1570        InvalidRawRegionRequestSnafu {
1571            err: err.to_string(),
1572        }
1573        .build()
1574    })
1575}
1576
1577/// Region option changes used by ALTER requests.
1578///
1579/// This type is serialized for request persistence. Keep future changes backward
1580/// compatible with previously serialized variants.
1581#[derive(Debug, Eq, PartialEq, Clone)]
1582pub enum SetRegionOption {
1583    WriteBufferSize(Option<ReadableSize>),
1584    Ttl(Option<TimeToLive>),
1585    // Modifying TwscOptions with values as (option name, new value).
1586    Twsc(String, String),
1587    // Modifying the SST format.
1588    Format(String),
1589    // Modifying the append mode.
1590    AppendMode(bool),
1591    // Modifying the per-region auto flush interval override.
1592    AutoFlushInterval(Option<Duration>),
1593    // Modifying the max number of rows in a parquet row group.
1594    MaxRowGroupRowCount(Option<usize>),
1595    PreserveRowSequence(bool),
1596    // Whether to skip writing new WAL entries.
1597    SkipWal(bool),
1598}
1599
1600#[derive(Serialize, Deserialize)]
1601enum SetRegionOptionSerde {
1602    WriteBufferSize(Option<ReadableSize>),
1603    Ttl(Option<TimeToLive>),
1604    Twsc(String, String),
1605    Format(String),
1606    AppendMode(bool),
1607    AutoFlushInterval(Option<Duration>),
1608    MaxRowGroupRowCount(Option<usize>),
1609    PreserveRowSequence(bool),
1610    SkipWal(bool),
1611}
1612
1613impl Serialize for SetRegionOption {
1614    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1615    where
1616        S: Serializer,
1617    {
1618        // Older binaries deserialize the disable request as a unit variant.
1619        if matches!(self, Self::SkipWal(true)) {
1620            return serializer.serialize_unit_variant("SetRegionOption", 8, "SkipWal");
1621        }
1622
1623        let option = match self {
1624            Self::WriteBufferSize(value) => SetRegionOptionSerde::WriteBufferSize(*value),
1625            Self::Ttl(value) => SetRegionOptionSerde::Ttl(*value),
1626            Self::Twsc(key, value) => SetRegionOptionSerde::Twsc(key.clone(), value.clone()),
1627            Self::Format(value) => SetRegionOptionSerde::Format(value.clone()),
1628            Self::AppendMode(value) => SetRegionOptionSerde::AppendMode(*value),
1629            Self::AutoFlushInterval(value) => SetRegionOptionSerde::AutoFlushInterval(*value),
1630            Self::MaxRowGroupRowCount(value) => SetRegionOptionSerde::MaxRowGroupRowCount(*value),
1631            Self::PreserveRowSequence(value) => SetRegionOptionSerde::PreserveRowSequence(*value),
1632            Self::SkipWal(value) => SetRegionOptionSerde::SkipWal(*value),
1633        };
1634        option.serialize(serializer)
1635    }
1636}
1637
1638#[derive(Deserialize)]
1639enum LegacySetRegionOption {
1640    SkipWal,
1641}
1642
1643#[derive(Deserialize)]
1644#[serde(untagged)]
1645enum BackwardCompatibleSetRegionOption {
1646    Current(SetRegionOptionSerde),
1647    Legacy(LegacySetRegionOption),
1648}
1649
1650impl From<SetRegionOptionSerde> for SetRegionOption {
1651    fn from(option: SetRegionOptionSerde) -> Self {
1652        match option {
1653            SetRegionOptionSerde::WriteBufferSize(value) => Self::WriteBufferSize(value),
1654            SetRegionOptionSerde::Ttl(value) => Self::Ttl(value),
1655            SetRegionOptionSerde::Twsc(key, value) => Self::Twsc(key, value),
1656            SetRegionOptionSerde::Format(value) => Self::Format(value),
1657            SetRegionOptionSerde::AppendMode(value) => Self::AppendMode(value),
1658            SetRegionOptionSerde::AutoFlushInterval(value) => Self::AutoFlushInterval(value),
1659            SetRegionOptionSerde::MaxRowGroupRowCount(value) => Self::MaxRowGroupRowCount(value),
1660            SetRegionOptionSerde::PreserveRowSequence(value) => Self::PreserveRowSequence(value),
1661            SetRegionOptionSerde::SkipWal(value) => Self::SkipWal(value),
1662        }
1663    }
1664}
1665
1666impl<'de> Deserialize<'de> for SetRegionOption {
1667    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1668    where
1669        D: Deserializer<'de>,
1670    {
1671        Ok(
1672            match BackwardCompatibleSetRegionOption::deserialize(deserializer)? {
1673                BackwardCompatibleSetRegionOption::Current(option) => option.into(),
1674                BackwardCompatibleSetRegionOption::Legacy(LegacySetRegionOption::SkipWal) => {
1675                    Self::SkipWal(true)
1676                }
1677            },
1678        )
1679    }
1680}
1681
1682impl TryFrom<&PbOption> for SetRegionOption {
1683    type Error = MetadataError;
1684
1685    fn try_from(value: &PbOption) -> std::result::Result<Self, Self::Error> {
1686        let PbOption { key, value } = value;
1687        match key.as_str() {
1688            WRITE_BUFFER_SIZE_KEY => {
1689                let size = value
1690                    .parse::<ReadableSize>()
1691                    .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1692                Ok(Self::WriteBufferSize(Some(size)))
1693            }
1694            TTL_KEY => {
1695                let ttl = TimeToLive::from_humantime_or_str(value)
1696                    .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1697
1698                Ok(Self::Ttl(Some(ttl)))
1699            }
1700            TWCS_TRIGGER_FILE_NUM
1701            | TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM
1702            | TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER
1703            | TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM
1704            | TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER
1705            | TWCS_MAX_OUTPUT_FILE_SIZE
1706            | TWCS_TIME_WINDOW => Ok(Self::Twsc(key.clone(), value.clone())),
1707            SST_FORMAT_KEY => Ok(Self::Format(value.clone())),
1708            APPEND_MODE_KEY => {
1709                let append_mode = value
1710                    .parse::<bool>()
1711                    .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1712                Ok(Self::AppendMode(append_mode))
1713            }
1714            AUTO_FLUSH_INTERVAL_KEY => {
1715                if value.is_empty() {
1716                    // SET 'auto_flush_interval' = NULL comes through as an empty
1717                    // string; treat it as clearing the override (fall back to
1718                    // the global default), same as Ttl.
1719                    return Ok(Self::AutoFlushInterval(None));
1720                }
1721                let interval = humantime::parse_duration(value)
1722                    .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1723                if interval <= Duration::ZERO {
1724                    return InvalidSetRegionOptionRequestSnafu { key, value }.fail();
1725                }
1726                Ok(Self::AutoFlushInterval(Some(interval)))
1727            }
1728            MAX_ROW_GROUP_ROW_COUNT => {
1729                if value.is_empty() {
1730                    return Ok(Self::MaxRowGroupRowCount(None));
1731                }
1732                let row_count = value
1733                    .parse::<usize>()
1734                    .ok()
1735                    .filter(|row_count| {
1736                        *row_count > 0 && *row_count <= MAX_ROW_GROUP_ROW_COUNT_LIMIT
1737                    })
1738                    .ok_or_else(|| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1739                Ok(Self::MaxRowGroupRowCount(Some(row_count)))
1740            }
1741            PRESERVE_ROW_SEQUENCE => {
1742                let preserve = value
1743                    .parse::<bool>()
1744                    .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1745                Ok(Self::PreserveRowSequence(preserve))
1746            }
1747            SKIP_WAL_KEY => {
1748                let skip_wal = value
1749                    .parse::<bool>()
1750                    .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1751                Ok(Self::SkipWal(skip_wal))
1752            }
1753            _ => InvalidSetRegionOptionRequestSnafu { key, value }.fail(),
1754        }
1755    }
1756}
1757
1758impl From<&UnsetRegionOption> for SetRegionOption {
1759    fn from(unset_option: &UnsetRegionOption) -> Self {
1760        match unset_option {
1761            UnsetRegionOption::TwcsTriggerFileNum => {
1762                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1763            }
1764            UnsetRegionOption::TwcsActiveWindowTriggerFileNum => {
1765                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1766            }
1767            UnsetRegionOption::TwcsActiveWindowL1MergeTrigger => {
1768                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1769            }
1770            UnsetRegionOption::TwcsInactiveWindowTriggerFileNum => {
1771                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1772            }
1773            UnsetRegionOption::TwcsInactiveWindowL1MergeTrigger => {
1774                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1775            }
1776            UnsetRegionOption::TwcsMaxOutputFileSize => {
1777                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1778            }
1779            UnsetRegionOption::TwcsTimeWindow => {
1780                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1781            }
1782            UnsetRegionOption::Ttl => SetRegionOption::Ttl(Default::default()),
1783            UnsetRegionOption::MaxRowGroupRowCount => SetRegionOption::MaxRowGroupRowCount(None),
1784            UnsetRegionOption::WriteBufferSize => SetRegionOption::WriteBufferSize(None),
1785            UnsetRegionOption::PreserveRowSequence => SetRegionOption::PreserveRowSequence(false),
1786        }
1787    }
1788}
1789
1790impl TryFrom<&str> for UnsetRegionOption {
1791    type Error = MetadataError;
1792
1793    fn try_from(key: &str) -> Result<Self> {
1794        match key.to_ascii_lowercase().as_str() {
1795            TTL_KEY => Ok(Self::Ttl),
1796            WRITE_BUFFER_SIZE_KEY => Ok(Self::WriteBufferSize),
1797            TWCS_TRIGGER_FILE_NUM => Ok(Self::TwcsTriggerFileNum),
1798            TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM => Ok(Self::TwcsActiveWindowTriggerFileNum),
1799            TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER => Ok(Self::TwcsActiveWindowL1MergeTrigger),
1800            TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM => Ok(Self::TwcsInactiveWindowTriggerFileNum),
1801            TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER => Ok(Self::TwcsInactiveWindowL1MergeTrigger),
1802            TWCS_MAX_OUTPUT_FILE_SIZE => Ok(Self::TwcsMaxOutputFileSize),
1803            TWCS_TIME_WINDOW => Ok(Self::TwcsTimeWindow),
1804            MAX_ROW_GROUP_ROW_COUNT => Ok(Self::MaxRowGroupRowCount),
1805            PRESERVE_ROW_SEQUENCE => Ok(Self::PreserveRowSequence),
1806            _ => InvalidUnsetRegionOptionRequestSnafu { key }.fail(),
1807        }
1808    }
1809}
1810
1811#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
1812pub enum UnsetRegionOption {
1813    TwcsTriggerFileNum,
1814    TwcsActiveWindowTriggerFileNum,
1815    TwcsInactiveWindowTriggerFileNum,
1816    TwcsInactiveWindowL1MergeTrigger,
1817    TwcsMaxOutputFileSize,
1818    TwcsTimeWindow,
1819    Ttl,
1820    MaxRowGroupRowCount,
1821    WriteBufferSize,
1822    PreserveRowSequence,
1823    TwcsActiveWindowL1MergeTrigger,
1824}
1825
1826impl UnsetRegionOption {
1827    pub fn as_str(&self) -> &str {
1828        match self {
1829            Self::Ttl => TTL_KEY,
1830            Self::WriteBufferSize => WRITE_BUFFER_SIZE_KEY,
1831            Self::TwcsTriggerFileNum => TWCS_TRIGGER_FILE_NUM,
1832            Self::TwcsActiveWindowTriggerFileNum => TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM,
1833            Self::TwcsActiveWindowL1MergeTrigger => TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER,
1834            Self::TwcsInactiveWindowTriggerFileNum => TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM,
1835            Self::TwcsInactiveWindowL1MergeTrigger => TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER,
1836            Self::TwcsMaxOutputFileSize => TWCS_MAX_OUTPUT_FILE_SIZE,
1837            Self::TwcsTimeWindow => TWCS_TIME_WINDOW,
1838            Self::MaxRowGroupRowCount => MAX_ROW_GROUP_ROW_COUNT,
1839            Self::PreserveRowSequence => PRESERVE_ROW_SEQUENCE,
1840        }
1841    }
1842}
1843
1844impl Display for UnsetRegionOption {
1845    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1846        write!(f, "{}", self.as_str())
1847    }
1848}
1849
1850#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1851pub enum RegionFlushReason {
1852    /// Flush triggered before region migration.
1853    RegionMigration,
1854    /// Flush triggered by repartition procedure.
1855    Repartition,
1856    /// Flush triggered by remote WAL pruning.
1857    RemoteWalPrune,
1858    /// Flush region before closing region.
1859    Closing,
1860    /// Flush region before downgrading region.
1861    Downgrading,
1862}
1863
1864#[derive(Debug, Clone, Default)]
1865pub struct RegionFlushRequest {
1866    pub row_group_size: Option<usize>,
1867    pub reason: Option<RegionFlushReason>,
1868}
1869
1870#[derive(Debug)]
1871pub struct RegionCompactRequest {
1872    pub options: compact_request::Options,
1873    pub parallelism: Option<u32>,
1874    pub time_range: Option<TimestampRange>,
1875}
1876
1877impl Default for RegionCompactRequest {
1878    fn default() -> Self {
1879        Self {
1880            // Default to regular compaction.
1881            options: compact_request::Options::Regular(Default::default()),
1882            parallelism: None,
1883            time_range: None,
1884        }
1885    }
1886}
1887
1888#[derive(Debug, Clone, Default)]
1889pub struct RegionBuildIndexRequest {
1890    /// The index build mode. Absent options select SST indexes.
1891    pub options: Option<build_index_request::Options>,
1892}
1893
1894/// Truncate region request.
1895#[derive(Debug)]
1896pub enum RegionTruncateRequest {
1897    /// Truncate all data in the region.
1898    All,
1899    /// Discard all unflushed data while preserving persisted SST files.
1900    ///
1901    /// This destroys the region's in-memory data irreversibly. Persisted SST files and
1902    /// the writable state are preserved, and the WAL is obsoleted up to the discard point
1903    /// so a restart won't replay the discarded data.
1904    ///
1905    /// An error may be returned after the data has already been discarded, because the
1906    /// WAL is obsoleted last. Retrying is safe: a request against a region with nothing
1907    /// left to discard only re-attempts the WAL obsoletion.
1908    Unflushed,
1909    ByTimeRanges {
1910        /// Time ranges to truncate. Both bound are inclusive.
1911        /// only files that are fully contained in the time range will be truncated.
1912        /// so no guarantee that all data in the time range will be truncated.
1913        time_ranges: Vec<(Timestamp, Timestamp)>,
1914    },
1915}
1916
1917/// Catchup region request.
1918///
1919/// Makes a readonly region to catch up to leader region changes.
1920/// There is no effect if it operating on a leader region.
1921#[derive(Debug, Clone, Copy, Default)]
1922pub struct RegionCatchupRequest {
1923    /// Sets it to writable if it's available after it has caught up with all changes.
1924    pub set_writable: bool,
1925    /// The `entry_id` that was expected to reply to.
1926    /// `None` stands replaying to latest.
1927    pub entry_id: Option<entry::Id>,
1928    /// Used for metrics metadata region.
1929    /// The `entry_id` that was expected to reply to.
1930    /// `None` stands replaying to latest.
1931    pub metadata_entry_id: Option<entry::Id>,
1932    /// Replay checkpoint.
1933    pub checkpoint: Option<ReplayCheckpoint>,
1934}
1935
1936#[derive(Debug, Clone)]
1937pub struct RegionBulkInsertsRequest {
1938    /// Whether this request should skip WAL.
1939    pub skip_wal: bool,
1940    pub region_id: RegionId,
1941    pub payload: DfRecordBatch,
1942    pub raw_data: ArrowIpc,
1943    pub partition_expr_version: Option<u64>,
1944    pub aligned_schema_version: Option<u64>,
1945}
1946
1947impl RegionBulkInsertsRequest {
1948    pub fn estimated_size(&self) -> usize {
1949        self.payload.get_array_memory_size()
1950    }
1951}
1952
1953/// Request to stage a region with a new partition directive.
1954///
1955/// This request transitions a region into the staging mode.
1956/// It first flushes the memtable for the old partition expression if it is not
1957/// empty, then enters the staging mode with the new directive.
1958#[derive(Debug, Clone, PartialEq, Eq)]
1959pub enum StagingPartitionDirective {
1960    UpdatePartitionExpr(String),
1961    RejectAllWrites,
1962}
1963
1964impl StagingPartitionDirective {
1965    /// Returns the partition expression carried by this directive, if any.
1966    pub fn partition_expr(&self) -> Option<&str> {
1967        match self {
1968            Self::UpdatePartitionExpr(expr) => Some(expr),
1969            Self::RejectAllWrites => None,
1970        }
1971    }
1972}
1973
1974#[derive(Debug, Clone)]
1975pub struct EnterStagingRequest {
1976    /// The staging partition directive of the region.
1977    pub partition_directive: StagingPartitionDirective,
1978}
1979
1980impl EnterStagingRequest {
1981    /// Builds an enter-staging request with a partition expression directive.
1982    pub fn with_partition_expr(partition_expr: String) -> Self {
1983        Self {
1984            partition_directive: StagingPartitionDirective::UpdatePartitionExpr(partition_expr),
1985        }
1986    }
1987}
1988
1989/// This request is used as part of the region repartition.
1990///
1991/// After a region has entered staging mode with a new partition expression
1992/// expression) and a separate process (for example, `remap_manifests`) has
1993/// generated the new file assignments for the staging region, this request
1994/// applies that generated manifest to the region.
1995///
1996/// In practice, this means:
1997/// - The `partition_expr` identifies the staging partition expression that the manifest
1998///   was generated for.
1999/// - `central_region_id` specifies which region holds the staging blob storage
2000///   where the manifest was written during the `remap_manifests` operation.
2001/// - `manifest_path` is the relative path within the central region's staging
2002///   blob storage to fetch the generated manifest.
2003///
2004/// It should typically be called **after** the staging region has been
2005/// initialized by [`EnterStagingRequest`] and the new file layout has been
2006/// computed, to finalize the repartition operation.
2007#[derive(Debug, Clone)]
2008pub struct ApplyStagingManifestRequest {
2009    /// The partition expression of the staging region.
2010    pub partition_expr: String,
2011    /// The region that stores the staging manifests in its staging blob storage.
2012    pub central_region_id: RegionId,
2013    /// The relative path to the staging manifest within the central region's
2014    /// staging blob storage.
2015    pub manifest_path: String,
2016}
2017
2018impl fmt::Display for RegionRequest {
2019    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2020        match self {
2021            RegionRequest::Put(_) => write!(f, "Put"),
2022            RegionRequest::Delete(_) => write!(f, "Delete"),
2023            RegionRequest::Create(_) => write!(f, "Create"),
2024            RegionRequest::Drop(_) => write!(f, "Drop"),
2025            RegionRequest::Open(_) => write!(f, "Open"),
2026            RegionRequest::CleanUp(_) => write!(f, "CleanUp"),
2027            RegionRequest::Close(_) => write!(f, "Close"),
2028            RegionRequest::Alter(_) => write!(f, "Alter"),
2029            RegionRequest::Flush(_) => write!(f, "Flush"),
2030            RegionRequest::Compact(_) => write!(f, "Compact"),
2031            RegionRequest::BuildIndex(_) => write!(f, "BuildIndex"),
2032            RegionRequest::Truncate(_) => write!(f, "Truncate"),
2033            RegionRequest::Catchup(_) => write!(f, "Catchup"),
2034            RegionRequest::BulkInserts(_) => write!(f, "BulkInserts"),
2035            RegionRequest::EnterStaging(_) => write!(f, "EnterStaging"),
2036            RegionRequest::ApplyStagingManifest(_) => write!(f, "ApplyStagingManifest"),
2037        }
2038    }
2039}
2040
2041#[cfg(test)]
2042mod tests {
2043
2044    use api::v1::region::RegionColumnDef;
2045    use api::v1::{ColumnDataType, ColumnDef};
2046    use common_time::range::TimestampRange;
2047    use datatypes::extension::json::{Json2ExtensionType, JsonMetadata};
2048    use datatypes::json::JsonSettings;
2049    use datatypes::prelude::ConcreteDataType;
2050    use datatypes::schema::{ColumnSchema, FulltextAnalyzer, FulltextBackend};
2051    use datatypes::types::JsonType;
2052
2053    use super::*;
2054    use crate::metadata::RegionMetadataBuilder;
2055
2056    #[test]
2057    fn test_build_index_options_round_trip() {
2058        use prost::Message;
2059
2060        for options in [
2061            None,
2062            Some(build_index_request::Options::SstIndex(Default::default())),
2063            Some(build_index_request::Options::SeriesIndex(Default::default())),
2064        ] {
2065            let request = BuildIndexRequest {
2066                region_id: 42,
2067                options,
2068            };
2069            let decoded = BuildIndexRequest::decode(request.encode_to_vec().as_slice()).unwrap();
2070            let requests = make_region_build_index(decoded).unwrap();
2071            assert_eq!(1, requests.len());
2072            assert_eq!(RegionId::from_u64(42), requests[0].0);
2073            let RegionRequest::BuildIndex(request) = &requests[0].1 else {
2074                panic!("expected build-index request");
2075            };
2076            assert_eq!(options, request.options);
2077        }
2078        // The legacy wire message contains only region_id (field 1).
2079        let legacy = BuildIndexRequest::decode(&[0x08, 42][..]).unwrap();
2080        assert!(legacy.options.is_none());
2081    }
2082
2083    #[test]
2084    fn test_make_region_puts_preserves_skip_wal() {
2085        let region_id = RegionId::new(42, 3);
2086        let rows = Rows::default();
2087        let requests = make_region_puts(InsertRequests {
2088            requests: [false, true, false]
2089                .into_iter()
2090                .map(|skip_wal| api::v1::region::InsertRequest {
2091                    region_id: region_id.as_u64(),
2092                    rows: Some(rows.clone()),
2093                    partition_expr_version: Some(api::v1::PartitionExprVersion { value: 7 }),
2094                    skip_wal,
2095                })
2096                .collect(),
2097        })
2098        .unwrap();
2099
2100        assert_eq!(3, requests.len());
2101        for ((id, request), skip_wal) in requests.into_iter().zip([false, true, false]) {
2102            assert_eq!(region_id, id);
2103            let RegionRequest::Put(request) = request else {
2104                panic!("expected a put request");
2105            };
2106            assert_eq!(rows, request.rows);
2107            assert_eq!(skip_wal, request.skip_wal);
2108            assert_eq!(Some(7), request.partition_expr_version);
2109            assert!(request.hint.is_none());
2110        }
2111    }
2112
2113    #[test]
2114    fn test_make_region_compact_with_time_range() {
2115        let requests = make_region_compact(CompactRequest {
2116            region_id: 42,
2117            time_range: Some(api::v1::region::CompactionTimeRange {
2118                start: 1_000,
2119                end: 2_000,
2120                time_unit: api::v1::TimeUnit::Microsecond as i32,
2121            }),
2122            ..Default::default()
2123        })
2124        .unwrap();
2125
2126        let RegionRequest::Compact(request) = &requests[0].1 else {
2127            unreachable!();
2128        };
2129        assert_eq!(
2130            Some(
2131                TimestampRange::new(
2132                    Timestamp::new_microsecond(1_000),
2133                    Timestamp::new_microsecond(2_000),
2134                )
2135                .unwrap()
2136            ),
2137            request.time_range
2138        );
2139    }
2140
2141    #[test]
2142    fn test_make_region_truncate_unflushed() {
2143        let region_id = RegionId::new(42, 3);
2144        let requests =
2145            RegionRequest::try_from_request_body(region_request::Body::Truncate(TruncateRequest {
2146                region_id: region_id.as_u64(),
2147                kind: Some(truncate_request::Kind::Unflushed(
2148                    api::v1::region::Unflushed {},
2149                )),
2150            }))
2151            .unwrap();
2152
2153        assert_eq!(region_id, requests[0].0);
2154        assert!(matches!(
2155            requests[0].1,
2156            RegionRequest::Truncate(RegionTruncateRequest::Unflushed)
2157        ));
2158    }
2159
2160    #[test]
2161    fn test_make_region_truncate_requires_kind() {
2162        let error =
2163            RegionRequest::try_from_request_body(region_request::Body::Truncate(TruncateRequest {
2164                region_id: RegionId::new(42, 3).as_u64(),
2165                kind: None,
2166            }))
2167            .unwrap_err();
2168
2169        assert!(
2170            error
2171                .to_string()
2172                .contains("missing kind in TruncateRequest")
2173        );
2174    }
2175
2176    #[test]
2177    fn test_from_proto_location() {
2178        let proto_location = v1::AddColumnLocation {
2179            location_type: LocationType::First as i32,
2180            after_column_name: String::default(),
2181        };
2182        let location = AddColumnLocation::try_from(proto_location).unwrap();
2183        assert_eq!(location, AddColumnLocation::First);
2184
2185        let proto_location = v1::AddColumnLocation {
2186            location_type: 10,
2187            after_column_name: String::default(),
2188        };
2189        AddColumnLocation::try_from(proto_location).unwrap_err();
2190
2191        let proto_location = v1::AddColumnLocation {
2192            location_type: LocationType::After as i32,
2193            after_column_name: "a".to_string(),
2194        };
2195        let location = AddColumnLocation::try_from(proto_location).unwrap();
2196        assert_eq!(
2197            location,
2198            AddColumnLocation::After {
2199                column_name: "a".to_string()
2200            }
2201        );
2202    }
2203
2204    #[test]
2205    fn test_from_none_proto_add_column() {
2206        AddColumn::try_from(v1::region::AddColumn {
2207            column_def: None,
2208            location: None,
2209        })
2210        .unwrap_err();
2211    }
2212
2213    #[test]
2214    fn test_set_region_option_auto_flush_interval_try_from() {
2215        use std::time::Duration;
2216
2217        // Valid duration
2218        let pb = PbOption {
2219            key: "auto_flush_interval".to_string(),
2220            value: "5m".to_string(),
2221        };
2222        let opt = SetRegionOption::try_from(&pb).unwrap();
2223        assert_eq!(
2224            opt,
2225            SetRegionOption::AutoFlushInterval(Some(Duration::from_secs(300)))
2226        );
2227
2228        // Empty value clears the override (mirrors Ttl's behaviour for `SET key = NULL`).
2229        let pb = PbOption {
2230            key: "auto_flush_interval".to_string(),
2231            value: String::new(),
2232        };
2233        let opt = SetRegionOption::try_from(&pb).unwrap();
2234        assert_eq!(opt, SetRegionOption::AutoFlushInterval(None));
2235
2236        // Zero is rejected up front (engine invariant).
2237        let pb = PbOption {
2238            key: "auto_flush_interval".to_string(),
2239            value: "0s".to_string(),
2240        };
2241        assert!(SetRegionOption::try_from(&pb).is_err());
2242
2243        // Garbage value is rejected.
2244        let pb = PbOption {
2245            key: "auto_flush_interval".to_string(),
2246            value: "not_a_duration".to_string(),
2247        };
2248        assert!(SetRegionOption::try_from(&pb).is_err());
2249    }
2250
2251    #[test]
2252    fn test_set_region_option_skip_wal_try_from() {
2253        let pb = PbOption {
2254            key: SKIP_WAL_KEY.to_string(),
2255            value: "true".to_string(),
2256        };
2257        assert_eq!(
2258            SetRegionOption::SkipWal(true),
2259            SetRegionOption::try_from(&pb).unwrap()
2260        );
2261
2262        let pb = PbOption {
2263            key: SKIP_WAL_KEY.to_string(),
2264            value: "false".to_string(),
2265        };
2266        assert_eq!(
2267            SetRegionOption::SkipWal(false),
2268            SetRegionOption::try_from(&pb).unwrap()
2269        );
2270
2271        for value in ["", "invalid"] {
2272            let pb = PbOption {
2273                key: SKIP_WAL_KEY.to_string(),
2274                value: value.to_string(),
2275            };
2276            assert!(SetRegionOption::try_from(&pb).is_err());
2277        }
2278
2279        assert!(UnsetRegionOption::try_from(SKIP_WAL_KEY).is_err());
2280    }
2281
2282    #[test]
2283    fn test_set_region_option_skip_wal_serde_compatibility() {
2284        let legacy = serde_json::from_str::<SetRegionOption>(r#""SkipWal""#).unwrap();
2285        assert_eq!(SetRegionOption::SkipWal(true), legacy);
2286
2287        check_set_region_option_skip_wal_serde_compatibility(false);
2288        check_set_region_option_skip_wal_serde_compatibility(true);
2289    }
2290
2291    fn check_set_region_option_skip_wal_serde_compatibility(skip_wal: bool) {
2292        let option = SetRegionOption::SkipWal(skip_wal);
2293        let serialized = serde_json::to_string(&option).unwrap();
2294        let expected = if skip_wal {
2295            r#""SkipWal""#
2296        } else {
2297            r#"{"SkipWal":false}"#
2298        };
2299        assert_eq!(expected, serialized);
2300        assert_eq!(
2301            option,
2302            serde_json::from_str::<SetRegionOption>(&serialized).unwrap()
2303        );
2304        if skip_wal {
2305            assert!(serde_json::from_str::<LegacySetRegionOption>(&serialized).is_ok());
2306            assert_eq!(
2307                option,
2308                serde_json::from_str::<SetRegionOption>(r#"{"SkipWal":true}"#).unwrap()
2309            );
2310        }
2311    }
2312
2313    #[test]
2314    fn test_set_region_option_max_row_group_row_count_try_from() {
2315        let pb = PbOption {
2316            key: MAX_ROW_GROUP_ROW_COUNT.to_string(),
2317            value: "512".to_string(),
2318        };
2319        assert_eq!(
2320            SetRegionOption::MaxRowGroupRowCount(Some(512)),
2321            SetRegionOption::try_from(&pb).unwrap()
2322        );
2323
2324        let pb = PbOption {
2325            key: MAX_ROW_GROUP_ROW_COUNT.to_string(),
2326            value: String::new(),
2327        };
2328        assert_eq!(
2329            SetRegionOption::MaxRowGroupRowCount(None),
2330            SetRegionOption::try_from(&pb).unwrap()
2331        );
2332
2333        for value in [
2334            "0".to_string(),
2335            (MAX_ROW_GROUP_ROW_COUNT_LIMIT + 1).to_string(),
2336            "invalid".to_string(),
2337        ] {
2338            let pb = PbOption {
2339                key: MAX_ROW_GROUP_ROW_COUNT.to_string(),
2340                value,
2341            };
2342            assert!(SetRegionOption::try_from(&pb).is_err());
2343        }
2344
2345        assert_eq!(
2346            UnsetRegionOption::MaxRowGroupRowCount,
2347            UnsetRegionOption::try_from(MAX_ROW_GROUP_ROW_COUNT).unwrap()
2348        );
2349    }
2350
2351    #[test]
2352    fn test_set_region_option_preserve_row_sequence_try_from() {
2353        for (value, expected) in [("true", true), ("false", false)] {
2354            let pb = PbOption {
2355                key: PRESERVE_ROW_SEQUENCE.to_string(),
2356                value: value.to_string(),
2357            };
2358            assert_eq!(
2359                SetRegionOption::PreserveRowSequence(expected),
2360                SetRegionOption::try_from(&pb).unwrap()
2361            );
2362        }
2363
2364        for value in ["1", "invalid"] {
2365            let pb = PbOption {
2366                key: PRESERVE_ROW_SEQUENCE.to_string(),
2367                value: value.to_string(),
2368            };
2369            assert!(SetRegionOption::try_from(&pb).is_err());
2370        }
2371
2372        assert_eq!(
2373            UnsetRegionOption::PreserveRowSequence,
2374            UnsetRegionOption::try_from(PRESERVE_ROW_SEQUENCE).unwrap()
2375        );
2376        assert_eq!(
2377            SetRegionOption::PreserveRowSequence(false),
2378            (&UnsetRegionOption::PreserveRowSequence).into()
2379        );
2380    }
2381
2382    #[test]
2383    fn test_set_twcs_window_trigger_options_try_from() {
2384        for key in [
2385            "compaction.twcs.active_window.trigger_file_num",
2386            "compaction.twcs.active_window.l1_merge_trigger",
2387            "compaction.twcs.inactive_window.trigger_file_num",
2388            "compaction.twcs.inactive_window.l1_merge_trigger",
2389        ] {
2390            let option = PbOption {
2391                key: key.to_string(),
2392                value: "8".to_string(),
2393            };
2394            assert_eq!(
2395                SetRegionOption::Twsc(key.to_string(), "8".to_string()),
2396                SetRegionOption::try_from(&option).unwrap(),
2397                "{key}"
2398            );
2399        }
2400    }
2401
2402    #[test]
2403    fn test_unset_twcs_window_trigger_options_try_from() {
2404        for (key, expected) in [
2405            (
2406                "compaction.twcs.active_window.trigger_file_num",
2407                UnsetRegionOption::TwcsActiveWindowTriggerFileNum,
2408            ),
2409            (
2410                "compaction.twcs.active_window.l1_merge_trigger",
2411                UnsetRegionOption::TwcsActiveWindowL1MergeTrigger,
2412            ),
2413            (
2414                "compaction.twcs.inactive_window.trigger_file_num",
2415                UnsetRegionOption::TwcsInactiveWindowTriggerFileNum,
2416            ),
2417        ] {
2418            assert_eq!(expected, UnsetRegionOption::try_from(key).unwrap());
2419            assert_eq!(
2420                SetRegionOption::Twsc(key.to_string(), String::new()),
2421                SetRegionOption::from(&expected)
2422            );
2423        }
2424
2425        let key = "compaction.twcs.inactive_window.l1_merge_trigger";
2426        let option = UnsetRegionOption::try_from(key).unwrap();
2427        assert_eq!(key, option.to_string());
2428        assert_eq!(
2429            SetRegionOption::Twsc(key.to_string(), String::new()),
2430            SetRegionOption::from(&option)
2431        );
2432    }
2433
2434    #[test]
2435    fn test_from_proto_alter_request() {
2436        RegionAlterRequest::try_from(AlterRequest {
2437            region_id: 0,
2438            schema_version: 1,
2439            kind: None,
2440        })
2441        .unwrap_err();
2442
2443        let request = RegionAlterRequest::try_from(AlterRequest {
2444            region_id: 0,
2445            schema_version: 1,
2446            kind: Some(alter_request::Kind::AddColumns(v1::region::AddColumns {
2447                add_columns: vec![v1::region::AddColumn {
2448                    column_def: Some(RegionColumnDef {
2449                        column_def: Some(ColumnDef {
2450                            name: "a".to_string(),
2451                            data_type: ColumnDataType::String as i32,
2452                            is_nullable: true,
2453                            default_constraint: vec![],
2454                            semantic_type: SemanticType::Field as i32,
2455                            comment: String::new(),
2456                            ..Default::default()
2457                        }),
2458                        column_id: 1,
2459                    }),
2460                    location: Some(v1::AddColumnLocation {
2461                        location_type: LocationType::First as i32,
2462                        after_column_name: String::default(),
2463                    }),
2464                }],
2465            })),
2466        })
2467        .unwrap();
2468
2469        assert_eq!(
2470            request,
2471            RegionAlterRequest {
2472                kind: AlterKind::AddColumns {
2473                    columns: vec![AddColumn {
2474                        column_metadata: ColumnMetadata {
2475                            column_schema: ColumnSchema::new(
2476                                "a",
2477                                ConcreteDataType::string_datatype(),
2478                                true,
2479                            ),
2480                            semantic_type: SemanticType::Field,
2481                            column_id: 1,
2482                        },
2483                        location: Some(AddColumnLocation::First),
2484                    }]
2485                },
2486            }
2487        );
2488
2489        let request = RegionAlterRequest::try_from(AlterRequest {
2490            region_id: 0,
2491            schema_version: 1,
2492            kind: Some(alter_request::Kind::SetJsonSettings(v1::SetJsonSettings {
2493                column_name: "payload".to_string(),
2494                settings: Some(v1::JsonSettings {
2495                    type_hints: vec![v1::JsonTypeHint {
2496                        path: vec!["service".to_string()],
2497                        data_type: ColumnDataType::String as i32,
2498                        datatype_extension: None,
2499                    }],
2500                    max_auto_expanded_paths: Some(10),
2501                }),
2502            })),
2503        })
2504        .unwrap();
2505
2506        let AlterKind::SetJsonSettings {
2507            column_name,
2508            settings,
2509        } = request.kind
2510        else {
2511            unreachable!()
2512        };
2513        assert_eq!("payload", column_name);
2514        assert_eq!(Some(10), settings.max_auto_expanded_paths());
2515        assert_eq!(1, settings.type_hints().len());
2516    }
2517
2518    #[test]
2519    fn test_write_buffer_size_region_options() {
2520        let option = PbOption {
2521            key: WRITE_BUFFER_SIZE_KEY.to_string(),
2522            value: "128MiB".to_string(),
2523        };
2524        assert_eq!(
2525            SetRegionOption::WriteBufferSize(Some(ReadableSize::mb(128))),
2526            SetRegionOption::try_from(&option).unwrap()
2527        );
2528
2529        let option = PbOption {
2530            key: WRITE_BUFFER_SIZE_KEY.to_string(),
2531            value: "invalid".to_string(),
2532        };
2533        SetRegionOption::try_from(&option).unwrap_err();
2534
2535        // Clearing the option uses UNSET. Empty SET values, including SQL NULL,
2536        // remain invalid because the SQL layer does not distinguish NULL from ''.
2537        let option = PbOption {
2538            key: WRITE_BUFFER_SIZE_KEY.to_string(),
2539            value: String::new(),
2540        };
2541        SetRegionOption::try_from(&option).unwrap_err();
2542
2543        assert_eq!(
2544            UnsetRegionOption::WriteBufferSize,
2545            UnsetRegionOption::try_from(WRITE_BUFFER_SIZE_KEY).unwrap()
2546        );
2547        assert_eq!(
2548            SetRegionOption::WriteBufferSize(None),
2549            SetRegionOption::from(&UnsetRegionOption::WriteBufferSize)
2550        );
2551    }
2552
2553    /// Returns a new region metadata for testing. Metadata:
2554    /// `[(ts, ms, 1), (tag_0, string, 2), (field_0, string, 3), (field_1, bool, 4)]`
2555    fn new_metadata() -> RegionMetadata {
2556        let mut builder = RegionMetadataBuilder::new(RegionId::new(1, 1));
2557        builder
2558            .push_column_metadata(ColumnMetadata {
2559                column_schema: ColumnSchema::new(
2560                    "ts",
2561                    ConcreteDataType::timestamp_millisecond_datatype(),
2562                    false,
2563                ),
2564                semantic_type: SemanticType::Timestamp,
2565                column_id: 1,
2566            })
2567            .push_column_metadata(ColumnMetadata {
2568                column_schema: ColumnSchema::new(
2569                    "tag_0",
2570                    ConcreteDataType::string_datatype(),
2571                    true,
2572                ),
2573                semantic_type: SemanticType::Tag,
2574                column_id: 2,
2575            })
2576            .push_column_metadata(ColumnMetadata {
2577                column_schema: ColumnSchema::new(
2578                    "field_0",
2579                    ConcreteDataType::string_datatype(),
2580                    true,
2581                ),
2582                semantic_type: SemanticType::Field,
2583                column_id: 3,
2584            })
2585            .push_column_metadata(ColumnMetadata {
2586                column_schema: ColumnSchema::new(
2587                    "field_1",
2588                    ConcreteDataType::boolean_datatype(),
2589                    true,
2590                ),
2591                semantic_type: SemanticType::Field,
2592                column_id: 4,
2593            })
2594            .primary_key(vec![2]);
2595        builder.build().unwrap()
2596    }
2597
2598    fn json2_column_schema(name: &str, settings: JsonSettings) -> ColumnSchema {
2599        let mut column_schema =
2600            ColumnSchema::new(name, ConcreteDataType::Json(JsonType::null()), true);
2601        column_schema.with_extension_type(&Json2ExtensionType::new(std::sync::Arc::new(
2602            JsonMetadata::new(settings),
2603        )));
2604        column_schema
2605    }
2606
2607    #[test]
2608    fn test_add_column_validate() {
2609        let metadata = new_metadata();
2610        let add_column = AddColumn {
2611            column_metadata: ColumnMetadata {
2612                column_schema: ColumnSchema::new(
2613                    "tag_1",
2614                    ConcreteDataType::string_datatype(),
2615                    true,
2616                ),
2617                semantic_type: SemanticType::Tag,
2618                column_id: 5,
2619            },
2620            location: None,
2621        };
2622        add_column.validate(&metadata).unwrap();
2623        assert!(add_column.need_alter(&metadata));
2624
2625        // Add not null column.
2626        AddColumn {
2627            column_metadata: ColumnMetadata {
2628                column_schema: ColumnSchema::new(
2629                    "tag_1",
2630                    ConcreteDataType::string_datatype(),
2631                    false,
2632                ),
2633                semantic_type: SemanticType::Tag,
2634                column_id: 5,
2635            },
2636            location: None,
2637        }
2638        .validate(&metadata)
2639        .unwrap_err();
2640
2641        // Add existing column.
2642        let add_column = AddColumn {
2643            column_metadata: ColumnMetadata {
2644                column_schema: ColumnSchema::new(
2645                    "tag_0",
2646                    ConcreteDataType::string_datatype(),
2647                    true,
2648                ),
2649                semantic_type: SemanticType::Tag,
2650                column_id: 2,
2651            },
2652            location: None,
2653        };
2654        add_column.validate(&metadata).unwrap();
2655        assert!(!add_column.need_alter(&metadata));
2656    }
2657
2658    #[test]
2659    fn test_add_duplicate_columns() {
2660        let kind = AlterKind::AddColumns {
2661            columns: vec![
2662                AddColumn {
2663                    column_metadata: ColumnMetadata {
2664                        column_schema: ColumnSchema::new(
2665                            "tag_1",
2666                            ConcreteDataType::string_datatype(),
2667                            true,
2668                        ),
2669                        semantic_type: SemanticType::Tag,
2670                        column_id: 5,
2671                    },
2672                    location: None,
2673                },
2674                AddColumn {
2675                    column_metadata: ColumnMetadata {
2676                        column_schema: ColumnSchema::new(
2677                            "tag_1",
2678                            ConcreteDataType::string_datatype(),
2679                            true,
2680                        ),
2681                        semantic_type: SemanticType::Field,
2682                        column_id: 6,
2683                    },
2684                    location: None,
2685                },
2686            ],
2687        };
2688        let metadata = new_metadata();
2689        kind.validate(&metadata).unwrap();
2690        assert!(kind.need_alter(&metadata));
2691    }
2692
2693    #[test]
2694    fn test_add_existing_column_different_metadata() {
2695        let metadata = new_metadata();
2696
2697        // Add existing column with different id.
2698        let kind = AlterKind::AddColumns {
2699            columns: vec![AddColumn {
2700                column_metadata: ColumnMetadata {
2701                    column_schema: ColumnSchema::new(
2702                        "tag_0",
2703                        ConcreteDataType::string_datatype(),
2704                        true,
2705                    ),
2706                    semantic_type: SemanticType::Tag,
2707                    column_id: 4,
2708                },
2709                location: None,
2710            }],
2711        };
2712        kind.validate(&metadata).unwrap_err();
2713
2714        // Add existing column with different type.
2715        let kind = AlterKind::AddColumns {
2716            columns: vec![AddColumn {
2717                column_metadata: ColumnMetadata {
2718                    column_schema: ColumnSchema::new(
2719                        "tag_0",
2720                        ConcreteDataType::int64_datatype(),
2721                        true,
2722                    ),
2723                    semantic_type: SemanticType::Tag,
2724                    column_id: 2,
2725                },
2726                location: None,
2727            }],
2728        };
2729        kind.validate(&metadata).unwrap_err();
2730
2731        // Add existing column with different name.
2732        let kind = AlterKind::AddColumns {
2733            columns: vec![AddColumn {
2734                column_metadata: ColumnMetadata {
2735                    column_schema: ColumnSchema::new(
2736                        "tag_1",
2737                        ConcreteDataType::string_datatype(),
2738                        true,
2739                    ),
2740                    semantic_type: SemanticType::Tag,
2741                    column_id: 2,
2742                },
2743                location: None,
2744            }],
2745        };
2746        kind.validate(&metadata).unwrap_err();
2747    }
2748
2749    #[test]
2750    fn test_add_existing_column_with_location() {
2751        let metadata = new_metadata();
2752        let kind = AlterKind::AddColumns {
2753            columns: vec![AddColumn {
2754                column_metadata: ColumnMetadata {
2755                    column_schema: ColumnSchema::new(
2756                        "tag_0",
2757                        ConcreteDataType::string_datatype(),
2758                        true,
2759                    ),
2760                    semantic_type: SemanticType::Tag,
2761                    column_id: 2,
2762                },
2763                location: Some(AddColumnLocation::First),
2764            }],
2765        };
2766        kind.validate(&metadata).unwrap_err();
2767    }
2768
2769    #[test]
2770    fn test_validate_drop_column() {
2771        let metadata = new_metadata();
2772        let kind = AlterKind::DropColumns {
2773            names: vec!["xxxx".to_string()],
2774        };
2775        kind.validate(&metadata).unwrap();
2776        assert!(!kind.need_alter(&metadata));
2777
2778        AlterKind::DropColumns {
2779            names: vec!["tag_0".to_string()],
2780        }
2781        .validate(&metadata)
2782        .unwrap_err();
2783
2784        let kind = AlterKind::DropColumns {
2785            names: vec!["field_0".to_string()],
2786        };
2787        kind.validate(&metadata).unwrap();
2788        assert!(kind.need_alter(&metadata));
2789    }
2790
2791    #[test]
2792    fn test_validate_modify_column_type() {
2793        let metadata = new_metadata();
2794        AlterKind::ModifyColumnTypes {
2795            columns: vec![ModifyColumnType {
2796                column_name: "xxxx".to_string(),
2797                target_type: ConcreteDataType::string_datatype(),
2798            }],
2799        }
2800        .validate(&metadata)
2801        .unwrap_err();
2802
2803        AlterKind::ModifyColumnTypes {
2804            columns: vec![ModifyColumnType {
2805                column_name: "field_1".to_string(),
2806                target_type: ConcreteDataType::date_datatype(),
2807            }],
2808        }
2809        .validate(&metadata)
2810        .unwrap_err();
2811
2812        AlterKind::ModifyColumnTypes {
2813            columns: vec![ModifyColumnType {
2814                column_name: "ts".to_string(),
2815                target_type: ConcreteDataType::date_datatype(),
2816            }],
2817        }
2818        .validate(&metadata)
2819        .unwrap_err();
2820
2821        // Time index unit widening is allowed.
2822        let kind = AlterKind::ModifyColumnTypes {
2823            columns: vec![ModifyColumnType {
2824                column_name: "ts".to_string(),
2825                target_type: ConcreteDataType::timestamp_microsecond_datatype(),
2826            }],
2827        };
2828        kind.validate(&metadata).unwrap();
2829        assert!(kind.need_alter(&metadata));
2830
2831        // Narrowing the time index unit is rejected.
2832        let metadata_nano = {
2833            let mut metadata = new_metadata();
2834            for col in metadata.column_metadatas.iter_mut() {
2835                if col.column_schema.name == "ts" {
2836                    col.column_schema.data_type = ConcreteDataType::timestamp_nanosecond_datatype();
2837                }
2838            }
2839            metadata
2840        };
2841        AlterKind::ModifyColumnTypes {
2842            columns: vec![ModifyColumnType {
2843                column_name: "ts".to_string(),
2844                target_type: ConcreteDataType::timestamp_millisecond_datatype(),
2845            }],
2846        }
2847        .validate(&metadata_nano)
2848        .unwrap_err();
2849
2850        // Changing the time index to the same type is a validated no-op, so a
2851        // retried alter procedure (region already altered) succeeds and is
2852        // skipped by `need_alter`.
2853        let same_type = AlterKind::ModifyColumnTypes {
2854            columns: vec![ModifyColumnType {
2855                column_name: "ts".to_string(),
2856                target_type: ConcreteDataType::timestamp_millisecond_datatype(),
2857            }],
2858        };
2859        same_type.validate(&metadata).unwrap();
2860        assert!(!same_type.need_alter(&metadata));
2861
2862        // Changing the time index to a non-timestamp type is rejected.
2863        AlterKind::ModifyColumnTypes {
2864            columns: vec![ModifyColumnType {
2865                column_name: "ts".to_string(),
2866                target_type: ConcreteDataType::string_datatype(),
2867            }],
2868        }
2869        .validate(&metadata)
2870        .unwrap_err();
2871
2872        AlterKind::ModifyColumnTypes {
2873            columns: vec![ModifyColumnType {
2874                column_name: "tag_0".to_string(),
2875                target_type: ConcreteDataType::date_datatype(),
2876            }],
2877        }
2878        .validate(&metadata)
2879        .unwrap_err();
2880
2881        let kind = AlterKind::ModifyColumnTypes {
2882            columns: vec![ModifyColumnType {
2883                column_name: "field_0".to_string(),
2884                target_type: ConcreteDataType::int32_datatype(),
2885            }],
2886        };
2887        kind.validate(&metadata).unwrap();
2888        assert!(kind.need_alter(&metadata));
2889    }
2890
2891    #[test]
2892    fn test_validate_set_json_settings() {
2893        let mut metadata = new_metadata();
2894        let current_schema = json2_column_schema("field_0", JsonSettings::new_v2());
2895        let target_settings = JsonSettings::try_new(vec![], Some(10)).unwrap();
2896        metadata
2897            .column_metadatas
2898            .iter_mut()
2899            .find(|column| column.column_schema.name == "field_0")
2900            .unwrap()
2901            .column_schema = current_schema.clone();
2902
2903        let kind = AlterKind::SetJsonSettings {
2904            column_name: "field_0".to_string(),
2905            settings: target_settings.clone(),
2906        };
2907        kind.validate(&metadata).unwrap();
2908        assert!(kind.need_alter(&metadata));
2909
2910        let no_op = AlterKind::SetJsonSettings {
2911            column_name: "field_0".to_string(),
2912            settings: JsonSettings::new_v2(),
2913        };
2914        no_op.validate(&metadata).unwrap();
2915        assert!(!no_op.need_alter(&metadata));
2916
2917        AlterKind::SetJsonSettings {
2918            column_name: "tag_0".to_string(),
2919            settings: target_settings,
2920        }
2921        .validate(&metadata)
2922        .unwrap_err();
2923    }
2924
2925    #[test]
2926    fn test_validate_add_columns() {
2927        let kind = AlterKind::AddColumns {
2928            columns: vec![
2929                AddColumn {
2930                    column_metadata: ColumnMetadata {
2931                        column_schema: ColumnSchema::new(
2932                            "tag_1",
2933                            ConcreteDataType::string_datatype(),
2934                            true,
2935                        ),
2936                        semantic_type: SemanticType::Tag,
2937                        column_id: 5,
2938                    },
2939                    location: None,
2940                },
2941                AddColumn {
2942                    column_metadata: ColumnMetadata {
2943                        column_schema: ColumnSchema::new(
2944                            "field_2",
2945                            ConcreteDataType::string_datatype(),
2946                            true,
2947                        ),
2948                        semantic_type: SemanticType::Field,
2949                        column_id: 6,
2950                    },
2951                    location: None,
2952                },
2953            ],
2954        };
2955        let request = RegionAlterRequest { kind };
2956        let mut metadata = new_metadata();
2957        metadata.schema_version = 1;
2958        request.validate(&metadata).unwrap();
2959    }
2960
2961    #[test]
2962    fn test_validate_create_region() {
2963        let column_metadatas = vec![
2964            ColumnMetadata {
2965                column_schema: ColumnSchema::new(
2966                    "ts",
2967                    ConcreteDataType::timestamp_millisecond_datatype(),
2968                    false,
2969                ),
2970                semantic_type: SemanticType::Timestamp,
2971                column_id: 1,
2972            },
2973            ColumnMetadata {
2974                column_schema: ColumnSchema::new(
2975                    "tag_0",
2976                    ConcreteDataType::string_datatype(),
2977                    true,
2978                ),
2979                semantic_type: SemanticType::Tag,
2980                column_id: 2,
2981            },
2982            ColumnMetadata {
2983                column_schema: ColumnSchema::new(
2984                    "field_0",
2985                    ConcreteDataType::string_datatype(),
2986                    true,
2987                ),
2988                semantic_type: SemanticType::Field,
2989                column_id: 3,
2990            },
2991        ];
2992        let create = RegionCreateRequest {
2993            engine: "mito".to_string(),
2994            column_metadatas,
2995            primary_key: vec![3, 4],
2996            options: HashMap::new(),
2997            table_dir: "path".to_string(),
2998            path_type: PathType::Bare,
2999            partition_expr_json: Some("".to_string()),
3000            requirements: Default::default(),
3001        };
3002
3003        assert!(create.validate().is_err());
3004    }
3005
3006    #[test]
3007    fn test_parse_create_region_requirements_defaults_to_empty() {
3008        let create = CreateRequest {
3009            region_id: RegionId::new(42, 0).as_u64(),
3010            engine: "mito".to_string(),
3011            column_defs: vec![],
3012            primary_key: vec![],
3013            path: "test".to_string(),
3014            options: HashMap::new(),
3015            partition: None,
3016            requirements: None,
3017        };
3018
3019        let requests =
3020            RegionRequest::try_from_request_body(region_request::Body::Create(create)).unwrap();
3021        let RegionRequest::Create(request) = &requests[0].1 else {
3022            unreachable!()
3023        };
3024
3025        assert_eq!(request.requirements, RegionRequirements::empty());
3026    }
3027
3028    #[test]
3029    fn test_parse_create_region_requirements_from_proto() {
3030        let create = CreateRequest {
3031            region_id: RegionId::new(42, 0).as_u64(),
3032            engine: "mito".to_string(),
3033            column_defs: vec![],
3034            primary_key: vec![],
3035            path: "test".to_string(),
3036            options: HashMap::new(),
3037            partition: None,
3038            requirements: Some(api::v1::region::RegionRequirements {
3039                object_storage: true,
3040            }),
3041        };
3042
3043        let requests =
3044            RegionRequest::try_from_request_body(region_request::Body::Create(create)).unwrap();
3045        let RegionRequest::Create(request) = &requests[0].1 else {
3046            unreachable!()
3047        };
3048
3049        assert_eq!(request.requirements, RegionRequirements::object_storage());
3050    }
3051
3052    #[test]
3053    fn test_parse_region_cleanup_from_proto() {
3054        let clean_up = api::v1::region::CleanUpRequest {
3055            region_id: RegionId::new(42, 3).as_u64(),
3056            engine: "mito".to_string(),
3057            path: "test".to_string(),
3058            options: HashMap::from([("k".to_string(), "v".to_string())]),
3059        };
3060
3061        let requests =
3062            RegionRequest::try_from_request_body(region_request::Body::CleanUp(clean_up)).unwrap();
3063        let RegionRequest::CleanUp(request) = &requests[0].1 else {
3064            unreachable!()
3065        };
3066
3067        assert_eq!(requests[0].0, RegionId::new(42, 3));
3068        assert_eq!(request.engine, "mito");
3069        assert_eq!(request.table_dir, "data/test/42/");
3070        assert_eq!(request.path_type, PathType::Bare);
3071        assert_eq!(request.options.get("k"), Some(&"v".to_string()));
3072    }
3073
3074    #[test]
3075    fn test_validate_modify_column_fulltext_options() {
3076        let kind = AlterKind::SetIndexes {
3077            options: vec![SetIndexOption::Fulltext {
3078                column_name: "tag_0".to_string(),
3079                options: FulltextOptions::new_unchecked(
3080                    true,
3081                    FulltextAnalyzer::Chinese,
3082                    false,
3083                    FulltextBackend::Bloom,
3084                    1000,
3085                    0.01,
3086                ),
3087            }],
3088        };
3089        let request = RegionAlterRequest { kind };
3090        let mut metadata = new_metadata();
3091        metadata.schema_version = 1;
3092        request.validate(&metadata).unwrap();
3093
3094        let kind = AlterKind::UnsetIndexes {
3095            options: vec![UnsetIndexOption::Fulltext {
3096                column_name: "tag_0".to_string(),
3097            }],
3098        };
3099        let request = RegionAlterRequest { kind };
3100        let mut metadata = new_metadata();
3101        metadata.schema_version = 1;
3102        request.validate(&metadata).unwrap();
3103    }
3104
3105    #[test]
3106    fn test_validate_sync_columns() {
3107        let metadata = new_metadata();
3108        let kind = AlterKind::SyncColumns {
3109            column_metadatas: vec![
3110                ColumnMetadata {
3111                    column_schema: ColumnSchema::new(
3112                        "tag_1",
3113                        ConcreteDataType::string_datatype(),
3114                        true,
3115                    ),
3116                    semantic_type: SemanticType::Tag,
3117                    column_id: 5,
3118                },
3119                ColumnMetadata {
3120                    column_schema: ColumnSchema::new(
3121                        "field_2",
3122                        ConcreteDataType::string_datatype(),
3123                        true,
3124                    ),
3125                    semantic_type: SemanticType::Field,
3126                    column_id: 6,
3127                },
3128            ],
3129        };
3130        let err = kind.validate(&metadata).unwrap_err();
3131        assert!(err.to_string().contains("not a primary key"));
3132
3133        // Change the timestamp column name.
3134        let mut column_metadatas_with_different_ts_column = metadata.column_metadatas.clone();
3135        let ts_column = column_metadatas_with_different_ts_column
3136            .iter_mut()
3137            .find(|c| c.semantic_type == SemanticType::Timestamp)
3138            .unwrap();
3139        ts_column.column_schema.name = "ts1".to_string();
3140
3141        let kind = AlterKind::SyncColumns {
3142            column_metadatas: column_metadatas_with_different_ts_column,
3143        };
3144        let err = kind.validate(&metadata).unwrap_err();
3145        assert!(
3146            err.to_string()
3147                .contains("timestamp column ts has different id")
3148        );
3149
3150        // Change the primary key column name.
3151        let mut column_metadatas_with_different_pk_column = metadata.column_metadatas.clone();
3152        let pk_column = column_metadatas_with_different_pk_column
3153            .iter_mut()
3154            .find(|c| c.column_schema.name == "tag_0")
3155            .unwrap();
3156        pk_column.column_id = 100;
3157        let kind = AlterKind::SyncColumns {
3158            column_metadatas: column_metadatas_with_different_pk_column,
3159        };
3160        let err = kind.validate(&metadata).unwrap_err();
3161        assert!(
3162            err.to_string()
3163                .contains("column with same name tag_0 has different id")
3164        );
3165
3166        // Add a new field column.
3167        let mut column_metadatas_with_new_field_column = metadata.column_metadatas.clone();
3168        column_metadatas_with_new_field_column.push(ColumnMetadata {
3169            column_schema: ColumnSchema::new("field_2", ConcreteDataType::string_datatype(), true),
3170            semantic_type: SemanticType::Field,
3171            column_id: 4,
3172        });
3173        let kind = AlterKind::SyncColumns {
3174            column_metadatas: column_metadatas_with_new_field_column,
3175        };
3176        kind.validate(&metadata).unwrap();
3177    }
3178
3179    #[test]
3180    fn test_cast_path_type_to_primitive() {
3181        assert_eq!(PathType::Bare as u8, 0);
3182        assert_eq!(PathType::Data as u8, 1);
3183        assert_eq!(PathType::Metadata as u8, 2);
3184        assert_eq!(
3185            PathType::try_from(PathType::Bare as u8).unwrap(),
3186            PathType::Bare
3187        );
3188        assert_eq!(
3189            PathType::try_from(PathType::Data as u8).unwrap(),
3190            PathType::Data
3191        );
3192        assert_eq!(
3193            PathType::try_from(PathType::Metadata as u8).unwrap(),
3194            PathType::Metadata
3195        );
3196    }
3197}