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