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