1mod 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
69pub struct AlterTableProcedure {
71 context: DdlContext,
73 data: AlterTableData,
75 new_table_info: Option<TableInfo>,
79 executor: AlterTableExecutor,
81}
82
83#[derive(Debug)]
84pub(crate) struct RegionRouteChanged;
85
86fn 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 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 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 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 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 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 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(®ion_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 async fn validate_region_partition_exprs(&self) -> Result<()> {
252 let old_schema = &self
254 .data
255 .table_info_value
256 .as_ref()
257 .unwrap()
258 .table_info
259 .meta
260 .schema;
261 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 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 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 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 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 Ok(Status::poisoned(
380 Some(self.table_poison_key()),
381 ProcedureError::external(error),
382 ))
383 }
384 MultipleResults::AllRetryable(error) => {
385 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 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 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 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 let table_info_value = self.data.table_info_value.as_ref().unwrap();
450 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 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 error!(e; "Unable to build info for table {} in update metadata step, table_id: {}", table_ref, table_id);
465 })?,
466 };
467
468 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 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 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 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 Prepare,
681 SubmitAlterRegionRequests,
683 UpdateMetadata,
685 InvalidateTableCache,
687}
688
689#[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: Option<DeserializedValueWithBytes<TableInfoValue>>,
699 region_distribution: Option<RegionDistribution>,
701 #[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 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}