1use std::collections::{HashMap, HashSet};
18use std::fmt;
19use std::str::FromStr;
20
21use common_base::readable_size::ReadableSize;
22use common_datasource::object_store::oss::is_supported_in_oss;
23use common_datasource::object_store::s3::is_supported_in_s3;
24use common_query::AddColumnLocation;
25use common_time::TimeToLive;
26use common_time::range::TimestampRange;
27use datatypes::data_type::ConcreteDataType;
28use datatypes::prelude::VectorRef;
29use datatypes::schema::{
30 ColumnDefaultConstraint, ColumnSchema, FulltextOptions, Schema, SkippingIndexOptions,
31};
32use greptime_proto::v1::region::compact_request;
33use once_cell::sync::Lazy;
34use serde::{Deserialize, Serialize};
35use store_api::metric_engine_consts::{
36 LOGICAL_TABLE_METADATA_KEY, PHYSICAL_TABLE_METADATA_KEY, is_metric_engine_option_key,
37};
38use store_api::mito_engine_options::{
39 APPEND_MODE_KEY, COMPACTION_TYPE, MEMTABLE_BULK_ENCODE_BYTES_THRESHOLD,
40 MEMTABLE_BULK_ENCODE_ROW_THRESHOLD, MEMTABLE_BULK_MAX_MERGE_GROUPS,
41 MEMTABLE_BULK_MERGE_THRESHOLD, MEMTABLE_TYPE, MERGE_MODE_KEY, SST_FORMAT_KEY,
42 TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER, TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM,
43 TWCS_FALLBACK_TO_LOCAL, TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER,
44 TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM, TWCS_MAX_OUTPUT_FILE_SIZE, TWCS_TIME_WINDOW,
45 TWCS_TRIGGER_FILE_NUM, is_mito_engine_option_key, normalize_twcs_trigger_options,
46};
47use store_api::region_request::{SetRegionOption, UnsetRegionOption};
48
49use crate::error::{ConflictingTableOptionsSnafu, ParseTableOptionSnafu, Result};
50use crate::metadata::{TableId, TableVersion};
51use crate::table_reference::TableReference;
52
53mod semantic;
54pub use semantic::*;
55
56pub const FILE_TABLE_META_KEY: &str = "__private.file_table_meta";
57pub const FILE_TABLE_LOCATION_KEY: &str = "location";
58pub const FILE_TABLE_PATTERN_KEY: &str = "pattern";
59pub const FILE_TABLE_FORMAT_KEY: &str = "format";
60
61pub const TABLE_DATA_MODEL: &str = "table_data_model";
62pub const TABLE_DATA_MODEL_TRACE_V1: &str = "greptime_trace_v1";
63
64pub fn is_trace_v1_table(table_info: &crate::metadata::TableInfo) -> bool {
68 table_info
69 .meta
70 .options
71 .extra_options
72 .get(TABLE_DATA_MODEL)
73 .map(|v| v == TABLE_DATA_MODEL_TRACE_V1)
74 .unwrap_or(false)
75}
76
77pub const OTLP_METRIC_COMPAT_KEY: &str = "otlp_metric_compat";
78pub const OTLP_METRIC_COMPAT_PROM: &str = "prom";
79
80pub const VALID_TABLE_OPTION_KEYS: [&str; 15] = [
81 WRITE_BUFFER_SIZE_KEY,
83 TTL_KEY,
84 STORAGE_KEY,
85 COMMENT_KEY,
86 SKIP_WAL_KEY,
87 SST_FORMAT_KEY,
88 FILE_TABLE_LOCATION_KEY,
90 FILE_TABLE_FORMAT_KEY,
91 FILE_TABLE_PATTERN_KEY,
92 PHYSICAL_TABLE_METADATA_KEY,
94 LOGICAL_TABLE_METADATA_KEY,
95 TABLE_DATA_MODEL,
97 OTLP_METRIC_COMPAT_KEY,
98 REPARTITION_COLUMN_HINT_KEY,
99 REPARTITION_PARTITION_NUM_HINT_KEY,
100];
101
102pub const DDL_TIMEOUT: &str = "timeout";
103pub const DDL_WAIT: &str = "wait";
104
105pub const VALID_DDL_OPTION_KEYS: [&str; 2] = [DDL_TIMEOUT, DDL_WAIT];
106
107static VALID_DB_OPT_KEYS: Lazy<HashSet<&str>> = Lazy::new(|| {
109 let mut set = HashSet::new();
110 set.insert(TTL_KEY);
111 set.insert(STORAGE_KEY);
112 set.insert(MEMTABLE_TYPE);
113 set.insert(MEMTABLE_BULK_MERGE_THRESHOLD);
114 set.insert(MEMTABLE_BULK_ENCODE_ROW_THRESHOLD);
115 set.insert(MEMTABLE_BULK_ENCODE_BYTES_THRESHOLD);
116 set.insert(MEMTABLE_BULK_MAX_MERGE_GROUPS);
117 set.insert(APPEND_MODE_KEY);
118 set.insert(MERGE_MODE_KEY);
119 set.insert(SKIP_WAL_KEY);
120 set.insert(COMPACTION_TYPE);
121 set.insert(TWCS_FALLBACK_TO_LOCAL);
122 set.insert(TWCS_TIME_WINDOW);
123 set.insert(TWCS_TRIGGER_FILE_NUM);
124 set.insert(TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM);
125 set.insert(TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER);
126 set.insert(TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM);
127 set.insert(TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER);
128 set.insert(TWCS_MAX_OUTPUT_FILE_SIZE);
129 set.insert(SST_FORMAT_KEY);
130 set
131});
132
133pub fn validate_database_option(key: &str) -> bool {
135 VALID_DB_OPT_KEYS.contains(&key)
136}
137
138pub fn validate_database_option_value(
140 key: &str,
141 value: Option<&str>,
142) -> std::result::Result<(), &'static str> {
143 let (minimum, constraint) = match key {
144 TWCS_TRIGGER_FILE_NUM
145 | TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM
146 | TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM => {
147 (0, "expected a non-negative integer fitting in usize")
148 }
149 TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER | TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER => {
150 (2, "expected an integer greater than or equal to 2")
151 }
152 _ => return Ok(()),
153 };
154 if value
155 .and_then(|value| value.parse::<usize>().ok())
156 .is_some_and(|files| files >= minimum)
157 {
158 Ok(())
159 } else {
160 Err(constraint)
161 }
162}
163
164pub fn validate_table_option(key: &str) -> bool {
166 if is_supported_in_s3(key) {
167 return true;
168 }
169
170 if is_supported_in_oss(key) {
171 return true;
172 }
173
174 if is_mito_engine_option_key(key) {
175 return true;
176 }
177
178 if is_metric_engine_option_key(key) {
179 return true;
180 }
181
182 if is_semantic_option_key(key) {
185 return true;
186 }
187
188 VALID_TABLE_OPTION_KEYS.contains(&key) || VALID_DDL_OPTION_KEYS.contains(&key)
189}
190
191#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
192#[serde(default)]
193pub struct TableOptions {
194 pub write_buffer_size: Option<ReadableSize>,
196 pub ttl: Option<TimeToLive>,
198 pub skip_wal: bool,
200 pub extra_options: HashMap<String, String>,
202}
203
204pub const WRITE_BUFFER_SIZE_KEY: &str = store_api::mito_engine_options::WRITE_BUFFER_SIZE_KEY;
205pub const TTL_KEY: &str = store_api::mito_engine_options::TTL_KEY;
206pub const STORAGE_KEY: &str = "storage";
207pub const COMMENT_KEY: &str = "comment";
208pub const AUTO_CREATE_TABLE_KEY: &str = "auto_create_table";
209pub const SKIP_WAL_KEY: &str = store_api::mito_engine_options::SKIP_WAL_KEY;
210pub const TRACE_TABLE_PARTITIONS_HINT_KEY: &str = "trace_table_partitions";
211pub const REPARTITION_COLUMN_HINT_KEY: &str = "repartition.column.hint";
212
213pub const REPARTITION_PARTITION_NUM_HINT_KEY: &str = "repartition.partition.num.hint";
215
216impl TableOptions {
217 pub fn try_from_iter<T: ToString, U: IntoIterator<Item = (T, T)>>(
218 iter: U,
219 ) -> Result<TableOptions> {
220 let mut options = TableOptions::default();
221
222 let mut kvs: HashMap<String, String> = iter
223 .into_iter()
224 .map(|(k, v)| (k.to_string(), v.to_string()))
225 .collect();
226
227 normalize_twcs_trigger_options(&mut kvs).map_err(|conflict| {
228 ConflictingTableOptionsSnafu {
229 first_key: TWCS_TRIGGER_FILE_NUM,
230 first_value: conflict.legacy_value,
231 second_key: TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM,
232 second_value: conflict.canonical_value,
233 }
234 .build()
235 })?;
236
237 if let Some(write_buffer_size) = kvs.get(WRITE_BUFFER_SIZE_KEY) {
238 let size = ReadableSize::from_str(write_buffer_size).map_err(|_| {
239 ParseTableOptionSnafu {
240 key: WRITE_BUFFER_SIZE_KEY,
241 value: write_buffer_size,
242 }
243 .build()
244 })?;
245 options.write_buffer_size = Some(size)
246 }
247
248 if let Some(ttl) = kvs.get(TTL_KEY) {
249 let ttl_value = TimeToLive::from_humantime_or_str(ttl).map_err(|_| {
250 ParseTableOptionSnafu {
251 key: TTL_KEY,
252 value: ttl,
253 }
254 .build()
255 })?;
256 options.ttl = Some(ttl_value);
257 }
258
259 if let Some(skip_wal) = kvs.get(SKIP_WAL_KEY) {
260 options.skip_wal = skip_wal.parse().map_err(|_| {
261 ParseTableOptionSnafu {
262 key: SKIP_WAL_KEY,
263 value: skip_wal,
264 }
265 .build()
266 })?;
267 }
268
269 options.extra_options = HashMap::from_iter(
270 kvs.into_iter()
271 .filter(|(k, _)| k != WRITE_BUFFER_SIZE_KEY && k != TTL_KEY),
272 );
273
274 Ok(options)
275 }
276}
277
278impl fmt::Display for TableOptions {
279 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
280 let mut key_vals = vec![];
281 if let Some(size) = self.write_buffer_size {
282 key_vals.push(format!("{}={}", WRITE_BUFFER_SIZE_KEY, size));
283 }
284
285 if let Some(ttl) = self.ttl.map(|ttl| ttl.to_string()) {
286 key_vals.push(format!("{}={}", TTL_KEY, ttl));
287 }
288
289 if self.skip_wal && !self.extra_options.contains_key(SKIP_WAL_KEY) {
290 key_vals.push(format!("{}={}", SKIP_WAL_KEY, self.skip_wal));
291 }
292
293 for (k, v) in &self.extra_options {
294 key_vals.push(format!("{}={}", k, v));
295 }
296
297 write!(f, "{}", key_vals.join(" "))
298 }
299}
300
301impl From<&TableOptions> for HashMap<String, String> {
302 fn from(opts: &TableOptions) -> Self {
303 let mut res = HashMap::with_capacity(3 + opts.extra_options.len());
304 if let Some(write_buffer_size) = opts.write_buffer_size {
305 let _ = res.insert(
306 WRITE_BUFFER_SIZE_KEY.to_string(),
307 write_buffer_size.to_string(),
308 );
309 }
310 if let Some(ttl_str) = opts.ttl.map(|ttl| ttl.to_string()) {
311 let _ = res.insert(TTL_KEY.to_string(), ttl_str);
312 }
313 if opts.skip_wal {
314 let _ = res.insert(SKIP_WAL_KEY.to_string(), true.to_string());
315 }
316 res.extend(opts.extra_options.clone());
317 res
318 }
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct AlterTableRequest {
324 pub catalog_name: String,
325 pub schema_name: String,
326 pub table_name: String,
327 pub table_id: TableId,
328 pub alter_kind: AlterKind,
329 pub table_version: Option<TableVersion>,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize)]
335pub struct AddColumnRequest {
336 pub column_schema: ColumnSchema,
337 pub is_key: bool,
338 pub location: Option<AddColumnLocation>,
339 pub add_if_not_exists: bool,
341}
342
343#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct ModifyColumnTypeRequest {
346 pub column_name: String,
347 pub target_type: ConcreteDataType,
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
354pub enum AnnotationFamily {
355 Semantic,
357 RepartitionHint,
359}
360
361impl AnnotationFamily {
362 pub fn namespace(self) -> &'static str {
364 match self {
365 Self::Semantic => SEMANTIC_PREFIX,
366 Self::RepartitionHint => "repartition.",
367 }
368 }
369
370 pub fn of_key(key: &str) -> Option<Self> {
371 if key.starts_with(SEMANTIC_PREFIX) {
372 Some(Self::Semantic)
373 } else if matches!(
374 key,
375 REPARTITION_COLUMN_HINT_KEY | REPARTITION_PARTITION_NUM_HINT_KEY
376 ) {
377 Some(Self::RepartitionHint)
378 } else {
379 None
380 }
381 }
382
383 pub fn allows_logical_tables(self) -> bool {
387 match self {
388 Self::Semantic => true,
389 Self::RepartitionHint => false,
390 }
391 }
392
393 pub fn requires_unique_keys(self) -> bool {
395 matches!(self, Self::RepartitionHint)
396 }
397
398 pub fn mixed_batch_error(self) -> String {
400 match self {
401 Self::Semantic => format!(
402 "`{SEMANTIC_PREFIX}*` options must be altered separately from other table options"
403 ),
404 Self::RepartitionHint => {
405 "repartition hints must be altered separately from other table options".to_string()
406 }
407 }
408 }
409}
410
411#[derive(Debug, Clone, PartialEq, Eq)]
413pub enum AnnotationKeyError {
414 MixedFamilies { family: AnnotationFamily },
415 DuplicateKey,
416}
417
418impl fmt::Display for AnnotationKeyError {
419 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
420 match self {
421 Self::MixedFamilies { family } => f.write_str(&family.mixed_batch_error()),
422 Self::DuplicateKey => f.write_str("duplicate repartition hint keys"),
423 }
424 }
425}
426
427pub fn validate_annotation_keys<'a>(
433 keys: impl IntoIterator<Item = &'a str>,
434) -> std::result::Result<Option<AnnotationFamily>, AnnotationKeyError> {
435 let mut keys = keys.into_iter();
436 let Some(first) = keys.next() else {
437 return Ok(None);
438 };
439 let family = AnnotationFamily::of_key(first);
440 let reject_duplicates = family.is_some_and(|family| family.requires_unique_keys());
441 let mut seen = HashSet::new();
442 if reject_duplicates {
443 seen.insert(first);
444 }
445 for key in keys {
446 let this = AnnotationFamily::of_key(key);
447 if this != family
448 && let Some(family) = family.or(this)
449 {
450 return Err(AnnotationKeyError::MixedFamilies { family });
451 }
452 if reject_duplicates && !seen.insert(key) {
453 return Err(AnnotationKeyError::DuplicateKey);
454 }
455 }
456 Ok(family)
457}
458
459pub struct AnnotationContext<'a> {
461 pub schema: &'a Schema,
462 pub partition_key_indices: &'a [usize],
463}
464
465#[derive(Debug, Clone, PartialEq, Eq)]
470pub enum AnnotationValidationError {
471 UnknownKey {
472 key: String,
473 },
474 InvalidValue {
475 key: String,
476 value: String,
477 },
478 ColumnNotFound {
479 column: String,
480 },
481 ColumnNotStringForm {
482 key: String,
483 column: String,
484 ty: ConcreteDataType,
485 },
486 InvalidPartitionNumHint {
487 value: String,
488 },
489 NotSingleColumn,
490 PartitionMetadataConflict,
491 TimeIndexConflict,
492}
493
494impl fmt::Display for AnnotationValidationError {
495 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
496 match self {
497 Self::UnknownKey { key } => write!(f, "unknown semantic option `{key}`"),
498 Self::InvalidValue { key, value } => {
499 write!(f, "invalid value `{value}` for semantic option `{key}`")
500 }
501 Self::ColumnNotFound { column } => write!(f, "column `{column}` not found"),
502 Self::ColumnNotStringForm { key, column, ty } => write!(
503 f,
504 "entity column `{column}` (option `{key}`) has type `{ty}`, \
505 which cannot render as a string"
506 ),
507 Self::InvalidPartitionNumHint { value } => write!(
508 f,
509 "{REPARTITION_PARTITION_NUM_HINT_KEY} expects a positive integer within u32 range, got `{value}`"
510 ),
511 Self::NotSingleColumn => write!(
512 f,
513 "{REPARTITION_COLUMN_HINT_KEY} expects exactly one column name"
514 ),
515 Self::PartitionMetadataConflict => write!(
516 f,
517 "cannot set {REPARTITION_COLUMN_HINT_KEY} on a table with partition metadata"
518 ),
519 Self::TimeIndexConflict => write!(
520 f,
521 "cannot set {REPARTITION_COLUMN_HINT_KEY} to the time index column"
522 ),
523 }
524 }
525}
526
527pub(crate) fn validate_and_normalize_annotation(
531 family: AnnotationFamily,
532 cx: &AnnotationContext<'_>,
533 key: &str,
534 value: &str,
535) -> std::result::Result<String, AnnotationValidationError> {
536 match family {
537 AnnotationFamily::Semantic => {
538 if !is_semantic_option_key(key) {
539 return Err(AnnotationValidationError::UnknownKey {
540 key: key.to_string(),
541 });
542 }
543 if !validate_semantic_option(key, value) {
544 return Err(AnnotationValidationError::InvalidValue {
545 key: key.to_string(),
546 value: value.to_string(),
547 });
548 }
549 if parse_entity_option_key(key).is_some() {
550 for column in parse_entity_columns(value) {
551 let schema = cx.schema.column_schema_by_name(&column).ok_or_else(|| {
552 AnnotationValidationError::ColumnNotFound {
553 column: column.clone(),
554 }
555 })?;
556 if !has_stable_string_form(&schema.data_type) {
557 return Err(AnnotationValidationError::ColumnNotStringForm {
558 key: key.to_string(),
559 column,
560 ty: schema.data_type.clone(),
561 });
562 }
563 }
564 }
565 Ok(value.to_string())
566 }
567 AnnotationFamily::RepartitionHint if key == REPARTITION_PARTITION_NUM_HINT_KEY => {
568 let value = value.trim();
569 if !matches!(value.parse::<u32>(), Ok(1..)) {
570 return Err(AnnotationValidationError::InvalidPartitionNumHint {
571 value: value.to_string(),
572 });
573 }
574 Ok(value.to_string())
575 }
576 AnnotationFamily::RepartitionHint => {
577 let column_name = value.trim();
578 if column_name.is_empty() || column_name.contains(',') {
579 return Err(AnnotationValidationError::NotSingleColumn);
580 }
581 if !cx.partition_key_indices.is_empty() {
582 return Err(AnnotationValidationError::PartitionMetadataConflict);
583 }
584 let column_index = cx.schema.column_index_by_name(column_name).ok_or_else(|| {
585 AnnotationValidationError::ColumnNotFound {
586 column: column_name.to_string(),
587 }
588 })?;
589 if cx.schema.timestamp_index() == Some(column_index) {
590 return Err(AnnotationValidationError::TimeIndexConflict);
591 }
592 Ok(column_name.to_string())
593 }
594 }
595}
596
597pub fn validate_and_normalize_annotation_options(
600 options: &mut TableOptions,
601 cx: &AnnotationContext<'_>,
602) -> std::result::Result<(), AnnotationValidationError> {
603 let mut normalized = Vec::new();
604 for (key, value) in &options.extra_options {
605 let Some(family) = AnnotationFamily::of_key(key) else {
606 continue;
607 };
608 let checked = validate_and_normalize_annotation(family, cx, key, value)?;
609 if checked != *value {
610 normalized.push((key.clone(), checked));
611 }
612 }
613 for (key, value) in normalized {
614 options.extra_options.insert(key, value);
615 }
616 Ok(())
617}
618
619#[derive(Debug, Clone, Serialize, Deserialize)]
620pub enum AlterKind {
621 AddColumns {
622 columns: Vec<AddColumnRequest>,
623 },
624 DropColumns {
625 names: Vec<String>,
626 },
627 ModifyColumnTypes {
628 columns: Vec<ModifyColumnTypeRequest>,
629 },
630 RenameTable {
631 new_table_name: String,
632 },
633 SetTableOptions {
634 options: Vec<SetRegionOption>,
635 },
636 UnsetTableOptions {
637 keys: Vec<UnsetRegionOption>,
638 },
639 SetAnnotations {
640 family: AnnotationFamily,
641 options: Vec<(String, String)>,
642 },
643 UnsetAnnotations {
644 family: AnnotationFamily,
645 keys: Vec<String>,
646 },
647 SetIndexes {
648 options: Vec<SetIndexOption>,
649 },
650 UnsetIndexes {
651 options: Vec<UnsetIndexOption>,
652 },
653 DropDefaults {
654 names: Vec<String>,
655 },
656 SetDefaults {
657 defaults: Vec<SetDefaultRequest>,
658 },
659}
660
661#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct SetDefaultRequest {
663 pub column_name: String,
664 pub default_constraint: Option<ColumnDefaultConstraint>,
665}
666
667#[derive(Debug, Clone, Serialize, Deserialize)]
668pub enum SetIndexOption {
669 Fulltext {
670 column_name: String,
671 options: FulltextOptions,
672 },
673 Inverted {
674 column_name: String,
675 },
676 Skipping {
677 column_name: String,
678 options: SkippingIndexOptions,
679 },
680}
681
682impl SetIndexOption {
683 pub fn column_name(&self) -> &str {
685 match self {
686 SetIndexOption::Fulltext { column_name, .. } => column_name,
687 SetIndexOption::Inverted { column_name, .. } => column_name,
688 SetIndexOption::Skipping { column_name, .. } => column_name,
689 }
690 }
691}
692
693#[derive(Debug, Clone, Serialize, Deserialize)]
694pub enum UnsetIndexOption {
695 Fulltext { column_name: String },
696 Inverted { column_name: String },
697 Skipping { column_name: String },
698}
699
700impl UnsetIndexOption {
701 pub fn column_name(&self) -> &str {
703 match self {
704 UnsetIndexOption::Fulltext { column_name, .. } => column_name,
705 UnsetIndexOption::Inverted { column_name, .. } => column_name,
706 UnsetIndexOption::Skipping { column_name, .. } => column_name,
707 }
708 }
709}
710
711#[derive(Debug)]
712pub struct InsertRequest {
713 pub catalog_name: String,
714 pub schema_name: String,
715 pub table_name: String,
716 pub columns_values: HashMap<String, VectorRef>,
717 pub skip_wal: bool,
719}
720
721#[derive(Debug)]
723pub struct DeleteRequest {
724 pub catalog_name: String,
725 pub schema_name: String,
726 pub table_name: String,
727 pub key_column_values: HashMap<String, VectorRef>,
731}
732
733#[derive(Debug)]
734pub enum CopyDirection {
735 Export,
736 Import,
737}
738
739#[derive(Debug)]
741pub struct CopyTableRequest {
742 pub catalog_name: String,
743 pub schema_name: String,
744 pub table_name: String,
745 pub location: String,
746 pub with: HashMap<String, String>,
747 pub connection: HashMap<String, String>,
748 pub pattern: Option<String>,
749 pub direction: CopyDirection,
750 pub timestamp_range: Option<TimestampRange>,
751 pub limit: Option<u64>,
752}
753
754#[derive(Debug, Clone, Default)]
755pub struct FlushTableRequest {
756 pub catalog_name: String,
757 pub schema_name: String,
758 pub table_name: String,
759}
760
761#[derive(Debug, Clone, Default)]
762pub struct BuildIndexTableRequest {
763 pub catalog_name: String,
764 pub schema_name: String,
765 pub table_name: String,
766}
767
768#[derive(Debug, Clone, PartialEq)]
769pub struct CompactTableRequest {
770 pub catalog_name: String,
771 pub schema_name: String,
772 pub table_name: String,
773 pub compact_options: compact_request::Options,
774 pub parallelism: u32,
775 pub time_range: Option<TimestampRange>,
776}
777
778impl Default for CompactTableRequest {
779 fn default() -> Self {
780 Self {
781 catalog_name: Default::default(),
782 schema_name: Default::default(),
783 table_name: Default::default(),
784 compact_options: compact_request::Options::Regular(Default::default()),
785 parallelism: 1,
786 time_range: None,
787 }
788 }
789}
790
791#[derive(Debug, Clone, Serialize, Deserialize)]
793pub struct TruncateTableRequest {
794 pub catalog_name: String,
795 pub schema_name: String,
796 pub table_name: String,
797 pub table_id: TableId,
798}
799
800impl TruncateTableRequest {
801 pub fn table_ref(&self) -> TableReference<'_> {
802 TableReference {
803 catalog: &self.catalog_name,
804 schema: &self.schema_name,
805 table: &self.table_name,
806 }
807 }
808}
809
810#[derive(Debug, Clone, Default, Deserialize, Serialize)]
811pub struct CopyDatabaseRequest {
812 pub catalog_name: String,
813 pub schema_name: String,
814 pub location: String,
815 pub with: HashMap<String, String>,
816 pub connection: HashMap<String, String>,
817 pub time_range: Option<TimestampRange>,
818}
819
820#[derive(Debug, Clone, Default, Deserialize, Serialize)]
821pub struct CopyQueryToRequest {
822 pub location: String,
823 pub with: HashMap<String, String>,
824 pub connection: HashMap<String, String>,
825}
826
827#[cfg(test)]
828mod tests {
829 use std::time::Duration;
830
831 use common_error::ext::ErrorExt;
832 use common_error::status_code::StatusCode;
833
834 use super::*;
835
836 #[test]
837 fn test_validate_annotation_keys() {
838 let column = REPARTITION_COLUMN_HINT_KEY;
839 let count = REPARTITION_PARTITION_NUM_HINT_KEY;
840 for (keys, expected) in [
841 (vec![], None),
842 (vec![TTL_KEY, TTL_KEY], None),
843 (vec!["repartition.unknown.hint"], None),
844 (vec![column], Some(AnnotationFamily::RepartitionHint)),
845 (vec![count], Some(AnnotationFamily::RepartitionHint)),
846 (vec![column, count], Some(AnnotationFamily::RepartitionHint)),
847 (vec![count, column], Some(AnnotationFamily::RepartitionHint)),
848 (
849 vec!["greptime.semantic.source", "greptime.semantic.source"],
850 Some(AnnotationFamily::Semantic),
851 ),
852 ] {
853 assert_eq!(validate_annotation_keys(keys), Ok(expected));
854 }
855 for keys in [
856 vec![column, column],
857 vec![count, count],
858 vec![column, count, column],
859 ] {
860 assert_eq!(
861 validate_annotation_keys(keys),
862 Err(AnnotationKeyError::DuplicateKey)
863 );
864 }
865 for keys in [
866 vec![column, TTL_KEY],
867 vec![TTL_KEY, count],
868 vec![column, "greptime.semantic.source"],
869 ] {
870 assert_eq!(
871 validate_annotation_keys(keys),
872 Err(AnnotationKeyError::MixedFamilies {
873 family: AnnotationFamily::RepartitionHint,
874 })
875 );
876 }
877 }
878
879 #[test]
880 fn test_validate_table_option() {
881 assert!(validate_table_option(FILE_TABLE_LOCATION_KEY));
882 assert!(validate_table_option(FILE_TABLE_FORMAT_KEY));
883 assert!(validate_table_option(FILE_TABLE_PATTERN_KEY));
884 assert!(validate_table_option(TTL_KEY));
885 assert!(validate_table_option(WRITE_BUFFER_SIZE_KEY));
886 assert!(validate_table_option(STORAGE_KEY));
887 assert!(validate_table_option(MEMTABLE_BULK_MERGE_THRESHOLD));
888 assert!(validate_table_option(REPARTITION_COLUMN_HINT_KEY));
889 assert!(validate_table_option(REPARTITION_PARTITION_NUM_HINT_KEY));
890 assert_eq!(AnnotationFamily::of_key("repartition.unknown.hint"), None);
891 assert!(!validate_table_option("foo"));
892
893 assert!(validate_table_option(SEMANTIC_SIGNAL_TYPE));
895 assert!(validate_table_option(SEMANTIC_METRIC_TYPE));
896 assert!(!validate_table_option("greptime.semantic.future.key"));
898 assert!(!validate_table_option("greptime.semanticx"));
899 assert!(!validate_table_option(SEMANTIC_PER_TABLE_INDEX_KEY));
900 }
901
902 #[test]
903 fn test_validate_database_option() {
904 assert!(validate_database_option(MEMTABLE_TYPE));
905 assert!(validate_database_option(MEMTABLE_BULK_MERGE_THRESHOLD));
906 assert!(validate_database_option(MEMTABLE_BULK_ENCODE_ROW_THRESHOLD));
907 assert!(validate_database_option(
908 MEMTABLE_BULK_ENCODE_BYTES_THRESHOLD
909 ));
910 assert!(validate_database_option(MEMTABLE_BULK_MAX_MERGE_GROUPS));
911 assert!(validate_database_option(
912 "compaction.twcs.active_window.trigger_file_num"
913 ));
914 assert!(validate_database_option(
915 "compaction.twcs.active_window.l1_merge_trigger"
916 ));
917 assert!(validate_database_option(
918 "compaction.twcs.inactive_window.trigger_file_num"
919 ));
920 assert!(validate_database_option(
921 "compaction.twcs.inactive_window.l1_merge_trigger"
922 ));
923 assert!(!validate_database_option("foo"));
924 }
925
926 #[test]
927 fn test_database_trigger_value_boundaries() {
928 let maximum = usize::MAX.to_string();
929 let overflow = format!("{maximum}0");
930 for (key, minimum) in [
931 (TWCS_TRIGGER_FILE_NUM, 0),
932 (TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM, 0),
933 (TWCS_INACTIVE_WINDOW_TRIGGER_FILE_NUM, 0),
934 (TWCS_ACTIVE_WINDOW_L1_MERGE_TRIGGER, 2),
935 (TWCS_INACTIVE_WINDOW_L1_MERGE_TRIGGER, 2),
936 ] {
937 for invalid in [
938 None,
939 Some(""),
940 Some("invalid"),
941 Some("-1"),
942 Some(overflow.as_str()),
943 ] {
944 assert!(
945 validate_database_option_value(key, invalid).is_err(),
946 "{key}: {invalid:?}"
947 );
948 }
949 for valid in ["2", maximum.as_str()] {
950 assert!(
951 validate_database_option_value(key, Some(valid)).is_ok(),
952 "{key}: {valid}"
953 );
954 }
955 for boundary in ["0", "1"] {
956 assert_eq!(
957 validate_database_option_value(key, Some(boundary)).is_ok(),
958 minimum == 0,
959 "{key}: {boundary}"
960 );
961 }
962 }
963 }
964
965 #[test]
966 fn test_serialize_table_options() {
967 let options = TableOptions {
968 write_buffer_size: None,
969 ttl: Some(Duration::from_secs(1000).into()),
970 extra_options: HashMap::new(),
971 skip_wal: false,
972 };
973 let serialized = serde_json::to_string(&options).unwrap();
974 let deserialized: TableOptions = serde_json::from_str(&serialized).unwrap();
975 assert_eq!(options, deserialized);
976 }
977
978 #[test]
979 fn test_convert_hashmap_between_table_options() {
980 let options = TableOptions {
981 write_buffer_size: Some(ReadableSize::mb(128)),
982 ttl: Some(Duration::from_secs(1000).into()),
983 extra_options: HashMap::new(),
984 skip_wal: false,
985 };
986 let serialized_map = HashMap::from(&options);
987 let serialized = TableOptions::try_from_iter(&serialized_map).unwrap();
988 assert_eq!(options, serialized);
989
990 let options = TableOptions {
991 write_buffer_size: None,
992 ttl: None,
993 extra_options: HashMap::from([(SKIP_WAL_KEY.to_string(), true.to_string())]),
994 skip_wal: true,
995 };
996 let serialized_map = HashMap::from(&options);
997 assert_eq!(
998 Some("true"),
999 serialized_map.get(SKIP_WAL_KEY).map(String::as_str)
1000 );
1001 let serialized = TableOptions::try_from_iter(&serialized_map).unwrap();
1002 assert_eq!(options, serialized);
1003
1004 let options = TableOptions {
1005 write_buffer_size: None,
1006 ttl: Default::default(),
1007 extra_options: HashMap::new(),
1008 skip_wal: false,
1009 };
1010 let serialized_map = HashMap::from(&options);
1011 let serialized = TableOptions::try_from_iter(&serialized_map).unwrap();
1012 assert_eq!(options, serialized);
1013
1014 let options = TableOptions {
1015 write_buffer_size: Some(ReadableSize::mb(128)),
1016 ttl: Some(Duration::from_secs(1000).into()),
1017 extra_options: HashMap::from([("a".to_string(), "A".to_string())]),
1018 skip_wal: false,
1019 };
1020 let serialized_map = HashMap::from(&options);
1021 let serialized = TableOptions::try_from_iter(&serialized_map).unwrap();
1022 assert_eq!(options, serialized);
1023
1024 let options = TableOptions {
1025 extra_options: HashMap::from([(SKIP_WAL_KEY.to_string(), false.to_string())]),
1026 skip_wal: false,
1027 ..Default::default()
1028 };
1029 let serialized_map = HashMap::from(&options);
1030 let serialized = TableOptions::try_from_iter(&serialized_map).unwrap();
1031 assert_eq!(options, serialized);
1032 }
1033
1034 #[test]
1035 fn test_table_options_normalizes_twcs_trigger_aliases() {
1036 for options in [
1037 vec![(TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM, "4")],
1038 vec![
1039 (TWCS_TRIGGER_FILE_NUM, "4"),
1040 (TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM, "4"),
1041 ],
1042 ] {
1043 let table_options = TableOptions::try_from_iter(options).unwrap();
1044 assert_eq!(
1045 HashMap::from([(TWCS_TRIGGER_FILE_NUM.to_string(), "4".to_string())]),
1046 table_options.extra_options
1047 );
1048 }
1049 }
1050
1051 #[test]
1052 fn test_table_options_rejects_conflicting_twcs_trigger_aliases() {
1053 let error = TableOptions::try_from_iter([
1054 (TWCS_TRIGGER_FILE_NUM, "4"),
1055 (TWCS_ACTIVE_WINDOW_TRIGGER_FILE_NUM, "8"),
1056 ])
1057 .unwrap_err();
1058 assert_eq!(StatusCode::InvalidArguments, error.status_code());
1059 assert_eq!(
1060 "Conflicting table options: compaction.twcs.trigger_file_num=4 and compaction.twcs.active_window.trigger_file_num=8",
1061 error.to_string()
1062 );
1063 }
1064
1065 #[test]
1066 fn test_table_options_to_string() {
1067 let options = TableOptions {
1068 write_buffer_size: Some(ReadableSize::mb(128)),
1069 ttl: Some(Duration::from_secs(1000).into()),
1070 extra_options: HashMap::new(),
1071 skip_wal: false,
1072 };
1073
1074 assert_eq!(
1075 "write_buffer_size=128.0MiB ttl=16m 40s",
1076 options.to_string()
1077 );
1078
1079 let options = TableOptions {
1080 write_buffer_size: Some(ReadableSize::mb(128)),
1081 ttl: Some(Duration::from_secs(1000).into()),
1082 extra_options: HashMap::from([("a".to_string(), "A".to_string())]),
1083 skip_wal: false,
1084 };
1085
1086 assert_eq!(
1087 "write_buffer_size=128.0MiB ttl=16m 40s a=A",
1088 options.to_string()
1089 );
1090
1091 let options = TableOptions {
1092 write_buffer_size: Some(ReadableSize::mb(128)),
1093 ttl: Some(Duration::from_secs(1000).into()),
1094 extra_options: HashMap::new(),
1095 skip_wal: true,
1096 };
1097 assert_eq!(
1098 "write_buffer_size=128.0MiB ttl=16m 40s skip_wal=true",
1099 options.to_string()
1100 );
1101
1102 let options = TableOptions {
1103 write_buffer_size: None,
1104 ttl: None,
1105 extra_options: HashMap::from([(SKIP_WAL_KEY.to_string(), "false".to_string())]),
1106 skip_wal: false,
1107 };
1108 assert_eq!("skip_wal=false", options.to_string());
1109 }
1110}