Skip to main content

mito2/region/
options.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
15//! Options for a region.
16//!
17//! If we add options in this mod, we also need to modify [store_api::mito_engine_options].
18
19use std::collections::HashMap;
20use std::time::Duration;
21
22use common_base::readable_size::ReadableSize;
23use common_stat::get_total_memory_readable;
24use common_time::TimeToLive;
25use common_wal::options::{WAL_OPTIONS_KEY, WalOptions};
26use serde::de::Error as _;
27use serde::{Deserialize, Deserializer, Serialize};
28use serde_json::Value;
29use serde_with::{DisplayFromStr, NoneAsEmptyString, serde_as, with_prefix};
30use snafu::{ResultExt, ensure};
31use store_api::codec::PrimaryKeyEncoding;
32use store_api::mito_engine_options::COMPACTION_OVERRIDE;
33use store_api::storage::ColumnId;
34use strum::EnumString;
35
36use crate::error::{Error, InvalidRegionOptionsSnafu, JsonOptionsSnafu, Result};
37use crate::memtable::partition_tree::{DEFAULT_FREEZE_THRESHOLD, DEFAULT_MAX_KEYS_PER_SHARD};
38use crate::sst::FormatType;
39
40const DEFAULT_INDEX_SEGMENT_ROW_COUNT: usize = 1024;
41const COMPACTION_TWCS_PREFIX: &str = "compaction.twcs.";
42const MEMTABLE_PARTITION_TREE_PREFIX: &str = "memtable.partition_tree.";
43
44pub(crate) fn parse_wal_options(
45    options_map: &HashMap<String, String>,
46) -> std::result::Result<WalOptions, serde_json::Error> {
47    options_map
48        .get(WAL_OPTIONS_KEY)
49        .map_or(Ok(WalOptions::default()), |encoded_wal_options| {
50            serde_json::from_str(encoded_wal_options)
51        })
52}
53
54/// Mode to handle duplicate rows while merging.
55#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, EnumString)]
56#[serde(rename_all = "snake_case")]
57#[strum(serialize_all = "snake_case")]
58pub enum MergeMode {
59    /// Keeps the last row.
60    #[default]
61    LastRow,
62    /// Keeps the last non-null field for each row.
63    LastNonNull,
64}
65
66// Note: We need to update [store_api::mito_engine_options::is_mito_engine_option_key()]
67// if we want expose the option to table options.
68/// Options that affect the entire region.
69///
70/// Users need to specify the options while creating/opening a region.
71#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
72#[serde(default)]
73pub struct RegionOptions {
74    /// Region SST files TTL.
75    pub ttl: Option<TimeToLive>,
76    /// Compaction options.
77    pub compaction: CompactionOptions,
78    pub compaction_override: bool,
79    /// Custom storage. Uses default storage if it is `None`.
80    pub storage: Option<String>,
81    /// If append mode is enabled, the region keeps duplicate rows.
82    pub append_mode: bool,
83    /// Wal options.
84    pub wal_options: WalOptions,
85    /// Index options.
86    pub index_options: IndexOptions,
87    /// Memtable options.
88    pub memtable: Option<MemtableOptions>,
89    /// The mode to merge duplicate rows.
90    /// Only takes effect when `append_mode` is `false`.
91    pub merge_mode: Option<MergeMode>,
92    /// SST format type.
93    pub sst_format: Option<FormatType>,
94}
95
96impl RegionOptions {
97    /// Validates options.
98    pub fn validate(&self) -> Result<()> {
99        if self.append_mode {
100            ensure!(
101                self.merge_mode
102                    .is_none_or(|mode| mode == MergeMode::LastRow),
103                InvalidRegionOptionsSnafu {
104                    reason: "only last_row merge_mode is allowed when append_mode is enabled",
105                }
106            );
107        }
108        Ok(())
109    }
110
111    /// Returns `true` if deduplication is needed.
112    pub fn need_dedup(&self) -> bool {
113        !self.append_mode
114    }
115
116    /// Returns the `merge_mode` if it is set, otherwise returns the default [`MergeMode`].
117    pub fn merge_mode(&self) -> MergeMode {
118        self.merge_mode.unwrap_or_default()
119    }
120
121    /// Returns the `primary_key_encoding` if it is set, otherwise returns the default [`PrimaryKeyEncoding`].
122    pub fn primary_key_encoding(&self) -> PrimaryKeyEncoding {
123        self.memtable
124            .as_ref()
125            .map_or(PrimaryKeyEncoding::default(), |memtable| {
126                memtable.primary_key_encoding()
127            })
128    }
129}
130
131impl TryFrom<&HashMap<String, String>> for RegionOptions {
132    type Error = Error;
133
134    fn try_from(options_map: &HashMap<String, String>) -> Result<Self> {
135        let value = options_map_to_value(options_map);
136        let json = serde_json::to_string(&value).context(JsonOptionsSnafu)?;
137
138        // #[serde(flatten)] doesn't work with #[serde(default)] so we need to parse
139        // each field manually instead of using #[serde(flatten)] for `compaction`.
140        // See https://github.com/serde-rs/serde/issues/1626
141        let options: RegionOptionsWithoutEnum =
142            serde_json::from_str(&json).context(JsonOptionsSnafu)?;
143        let has_compaction_type =
144            validate_enum_options(options_map, "compaction.type", &[COMPACTION_TWCS_PREFIX])?;
145        let compaction = if has_compaction_type {
146            serde_json::from_str(&json).context(JsonOptionsSnafu)?
147        } else {
148            CompactionOptions::default()
149        };
150
151        let wal_options = parse_wal_options(options_map).context(JsonOptionsSnafu)?;
152
153        let index_options: IndexOptions = serde_json::from_str(&json).context(JsonOptionsSnafu)?;
154        let memtable = if validate_enum_options(
155            options_map,
156            "memtable.type",
157            &[MEMTABLE_PARTITION_TREE_PREFIX],
158        )? {
159            Some(serde_json::from_str(&json).context(JsonOptionsSnafu)?)
160        } else {
161            None
162        };
163
164        let compaction_override_flag = options_map
165            .get(COMPACTION_OVERRIDE)
166            .map(|v| matches!(v.to_lowercase().as_str(), "true" | "1"))
167            .unwrap_or(false);
168        let compaction_override = has_compaction_type || compaction_override_flag;
169
170        let opts = RegionOptions {
171            ttl: options.ttl,
172            compaction,
173            compaction_override,
174            storage: options.storage,
175            append_mode: options.append_mode,
176            wal_options,
177            index_options,
178            memtable,
179            merge_mode: options.merge_mode,
180            sst_format: options.sst_format,
181        };
182        opts.validate()?;
183
184        Ok(opts)
185    }
186}
187
188/// Options for compactions
189#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
190#[serde(tag = "compaction.type")]
191#[serde(rename_all = "snake_case")]
192pub enum CompactionOptions {
193    /// Time window compaction strategy.
194    #[serde(with = "prefix_twcs")]
195    Twcs(TwcsOptions),
196}
197
198impl CompactionOptions {
199    pub(crate) fn time_window(&self) -> Option<Duration> {
200        match self {
201            CompactionOptions::Twcs(opts) => opts.time_window,
202        }
203    }
204
205    pub(crate) fn remote_compaction(&self) -> bool {
206        match self {
207            CompactionOptions::Twcs(opts) => opts.remote_compaction,
208        }
209    }
210
211    pub(crate) fn fallback_to_local(&self) -> bool {
212        match self {
213            CompactionOptions::Twcs(opts) => opts.fallback_to_local,
214        }
215    }
216}
217
218impl Default for CompactionOptions {
219    fn default() -> Self {
220        Self::Twcs(TwcsOptions::default())
221    }
222}
223
224/// Time window compaction options.
225#[serde_as]
226#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
227#[serde(default)]
228pub struct TwcsOptions {
229    /// Minimum file num in every time window to trigger a compaction.
230    #[serde_as(as = "DisplayFromStr")]
231    pub trigger_file_num: usize,
232    /// Compaction time window defined when creating tables.
233    #[serde(with = "humantime_serde")]
234    pub time_window: Option<Duration>,
235    /// Compaction time window defined when creating tables.
236    pub max_output_file_size: Option<ReadableSize>,
237    /// Whether to use remote compaction.
238    #[serde_as(as = "DisplayFromStr")]
239    pub remote_compaction: bool,
240    /// Whether to fall back to local compaction if remote compaction fails.
241    #[serde_as(as = "DisplayFromStr")]
242    pub fallback_to_local: bool,
243}
244
245with_prefix!(prefix_twcs "compaction.twcs.");
246
247impl TwcsOptions {
248    /// Returns time window in second resolution.
249    pub fn time_window_seconds(&self) -> Option<i64> {
250        self.time_window.and_then(|window| {
251            let window_secs = window.as_secs();
252            if window_secs == 0 {
253                None
254            } else {
255                window_secs.try_into().ok()
256            }
257        })
258    }
259}
260
261impl Default for TwcsOptions {
262    fn default() -> Self {
263        Self {
264            trigger_file_num: 4,
265            time_window: None,
266            max_output_file_size: Some(ReadableSize::mb(512)),
267            remote_compaction: false,
268            fallback_to_local: true,
269        }
270    }
271}
272
273/// We need to define a new struct without enum fields as `#[serde(default)]` does not
274/// support external tagging.
275#[serde_as]
276#[derive(Debug, Deserialize)]
277#[serde(default)]
278struct RegionOptionsWithoutEnum {
279    /// Region SST files TTL.
280    ttl: Option<TimeToLive>,
281    storage: Option<String>,
282    #[serde_as(as = "DisplayFromStr")]
283    append_mode: bool,
284    #[serde_as(as = "NoneAsEmptyString")]
285    merge_mode: Option<MergeMode>,
286    #[serde_as(as = "NoneAsEmptyString")]
287    sst_format: Option<FormatType>,
288}
289
290impl Default for RegionOptionsWithoutEnum {
291    fn default() -> Self {
292        let options = RegionOptions::default();
293        RegionOptionsWithoutEnum {
294            ttl: options.ttl,
295            storage: options.storage,
296            append_mode: options.append_mode,
297            merge_mode: options.merge_mode,
298            sst_format: options.sst_format,
299        }
300    }
301}
302
303with_prefix!(prefix_inverted_index "index.inverted_index.");
304
305/// Options for index.
306#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
307#[serde(default)]
308pub struct IndexOptions {
309    /// Options for the inverted index.
310    #[serde(flatten, with = "prefix_inverted_index")]
311    pub inverted_index: InvertedIndexOptions,
312}
313
314/// Options for the inverted index.
315#[serde_as]
316#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
317#[serde(default)]
318pub struct InvertedIndexOptions {
319    /// The column ids that should be ignored when building the inverted index.
320    /// The column ids are separated by commas. For example, "1,2,3".
321    #[serde(deserialize_with = "deserialize_ignore_column_ids")]
322    #[serde(serialize_with = "serialize_ignore_column_ids")]
323    pub ignore_column_ids: Vec<ColumnId>,
324
325    /// The number of rows in a segment.
326    #[serde_as(as = "DisplayFromStr")]
327    pub segment_row_count: usize,
328}
329
330impl Default for InvertedIndexOptions {
331    fn default() -> Self {
332        Self {
333            ignore_column_ids: Vec::new(),
334            segment_row_count: DEFAULT_INDEX_SEGMENT_ROW_COUNT,
335        }
336    }
337}
338
339/// Options for region level memtable.
340#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
341#[serde(tag = "memtable.type", rename_all = "snake_case")]
342pub enum MemtableOptions {
343    TimeSeries,
344    #[serde(with = "prefix_partition_tree")]
345    PartitionTree(PartitionTreeOptions),
346}
347
348with_prefix!(prefix_partition_tree "memtable.partition_tree.");
349
350impl MemtableOptions {
351    /// Returns the primary key encoding mode.
352    pub fn primary_key_encoding(&self) -> PrimaryKeyEncoding {
353        match self {
354            MemtableOptions::PartitionTree(opts) => opts.primary_key_encoding,
355            _ => PrimaryKeyEncoding::Dense,
356        }
357    }
358}
359
360/// Partition tree memtable options.
361#[serde_as]
362#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(default)]
364pub struct PartitionTreeOptions {
365    /// Max keys in an index shard.
366    #[serde_as(as = "DisplayFromStr")]
367    pub index_max_keys_per_shard: usize,
368    /// Number of rows to freeze a data part.
369    #[serde_as(as = "DisplayFromStr")]
370    pub data_freeze_threshold: usize,
371    /// Total bytes of dictionary to keep in fork.
372    pub fork_dictionary_bytes: ReadableSize,
373    /// Primary key encoding mode.
374    pub primary_key_encoding: PrimaryKeyEncoding,
375}
376
377impl Default for PartitionTreeOptions {
378    fn default() -> Self {
379        let mut fork_dictionary_bytes = ReadableSize::mb(512);
380        if let Some(total_memory) = get_total_memory_readable() {
381            let adjust_dictionary_bytes = std::cmp::min(
382                total_memory / crate::memtable::partition_tree::DICTIONARY_SIZE_FACTOR,
383                fork_dictionary_bytes,
384            );
385            if adjust_dictionary_bytes.0 > 0 {
386                fork_dictionary_bytes = adjust_dictionary_bytes;
387            }
388        }
389        Self {
390            index_max_keys_per_shard: DEFAULT_MAX_KEYS_PER_SHARD,
391            data_freeze_threshold: DEFAULT_FREEZE_THRESHOLD,
392            fork_dictionary_bytes,
393            primary_key_encoding: PrimaryKeyEncoding::Dense,
394        }
395    }
396}
397
398fn deserialize_ignore_column_ids<'de, D>(deserializer: D) -> Result<Vec<ColumnId>, D::Error>
399where
400    D: Deserializer<'de>,
401{
402    let s: String = Deserialize::deserialize(deserializer)?;
403    let mut column_ids = Vec::new();
404    if s.is_empty() {
405        return Ok(column_ids);
406    }
407    for item in s.split(',') {
408        let column_id = item.parse().map_err(D::Error::custom)?;
409        column_ids.push(column_id);
410    }
411    Ok(column_ids)
412}
413
414fn serialize_ignore_column_ids<S>(column_ids: &[ColumnId], serializer: S) -> Result<S::Ok, S::Error>
415where
416    S: serde::Serializer,
417{
418    let s = column_ids
419        .iter()
420        .map(|id| id.to_string())
421        .collect::<Vec<_>>()
422        .join(",");
423    serializer.serialize_str(&s)
424}
425
426/// Converts the `options` map to a json object.
427///
428/// Replaces "null" strings by `null` json values.
429fn options_map_to_value(options: &HashMap<String, String>) -> Value {
430    let map = options
431        .iter()
432        .map(|(key, value)| {
433            // Only convert the key to lowercase.
434            if value.eq_ignore_ascii_case("null") {
435                (key.clone(), Value::Null)
436            } else {
437                (key.clone(), Value::from(value.clone()))
438            }
439        })
440        .collect();
441    Value::Object(map)
442}
443
444// `#[serde(default)]` doesn't support enum (https://github.com/serde-rs/serde/issues/1799) so we
445// check the type key first.
446/// Validates whether the `options_map` has valid options for specific `enum_tag_key`
447/// and returns `true` if the map contains the enum tag.
448///
449/// Variant options must start with one of `enum_option_prefixes`. If variant options
450/// are provided, the tagged enum type key must also be provided.
451fn validate_enum_options(
452    options_map: &HashMap<String, String>,
453    enum_tag_key: &str,
454    enum_option_prefixes: &[&str],
455) -> Result<bool> {
456    let mut has_enum_options = false;
457    let mut has_tag = false;
458    for key in options_map.keys() {
459        if key == enum_tag_key {
460            has_tag = true;
461        } else if !has_enum_options
462            && enum_option_prefixes
463                .iter()
464                .any(|prefix| key.starts_with(prefix))
465        {
466            has_enum_options = true;
467        }
468
469        if has_tag && has_enum_options {
470            break;
471        }
472    }
473
474    // If tag is not provided, then other options for the enum should not exist.
475    ensure!(
476        has_tag || !has_enum_options,
477        InvalidRegionOptionsSnafu {
478            reason: format!("missing key {} in options", enum_tag_key),
479        }
480    );
481
482    Ok(has_tag)
483}
484
485#[cfg(test)]
486mod tests {
487    use common_error::ext::ErrorExt;
488    use common_error::status_code::StatusCode;
489    use common_wal::options::KafkaWalOptions;
490
491    use super::*;
492
493    fn make_map(options: &[(&str, &str)]) -> HashMap<String, String> {
494        options
495            .iter()
496            .map(|(k, v)| (k.to_string(), v.to_string()))
497            .collect()
498    }
499
500    #[test]
501    fn test_empty_region_options() {
502        let map = make_map(&[]);
503        let options = RegionOptions::try_from(&map).unwrap();
504        assert_eq!(RegionOptions::default(), options);
505    }
506
507    #[test]
508    fn test_with_ttl() {
509        let map = make_map(&[("ttl", "7d")]);
510        let options = RegionOptions::try_from(&map).unwrap();
511        let expect = RegionOptions {
512            ttl: Some(Duration::from_secs(3600 * 24 * 7).into()),
513            ..Default::default()
514        };
515        assert_eq!(expect, options);
516    }
517
518    #[test]
519    fn test_with_storage() {
520        let map = make_map(&[("storage", "S3")]);
521        let options = RegionOptions::try_from(&map).unwrap();
522        let expect = RegionOptions {
523            storage: Some("S3".to_string()),
524            ..Default::default()
525        };
526        assert_eq!(expect, options);
527    }
528
529    #[test]
530    fn test_without_compaction_type() {
531        let map = make_map(&[
532            ("compaction.twcs.trigger_file_num", "8"),
533            ("compaction.twcs.time_window", "2h"),
534        ]);
535        let err = RegionOptions::try_from(&map).unwrap_err();
536        assert_eq!(StatusCode::InvalidArguments, err.status_code());
537    }
538
539    #[test]
540    fn test_with_compaction_type() {
541        let map = make_map(&[
542            ("compaction.twcs.trigger_file_num", "8"),
543            ("compaction.twcs.time_window", "2h"),
544            ("compaction.type", "twcs"),
545        ]);
546        let options = RegionOptions::try_from(&map).unwrap();
547        let expect = RegionOptions {
548            compaction: CompactionOptions::Twcs(TwcsOptions {
549                trigger_file_num: 8,
550                time_window: Some(Duration::from_secs(3600 * 2)),
551                ..Default::default()
552            }),
553            compaction_override: true,
554            ..Default::default()
555        };
556        assert_eq!(expect, options);
557    }
558
559    #[test]
560    fn test_with_compaction_override_true_without_compaction_type() {
561        let map = make_map(&[(COMPACTION_OVERRIDE, "true")]);
562        let options = RegionOptions::try_from(&map).unwrap();
563        let expect = RegionOptions {
564            compaction_override: true,
565            ..Default::default()
566        };
567        assert_eq!(expect, options);
568    }
569
570    #[test]
571    fn test_with_compaction_override_false_without_compaction_type() {
572        let map = make_map(&[(COMPACTION_OVERRIDE, "false")]);
573        let options = RegionOptions::try_from(&map).unwrap();
574        assert_eq!(RegionOptions::default(), options);
575    }
576
577    #[test]
578    fn test_compaction_twcs_options_still_require_compaction_type_with_override() {
579        let map = make_map(&[
580            (COMPACTION_OVERRIDE, "true"),
581            ("compaction.twcs.time_window", "2h"),
582        ]);
583        let err = RegionOptions::try_from(&map).unwrap_err();
584        assert_eq!(StatusCode::InvalidArguments, err.status_code());
585    }
586
587    fn test_with_wal_options(wal_options: &WalOptions) -> bool {
588        let encoded_wal_options = serde_json::to_string(&wal_options).unwrap();
589        let map = make_map(&[(WAL_OPTIONS_KEY, &encoded_wal_options)]);
590        let got = RegionOptions::try_from(&map).unwrap();
591        let expect = RegionOptions {
592            wal_options: wal_options.clone(),
593            ..Default::default()
594        };
595        expect == got
596    }
597
598    #[test]
599    fn test_with_index() {
600        let map = make_map(&[
601            ("index.inverted_index.ignore_column_ids", "1,2,3"),
602            ("index.inverted_index.segment_row_count", "512"),
603        ]);
604        let options = RegionOptions::try_from(&map).unwrap();
605        let expect = RegionOptions {
606            index_options: IndexOptions {
607                inverted_index: InvertedIndexOptions {
608                    ignore_column_ids: vec![1, 2, 3],
609                    segment_row_count: 512,
610                },
611            },
612            ..Default::default()
613        };
614        assert_eq!(expect, options);
615    }
616
617    // No need to add compatible tests for RegionOptions since the above tests already check for compatibility.
618    #[test]
619    fn test_with_any_wal_options() {
620        let all_wal_options = [
621            WalOptions::RaftEngine,
622            WalOptions::Kafka(KafkaWalOptions {
623                topic: "test_topic".to_string(),
624            }),
625        ];
626        all_wal_options.iter().all(test_with_wal_options);
627    }
628
629    #[test]
630    fn test_with_memtable() {
631        let map = make_map(&[("memtable.type", "time_series")]);
632        let options = RegionOptions::try_from(&map).unwrap();
633        let expect = RegionOptions {
634            memtable: Some(MemtableOptions::TimeSeries),
635            ..Default::default()
636        };
637        assert_eq!(expect, options);
638
639        let map = make_map(&[("memtable.type", "partition_tree")]);
640        let options = RegionOptions::try_from(&map).unwrap();
641        let expect = RegionOptions {
642            memtable: Some(MemtableOptions::PartitionTree(
643                PartitionTreeOptions::default(),
644            )),
645            ..Default::default()
646        };
647        assert_eq!(expect, options);
648    }
649
650    #[test]
651    fn test_unknown_memtable_type() {
652        let map = make_map(&[("memtable.type", "no_such_memtable")]);
653        let err = RegionOptions::try_from(&map).unwrap_err();
654        assert_eq!(StatusCode::InvalidArguments, err.status_code());
655    }
656
657    #[test]
658    fn test_without_memtable_type() {
659        let map = make_map(&[("memtable.partition_tree.index_max_keys_per_shard", "2048")]);
660        let err = RegionOptions::try_from(&map).unwrap_err();
661        assert_eq!(StatusCode::InvalidArguments, err.status_code());
662    }
663
664    #[test]
665    fn test_with_merge_mode() {
666        let map = make_map(&[("merge_mode", "last_row")]);
667        let options = RegionOptions::try_from(&map).unwrap();
668        assert_eq!(MergeMode::LastRow, options.merge_mode());
669
670        let map = make_map(&[("merge_mode", "last_non_null")]);
671        let options = RegionOptions::try_from(&map).unwrap();
672        assert_eq!(MergeMode::LastNonNull, options.merge_mode());
673
674        let map = make_map(&[("merge_mode", "unknown")]);
675        let err = RegionOptions::try_from(&map).unwrap_err();
676        assert_eq!(StatusCode::InvalidArguments, err.status_code());
677    }
678
679    #[test]
680    fn test_append_mode_allows_last_row_merge_mode() {
681        let map = make_map(&[("append_mode", "true"), ("merge_mode", "last_row")]);
682        let options = RegionOptions::try_from(&map).unwrap();
683        assert!(options.append_mode);
684        assert_eq!(MergeMode::LastRow, options.merge_mode());
685
686        let map = make_map(&[("append_mode", "true"), ("merge_mode", "last_non_null")]);
687        let err = RegionOptions::try_from(&map).unwrap_err();
688        assert_eq!(StatusCode::InvalidArguments, err.status_code());
689    }
690
691    #[test]
692    fn test_with_all() {
693        let wal_options = WalOptions::Kafka(KafkaWalOptions {
694            topic: "test_topic".to_string(),
695        });
696        let map = make_map(&[
697            ("ttl", "7d"),
698            ("compaction.twcs.trigger_file_num", "8"),
699            ("compaction.twcs.max_output_file_size", "1GB"),
700            ("compaction.twcs.time_window", "2h"),
701            ("compaction.type", "twcs"),
702            ("compaction.twcs.remote_compaction", "false"),
703            ("compaction.twcs.fallback_to_local", "true"),
704            ("storage", "S3"),
705            ("append_mode", "false"),
706            ("index.inverted_index.ignore_column_ids", "1,2,3"),
707            ("index.inverted_index.segment_row_count", "512"),
708            (
709                WAL_OPTIONS_KEY,
710                &serde_json::to_string(&wal_options).unwrap(),
711            ),
712            ("memtable.type", "partition_tree"),
713            ("memtable.partition_tree.index_max_keys_per_shard", "2048"),
714            ("memtable.partition_tree.data_freeze_threshold", "2048"),
715            ("memtable.partition_tree.fork_dictionary_bytes", "128M"),
716            ("merge_mode", "last_non_null"),
717        ]);
718        let options = RegionOptions::try_from(&map).unwrap();
719        let expect = RegionOptions {
720            ttl: Some(Duration::from_secs(3600 * 24 * 7).into()),
721            compaction: CompactionOptions::Twcs(TwcsOptions {
722                trigger_file_num: 8,
723                time_window: Some(Duration::from_secs(3600 * 2)),
724                max_output_file_size: Some(ReadableSize::gb(1)),
725                remote_compaction: false,
726                fallback_to_local: true,
727            }),
728            compaction_override: true,
729            storage: Some("S3".to_string()),
730            append_mode: false,
731            wal_options,
732            index_options: IndexOptions {
733                inverted_index: InvertedIndexOptions {
734                    ignore_column_ids: vec![1, 2, 3],
735                    segment_row_count: 512,
736                },
737            },
738            memtable: Some(MemtableOptions::PartitionTree(PartitionTreeOptions {
739                index_max_keys_per_shard: 2048,
740                data_freeze_threshold: 2048,
741                fork_dictionary_bytes: ReadableSize::mb(128),
742                primary_key_encoding: PrimaryKeyEncoding::Dense,
743            })),
744            merge_mode: Some(MergeMode::LastNonNull),
745            sst_format: None,
746        };
747        assert_eq!(expect, options);
748    }
749
750    #[test]
751    fn test_region_options_serde() {
752        let options = RegionOptions {
753            ttl: Some(Duration::from_secs(3600 * 24 * 7).into()),
754            compaction: CompactionOptions::Twcs(TwcsOptions {
755                trigger_file_num: 8,
756                time_window: Some(Duration::from_secs(3600 * 2)),
757                max_output_file_size: None,
758                remote_compaction: false,
759                fallback_to_local: true,
760            }),
761            compaction_override: false,
762            storage: Some("S3".to_string()),
763            append_mode: false,
764            wal_options: WalOptions::Kafka(KafkaWalOptions {
765                topic: "test_topic".to_string(),
766            }),
767            index_options: IndexOptions {
768                inverted_index: InvertedIndexOptions {
769                    ignore_column_ids: vec![1, 2, 3],
770                    segment_row_count: 512,
771                },
772            },
773            memtable: Some(MemtableOptions::PartitionTree(PartitionTreeOptions {
774                index_max_keys_per_shard: 2048,
775                data_freeze_threshold: 2048,
776                fork_dictionary_bytes: ReadableSize::mb(128),
777                primary_key_encoding: PrimaryKeyEncoding::Dense,
778            })),
779            merge_mode: Some(MergeMode::LastNonNull),
780            sst_format: None,
781        };
782        let region_options_json_str = serde_json::to_string(&options).unwrap();
783        let got: RegionOptions = serde_json::from_str(&region_options_json_str).unwrap();
784        assert_eq!(options, got);
785    }
786
787    #[test]
788    fn test_region_options_str_serde() {
789        // Notes: use empty string for `ignore_column_ids` to test the empty string case.
790        let region_options_json_str = r#"{
791  "ttl": "7days",
792  "compaction": {
793    "compaction.type": "twcs",
794    "compaction.twcs.trigger_file_num": "8",
795    "compaction.twcs.max_output_file_size": "7MB",
796    "compaction.twcs.time_window": "2h"
797  },
798  "storage": "S3",
799  "append_mode": false,
800  "wal_options": {
801    "wal.provider": "kafka",
802    "wal.kafka.topic": "test_topic"
803  },
804  "index_options": {
805    "index.inverted_index.ignore_column_ids": "",
806    "index.inverted_index.segment_row_count": "512"
807  },
808  "memtable": {
809    "memtable.type": "partition_tree",
810    "memtable.partition_tree.index_max_keys_per_shard": "2048",
811    "memtable.partition_tree.data_freeze_threshold": "2048",
812    "memtable.partition_tree.fork_dictionary_bytes": "128MiB"
813  },
814  "merge_mode": "last_non_null"
815}"#;
816        let got: RegionOptions = serde_json::from_str(region_options_json_str).unwrap();
817        let options = RegionOptions {
818            ttl: Some(Duration::from_secs(3600 * 24 * 7).into()),
819            compaction: CompactionOptions::Twcs(TwcsOptions {
820                trigger_file_num: 8,
821                time_window: Some(Duration::from_secs(3600 * 2)),
822                max_output_file_size: Some(ReadableSize::mb(7)),
823                remote_compaction: false,
824                fallback_to_local: true,
825            }),
826            compaction_override: false,
827            storage: Some("S3".to_string()),
828            append_mode: false,
829            wal_options: WalOptions::Kafka(KafkaWalOptions {
830                topic: "test_topic".to_string(),
831            }),
832            index_options: IndexOptions {
833                inverted_index: InvertedIndexOptions {
834                    ignore_column_ids: vec![],
835                    segment_row_count: 512,
836                },
837            },
838            memtable: Some(MemtableOptions::PartitionTree(PartitionTreeOptions {
839                index_max_keys_per_shard: 2048,
840                data_freeze_threshold: 2048,
841                fork_dictionary_bytes: ReadableSize::mb(128),
842                primary_key_encoding: PrimaryKeyEncoding::Dense,
843            })),
844            merge_mode: Some(MergeMode::LastNonNull),
845            sst_format: None,
846        };
847        assert_eq!(options, got);
848    }
849}