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    /// The hint for replaying memtable.
1933    pub location_id: Option<u64>,
1934    /// Replay checkpoint.
1935    pub checkpoint: Option<ReplayCheckpoint>,
1936}
1937
1938#[derive(Debug, Clone)]
1939pub struct RegionBulkInsertsRequest {
1940    /// Whether this request should skip WAL.
1941    pub skip_wal: bool,
1942    pub region_id: RegionId,
1943    pub payload: DfRecordBatch,
1944    pub raw_data: ArrowIpc,
1945    pub partition_expr_version: Option<u64>,
1946    pub aligned_schema_version: Option<u64>,
1947}
1948
1949impl RegionBulkInsertsRequest {
1950    pub fn estimated_size(&self) -> usize {
1951        self.payload.get_array_memory_size()
1952    }
1953}
1954
1955/// Request to stage a region with a new partition directive.
1956///
1957/// This request transitions a region into the staging mode.
1958/// It first flushes the memtable for the old partition expression if it is not
1959/// empty, then enters the staging mode with the new directive.
1960#[derive(Debug, Clone, PartialEq, Eq)]
1961pub enum StagingPartitionDirective {
1962    UpdatePartitionExpr(String),
1963    RejectAllWrites,
1964}
1965
1966impl StagingPartitionDirective {
1967    /// Returns the partition expression carried by this directive, if any.
1968    pub fn partition_expr(&self) -> Option<&str> {
1969        match self {
1970            Self::UpdatePartitionExpr(expr) => Some(expr),
1971            Self::RejectAllWrites => None,
1972        }
1973    }
1974}
1975
1976#[derive(Debug, Clone)]
1977pub struct EnterStagingRequest {
1978    /// The staging partition directive of the region.
1979    pub partition_directive: StagingPartitionDirective,
1980}
1981
1982impl EnterStagingRequest {
1983    /// Builds an enter-staging request with a partition expression directive.
1984    pub fn with_partition_expr(partition_expr: String) -> Self {
1985        Self {
1986            partition_directive: StagingPartitionDirective::UpdatePartitionExpr(partition_expr),
1987        }
1988    }
1989}
1990
1991/// This request is used as part of the region repartition.
1992///
1993/// After a region has entered staging mode with a new partition expression
1994/// expression) and a separate process (for example, `remap_manifests`) has
1995/// generated the new file assignments for the staging region, this request
1996/// applies that generated manifest to the region.
1997///
1998/// In practice, this means:
1999/// - The `partition_expr` identifies the staging partition expression that the manifest
2000///   was generated for.
2001/// - `central_region_id` specifies which region holds the staging blob storage
2002///   where the manifest was written during the `remap_manifests` operation.
2003/// - `manifest_path` is the relative path within the central region's staging
2004///   blob storage to fetch the generated manifest.
2005///
2006/// It should typically be called **after** the staging region has been
2007/// initialized by [`EnterStagingRequest`] and the new file layout has been
2008/// computed, to finalize the repartition operation.
2009#[derive(Debug, Clone)]
2010pub struct ApplyStagingManifestRequest {
2011    /// The partition expression of the staging region.
2012    pub partition_expr: String,
2013    /// The region that stores the staging manifests in its staging blob storage.
2014    pub central_region_id: RegionId,
2015    /// The relative path to the staging manifest within the central region's
2016    /// staging blob storage.
2017    pub manifest_path: String,
2018}
2019
2020impl fmt::Display for RegionRequest {
2021    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2022        match self {
2023            RegionRequest::Put(_) => write!(f, "Put"),
2024            RegionRequest::Delete(_) => write!(f, "Delete"),
2025            RegionRequest::Create(_) => write!(f, "Create"),
2026            RegionRequest::Drop(_) => write!(f, "Drop"),
2027            RegionRequest::Open(_) => write!(f, "Open"),
2028            RegionRequest::CleanUp(_) => write!(f, "CleanUp"),
2029            RegionRequest::Close(_) => write!(f, "Close"),
2030            RegionRequest::Alter(_) => write!(f, "Alter"),
2031            RegionRequest::Flush(_) => write!(f, "Flush"),
2032            RegionRequest::Compact(_) => write!(f, "Compact"),
2033            RegionRequest::BuildIndex(_) => write!(f, "BuildIndex"),
2034            RegionRequest::Truncate(_) => write!(f, "Truncate"),
2035            RegionRequest::Catchup(_) => write!(f, "Catchup"),
2036            RegionRequest::BulkInserts(_) => write!(f, "BulkInserts"),
2037            RegionRequest::EnterStaging(_) => write!(f, "EnterStaging"),
2038            RegionRequest::ApplyStagingManifest(_) => write!(f, "ApplyStagingManifest"),
2039        }
2040    }
2041}
2042
2043#[cfg(test)]
2044mod tests {
2045
2046    use api::v1::region::RegionColumnDef;
2047    use api::v1::{ColumnDataType, ColumnDef};
2048    use common_time::range::TimestampRange;
2049    use datatypes::extension::json::{Json2ExtensionType, JsonMetadata};
2050    use datatypes::json::JsonSettings;
2051    use datatypes::prelude::ConcreteDataType;
2052    use datatypes::schema::{ColumnSchema, FulltextAnalyzer, FulltextBackend};
2053    use datatypes::types::JsonType;
2054
2055    use super::*;
2056    use crate::metadata::RegionMetadataBuilder;
2057
2058    #[test]
2059    fn test_build_index_options_round_trip() {
2060        use prost::Message;
2061
2062        for options in [
2063            None,
2064            Some(build_index_request::Options::SstIndex(Default::default())),
2065            Some(build_index_request::Options::SeriesIndex(Default::default())),
2066        ] {
2067            let request = BuildIndexRequest {
2068                region_id: 42,
2069                options,
2070            };
2071            let decoded = BuildIndexRequest::decode(request.encode_to_vec().as_slice()).unwrap();
2072            let requests = make_region_build_index(decoded).unwrap();
2073            assert_eq!(1, requests.len());
2074            assert_eq!(RegionId::from_u64(42), requests[0].0);
2075            let RegionRequest::BuildIndex(request) = &requests[0].1 else {
2076                panic!("expected build-index request");
2077            };
2078            assert_eq!(options, request.options);
2079        }
2080        // The legacy wire message contains only region_id (field 1).
2081        let legacy = BuildIndexRequest::decode(&[0x08, 42][..]).unwrap();
2082        assert!(legacy.options.is_none());
2083    }
2084
2085    #[test]
2086    fn test_make_region_puts_preserves_skip_wal() {
2087        let region_id = RegionId::new(42, 3);
2088        let rows = Rows::default();
2089        let requests = make_region_puts(InsertRequests {
2090            requests: [false, true, false]
2091                .into_iter()
2092                .map(|skip_wal| api::v1::region::InsertRequest {
2093                    region_id: region_id.as_u64(),
2094                    rows: Some(rows.clone()),
2095                    partition_expr_version: Some(api::v1::PartitionExprVersion { value: 7 }),
2096                    skip_wal,
2097                })
2098                .collect(),
2099        })
2100        .unwrap();
2101
2102        assert_eq!(3, requests.len());
2103        for ((id, request), skip_wal) in requests.into_iter().zip([false, true, false]) {
2104            assert_eq!(region_id, id);
2105            let RegionRequest::Put(request) = request else {
2106                panic!("expected a put request");
2107            };
2108            assert_eq!(rows, request.rows);
2109            assert_eq!(skip_wal, request.skip_wal);
2110            assert_eq!(Some(7), request.partition_expr_version);
2111            assert!(request.hint.is_none());
2112        }
2113    }
2114
2115    #[test]
2116    fn test_make_region_compact_with_time_range() {
2117        let requests = make_region_compact(CompactRequest {
2118            region_id: 42,
2119            time_range: Some(api::v1::region::CompactionTimeRange {
2120                start: 1_000,
2121                end: 2_000,
2122                time_unit: api::v1::TimeUnit::Microsecond as i32,
2123            }),
2124            ..Default::default()
2125        })
2126        .unwrap();
2127
2128        let RegionRequest::Compact(request) = &requests[0].1 else {
2129            unreachable!();
2130        };
2131        assert_eq!(
2132            Some(
2133                TimestampRange::new(
2134                    Timestamp::new_microsecond(1_000),
2135                    Timestamp::new_microsecond(2_000),
2136                )
2137                .unwrap()
2138            ),
2139            request.time_range
2140        );
2141    }
2142
2143    #[test]
2144    fn test_make_region_truncate_unflushed() {
2145        let region_id = RegionId::new(42, 3);
2146        let requests =
2147            RegionRequest::try_from_request_body(region_request::Body::Truncate(TruncateRequest {
2148                region_id: region_id.as_u64(),
2149                kind: Some(truncate_request::Kind::Unflushed(
2150                    api::v1::region::Unflushed {},
2151                )),
2152            }))
2153            .unwrap();
2154
2155        assert_eq!(region_id, requests[0].0);
2156        assert!(matches!(
2157            requests[0].1,
2158            RegionRequest::Truncate(RegionTruncateRequest::Unflushed)
2159        ));
2160    }
2161
2162    #[test]
2163    fn test_make_region_truncate_requires_kind() {
2164        let error =
2165            RegionRequest::try_from_request_body(region_request::Body::Truncate(TruncateRequest {
2166                region_id: RegionId::new(42, 3).as_u64(),
2167                kind: None,
2168            }))
2169            .unwrap_err();
2170
2171        assert!(
2172            error
2173                .to_string()
2174                .contains("missing kind in TruncateRequest")
2175        );
2176    }
2177
2178    #[test]
2179    fn test_from_proto_location() {
2180        let proto_location = v1::AddColumnLocation {
2181            location_type: LocationType::First as i32,
2182            after_column_name: String::default(),
2183        };
2184        let location = AddColumnLocation::try_from(proto_location).unwrap();
2185        assert_eq!(location, AddColumnLocation::First);
2186
2187        let proto_location = v1::AddColumnLocation {
2188            location_type: 10,
2189            after_column_name: String::default(),
2190        };
2191        AddColumnLocation::try_from(proto_location).unwrap_err();
2192
2193        let proto_location = v1::AddColumnLocation {
2194            location_type: LocationType::After as i32,
2195            after_column_name: "a".to_string(),
2196        };
2197        let location = AddColumnLocation::try_from(proto_location).unwrap();
2198        assert_eq!(
2199            location,
2200            AddColumnLocation::After {
2201                column_name: "a".to_string()
2202            }
2203        );
2204    }
2205
2206    #[test]
2207    fn test_from_none_proto_add_column() {
2208        AddColumn::try_from(v1::region::AddColumn {
2209            column_def: None,
2210            location: None,
2211        })
2212        .unwrap_err();
2213    }
2214
2215    #[test]
2216    fn test_set_region_option_auto_flush_interval_try_from() {
2217        use std::time::Duration;
2218
2219        // Valid duration
2220        let pb = PbOption {
2221            key: "auto_flush_interval".to_string(),
2222            value: "5m".to_string(),
2223        };
2224        let opt = SetRegionOption::try_from(&pb).unwrap();
2225        assert_eq!(
2226            opt,
2227            SetRegionOption::AutoFlushInterval(Some(Duration::from_secs(300)))
2228        );
2229
2230        // Empty value clears the override (mirrors Ttl's behaviour for `SET key = NULL`).
2231        let pb = PbOption {
2232            key: "auto_flush_interval".to_string(),
2233            value: String::new(),
2234        };
2235        let opt = SetRegionOption::try_from(&pb).unwrap();
2236        assert_eq!(opt, SetRegionOption::AutoFlushInterval(None));
2237
2238        // Zero is rejected up front (engine invariant).
2239        let pb = PbOption {
2240            key: "auto_flush_interval".to_string(),
2241            value: "0s".to_string(),
2242        };
2243        assert!(SetRegionOption::try_from(&pb).is_err());
2244
2245        // Garbage value is rejected.
2246        let pb = PbOption {
2247            key: "auto_flush_interval".to_string(),
2248            value: "not_a_duration".to_string(),
2249        };
2250        assert!(SetRegionOption::try_from(&pb).is_err());
2251    }
2252
2253    #[test]
2254    fn test_set_region_option_skip_wal_try_from() {
2255        let pb = PbOption {
2256            key: SKIP_WAL_KEY.to_string(),
2257            value: "true".to_string(),
2258        };
2259        assert_eq!(
2260            SetRegionOption::SkipWal(true),
2261            SetRegionOption::try_from(&pb).unwrap()
2262        );
2263
2264        let pb = PbOption {
2265            key: SKIP_WAL_KEY.to_string(),
2266            value: "false".to_string(),
2267        };
2268        assert_eq!(
2269            SetRegionOption::SkipWal(false),
2270            SetRegionOption::try_from(&pb).unwrap()
2271        );
2272
2273        for value in ["", "invalid"] {
2274            let pb = PbOption {
2275                key: SKIP_WAL_KEY.to_string(),
2276                value: value.to_string(),
2277            };
2278            assert!(SetRegionOption::try_from(&pb).is_err());
2279        }
2280
2281        assert!(UnsetRegionOption::try_from(SKIP_WAL_KEY).is_err());
2282    }
2283
2284    #[test]
2285    fn test_set_region_option_skip_wal_serde_compatibility() {
2286        let legacy = serde_json::from_str::<SetRegionOption>(r#""SkipWal""#).unwrap();
2287        assert_eq!(SetRegionOption::SkipWal(true), legacy);
2288
2289        check_set_region_option_skip_wal_serde_compatibility(false);
2290        check_set_region_option_skip_wal_serde_compatibility(true);
2291    }
2292
2293    fn check_set_region_option_skip_wal_serde_compatibility(skip_wal: bool) {
2294        let option = SetRegionOption::SkipWal(skip_wal);
2295        let serialized = serde_json::to_string(&option).unwrap();
2296        let expected = if skip_wal {
2297            r#""SkipWal""#
2298        } else {
2299            r#"{"SkipWal":false}"#
2300        };
2301        assert_eq!(expected, serialized);
2302        assert_eq!(
2303            option,
2304            serde_json::from_str::<SetRegionOption>(&serialized).unwrap()
2305        );
2306        if skip_wal {
2307            assert!(serde_json::from_str::<LegacySetRegionOption>(&serialized).is_ok());
2308            assert_eq!(
2309                option,
2310                serde_json::from_str::<SetRegionOption>(r#"{"SkipWal":true}"#).unwrap()
2311            );
2312        }
2313    }
2314
2315    #[test]
2316    fn test_set_region_option_max_row_group_row_count_try_from() {
2317        let pb = PbOption {
2318            key: MAX_ROW_GROUP_ROW_COUNT.to_string(),
2319            value: "512".to_string(),
2320        };
2321        assert_eq!(
2322            SetRegionOption::MaxRowGroupRowCount(Some(512)),
2323            SetRegionOption::try_from(&pb).unwrap()
2324        );
2325
2326        let pb = PbOption {
2327            key: MAX_ROW_GROUP_ROW_COUNT.to_string(),
2328            value: String::new(),
2329        };
2330        assert_eq!(
2331            SetRegionOption::MaxRowGroupRowCount(None),
2332            SetRegionOption::try_from(&pb).unwrap()
2333        );
2334
2335        for value in [
2336            "0".to_string(),
2337            (MAX_ROW_GROUP_ROW_COUNT_LIMIT + 1).to_string(),
2338            "invalid".to_string(),
2339        ] {
2340            let pb = PbOption {
2341                key: MAX_ROW_GROUP_ROW_COUNT.to_string(),
2342                value,
2343            };
2344            assert!(SetRegionOption::try_from(&pb).is_err());
2345        }
2346
2347        assert_eq!(
2348            UnsetRegionOption::MaxRowGroupRowCount,
2349            UnsetRegionOption::try_from(MAX_ROW_GROUP_ROW_COUNT).unwrap()
2350        );
2351    }
2352
2353    #[test]
2354    fn test_set_region_option_preserve_row_sequence_try_from() {
2355        for (value, expected) in [("true", true), ("false", false)] {
2356            let pb = PbOption {
2357                key: PRESERVE_ROW_SEQUENCE.to_string(),
2358                value: value.to_string(),
2359            };
2360            assert_eq!(
2361                SetRegionOption::PreserveRowSequence(expected),
2362                SetRegionOption::try_from(&pb).unwrap()
2363            );
2364        }
2365
2366        for value in ["1", "invalid"] {
2367            let pb = PbOption {
2368                key: PRESERVE_ROW_SEQUENCE.to_string(),
2369                value: value.to_string(),
2370            };
2371            assert!(SetRegionOption::try_from(&pb).is_err());
2372        }
2373
2374        assert_eq!(
2375            UnsetRegionOption::PreserveRowSequence,
2376            UnsetRegionOption::try_from(PRESERVE_ROW_SEQUENCE).unwrap()
2377        );
2378        assert_eq!(
2379            SetRegionOption::PreserveRowSequence(false),
2380            (&UnsetRegionOption::PreserveRowSequence).into()
2381        );
2382    }
2383
2384    #[test]
2385    fn test_set_twcs_window_trigger_options_try_from() {
2386        for key in [
2387            "compaction.twcs.active_window.trigger_file_num",
2388            "compaction.twcs.active_window.l1_merge_trigger",
2389            "compaction.twcs.inactive_window.trigger_file_num",
2390            "compaction.twcs.inactive_window.l1_merge_trigger",
2391        ] {
2392            let option = PbOption {
2393                key: key.to_string(),
2394                value: "8".to_string(),
2395            };
2396            assert_eq!(
2397                SetRegionOption::Twsc(key.to_string(), "8".to_string()),
2398                SetRegionOption::try_from(&option).unwrap(),
2399                "{key}"
2400            );
2401        }
2402    }
2403
2404    #[test]
2405    fn test_unset_twcs_window_trigger_options_try_from() {
2406        for (key, expected) in [
2407            (
2408                "compaction.twcs.active_window.trigger_file_num",
2409                UnsetRegionOption::TwcsActiveWindowTriggerFileNum,
2410            ),
2411            (
2412                "compaction.twcs.active_window.l1_merge_trigger",
2413                UnsetRegionOption::TwcsActiveWindowL1MergeTrigger,
2414            ),
2415            (
2416                "compaction.twcs.inactive_window.trigger_file_num",
2417                UnsetRegionOption::TwcsInactiveWindowTriggerFileNum,
2418            ),
2419        ] {
2420            assert_eq!(expected, UnsetRegionOption::try_from(key).unwrap());
2421            assert_eq!(
2422                SetRegionOption::Twsc(key.to_string(), String::new()),
2423                SetRegionOption::from(&expected)
2424            );
2425        }
2426
2427        let key = "compaction.twcs.inactive_window.l1_merge_trigger";
2428        let option = UnsetRegionOption::try_from(key).unwrap();
2429        assert_eq!(key, option.to_string());
2430        assert_eq!(
2431            SetRegionOption::Twsc(key.to_string(), String::new()),
2432            SetRegionOption::from(&option)
2433        );
2434    }
2435
2436    #[test]
2437    fn test_from_proto_alter_request() {
2438        RegionAlterRequest::try_from(AlterRequest {
2439            region_id: 0,
2440            schema_version: 1,
2441            kind: None,
2442        })
2443        .unwrap_err();
2444
2445        let request = RegionAlterRequest::try_from(AlterRequest {
2446            region_id: 0,
2447            schema_version: 1,
2448            kind: Some(alter_request::Kind::AddColumns(v1::region::AddColumns {
2449                add_columns: vec![v1::region::AddColumn {
2450                    column_def: Some(RegionColumnDef {
2451                        column_def: Some(ColumnDef {
2452                            name: "a".to_string(),
2453                            data_type: ColumnDataType::String as i32,
2454                            is_nullable: true,
2455                            default_constraint: vec![],
2456                            semantic_type: SemanticType::Field as i32,
2457                            comment: String::new(),
2458                            ..Default::default()
2459                        }),
2460                        column_id: 1,
2461                    }),
2462                    location: Some(v1::AddColumnLocation {
2463                        location_type: LocationType::First as i32,
2464                        after_column_name: String::default(),
2465                    }),
2466                }],
2467            })),
2468        })
2469        .unwrap();
2470
2471        assert_eq!(
2472            request,
2473            RegionAlterRequest {
2474                kind: AlterKind::AddColumns {
2475                    columns: vec![AddColumn {
2476                        column_metadata: ColumnMetadata {
2477                            column_schema: ColumnSchema::new(
2478                                "a",
2479                                ConcreteDataType::string_datatype(),
2480                                true,
2481                            ),
2482                            semantic_type: SemanticType::Field,
2483                            column_id: 1,
2484                        },
2485                        location: Some(AddColumnLocation::First),
2486                    }]
2487                },
2488            }
2489        );
2490
2491        let request = RegionAlterRequest::try_from(AlterRequest {
2492            region_id: 0,
2493            schema_version: 1,
2494            kind: Some(alter_request::Kind::SetJsonSettings(v1::SetJsonSettings {
2495                column_name: "payload".to_string(),
2496                settings: Some(v1::JsonSettings {
2497                    type_hints: vec![v1::JsonTypeHint {
2498                        path: vec!["service".to_string()],
2499                        data_type: ColumnDataType::String as i32,
2500                        datatype_extension: None,
2501                    }],
2502                    max_auto_expanded_paths: Some(10),
2503                }),
2504            })),
2505        })
2506        .unwrap();
2507
2508        let AlterKind::SetJsonSettings {
2509            column_name,
2510            settings,
2511        } = request.kind
2512        else {
2513            unreachable!()
2514        };
2515        assert_eq!("payload", column_name);
2516        assert_eq!(Some(10), settings.max_auto_expanded_paths());
2517        assert_eq!(1, settings.type_hints().len());
2518    }
2519
2520    #[test]
2521    fn test_write_buffer_size_region_options() {
2522        let option = PbOption {
2523            key: WRITE_BUFFER_SIZE_KEY.to_string(),
2524            value: "128MiB".to_string(),
2525        };
2526        assert_eq!(
2527            SetRegionOption::WriteBufferSize(Some(ReadableSize::mb(128))),
2528            SetRegionOption::try_from(&option).unwrap()
2529        );
2530
2531        let option = PbOption {
2532            key: WRITE_BUFFER_SIZE_KEY.to_string(),
2533            value: "invalid".to_string(),
2534        };
2535        SetRegionOption::try_from(&option).unwrap_err();
2536
2537        // Clearing the option uses UNSET. Empty SET values, including SQL NULL,
2538        // remain invalid because the SQL layer does not distinguish NULL from ''.
2539        let option = PbOption {
2540            key: WRITE_BUFFER_SIZE_KEY.to_string(),
2541            value: String::new(),
2542        };
2543        SetRegionOption::try_from(&option).unwrap_err();
2544
2545        assert_eq!(
2546            UnsetRegionOption::WriteBufferSize,
2547            UnsetRegionOption::try_from(WRITE_BUFFER_SIZE_KEY).unwrap()
2548        );
2549        assert_eq!(
2550            SetRegionOption::WriteBufferSize(None),
2551            SetRegionOption::from(&UnsetRegionOption::WriteBufferSize)
2552        );
2553    }
2554
2555    /// Returns a new region metadata for testing. Metadata:
2556    /// `[(ts, ms, 1), (tag_0, string, 2), (field_0, string, 3), (field_1, bool, 4)]`
2557    fn new_metadata() -> RegionMetadata {
2558        let mut builder = RegionMetadataBuilder::new(RegionId::new(1, 1));
2559        builder
2560            .push_column_metadata(ColumnMetadata {
2561                column_schema: ColumnSchema::new(
2562                    "ts",
2563                    ConcreteDataType::timestamp_millisecond_datatype(),
2564                    false,
2565                ),
2566                semantic_type: SemanticType::Timestamp,
2567                column_id: 1,
2568            })
2569            .push_column_metadata(ColumnMetadata {
2570                column_schema: ColumnSchema::new(
2571                    "tag_0",
2572                    ConcreteDataType::string_datatype(),
2573                    true,
2574                ),
2575                semantic_type: SemanticType::Tag,
2576                column_id: 2,
2577            })
2578            .push_column_metadata(ColumnMetadata {
2579                column_schema: ColumnSchema::new(
2580                    "field_0",
2581                    ConcreteDataType::string_datatype(),
2582                    true,
2583                ),
2584                semantic_type: SemanticType::Field,
2585                column_id: 3,
2586            })
2587            .push_column_metadata(ColumnMetadata {
2588                column_schema: ColumnSchema::new(
2589                    "field_1",
2590                    ConcreteDataType::boolean_datatype(),
2591                    true,
2592                ),
2593                semantic_type: SemanticType::Field,
2594                column_id: 4,
2595            })
2596            .primary_key(vec![2]);
2597        builder.build().unwrap()
2598    }
2599
2600    fn json2_column_schema(name: &str, settings: JsonSettings) -> ColumnSchema {
2601        let mut column_schema =
2602            ColumnSchema::new(name, ConcreteDataType::Json(JsonType::null()), true);
2603        column_schema.with_extension_type(&Json2ExtensionType::new(std::sync::Arc::new(
2604            JsonMetadata::new(settings),
2605        )));
2606        column_schema
2607    }
2608
2609    #[test]
2610    fn test_add_column_validate() {
2611        let metadata = new_metadata();
2612        let add_column = AddColumn {
2613            column_metadata: ColumnMetadata {
2614                column_schema: ColumnSchema::new(
2615                    "tag_1",
2616                    ConcreteDataType::string_datatype(),
2617                    true,
2618                ),
2619                semantic_type: SemanticType::Tag,
2620                column_id: 5,
2621            },
2622            location: None,
2623        };
2624        add_column.validate(&metadata).unwrap();
2625        assert!(add_column.need_alter(&metadata));
2626
2627        // Add not null column.
2628        AddColumn {
2629            column_metadata: ColumnMetadata {
2630                column_schema: ColumnSchema::new(
2631                    "tag_1",
2632                    ConcreteDataType::string_datatype(),
2633                    false,
2634                ),
2635                semantic_type: SemanticType::Tag,
2636                column_id: 5,
2637            },
2638            location: None,
2639        }
2640        .validate(&metadata)
2641        .unwrap_err();
2642
2643        // Add existing column.
2644        let add_column = AddColumn {
2645            column_metadata: ColumnMetadata {
2646                column_schema: ColumnSchema::new(
2647                    "tag_0",
2648                    ConcreteDataType::string_datatype(),
2649                    true,
2650                ),
2651                semantic_type: SemanticType::Tag,
2652                column_id: 2,
2653            },
2654            location: None,
2655        };
2656        add_column.validate(&metadata).unwrap();
2657        assert!(!add_column.need_alter(&metadata));
2658    }
2659
2660    #[test]
2661    fn test_add_duplicate_columns() {
2662        let kind = AlterKind::AddColumns {
2663            columns: vec![
2664                AddColumn {
2665                    column_metadata: ColumnMetadata {
2666                        column_schema: ColumnSchema::new(
2667                            "tag_1",
2668                            ConcreteDataType::string_datatype(),
2669                            true,
2670                        ),
2671                        semantic_type: SemanticType::Tag,
2672                        column_id: 5,
2673                    },
2674                    location: None,
2675                },
2676                AddColumn {
2677                    column_metadata: ColumnMetadata {
2678                        column_schema: ColumnSchema::new(
2679                            "tag_1",
2680                            ConcreteDataType::string_datatype(),
2681                            true,
2682                        ),
2683                        semantic_type: SemanticType::Field,
2684                        column_id: 6,
2685                    },
2686                    location: None,
2687                },
2688            ],
2689        };
2690        let metadata = new_metadata();
2691        kind.validate(&metadata).unwrap();
2692        assert!(kind.need_alter(&metadata));
2693    }
2694
2695    #[test]
2696    fn test_add_existing_column_different_metadata() {
2697        let metadata = new_metadata();
2698
2699        // Add existing column with different id.
2700        let kind = AlterKind::AddColumns {
2701            columns: vec![AddColumn {
2702                column_metadata: ColumnMetadata {
2703                    column_schema: ColumnSchema::new(
2704                        "tag_0",
2705                        ConcreteDataType::string_datatype(),
2706                        true,
2707                    ),
2708                    semantic_type: SemanticType::Tag,
2709                    column_id: 4,
2710                },
2711                location: None,
2712            }],
2713        };
2714        kind.validate(&metadata).unwrap_err();
2715
2716        // Add existing column with different type.
2717        let kind = AlterKind::AddColumns {
2718            columns: vec![AddColumn {
2719                column_metadata: ColumnMetadata {
2720                    column_schema: ColumnSchema::new(
2721                        "tag_0",
2722                        ConcreteDataType::int64_datatype(),
2723                        true,
2724                    ),
2725                    semantic_type: SemanticType::Tag,
2726                    column_id: 2,
2727                },
2728                location: None,
2729            }],
2730        };
2731        kind.validate(&metadata).unwrap_err();
2732
2733        // Add existing column with different name.
2734        let kind = AlterKind::AddColumns {
2735            columns: vec![AddColumn {
2736                column_metadata: ColumnMetadata {
2737                    column_schema: ColumnSchema::new(
2738                        "tag_1",
2739                        ConcreteDataType::string_datatype(),
2740                        true,
2741                    ),
2742                    semantic_type: SemanticType::Tag,
2743                    column_id: 2,
2744                },
2745                location: None,
2746            }],
2747        };
2748        kind.validate(&metadata).unwrap_err();
2749    }
2750
2751    #[test]
2752    fn test_add_existing_column_with_location() {
2753        let metadata = new_metadata();
2754        let kind = AlterKind::AddColumns {
2755            columns: vec![AddColumn {
2756                column_metadata: ColumnMetadata {
2757                    column_schema: ColumnSchema::new(
2758                        "tag_0",
2759                        ConcreteDataType::string_datatype(),
2760                        true,
2761                    ),
2762                    semantic_type: SemanticType::Tag,
2763                    column_id: 2,
2764                },
2765                location: Some(AddColumnLocation::First),
2766            }],
2767        };
2768        kind.validate(&metadata).unwrap_err();
2769    }
2770
2771    #[test]
2772    fn test_validate_drop_column() {
2773        let metadata = new_metadata();
2774        let kind = AlterKind::DropColumns {
2775            names: vec!["xxxx".to_string()],
2776        };
2777        kind.validate(&metadata).unwrap();
2778        assert!(!kind.need_alter(&metadata));
2779
2780        AlterKind::DropColumns {
2781            names: vec!["tag_0".to_string()],
2782        }
2783        .validate(&metadata)
2784        .unwrap_err();
2785
2786        let kind = AlterKind::DropColumns {
2787            names: vec!["field_0".to_string()],
2788        };
2789        kind.validate(&metadata).unwrap();
2790        assert!(kind.need_alter(&metadata));
2791    }
2792
2793    #[test]
2794    fn test_validate_modify_column_type() {
2795        let metadata = new_metadata();
2796        AlterKind::ModifyColumnTypes {
2797            columns: vec![ModifyColumnType {
2798                column_name: "xxxx".to_string(),
2799                target_type: ConcreteDataType::string_datatype(),
2800            }],
2801        }
2802        .validate(&metadata)
2803        .unwrap_err();
2804
2805        AlterKind::ModifyColumnTypes {
2806            columns: vec![ModifyColumnType {
2807                column_name: "field_1".to_string(),
2808                target_type: ConcreteDataType::date_datatype(),
2809            }],
2810        }
2811        .validate(&metadata)
2812        .unwrap_err();
2813
2814        AlterKind::ModifyColumnTypes {
2815            columns: vec![ModifyColumnType {
2816                column_name: "ts".to_string(),
2817                target_type: ConcreteDataType::date_datatype(),
2818            }],
2819        }
2820        .validate(&metadata)
2821        .unwrap_err();
2822
2823        // Time index unit widening is allowed.
2824        let kind = AlterKind::ModifyColumnTypes {
2825            columns: vec![ModifyColumnType {
2826                column_name: "ts".to_string(),
2827                target_type: ConcreteDataType::timestamp_microsecond_datatype(),
2828            }],
2829        };
2830        kind.validate(&metadata).unwrap();
2831        assert!(kind.need_alter(&metadata));
2832
2833        // Narrowing the time index unit is rejected.
2834        let metadata_nano = {
2835            let mut metadata = new_metadata();
2836            for col in metadata.column_metadatas.iter_mut() {
2837                if col.column_schema.name == "ts" {
2838                    col.column_schema.data_type = ConcreteDataType::timestamp_nanosecond_datatype();
2839                }
2840            }
2841            metadata
2842        };
2843        AlterKind::ModifyColumnTypes {
2844            columns: vec![ModifyColumnType {
2845                column_name: "ts".to_string(),
2846                target_type: ConcreteDataType::timestamp_millisecond_datatype(),
2847            }],
2848        }
2849        .validate(&metadata_nano)
2850        .unwrap_err();
2851
2852        // Changing the time index to the same type is a validated no-op, so a
2853        // retried alter procedure (region already altered) succeeds and is
2854        // skipped by `need_alter`.
2855        let same_type = AlterKind::ModifyColumnTypes {
2856            columns: vec![ModifyColumnType {
2857                column_name: "ts".to_string(),
2858                target_type: ConcreteDataType::timestamp_millisecond_datatype(),
2859            }],
2860        };
2861        same_type.validate(&metadata).unwrap();
2862        assert!(!same_type.need_alter(&metadata));
2863
2864        // Changing the time index to a non-timestamp type is rejected.
2865        AlterKind::ModifyColumnTypes {
2866            columns: vec![ModifyColumnType {
2867                column_name: "ts".to_string(),
2868                target_type: ConcreteDataType::string_datatype(),
2869            }],
2870        }
2871        .validate(&metadata)
2872        .unwrap_err();
2873
2874        AlterKind::ModifyColumnTypes {
2875            columns: vec![ModifyColumnType {
2876                column_name: "tag_0".to_string(),
2877                target_type: ConcreteDataType::date_datatype(),
2878            }],
2879        }
2880        .validate(&metadata)
2881        .unwrap_err();
2882
2883        let kind = AlterKind::ModifyColumnTypes {
2884            columns: vec![ModifyColumnType {
2885                column_name: "field_0".to_string(),
2886                target_type: ConcreteDataType::int32_datatype(),
2887            }],
2888        };
2889        kind.validate(&metadata).unwrap();
2890        assert!(kind.need_alter(&metadata));
2891    }
2892
2893    #[test]
2894    fn test_validate_set_json_settings() {
2895        let mut metadata = new_metadata();
2896        let current_schema = json2_column_schema("field_0", JsonSettings::new_v2());
2897        let target_settings = JsonSettings::try_new(vec![], Some(10)).unwrap();
2898        metadata
2899            .column_metadatas
2900            .iter_mut()
2901            .find(|column| column.column_schema.name == "field_0")
2902            .unwrap()
2903            .column_schema = current_schema.clone();
2904
2905        let kind = AlterKind::SetJsonSettings {
2906            column_name: "field_0".to_string(),
2907            settings: target_settings.clone(),
2908        };
2909        kind.validate(&metadata).unwrap();
2910        assert!(kind.need_alter(&metadata));
2911
2912        let no_op = AlterKind::SetJsonSettings {
2913            column_name: "field_0".to_string(),
2914            settings: JsonSettings::new_v2(),
2915        };
2916        no_op.validate(&metadata).unwrap();
2917        assert!(!no_op.need_alter(&metadata));
2918
2919        AlterKind::SetJsonSettings {
2920            column_name: "tag_0".to_string(),
2921            settings: target_settings,
2922        }
2923        .validate(&metadata)
2924        .unwrap_err();
2925    }
2926
2927    #[test]
2928    fn test_validate_add_columns() {
2929        let kind = AlterKind::AddColumns {
2930            columns: vec![
2931                AddColumn {
2932                    column_metadata: ColumnMetadata {
2933                        column_schema: ColumnSchema::new(
2934                            "tag_1",
2935                            ConcreteDataType::string_datatype(),
2936                            true,
2937                        ),
2938                        semantic_type: SemanticType::Tag,
2939                        column_id: 5,
2940                    },
2941                    location: None,
2942                },
2943                AddColumn {
2944                    column_metadata: ColumnMetadata {
2945                        column_schema: ColumnSchema::new(
2946                            "field_2",
2947                            ConcreteDataType::string_datatype(),
2948                            true,
2949                        ),
2950                        semantic_type: SemanticType::Field,
2951                        column_id: 6,
2952                    },
2953                    location: None,
2954                },
2955            ],
2956        };
2957        let request = RegionAlterRequest { kind };
2958        let mut metadata = new_metadata();
2959        metadata.schema_version = 1;
2960        request.validate(&metadata).unwrap();
2961    }
2962
2963    #[test]
2964    fn test_validate_create_region() {
2965        let column_metadatas = vec![
2966            ColumnMetadata {
2967                column_schema: ColumnSchema::new(
2968                    "ts",
2969                    ConcreteDataType::timestamp_millisecond_datatype(),
2970                    false,
2971                ),
2972                semantic_type: SemanticType::Timestamp,
2973                column_id: 1,
2974            },
2975            ColumnMetadata {
2976                column_schema: ColumnSchema::new(
2977                    "tag_0",
2978                    ConcreteDataType::string_datatype(),
2979                    true,
2980                ),
2981                semantic_type: SemanticType::Tag,
2982                column_id: 2,
2983            },
2984            ColumnMetadata {
2985                column_schema: ColumnSchema::new(
2986                    "field_0",
2987                    ConcreteDataType::string_datatype(),
2988                    true,
2989                ),
2990                semantic_type: SemanticType::Field,
2991                column_id: 3,
2992            },
2993        ];
2994        let create = RegionCreateRequest {
2995            engine: "mito".to_string(),
2996            column_metadatas,
2997            primary_key: vec![3, 4],
2998            options: HashMap::new(),
2999            table_dir: "path".to_string(),
3000            path_type: PathType::Bare,
3001            partition_expr_json: Some("".to_string()),
3002            requirements: Default::default(),
3003        };
3004
3005        assert!(create.validate().is_err());
3006    }
3007
3008    #[test]
3009    fn test_parse_create_region_requirements_defaults_to_empty() {
3010        let create = CreateRequest {
3011            region_id: RegionId::new(42, 0).as_u64(),
3012            engine: "mito".to_string(),
3013            column_defs: vec![],
3014            primary_key: vec![],
3015            path: "test".to_string(),
3016            options: HashMap::new(),
3017            partition: None,
3018            requirements: None,
3019        };
3020
3021        let requests =
3022            RegionRequest::try_from_request_body(region_request::Body::Create(create)).unwrap();
3023        let RegionRequest::Create(request) = &requests[0].1 else {
3024            unreachable!()
3025        };
3026
3027        assert_eq!(request.requirements, RegionRequirements::empty());
3028    }
3029
3030    #[test]
3031    fn test_parse_create_region_requirements_from_proto() {
3032        let create = CreateRequest {
3033            region_id: RegionId::new(42, 0).as_u64(),
3034            engine: "mito".to_string(),
3035            column_defs: vec![],
3036            primary_key: vec![],
3037            path: "test".to_string(),
3038            options: HashMap::new(),
3039            partition: None,
3040            requirements: Some(api::v1::region::RegionRequirements {
3041                object_storage: true,
3042            }),
3043        };
3044
3045        let requests =
3046            RegionRequest::try_from_request_body(region_request::Body::Create(create)).unwrap();
3047        let RegionRequest::Create(request) = &requests[0].1 else {
3048            unreachable!()
3049        };
3050
3051        assert_eq!(request.requirements, RegionRequirements::object_storage());
3052    }
3053
3054    #[test]
3055    fn test_parse_region_cleanup_from_proto() {
3056        let clean_up = api::v1::region::CleanUpRequest {
3057            region_id: RegionId::new(42, 3).as_u64(),
3058            engine: "mito".to_string(),
3059            path: "test".to_string(),
3060            options: HashMap::from([("k".to_string(), "v".to_string())]),
3061        };
3062
3063        let requests =
3064            RegionRequest::try_from_request_body(region_request::Body::CleanUp(clean_up)).unwrap();
3065        let RegionRequest::CleanUp(request) = &requests[0].1 else {
3066            unreachable!()
3067        };
3068
3069        assert_eq!(requests[0].0, RegionId::new(42, 3));
3070        assert_eq!(request.engine, "mito");
3071        assert_eq!(request.table_dir, "data/test/42/");
3072        assert_eq!(request.path_type, PathType::Bare);
3073        assert_eq!(request.options.get("k"), Some(&"v".to_string()));
3074    }
3075
3076    #[test]
3077    fn test_validate_modify_column_fulltext_options() {
3078        let kind = AlterKind::SetIndexes {
3079            options: vec![SetIndexOption::Fulltext {
3080                column_name: "tag_0".to_string(),
3081                options: FulltextOptions::new_unchecked(
3082                    true,
3083                    FulltextAnalyzer::Chinese,
3084                    false,
3085                    FulltextBackend::Bloom,
3086                    1000,
3087                    0.01,
3088                ),
3089            }],
3090        };
3091        let request = RegionAlterRequest { kind };
3092        let mut metadata = new_metadata();
3093        metadata.schema_version = 1;
3094        request.validate(&metadata).unwrap();
3095
3096        let kind = AlterKind::UnsetIndexes {
3097            options: vec![UnsetIndexOption::Fulltext {
3098                column_name: "tag_0".to_string(),
3099            }],
3100        };
3101        let request = RegionAlterRequest { kind };
3102        let mut metadata = new_metadata();
3103        metadata.schema_version = 1;
3104        request.validate(&metadata).unwrap();
3105    }
3106
3107    #[test]
3108    fn test_validate_sync_columns() {
3109        let metadata = new_metadata();
3110        let kind = AlterKind::SyncColumns {
3111            column_metadatas: vec![
3112                ColumnMetadata {
3113                    column_schema: ColumnSchema::new(
3114                        "tag_1",
3115                        ConcreteDataType::string_datatype(),
3116                        true,
3117                    ),
3118                    semantic_type: SemanticType::Tag,
3119                    column_id: 5,
3120                },
3121                ColumnMetadata {
3122                    column_schema: ColumnSchema::new(
3123                        "field_2",
3124                        ConcreteDataType::string_datatype(),
3125                        true,
3126                    ),
3127                    semantic_type: SemanticType::Field,
3128                    column_id: 6,
3129                },
3130            ],
3131        };
3132        let err = kind.validate(&metadata).unwrap_err();
3133        assert!(err.to_string().contains("not a primary key"));
3134
3135        // Change the timestamp column name.
3136        let mut column_metadatas_with_different_ts_column = metadata.column_metadatas.clone();
3137        let ts_column = column_metadatas_with_different_ts_column
3138            .iter_mut()
3139            .find(|c| c.semantic_type == SemanticType::Timestamp)
3140            .unwrap();
3141        ts_column.column_schema.name = "ts1".to_string();
3142
3143        let kind = AlterKind::SyncColumns {
3144            column_metadatas: column_metadatas_with_different_ts_column,
3145        };
3146        let err = kind.validate(&metadata).unwrap_err();
3147        assert!(
3148            err.to_string()
3149                .contains("timestamp column ts has different id")
3150        );
3151
3152        // Change the primary key column name.
3153        let mut column_metadatas_with_different_pk_column = metadata.column_metadatas.clone();
3154        let pk_column = column_metadatas_with_different_pk_column
3155            .iter_mut()
3156            .find(|c| c.column_schema.name == "tag_0")
3157            .unwrap();
3158        pk_column.column_id = 100;
3159        let kind = AlterKind::SyncColumns {
3160            column_metadatas: column_metadatas_with_different_pk_column,
3161        };
3162        let err = kind.validate(&metadata).unwrap_err();
3163        assert!(
3164            err.to_string()
3165                .contains("column with same name tag_0 has different id")
3166        );
3167
3168        // Add a new field column.
3169        let mut column_metadatas_with_new_field_column = metadata.column_metadatas.clone();
3170        column_metadatas_with_new_field_column.push(ColumnMetadata {
3171            column_schema: ColumnSchema::new("field_2", ConcreteDataType::string_datatype(), true),
3172            semantic_type: SemanticType::Field,
3173            column_id: 4,
3174        });
3175        let kind = AlterKind::SyncColumns {
3176            column_metadatas: column_metadatas_with_new_field_column,
3177        };
3178        kind.validate(&metadata).unwrap();
3179    }
3180
3181    #[test]
3182    fn test_cast_path_type_to_primitive() {
3183        assert_eq!(PathType::Bare as u8, 0);
3184        assert_eq!(PathType::Data as u8, 1);
3185        assert_eq!(PathType::Metadata as u8, 2);
3186        assert_eq!(
3187            PathType::try_from(PathType::Bare as u8).unwrap(),
3188            PathType::Bare
3189        );
3190        assert_eq!(
3191            PathType::try_from(PathType::Data as u8).unwrap(),
3192            PathType::Data
3193        );
3194        assert_eq!(
3195            PathType::try_from(PathType::Metadata as u8).unwrap(),
3196            PathType::Metadata
3197        );
3198    }
3199}