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