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