Skip to main content

common_meta/ddl/
alter_table.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
15mod executor;
16mod metadata;
17mod partition_compat;
18mod region_request;
19
20pub use partition_compat::validate_region_partition_bounds;
21
22use std::collections::HashSet;
23use std::vec;
24
25use api::region::RegionResponse;
26use api::v1::alter_table_expr::Kind;
27use api::v1::{RenameTable, SetTableOptions};
28use async_trait::async_trait;
29use common_catalog::consts::{METRIC_ENGINE, MITO_ENGINE};
30use common_error::ext::BoxedError;
31use common_procedure::error::{FromJsonSnafu, Result as ProcedureResult, ToJsonSnafu};
32use common_procedure::{
33    Context as ProcedureContext, ContextProvider, Error as ProcedureError, EventContext,
34    EventTrigger, LockKey, PoisonKey, PoisonKeys, Procedure, ProcedureId, Status, StringKey,
35};
36use common_telemetry::{error, info, warn};
37use common_wal::options::WalOptions;
38use serde::{Deserialize, Serialize};
39use snafu::{ResultExt, ensure};
40use store_api::metadata::ColumnMetadata;
41use store_api::metric_engine_consts::TABLE_COLUMN_METADATA_EXTENSION_KEY;
42use store_api::storage::RegionId;
43use strum::AsRefStr;
44use table::metadata::{TableId, TableInfo};
45use table::requests::SKIP_WAL_KEY;
46use table::table_reference::TableReference;
47
48use crate::ddl::DdlContext;
49use crate::ddl::alter_table::executor::AlterTableExecutor;
50use crate::ddl::event::table::{
51    TableDdlEvent, TableDdlEventType, TableDdlLocator, alter_table_kind_name,
52};
53use crate::ddl::utils::{
54    MultipleResults, extract_column_metadatas, extract_region_wal_options, handle_multiple_results,
55    map_to_procedure_error, sync_follower_regions,
56};
57use crate::error::{
58    AbortProcedureSnafu, ConvertAlterTableRequestSnafu, NoLeaderSnafu, PutPoisonSnafu, Result,
59    RetryLaterSnafu, UnexpectedSnafu, UnsupportedSnafu,
60};
61use crate::key::table_info::TableInfoValue;
62use crate::key::{DeserializedValueWithBytes, RegionDistribution};
63use crate::lock_key::{CatalogLock, RegionLock, SchemaLock, TableLock, TableNameLock};
64use crate::metrics;
65use crate::poison_key::table_poison_key;
66use crate::rpc::ddl::AlterTableTask;
67use crate::rpc::router::{RegionRoute, find_leaders, region_distribution};
68
69/// The alter table procedure
70pub struct AlterTableProcedure {
71    /// The runtime context.
72    context: DdlContext,
73    /// The serialized data.
74    data: AlterTableData,
75    /// Cached new table metadata in the prepare step.
76    /// If we recover the procedure from json, then the table info value is not cached.
77    /// But we already validated it in the prepare step.
78    new_table_info: Option<TableInfo>,
79    /// The alter table executor.
80    executor: AlterTableExecutor,
81}
82
83#[derive(Debug)]
84pub(crate) struct RegionRouteChanged;
85
86/// Builds the executor from the [`AlterTableData`].
87///
88/// # Panics
89/// - If the alter kind is not set.
90fn build_executor_from_alter_expr(alter_data: &AlterTableData) -> AlterTableExecutor {
91    let table_name = alter_data.table_ref().into();
92    let table_id = alter_data.table_id;
93    let alter_kind = alter_data.task.alter_table.kind.as_ref().unwrap();
94    let new_table_name = if let Kind::RenameTable(RenameTable { new_table_name }) = alter_kind {
95        Some(new_table_name.clone())
96    } else {
97        None
98    };
99    AlterTableExecutor::new(table_name, table_id, new_table_name)
100}
101
102impl AlterTableProcedure {
103    pub const TYPE_NAME: &'static str = "metasrv-procedure::AlterTable";
104
105    pub fn new(table_id: TableId, task: AlterTableTask, context: DdlContext) -> Result<Self> {
106        Self::new_with_region_locks(table_id, task, vec![], context)
107    }
108
109    pub(crate) fn new_with_region_locks(
110        table_id: TableId,
111        task: AlterTableTask,
112        region_locks: Vec<RegionId>,
113        context: DdlContext,
114    ) -> Result<Self> {
115        task.validate()?;
116        let data = AlterTableData::new(task, table_id, region_locks);
117        let executor = build_executor_from_alter_expr(&data);
118        Ok(Self {
119            context,
120            data,
121            new_table_info: None,
122            executor,
123        })
124    }
125
126    pub fn from_json(json: &str, context: DdlContext) -> ProcedureResult<Self> {
127        let data: AlterTableData = serde_json::from_str(json).context(FromJsonSnafu)?;
128        let executor = build_executor_from_alter_expr(&data);
129
130        Ok(AlterTableProcedure {
131            context,
132            data,
133            new_table_info: None,
134            executor,
135        })
136    }
137
138    // Checks whether the table exists.
139    pub(crate) async fn on_prepare(&mut self) -> Result<Status> {
140        self.executor
141            .on_prepare(&self.context.table_metadata_manager)
142            .await?;
143        self.fill_table_info().await?;
144
145        // Safety: filled in `fill_table_info`.
146        let table_info_value = self.data.table_info_value.as_ref().unwrap();
147        let new_table_info = AlterTableExecutor::validate_alter_table_expr(
148            &table_info_value.table_info,
149            self.data.task.alter_table.clone(),
150        )?;
151        self.new_table_info = Some(new_table_info);
152
153        self.validate_region_partition_exprs().await?;
154
155        // Safety: Checked in `AlterTableProcedure::new`.
156        let alter_kind = self.data.task.alter_table.kind.as_ref().unwrap();
157        if sets_skip_wal(alter_kind) {
158            ensure!(
159                only_sets_skip_wal(alter_kind),
160                UnsupportedSnafu {
161                    operation: "combining skip_wal with other table options".to_string()
162                }
163            );
164            let engine = table_info_value.table_info.meta.engine.as_str();
165            ensure!(
166                engine == MITO_ENGINE || engine == METRIC_ENGINE,
167                UnsupportedSnafu {
168                    operation: format!("setting skip_wal on {engine} engine tables")
169                }
170            );
171            // Keep skip-WAL changes serialized with region migration and use the same
172            // route snapshot for validation and RPC delivery.
173            let (physical_table_id, physical_table_route) = self
174                .context
175                .table_metadata_manager
176                .table_route_manager()
177                .get_physical_table_route(self.data.table_id())
178                .await?;
179            ensure!(
180                physical_table_id == self.data.table_id(),
181                UnsupportedSnafu {
182                    operation: "setting skip_wal on logical tables".to_string()
183                }
184            );
185            let current_region_ids = physical_table_route
186                .region_routes
187                .iter()
188                .map(|route| route.region.id)
189                .collect::<HashSet<_>>();
190            let locked_region_ids = self
191                .data
192                .region_locks
193                .iter()
194                .copied()
195                .collect::<HashSet<_>>();
196            if physical_table_route.region_routes.len() != self.data.region_locks.len()
197                || current_region_ids != locked_region_ids
198            {
199                // Procedure lock keys are fixed at submission. Finish this attempt so the
200                // DDL manager can submit another procedure with locks for the current route.
201                return Ok(Status::done_with_output(RegionRouteChanged));
202            }
203            ensure!(
204                !find_leaders(&physical_table_route.region_routes).is_empty(),
205                NoLeaderSnafu {
206                    table_id: physical_table_id
207                }
208            );
209            // validate_alter_table_expr() has already parsed this single skip-WAL option.
210            let Some(skip_wal) = skip_wal_value(alter_kind) else {
211                return UnexpectedSnafu {
212                    err_msg: "missing or invalid skip_wal value after validation".to_string(),
213                }
214                .fail();
215            };
216            if !skip_wal {
217                let datanode_table_values = self
218                    .context
219                    .table_metadata_manager
220                    .datanode_table_manager()
221                    .regions(physical_table_id, &physical_table_route)
222                    .await?;
223                let region_wal_options = extract_region_wal_options(&datanode_table_values)?;
224                ensure!(
225                    current_region_ids.iter().all(|region_id| {
226                        matches!(
227                            region_wal_options
228                                .get(&region_id.region_number())
229                                .cloned()
230                                .unwrap_or_default(),
231                            WalOptions::RaftEngine
232                                | WalOptions::Kafka(_)
233                                | WalOptions::ObjectStore(_)
234                        )
235                    }),
236                    UnsupportedSnafu {
237                        operation: "setting skip_wal = 'false' without an existing WAL provider"
238                            .to_string()
239                    }
240                );
241            }
242            self.data.region_distribution =
243                Some(region_distribution(&physical_table_route.region_routes));
244        }
245        self.data.state = self.data.flow()?.after_prepare();
246        Ok(Status::executing(true))
247    }
248
249    /// Rejects type changes the persisted partition expressions can't take, before
250    /// any region is altered.
251    async fn validate_region_partition_exprs(&self) -> Result<()> {
252        // Safety: filled in `fill_table_info`.
253        let old_schema = &self
254            .data
255            .table_info_value
256            .as_ref()
257            .unwrap()
258            .table_info
259            .meta
260            .schema;
261        // Safety: set right after the new table info is built in `on_prepare`.
262        let new_schema = &self.new_table_info.as_ref().unwrap().meta.schema;
263        let changed_columns = partition_compat::changed_column_types(old_schema, new_schema);
264        if changed_columns.is_empty() {
265            return Ok(());
266        }
267
268        let (physical_table_id, physical_table_route) = self
269            .context
270            .table_metadata_manager
271            .table_route_manager()
272            .get_physical_table_route(self.data.table_id())
273            .await?;
274        if physical_table_id != self.data.table_id() {
275            // Partition expressions live on the physical table's regions.
276            return Ok(());
277        }
278
279        partition_compat::validate_region_partition_bounds(
280            &changed_columns,
281            &physical_table_route.region_routes,
282        )
283    }
284
285    fn table_poison_key(&self) -> PoisonKey {
286        table_poison_key(self.data.table_id())
287    }
288
289    async fn put_poison(
290        &self,
291        ctx_provider: &dyn ContextProvider,
292        procedure_id: ProcedureId,
293    ) -> Result<()> {
294        let poison_key = self.table_poison_key();
295        ctx_provider
296            .try_put_poison(&poison_key, procedure_id)
297            .await
298            .context(PutPoisonSnafu)
299    }
300
301    pub async fn submit_alter_region_requests(
302        &mut self,
303        procedure_id: ProcedureId,
304        ctx_provider: &dyn ContextProvider,
305    ) -> Result<Status> {
306        let table_id = self.data.table_id();
307        let (_, physical_table_route) = self
308            .context
309            .table_metadata_manager
310            .table_route_manager()
311            .get_physical_table_route(table_id)
312            .await?;
313
314        self.data.region_distribution =
315            Some(region_distribution(&physical_table_route.region_routes));
316        let leaders = find_leaders(&physical_table_route.region_routes);
317        let alter_kind = self.make_region_alter_kind()?;
318
319        info!(
320            "Submitting alter region requests for table {}, table_id: {}, alter_kind: {:?}",
321            self.data.table_ref(),
322            table_id,
323            alter_kind,
324        );
325
326        ensure!(!leaders.is_empty(), NoLeaderSnafu { table_id });
327        // Puts the poison before submitting alter region requests to datanodes.
328        self.put_poison(ctx_provider, procedure_id).await?;
329        let flow = self.data.flow()?;
330        if flow == AlterTableFlow::MetadataFirst {
331            let results = self
332                .executor
333                .on_alter_skip_wal_regions(
334                    &self.context.node_manager,
335                    &physical_table_route.region_routes,
336                    alter_kind,
337                )
338                .await;
339
340            return match handle_multiple_results(results) {
341                MultipleResults::PartialRetryable(error) => Err(error),
342                MultipleResults::PartialNonRetryable(error)
343                | MultipleResults::AllNonRetryable(error) => {
344                    // The metadata already contains the requested skip-WAL value. Retry the
345                    // idempotent request so later attempts can update the remaining replicas.
346                    Err(BoxedError::new(error)).context(RetryLaterSnafu {
347                        clean_poisons: true,
348                    })
349                }
350                MultipleResults::AllRetryable(error) => {
351                    Err(BoxedError::new(error)).context(RetryLaterSnafu {
352                        clean_poisons: true,
353                    })
354                }
355                MultipleResults::Ok(results) => {
356                    self.handle_alter_region_response(results)?;
357                    Ok(Status::executing_with_clean_poisons(true))
358                }
359            };
360        }
361
362        let results = self
363            .executor
364            .on_alter_regions(
365                &self.context.node_manager,
366                &physical_table_route.region_routes,
367                alter_kind,
368            )
369            .await;
370
371        match handle_multiple_results(results) {
372            MultipleResults::PartialRetryable(error) => {
373                // Just returns the error, and wait for the next try.
374                Err(error)
375            }
376            MultipleResults::PartialNonRetryable(error) => {
377                error!(error; "Partial non-retryable errors occurred during alter table, table {}, table_id: {}", self.data.table_ref(), self.data.table_id());
378                // No retry will be done.
379                Ok(Status::poisoned(
380                    Some(self.table_poison_key()),
381                    ProcedureError::external(error),
382                ))
383            }
384            MultipleResults::AllRetryable(error) => {
385                // Just returns the error, and wait for the next try.
386                let err = BoxedError::new(error);
387                Err(err).context(RetryLaterSnafu {
388                    clean_poisons: true,
389                })
390            }
391            MultipleResults::Ok(results) => {
392                self.submit_sync_region_requests(&results, &physical_table_route.region_routes)
393                    .await;
394                self.handle_alter_region_response(results)?;
395                Ok(Status::executing_with_clean_poisons(true))
396            }
397            MultipleResults::AllNonRetryable(error) => {
398                error!(error; "All alter requests returned non-retryable errors for table {}, table_id: {}", self.data.table_ref(), self.data.table_id());
399                // It assumes the metadata on datanode is not changed.
400                // Case: The alter region request is sent but not applied. (e.g., InvalidArgument)
401
402                let err = BoxedError::new(error);
403                Err(err).context(AbortProcedureSnafu {
404                    clean_poisons: true,
405                })
406            }
407        }
408    }
409
410    fn handle_alter_region_response(&mut self, mut results: Vec<RegionResponse>) -> Result<()> {
411        if let Some(column_metadatas) =
412            extract_column_metadatas(&mut results, TABLE_COLUMN_METADATA_EXTENSION_KEY)?
413        {
414            self.data.column_metadatas = column_metadatas;
415        } else {
416            warn!(
417                "altering table result doesn't contains extension key `{TABLE_COLUMN_METADATA_EXTENSION_KEY}`,leaving the table's column metadata unchanged"
418            );
419        }
420        self.data.state = self.data.flow()?.after_regions();
421        Ok(())
422    }
423
424    async fn submit_sync_region_requests(
425        &mut self,
426        results: &[RegionResponse],
427        region_routes: &[RegionRoute],
428    ) {
429        // Safety: filled in `prepare` step.
430        let table_info = self.data.table_info().unwrap();
431        if let Err(err) = sync_follower_regions(
432            &self.context,
433            self.data.table_id(),
434            results,
435            region_routes,
436            table_info.meta.engine.as_str(),
437        )
438        .await
439        {
440            error!(err; "Failed to sync regions for table {}, table_id: {}", self.data.table_ref(), self.data.table_id());
441        }
442    }
443
444    /// Update table metadata.
445    pub(crate) async fn on_update_metadata(&mut self) -> Result<Status> {
446        let table_id = self.data.table_id();
447        let table_ref = self.data.table_ref();
448        // Safety: filled in `fill_table_info`.
449        let table_info_value = self.data.table_info_value.as_ref().unwrap();
450        // Safety: Checked in `AlterTableProcedure::new`.
451        let alter_kind = self.data.task.alter_table.kind.as_ref().unwrap();
452        let flow = self.data.flow()?;
453        let metadata_only_alter = flow == AlterTableFlow::MetadataOnly;
454
455        // Gets the table info from the cache or builds it.
456        let  new_info = match &self.new_table_info {
457            Some(cached) => cached.clone(),
458            None => AlterTableExecutor::validate_alter_table_expr(
459                &table_info_value.table_info,
460                self.data.task.alter_table.clone(),
461               )
462                .inspect_err(|e| {
463                    // We already check the table info in the prepare step so this should not happen.
464                    error!(e; "Unable to build info for table {} in update metadata step, table_id: {}", table_ref, table_id);
465                })?,
466        };
467
468        // Safety: region distribution is set in `submit_alter_region_requests`.
469        self.executor
470            .on_alter_metadata(
471                &self.context.table_metadata_manager,
472                table_info_value,
473                self.data.region_distribution.as_ref(),
474                new_info,
475                &self.data.column_metadatas,
476                metadata_only_alter,
477            )
478            .await?;
479
480        info!(
481            "Updated table metadata for table {table_ref}, table_id: {table_id}, kind: {alter_kind:?}"
482        );
483        self.data.state = flow.after_metadata();
484        Ok(Status::executing(true))
485    }
486
487    /// Broadcasts the invalidating table cache instructions.
488    async fn on_broadcast(&mut self) -> Result<Status> {
489        self.executor
490            .invalidate_table_cache(&self.context.cache_invalidator)
491            .await?;
492        Ok(Status::done())
493    }
494
495    fn lock_key_inner(&self) -> Vec<StringKey> {
496        let mut lock_key = vec![];
497        let table_ref = self.data.table_ref();
498        let table_id = self.data.table_id();
499        lock_key.push(CatalogLock::Read(table_ref.catalog).into());
500        lock_key.push(SchemaLock::read(table_ref.catalog, table_ref.schema).into());
501        lock_key.push(TableLock::Write(table_id).into());
502
503        for region_id in &self.data.region_locks {
504            lock_key.push(RegionLock::Write(*region_id).into());
505        }
506
507        // Safety: Checked in `AlterTableProcedure::new`.
508        let alter_kind = self.data.task.alter_table.kind.as_ref().unwrap();
509        if let Kind::RenameTable(RenameTable { new_table_name }) = alter_kind {
510            lock_key.push(
511                TableNameLock::new(table_ref.catalog, table_ref.schema, new_table_name).into(),
512            )
513        }
514
515        lock_key
516    }
517
518    #[cfg(test)]
519    pub(crate) fn data(&self) -> &AlterTableData {
520        &self.data
521    }
522
523    #[cfg(test)]
524    pub(crate) fn mut_data(&mut self) -> &mut AlterTableData {
525        &mut self.data
526    }
527}
528
529fn sets_skip_wal(alter_kind: &Kind) -> bool {
530    let Kind::SetTableOptions(SetTableOptions { table_options }) = alter_kind else {
531        return false;
532    };
533
534    table_options
535        .iter()
536        .any(|option| option.key == SKIP_WAL_KEY)
537}
538
539pub(crate) fn only_sets_skip_wal(alter_kind: &Kind) -> bool {
540    let Kind::SetTableOptions(SetTableOptions { table_options }) = alter_kind else {
541        return false;
542    };
543
544    table_options.len() == 1 && table_options[0].key == SKIP_WAL_KEY
545}
546
547fn skip_wal_value(alter_kind: &Kind) -> Option<bool> {
548    let Kind::SetTableOptions(SetTableOptions { table_options }) = alter_kind else {
549        return None;
550    };
551    let [option] = table_options.as_slice() else {
552        return None;
553    };
554    if option.key != SKIP_WAL_KEY {
555        return None;
556    }
557    option.value.parse().ok()
558}
559
560fn is_metadata_only_alter(alter_kind: &Kind) -> Result<bool> {
561    // A mixed annotation batch is an error, never "not metadata-only": falling
562    // through to the region-first flow would dispatch it to regions.
563    let family = common_grpc_expr::annotation_alter_family(alter_kind)
564        .context(ConvertAlterTableRequestSnafu)?;
565    Ok(family.is_some() || matches!(alter_kind, Kind::RenameTable { .. }))
566}
567
568#[derive(Debug, Clone, Copy, PartialEq, Eq)]
569enum AlterTableFlow {
570    RegionFirst,
571    MetadataFirst,
572    MetadataOnly,
573}
574
575impl AlterTableFlow {
576    fn from_kind(kind: &Kind) -> Result<Self> {
577        Ok(if only_sets_skip_wal(kind) {
578            Self::MetadataFirst
579        } else if is_metadata_only_alter(kind)? {
580            Self::MetadataOnly
581        } else {
582            Self::RegionFirst
583        })
584    }
585
586    fn after_prepare(self) -> AlterTableState {
587        match self {
588            Self::RegionFirst => AlterTableState::SubmitAlterRegionRequests,
589            Self::MetadataFirst | Self::MetadataOnly => AlterTableState::UpdateMetadata,
590        }
591    }
592
593    fn after_regions(self) -> AlterTableState {
594        match self {
595            Self::RegionFirst => AlterTableState::UpdateMetadata,
596            Self::MetadataFirst | Self::MetadataOnly => AlterTableState::InvalidateTableCache,
597        }
598    }
599
600    fn after_metadata(self) -> AlterTableState {
601        match self {
602            Self::MetadataFirst => AlterTableState::SubmitAlterRegionRequests,
603            Self::RegionFirst | Self::MetadataOnly => AlterTableState::InvalidateTableCache,
604        }
605    }
606}
607
608#[async_trait]
609impl Procedure for AlterTableProcedure {
610    fn type_name(&self) -> &str {
611        Self::TYPE_NAME
612    }
613
614    async fn execute(&mut self, ctx: &ProcedureContext) -> ProcedureResult<Status> {
615        let state = &self.data.state;
616
617        let step = state.as_ref();
618
619        let _timer = metrics::METRIC_META_PROCEDURE_ALTER_TABLE
620            .with_label_values(&[step])
621            .start_timer();
622
623        match state {
624            AlterTableState::Prepare => self.on_prepare().await,
625            AlterTableState::SubmitAlterRegionRequests => {
626                self.submit_alter_region_requests(ctx.procedure_id, ctx.provider.as_ref())
627                    .await
628            }
629            AlterTableState::UpdateMetadata => self.on_update_metadata().await,
630            AlterTableState::InvalidateTableCache => self.on_broadcast().await,
631        }
632        .map_err(map_to_procedure_error)
633    }
634
635    fn dump(&self) -> ProcedureResult<String> {
636        serde_json::to_string(&self.data).context(ToJsonSnafu)
637    }
638
639    fn lock_key(&self) -> LockKey {
640        let key = self.lock_key_inner();
641
642        LockKey::new(key)
643    }
644
645    fn poison_keys(&self) -> PoisonKeys {
646        PoisonKeys::new(vec![self.table_poison_key()])
647    }
648
649    fn event(&self, ctx: &EventContext<'_>) -> Option<Box<dyn common_event_recorder::Event>> {
650        if !ctx
651            .event_type_filter
652            .allows(TableDdlEventType::AlterTable.as_str())
653        {
654            return None;
655        }
656        let table_ref = self.data.table_ref();
657        let locator = TableDdlLocator::new(table_ref.catalog, table_ref.schema, table_ref.table)
658            .with_table_id(self.data.table_id());
659        let event = match &ctx.trigger {
660            EventTrigger::Submitted => {
661                let kind = self
662                    .data
663                    .task
664                    .alter_table
665                    .kind
666                    .as_ref()
667                    .and_then(alter_table_kind_name);
668                TableDdlEvent::alter_table_submitted(locator, kind)
669            }
670            _ => TableDdlEvent::lifecycle(TableDdlEventType::AlterTable, [locator]),
671        };
672
673        Some(Box::new(event))
674    }
675}
676
677#[derive(Debug, Serialize, Deserialize, AsRefStr)]
678enum AlterTableState {
679    /// Prepares to alter the table.
680    Prepare,
681    /// Sends alter region requests to Datanode.
682    SubmitAlterRegionRequests,
683    /// Updates table metadata.
684    UpdateMetadata,
685    /// Broadcasts the invalidating table cache instruction.
686    InvalidateTableCache,
687}
688
689// The serialized data of alter table.
690#[derive(Debug, Serialize, Deserialize)]
691pub struct AlterTableData {
692    state: AlterTableState,
693    task: AlterTableTask,
694    table_id: TableId,
695    #[serde(default)]
696    column_metadatas: Vec<ColumnMetadata>,
697    /// Table info value before alteration.
698    table_info_value: Option<DeserializedValueWithBytes<TableInfoValue>>,
699    /// Region distribution for table in case we need to update region options.
700    region_distribution: Option<RegionDistribution>,
701    /// Region locks held by skip-WAL alters.
702    #[serde(default)]
703    region_locks: Vec<RegionId>,
704}
705
706impl AlterTableData {
707    pub fn new(task: AlterTableTask, table_id: TableId, region_locks: Vec<RegionId>) -> Self {
708        Self {
709            state: AlterTableState::Prepare,
710            task,
711            table_id,
712            column_metadatas: vec![],
713            table_info_value: None,
714            region_distribution: None,
715            region_locks,
716        }
717    }
718
719    fn table_ref(&self) -> TableReference<'_> {
720        self.task.table_ref()
721    }
722
723    fn table_id(&self) -> TableId {
724        self.table_id
725    }
726
727    fn table_info(&self) -> Option<&TableInfo> {
728        self.table_info_value
729            .as_ref()
730            .map(|value| &value.table_info)
731    }
732
733    fn flow(&self) -> Result<AlterTableFlow> {
734        // Safety: Checked in `AlterTableProcedure::new`.
735        AlterTableFlow::from_kind(self.task.alter_table.kind.as_ref().unwrap())
736    }
737
738    #[cfg(test)]
739    pub(crate) fn column_metadatas(&self) -> &[ColumnMetadata] {
740        &self.column_metadatas
741    }
742
743    #[cfg(test)]
744    pub(crate) fn set_column_metadatas(&mut self, column_metadatas: Vec<ColumnMetadata>) {
745        self.column_metadatas = column_metadatas;
746    }
747}