Skip to main content

common_grpc_expr/
alter.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 api::helper::ColumnDataTypeWrapper;
16use api::v1::add_column_location::LocationType;
17use api::v1::alter_table_expr::Kind;
18use api::v1::column_def::{
19    as_fulltext_option_analyzer, as_fulltext_option_backend, as_skipping_index_type,
20};
21use api::v1::{
22    AddColumnLocation as Location, AlterTableExpr, Analyzer, CreateTableExpr, DropColumns,
23    FulltextBackend as PbFulltextBackend, ModifyColumnTypes, RenameTable, SemanticType,
24    SkippingIndexType as PbSkippingIndexType, column_def,
25};
26use common_query::AddColumnLocation;
27use datatypes::json::{JsonSettings, JsonTypeHint};
28use datatypes::prelude::ConcreteDataType;
29use datatypes::schema::{ColumnSchema, FulltextOptions, Schema, SkippingIndexOptions};
30use snafu::{OptionExt, ResultExt, ensure};
31use store_api::region_request::{SetRegionOption, UnsetRegionOption};
32use table::metadata::{TableId, TableMeta};
33use table::requests::{
34    AddColumnRequest, AlterKind, AlterTableRequest, AnnotationFamily, ModifyColumnTypeRequest,
35    SetDefaultRequest, SetIndexOption, SetJsonSettingsRequest, UnsetIndexOption,
36};
37
38use crate::error::{
39    self, ColumnNotFoundSnafu, InvalidColumnDefSnafu, InvalidIndexOptionSnafu,
40    InvalidJsonSettingsSnafu, InvalidSetFulltextOptionRequestSnafu,
41    InvalidSetSkippingIndexOptionRequestSnafu, InvalidSetTableOptionRequestSnafu,
42    InvalidUnsetTableOptionRequestSnafu, MissingAlterIndexOptionSnafu, MissingFieldSnafu,
43    MissingTableMetaSnafu, MissingTimestampColumnSnafu, Result, UnknownLocationTypeSnafu,
44};
45
46const LOCATION_TYPE_FIRST: i32 = LocationType::First as i32;
47const LOCATION_TYPE_AFTER: i32 = LocationType::After as i32;
48
49/// Classifies a SET/UNSET key batch: `Ok(Some(family))` when every key belongs
50/// to the same annotation family, `Ok(None)` when none does, and an error on a
51/// mixed batch — annotation alters skip region dispatch, so they cannot share
52/// a statement with options that regions must see.
53fn annotation_family_of_keys<'a>(
54    keys: impl Iterator<Item = &'a str>,
55) -> Result<Option<AnnotationFamily>> {
56    table::requests::validate_annotation_keys(keys).map_err(|err| {
57        error::InvalidTableOptionRequestSnafu {
58            err_msg: err.to_string(),
59        }
60        .build()
61    })
62}
63
64fn json_settings_from_proto(settings: api::v1::JsonSettings) -> Result<JsonSettings> {
65    let type_hints = settings
66        .type_hints
67        .into_iter()
68        .map(|hint| {
69            let data_type = ConcreteDataType::from(
70                ColumnDataTypeWrapper::try_new(hint.data_type, hint.datatype_extension)
71                    .context(error::ColumnDataTypeSnafu)?,
72            );
73
74            Ok(JsonTypeHint {
75                path: hint.path,
76                data_type,
77                inverted_index: hint.inverted_index,
78                skipping_index: hint
79                    .skipping_index
80                    .map(api::v1::column_def::json_skipping_options_from_proto)
81                    .transpose()
82                    .map_err(|err| {
83                        InvalidJsonSettingsSnafu {
84                            err: err.to_string(),
85                        }
86                        .build()
87                    })?,
88            })
89        })
90        .collect::<Result<Vec<_>>>()?;
91
92    JsonSettings::try_new(type_hints, settings.max_auto_expanded_paths).map_err(|err| {
93        InvalidJsonSettingsSnafu {
94            err: err.to_string(),
95        }
96        .build()
97    })
98}
99
100/// Returns the annotation family when `kind` is a SET/UNSET whose keys all
101/// belong to one family — the alters that only rewrite table metadata and skip
102/// region dispatch. A mixed batch is an error; never interpret it as "not an
103/// annotation alter", or the batch falls through to a path that reports a
104/// misleading error (or dispatches to regions).
105pub fn annotation_alter_family(kind: &Kind) -> Result<Option<AnnotationFamily>> {
106    match kind {
107        Kind::SetTableOptions(api::v1::SetTableOptions { table_options }) => {
108            annotation_family_of_keys(table_options.iter().map(|option| option.key.as_str()))
109        }
110        Kind::UnsetTableOptions(api::v1::UnsetTableOptions { keys }) => {
111            annotation_family_of_keys(keys.iter().map(|key| key.as_str()))
112        }
113        _ => Ok(None),
114    }
115}
116
117fn set_index_option_from_proto(set_index: api::v1::SetIndex) -> Result<SetIndexOption> {
118    let options = set_index.options.context(MissingAlterIndexOptionSnafu)?;
119    Ok(match options {
120        api::v1::set_index::Options::Fulltext(f) => SetIndexOption::Fulltext {
121            column_name: f.column_name.clone(),
122            options: FulltextOptions::new(
123                f.enable,
124                as_fulltext_option_analyzer(
125                    Analyzer::try_from(f.analyzer).context(InvalidSetFulltextOptionRequestSnafu)?,
126                ),
127                f.case_sensitive,
128                as_fulltext_option_backend(
129                    PbFulltextBackend::try_from(f.backend)
130                        .context(InvalidSetFulltextOptionRequestSnafu)?,
131                ),
132                f.granularity as u32,
133                f.false_positive_rate,
134            )
135            .context(InvalidIndexOptionSnafu)?,
136        },
137        api::v1::set_index::Options::Inverted(i) => SetIndexOption::Inverted {
138            column_name: i.column_name,
139        },
140        api::v1::set_index::Options::Skipping(s) => SetIndexOption::Skipping {
141            column_name: s.column_name,
142            options: SkippingIndexOptions::new(
143                s.granularity as u32,
144                s.false_positive_rate,
145                as_skipping_index_type(
146                    PbSkippingIndexType::try_from(s.skipping_index_type)
147                        .context(InvalidSetSkippingIndexOptionRequestSnafu)?,
148                ),
149            )
150            .context(InvalidIndexOptionSnafu)?,
151        },
152    })
153}
154
155fn unset_index_option_from_proto(unset_index: api::v1::UnsetIndex) -> Result<UnsetIndexOption> {
156    let options = unset_index.options.context(MissingAlterIndexOptionSnafu)?;
157    Ok(match options {
158        api::v1::unset_index::Options::Fulltext(f) => UnsetIndexOption::Fulltext {
159            column_name: f.column_name,
160        },
161        api::v1::unset_index::Options::Inverted(i) => UnsetIndexOption::Inverted {
162            column_name: i.column_name,
163        },
164        api::v1::unset_index::Options::Skipping(s) => UnsetIndexOption::Skipping {
165            column_name: s.column_name,
166        },
167    })
168}
169
170/// Convert an [`AlterTableExpr`] to an [`AlterTableRequest`]
171///
172/// note: `table_meta` must not be None if [`AlterTableExpr`] is `SetDefault`
173pub fn alter_expr_to_request(
174    table_id: TableId,
175    expr: AlterTableExpr,
176    table_meta: Option<&TableMeta>,
177) -> Result<AlterTableRequest> {
178    let catalog_name = expr.catalog_name;
179    let schema_name = expr.schema_name;
180    let kind = expr.kind.context(MissingFieldSnafu { field: "kind" })?;
181    let alter_kind = match kind {
182        Kind::AddColumns(add_columns) => {
183            let add_column_requests = add_columns
184                .add_columns
185                .into_iter()
186                .map(|ac| {
187                    let column_def = ac.column_def.context(MissingFieldSnafu {
188                        field: "column_def",
189                    })?;
190
191                    let schema = column_def::try_as_column_schema(&column_def).context(
192                        InvalidColumnDefSnafu {
193                            column: &column_def.name,
194                        },
195                    )?;
196                    Ok(AddColumnRequest {
197                        column_schema: schema,
198                        is_key: column_def.semantic_type == SemanticType::Tag as i32,
199                        location: parse_location(ac.location)?,
200                        add_if_not_exists: ac.add_if_not_exists,
201                    })
202                })
203                .collect::<Result<Vec<_>>>()?;
204
205            AlterKind::AddColumns {
206                columns: add_column_requests,
207            }
208        }
209        Kind::ModifyColumnTypes(ModifyColumnTypes {
210            modify_column_types,
211        }) => {
212            let modify_column_type_requests = modify_column_types
213                .into_iter()
214                .map(|cct| {
215                    let target_type =
216                        ColumnDataTypeWrapper::new(cct.target_type(), cct.target_type_extension)
217                            .into();
218
219                    Ok(ModifyColumnTypeRequest {
220                        column_name: cct.column_name,
221                        target_type,
222                    })
223                })
224                .collect::<Result<Vec<_>>>()?;
225
226            AlterKind::ModifyColumnTypes {
227                columns: modify_column_type_requests,
228            }
229        }
230        Kind::SetJsonSettings(set_json_settings) => {
231            let settings = set_json_settings
232                .settings
233                .context(MissingFieldSnafu { field: "settings" })?;
234            AlterKind::SetJsonSettings {
235                request: SetJsonSettingsRequest {
236                    column_name: set_json_settings.column_name,
237                    settings: json_settings_from_proto(settings)?,
238                },
239            }
240        }
241        Kind::DropColumns(DropColumns { drop_columns }) => AlterKind::DropColumns {
242            names: drop_columns.into_iter().map(|c| c.name).collect(),
243        },
244        Kind::RenameTable(RenameTable { new_table_name }) => {
245            AlterKind::RenameTable { new_table_name }
246        }
247        Kind::SetTableOptions(api::v1::SetTableOptions { table_options }) => {
248            if let Some(family) = annotation_family_of_keys(
249                table_options.iter().map(|option| option.key.as_str()),
250            )? {
251                AlterKind::SetAnnotations {
252                    family,
253                    options: table_options
254                        .into_iter()
255                        .map(|option| (option.key, option.value))
256                        .collect(),
257                }
258            } else {
259                AlterKind::SetTableOptions {
260                    options: table_options
261                        .iter()
262                        .map(SetRegionOption::try_from)
263                        .collect::<std::result::Result<Vec<_>, _>>()
264                        .context(InvalidSetTableOptionRequestSnafu)?,
265                }
266            }
267        }
268        Kind::UnsetTableOptions(api::v1::UnsetTableOptions { keys }) => {
269            if let Some(family) = annotation_family_of_keys(keys.iter().map(|key| key.as_str()))? {
270                AlterKind::UnsetAnnotations { family, keys }
271            } else {
272                AlterKind::UnsetTableOptions {
273                    keys: keys
274                        .iter()
275                        .map(|key| UnsetRegionOption::try_from(key.as_str()))
276                        .collect::<std::result::Result<Vec<_>, _>>()
277                        .context(InvalidUnsetTableOptionRequestSnafu)?,
278                }
279            }
280        }
281        Kind::SetIndex(o) => {
282            let option = set_index_option_from_proto(o)?;
283            AlterKind::SetIndexes {
284                options: vec![option],
285            }
286        }
287        Kind::UnsetIndex(o) => {
288            let option = unset_index_option_from_proto(o)?;
289            AlterKind::UnsetIndexes {
290                options: vec![option],
291            }
292        }
293        Kind::SetIndexes(o) => {
294            let options = o
295                .set_indexes
296                .into_iter()
297                .map(set_index_option_from_proto)
298                .collect::<Result<Vec<_>>>()?;
299            AlterKind::SetIndexes { options }
300        }
301        Kind::UnsetIndexes(o) => {
302            let options = o
303                .unset_indexes
304                .into_iter()
305                .map(unset_index_option_from_proto)
306                .collect::<Result<Vec<_>>>()?;
307            AlterKind::UnsetIndexes { options }
308        }
309        Kind::DropDefaults(o) => {
310            let names = o
311                .drop_defaults
312                .into_iter()
313                .map(|col| {
314                    ensure!(
315                        !col.column_name.is_empty(),
316                        MissingFieldSnafu {
317                            field: "column_name"
318                        }
319                    );
320                    Ok(col.column_name)
321                })
322                .collect::<Result<Vec<_>>>()?;
323            AlterKind::DropDefaults { names }
324        }
325        Kind::SetDefaults(o) => {
326            let table_meta = table_meta.context(MissingTableMetaSnafu { table_id })?;
327            let defaults = o
328                .set_defaults
329                .into_iter()
330                .map(|col| {
331                    let column_scheme = table_meta
332                        .schema
333                        .column_schema_by_name(&col.column_name)
334                        .context(ColumnNotFoundSnafu {
335                        column_name: &col.column_name,
336                    })?;
337                    let default_constraint = common_sql::convert::deserialize_default_constraint(
338                        col.default_constraint.as_slice(),
339                        &col.column_name,
340                        &column_scheme.data_type,
341                    )
342                    .context(crate::error::SqlCommonSnafu)?;
343                    Ok(SetDefaultRequest {
344                        column_name: col.column_name,
345                        default_constraint,
346                    })
347                })
348                .collect::<Result<Vec<_>>>()?;
349            AlterKind::SetDefaults { defaults }
350        }
351        Kind::Repartition(_) => error::UnexpectedSnafu {
352            err_msg: "Repartition operation should be handled through DdlManager and not converted to AlterTableRequest",
353        }
354        .fail()?,
355    };
356
357    let request = AlterTableRequest {
358        catalog_name,
359        schema_name,
360        table_name: expr.table_name,
361        table_id,
362        alter_kind,
363        table_version: None,
364    };
365    Ok(request)
366}
367
368pub fn create_table_schema(expr: &CreateTableExpr, require_time_index: bool) -> Result<Schema> {
369    let column_schemas = expr
370        .column_defs
371        .iter()
372        .map(|x| {
373            column_def::try_as_column_schema(x).context(InvalidColumnDefSnafu { column: &x.name })
374        })
375        .collect::<Result<Vec<ColumnSchema>>>()?;
376
377    // allow external table schema without the time index
378    if require_time_index {
379        ensure!(
380            column_schemas
381                .iter()
382                .any(|column| column.name == expr.time_index),
383            MissingTimestampColumnSnafu {
384                msg: format!("CreateExpr: {expr:?}")
385            }
386        );
387    }
388
389    let column_schemas = column_schemas
390        .into_iter()
391        .map(|column_schema| {
392            if column_schema.name == expr.time_index {
393                column_schema.with_time_index(true)
394            } else {
395                column_schema
396            }
397        })
398        .collect::<Vec<_>>();
399
400    Ok(Schema::new(column_schemas))
401}
402
403fn parse_location(location: Option<Location>) -> Result<Option<AddColumnLocation>> {
404    match location {
405        Some(Location {
406            location_type: LOCATION_TYPE_FIRST,
407            ..
408        }) => Ok(Some(AddColumnLocation::First)),
409        Some(Location {
410            location_type: LOCATION_TYPE_AFTER,
411            after_column_name,
412        }) => Ok(Some(AddColumnLocation::After {
413            column_name: after_column_name,
414        })),
415        Some(Location { location_type, .. }) => UnknownLocationTypeSnafu { location_type }.fail(),
416        None => Ok(None),
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use api::v1::{
423        AddColumn, AddColumns, ColumnDataType, ColumnDef, DropColumn, ModifyColumnType,
424        Option as PbOption, SemanticType, SetTableOptions, UnsetTableOptions,
425    };
426    use datatypes::prelude::ConcreteDataType;
427    use table::requests::REPARTITION_COLUMN_HINT_KEY;
428
429    use super::*;
430
431    #[test]
432    fn test_json_settings_preserve_inverted_index() {
433        for enabled in [false, true] {
434            let settings = json_settings_from_proto(api::v1::JsonSettings {
435                type_hints: vec![api::v1::JsonTypeHint {
436                    path: vec!["service".into()],
437                    data_type: ColumnDataType::String as i32,
438                    inverted_index: enabled,
439                    skipping_index: None,
440                    ..Default::default()
441                }],
442                max_auto_expanded_paths: Some(0),
443            })
444            .unwrap();
445            assert_eq!(enabled, settings.type_hints()[0].inverted_index);
446        }
447    }
448
449    #[test]
450    fn test_json_settings_skipping_options_from_proto() {
451        for (granularity, rate, index_type, valid) in [
452            (8, 0.05, 0, true),
453            (0, 0.05, 0, false),
454            (8, 0.0, 0, false),
455            (8, 0.05, 9, false),
456        ] {
457            let settings = json_settings_from_proto(api::v1::JsonSettings {
458                type_hints: vec![api::v1::JsonTypeHint {
459                    path: vec!["service".into()],
460                    data_type: ColumnDataType::String as i32,
461                    skipping_index: Some(api::v1::JsonSkippingIndexOptions {
462                        granularity,
463                        index_type,
464                        false_positive_rate: rate,
465                    }),
466                    ..Default::default()
467                }],
468                max_auto_expanded_paths: Some(0),
469            });
470            assert_eq!(settings.is_ok(), valid);
471            if let Ok(settings) = settings {
472                let options = settings.type_hints()[0].skipping_index.as_ref().unwrap();
473                assert_eq!(options.granularity, granularity);
474                assert_eq!(options.false_positive_rate(), rate);
475            }
476        }
477    }
478
479    #[test]
480    fn test_json_settings_path_validation_from_proto() {
481        for (path, valid_path) in [
482            (vec!["nested", "leaf"], true),
483            (vec![""], false),
484            (vec!["nested", ""], false),
485            (
486                vec!["nested", datatypes::json::JSON2_REMAINDER_FIELD_NAME],
487                false,
488            ),
489        ] {
490            for inverted_index in [false, true] {
491                let settings = api::v1::SetJsonSettings {
492                    column_name: "payload".into(),
493                    settings: Some(api::v1::JsonSettings {
494                        type_hints: vec![api::v1::JsonTypeHint {
495                            path: path.iter().map(|part| (*part).to_string()).collect(),
496                            data_type: ColumnDataType::String as i32,
497                            inverted_index,
498                            skipping_index: None,
499                            ..Default::default()
500                        }],
501                        max_auto_expanded_paths: Some(0),
502                    }),
503                };
504                let result = alter_expr_to_request(
505                    1,
506                    AlterTableExpr {
507                        catalog_name: "greptime".into(),
508                        schema_name: "public".into(),
509                        table_name: "logs".into(),
510                        kind: Some(Kind::SetJsonSettings(settings)),
511                    },
512                    None,
513                );
514                assert_eq!(
515                    result.is_ok(),
516                    valid_path,
517                    "path: {path:?}, inverted_index: {inverted_index}"
518                );
519            }
520        }
521    }
522
523    #[test]
524    fn test_alter_expr_to_request() {
525        let expr = AlterTableExpr {
526            catalog_name: String::default(),
527            schema_name: String::default(),
528            table_name: "monitor".to_string(),
529
530            kind: Some(Kind::AddColumns(AddColumns {
531                add_columns: vec![AddColumn {
532                    column_def: Some(ColumnDef {
533                        name: "mem_usage".to_string(),
534                        data_type: ColumnDataType::Float64 as i32,
535                        is_nullable: false,
536                        default_constraint: vec![],
537                        semantic_type: SemanticType::Field as i32,
538                        comment: String::new(),
539                        ..Default::default()
540                    }),
541                    location: None,
542                    add_if_not_exists: true,
543                }],
544            })),
545        };
546
547        let alter_request = alter_expr_to_request(1, expr, None).unwrap();
548        assert_eq!(alter_request.catalog_name, "");
549        assert_eq!(alter_request.schema_name, "");
550        assert_eq!("monitor".to_string(), alter_request.table_name);
551        let add_column = match alter_request.alter_kind {
552            AlterKind::AddColumns { mut columns } => columns.pop().unwrap(),
553            _ => unreachable!(),
554        };
555
556        assert!(!add_column.is_key);
557        assert_eq!("mem_usage", add_column.column_schema.name);
558        assert_eq!(
559            ConcreteDataType::float64_datatype(),
560            add_column.column_schema.data_type
561        );
562        assert_eq!(None, add_column.location);
563        assert!(add_column.add_if_not_exists);
564    }
565
566    #[test]
567    fn test_alter_expr_with_location_to_request() {
568        let expr = AlterTableExpr {
569            catalog_name: String::default(),
570            schema_name: String::default(),
571            table_name: "monitor".to_string(),
572
573            kind: Some(Kind::AddColumns(AddColumns {
574                add_columns: vec![
575                    AddColumn {
576                        column_def: Some(ColumnDef {
577                            name: "mem_usage".to_string(),
578                            data_type: ColumnDataType::Float64 as i32,
579                            is_nullable: false,
580                            default_constraint: vec![],
581                            semantic_type: SemanticType::Field as i32,
582                            comment: String::new(),
583                            ..Default::default()
584                        }),
585                        location: Some(Location {
586                            location_type: LocationType::First.into(),
587                            after_column_name: String::default(),
588                        }),
589                        add_if_not_exists: false,
590                    },
591                    AddColumn {
592                        column_def: Some(ColumnDef {
593                            name: "cpu_usage".to_string(),
594                            data_type: ColumnDataType::Float64 as i32,
595                            is_nullable: false,
596                            default_constraint: vec![],
597                            semantic_type: SemanticType::Field as i32,
598                            comment: String::new(),
599                            ..Default::default()
600                        }),
601                        location: Some(Location {
602                            location_type: LocationType::After.into(),
603                            after_column_name: "ts".to_string(),
604                        }),
605                        add_if_not_exists: true,
606                    },
607                ],
608            })),
609        };
610
611        let alter_request = alter_expr_to_request(1, expr, None).unwrap();
612        assert_eq!(alter_request.catalog_name, "");
613        assert_eq!(alter_request.schema_name, "");
614        assert_eq!("monitor".to_string(), alter_request.table_name);
615
616        let mut add_columns = match alter_request.alter_kind {
617            AlterKind::AddColumns { columns } => columns,
618            _ => unreachable!(),
619        };
620
621        let add_column = add_columns.pop().unwrap();
622        assert!(!add_column.is_key);
623        assert_eq!("cpu_usage", add_column.column_schema.name);
624        assert_eq!(
625            ConcreteDataType::float64_datatype(),
626            add_column.column_schema.data_type
627        );
628        assert_eq!(
629            Some(AddColumnLocation::After {
630                column_name: "ts".to_string()
631            }),
632            add_column.location
633        );
634        assert!(add_column.add_if_not_exists);
635
636        let add_column = add_columns.pop().unwrap();
637        assert!(!add_column.is_key);
638        assert_eq!("mem_usage", add_column.column_schema.name);
639        assert_eq!(
640            ConcreteDataType::float64_datatype(),
641            add_column.column_schema.data_type
642        );
643        assert_eq!(Some(AddColumnLocation::First), add_column.location);
644        assert!(!add_column.add_if_not_exists);
645    }
646
647    #[test]
648    fn test_modify_column_type_expr() {
649        let expr = AlterTableExpr {
650            catalog_name: "test_catalog".to_string(),
651            schema_name: "test_schema".to_string(),
652            table_name: "monitor".to_string(),
653
654            kind: Some(Kind::ModifyColumnTypes(ModifyColumnTypes {
655                modify_column_types: vec![ModifyColumnType {
656                    column_name: "mem_usage".to_string(),
657                    target_type: ColumnDataType::String as i32,
658                    target_type_extension: None,
659                }],
660            })),
661        };
662
663        let alter_request = alter_expr_to_request(1, expr, None).unwrap();
664        assert_eq!(alter_request.catalog_name, "test_catalog");
665        assert_eq!(alter_request.schema_name, "test_schema");
666        assert_eq!("monitor".to_string(), alter_request.table_name);
667
668        let mut modify_column_types = match alter_request.alter_kind {
669            AlterKind::ModifyColumnTypes { columns } => columns,
670            _ => unreachable!(),
671        };
672
673        let modify_column_type = modify_column_types.pop().unwrap();
674        assert_eq!("mem_usage", modify_column_type.column_name);
675        assert_eq!(
676            ConcreteDataType::string_datatype(),
677            modify_column_type.target_type
678        );
679    }
680
681    #[test]
682    fn test_drop_column_expr() {
683        let expr = AlterTableExpr {
684            catalog_name: "test_catalog".to_string(),
685            schema_name: "test_schema".to_string(),
686            table_name: "monitor".to_string(),
687
688            kind: Some(Kind::DropColumns(DropColumns {
689                drop_columns: vec![DropColumn {
690                    name: "mem_usage".to_string(),
691                }],
692            })),
693        };
694
695        let alter_request = alter_expr_to_request(1, expr, None).unwrap();
696        assert_eq!(alter_request.catalog_name, "test_catalog");
697        assert_eq!(alter_request.schema_name, "test_schema");
698        assert_eq!("monitor".to_string(), alter_request.table_name);
699
700        let mut drop_names = match alter_request.alter_kind {
701            AlterKind::DropColumns { names } => names,
702            _ => unreachable!(),
703        };
704        assert_eq!(1, drop_names.len());
705        assert_eq!("mem_usage".to_string(), drop_names.pop().unwrap());
706    }
707
708    #[test]
709    fn test_repartition_hints_together() {
710        let keys = [
711            REPARTITION_COLUMN_HINT_KEY,
712            table::requests::REPARTITION_PARTITION_NUM_HINT_KEY,
713        ];
714        let options = vec![
715            (keys[0].to_string(), "host".to_string()),
716            (keys[1].to_string(), "10".to_string()),
717        ];
718        let set = Kind::SetTableOptions(SetTableOptions {
719            table_options: options
720                .iter()
721                .map(|(key, value)| PbOption {
722                    key: key.clone(),
723                    value: value.clone(),
724                })
725                .collect(),
726        });
727        let unset = Kind::UnsetTableOptions(UnsetTableOptions {
728            keys: keys.map(str::to_string).to_vec(),
729        });
730        for kind in [set, unset] {
731            assert_eq!(
732                annotation_alter_family(&kind).unwrap(),
733                Some(AnnotationFamily::RepartitionHint)
734            );
735            let request = alter_expr_to_request(
736                1,
737                AlterTableExpr {
738                    kind: Some(kind),
739                    ..Default::default()
740                },
741                None,
742            )
743            .unwrap();
744            match request.alter_kind {
745                AlterKind::SetAnnotations {
746                    family,
747                    options: actual,
748                } => {
749                    assert_eq!(family, AnnotationFamily::RepartitionHint);
750                    assert_eq!(actual, options);
751                }
752                AlterKind::UnsetAnnotations {
753                    family,
754                    keys: actual,
755                } => {
756                    assert_eq!(family, AnnotationFamily::RepartitionHint);
757                    assert_eq!(actual, keys);
758                }
759                other => panic!("unexpected alter kind: {other:?}"),
760            }
761        }
762    }
763
764    #[test]
765    fn test_repartition_partition_num_hint_expr() {
766        let key = table::requests::REPARTITION_PARTITION_NUM_HINT_KEY;
767        let set = Kind::SetTableOptions(SetTableOptions {
768            table_options: vec![PbOption {
769                key: key.to_string(),
770                value: "8".to_string(),
771            }],
772        });
773        let unset = Kind::UnsetTableOptions(UnsetTableOptions {
774            keys: vec![key.to_string()],
775        });
776        for kind in [set, unset] {
777            assert_eq!(
778                annotation_alter_family(&kind).unwrap(),
779                Some(AnnotationFamily::RepartitionHint)
780            );
781            let request = alter_expr_to_request(
782                1,
783                AlterTableExpr {
784                    kind: Some(kind),
785                    ..Default::default()
786                },
787                None,
788            )
789            .unwrap();
790            match request.alter_kind {
791                AlterKind::SetAnnotations { family, options } => {
792                    assert_eq!(family, AnnotationFamily::RepartitionHint);
793                    assert_eq!(options, vec![(key.to_string(), "8".to_string())]);
794                }
795                AlterKind::UnsetAnnotations { family, keys } => {
796                    assert_eq!(family, AnnotationFamily::RepartitionHint);
797                    assert_eq!(keys, vec![key.to_string()]);
798                }
799                other => panic!("unexpected alter kind: {other:?}"),
800            }
801        }
802        for other in [key, table::requests::TTL_KEY] {
803            for kind in [
804                Kind::SetTableOptions(SetTableOptions {
805                    table_options: [key, other]
806                        .into_iter()
807                        .map(|key| PbOption {
808                            key: key.to_string(),
809                            value: "8".to_string(),
810                        })
811                        .collect(),
812                }),
813                Kind::UnsetTableOptions(UnsetTableOptions {
814                    keys: vec![key.to_string(), other.to_string()],
815                }),
816            ] {
817                assert!(annotation_alter_family(&kind).is_err());
818                assert!(
819                    alter_expr_to_request(
820                        1,
821                        AlterTableExpr {
822                            kind: Some(kind),
823                            ..Default::default()
824                        },
825                        None
826                    )
827                    .is_err()
828                );
829            }
830        }
831    }
832
833    #[test]
834    fn test_set_repartition_column_hint_expr() {
835        let expr = AlterTableExpr {
836            catalog_name: "test_catalog".to_string(),
837            schema_name: "test_schema".to_string(),
838            table_name: "monitor".to_string(),
839            kind: Some(Kind::SetTableOptions(SetTableOptions {
840                table_options: vec![PbOption {
841                    key: REPARTITION_COLUMN_HINT_KEY.to_string(),
842                    value: "host".to_string(),
843                }],
844            })),
845        };
846
847        let alter_request = alter_expr_to_request(1, expr, None).unwrap();
848        match alter_request.alter_kind {
849            AlterKind::SetAnnotations { family, options } => {
850                assert_eq!(AnnotationFamily::RepartitionHint, family);
851                assert_eq!(
852                    vec![(REPARTITION_COLUMN_HINT_KEY.to_string(), "host".to_string())],
853                    options
854                );
855            }
856            _ => unreachable!(),
857        }
858    }
859
860    #[test]
861    fn test_set_repartition_column_hint_rejects_mixed_options() {
862        let expr = AlterTableExpr {
863            catalog_name: "test_catalog".to_string(),
864            schema_name: "test_schema".to_string(),
865            table_name: "monitor".to_string(),
866            kind: Some(Kind::SetTableOptions(SetTableOptions {
867                table_options: vec![
868                    PbOption {
869                        key: REPARTITION_COLUMN_HINT_KEY.to_string(),
870                        value: "host".to_string(),
871                    },
872                    PbOption {
873                        key: table::requests::TTL_KEY.to_string(),
874                        value: "7d".to_string(),
875                    },
876                ],
877            })),
878        };
879
880        let err = alter_expr_to_request(1, expr, None).unwrap_err();
881        assert!(
882            err.to_string()
883                .contains("repartition hints must be altered separately")
884        );
885
886        // Duplicate hint entries are not a meaningful batch either.
887        let dup = AlterTableExpr {
888            catalog_name: "test_catalog".to_string(),
889            schema_name: "test_schema".to_string(),
890            table_name: "monitor".to_string(),
891            kind: Some(Kind::SetTableOptions(SetTableOptions {
892                table_options: vec![
893                    PbOption {
894                        key: REPARTITION_COLUMN_HINT_KEY.to_string(),
895                        value: "host".to_string(),
896                    },
897                    PbOption {
898                        key: REPARTITION_COLUMN_HINT_KEY.to_string(),
899                        value: "region".to_string(),
900                    },
901                ],
902            })),
903        };
904        let err = alter_expr_to_request(1, dup, None).unwrap_err();
905        assert!(err.to_string().contains("duplicate repartition hint keys"));
906    }
907
908    #[test]
909    fn test_unset_repartition_column_hint_expr() {
910        let expr = AlterTableExpr {
911            catalog_name: "test_catalog".to_string(),
912            schema_name: "test_schema".to_string(),
913            table_name: "monitor".to_string(),
914            kind: Some(Kind::UnsetTableOptions(UnsetTableOptions {
915                keys: vec![REPARTITION_COLUMN_HINT_KEY.to_string()],
916            })),
917        };
918
919        let alter_request = alter_expr_to_request(1, expr, None).unwrap();
920        match alter_request.alter_kind {
921            AlterKind::UnsetAnnotations { family, keys } => {
922                assert_eq!(AnnotationFamily::RepartitionHint, family);
923                assert_eq!(vec![REPARTITION_COLUMN_HINT_KEY.to_string()], keys);
924            }
925            _ => unreachable!(),
926        }
927    }
928
929    #[test]
930    fn test_semantic_options_classified_as_annotations() {
931        let expr = AlterTableExpr {
932            catalog_name: "test_catalog".to_string(),
933            schema_name: "test_schema".to_string(),
934            table_name: "monitor".to_string(),
935            kind: Some(Kind::SetTableOptions(SetTableOptions {
936                table_options: vec![
937                    PbOption {
938                        key: "greptime.semantic.signal_type".to_string(),
939                        value: "trace".to_string(),
940                    },
941                    PbOption {
942                        key: "greptime.semantic.entity.host.id".to_string(),
943                        value: "host".to_string(),
944                    },
945                ],
946            })),
947        };
948
949        let alter_request = alter_expr_to_request(1, expr, None).unwrap();
950        let AlterKind::SetAnnotations { family, options } = alter_request.alter_kind else {
951            panic!(
952                "expected SetAnnotations, got {:?}",
953                alter_request.alter_kind
954            );
955        };
956        assert_eq!(family, AnnotationFamily::Semantic);
957        assert_eq!(options.len(), 2);
958
959        let expr = AlterTableExpr {
960            catalog_name: "test_catalog".to_string(),
961            schema_name: "test_schema".to_string(),
962            table_name: "monitor".to_string(),
963            kind: Some(Kind::UnsetTableOptions(UnsetTableOptions {
964                keys: vec!["greptime.semantic.signal_type".to_string()],
965            })),
966        };
967        let alter_request = alter_expr_to_request(1, expr, None).unwrap();
968        assert!(matches!(
969            alter_request.alter_kind,
970            AlterKind::UnsetAnnotations {
971                family: AnnotationFamily::Semantic,
972                ..
973            }
974        ));
975    }
976
977    #[test]
978    fn test_semantic_options_reject_mixed_batch() {
979        let mixed_set = AlterTableExpr {
980            catalog_name: "test_catalog".to_string(),
981            schema_name: "test_schema".to_string(),
982            table_name: "monitor".to_string(),
983            kind: Some(Kind::SetTableOptions(SetTableOptions {
984                table_options: vec![
985                    PbOption {
986                        key: "greptime.semantic.signal_type".to_string(),
987                        value: "trace".to_string(),
988                    },
989                    PbOption {
990                        key: table::requests::TTL_KEY.to_string(),
991                        value: "7d".to_string(),
992                    },
993                ],
994            })),
995        };
996        let err = alter_expr_to_request(1, mixed_set, None).unwrap_err();
997        assert!(err.to_string().contains("altered separately"), "{err}");
998
999        let mixed_unset = AlterTableExpr {
1000            catalog_name: "test_catalog".to_string(),
1001            schema_name: "test_schema".to_string(),
1002            table_name: "monitor".to_string(),
1003            kind: Some(Kind::UnsetTableOptions(UnsetTableOptions {
1004                keys: vec![
1005                    "ttl".to_string(),
1006                    "greptime.semantic.signal_type".to_string(),
1007                ],
1008            })),
1009        };
1010        let err = alter_expr_to_request(1, mixed_unset, None).unwrap_err();
1011        assert!(err.to_string().contains("altered separately"), "{err}");
1012    }
1013
1014    #[test]
1015    fn test_annotation_alter_family() {
1016        let mixed = Kind::SetTableOptions(SetTableOptions {
1017            table_options: vec![
1018                PbOption {
1019                    key: "greptime.semantic.signal_type".to_string(),
1020                    value: "trace".to_string(),
1021                },
1022                PbOption {
1023                    key: "ttl".to_string(),
1024                    value: "7d".to_string(),
1025                },
1026            ],
1027        });
1028        let err = annotation_alter_family(&mixed).unwrap_err();
1029        assert!(
1030            err.to_string().contains("must be altered separately"),
1031            "{err}"
1032        );
1033
1034        // Two annotation families cannot share a batch either.
1035        let cross = Kind::SetTableOptions(SetTableOptions {
1036            table_options: vec![
1037                PbOption {
1038                    key: "greptime.semantic.signal_type".to_string(),
1039                    value: "trace".to_string(),
1040                },
1041                PbOption {
1042                    key: REPARTITION_COLUMN_HINT_KEY.to_string(),
1043                    value: "host".to_string(),
1044                },
1045            ],
1046        });
1047        let err = annotation_alter_family(&cross).unwrap_err();
1048        assert!(
1049            err.to_string().contains("must be altered separately"),
1050            "{err}"
1051        );
1052    }
1053}