Skip to main content

common_meta/ddl/
comment_on.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 std::sync::Arc;
16
17use async_trait::async_trait;
18use chrono::Utc;
19use common_catalog::{format_full_flow_name, format_full_table_name};
20use common_procedure::error::{FromJsonSnafu, Result as ProcedureResult, ToJsonSnafu};
21use common_procedure::{Context as ProcedureContext, LockKey, Procedure, Status};
22use common_telemetry::tracing::info;
23use datatypes::schema::{COMMENT_KEY as COLUMN_COMMENT_KEY, Schema};
24use serde::{Deserialize, Serialize};
25use snafu::{OptionExt, ResultExt, ensure};
26use store_api::storage::TableId;
27use strum::AsRefStr;
28use table::metadata::TableInfo;
29use table::requests::COMMENT_KEY as TABLE_COMMENT_KEY;
30use table::table_name::TableName;
31
32use crate::cache_invalidator::Context;
33use crate::ddl::DdlContext;
34use crate::ddl::utils::map_to_procedure_error;
35use crate::error::{ColumnNotFoundSnafu, FlowNotFoundSnafu, Result, TableNotFoundSnafu};
36use crate::instruction::CacheIdent;
37use crate::key::flow::flow_info::{FlowInfoKey, FlowInfoValue};
38use crate::key::table_info::{TableInfoKey, TableInfoValue};
39use crate::key::table_name::TableNameKey;
40use crate::key::{DeserializedValueWithBytes, FlowId, MetadataKey, MetadataValue};
41use crate::lock_key::{CatalogLock, FlowLock, FlowNameLock, SchemaLock, TableLock, TableNameLock};
42use crate::rpc::ddl::{CommentObjectId, CommentObjectType, CommentOnTask};
43use crate::rpc::store::PutRequest;
44
45pub struct CommentOnProcedure {
46    pub context: DdlContext,
47    pub data: CommentOnData,
48}
49
50impl CommentOnProcedure {
51    pub const TYPE_NAME: &'static str = "metasrv-procedure::CommentOn";
52
53    pub fn new(task: CommentOnTask, context: DdlContext) -> Self {
54        Self {
55            context,
56            data: CommentOnData::new(task),
57        }
58    }
59
60    pub fn from_json(json: &str, context: DdlContext) -> ProcedureResult<Self> {
61        let data = serde_json::from_str(json).context(FromJsonSnafu)?;
62
63        Ok(Self { context, data })
64    }
65
66    pub async fn on_prepare(&mut self) -> Result<Status> {
67        match self.data.object_type {
68            CommentObjectType::Table | CommentObjectType::Column => {
69                self.prepare_table_or_column().await?;
70            }
71            CommentObjectType::Flow => {
72                self.prepare_flow().await?;
73            }
74        }
75
76        // Fast path: if comment is unchanged, skip update
77        if self.data.is_unchanged {
78            let object_desc = match self.data.object_type {
79                CommentObjectType::Table => format!(
80                    "table {}",
81                    format_full_table_name(
82                        &self.data.catalog_name,
83                        &self.data.schema_name,
84                        &self.data.object_name,
85                    )
86                ),
87                CommentObjectType::Column => format!(
88                    "column {}.{}",
89                    format_full_table_name(
90                        &self.data.catalog_name,
91                        &self.data.schema_name,
92                        &self.data.object_name,
93                    ),
94                    self.data.column_name.as_ref().unwrap()
95                ),
96                CommentObjectType::Flow => {
97                    format!("flow {}.{}", self.data.catalog_name, self.data.object_name)
98                }
99            };
100            info!("Comment unchanged for {}, skipping update", object_desc);
101            return Ok(Status::done());
102        }
103
104        self.data.state = CommentOnState::UpdateMetadata;
105        Ok(Status::executing(true))
106    }
107
108    async fn prepare_table_or_column(&mut self) -> Result<()> {
109        let table_id = if let Some(table_id) = self.data.table_id {
110            table_id
111        } else {
112            let table_name_key = TableNameKey::new(
113                &self.data.catalog_name,
114                &self.data.schema_name,
115                &self.data.object_name,
116            );
117
118            self.context
119                .table_metadata_manager
120                .table_name_manager()
121                .get(table_name_key)
122                .await?
123                .with_context(|| TableNotFoundSnafu {
124                    table_name: format_full_table_name(
125                        &self.data.catalog_name,
126                        &self.data.schema_name,
127                        &self.data.object_name,
128                    ),
129                })?
130                .table_id()
131        };
132
133        let table_info = self
134            .context
135            .table_metadata_manager
136            .table_info_manager()
137            .get(table_id)
138            .await?
139            .with_context(|| TableNotFoundSnafu {
140                table_name: format_full_table_name(
141                    &self.data.catalog_name,
142                    &self.data.schema_name,
143                    &self.data.object_name,
144                ),
145            })?;
146
147        // For column comments, validate the column exists
148        if self.data.object_type == CommentObjectType::Column {
149            let column_name = self.data.column_name.as_ref().unwrap();
150            let column_exists = table_info
151                .table_info
152                .meta
153                .schema
154                .column_schemas()
155                .iter()
156                .any(|col| &col.name == column_name);
157
158            ensure!(
159                column_exists,
160                ColumnNotFoundSnafu {
161                    column_name,
162                    column_id: 0u32, // column_id is not known here
163                }
164            );
165        }
166
167        self.data.table_id = Some(table_id);
168
169        // Check if comment is unchanged for early exit optimization
170        match self.data.object_type {
171            CommentObjectType::Table => {
172                let current_comment = &table_info.table_info.desc;
173                if &self.data.comment == current_comment {
174                    self.data.is_unchanged = true;
175                }
176            }
177            CommentObjectType::Column => {
178                let column_name = self.data.column_name.as_ref().unwrap();
179                let column_schema = table_info
180                    .table_info
181                    .meta
182                    .schema
183                    .column_schemas()
184                    .iter()
185                    .find(|col| &col.name == column_name)
186                    .unwrap(); // Safe: validated above
187
188                let current_comment = column_schema.metadata().get(COLUMN_COMMENT_KEY);
189                if self.data.comment.as_deref() == current_comment.map(String::as_str) {
190                    self.data.is_unchanged = true;
191                }
192            }
193            CommentObjectType::Flow => {
194                // this branch is handled in `prepare_flow`
195            }
196        }
197
198        self.data.table_info = Some(table_info);
199
200        Ok(())
201    }
202
203    async fn prepare_flow(&mut self) -> Result<()> {
204        let flow_id = if let Some(flow_id) = self.data.flow_id {
205            flow_id
206        } else {
207            self.context
208                .flow_metadata_manager
209                .flow_name_manager()
210                .get(&self.data.catalog_name, &self.data.object_name)
211                .await?
212                .with_context(|| FlowNotFoundSnafu {
213                    flow_name: format_full_flow_name(
214                        &self.data.catalog_name,
215                        &self.data.object_name,
216                    ),
217                })?
218                .flow_id()
219        };
220        let flow_info = self
221            .context
222            .flow_metadata_manager
223            .flow_info_manager()
224            .get_raw(flow_id)
225            .await?
226            .with_context(|| FlowNotFoundSnafu {
227                flow_name: format_full_flow_name(&self.data.catalog_name, &self.data.object_name),
228            })?;
229
230        self.data.flow_id = Some(flow_id);
231
232        // Check if comment is unchanged for early exit optimization
233        let current_comment = &flow_info.get_inner_ref().comment;
234        let new_comment = self.data.comment.as_deref().unwrap_or("");
235        if new_comment == current_comment.as_str() {
236            self.data.is_unchanged = true;
237        }
238
239        self.data.flow_info = Some(flow_info);
240
241        Ok(())
242    }
243
244    pub async fn on_update_metadata(&mut self) -> Result<Status> {
245        match self.data.object_type {
246            CommentObjectType::Table => {
247                self.update_table_comment().await?;
248            }
249            CommentObjectType::Column => {
250                self.update_column_comment().await?;
251            }
252            CommentObjectType::Flow => {
253                self.update_flow_comment().await?;
254            }
255        }
256
257        self.data.state = CommentOnState::InvalidateCache;
258        Ok(Status::executing(true))
259    }
260
261    async fn update_table_comment(&mut self) -> Result<()> {
262        let table_info_value = self.data.table_info.as_ref().unwrap();
263        let mut new_table_info = table_info_value.table_info.clone();
264
265        new_table_info.desc = self.data.comment.clone();
266
267        // Sync comment to table options
268        sync_table_comment_option(
269            &mut new_table_info.meta.options,
270            new_table_info.desc.as_deref(),
271        );
272
273        self.update_table_info(table_info_value, new_table_info)
274            .await?;
275
276        info!(
277            "Updated comment for table {}.{}.{}",
278            self.data.catalog_name, self.data.schema_name, self.data.object_name
279        );
280
281        Ok(())
282    }
283
284    async fn update_column_comment(&mut self) -> Result<()> {
285        let table_info_value = self.data.table_info.as_ref().unwrap();
286        let mut new_table_info = table_info_value.table_info.clone();
287
288        let column_name = self.data.column_name.as_ref().unwrap();
289        let mut column_schemas = new_table_info.meta.schema.column_schemas().to_vec();
290        let column_schema = column_schemas
291            .iter_mut()
292            .find(|col| &col.name == column_name)
293            .unwrap(); // Safe: validated in prepare
294
295        update_column_comment_metadata(column_schema, self.data.comment.clone());
296
297        new_table_info.meta.schema = Arc::new(Schema::new_with_version(
298            column_schemas,
299            new_table_info.meta.schema.version(),
300        ));
301        self.update_table_info(table_info_value, new_table_info)
302            .await?;
303
304        info!(
305            "Updated comment for column {}.{}.{}.{}",
306            self.data.catalog_name, self.data.schema_name, self.data.object_name, column_name
307        );
308
309        Ok(())
310    }
311
312    async fn update_flow_comment(&mut self) -> Result<()> {
313        let flow_id = self.data.flow_id.unwrap();
314        let flow_info_value = self.data.flow_info.as_ref().unwrap();
315
316        let mut new_flow_info = flow_info_value.get_inner_ref().clone();
317        new_flow_info.comment = self.data.comment.clone().unwrap_or_default();
318        new_flow_info.updated_time = Utc::now();
319
320        let raw_value = new_flow_info.try_as_raw_value()?;
321
322        self.context
323            .table_metadata_manager
324            .kv_backend()
325            .put(
326                PutRequest::new()
327                    .with_key(FlowInfoKey::new(flow_id).to_bytes())
328                    .with_value(raw_value),
329            )
330            .await?;
331
332        info!(
333            "Updated comment for flow {}.{}",
334            self.data.catalog_name, self.data.object_name
335        );
336
337        Ok(())
338    }
339
340    async fn update_table_info(
341        &self,
342        current_table_info: &DeserializedValueWithBytes<TableInfoValue>,
343        new_table_info: TableInfo,
344    ) -> Result<()> {
345        let table_id = current_table_info.table_info.ident.table_id;
346        let new_table_info_value = current_table_info.update(new_table_info);
347        let raw_value = new_table_info_value.try_as_raw_value()?;
348
349        self.context
350            .table_metadata_manager
351            .kv_backend()
352            .put(
353                PutRequest::new()
354                    .with_key(TableInfoKey::new(table_id).to_bytes())
355                    .with_value(raw_value),
356            )
357            .await?;
358
359        Ok(())
360    }
361
362    pub async fn on_invalidate_cache(&mut self) -> Result<Status> {
363        let cache_invalidator = &self.context.cache_invalidator;
364
365        match self.data.object_type {
366            CommentObjectType::Table | CommentObjectType::Column => {
367                let table_id = self.data.table_id.unwrap();
368                let table_name = TableName::new(
369                    self.data.catalog_name.clone(),
370                    self.data.schema_name.clone(),
371                    self.data.object_name.clone(),
372                );
373
374                let cache_ident = vec![
375                    CacheIdent::TableId(table_id),
376                    CacheIdent::TableName(table_name),
377                ];
378
379                cache_invalidator
380                    .invalidate(&Context::default(), &cache_ident)
381                    .await?;
382            }
383            CommentObjectType::Flow => {
384                let flow_id = self.data.flow_id.unwrap();
385                let cache_ident = vec![CacheIdent::FlowId(flow_id)];
386
387                cache_invalidator
388                    .invalidate(&Context::default(), &cache_ident)
389                    .await?;
390            }
391        }
392
393        Ok(Status::done())
394    }
395}
396
397#[async_trait]
398impl Procedure for CommentOnProcedure {
399    fn type_name(&self) -> &str {
400        Self::TYPE_NAME
401    }
402
403    async fn execute(&mut self, _ctx: &ProcedureContext) -> ProcedureResult<Status> {
404        match self.data.state {
405            CommentOnState::Prepare => self.on_prepare().await,
406            CommentOnState::UpdateMetadata => self.on_update_metadata().await,
407            CommentOnState::InvalidateCache => self.on_invalidate_cache().await,
408        }
409        .map_err(map_to_procedure_error)
410    }
411
412    fn dump(&self) -> ProcedureResult<String> {
413        serde_json::to_string(&self.data).context(ToJsonSnafu)
414    }
415
416    fn lock_key(&self) -> LockKey {
417        let catalog = &self.data.catalog_name;
418        let schema = &self.data.schema_name;
419
420        let lock_key = match self.data.object_type {
421            CommentObjectType::Table | CommentObjectType::Column => {
422                let mut lock_key = vec![
423                    CatalogLock::Read(catalog).into(),
424                    SchemaLock::read(catalog, schema).into(),
425                ];
426                if let Some(table_id) = self.data.table_id {
427                    lock_key.push(TableLock::Write(table_id).into());
428                }
429                lock_key.push(TableNameLock::new(catalog, schema, &self.data.object_name).into());
430                lock_key
431            }
432            CommentObjectType::Flow => {
433                let mut lock_key = vec![CatalogLock::Read(catalog).into()];
434                if let Some(flow_id) = self.data.flow_id {
435                    lock_key.push(FlowLock::Write(flow_id).into());
436                }
437                lock_key.push(FlowNameLock::new(catalog, &self.data.object_name).into());
438                lock_key
439            }
440        };
441
442        LockKey::new(lock_key)
443    }
444}
445
446#[derive(Debug, Serialize, Deserialize, AsRefStr)]
447enum CommentOnState {
448    Prepare,
449    UpdateMetadata,
450    InvalidateCache,
451}
452
453/// The data of comment on procedure.
454#[derive(Debug, Serialize, Deserialize)]
455pub struct CommentOnData {
456    state: CommentOnState,
457    catalog_name: String,
458    schema_name: String,
459    object_type: CommentObjectType,
460    object_name: String,
461    /// Column name (only for Column comments)
462    column_name: Option<String>,
463    comment: Option<String>,
464    /// Cached table ID (for Table/Column)
465    #[serde(skip_serializing_if = "Option::is_none")]
466    table_id: Option<TableId>,
467    /// Cached table info (for Table/Column)
468    #[serde(skip)]
469    table_info: Option<DeserializedValueWithBytes<TableInfoValue>>,
470    /// Cached flow ID (for Flow)
471    #[serde(skip_serializing_if = "Option::is_none")]
472    flow_id: Option<FlowId>,
473    /// Cached flow info (for Flow)
474    #[serde(skip)]
475    flow_info: Option<DeserializedValueWithBytes<FlowInfoValue>>,
476    /// Whether the comment is unchanged (optimization for early exit)
477    #[serde(skip)]
478    is_unchanged: bool,
479}
480
481impl CommentOnData {
482    pub fn new(task: CommentOnTask) -> Self {
483        let (table_id, flow_id) = match task.object_id {
484            Some(CommentObjectId::Table(table_id)) => (Some(table_id), None),
485            Some(CommentObjectId::Flow(flow_id)) => (None, Some(flow_id)),
486            None => (None, None),
487        };
488
489        Self {
490            state: CommentOnState::Prepare,
491            catalog_name: task.catalog_name,
492            schema_name: task.schema_name,
493            object_type: task.object_type,
494            object_name: task.object_name,
495            column_name: task.column_name,
496            comment: task.comment,
497            table_id,
498            table_info: None,
499            flow_id,
500            flow_info: None,
501            is_unchanged: false,
502        }
503    }
504}
505
506fn update_column_comment_metadata(
507    column_schema: &mut datatypes::schema::ColumnSchema,
508    comment: Option<String>,
509) {
510    match comment {
511        Some(value) => {
512            column_schema
513                .mut_metadata()
514                .insert(COLUMN_COMMENT_KEY.to_string(), value);
515        }
516        None => {
517            column_schema.mut_metadata().remove(COLUMN_COMMENT_KEY);
518        }
519    }
520}
521
522fn sync_table_comment_option(options: &mut table::requests::TableOptions, comment: Option<&str>) {
523    match comment {
524        Some(value) => {
525            options
526                .extra_options
527                .insert(TABLE_COMMENT_KEY.to_string(), value.to_string());
528        }
529        None => {
530            options.extra_options.remove(TABLE_COMMENT_KEY);
531        }
532    }
533}