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                skipping_index: hint
1575                    .skipping_index
1576                    .map(api::v1::column_def::json_skipping_options_from_proto)
1577                    .transpose()
1578                    .map_err(|err| {
1579                        InvalidRawRegionRequestSnafu {
1580                            err: err.to_string(),
1581                        }
1582                        .build()
1583                    })?,
1584            })
1585        })
1586        .collect::<Result<Vec<_>>>()?;
1587
1588    JsonSettings::try_new(type_hints, settings.max_auto_expanded_paths).map_err(|err| {
1589        InvalidRawRegionRequestSnafu {
1590            err: err.to_string(),
1591        }
1592        .build()
1593    })
1594}
1595
1596/// Region option changes used by ALTER requests.
1597///
1598/// This type is serialized for request persistence. Keep future changes backward
1599/// compatible with previously serialized variants.
1600#[derive(Debug, Eq, PartialEq, Clone)]
1601pub enum SetRegionOption {
1602    WriteBufferSize(Option<ReadableSize>),
1603    Ttl(Option<TimeToLive>),
1604    // Modifying TwscOptions with values as (option name, new value).
1605    Twsc(String, String),
1606    // Modifying the SST format.
1607    Format(String),
1608    // Modifying the append mode.
1609    AppendMode(bool),
1610    // Modifying the per-region auto flush interval override.
1611    AutoFlushInterval(Option<Duration>),
1612    // Modifying the max number of rows in a parquet row group.
1613    MaxRowGroupRowCount(Option<usize>),
1614    PreserveRowSequence(bool),
1615    // Whether to skip writing new WAL entries.
1616    SkipWal(bool),
1617    /// Overrides per-output compaction parallelism; None restores the engine default.
1618    CompactionOutputParallelism(Option<usize>),
1619}
1620
1621#[derive(Serialize, Deserialize)]
1622enum SetRegionOptionSerde {
1623    WriteBufferSize(Option<ReadableSize>),
1624    Ttl(Option<TimeToLive>),
1625    Twsc(String, String),
1626    Format(String),
1627    AppendMode(bool),
1628    AutoFlushInterval(Option<Duration>),
1629    MaxRowGroupRowCount(Option<usize>),
1630    PreserveRowSequence(bool),
1631    SkipWal(bool),
1632    CompactionOutputParallelism(Option<usize>),
1633}
1634
1635impl Serialize for SetRegionOption {
1636    fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
1637    where
1638        S: Serializer,
1639    {
1640        // Older binaries deserialize the disable request as a unit variant.
1641        if matches!(self, Self::SkipWal(true)) {
1642            return serializer.serialize_unit_variant("SetRegionOption", 8, "SkipWal");
1643        }
1644
1645        let option = match self {
1646            Self::WriteBufferSize(value) => SetRegionOptionSerde::WriteBufferSize(*value),
1647            Self::Ttl(value) => SetRegionOptionSerde::Ttl(*value),
1648            Self::Twsc(key, value) => SetRegionOptionSerde::Twsc(key.clone(), value.clone()),
1649            Self::Format(value) => SetRegionOptionSerde::Format(value.clone()),
1650            Self::AppendMode(value) => SetRegionOptionSerde::AppendMode(*value),
1651            Self::AutoFlushInterval(value) => SetRegionOptionSerde::AutoFlushInterval(*value),
1652            Self::MaxRowGroupRowCount(value) => SetRegionOptionSerde::MaxRowGroupRowCount(*value),
1653            Self::PreserveRowSequence(value) => SetRegionOptionSerde::PreserveRowSequence(*value),
1654            Self::SkipWal(value) => SetRegionOptionSerde::SkipWal(*value),
1655            Self::CompactionOutputParallelism(value) => {
1656                SetRegionOptionSerde::CompactionOutputParallelism(*value)
1657            }
1658        };
1659        option.serialize(serializer)
1660    }
1661}
1662
1663#[derive(Deserialize)]
1664enum LegacySetRegionOption {
1665    SkipWal,
1666}
1667
1668#[derive(Deserialize)]
1669#[serde(untagged)]
1670enum BackwardCompatibleSetRegionOption {
1671    Current(SetRegionOptionSerde),
1672    Legacy(LegacySetRegionOption),
1673}
1674
1675impl From<SetRegionOptionSerde> for SetRegionOption {
1676    fn from(option: SetRegionOptionSerde) -> Self {
1677        match option {
1678            SetRegionOptionSerde::WriteBufferSize(value) => Self::WriteBufferSize(value),
1679            SetRegionOptionSerde::Ttl(value) => Self::Ttl(value),
1680            SetRegionOptionSerde::Twsc(key, value) => Self::Twsc(key, value),
1681            SetRegionOptionSerde::Format(value) => Self::Format(value),
1682            SetRegionOptionSerde::AppendMode(value) => Self::AppendMode(value),
1683            SetRegionOptionSerde::AutoFlushInterval(value) => Self::AutoFlushInterval(value),
1684            SetRegionOptionSerde::MaxRowGroupRowCount(value) => Self::MaxRowGroupRowCount(value),
1685            SetRegionOptionSerde::PreserveRowSequence(value) => Self::PreserveRowSequence(value),
1686            SetRegionOptionSerde::SkipWal(value) => Self::SkipWal(value),
1687            SetRegionOptionSerde::CompactionOutputParallelism(value) => {
1688                Self::CompactionOutputParallelism(value)
1689            }
1690        }
1691    }
1692}
1693
1694impl<'de> Deserialize<'de> for SetRegionOption {
1695    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
1696    where
1697        D: Deserializer<'de>,
1698    {
1699        Ok(
1700            match BackwardCompatibleSetRegionOption::deserialize(deserializer)? {
1701                BackwardCompatibleSetRegionOption::Current(option) => option.into(),
1702                BackwardCompatibleSetRegionOption::Legacy(LegacySetRegionOption::SkipWal) => {
1703                    Self::SkipWal(true)
1704                }
1705            },
1706        )
1707    }
1708}
1709
1710impl TryFrom<&PbOption> for SetRegionOption {
1711    type Error = MetadataError;
1712
1713    fn try_from(value: &PbOption) -> std::result::Result<Self, Self::Error> {
1714        let PbOption { key, value } = value;
1715        match key.as_str() {
1716            WRITE_BUFFER_SIZE_KEY => {
1717                let size = value
1718                    .parse::<ReadableSize>()
1719                    .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1720                Ok(Self::WriteBufferSize(Some(size)))
1721            }
1722            TTL_KEY => {
1723                let ttl = TimeToLive::from_humantime_or_str(value)
1724                    .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1725
1726                Ok(Self::Ttl(Some(ttl)))
1727            }
1728            TWCS_TRIGGER_FILE_NUM
1729            | TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM
1730            | TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER
1731            | TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM
1732            | TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER
1733            | TWCS_MAX_OUTPUT_FILE_SIZE
1734            | TWCS_TIME_WINDOW => Ok(Self::Twsc(key.clone(), value.clone())),
1735            SST_FORMAT_KEY => Ok(Self::Format(value.clone())),
1736            APPEND_MODE_KEY => {
1737                let append_mode = value
1738                    .parse::<bool>()
1739                    .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1740                Ok(Self::AppendMode(append_mode))
1741            }
1742            AUTO_FLUSH_INTERVAL_KEY => {
1743                if value.is_empty() {
1744                    // SET 'auto_flush_interval' = NULL comes through as an empty
1745                    // string; treat it as clearing the override (fall back to
1746                    // the global default), same as Ttl.
1747                    return Ok(Self::AutoFlushInterval(None));
1748                }
1749                let interval = humantime::parse_duration(value)
1750                    .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1751                if interval <= Duration::ZERO {
1752                    return InvalidSetRegionOptionRequestSnafu { key, value }.fail();
1753                }
1754                Ok(Self::AutoFlushInterval(Some(interval)))
1755            }
1756            COMPACTION_EXPERIMENTAL_OUTPUT_PARALLELISM => {
1757                if value.is_empty() {
1758                    return Ok(Self::CompactionOutputParallelism(None));
1759                }
1760                let parallelism = value
1761                    .parse::<usize>()
1762                    .ok()
1763                    .filter(|parallelism| *parallelism > 0)
1764                    .ok_or_else(|| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1765                Ok(Self::CompactionOutputParallelism(Some(parallelism)))
1766            }
1767            MAX_ROW_GROUP_ROW_COUNT => {
1768                if value.is_empty() {
1769                    return Ok(Self::MaxRowGroupRowCount(None));
1770                }
1771                let row_count = value
1772                    .parse::<usize>()
1773                    .ok()
1774                    .filter(|row_count| {
1775                        *row_count > 0 && *row_count <= MAX_ROW_GROUP_ROW_COUNT_LIMIT
1776                    })
1777                    .ok_or_else(|| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1778                Ok(Self::MaxRowGroupRowCount(Some(row_count)))
1779            }
1780            PRESERVE_ROW_SEQUENCE => {
1781                let preserve = value
1782                    .parse::<bool>()
1783                    .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1784                Ok(Self::PreserveRowSequence(preserve))
1785            }
1786            SKIP_WAL_KEY => {
1787                let skip_wal = value
1788                    .parse::<bool>()
1789                    .map_err(|_| InvalidSetRegionOptionRequestSnafu { key, value }.build())?;
1790                Ok(Self::SkipWal(skip_wal))
1791            }
1792            _ => InvalidSetRegionOptionRequestSnafu { key, value }.fail(),
1793        }
1794    }
1795}
1796
1797impl From<&UnsetRegionOption> for SetRegionOption {
1798    fn from(unset_option: &UnsetRegionOption) -> Self {
1799        match unset_option {
1800            UnsetRegionOption::TwcsTriggerFileNum => {
1801                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1802            }
1803            UnsetRegionOption::TwcsActiveWindowTriggerFileNum => {
1804                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1805            }
1806            UnsetRegionOption::TwcsActiveWindowL1MergeTrigger => {
1807                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1808            }
1809            UnsetRegionOption::TwcsInactiveWindowTriggerFileNum => {
1810                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1811            }
1812            UnsetRegionOption::TwcsInactiveWindowL1MergeTrigger => {
1813                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1814            }
1815            UnsetRegionOption::TwcsMaxOutputFileSize => {
1816                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1817            }
1818            UnsetRegionOption::TwcsTimeWindow => {
1819                SetRegionOption::Twsc(unset_option.to_string(), String::new())
1820            }
1821            UnsetRegionOption::Ttl => SetRegionOption::Ttl(Default::default()),
1822            UnsetRegionOption::MaxRowGroupRowCount => SetRegionOption::MaxRowGroupRowCount(None),
1823            UnsetRegionOption::WriteBufferSize => SetRegionOption::WriteBufferSize(None),
1824            UnsetRegionOption::PreserveRowSequence => SetRegionOption::PreserveRowSequence(false),
1825            UnsetRegionOption::CompactionOutputParallelism => {
1826                SetRegionOption::CompactionOutputParallelism(None)
1827            }
1828        }
1829    }
1830}
1831
1832impl TryFrom<&str> for UnsetRegionOption {
1833    type Error = MetadataError;
1834
1835    fn try_from(key: &str) -> Result<Self> {
1836        match key.to_ascii_lowercase().as_str() {
1837            TTL_KEY => Ok(Self::Ttl),
1838            WRITE_BUFFER_SIZE_KEY => Ok(Self::WriteBufferSize),
1839            TWCS_TRIGGER_FILE_NUM => Ok(Self::TwcsTriggerFileNum),
1840            TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM => Ok(Self::TwcsActiveWindowTriggerFileNum),
1841            TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER => Ok(Self::TwcsActiveWindowL1MergeTrigger),
1842            TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM => Ok(Self::TwcsInactiveWindowTriggerFileNum),
1843            TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER => Ok(Self::TwcsInactiveWindowL1MergeTrigger),
1844            TWCS_MAX_OUTPUT_FILE_SIZE => Ok(Self::TwcsMaxOutputFileSize),
1845            TWCS_TIME_WINDOW => Ok(Self::TwcsTimeWindow),
1846            MAX_ROW_GROUP_ROW_COUNT => Ok(Self::MaxRowGroupRowCount),
1847            PRESERVE_ROW_SEQUENCE => Ok(Self::PreserveRowSequence),
1848            COMPACTION_EXPERIMENTAL_OUTPUT_PARALLELISM => Ok(Self::CompactionOutputParallelism),
1849            _ => InvalidUnsetRegionOptionRequestSnafu { key }.fail(),
1850        }
1851    }
1852}
1853
1854#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
1855pub enum UnsetRegionOption {
1856    TwcsTriggerFileNum,
1857    TwcsActiveWindowTriggerFileNum,
1858    TwcsInactiveWindowTriggerFileNum,
1859    TwcsInactiveWindowL1MergeTrigger,
1860    TwcsMaxOutputFileSize,
1861    TwcsTimeWindow,
1862    Ttl,
1863    MaxRowGroupRowCount,
1864    WriteBufferSize,
1865    PreserveRowSequence,
1866    TwcsActiveWindowL1MergeTrigger,
1867    CompactionOutputParallelism,
1868}
1869
1870impl UnsetRegionOption {
1871    pub fn as_str(&self) -> &str {
1872        match self {
1873            Self::Ttl => TTL_KEY,
1874            Self::WriteBufferSize => WRITE_BUFFER_SIZE_KEY,
1875            Self::TwcsTriggerFileNum => TWCS_TRIGGER_FILE_NUM,
1876            Self::TwcsActiveWindowTriggerFileNum => TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM,
1877            Self::TwcsActiveWindowL1MergeTrigger => TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER,
1878            Self::TwcsInactiveWindowTriggerFileNum => TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM,
1879            Self::TwcsInactiveWindowL1MergeTrigger => TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER,
1880            Self::TwcsMaxOutputFileSize => TWCS_MAX_OUTPUT_FILE_SIZE,
1881            Self::TwcsTimeWindow => TWCS_TIME_WINDOW,
1882            Self::MaxRowGroupRowCount => MAX_ROW_GROUP_ROW_COUNT,
1883            Self::PreserveRowSequence => PRESERVE_ROW_SEQUENCE,
1884            Self::CompactionOutputParallelism => COMPACTION_EXPERIMENTAL_OUTPUT_PARALLELISM,
1885        }
1886    }
1887}
1888
1889impl Display for UnsetRegionOption {
1890    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1891        write!(f, "{}", self.as_str())
1892    }
1893}
1894
1895#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1896pub enum RegionFlushReason {
1897    /// Flush triggered before region migration.
1898    RegionMigration,
1899    /// Flush triggered by repartition procedure.
1900    Repartition,
1901    /// Flush triggered by remote WAL pruning.
1902    RemoteWalPrune,
1903    /// Flush region before closing region.
1904    Closing,
1905    /// Flush region before downgrading region.
1906    Downgrading,
1907}
1908
1909#[derive(Debug, Clone, Default)]
1910pub struct RegionFlushRequest {
1911    pub row_group_size: Option<usize>,
1912    pub reason: Option<RegionFlushReason>,
1913}
1914
1915#[derive(Debug)]
1916pub struct RegionCompactRequest {
1917    pub options: compact_request::Options,
1918    pub parallelism: Option<u32>,
1919    pub time_range: Option<TimestampRange>,
1920}
1921
1922impl Default for RegionCompactRequest {
1923    fn default() -> Self {
1924        Self {
1925            // Default to regular compaction.
1926            options: compact_request::Options::Regular(Default::default()),
1927            parallelism: None,
1928            time_range: None,
1929        }
1930    }
1931}
1932
1933#[derive(Debug, Clone, Default)]
1934pub struct RegionBuildIndexRequest {
1935    /// The index build mode. Absent options select SST indexes.
1936    pub options: Option<build_index_request::Options>,
1937}
1938
1939/// Truncate region request.
1940#[derive(Debug)]
1941pub enum RegionTruncateRequest {
1942    /// Truncate all data in the region.
1943    All,
1944    /// Discard all unflushed data while preserving persisted SST files.
1945    ///
1946    /// This destroys the region's in-memory data irreversibly. Persisted SST files and
1947    /// the writable state are preserved, and the WAL is obsoleted up to the discard point
1948    /// so a restart won't replay the discarded data.
1949    ///
1950    /// An error may be returned after the data has already been discarded, because the
1951    /// WAL is obsoleted last. Retrying is safe: a request against a region with nothing
1952    /// left to discard only re-attempts the WAL obsoletion.
1953    Unflushed,
1954    ByTimeRanges {
1955        /// Time ranges to truncate. Both bound are inclusive.
1956        /// only files that are fully contained in the time range will be truncated.
1957        /// so no guarantee that all data in the time range will be truncated.
1958        time_ranges: Vec<(Timestamp, Timestamp)>,
1959    },
1960}
1961
1962/// Catchup region request.
1963///
1964/// Makes a readonly region to catch up to leader region changes.
1965/// There is no effect if it operating on a leader region.
1966#[derive(Debug, Clone, Copy, Default)]
1967pub struct RegionCatchupRequest {
1968    /// Sets it to writable if it's available after it has caught up with all changes.
1969    pub set_writable: bool,
1970    /// The `entry_id` that was expected to reply to.
1971    /// `None` stands replaying to latest.
1972    pub entry_id: Option<entry::Id>,
1973    /// Used for metrics metadata region.
1974    /// The `entry_id` that was expected to reply to.
1975    /// `None` stands replaying to latest.
1976    pub metadata_entry_id: Option<entry::Id>,
1977    /// Replay checkpoint.
1978    pub checkpoint: Option<ReplayCheckpoint>,
1979}
1980
1981#[derive(Debug, Clone)]
1982pub struct RegionBulkInsertsRequest {
1983    /// Whether this request should skip WAL.
1984    pub skip_wal: bool,
1985    pub region_id: RegionId,
1986    pub payload: DfRecordBatch,
1987    pub raw_data: ArrowIpc,
1988    pub partition_expr_version: Option<u64>,
1989    pub aligned_schema_version: Option<u64>,
1990}
1991
1992impl RegionBulkInsertsRequest {
1993    pub fn estimated_size(&self) -> usize {
1994        self.payload.get_array_memory_size()
1995    }
1996}
1997
1998/// Request to stage a region with a new partition directive.
1999///
2000/// This request transitions a region into the staging mode.
2001/// It first flushes the memtable for the old partition expression if it is not
2002/// empty, then enters the staging mode with the new directive.
2003#[derive(Debug, Clone, PartialEq, Eq)]
2004pub enum StagingPartitionDirective {
2005    UpdatePartitionExpr(String),
2006    RejectAllWrites,
2007}
2008
2009impl StagingPartitionDirective {
2010    /// Returns the partition expression carried by this directive, if any.
2011    pub fn partition_expr(&self) -> Option<&str> {
2012        match self {
2013            Self::UpdatePartitionExpr(expr) => Some(expr),
2014            Self::RejectAllWrites => None,
2015        }
2016    }
2017}
2018
2019#[derive(Debug, Clone)]
2020pub struct EnterStagingRequest {
2021    /// The staging partition directive of the region.
2022    pub partition_directive: StagingPartitionDirective,
2023}
2024
2025impl EnterStagingRequest {
2026    /// Builds an enter-staging request with a partition expression directive.
2027    pub fn with_partition_expr(partition_expr: String) -> Self {
2028        Self {
2029            partition_directive: StagingPartitionDirective::UpdatePartitionExpr(partition_expr),
2030        }
2031    }
2032}
2033
2034/// This request is used as part of the region repartition.
2035///
2036/// After a region has entered staging mode with a new partition expression
2037/// expression) and a separate process (for example, `remap_manifests`) has
2038/// generated the new file assignments for the staging region, this request
2039/// applies that generated manifest to the region.
2040///
2041/// In practice, this means:
2042/// - The `partition_expr` identifies the staging partition expression that the manifest
2043///   was generated for.
2044/// - `central_region_id` specifies which region holds the staging blob storage
2045///   where the manifest was written during the `remap_manifests` operation.
2046/// - `manifest_path` is the relative path within the central region's staging
2047///   blob storage to fetch the generated manifest.
2048///
2049/// It should typically be called **after** the staging region has been
2050/// initialized by [`EnterStagingRequest`] and the new file layout has been
2051/// computed, to finalize the repartition operation.
2052#[derive(Debug, Clone)]
2053pub struct ApplyStagingManifestRequest {
2054    /// The partition expression of the staging region.
2055    pub partition_expr: String,
2056    /// The region that stores the staging manifests in its staging blob storage.
2057    pub central_region_id: RegionId,
2058    /// The relative path to the staging manifest within the central region's
2059    /// staging blob storage.
2060    pub manifest_path: String,
2061}
2062
2063impl fmt::Display for RegionRequest {
2064    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2065        match self {
2066            RegionRequest::Put(_) => write!(f, "Put"),
2067            RegionRequest::Delete(_) => write!(f, "Delete"),
2068            RegionRequest::Create(_) => write!(f, "Create"),
2069            RegionRequest::Drop(_) => write!(f, "Drop"),
2070            RegionRequest::Open(_) => write!(f, "Open"),
2071            RegionRequest::CleanUp(_) => write!(f, "CleanUp"),
2072            RegionRequest::Close(_) => write!(f, "Close"),
2073            RegionRequest::Alter(_) => write!(f, "Alter"),
2074            RegionRequest::Flush(_) => write!(f, "Flush"),
2075            RegionRequest::Compact(_) => write!(f, "Compact"),
2076            RegionRequest::BuildIndex(_) => write!(f, "BuildIndex"),
2077            RegionRequest::Truncate(_) => write!(f, "Truncate"),
2078            RegionRequest::Catchup(_) => write!(f, "Catchup"),
2079            RegionRequest::BulkInserts(_) => write!(f, "BulkInserts"),
2080            RegionRequest::EnterStaging(_) => write!(f, "EnterStaging"),
2081            RegionRequest::ApplyStagingManifest(_) => write!(f, "ApplyStagingManifest"),
2082        }
2083    }
2084}
2085
2086#[cfg(test)]
2087mod tests {
2088
2089    use api::v1::region::RegionColumnDef;
2090    use api::v1::{ColumnDataType, ColumnDef};
2091    use common_time::range::TimestampRange;
2092    use datatypes::extension::json::{Json2ExtensionType, JsonMetadata};
2093    use datatypes::json::JsonSettings;
2094    use datatypes::prelude::ConcreteDataType;
2095    use datatypes::schema::{ColumnSchema, FulltextAnalyzer, FulltextBackend};
2096    use datatypes::types::JsonType;
2097
2098    use super::*;
2099    use crate::metadata::RegionMetadataBuilder;
2100
2101    #[test]
2102    fn test_build_index_options_round_trip() {
2103        use prost::Message;
2104
2105        for options in [
2106            None,
2107            Some(build_index_request::Options::SstIndex(Default::default())),
2108            Some(build_index_request::Options::SeriesIndex(Default::default())),
2109        ] {
2110            let request = BuildIndexRequest {
2111                region_id: 42,
2112                options,
2113            };
2114            let decoded = BuildIndexRequest::decode(request.encode_to_vec().as_slice()).unwrap();
2115            let requests = make_region_build_index(decoded).unwrap();
2116            assert_eq!(1, requests.len());
2117            assert_eq!(RegionId::from_u64(42), requests[0].0);
2118            let RegionRequest::BuildIndex(request) = &requests[0].1 else {
2119                panic!("expected build-index request");
2120            };
2121            assert_eq!(options, request.options);
2122        }
2123        // The legacy wire message contains only region_id (field 1).
2124        let legacy = BuildIndexRequest::decode(&[0x08, 42][..]).unwrap();
2125        assert!(legacy.options.is_none());
2126    }
2127
2128    #[test]
2129    fn test_make_region_puts_preserves_skip_wal() {
2130        let region_id = RegionId::new(42, 3);
2131        let rows = Rows::default();
2132        let requests = make_region_puts(InsertRequests {
2133            requests: [false, true, false]
2134                .into_iter()
2135                .map(|skip_wal| api::v1::region::InsertRequest {
2136                    region_id: region_id.as_u64(),
2137                    rows: Some(rows.clone()),
2138                    partition_expr_version: Some(api::v1::PartitionExprVersion { value: 7 }),
2139                    skip_wal,
2140                })
2141                .collect(),
2142        })
2143        .unwrap();
2144
2145        assert_eq!(3, requests.len());
2146        for ((id, request), skip_wal) in requests.into_iter().zip([false, true, false]) {
2147            assert_eq!(region_id, id);
2148            let RegionRequest::Put(request) = request else {
2149                panic!("expected a put request");
2150            };
2151            assert_eq!(rows, request.rows);
2152            assert_eq!(skip_wal, request.skip_wal);
2153            assert_eq!(Some(7), request.partition_expr_version);
2154            assert!(request.hint.is_none());
2155        }
2156    }
2157
2158    #[test]
2159    fn test_make_region_compact_with_time_range() {
2160        let requests = make_region_compact(CompactRequest {
2161            region_id: 42,
2162            time_range: Some(api::v1::region::CompactionTimeRange {
2163                start: 1_000,
2164                end: 2_000,
2165                time_unit: api::v1::TimeUnit::Microsecond as i32,
2166            }),
2167            ..Default::default()
2168        })
2169        .unwrap();
2170
2171        let RegionRequest::Compact(request) = &requests[0].1 else {
2172            unreachable!();
2173        };
2174        assert_eq!(
2175            Some(
2176                TimestampRange::new(
2177                    Timestamp::new_microsecond(1_000),
2178                    Timestamp::new_microsecond(2_000),
2179                )
2180                .unwrap()
2181            ),
2182            request.time_range
2183        );
2184    }
2185
2186    #[test]
2187    fn test_make_region_truncate_unflushed() {
2188        let region_id = RegionId::new(42, 3);
2189        let requests =
2190            RegionRequest::try_from_request_body(region_request::Body::Truncate(TruncateRequest {
2191                region_id: region_id.as_u64(),
2192                kind: Some(truncate_request::Kind::Unflushed(
2193                    api::v1::region::Unflushed {},
2194                )),
2195            }))
2196            .unwrap();
2197
2198        assert_eq!(region_id, requests[0].0);
2199        assert!(matches!(
2200            requests[0].1,
2201            RegionRequest::Truncate(RegionTruncateRequest::Unflushed)
2202        ));
2203    }
2204
2205    #[test]
2206    fn test_make_region_truncate_requires_kind() {
2207        let error =
2208            RegionRequest::try_from_request_body(region_request::Body::Truncate(TruncateRequest {
2209                region_id: RegionId::new(42, 3).as_u64(),
2210                kind: None,
2211            }))
2212            .unwrap_err();
2213
2214        assert!(
2215            error
2216                .to_string()
2217                .contains("missing kind in TruncateRequest")
2218        );
2219    }
2220
2221    #[test]
2222    fn test_from_proto_location() {
2223        let proto_location = v1::AddColumnLocation {
2224            location_type: LocationType::First as i32,
2225            after_column_name: String::default(),
2226        };
2227        let location = AddColumnLocation::try_from(proto_location).unwrap();
2228        assert_eq!(location, AddColumnLocation::First);
2229
2230        let proto_location = v1::AddColumnLocation {
2231            location_type: 10,
2232            after_column_name: String::default(),
2233        };
2234        AddColumnLocation::try_from(proto_location).unwrap_err();
2235
2236        let proto_location = v1::AddColumnLocation {
2237            location_type: LocationType::After as i32,
2238            after_column_name: "a".to_string(),
2239        };
2240        let location = AddColumnLocation::try_from(proto_location).unwrap();
2241        assert_eq!(
2242            location,
2243            AddColumnLocation::After {
2244                column_name: "a".to_string()
2245            }
2246        );
2247    }
2248
2249    #[test]
2250    fn test_from_none_proto_add_column() {
2251        AddColumn::try_from(v1::region::AddColumn {
2252            column_def: None,
2253            location: None,
2254        })
2255        .unwrap_err();
2256    }
2257
2258    #[test]
2259    fn test_compaction_output_parallelism_set_unset_and_serde() {
2260        for (raw, expected) in [("", None), ("1", Some(1)), ("4", Some(4))] {
2261            let option = SetRegionOption::try_from(&PbOption {
2262                key: COMPACTION_EXPERIMENTAL_OUTPUT_PARALLELISM.to_string(),
2263                value: raw.to_string(),
2264            })
2265            .unwrap();
2266            assert_eq!(
2267                SetRegionOption::CompactionOutputParallelism(expected),
2268                option
2269            );
2270            let json = serde_json::to_string(&option).unwrap();
2271            assert_eq!(
2272                option,
2273                serde_json::from_str::<SetRegionOption>(&json).unwrap()
2274            );
2275        }
2276        for raw in ["0", "-1", "1.5", "invalid", "18446744073709551616"] {
2277            assert!(
2278                SetRegionOption::try_from(&PbOption {
2279                    key: COMPACTION_EXPERIMENTAL_OUTPUT_PARALLELISM.to_string(),
2280                    value: raw.to_string(),
2281                })
2282                .is_err()
2283            );
2284        }
2285        let unset =
2286            UnsetRegionOption::try_from(COMPACTION_EXPERIMENTAL_OUTPUT_PARALLELISM).unwrap();
2287        assert_eq!(COMPACTION_EXPERIMENTAL_OUTPUT_PARALLELISM, unset.as_str());
2288        assert_eq!(
2289            SetRegionOption::CompactionOutputParallelism(None),
2290            (&unset).into()
2291        );
2292    }
2293
2294    #[test]
2295    fn test_set_region_option_auto_flush_interval_try_from() {
2296        use std::time::Duration;
2297
2298        // Valid duration
2299        let pb = PbOption {
2300            key: "auto_flush_interval".to_string(),
2301            value: "5m".to_string(),
2302        };
2303        let opt = SetRegionOption::try_from(&pb).unwrap();
2304        assert_eq!(
2305            opt,
2306            SetRegionOption::AutoFlushInterval(Some(Duration::from_secs(300)))
2307        );
2308
2309        // Empty value clears the override (mirrors Ttl's behaviour for `SET key = NULL`).
2310        let pb = PbOption {
2311            key: "auto_flush_interval".to_string(),
2312            value: String::new(),
2313        };
2314        let opt = SetRegionOption::try_from(&pb).unwrap();
2315        assert_eq!(opt, SetRegionOption::AutoFlushInterval(None));
2316
2317        // Zero is rejected up front (engine invariant).
2318        let pb = PbOption {
2319            key: "auto_flush_interval".to_string(),
2320            value: "0s".to_string(),
2321        };
2322        assert!(SetRegionOption::try_from(&pb).is_err());
2323
2324        // Garbage value is rejected.
2325        let pb = PbOption {
2326            key: "auto_flush_interval".to_string(),
2327            value: "not_a_duration".to_string(),
2328        };
2329        assert!(SetRegionOption::try_from(&pb).is_err());
2330    }
2331
2332    #[test]
2333    fn test_set_region_option_skip_wal_try_from() {
2334        let pb = PbOption {
2335            key: SKIP_WAL_KEY.to_string(),
2336            value: "true".to_string(),
2337        };
2338        assert_eq!(
2339            SetRegionOption::SkipWal(true),
2340            SetRegionOption::try_from(&pb).unwrap()
2341        );
2342
2343        let pb = PbOption {
2344            key: SKIP_WAL_KEY.to_string(),
2345            value: "false".to_string(),
2346        };
2347        assert_eq!(
2348            SetRegionOption::SkipWal(false),
2349            SetRegionOption::try_from(&pb).unwrap()
2350        );
2351
2352        for value in ["", "invalid"] {
2353            let pb = PbOption {
2354                key: SKIP_WAL_KEY.to_string(),
2355                value: value.to_string(),
2356            };
2357            assert!(SetRegionOption::try_from(&pb).is_err());
2358        }
2359
2360        assert!(UnsetRegionOption::try_from(SKIP_WAL_KEY).is_err());
2361    }
2362
2363    #[test]
2364    fn test_set_region_option_skip_wal_serde_compatibility() {
2365        let legacy = serde_json::from_str::<SetRegionOption>(r#""SkipWal""#).unwrap();
2366        assert_eq!(SetRegionOption::SkipWal(true), legacy);
2367
2368        check_set_region_option_skip_wal_serde_compatibility(false);
2369        check_set_region_option_skip_wal_serde_compatibility(true);
2370    }
2371
2372    fn check_set_region_option_skip_wal_serde_compatibility(skip_wal: bool) {
2373        let option = SetRegionOption::SkipWal(skip_wal);
2374        let serialized = serde_json::to_string(&option).unwrap();
2375        let expected = if skip_wal {
2376            r#""SkipWal""#
2377        } else {
2378            r#"{"SkipWal":false}"#
2379        };
2380        assert_eq!(expected, serialized);
2381        assert_eq!(
2382            option,
2383            serde_json::from_str::<SetRegionOption>(&serialized).unwrap()
2384        );
2385        if skip_wal {
2386            assert!(serde_json::from_str::<LegacySetRegionOption>(&serialized).is_ok());
2387            assert_eq!(
2388                option,
2389                serde_json::from_str::<SetRegionOption>(r#"{"SkipWal":true}"#).unwrap()
2390            );
2391        }
2392    }
2393
2394    #[test]
2395    fn test_set_region_option_max_row_group_row_count_try_from() {
2396        let pb = PbOption {
2397            key: MAX_ROW_GROUP_ROW_COUNT.to_string(),
2398            value: "512".to_string(),
2399        };
2400        assert_eq!(
2401            SetRegionOption::MaxRowGroupRowCount(Some(512)),
2402            SetRegionOption::try_from(&pb).unwrap()
2403        );
2404
2405        let pb = PbOption {
2406            key: MAX_ROW_GROUP_ROW_COUNT.to_string(),
2407            value: String::new(),
2408        };
2409        assert_eq!(
2410            SetRegionOption::MaxRowGroupRowCount(None),
2411            SetRegionOption::try_from(&pb).unwrap()
2412        );
2413
2414        for value in [
2415            "0".to_string(),
2416            (MAX_ROW_GROUP_ROW_COUNT_LIMIT + 1).to_string(),
2417            "invalid".to_string(),
2418        ] {
2419            let pb = PbOption {
2420                key: MAX_ROW_GROUP_ROW_COUNT.to_string(),
2421                value,
2422            };
2423            assert!(SetRegionOption::try_from(&pb).is_err());
2424        }
2425
2426        assert_eq!(
2427            UnsetRegionOption::MaxRowGroupRowCount,
2428            UnsetRegionOption::try_from(MAX_ROW_GROUP_ROW_COUNT).unwrap()
2429        );
2430    }
2431
2432    #[test]
2433    fn test_set_region_option_preserve_row_sequence_try_from() {
2434        for (value, expected) in [("true", true), ("false", false)] {
2435            let pb = PbOption {
2436                key: PRESERVE_ROW_SEQUENCE.to_string(),
2437                value: value.to_string(),
2438            };
2439            assert_eq!(
2440                SetRegionOption::PreserveRowSequence(expected),
2441                SetRegionOption::try_from(&pb).unwrap()
2442            );
2443        }
2444
2445        for value in ["1", "invalid"] {
2446            let pb = PbOption {
2447                key: PRESERVE_ROW_SEQUENCE.to_string(),
2448                value: value.to_string(),
2449            };
2450            assert!(SetRegionOption::try_from(&pb).is_err());
2451        }
2452
2453        assert_eq!(
2454            UnsetRegionOption::PreserveRowSequence,
2455            UnsetRegionOption::try_from(PRESERVE_ROW_SEQUENCE).unwrap()
2456        );
2457        assert_eq!(
2458            SetRegionOption::PreserveRowSequence(false),
2459            (&UnsetRegionOption::PreserveRowSequence).into()
2460        );
2461    }
2462
2463    #[test]
2464    fn test_set_twcs_window_trigger_options_try_from() {
2465        for key in [
2466            "compaction.twcs.active_window.trigger_file_num",
2467            "compaction.twcs.active_window.l1_merge_trigger",
2468            "compaction.twcs.inactive_window.trigger_file_num",
2469            "compaction.twcs.inactive_window.l1_merge_trigger",
2470        ] {
2471            let option = PbOption {
2472                key: key.to_string(),
2473                value: "8".to_string(),
2474            };
2475            assert_eq!(
2476                SetRegionOption::Twsc(key.to_string(), "8".to_string()),
2477                SetRegionOption::try_from(&option).unwrap(),
2478                "{key}"
2479            );
2480        }
2481    }
2482
2483    #[test]
2484    fn test_unset_twcs_window_trigger_options_try_from() {
2485        for (key, expected) in [
2486            (
2487                "compaction.twcs.active_window.trigger_file_num",
2488                UnsetRegionOption::TwcsActiveWindowTriggerFileNum,
2489            ),
2490            (
2491                "compaction.twcs.active_window.l1_merge_trigger",
2492                UnsetRegionOption::TwcsActiveWindowL1MergeTrigger,
2493            ),
2494            (
2495                "compaction.twcs.inactive_window.trigger_file_num",
2496                UnsetRegionOption::TwcsInactiveWindowTriggerFileNum,
2497            ),
2498        ] {
2499            assert_eq!(expected, UnsetRegionOption::try_from(key).unwrap());
2500            assert_eq!(
2501                SetRegionOption::Twsc(key.to_string(), String::new()),
2502                SetRegionOption::from(&expected)
2503            );
2504        }
2505
2506        let key = "compaction.twcs.inactive_window.l1_merge_trigger";
2507        let option = UnsetRegionOption::try_from(key).unwrap();
2508        assert_eq!(key, option.to_string());
2509        assert_eq!(
2510            SetRegionOption::Twsc(key.to_string(), String::new()),
2511            SetRegionOption::from(&option)
2512        );
2513    }
2514
2515    #[test]
2516    fn test_json_settings_path_validation_from_proto() {
2517        for (path, valid_path) in [
2518            (vec!["nested", "leaf"], true),
2519            (vec![""], false),
2520            (vec!["nested", ""], false),
2521            (
2522                vec!["nested", datatypes::json::JSON2_REMAINDER_FIELD_NAME],
2523                false,
2524            ),
2525        ] {
2526            for inverted_index in [false, true] {
2527                let settings = api::v1::SetJsonSettings {
2528                    column_name: "payload".into(),
2529                    settings: Some(api::v1::JsonSettings {
2530                        type_hints: vec![api::v1::JsonTypeHint {
2531                            path: path.iter().map(|part| (*part).to_string()).collect(),
2532                            data_type: ColumnDataType::String as i32,
2533                            inverted_index,
2534                            skipping_index: None,
2535                            ..Default::default()
2536                        }],
2537                        max_auto_expanded_paths: Some(0),
2538                    }),
2539                };
2540                let result = RegionAlterRequest::try_from(AlterRequest {
2541                    region_id: 0,
2542                    schema_version: 1,
2543                    kind: Some(alter_request::Kind::SetJsonSettings(settings)),
2544                });
2545                assert_eq!(
2546                    result.is_ok(),
2547                    valid_path,
2548                    "path: {path:?}, inverted_index: {inverted_index}"
2549                );
2550            }
2551        }
2552    }
2553
2554    #[test]
2555    fn test_from_proto_alter_request() {
2556        RegionAlterRequest::try_from(AlterRequest {
2557            region_id: 0,
2558            schema_version: 1,
2559            kind: None,
2560        })
2561        .unwrap_err();
2562
2563        let request = RegionAlterRequest::try_from(AlterRequest {
2564            region_id: 0,
2565            schema_version: 1,
2566            kind: Some(alter_request::Kind::AddColumns(v1::region::AddColumns {
2567                add_columns: vec![v1::region::AddColumn {
2568                    column_def: Some(RegionColumnDef {
2569                        column_def: Some(ColumnDef {
2570                            name: "a".to_string(),
2571                            data_type: ColumnDataType::String as i32,
2572                            is_nullable: true,
2573                            default_constraint: vec![],
2574                            semantic_type: SemanticType::Field as i32,
2575                            comment: String::new(),
2576                            ..Default::default()
2577                        }),
2578                        column_id: 1,
2579                    }),
2580                    location: Some(v1::AddColumnLocation {
2581                        location_type: LocationType::First as i32,
2582                        after_column_name: String::default(),
2583                    }),
2584                }],
2585            })),
2586        })
2587        .unwrap();
2588
2589        assert_eq!(
2590            request,
2591            RegionAlterRequest {
2592                kind: AlterKind::AddColumns {
2593                    columns: vec![AddColumn {
2594                        column_metadata: ColumnMetadata {
2595                            column_schema: ColumnSchema::new(
2596                                "a",
2597                                ConcreteDataType::string_datatype(),
2598                                true,
2599                            ),
2600                            semantic_type: SemanticType::Field,
2601                            column_id: 1,
2602                        },
2603                        location: Some(AddColumnLocation::First),
2604                    }]
2605                },
2606            }
2607        );
2608
2609        let request = RegionAlterRequest::try_from(AlterRequest {
2610            region_id: 0,
2611            schema_version: 1,
2612            kind: Some(alter_request::Kind::SetJsonSettings(v1::SetJsonSettings {
2613                column_name: "payload".to_string(),
2614                settings: Some(v1::JsonSettings {
2615                    type_hints: vec![v1::JsonTypeHint {
2616                        path: vec!["service".to_string()],
2617                        data_type: ColumnDataType::String as i32,
2618                        datatype_extension: None,
2619                        inverted_index: true,
2620                        skipping_index: Some(v1::JsonSkippingIndexOptions {
2621                            granularity: 8,
2622                            index_type: 0,
2623                            false_positive_rate: 0.05,
2624                        }),
2625                    }],
2626                    max_auto_expanded_paths: Some(10),
2627                }),
2628            })),
2629        })
2630        .unwrap();
2631
2632        let AlterKind::SetJsonSettings {
2633            column_name,
2634            settings,
2635        } = request.kind
2636        else {
2637            unreachable!()
2638        };
2639        assert_eq!("payload", column_name);
2640        assert_eq!(Some(10), settings.max_auto_expanded_paths());
2641        assert_eq!(1, settings.type_hints().len());
2642        assert!(settings.type_hints()[0].inverted_index);
2643        assert_eq!(
2644            settings.type_hints()[0]
2645                .skipping_index
2646                .as_ref()
2647                .unwrap()
2648                .granularity,
2649            8
2650        );
2651    }
2652
2653    #[test]
2654    fn test_write_buffer_size_region_options() {
2655        let option = PbOption {
2656            key: WRITE_BUFFER_SIZE_KEY.to_string(),
2657            value: "128MiB".to_string(),
2658        };
2659        assert_eq!(
2660            SetRegionOption::WriteBufferSize(Some(ReadableSize::mb(128))),
2661            SetRegionOption::try_from(&option).unwrap()
2662        );
2663
2664        let option = PbOption {
2665            key: WRITE_BUFFER_SIZE_KEY.to_string(),
2666            value: "invalid".to_string(),
2667        };
2668        SetRegionOption::try_from(&option).unwrap_err();
2669
2670        // Clearing the option uses UNSET. Empty SET values, including SQL NULL,
2671        // remain invalid because the SQL layer does not distinguish NULL from ''.
2672        let option = PbOption {
2673            key: WRITE_BUFFER_SIZE_KEY.to_string(),
2674            value: String::new(),
2675        };
2676        SetRegionOption::try_from(&option).unwrap_err();
2677
2678        assert_eq!(
2679            UnsetRegionOption::WriteBufferSize,
2680            UnsetRegionOption::try_from(WRITE_BUFFER_SIZE_KEY).unwrap()
2681        );
2682        assert_eq!(
2683            SetRegionOption::WriteBufferSize(None),
2684            SetRegionOption::from(&UnsetRegionOption::WriteBufferSize)
2685        );
2686    }
2687
2688    /// Returns a new region metadata for testing. Metadata:
2689    /// `[(ts, ms, 1), (tag_0, string, 2), (field_0, string, 3), (field_1, bool, 4)]`
2690    fn new_metadata() -> RegionMetadata {
2691        let mut builder = RegionMetadataBuilder::new(RegionId::new(1, 1));
2692        builder
2693            .push_column_metadata(ColumnMetadata {
2694                column_schema: ColumnSchema::new(
2695                    "ts",
2696                    ConcreteDataType::timestamp_millisecond_datatype(),
2697                    false,
2698                ),
2699                semantic_type: SemanticType::Timestamp,
2700                column_id: 1,
2701            })
2702            .push_column_metadata(ColumnMetadata {
2703                column_schema: ColumnSchema::new(
2704                    "tag_0",
2705                    ConcreteDataType::string_datatype(),
2706                    true,
2707                ),
2708                semantic_type: SemanticType::Tag,
2709                column_id: 2,
2710            })
2711            .push_column_metadata(ColumnMetadata {
2712                column_schema: ColumnSchema::new(
2713                    "field_0",
2714                    ConcreteDataType::string_datatype(),
2715                    true,
2716                ),
2717                semantic_type: SemanticType::Field,
2718                column_id: 3,
2719            })
2720            .push_column_metadata(ColumnMetadata {
2721                column_schema: ColumnSchema::new(
2722                    "field_1",
2723                    ConcreteDataType::boolean_datatype(),
2724                    true,
2725                ),
2726                semantic_type: SemanticType::Field,
2727                column_id: 4,
2728            })
2729            .primary_key(vec![2]);
2730        builder.build().unwrap()
2731    }
2732
2733    fn json2_column_schema(name: &str, settings: JsonSettings) -> ColumnSchema {
2734        let mut column_schema =
2735            ColumnSchema::new(name, ConcreteDataType::Json(JsonType::null()), true);
2736        column_schema.with_extension_type(&Json2ExtensionType::new(std::sync::Arc::new(
2737            JsonMetadata::new(settings),
2738        )));
2739        column_schema
2740    }
2741
2742    #[test]
2743    fn test_add_column_validate() {
2744        let metadata = new_metadata();
2745        let add_column = AddColumn {
2746            column_metadata: ColumnMetadata {
2747                column_schema: ColumnSchema::new(
2748                    "tag_1",
2749                    ConcreteDataType::string_datatype(),
2750                    true,
2751                ),
2752                semantic_type: SemanticType::Tag,
2753                column_id: 5,
2754            },
2755            location: None,
2756        };
2757        add_column.validate(&metadata).unwrap();
2758        assert!(add_column.need_alter(&metadata));
2759
2760        // Add not null column.
2761        AddColumn {
2762            column_metadata: ColumnMetadata {
2763                column_schema: ColumnSchema::new(
2764                    "tag_1",
2765                    ConcreteDataType::string_datatype(),
2766                    false,
2767                ),
2768                semantic_type: SemanticType::Tag,
2769                column_id: 5,
2770            },
2771            location: None,
2772        }
2773        .validate(&metadata)
2774        .unwrap_err();
2775
2776        // Add existing column.
2777        let add_column = AddColumn {
2778            column_metadata: ColumnMetadata {
2779                column_schema: ColumnSchema::new(
2780                    "tag_0",
2781                    ConcreteDataType::string_datatype(),
2782                    true,
2783                ),
2784                semantic_type: SemanticType::Tag,
2785                column_id: 2,
2786            },
2787            location: None,
2788        };
2789        add_column.validate(&metadata).unwrap();
2790        assert!(!add_column.need_alter(&metadata));
2791    }
2792
2793    #[test]
2794    fn test_add_duplicate_columns() {
2795        let kind = AlterKind::AddColumns {
2796            columns: vec![
2797                AddColumn {
2798                    column_metadata: ColumnMetadata {
2799                        column_schema: ColumnSchema::new(
2800                            "tag_1",
2801                            ConcreteDataType::string_datatype(),
2802                            true,
2803                        ),
2804                        semantic_type: SemanticType::Tag,
2805                        column_id: 5,
2806                    },
2807                    location: None,
2808                },
2809                AddColumn {
2810                    column_metadata: ColumnMetadata {
2811                        column_schema: ColumnSchema::new(
2812                            "tag_1",
2813                            ConcreteDataType::string_datatype(),
2814                            true,
2815                        ),
2816                        semantic_type: SemanticType::Field,
2817                        column_id: 6,
2818                    },
2819                    location: None,
2820                },
2821            ],
2822        };
2823        let metadata = new_metadata();
2824        kind.validate(&metadata).unwrap();
2825        assert!(kind.need_alter(&metadata));
2826    }
2827
2828    #[test]
2829    fn test_add_existing_column_different_metadata() {
2830        let metadata = new_metadata();
2831
2832        // Add existing column with different id.
2833        let kind = AlterKind::AddColumns {
2834            columns: vec![AddColumn {
2835                column_metadata: ColumnMetadata {
2836                    column_schema: ColumnSchema::new(
2837                        "tag_0",
2838                        ConcreteDataType::string_datatype(),
2839                        true,
2840                    ),
2841                    semantic_type: SemanticType::Tag,
2842                    column_id: 4,
2843                },
2844                location: None,
2845            }],
2846        };
2847        kind.validate(&metadata).unwrap_err();
2848
2849        // Add existing column with different type.
2850        let kind = AlterKind::AddColumns {
2851            columns: vec![AddColumn {
2852                column_metadata: ColumnMetadata {
2853                    column_schema: ColumnSchema::new(
2854                        "tag_0",
2855                        ConcreteDataType::int64_datatype(),
2856                        true,
2857                    ),
2858                    semantic_type: SemanticType::Tag,
2859                    column_id: 2,
2860                },
2861                location: None,
2862            }],
2863        };
2864        kind.validate(&metadata).unwrap_err();
2865
2866        // Add existing column with different name.
2867        let kind = AlterKind::AddColumns {
2868            columns: vec![AddColumn {
2869                column_metadata: ColumnMetadata {
2870                    column_schema: ColumnSchema::new(
2871                        "tag_1",
2872                        ConcreteDataType::string_datatype(),
2873                        true,
2874                    ),
2875                    semantic_type: SemanticType::Tag,
2876                    column_id: 2,
2877                },
2878                location: None,
2879            }],
2880        };
2881        kind.validate(&metadata).unwrap_err();
2882    }
2883
2884    #[test]
2885    fn test_add_existing_column_with_location() {
2886        let metadata = new_metadata();
2887        let kind = AlterKind::AddColumns {
2888            columns: vec![AddColumn {
2889                column_metadata: ColumnMetadata {
2890                    column_schema: ColumnSchema::new(
2891                        "tag_0",
2892                        ConcreteDataType::string_datatype(),
2893                        true,
2894                    ),
2895                    semantic_type: SemanticType::Tag,
2896                    column_id: 2,
2897                },
2898                location: Some(AddColumnLocation::First),
2899            }],
2900        };
2901        kind.validate(&metadata).unwrap_err();
2902    }
2903
2904    #[test]
2905    fn test_validate_drop_column() {
2906        let metadata = new_metadata();
2907        let kind = AlterKind::DropColumns {
2908            names: vec!["xxxx".to_string()],
2909        };
2910        kind.validate(&metadata).unwrap();
2911        assert!(!kind.need_alter(&metadata));
2912
2913        AlterKind::DropColumns {
2914            names: vec!["tag_0".to_string()],
2915        }
2916        .validate(&metadata)
2917        .unwrap_err();
2918
2919        let kind = AlterKind::DropColumns {
2920            names: vec!["field_0".to_string()],
2921        };
2922        kind.validate(&metadata).unwrap();
2923        assert!(kind.need_alter(&metadata));
2924    }
2925
2926    #[test]
2927    fn test_validate_modify_column_type() {
2928        let metadata = new_metadata();
2929        AlterKind::ModifyColumnTypes {
2930            columns: vec![ModifyColumnType {
2931                column_name: "xxxx".to_string(),
2932                target_type: ConcreteDataType::string_datatype(),
2933            }],
2934        }
2935        .validate(&metadata)
2936        .unwrap_err();
2937
2938        AlterKind::ModifyColumnTypes {
2939            columns: vec![ModifyColumnType {
2940                column_name: "field_1".to_string(),
2941                target_type: ConcreteDataType::date_datatype(),
2942            }],
2943        }
2944        .validate(&metadata)
2945        .unwrap_err();
2946
2947        AlterKind::ModifyColumnTypes {
2948            columns: vec![ModifyColumnType {
2949                column_name: "ts".to_string(),
2950                target_type: ConcreteDataType::date_datatype(),
2951            }],
2952        }
2953        .validate(&metadata)
2954        .unwrap_err();
2955
2956        // Time index unit widening is allowed.
2957        let kind = AlterKind::ModifyColumnTypes {
2958            columns: vec![ModifyColumnType {
2959                column_name: "ts".to_string(),
2960                target_type: ConcreteDataType::timestamp_microsecond_datatype(),
2961            }],
2962        };
2963        kind.validate(&metadata).unwrap();
2964        assert!(kind.need_alter(&metadata));
2965
2966        // Narrowing the time index unit is rejected.
2967        let metadata_nano = {
2968            let mut metadata = new_metadata();
2969            for col in metadata.column_metadatas.iter_mut() {
2970                if col.column_schema.name == "ts" {
2971                    col.column_schema.data_type = ConcreteDataType::timestamp_nanosecond_datatype();
2972                }
2973            }
2974            metadata
2975        };
2976        AlterKind::ModifyColumnTypes {
2977            columns: vec![ModifyColumnType {
2978                column_name: "ts".to_string(),
2979                target_type: ConcreteDataType::timestamp_millisecond_datatype(),
2980            }],
2981        }
2982        .validate(&metadata_nano)
2983        .unwrap_err();
2984
2985        // Changing the time index to the same type is a validated no-op, so a
2986        // retried alter procedure (region already altered) succeeds and is
2987        // skipped by `need_alter`.
2988        let same_type = AlterKind::ModifyColumnTypes {
2989            columns: vec![ModifyColumnType {
2990                column_name: "ts".to_string(),
2991                target_type: ConcreteDataType::timestamp_millisecond_datatype(),
2992            }],
2993        };
2994        same_type.validate(&metadata).unwrap();
2995        assert!(!same_type.need_alter(&metadata));
2996
2997        // Changing the time index to a non-timestamp type is rejected.
2998        AlterKind::ModifyColumnTypes {
2999            columns: vec![ModifyColumnType {
3000                column_name: "ts".to_string(),
3001                target_type: ConcreteDataType::string_datatype(),
3002            }],
3003        }
3004        .validate(&metadata)
3005        .unwrap_err();
3006
3007        AlterKind::ModifyColumnTypes {
3008            columns: vec![ModifyColumnType {
3009                column_name: "tag_0".to_string(),
3010                target_type: ConcreteDataType::date_datatype(),
3011            }],
3012        }
3013        .validate(&metadata)
3014        .unwrap_err();
3015
3016        let kind = AlterKind::ModifyColumnTypes {
3017            columns: vec![ModifyColumnType {
3018                column_name: "field_0".to_string(),
3019                target_type: ConcreteDataType::int32_datatype(),
3020            }],
3021        };
3022        kind.validate(&metadata).unwrap();
3023        assert!(kind.need_alter(&metadata));
3024    }
3025
3026    #[test]
3027    fn test_validate_set_json_settings() {
3028        let mut metadata = new_metadata();
3029        let current_schema = json2_column_schema("field_0", JsonSettings::new_v2());
3030        let target_settings = JsonSettings::try_new(vec![], Some(10)).unwrap();
3031        metadata
3032            .column_metadatas
3033            .iter_mut()
3034            .find(|column| column.column_schema.name == "field_0")
3035            .unwrap()
3036            .column_schema = current_schema.clone();
3037
3038        let kind = AlterKind::SetJsonSettings {
3039            column_name: "field_0".to_string(),
3040            settings: target_settings.clone(),
3041        };
3042        kind.validate(&metadata).unwrap();
3043        assert!(kind.need_alter(&metadata));
3044
3045        let no_op = AlterKind::SetJsonSettings {
3046            column_name: "field_0".to_string(),
3047            settings: JsonSettings::new_v2(),
3048        };
3049        no_op.validate(&metadata).unwrap();
3050        assert!(!no_op.need_alter(&metadata));
3051
3052        AlterKind::SetJsonSettings {
3053            column_name: "tag_0".to_string(),
3054            settings: target_settings,
3055        }
3056        .validate(&metadata)
3057        .unwrap_err();
3058    }
3059
3060    #[test]
3061    fn test_validate_add_columns() {
3062        let kind = AlterKind::AddColumns {
3063            columns: vec![
3064                AddColumn {
3065                    column_metadata: ColumnMetadata {
3066                        column_schema: ColumnSchema::new(
3067                            "tag_1",
3068                            ConcreteDataType::string_datatype(),
3069                            true,
3070                        ),
3071                        semantic_type: SemanticType::Tag,
3072                        column_id: 5,
3073                    },
3074                    location: None,
3075                },
3076                AddColumn {
3077                    column_metadata: ColumnMetadata {
3078                        column_schema: ColumnSchema::new(
3079                            "field_2",
3080                            ConcreteDataType::string_datatype(),
3081                            true,
3082                        ),
3083                        semantic_type: SemanticType::Field,
3084                        column_id: 6,
3085                    },
3086                    location: None,
3087                },
3088            ],
3089        };
3090        let request = RegionAlterRequest { kind };
3091        let mut metadata = new_metadata();
3092        metadata.schema_version = 1;
3093        request.validate(&metadata).unwrap();
3094    }
3095
3096    #[test]
3097    fn test_validate_create_region() {
3098        let column_metadatas = vec![
3099            ColumnMetadata {
3100                column_schema: ColumnSchema::new(
3101                    "ts",
3102                    ConcreteDataType::timestamp_millisecond_datatype(),
3103                    false,
3104                ),
3105                semantic_type: SemanticType::Timestamp,
3106                column_id: 1,
3107            },
3108            ColumnMetadata {
3109                column_schema: ColumnSchema::new(
3110                    "tag_0",
3111                    ConcreteDataType::string_datatype(),
3112                    true,
3113                ),
3114                semantic_type: SemanticType::Tag,
3115                column_id: 2,
3116            },
3117            ColumnMetadata {
3118                column_schema: ColumnSchema::new(
3119                    "field_0",
3120                    ConcreteDataType::string_datatype(),
3121                    true,
3122                ),
3123                semantic_type: SemanticType::Field,
3124                column_id: 3,
3125            },
3126        ];
3127        let create = RegionCreateRequest {
3128            engine: "mito".to_string(),
3129            column_metadatas,
3130            primary_key: vec![3, 4],
3131            options: HashMap::new(),
3132            table_dir: "path".to_string(),
3133            path_type: PathType::Bare,
3134            partition_expr_json: Some("".to_string()),
3135            requirements: Default::default(),
3136        };
3137
3138        assert!(create.validate().is_err());
3139    }
3140
3141    #[test]
3142    fn test_parse_create_region_requirements_defaults_to_empty() {
3143        let create = CreateRequest {
3144            region_id: RegionId::new(42, 0).as_u64(),
3145            engine: "mito".to_string(),
3146            column_defs: vec![],
3147            primary_key: vec![],
3148            path: "test".to_string(),
3149            options: HashMap::new(),
3150            partition: None,
3151            requirements: None,
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::empty());
3161    }
3162
3163    #[test]
3164    fn test_parse_create_region_requirements_from_proto() {
3165        let create = CreateRequest {
3166            region_id: RegionId::new(42, 0).as_u64(),
3167            engine: "mito".to_string(),
3168            column_defs: vec![],
3169            primary_key: vec![],
3170            path: "test".to_string(),
3171            options: HashMap::new(),
3172            partition: None,
3173            requirements: Some(api::v1::region::RegionRequirements {
3174                object_storage: true,
3175            }),
3176        };
3177
3178        let requests =
3179            RegionRequest::try_from_request_body(region_request::Body::Create(create)).unwrap();
3180        let RegionRequest::Create(request) = &requests[0].1 else {
3181            unreachable!()
3182        };
3183
3184        assert_eq!(request.requirements, RegionRequirements::object_storage());
3185    }
3186
3187    #[test]
3188    fn test_parse_region_cleanup_from_proto() {
3189        let clean_up = api::v1::region::CleanUpRequest {
3190            region_id: RegionId::new(42, 3).as_u64(),
3191            engine: "mito".to_string(),
3192            path: "test".to_string(),
3193            options: HashMap::from([("k".to_string(), "v".to_string())]),
3194        };
3195
3196        let requests =
3197            RegionRequest::try_from_request_body(region_request::Body::CleanUp(clean_up)).unwrap();
3198        let RegionRequest::CleanUp(request) = &requests[0].1 else {
3199            unreachable!()
3200        };
3201
3202        assert_eq!(requests[0].0, RegionId::new(42, 3));
3203        assert_eq!(request.engine, "mito");
3204        assert_eq!(request.table_dir, "data/test/42/");
3205        assert_eq!(request.path_type, PathType::Bare);
3206        assert_eq!(request.options.get("k"), Some(&"v".to_string()));
3207    }
3208
3209    #[test]
3210    fn test_validate_modify_column_fulltext_options() {
3211        let kind = AlterKind::SetIndexes {
3212            options: vec![SetIndexOption::Fulltext {
3213                column_name: "tag_0".to_string(),
3214                options: FulltextOptions::new_unchecked(
3215                    true,
3216                    FulltextAnalyzer::Chinese,
3217                    false,
3218                    FulltextBackend::Bloom,
3219                    1000,
3220                    0.01,
3221                ),
3222            }],
3223        };
3224        let request = RegionAlterRequest { kind };
3225        let mut metadata = new_metadata();
3226        metadata.schema_version = 1;
3227        request.validate(&metadata).unwrap();
3228
3229        let kind = AlterKind::UnsetIndexes {
3230            options: vec![UnsetIndexOption::Fulltext {
3231                column_name: "tag_0".to_string(),
3232            }],
3233        };
3234        let request = RegionAlterRequest { kind };
3235        let mut metadata = new_metadata();
3236        metadata.schema_version = 1;
3237        request.validate(&metadata).unwrap();
3238    }
3239
3240    #[test]
3241    fn test_validate_sync_columns() {
3242        let metadata = new_metadata();
3243        let kind = AlterKind::SyncColumns {
3244            column_metadatas: vec![
3245                ColumnMetadata {
3246                    column_schema: ColumnSchema::new(
3247                        "tag_1",
3248                        ConcreteDataType::string_datatype(),
3249                        true,
3250                    ),
3251                    semantic_type: SemanticType::Tag,
3252                    column_id: 5,
3253                },
3254                ColumnMetadata {
3255                    column_schema: ColumnSchema::new(
3256                        "field_2",
3257                        ConcreteDataType::string_datatype(),
3258                        true,
3259                    ),
3260                    semantic_type: SemanticType::Field,
3261                    column_id: 6,
3262                },
3263            ],
3264        };
3265        let err = kind.validate(&metadata).unwrap_err();
3266        assert!(err.to_string().contains("not a primary key"));
3267
3268        // Change the timestamp column name.
3269        let mut column_metadatas_with_different_ts_column = metadata.column_metadatas.clone();
3270        let ts_column = column_metadatas_with_different_ts_column
3271            .iter_mut()
3272            .find(|c| c.semantic_type == SemanticType::Timestamp)
3273            .unwrap();
3274        ts_column.column_schema.name = "ts1".to_string();
3275
3276        let kind = AlterKind::SyncColumns {
3277            column_metadatas: column_metadatas_with_different_ts_column,
3278        };
3279        let err = kind.validate(&metadata).unwrap_err();
3280        assert!(
3281            err.to_string()
3282                .contains("timestamp column ts has different id")
3283        );
3284
3285        // Change the primary key column name.
3286        let mut column_metadatas_with_different_pk_column = metadata.column_metadatas.clone();
3287        let pk_column = column_metadatas_with_different_pk_column
3288            .iter_mut()
3289            .find(|c| c.column_schema.name == "tag_0")
3290            .unwrap();
3291        pk_column.column_id = 100;
3292        let kind = AlterKind::SyncColumns {
3293            column_metadatas: column_metadatas_with_different_pk_column,
3294        };
3295        let err = kind.validate(&metadata).unwrap_err();
3296        assert!(
3297            err.to_string()
3298                .contains("column with same name tag_0 has different id")
3299        );
3300
3301        // Add a new field column.
3302        let mut column_metadatas_with_new_field_column = metadata.column_metadatas.clone();
3303        column_metadatas_with_new_field_column.push(ColumnMetadata {
3304            column_schema: ColumnSchema::new("field_2", ConcreteDataType::string_datatype(), true),
3305            semantic_type: SemanticType::Field,
3306            column_id: 4,
3307        });
3308        let kind = AlterKind::SyncColumns {
3309            column_metadatas: column_metadatas_with_new_field_column,
3310        };
3311        kind.validate(&metadata).unwrap();
3312    }
3313
3314    #[test]
3315    fn test_cast_path_type_to_primitive() {
3316        assert_eq!(PathType::Bare as u8, 0);
3317        assert_eq!(PathType::Data as u8, 1);
3318        assert_eq!(PathType::Metadata as u8, 2);
3319        assert_eq!(
3320            PathType::try_from(PathType::Bare as u8).unwrap(),
3321            PathType::Bare
3322        );
3323        assert_eq!(
3324            PathType::try_from(PathType::Data as u8).unwrap(),
3325            PathType::Data
3326        );
3327        assert_eq!(
3328            PathType::try_from(PathType::Metadata as u8).unwrap(),
3329            PathType::Metadata
3330        );
3331    }
3332}