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?;
156
157 let alter_kind = self.data.task.alter_table.kind.as_ref().unwrap();
159 if sets_skip_wal(alter_kind) {
160 ensure!(
161 only_sets_skip_wal(alter_kind),
162 UnsupportedSnafu {
163 operation: "combining skip_wal with other table options".to_string()
164 }
165 );
166 let engine = table_info_value.table_info.meta.engine.as_str();
167 ensure!(
168 engine == MITO_ENGINE || engine == METRIC_ENGINE,
169 UnsupportedSnafu {
170 operation: format!("setting skip_wal on {engine} engine tables")
171 }
172 );
173 let (physical_table_id, physical_table_route) = self
176 .context
177 .table_metadata_manager
178 .table_route_manager()
179 .get_physical_table_route(self.data.table_id())
180 .await?;
181 ensure!(
182 physical_table_id == self.data.table_id(),
183 UnsupportedSnafu {
184 operation: "setting skip_wal on logical tables".to_string()
185 }
186 );
187 let current_region_ids = physical_table_route
188 .region_routes
189 .iter()
190 .map(|route| route.region.id)
191 .collect::<HashSet<_>>();
192 let locked_region_ids = self
193 .data
194 .region_locks
195 .iter()
196 .copied()
197 .collect::<HashSet<_>>();
198 if physical_table_route.region_routes.len() != self.data.region_locks.len()
199 || current_region_ids != locked_region_ids
200 {
201 return Ok(Status::done_with_output(RegionRouteChanged));
204 }
205 ensure!(
206 !find_leaders(&physical_table_route.region_routes).is_empty(),
207 NoLeaderSnafu {
208 table_id: physical_table_id
209 }
210 );
211 let Some(skip_wal) = skip_wal_value(alter_kind) else {
213 return UnexpectedSnafu {
214 err_msg: "missing or invalid skip_wal value after validation".to_string(),
215 }
216 .fail();
217 };
218 if !skip_wal {
219 let datanode_table_values = self
220 .context
221 .table_metadata_manager
222 .datanode_table_manager()
223 .regions(physical_table_id, &physical_table_route)
224 .await?;
225 let region_wal_options = extract_region_wal_options(&datanode_table_values)?;
226 ensure!(
227 current_region_ids.iter().all(|region_id| {
228 matches!(
229 region_wal_options
230 .get(®ion_id.region_number())
231 .cloned()
232 .unwrap_or_default(),
233 WalOptions::RaftEngine
234 | WalOptions::Kafka(_)
235 | WalOptions::ObjectStore(_)
236 )
237 }),
238 UnsupportedSnafu {
239 operation: "setting skip_wal = 'false' without an existing WAL provider"
240 .to_string()
241 }
242 );
243 }
244 self.data.region_distribution =
245 Some(region_distribution(&physical_table_route.region_routes));
246 }
247 self.data.state = self.data.flow()?.after_prepare();
248 Ok(Status::executing(true))
249 }
250
251 async fn validate_region_partition_exprs(&self) -> Result<()> {
255 let old_schema = &self
257 .data
258 .table_info_value
259 .as_ref()
260 .unwrap()
261 .table_info
262 .meta
263 .schema;
264 let new_schema = &self.new_table_info.as_ref().unwrap().meta.schema;
266 let changed_columns = partition_compat::changed_column_types(old_schema, new_schema);
267 if changed_columns.is_empty() {
268 return Ok(());
269 }
270
271 let (physical_table_id, physical_table_route) = self
272 .context
273 .table_metadata_manager
274 .table_route_manager()
275 .get_physical_table_route(self.data.table_id())
276 .await?;
277 if physical_table_id != self.data.table_id() {
278 return Ok(());
281 }
282
283 partition_compat::validate_region_partition_bounds(
284 &changed_columns,
285 &physical_table_route.region_routes,
286 )
287 }
288
289 fn table_poison_key(&self) -> PoisonKey {
290 table_poison_key(self.data.table_id())
291 }
292
293 async fn put_poison(
294 &self,
295 ctx_provider: &dyn ContextProvider,
296 procedure_id: ProcedureId,
297 ) -> Result<()> {
298 let poison_key = self.table_poison_key();
299 ctx_provider
300 .try_put_poison(&poison_key, procedure_id)
301 .await
302 .context(PutPoisonSnafu)
303 }
304
305 pub async fn submit_alter_region_requests(
306 &mut self,
307 procedure_id: ProcedureId,
308 ctx_provider: &dyn ContextProvider,
309 ) -> Result<Status> {
310 let table_id = self.data.table_id();
311 let (_, physical_table_route) = self
312 .context
313 .table_metadata_manager
314 .table_route_manager()
315 .get_physical_table_route(table_id)
316 .await?;
317
318 self.data.region_distribution =
319 Some(region_distribution(&physical_table_route.region_routes));
320 let leaders = find_leaders(&physical_table_route.region_routes);
321 let alter_kind = self.make_region_alter_kind()?;
322
323 info!(
324 "Submitting alter region requests for table {}, table_id: {}, alter_kind: {:?}",
325 self.data.table_ref(),
326 table_id,
327 alter_kind,
328 );
329
330 ensure!(!leaders.is_empty(), NoLeaderSnafu { table_id });
331 self.put_poison(ctx_provider, procedure_id).await?;
333 let flow = self.data.flow()?;
334 if flow == AlterTableFlow::MetadataFirst {
335 let results = self
336 .executor
337 .on_alter_skip_wal_regions(
338 &self.context.node_manager,
339 &physical_table_route.region_routes,
340 alter_kind,
341 )
342 .await;
343
344 return match handle_multiple_results(results) {
345 MultipleResults::PartialRetryable(error) => Err(error),
346 MultipleResults::PartialNonRetryable(error)
347 | MultipleResults::AllNonRetryable(error) => {
348 Err(BoxedError::new(error)).context(RetryLaterSnafu {
351 clean_poisons: true,
352 })
353 }
354 MultipleResults::AllRetryable(error) => {
355 Err(BoxedError::new(error)).context(RetryLaterSnafu {
356 clean_poisons: true,
357 })
358 }
359 MultipleResults::Ok(results) => {
360 self.handle_alter_region_response(results)?;
361 Ok(Status::executing_with_clean_poisons(true))
362 }
363 };
364 }
365
366 let results = self
367 .executor
368 .on_alter_regions(
369 &self.context.node_manager,
370 &physical_table_route.region_routes,
371 alter_kind,
372 )
373 .await;
374
375 match handle_multiple_results(results) {
376 MultipleResults::PartialRetryable(error) => {
377 Err(error)
379 }
380 MultipleResults::PartialNonRetryable(error) => {
381 error!(error; "Partial non-retryable errors occurred during alter table, table {}, table_id: {}", self.data.table_ref(), self.data.table_id());
382 Ok(Status::poisoned(
384 Some(self.table_poison_key()),
385 ProcedureError::external(error),
386 ))
387 }
388 MultipleResults::AllRetryable(error) => {
389 let err = BoxedError::new(error);
391 Err(err).context(RetryLaterSnafu {
392 clean_poisons: true,
393 })
394 }
395 MultipleResults::Ok(results) => {
396 self.submit_sync_region_requests(&results, &physical_table_route.region_routes)
397 .await;
398 self.handle_alter_region_response(results)?;
399 Ok(Status::executing_with_clean_poisons(true))
400 }
401 MultipleResults::AllNonRetryable(error) => {
402 error!(error; "All alter requests returned non-retryable errors for table {}, table_id: {}", self.data.table_ref(), self.data.table_id());
403 let err = BoxedError::new(error);
407 Err(err).context(AbortProcedureSnafu {
408 clean_poisons: true,
409 })
410 }
411 }
412 }
413
414 fn handle_alter_region_response(&mut self, mut results: Vec<RegionResponse>) -> Result<()> {
415 if let Some(column_metadatas) =
416 extract_column_metadatas(&mut results, TABLE_COLUMN_METADATA_EXTENSION_KEY)?
417 {
418 self.data.column_metadatas = column_metadatas;
419 } else {
420 warn!(
421 "altering table result doesn't contains extension key `{TABLE_COLUMN_METADATA_EXTENSION_KEY}`,leaving the table's column metadata unchanged"
422 );
423 }
424 self.data.state = self.data.flow()?.after_regions();
425 Ok(())
426 }
427
428 async fn submit_sync_region_requests(
429 &mut self,
430 results: &[RegionResponse],
431 region_routes: &[RegionRoute],
432 ) {
433 let table_info = self.data.table_info().unwrap();
435 if let Err(err) = sync_follower_regions(
436 &self.context,
437 self.data.table_id(),
438 results,
439 region_routes,
440 table_info.meta.engine.as_str(),
441 )
442 .await
443 {
444 error!(err; "Failed to sync regions for table {}, table_id: {}", self.data.table_ref(), self.data.table_id());
445 }
446 }
447
448 pub(crate) async fn on_update_metadata(&mut self) -> Result<Status> {
450 let table_id = self.data.table_id();
451 let table_ref = self.data.table_ref();
452 let table_info_value = self.data.table_info_value.as_ref().unwrap();
454 let alter_kind = self.data.task.alter_table.kind.as_ref().unwrap();
456 let flow = self.data.flow()?;
457 let metadata_only_alter = flow == AlterTableFlow::MetadataOnly;
458
459 let new_info = match &self.new_table_info {
461 Some(cached) => cached.clone(),
462 None => AlterTableExecutor::validate_alter_table_expr(
463 &table_info_value.table_info,
464 self.data.task.alter_table.clone(),
465 )
466 .inspect_err(|e| {
467 error!(e; "Unable to build info for table {} in update metadata step, table_id: {}", table_ref, table_id);
469 })?,
470 };
471
472 self.executor
474 .on_alter_metadata(
475 &self.context.table_metadata_manager,
476 table_info_value,
477 self.data.region_distribution.as_ref(),
478 new_info,
479 &self.data.column_metadatas,
480 metadata_only_alter,
481 )
482 .await?;
483
484 info!(
485 "Updated table metadata for table {table_ref}, table_id: {table_id}, kind: {alter_kind:?}"
486 );
487 self.data.state = flow.after_metadata();
488 Ok(Status::executing(true))
489 }
490
491 async fn on_broadcast(&mut self) -> Result<Status> {
493 self.executor
494 .invalidate_table_cache(&self.context.cache_invalidator)
495 .await?;
496 Ok(Status::done())
497 }
498
499 fn lock_key_inner(&self) -> Vec<StringKey> {
500 let mut lock_key = vec![];
501 let table_ref = self.data.table_ref();
502 let table_id = self.data.table_id();
503 lock_key.push(CatalogLock::Read(table_ref.catalog).into());
504 lock_key.push(SchemaLock::read(table_ref.catalog, table_ref.schema).into());
505 lock_key.push(TableLock::Write(table_id).into());
506
507 for region_id in &self.data.region_locks {
508 lock_key.push(RegionLock::Write(*region_id).into());
509 }
510
511 let alter_kind = self.data.task.alter_table.kind.as_ref().unwrap();
513 if let Kind::RenameTable(RenameTable { new_table_name }) = alter_kind {
514 lock_key.push(
515 TableNameLock::new(table_ref.catalog, table_ref.schema, new_table_name).into(),
516 )
517 }
518
519 lock_key
520 }
521
522 #[cfg(test)]
523 pub(crate) fn data(&self) -> &AlterTableData {
524 &self.data
525 }
526
527 #[cfg(test)]
528 pub(crate) fn mut_data(&mut self) -> &mut AlterTableData {
529 &mut self.data
530 }
531}
532
533fn sets_skip_wal(alter_kind: &Kind) -> bool {
534 let Kind::SetTableOptions(SetTableOptions { table_options }) = alter_kind else {
535 return false;
536 };
537
538 table_options
539 .iter()
540 .any(|option| option.key == SKIP_WAL_KEY)
541}
542
543pub(crate) fn only_sets_skip_wal(alter_kind: &Kind) -> bool {
544 let Kind::SetTableOptions(SetTableOptions { table_options }) = alter_kind else {
545 return false;
546 };
547
548 table_options.len() == 1 && table_options[0].key == SKIP_WAL_KEY
549}
550
551fn skip_wal_value(alter_kind: &Kind) -> Option<bool> {
552 let Kind::SetTableOptions(SetTableOptions { table_options }) = alter_kind else {
553 return None;
554 };
555 let [option] = table_options.as_slice() else {
556 return None;
557 };
558 if option.key != SKIP_WAL_KEY {
559 return None;
560 }
561 option.value.parse().ok()
562}
563
564fn is_metadata_only_alter(alter_kind: &Kind) -> Result<bool> {
565 let family = common_grpc_expr::annotation_alter_family(alter_kind)
568 .context(ConvertAlterTableRequestSnafu)?;
569 Ok(family.is_some() || matches!(alter_kind, Kind::RenameTable { .. }))
570}
571
572#[derive(Debug, Clone, Copy, PartialEq, Eq)]
573enum AlterTableFlow {
574 RegionFirst,
575 MetadataFirst,
576 MetadataOnly,
577}
578
579impl AlterTableFlow {
580 fn from_kind(kind: &Kind) -> Result<Self> {
581 Ok(if only_sets_skip_wal(kind) {
582 Self::MetadataFirst
583 } else if is_metadata_only_alter(kind)? {
584 Self::MetadataOnly
585 } else {
586 Self::RegionFirst
587 })
588 }
589
590 fn after_prepare(self) -> AlterTableState {
591 match self {
592 Self::RegionFirst => AlterTableState::SubmitAlterRegionRequests,
593 Self::MetadataFirst | Self::MetadataOnly => AlterTableState::UpdateMetadata,
594 }
595 }
596
597 fn after_regions(self) -> AlterTableState {
598 match self {
599 Self::RegionFirst => AlterTableState::UpdateMetadata,
600 Self::MetadataFirst | Self::MetadataOnly => AlterTableState::InvalidateTableCache,
601 }
602 }
603
604 fn after_metadata(self) -> AlterTableState {
605 match self {
606 Self::MetadataFirst => AlterTableState::SubmitAlterRegionRequests,
607 Self::RegionFirst | Self::MetadataOnly => AlterTableState::InvalidateTableCache,
608 }
609 }
610}
611
612#[async_trait]
613impl Procedure for AlterTableProcedure {
614 fn type_name(&self) -> &str {
615 Self::TYPE_NAME
616 }
617
618 async fn execute(&mut self, ctx: &ProcedureContext) -> ProcedureResult<Status> {
619 let state = &self.data.state;
620
621 let step = state.as_ref();
622
623 let _timer = metrics::METRIC_META_PROCEDURE_ALTER_TABLE
624 .with_label_values(&[step])
625 .start_timer();
626
627 match state {
628 AlterTableState::Prepare => self.on_prepare().await,
629 AlterTableState::SubmitAlterRegionRequests => {
630 self.submit_alter_region_requests(ctx.procedure_id, ctx.provider.as_ref())
631 .await
632 }
633 AlterTableState::UpdateMetadata => self.on_update_metadata().await,
634 AlterTableState::InvalidateTableCache => self.on_broadcast().await,
635 }
636 .map_err(map_to_procedure_error)
637 }
638
639 fn dump(&self) -> ProcedureResult<String> {
640 serde_json::to_string(&self.data).context(ToJsonSnafu)
641 }
642
643 fn lock_key(&self) -> LockKey {
644 let key = self.lock_key_inner();
645
646 LockKey::new(key)
647 }
648
649 fn poison_keys(&self) -> PoisonKeys {
650 PoisonKeys::new(vec![self.table_poison_key()])
651 }
652
653 fn event(&self, ctx: &EventContext<'_>) -> Option<Box<dyn common_event_recorder::Event>> {
654 if !ctx
655 .event_type_filter
656 .allows(TableDdlEventType::AlterTable.as_str())
657 {
658 return None;
659 }
660 let table_ref = self.data.table_ref();
661 let locator = TableDdlLocator::new(table_ref.catalog, table_ref.schema, table_ref.table)
662 .with_table_id(self.data.table_id());
663 let event = match &ctx.trigger {
664 EventTrigger::Submitted => {
665 let kind = self
666 .data
667 .task
668 .alter_table
669 .kind
670 .as_ref()
671 .and_then(alter_table_kind_name);
672 TableDdlEvent::alter_table_submitted(locator, kind)
673 }
674 _ => TableDdlEvent::lifecycle(TableDdlEventType::AlterTable, [locator]),
675 };
676
677 Some(Box::new(event))
678 }
679}
680
681#[derive(Debug, Serialize, Deserialize, AsRefStr)]
682enum AlterTableState {
683 Prepare,
685 SubmitAlterRegionRequests,
687 UpdateMetadata,
689 InvalidateTableCache,
691}
692
693#[derive(Debug, Serialize, Deserialize)]
695pub struct AlterTableData {
696 state: AlterTableState,
697 task: AlterTableTask,
698 table_id: TableId,
699 #[serde(default)]
700 column_metadatas: Vec<ColumnMetadata>,
701 table_info_value: Option<DeserializedValueWithBytes<TableInfoValue>>,
703 region_distribution: Option<RegionDistribution>,
705 #[serde(default)]
707 region_locks: Vec<RegionId>,
708}
709
710impl AlterTableData {
711 pub fn new(task: AlterTableTask, table_id: TableId, region_locks: Vec<RegionId>) -> Self {
712 Self {
713 state: AlterTableState::Prepare,
714 task,
715 table_id,
716 column_metadatas: vec![],
717 table_info_value: None,
718 region_distribution: None,
719 region_locks,
720 }
721 }
722
723 fn table_ref(&self) -> TableReference<'_> {
724 self.task.table_ref()
725 }
726
727 fn table_id(&self) -> TableId {
728 self.table_id
729 }
730
731 fn table_info(&self) -> Option<&TableInfo> {
732 self.table_info_value
733 .as_ref()
734 .map(|value| &value.table_info)
735 }
736
737 fn flow(&self) -> Result<AlterTableFlow> {
738 AlterTableFlow::from_kind(self.task.alter_table.kind.as_ref().unwrap())
740 }
741
742 #[cfg(test)]
743 pub(crate) fn column_metadatas(&self) -> &[ColumnMetadata] {
744 &self.column_metadatas
745 }
746
747 #[cfg(test)]
748 pub(crate) fn set_column_metadatas(&mut self, column_metadatas: Vec<ColumnMetadata>) {
749 self.column_metadatas = column_metadatas;
750 }
751}