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