1use std::collections::{BTreeMap, HashMap, HashSet};
16use std::num::NonZeroUsize;
17
18use anyhow::anyhow;
19use indexmap::IndexMap;
20use itertools::Itertools;
21use risingwave_common::catalog::{
22 ColumnCatalog, FragmentTypeFlag, FragmentTypeMask, ICEBERG_SINK_PREFIX, ICEBERG_SOURCE_PREFIX,
23 RISINGWAVE_ICEBERG_ROW_ID, ROW_ID_COLUMN_NAME, max_column_id,
24};
25use risingwave_common::config::DefaultParallelism;
26use risingwave_common::hash::VnodeCountCompat;
27use risingwave_common::id::JobId;
28use risingwave_common::secret::LocalSecretManager;
29use risingwave_common::system_param::AdaptiveParallelismStrategy;
30use risingwave_common::system_param::adaptive_parallelism_strategy::parse_strategy;
31use risingwave_common::util::iter_util::ZipEqDebug;
32use risingwave_common::util::stream_graph_visitor::{
33 visit_stream_node_body, visit_stream_node_mut, visit_stream_node_stream_scan,
34};
35use risingwave_common::{bail, current_cluster_version};
36use risingwave_connector::allow_alter_on_fly_fields::check_sink_allow_alter_on_fly_fields;
37use risingwave_connector::connector_common::validate_connection;
38use risingwave_connector::error::ConnectorError;
39use risingwave_connector::sink::file_sink::fs::FsSink;
40use risingwave_connector::sink::{CONNECTOR_TYPE_KEY, SinkError};
41use risingwave_connector::source::{
42 ConnectorProperties, UPSTREAM_SOURCE_KEY, pb_connection_type_to_connection_type,
43};
44use risingwave_connector::{WithOptionsSecResolved, WithPropertiesExt, match_sink_name_str};
45use risingwave_meta_model::object::ObjectType;
46use risingwave_meta_model::prelude::{StreamingJob as StreamingJobModel, *};
47use risingwave_meta_model::refresh_job::RefreshState;
48use risingwave_meta_model::streaming_job::BackfillOrders;
49use risingwave_meta_model::user_privilege::Action;
50use risingwave_meta_model::*;
51use risingwave_pb::catalog::table::PbEngine;
52use risingwave_pb::catalog::{PbConnection, PbCreateType, PbTable};
53use risingwave_pb::common::ThrottleType;
54use risingwave_pb::ddl_service::streaming_job_resource_type;
55use risingwave_pb::meta::alter_connector_props_request::AlterIcebergTableIds;
56use risingwave_pb::meta::list_rate_limits_response::RateLimitInfo;
57use risingwave_pb::meta::object::PbObjectInfo;
58use risingwave_pb::meta::subscribe_response::{
59 Info as NotificationInfo, Operation as NotificationOperation, Operation,
60};
61use risingwave_pb::meta::{ObjectDependency as PbObjectDependency, PbObject, PbObjectGroup};
62use risingwave_pb::plan_common::PbColumnCatalog;
63use risingwave_pb::plan_common::source_refresh_mode::{
64 RefreshMode, SourceRefreshModeFullReload, SourceRefreshModeStreaming,
65};
66use risingwave_pb::secret::PbSecretRef;
67use risingwave_pb::stream_plan::stream_fragment_graph::Parallelism;
68use risingwave_pb::stream_plan::stream_node::PbNodeBody;
69use risingwave_pb::stream_plan::{PbSinkLogStoreType, PbStreamNode, StreamScanType};
70use risingwave_pb::user::PbUserInfo;
71use risingwave_sqlparser::ast::{Engine, SqlOption, Statement};
72use risingwave_sqlparser::parser::{Parser, ParserError};
73use sea_orm::ActiveValue::Set;
74use sea_orm::sea_query::{Expr, Query, SimpleExpr};
75use sea_orm::{
76 ActiveModelTrait, ColumnTrait, DatabaseTransaction, EntityTrait, IntoActiveModel, JoinType,
77 NotSet, PaginatorTrait, QueryFilter, QuerySelect, RelationTrait, TransactionTrait,
78};
79use thiserror_ext::AsReport;
80
81use super::rename::IndexItemRewriter;
82use crate::barrier::Command;
83use crate::controller::ObjectModel;
84use crate::controller::catalog::{CatalogController, DropTableConnectorContext};
85use crate::controller::fragment::FragmentTypeMaskExt;
86use crate::controller::utils::{
87 PartialObject, build_object_group_for_delete, check_if_belongs_to_iceberg_table,
88 check_relation_name_duplicate, check_sink_into_table_cycle, ensure_job_not_canceled,
89 ensure_object_id, ensure_user_id, fetch_target_fragments, get_internal_tables_by_id,
90 get_table_columns, grant_default_privileges_automatically, insert_fragment_relations,
91 list_object_dependencies_by_object_id, list_user_info_by_ids,
92 try_get_iceberg_table_by_downstream_sink,
93};
94use crate::error::MetaErrorInner;
95use crate::manager::{NotificationVersion, StreamingJob, StreamingJobType};
96use crate::model::{
97 FragmentDownstreamRelation, FragmentReplaceUpstream, StreamContext, StreamJobFragments,
98 StreamJobFragmentsToCreate,
99};
100use crate::stream::SplitAssignment;
101use crate::{MetaError, MetaResult};
102
103pub struct AbortCreatingJobResult {
105 pub aborted: bool,
108 pub database_id: Option<DatabaseId>,
110 pub aborted_sink_id: Option<SinkId>,
113}
114
115fn serverless_backfill_resource_group_placeholder(job_id: JobId) -> String {
116 format!("SERVERLESS_BACKFILL_RESOURCE_GROUP_TBD_FOR_{job_id}")
117}
118
119fn job_initial_catalog_resource_group(
120 resource_type: &streaming_job_resource_type::ResourceType,
121 job_id: JobId,
122) -> Option<String> {
123 match resource_type {
124 streaming_job_resource_type::ResourceType::Regular(_)
125 | streaming_job_resource_type::ResourceType::ServerlessBackfill(false) => None,
126 streaming_job_resource_type::ResourceType::SpecificResourceGroup(group) => {
127 Some(group.clone())
128 }
129 streaming_job_resource_type::ResourceType::ServerlessBackfill(true) => {
130 Some(serverless_backfill_resource_group_placeholder(job_id))
131 }
132 }
133}
134
135#[derive(Debug)]
140struct DependentSourceFragmentUpdate {
141 job_ids: Vec<JobId>,
142 with_properties: BTreeMap<String, String>,
143 secret_refs: BTreeMap<String, PbSecretRef>,
144 is_shared_source: bool,
145}
146
147#[derive(Debug)]
148struct ReplaceOriginalJobInfo {
149 max_parallelism: i32,
150 timezone: Option<String>,
151 config_override: Option<String>,
152 adaptive_parallelism_strategy: Option<String>,
153 parallelism: StreamingParallelism,
154 specific_resource_group: Option<String>,
155}
156
157impl ReplaceOriginalJobInfo {
158 fn resolved_parallelism(
159 &self,
160 specified_parallelism: Option<&NonZeroUsize>,
161 ) -> StreamingParallelism {
162 specified_parallelism
163 .map(|n| StreamingParallelism::Fixed(n.get() as _))
164 .unwrap_or_else(|| self.parallelism.clone())
165 }
166
167 fn stream_context(&self, ctx_override: Option<&StreamContext>) -> StreamContext {
168 StreamContext {
169 timezone: ctx_override
170 .and_then(|ctx| ctx.timezone.clone())
171 .or_else(|| self.timezone.clone()),
172 config_override: self.config_override.clone().unwrap_or_default().into(),
175 }
176 }
177
178 fn resource_type(&self) -> streaming_job_resource_type::ResourceType {
179 match &self.specific_resource_group {
180 Some(group) => {
181 streaming_job_resource_type::ResourceType::SpecificResourceGroup(group.clone())
182 }
183 None => streaming_job_resource_type::ResourceType::Regular(true),
184 }
185 }
186}
187
188impl From<streaming_job::Model> for ReplaceOriginalJobInfo {
189 fn from(model: streaming_job::Model) -> Self {
190 Self {
191 max_parallelism: model.max_parallelism,
192 timezone: model.timezone,
193 config_override: model.config_override,
194 adaptive_parallelism_strategy: model.adaptive_parallelism_strategy,
195 parallelism: model.parallelism,
196 specific_resource_group: model.specific_resource_group,
197 }
198 }
199}
200
201fn update_sink_node_rate_limit(node: &mut PbNodeBody, rate_limit: Option<u32>) -> MetaResult<bool> {
202 let PbNodeBody::Sink(node) = node else {
203 return Ok(false);
204 };
205 if node.log_store_type != PbSinkLogStoreType::KvLogStore as i32 {
206 return Err(MetaError::invalid_parameter(
207 "sink rate limit is only supported for kv log store, please SET sink_decouple = TRUE before CREATE SINK",
208 ));
209 }
210 node.rate_limit = rate_limit;
211 Ok(true)
212}
213
214impl CatalogController {
215 pub async fn get_pinned_snapshot_epochs(&self) -> MetaResult<HashMap<TableId, HashSet<u64>>> {
216 let inner = self.inner.read().await;
219 let creating_job_ids = StreamingJobModel::find()
220 .select_only()
221 .column(streaming_job::Column::JobId)
222 .filter(streaming_job::Column::JobStatus.eq(JobStatus::Creating))
223 .into_tuple::<JobId>()
224 .all(&inner.db)
225 .await?;
226 if creating_job_ids.is_empty() {
227 return Ok(HashMap::new());
228 }
229 let fragments = Fragment::find()
230 .filter(fragment::Column::JobId.is_in(creating_job_ids))
231 .all(&inner.db)
232 .await?;
233 let mut pinned_snapshot_epochs: HashMap<TableId, HashSet<u64>> = HashMap::new();
234 for fragment in fragments {
235 visit_stream_node_stream_scan(&fragment.stream_node.to_protobuf(), |stream_scan| {
236 let scan_type = match StreamScanType::try_from(stream_scan.stream_scan_type) {
237 Ok(scan_type) => scan_type,
238 Err(err) => {
239 tracing::warn!(
240 job_id = %fragment.job_id,
241 fragment_id = %fragment.fragment_id,
242 stream_scan_type = stream_scan.stream_scan_type,
243 error = %err.as_report(),
244 "invalid persisted stream scan type, skip collecting snapshot pin"
245 );
246 return;
247 }
248 };
249 if !matches!(
250 scan_type,
251 StreamScanType::SnapshotBackfill | StreamScanType::CrossDbSnapshotBackfill
252 ) {
253 return;
254 }
255 let Some(epoch) = stream_scan.snapshot_backfill_epoch else {
256 tracing::warn!(
257 job_id = %fragment.job_id,
258 fragment_id = %fragment.fragment_id,
259 table_id = %stream_scan.table_id,
260 "persisted snapshot backfill epoch is not set, skip collecting snapshot pin"
261 );
262 return;
263 };
264 pinned_snapshot_epochs
265 .entry(stream_scan.table_id)
266 .or_default()
267 .insert(epoch);
268 });
269 }
270 Ok(pinned_snapshot_epochs)
271 }
272
273 #[expect(clippy::too_many_arguments)]
274 pub async fn create_streaming_job_obj(
275 txn: &DatabaseTransaction,
276 obj_type: ObjectType,
277 owner_id: UserId,
278 database_id: Option<DatabaseId>,
279 schema_id: Option<SchemaId>,
280 create_type: PbCreateType,
281 ctx: StreamContext,
282 adaptive_parallelism_strategy: Option<AdaptiveParallelismStrategy>,
283 streaming_parallelism: StreamingParallelism,
284 max_parallelism: usize,
285 resource_type: streaming_job_resource_type::ResourceType,
286 backfill_parallelism: Option<StreamingParallelism>,
287 backfill_adaptive_parallelism_strategy: Option<AdaptiveParallelismStrategy>,
288 refresh_interval_sec: Option<u64>,
289 ) -> MetaResult<streaming_job::Model> {
290 let obj = Self::create_object(txn, obj_type, owner_id, database_id, schema_id).await?;
291 let job_id = obj.oid.as_job_id();
292 let is_serverless_backfill = matches!(
293 &resource_type,
294 streaming_job_resource_type::ResourceType::ServerlessBackfill(true)
295 );
296 let model = streaming_job::Model {
297 job_id,
298 job_status: JobStatus::Initial,
299 create_type: create_type.into(),
300 timezone: ctx.timezone,
301 config_override: Some(ctx.config_override.to_string()),
302 adaptive_parallelism_strategy: adaptive_parallelism_strategy
303 .as_ref()
304 .map(ToString::to_string),
305 parallelism: streaming_parallelism,
306 backfill_parallelism,
307 backfill_adaptive_parallelism_strategy: backfill_adaptive_parallelism_strategy
308 .as_ref()
309 .map(ToString::to_string),
310 backfill_orders: None,
311 max_parallelism: max_parallelism as _,
312 specific_resource_group: job_initial_catalog_resource_group(&resource_type, job_id),
313 is_serverless_backfill,
314 refresh_interval_sec: refresh_interval_sec.map(|s| s as i64),
315 };
316 let job = model.clone().into_active_model();
317 StreamingJobModel::insert(job).exec(txn).await?;
318
319 Ok(model)
320 }
321
322 #[expect(clippy::too_many_arguments)]
328 #[await_tree::instrument]
329 pub async fn create_job_catalog(
330 &self,
331 streaming_job: &mut StreamingJob,
332 ctx: &StreamContext,
333 parallelism: &Option<Parallelism>,
334 max_parallelism: usize,
335 mut dependencies: HashSet<ObjectId>,
336 resource_type: streaming_job_resource_type::ResourceType,
337 backfill_parallelism: &Option<Parallelism>,
338 adaptive_parallelism_strategy: Option<AdaptiveParallelismStrategy>,
339 backfill_adaptive_parallelism_strategy: Option<AdaptiveParallelismStrategy>,
340 replace_sink: Option<&SinkId>,
341 refresh_interval_sec: Option<u64>,
342 ) -> MetaResult<streaming_job::Model> {
343 let inner = self.inner.write().await;
344 let txn = inner.db.begin().await?;
345 let create_type = streaming_job.create_type();
346
347 let streaming_parallelism = match (parallelism, self.env.opts.default_parallelism) {
348 (None, DefaultParallelism::Full) => StreamingParallelism::Adaptive,
349 (None, DefaultParallelism::Default(n)) => StreamingParallelism::Fixed(n.get()),
350 (Some(n), _) => StreamingParallelism::Fixed(n.parallelism as _),
351 };
352 let backfill_parallelism = backfill_parallelism
353 .as_ref()
354 .map(|p| StreamingParallelism::Fixed(p.parallelism as _))
355 .or_else(|| {
356 backfill_adaptive_parallelism_strategy
357 .as_ref()
358 .map(|_| StreamingParallelism::Adaptive)
359 });
360
361 ensure_user_id(streaming_job.owner() as _, &txn).await?;
362 ensure_object_id(ObjectType::Database, streaming_job.database_id(), &txn).await?;
363 ensure_object_id(ObjectType::Schema, streaming_job.schema_id(), &txn).await?;
364 if let Some(old_sink_id) = replace_sink {
365 let StreamingJob::Sink(sink) = streaming_job else {
366 bail!("replacement sink catalog requires a sink job")
367 };
368 let (old_sink, old_object) = Sink::find_by_id(*old_sink_id)
369 .find_also_related(Object)
370 .one(&txn)
371 .await?
372 .and_then(|(sink, object)| object.map(|object| (sink, object)))
373 .ok_or_else(|| MetaError::catalog_id_not_found("sink", *old_sink_id))?;
374 let old_streaming_job = StreamingJobModel::find_by_id(old_sink_id.as_job_id())
375 .one(&txn)
376 .await?
377 .ok_or_else(|| MetaError::catalog_id_not_found("sink", *old_sink_id))?;
378 if old_object.obj_type != ObjectType::Sink
379 || old_object.database_id != Some(sink.database_id)
380 || old_object.schema_id != Some(sink.schema_id)
381 || old_sink.name != sink.name
382 {
383 bail!(
384 "old sink {} does not match replacement sink {}",
385 old_sink_id,
386 sink.name
387 );
388 }
389 if old_sink.target_table.is_some() || sink.target_table.is_some() {
390 bail!("replace sink into table is not supported");
391 }
392 if old_streaming_job.job_status != JobStatus::Created {
393 bail!("sink {} is not ready to be replaced", old_sink_id);
394 }
395 } else {
396 check_relation_name_duplicate(
397 &streaming_job.name(),
398 streaming_job.database_id(),
399 streaming_job.schema_id(),
400 &txn,
401 )
402 .await?;
403 }
404
405 if !dependencies.is_empty() {
407 let altering_cnt = ObjectDependency::find()
408 .join(
409 JoinType::InnerJoin,
410 object_dependency::Relation::Object1.def(),
411 )
412 .join(JoinType::InnerJoin, object::Relation::StreamingJob.def())
413 .filter(
414 object_dependency::Column::Oid
415 .is_in(dependencies.clone())
416 .and(object::Column::ObjType.eq(ObjectType::Table))
417 .and(streaming_job::Column::JobStatus.ne(JobStatus::Created))
418 .and(
419 object::Column::Oid.not_in_subquery(
421 Query::select()
422 .column(table::Column::TableId)
423 .from(Table)
424 .to_owned(),
425 ),
426 ),
427 )
428 .count(&txn)
429 .await?;
430 if altering_cnt != 0 {
431 return Err(MetaError::permission_denied(
432 "some dependent relations are being altered",
433 ));
434 }
435
436 let batch_refresh_cnt = StreamingJobModel::find()
439 .filter(
440 streaming_job::Column::JobId
441 .is_in(dependencies.iter().map(|id| JobId::new(id.as_raw_id())))
442 .and(streaming_job::Column::RefreshIntervalSec.is_not_null()),
443 )
444 .count(&txn)
445 .await?;
446 if batch_refresh_cnt != 0 {
447 return Err(MetaError::permission_denied(
448 "creating streaming jobs on batch refresh materialized views is not supported",
449 ));
450 }
451 }
452
453 let streaming_job_model = match streaming_job {
454 StreamingJob::MaterializedView(table) => {
455 let streaming_job_model = Self::create_streaming_job_obj(
456 &txn,
457 ObjectType::Table,
458 table.owner as _,
459 Some(table.database_id),
460 Some(table.schema_id),
461 create_type,
462 ctx.clone(),
463 adaptive_parallelism_strategy,
464 streaming_parallelism,
465 max_parallelism,
466 resource_type.clone(),
467 backfill_parallelism.clone(),
468 backfill_adaptive_parallelism_strategy,
469 refresh_interval_sec,
470 )
471 .await?;
472 table.id = streaming_job_model.job_id.as_mv_table_id();
473 let table_model: table::ActiveModel = table.clone().into();
474 Table::insert(table_model).exec(&txn).await?;
475 streaming_job_model
476 }
477 StreamingJob::Sink(sink) => {
478 if let Some(target_table_id) = sink.target_table
479 && check_sink_into_table_cycle(
480 target_table_id.into(),
481 dependencies.iter().cloned().collect(),
482 &txn,
483 )
484 .await?
485 {
486 bail!("Creating such a sink will result in circular dependency.");
487 }
488
489 let streaming_job_model = Self::create_streaming_job_obj(
490 &txn,
491 ObjectType::Sink,
492 sink.owner as _,
493 Some(sink.database_id),
494 Some(sink.schema_id),
495 create_type,
496 ctx.clone(),
497 adaptive_parallelism_strategy,
498 streaming_parallelism,
499 max_parallelism,
500 resource_type.clone(),
501 backfill_parallelism.clone(),
502 backfill_adaptive_parallelism_strategy,
503 None, )
505 .await?;
506 sink.id = streaming_job_model.job_id.as_sink_id();
507 if let Some(old_sink_id) = replace_sink {
508 let final_sink_name = sink.name.clone();
509 sink.name = format!("__rw_replacing_sink_{}_{}", old_sink_id, sink.id);
513 tracing::debug!(
514 old_sink_id = %old_sink_id,
515 new_sink_id = %sink.id,
516 final_name = %final_sink_name,
517 temp_name = %sink.name,
518 "created replacement sink catalog with temporary name"
519 );
520 }
521 let sink_model: sink::ActiveModel = sink.clone().into();
522 Sink::insert(sink_model).exec(&txn).await?;
523 streaming_job_model
524 }
525 StreamingJob::Table(src, table, _) => {
526 let streaming_job_model = Self::create_streaming_job_obj(
527 &txn,
528 ObjectType::Table,
529 table.owner as _,
530 Some(table.database_id),
531 Some(table.schema_id),
532 create_type,
533 ctx.clone(),
534 adaptive_parallelism_strategy,
535 streaming_parallelism,
536 max_parallelism,
537 resource_type.clone(),
538 backfill_parallelism.clone(),
539 backfill_adaptive_parallelism_strategy,
540 None, )
542 .await?;
543 let job_id = streaming_job_model.job_id;
544 table.id = job_id.as_mv_table_id();
545 if let Some(src) = src {
546 let src_obj = Self::create_object(
547 &txn,
548 ObjectType::Source,
549 src.owner as _,
550 Some(src.database_id),
551 Some(src.schema_id),
552 )
553 .await?;
554 src.id = src_obj.oid.as_source_id();
555 src.optional_associated_table_id = Some(job_id.as_mv_table_id().into());
556 table.optional_associated_source_id = Some(src_obj.oid.as_source_id().into());
557 let source: source::ActiveModel = src.clone().into();
558 Source::insert(source).exec(&txn).await?;
559 }
560 let table_model: table::ActiveModel = table.clone().into();
561 Table::insert(table_model).exec(&txn).await?;
562
563 if table.refreshable {
564 let trigger_interval_secs = src
565 .as_ref()
566 .and_then(|source_catalog| source_catalog.refresh_mode)
567 .and_then(
568 |source_refresh_mode| match source_refresh_mode.refresh_mode {
569 Some(RefreshMode::FullReload(SourceRefreshModeFullReload {
570 refresh_interval_sec,
571 })) => refresh_interval_sec,
572 Some(RefreshMode::Streaming(SourceRefreshModeStreaming {})) => None,
573 None => None,
574 },
575 );
576
577 RefreshJob::insert(refresh_job::ActiveModel {
578 table_id: Set(table.id),
579 last_trigger_time: Set(None),
580 trigger_interval_secs: Set(trigger_interval_secs),
581 current_status: Set(RefreshState::Idle),
582 last_success_time: Set(None),
583 })
584 .exec(&txn)
585 .await?;
586 }
587 streaming_job_model
588 }
589 StreamingJob::Index(index, table) => {
590 ensure_object_id(ObjectType::Table, index.primary_table_id, &txn).await?;
591 let streaming_job_model = Self::create_streaming_job_obj(
592 &txn,
593 ObjectType::Index,
594 index.owner as _,
595 Some(index.database_id),
596 Some(index.schema_id),
597 create_type,
598 ctx.clone(),
599 adaptive_parallelism_strategy,
600 streaming_parallelism,
601 max_parallelism,
602 resource_type.clone(),
603 backfill_parallelism.clone(),
604 backfill_adaptive_parallelism_strategy,
605 None, )
607 .await?;
608 let job_id = streaming_job_model.job_id;
610 index.id = job_id.as_index_id();
611 index.index_table_id = job_id.as_mv_table_id();
612 table.id = job_id.as_mv_table_id();
613
614 ObjectDependency::insert(object_dependency::ActiveModel {
615 oid: Set(index.primary_table_id.into()),
616 used_by: Set(table.id.into()),
617 ..Default::default()
618 })
619 .exec(&txn)
620 .await?;
621
622 let table_model: table::ActiveModel = table.clone().into();
623 Table::insert(table_model).exec(&txn).await?;
624 let index_model: index::ActiveModel = index.clone().into();
625 Index::insert(index_model).exec(&txn).await?;
626 streaming_job_model
627 }
628 StreamingJob::Source(src) => {
629 let streaming_job_model = Self::create_streaming_job_obj(
630 &txn,
631 ObjectType::Source,
632 src.owner as _,
633 Some(src.database_id),
634 Some(src.schema_id),
635 create_type,
636 ctx.clone(),
637 adaptive_parallelism_strategy,
638 streaming_parallelism,
639 max_parallelism,
640 resource_type.clone(),
641 backfill_parallelism.clone(),
642 backfill_adaptive_parallelism_strategy,
643 None, )
645 .await?;
646 src.id = streaming_job_model.job_id.as_shared_source_id();
647 let source_model: source::ActiveModel = src.clone().into();
648 Source::insert(source_model).exec(&txn).await?;
649 streaming_job_model
650 }
651 };
652
653 dependencies.extend(
655 streaming_job
656 .dependent_secret_ids()?
657 .into_iter()
658 .map(|id| id.as_object_id()),
659 );
660 dependencies.extend(
662 streaming_job
663 .dependent_connection_ids()?
664 .into_iter()
665 .map(|id| id.as_object_id()),
666 );
667
668 if !dependencies.is_empty() {
670 ObjectDependency::insert_many(dependencies.into_iter().map(|oid| {
671 object_dependency::ActiveModel {
672 oid: Set(oid),
673 used_by: Set(streaming_job.id().as_object_id()),
674 ..Default::default()
675 }
676 }))
677 .exec(&txn)
678 .await?;
679 }
680
681 txn.commit().await?;
682
683 Ok(streaming_job_model)
684 }
685
686 pub async fn create_internal_table_catalog(
694 &self,
695 job: &StreamingJob,
696 mut incomplete_internal_tables: Vec<PbTable>,
697 ) -> MetaResult<HashMap<TableId, TableId>> {
698 let job_id = job.id();
699 let inner = self.inner.write().await;
700 let txn = inner.db.begin().await?;
701
702 ensure_job_not_canceled(job_id, &txn).await?;
704
705 let mut table_id_map = HashMap::new();
706 for table in &mut incomplete_internal_tables {
707 let table_id = Self::create_object(
708 &txn,
709 ObjectType::Table,
710 table.owner as _,
711 Some(table.database_id),
712 Some(table.schema_id),
713 )
714 .await?
715 .oid
716 .as_table_id();
717 table_id_map.insert(table.id, table_id);
718 table.id = table_id;
719 table.job_id = Some(job_id);
720
721 let table_model = table::ActiveModel {
722 table_id: Set(table_id),
723 belongs_to_job_id: Set(Some(job_id)),
724 fragment_id: NotSet,
725 ..table.clone().into()
726 };
727 Table::insert(table_model).exec(&txn).await?;
728 }
729 txn.commit().await?;
730
731 Ok(table_id_map)
732 }
733
734 pub async fn update_streaming_job_resource_group(
735 &self,
736 job_id: JobId,
737 resource_group: String,
738 ) -> MetaResult<()> {
739 let inner = self.inner.write().await;
740 let txn = inner.db.begin().await?;
741
742 ensure_job_not_canceled(job_id, &txn).await?;
743
744 let job = streaming_job::ActiveModel {
745 job_id: Set(job_id),
746 specific_resource_group: Set(Some(resource_group)),
747 ..Default::default()
748 };
749 StreamingJobModel::update(job).exec(&txn).await?;
750
751 txn.commit().await?;
752
753 Ok(())
754 }
755
756 pub async fn prepare_stream_job_fragments(
757 &self,
758 stream_job_fragments: &StreamJobFragmentsToCreate,
759 streaming_job: &StreamingJob,
760 for_replace: bool,
761 backfill_orders: Option<BackfillOrders>,
762 ) -> MetaResult<()> {
763 self.prepare_streaming_job(
764 stream_job_fragments.stream_job_id(),
765 || stream_job_fragments.fragments.values(),
766 &stream_job_fragments.downstreams,
767 for_replace,
768 Some(streaming_job),
769 backfill_orders,
770 )
771 .await
772 }
773
774 #[await_tree::instrument("prepare_streaming_job_for_{}", if for_replace { "replace" } else { "create" }
780 )]
781 pub async fn prepare_streaming_job<'a, I: Iterator<Item = &'a crate::model::Fragment> + 'a>(
782 &self,
783 job_id: JobId,
784 get_fragments: impl Fn() -> I + 'a,
785 downstreams: &FragmentDownstreamRelation,
786 for_replace: bool,
787 creating_streaming_job: Option<&'a StreamingJob>,
788 backfill_orders: Option<BackfillOrders>,
789 ) -> MetaResult<()> {
790 let fragments = Self::prepare_fragment_models_from_fragments(job_id, get_fragments())?;
791
792 let inner = self.inner.write().await;
793
794 let txn = inner.db.begin().await?;
795
796 ensure_job_not_canceled(job_id, &txn).await?;
798
799 if let Some(backfill_orders) = backfill_orders {
800 let job = streaming_job::ActiveModel {
801 job_id: Set(job_id),
802 backfill_orders: Set(Some(backfill_orders)),
803 ..Default::default()
804 };
805 StreamingJobModel::update(job).exec(&txn).await?;
806 }
807
808 let state_table_ids = fragments
809 .iter()
810 .flat_map(|fragment| fragment.state_table_ids.inner_ref().clone())
811 .collect_vec();
812
813 let inserted_fragment_ids: Vec<crate::model::FragmentId> = fragments
815 .iter()
816 .map(|f| f.fragment_id as crate::model::FragmentId)
817 .collect();
818
819 if !fragments.is_empty() {
820 let fragment_models = fragments
821 .into_iter()
822 .map(|fragment| fragment.into_active_model())
823 .collect_vec();
824 Fragment::insert_many(fragment_models).exec(&txn).await?;
825 }
826
827 if !for_replace {
830 let all_tables = StreamJobFragments::collect_tables(get_fragments());
831 for state_table_id in state_table_ids {
832 let table = all_tables
836 .get(&state_table_id)
837 .unwrap_or_else(|| panic!("table {} not found", state_table_id));
838 assert_eq!(table.id, state_table_id);
839 let vnode_count = table.vnode_count();
840
841 Table::update(table::ActiveModel {
842 table_id: Set(state_table_id as _),
843 fragment_id: Set(Some(table.fragment_id)),
844 vnode_count: Set(vnode_count as _),
845 ..Default::default()
846 })
847 .exec(&txn)
848 .await?;
849 }
850 }
851
852 insert_fragment_relations(&txn, downstreams).await?;
853
854 if !for_replace {
855 if let Some(StreamingJob::Table(_, table, _)) = creating_streaming_job {
857 Table::update(table::ActiveModel {
858 table_id: Set(table.id),
859 dml_fragment_id: Set(table.dml_fragment_id),
860 ..Default::default()
861 })
862 .exec(&txn)
863 .await?;
864 }
865 }
866
867 txn.commit().await?;
868
869 self.env
873 .notification_manager()
874 .notify_serving_fragment_mapping_update(inserted_fragment_ids);
875
876 Ok(())
877 }
878
879 pub async fn build_cancel_command(
883 &self,
884 job_id: JobId,
885 ) -> MetaResult<Option<(Command, Vec<TableId>)>> {
886 let inner = self.inner.read().await;
887 let txn = inner.db.begin().await?;
888
889 let Some(streaming_job) = StreamingJobModel::find_by_id(job_id).one(&txn).await? else {
890 tracing::warn!(
891 %job_id,
892 "streaming job not found when building cancel command, might be cancelled already"
893 );
894 return Ok(None);
895 };
896 if streaming_job.job_status == JobStatus::Created {
897 tracing::warn!(
898 "streaming job {} is already created, ignore cancel request",
899 job_id
900 );
901 return Ok(None);
902 }
903
904 let fragments = Fragment::find()
905 .filter(fragment::Column::JobId.eq(job_id))
906 .all(&txn)
907 .await?;
908
909 let state_table_ids = fragments
910 .iter()
911 .flat_map(|fragment| fragment.state_table_ids.inner_ref().iter().copied())
912 .collect_vec();
913 let sink_fragment_ids = fragments
914 .iter()
915 .filter_map(|fragment| {
916 FragmentTypeMask::from(fragment.fragment_type_mask)
917 .contains(FragmentTypeFlag::Sink)
918 .then_some(fragment.fragment_id)
919 })
920 .collect_vec();
921
922 let sink_target_fragments = fetch_target_fragments(&txn, sink_fragment_ids).await?;
923 let dropped_sink_fragment_by_targets = sink_target_fragments
924 .into_iter()
925 .filter_map(|(sink_fragment, target_fragments)| {
926 target_fragments
927 .first()
928 .map(|target_fragment| (*target_fragment, vec![sink_fragment]))
929 })
930 .collect();
931
932 Ok(Some((
933 Command::DropStreamingJobs {
934 streaming_job_ids: HashSet::from_iter([job_id]),
935 unregistered_state_table_ids: state_table_ids.iter().copied().collect(),
936 dropped_sink_fragment_by_targets,
937 },
938 state_table_ids,
939 )))
940 }
941
942 #[await_tree::instrument]
944 pub async fn try_abort_creating_streaming_job(
945 &self,
946 mut job_id: JobId,
947 is_cancelled: bool,
948 ) -> MetaResult<AbortCreatingJobResult> {
949 let mut inner = self.inner.write().await;
950 let txn = inner.db.begin().await?;
951
952 let obj = Object::find_by_id(job_id).one(&txn).await?;
953 let Some(obj) = obj else {
954 tracing::warn!(
955 id = %job_id,
956 "streaming job not found when aborting creating, might be cancelled already or cleaned by recovery"
957 );
958 return Ok(AbortCreatingJobResult {
959 aborted: true,
960 database_id: None,
961 aborted_sink_id: None,
962 });
963 };
964 let database_id = obj
965 .database_id
966 .ok_or_else(|| anyhow!("obj has no database id: {:?}", obj))?;
967 let streaming_job = streaming_job::Entity::find_by_id(job_id).one(&txn).await?;
968
969 if !is_cancelled && let Some(streaming_job) = &streaming_job {
970 assert_ne!(streaming_job.job_status, JobStatus::Created);
971 if streaming_job.create_type == CreateType::Background
972 && streaming_job.job_status == JobStatus::Creating
973 {
974 if (obj.obj_type == ObjectType::Table || obj.obj_type == ObjectType::Sink)
975 && check_if_belongs_to_iceberg_table(&txn, job_id).await?
976 {
977 } else {
979 tracing::warn!(
981 id = %job_id,
982 "streaming job is created in background and still in creating status"
983 );
984 return Ok(AbortCreatingJobResult {
985 aborted: false,
986 database_id: Some(database_id),
987 aborted_sink_id: None,
988 });
989 }
990 }
991 }
992
993 let original_job_id = job_id;
995 let original_obj_type = obj.obj_type;
996
997 let iceberg_table_id =
998 try_get_iceberg_table_by_downstream_sink(&txn, job_id.as_sink_id()).await?;
999 if let Some(iceberg_table_id) = iceberg_table_id {
1000 let internal_tables = get_internal_tables_by_id(job_id, &txn).await?;
1003 Object::delete_many()
1004 .filter(
1005 object::Column::Oid
1006 .eq(job_id)
1007 .or(object::Column::Oid.is_in(internal_tables)),
1008 )
1009 .exec(&txn)
1010 .await?;
1011 job_id = iceberg_table_id.as_job_id();
1012 };
1013
1014 let internal_table_ids = get_internal_tables_by_id(job_id, &txn).await?;
1015
1016 let mut objs = vec![];
1018 let table_obj = Table::find_by_id(job_id.as_mv_table_id()).one(&txn).await?;
1019 let need_notify = streaming_job
1020 .as_ref()
1021 .is_some_and(|job| job.job_status != JobStatus::Initial);
1022
1023 if is_cancelled {
1024 let dropped_tables = Table::find()
1025 .find_also_related(Object)
1026 .filter(
1027 table::Column::TableId.is_in(
1028 internal_table_ids
1029 .iter()
1030 .cloned()
1031 .chain(table_obj.iter().map(|t| t.table_id as _)),
1032 ),
1033 )
1034 .all(&txn)
1035 .await?
1036 .into_iter()
1037 .map(|(table, obj)| PbTable::from(ObjectModel(table, obj.unwrap(), None)));
1038 inner
1039 .dropped_tables
1040 .extend(dropped_tables.map(|t| (t.id, t)));
1041 }
1042
1043 if need_notify {
1044 if original_obj_type == ObjectType::Sink && original_job_id != job_id {
1047 let orig_obj: Option<PartialObject> = Object::find_by_id(original_job_id)
1048 .select_only()
1049 .columns([
1050 object::Column::Oid,
1051 object::Column::ObjType,
1052 object::Column::SchemaId,
1053 object::Column::DatabaseId,
1054 ])
1055 .into_partial_model()
1056 .one(&txn)
1057 .await?;
1058 if let Some(orig_obj) = orig_obj {
1059 objs.push(orig_obj);
1060 }
1061 }
1062
1063 let obj: Option<PartialObject> = Object::find_by_id(job_id)
1064 .select_only()
1065 .columns([
1066 object::Column::Oid,
1067 object::Column::ObjType,
1068 object::Column::SchemaId,
1069 object::Column::DatabaseId,
1070 ])
1071 .into_partial_model()
1072 .one(&txn)
1073 .await?;
1074 let obj =
1075 obj.ok_or_else(|| MetaError::catalog_id_not_found("streaming job", job_id))?;
1076 objs.push(obj);
1077 if let Some(table) = &table_obj
1078 && let Some(source_id) = table.optional_associated_source_id
1079 && let Some(source_obj) = Object::find_by_id(source_id)
1080 .select_only()
1081 .columns([
1082 object::Column::Oid,
1083 object::Column::ObjType,
1084 object::Column::SchemaId,
1085 object::Column::DatabaseId,
1086 ])
1087 .into_partial_model()
1088 .one(&txn)
1089 .await?
1090 {
1091 objs.push(source_obj);
1092 }
1093 let internal_table_objs: Vec<PartialObject> = Object::find()
1094 .select_only()
1095 .columns([
1096 object::Column::Oid,
1097 object::Column::ObjType,
1098 object::Column::SchemaId,
1099 object::Column::DatabaseId,
1100 ])
1101 .join(JoinType::InnerJoin, object::Relation::Table.def())
1102 .filter(table::Column::BelongsToJobId.eq(job_id))
1103 .into_partial_model()
1104 .all(&txn)
1105 .await?;
1106 objs.extend(internal_table_objs);
1107 }
1108
1109 let abort_fragment_ids: Vec<FragmentId> = Fragment::find()
1111 .select_only()
1112 .column(fragment::Column::FragmentId)
1113 .filter(fragment::Column::JobId.eq(job_id))
1114 .into_tuple()
1115 .all(&txn)
1116 .await?;
1117
1118 Object::delete_by_id(job_id).exec(&txn).await?;
1119 if !internal_table_ids.is_empty() {
1120 Object::delete_many()
1121 .filter(object::Column::Oid.is_in(internal_table_ids))
1122 .exec(&txn)
1123 .await?;
1124 }
1125 if let Some(t) = &table_obj
1126 && let Some(source_id) = t.optional_associated_source_id
1127 {
1128 Object::delete_by_id(source_id).exec(&txn).await?;
1129 }
1130
1131 let err = if is_cancelled {
1132 MetaError::cancelled(format!("streaming job {job_id} is cancelled"))
1133 } else {
1134 MetaError::catalog_id_not_found("stream job", format!("streaming job {job_id} failed"))
1135 };
1136 let abort_reason = format!("streaming job aborted {}", err.as_report());
1137 for tx in inner
1138 .creating_table_finish_notifier
1139 .get_mut(&database_id)
1140 .map(|creating_tables| creating_tables.remove(&job_id).into_iter())
1141 .into_iter()
1142 .flatten()
1143 .flatten()
1144 {
1145 let _ = tx.send(Err(abort_reason.clone()));
1146 }
1147 txn.commit().await?;
1148
1149 self.env
1151 .notification_manager()
1152 .notify_serving_fragment_mapping_delete(
1153 abort_fragment_ids.iter().map(|id| *id as _).collect(),
1154 );
1155
1156 if !objs.is_empty() {
1157 self.notify_frontend(Operation::Delete, build_object_group_for_delete(objs))
1160 .await;
1161 }
1162 let aborted_sink_id =
1163 (original_obj_type == ObjectType::Sink).then(|| original_job_id.as_sink_id());
1164 Ok(AbortCreatingJobResult {
1165 aborted: true,
1166 database_id: Some(database_id),
1167 aborted_sink_id,
1168 })
1169 }
1170
1171 async fn build_creating_streaming_job_objects(
1172 txn: &DatabaseTransaction,
1173 job_id: JobId,
1174 ) -> MetaResult<Vec<PbObject>> {
1175 let job_type = Object::find_by_id(job_id)
1176 .select_only()
1177 .column(object::Column::ObjType)
1178 .into_tuple()
1179 .one(txn)
1180 .await?
1181 .ok_or_else(|| MetaError::catalog_id_not_found("streaming job", job_id))?;
1182 let streaming_job = StreamingJobModel::find_by_id(job_id)
1183 .one(txn)
1184 .await?
1185 .ok_or_else(|| MetaError::catalog_id_not_found("streaming job", job_id))?;
1186
1187 let table_objs = Table::find()
1188 .find_also_related(Object)
1189 .filter(
1190 table::Column::BelongsToJobId
1191 .eq(job_id)
1192 .or(table::Column::TableId.eq(job_id.as_mv_table_id())),
1193 )
1194 .all(txn)
1195 .await?;
1196 let associated_source_id = table_objs
1197 .iter()
1198 .find(|(table, _)| table.table_id == job_id.as_mv_table_id())
1199 .and_then(|(table, _)| table.optional_associated_source_id);
1200 let mut objects = table_objs
1201 .into_iter()
1202 .map(|(table, obj)| PbObject {
1203 object_info: Some(PbObjectInfo::Table(
1204 ObjectModel(table, obj.unwrap(), Some(streaming_job.clone())).into(),
1205 )),
1206 })
1207 .collect_vec();
1208
1209 match job_type {
1210 ObjectType::Table => {
1211 if let Some(source_id) = associated_source_id {
1212 let (source, obj) = Source::find_by_id(source_id)
1213 .find_also_related(Object)
1214 .one(txn)
1215 .await?
1216 .ok_or_else(|| MetaError::catalog_id_not_found("source", source_id))?;
1217 objects.push(PbObject {
1218 object_info: Some(PbObjectInfo::Source(
1219 ObjectModel(source, obj.unwrap(), None).into(),
1220 )),
1221 });
1222 }
1223 }
1224 ObjectType::Sink => {
1225 let (sink, obj) = Sink::find_by_id(job_id.as_sink_id())
1226 .find_also_related(Object)
1227 .one(txn)
1228 .await?
1229 .ok_or_else(|| MetaError::catalog_id_not_found("sink", job_id))?;
1230 objects.push(PbObject {
1231 object_info: Some(PbObjectInfo::Sink(
1232 ObjectModel(sink, obj.unwrap(), Some(streaming_job)).into(),
1233 )),
1234 });
1235 }
1236 ObjectType::Index => {
1237 let (index, obj) = Index::find_by_id(job_id.as_index_id())
1238 .find_also_related(Object)
1239 .one(txn)
1240 .await?
1241 .ok_or_else(|| MetaError::catalog_id_not_found("index", job_id))?;
1242 objects.push(PbObject {
1243 object_info: Some(PbObjectInfo::Index(
1244 ObjectModel(index, obj.unwrap(), Some(streaming_job)).into(),
1245 )),
1246 });
1247 }
1248 ObjectType::Source => {
1249 let (source, obj) = Source::find_by_id(job_id.as_shared_source_id())
1250 .find_also_related(Object)
1251 .one(txn)
1252 .await?
1253 .ok_or_else(|| MetaError::catalog_id_not_found("source", job_id))?;
1254 objects.push(PbObject {
1255 object_info: Some(PbObjectInfo::Source(
1256 ObjectModel(source, obj.unwrap(), None).into(),
1257 )),
1258 });
1259 }
1260 _ => unreachable!("invalid streaming job type: {job_type:?}"),
1261 }
1262
1263 Ok(objects)
1264 }
1265
1266 #[await_tree::instrument]
1269 pub async fn post_collect_job_fragments(
1270 &self,
1271 job_id: JobId,
1272 upstream_fragment_new_downstreams: &FragmentDownstreamRelation,
1273 new_sink_downstream: Option<FragmentDownstreamRelation>,
1274 split_assignment: Option<&SplitAssignment>,
1275 replace_sink: Option<&SinkId>,
1276 notify_creating: bool,
1277 ) -> MetaResult<Option<Vec<TableId>>> {
1278 let mut inner = self.inner.write().await;
1279 let txn = inner.db.begin().await?;
1280 let mut replace_sink_post_collect = None;
1281
1282 insert_fragment_relations(&txn, upstream_fragment_new_downstreams).await?;
1283
1284 if let Some(new_downstream) = new_sink_downstream {
1285 insert_fragment_relations(&txn, &new_downstream).await?;
1286 }
1287
1288 let mut streaming_job_model = streaming_job::ActiveModel {
1290 job_id: Set(job_id),
1291 job_status: Set(JobStatus::Creating),
1292 ..Default::default()
1293 };
1294 if replace_sink.is_some() {
1295 streaming_job_model.create_type = Set(CreateType::Background);
1300 }
1301 StreamingJobModel::update(streaming_job_model)
1302 .exec(&txn)
1303 .await?;
1304
1305 if let Some(split_assignment) = split_assignment {
1306 let fragment_splits = split_assignment
1307 .iter()
1308 .map(|(fragment_id, splits)| {
1309 (
1310 *fragment_id as _,
1311 splits.values().flatten().cloned().collect_vec(),
1312 )
1313 })
1314 .collect();
1315
1316 self.update_fragment_splits(&txn, &fragment_splits).await?;
1317 }
1318
1319 if let Some(old_sink_id) = replace_sink {
1320 let old_sink_id = *old_sink_id;
1321 let old_job_id = old_sink_id.as_job_id();
1322
1323 let (old_sink, old_sink_object) = Sink::find_by_id(old_sink_id)
1324 .find_also_related(Object)
1325 .one(&txn)
1326 .await?
1327 .and_then(|(sink, object)| object.map(|object| (sink, object)))
1328 .ok_or_else(|| MetaError::catalog_id_not_found("sink", old_sink_id))?;
1329 let old_sink_object = PartialObject {
1330 oid: old_sink_object.oid,
1331 obj_type: old_sink_object.obj_type,
1332 schema_id: old_sink_object.schema_id,
1333 database_id: old_sink_object.database_id,
1334 };
1335 if old_sink_object.obj_type != ObjectType::Sink {
1336 bail!("object {} is not a sink", old_sink_id);
1337 }
1338 let final_sink_name = old_sink.name.clone();
1339
1340 let old_internal_table_objs: Vec<PartialObject> = Object::find()
1341 .select_only()
1342 .columns([
1343 object::Column::Oid,
1344 object::Column::ObjType,
1345 object::Column::SchemaId,
1346 object::Column::DatabaseId,
1347 ])
1348 .join(JoinType::InnerJoin, object::Relation::Table.def())
1349 .filter(table::Column::BelongsToJobId.eq(old_job_id))
1350 .into_partial_model()
1351 .all(&txn)
1352 .await?;
1353 let old_state_table_ids = old_internal_table_objs
1354 .iter()
1355 .map(|obj| obj.oid.as_table_id())
1356 .collect_vec();
1357 let old_fragment_ids: Vec<FragmentId> = Fragment::find()
1358 .select_only()
1359 .column(fragment::Column::FragmentId)
1360 .filter(fragment::Column::JobId.eq(old_job_id))
1361 .into_tuple()
1362 .all(&txn)
1363 .await?;
1364 let dropped_tables = Table::find()
1365 .find_also_related(Object)
1366 .filter(table::Column::TableId.is_in(old_state_table_ids.iter().copied()))
1367 .all(&txn)
1368 .await?
1369 .into_iter()
1370 .map(|(table, obj)| PbTable::from(ObjectModel(table, obj.unwrap(), None)))
1371 .collect_vec();
1372
1373 Sink::update(sink::ActiveModel {
1374 sink_id: Set(job_id.as_sink_id()),
1375 name: Set(final_sink_name),
1376 ..Default::default()
1377 })
1378 .exec(&txn)
1379 .await?;
1380
1381 let old_objects = std::iter::once(old_sink_object)
1382 .chain(old_internal_table_objs)
1383 .collect_vec();
1384 let old_object_ids = old_objects.iter().map(|obj| obj.oid).collect_vec();
1385 let updated_user_ids: Vec<UserId> = UserPrivilege::find()
1386 .select_only()
1387 .distinct()
1388 .column(user_privilege::Column::UserId)
1389 .filter(user_privilege::Column::Oid.is_in(old_object_ids.clone()))
1390 .into_tuple()
1391 .all(&txn)
1392 .await?;
1393
1394 let res = Object::delete_many()
1398 .filter(object::Column::Oid.is_in(old_object_ids))
1399 .exec(&txn)
1400 .await?;
1401 if res.rows_affected == 0 {
1402 return Err(MetaError::catalog_id_not_found("sink", old_sink_id));
1403 }
1404
1405 let (new_sink, new_obj) = Sink::find_by_id(job_id.as_sink_id())
1406 .find_also_related(Object)
1407 .one(&txn)
1408 .await?
1409 .ok_or_else(|| MetaError::catalog_id_not_found("sink", job_id))?;
1410 let streaming_job = StreamingJobModel::find_by_id(job_id).one(&txn).await?;
1411 let mut new_objects = Table::find()
1412 .find_also_related(Object)
1413 .filter(table::Column::BelongsToJobId.eq(job_id))
1414 .all(&txn)
1415 .await?
1416 .into_iter()
1417 .map(|(table, obj)| PbObject {
1418 object_info: Some(PbObjectInfo::Table(
1419 ObjectModel(table, obj.unwrap(), streaming_job.clone()).into(),
1420 )),
1421 })
1422 .collect_vec();
1423 new_objects.push(PbObject {
1424 object_info: Some(PbObjectInfo::Sink(
1425 ObjectModel(new_sink, new_obj.unwrap(), streaming_job).into(),
1426 )),
1427 });
1428 let dependencies =
1429 list_object_dependencies_by_object_id(&txn, job_id.as_object_id()).await?;
1430 let updated_user_info = list_user_info_by_ids(updated_user_ids, &txn).await?;
1431
1432 replace_sink_post_collect = Some((
1433 old_state_table_ids,
1434 old_fragment_ids,
1435 old_objects,
1436 dropped_tables,
1437 new_objects,
1438 dependencies,
1439 updated_user_info,
1440 ));
1441 }
1442
1443 let creating_objects = if notify_creating {
1444 Some(Self::build_creating_streaming_job_objects(&txn, job_id).await?)
1445 } else {
1446 None
1447 };
1448
1449 txn.commit().await?;
1450
1451 if let Some(objects) = creating_objects {
1452 self.notify_frontend(
1453 NotificationOperation::Add,
1454 NotificationInfo::ObjectGroup(PbObjectGroup {
1455 objects,
1456 dependencies: vec![],
1457 }),
1458 )
1459 .await;
1460 }
1461
1462 let Some((
1463 old_state_table_ids,
1464 old_fragment_ids,
1465 old_objects,
1466 dropped_tables,
1467 new_objects,
1468 dependencies,
1469 updated_user_info,
1470 )) = replace_sink_post_collect
1471 else {
1472 return Ok(None);
1473 };
1474
1475 inner
1476 .dropped_tables
1477 .extend(dropped_tables.into_iter().map(|table| (table.id, table)));
1478 drop(inner);
1479
1480 self.env
1481 .notification_manager()
1482 .notify_serving_fragment_mapping_delete(
1483 old_fragment_ids.iter().map(|id| *id as _).collect(),
1484 );
1485
1486 let _ = self
1487 .notify_frontend(
1488 NotificationOperation::Delete,
1489 build_object_group_for_delete(old_objects),
1490 )
1491 .await;
1492
1493 let _ = self
1494 .notify_frontend(
1495 NotificationOperation::Add,
1498 NotificationInfo::ObjectGroup(PbObjectGroup {
1499 objects: new_objects,
1500 dependencies,
1501 }),
1502 )
1503 .await;
1504
1505 if !updated_user_info.is_empty() {
1506 let _ = self.notify_users_update(updated_user_info).await;
1507 }
1508
1509 Ok(Some(old_state_table_ids))
1510 }
1511
1512 pub async fn create_job_catalog_for_replace(
1513 &self,
1514 streaming_job: &StreamingJob,
1515 ctx: Option<&StreamContext>,
1516 specified_parallelism: Option<&NonZeroUsize>,
1517 expected_original_max_parallelism: Option<usize>,
1518 ) -> MetaResult<streaming_job::Model> {
1519 let id = streaming_job.id();
1520 let inner = self.inner.write().await;
1521 let txn = inner.db.begin().await?;
1522
1523 streaming_job.verify_version_for_replace(&txn).await?;
1525 let referring_cnt = ObjectDependency::find()
1527 .join(
1528 JoinType::InnerJoin,
1529 object_dependency::Relation::Object1.def(),
1530 )
1531 .join(JoinType::InnerJoin, object::Relation::StreamingJob.def())
1532 .filter(
1533 object_dependency::Column::Oid
1534 .eq(id)
1535 .and(object::Column::ObjType.eq(ObjectType::Table))
1536 .and(streaming_job::Column::JobStatus.ne(JobStatus::Created)),
1537 )
1538 .count(&txn)
1539 .await?;
1540 if referring_cnt != 0 {
1541 return Err(MetaError::permission_denied(
1542 "job is being altered or referenced by some creating jobs",
1543 ));
1544 }
1545
1546 let batch_refresh_dep_cnt = ObjectDependency::find()
1549 .join(
1550 JoinType::InnerJoin,
1551 object_dependency::Relation::Object1.def(),
1552 )
1553 .join(JoinType::InnerJoin, object::Relation::StreamingJob.def())
1554 .filter(
1555 object_dependency::Column::Oid
1556 .eq(id)
1557 .and(streaming_job::Column::RefreshIntervalSec.is_not_null()),
1558 )
1559 .count(&txn)
1560 .await?;
1561 if batch_refresh_dep_cnt != 0 {
1562 return Err(MetaError::permission_denied(
1563 "replacing a table with dependent batch refresh materialized views is not supported",
1564 ));
1565 }
1566
1567 let original_job = StreamingJobModel::find_by_id(id)
1569 .one(&txn)
1570 .await?
1571 .map(ReplaceOriginalJobInfo::from)
1572 .ok_or_else(|| MetaError::catalog_id_not_found(streaming_job.job_type_str(), id))?;
1573
1574 if let Some(max_parallelism) = expected_original_max_parallelism
1575 && original_job.max_parallelism != max_parallelism as i32
1576 {
1577 bail!(
1580 "cannot use a different max parallelism \
1581 when replacing streaming job, \
1582 original: {}, new: {}",
1583 original_job.max_parallelism,
1584 max_parallelism
1585 );
1586 }
1587
1588 let parallelism = original_job.resolved_parallelism(specified_parallelism);
1589 let ctx = original_job.stream_context(ctx);
1590 let adaptive_parallelism_strategy = original_job
1591 .adaptive_parallelism_strategy
1592 .as_deref()
1593 .map(|s| parse_strategy(s).expect("strategy should be validated before persisting"));
1594 let resource_type = original_job.resource_type();
1595
1596 let tmp_model = Self::create_streaming_job_obj(
1598 &txn,
1599 streaming_job.object_type(),
1600 streaming_job.owner() as _,
1601 Some(streaming_job.database_id() as _),
1602 Some(streaming_job.schema_id() as _),
1603 streaming_job.create_type(),
1604 ctx,
1605 adaptive_parallelism_strategy,
1606 parallelism,
1607 original_job.max_parallelism as _,
1608 resource_type,
1609 None,
1613 None,
1614 None, )
1616 .await?;
1617
1618 ObjectDependency::insert(object_dependency::ActiveModel {
1620 oid: Set(id.as_object_id()),
1621 used_by: Set(tmp_model.job_id.as_object_id()),
1622 ..Default::default()
1623 })
1624 .exec(&txn)
1625 .await?;
1626
1627 txn.commit().await?;
1628
1629 Ok(tmp_model)
1630 }
1631
1632 pub async fn finish_streaming_job(&self, job_id: JobId) -> MetaResult<()> {
1634 let mut inner = self.inner.write().await;
1635 let txn = inner.db.begin().await?;
1636
1637 if check_if_belongs_to_iceberg_table(&txn, job_id).await? {
1639 tracing::info!(
1640 "streaming job {} is for iceberg table, wait for manual finish operation",
1641 job_id
1642 );
1643 return Ok(());
1644 }
1645
1646 let (notification_op, objects, updated_user_info, dependencies) =
1647 self.finish_streaming_job_inner(&txn, job_id).await?;
1648
1649 txn.commit().await?;
1650
1651 let mut version = self
1652 .notify_frontend(
1653 notification_op,
1654 NotificationInfo::ObjectGroup(PbObjectGroup {
1655 objects,
1656 dependencies,
1657 }),
1658 )
1659 .await;
1660
1661 if !updated_user_info.is_empty() {
1663 version = self.notify_users_update(updated_user_info).await;
1664 }
1665
1666 inner
1667 .creating_table_finish_notifier
1668 .values_mut()
1669 .for_each(|creating_tables| {
1670 if let Some(txs) = creating_tables.remove(&job_id) {
1671 for tx in txs {
1672 let _ = tx.send(Ok(version));
1673 }
1674 }
1675 });
1676
1677 Ok(())
1678 }
1679
1680 pub async fn finish_streaming_job_inner(
1682 &self,
1683 txn: &DatabaseTransaction,
1684 job_id: JobId,
1685 ) -> MetaResult<(
1686 Operation,
1687 Vec<risingwave_pb::meta::Object>,
1688 Vec<PbUserInfo>,
1689 Vec<PbObjectDependency>,
1690 )> {
1691 let job_type = Object::find_by_id(job_id)
1692 .select_only()
1693 .column(object::Column::ObjType)
1694 .into_tuple()
1695 .one(txn)
1696 .await?
1697 .ok_or_else(|| MetaError::catalog_id_not_found("streaming job", job_id))?;
1698
1699 let res = Object::update_many()
1701 .col_expr(object::Column::CreatedAt, Expr::current_timestamp().into())
1702 .col_expr(
1703 object::Column::CreatedAtClusterVersion,
1704 current_cluster_version().into(),
1705 )
1706 .filter(object::Column::Oid.eq(job_id))
1707 .exec(txn)
1708 .await?;
1709 if res.rows_affected == 0 {
1710 return Err(MetaError::catalog_id_not_found("streaming job", job_id));
1711 }
1712
1713 let job = streaming_job::ActiveModel {
1715 job_id: Set(job_id),
1716 job_status: Set(JobStatus::Created),
1717 ..Default::default()
1718 };
1719 let streaming_job = Some(job.update(txn).await?);
1720
1721 let internal_table_objs = Table::find()
1723 .find_also_related(Object)
1724 .filter(table::Column::BelongsToJobId.eq(job_id))
1725 .all(txn)
1726 .await?;
1727 let mut objects = internal_table_objs
1728 .iter()
1729 .map(|(table, obj)| PbObject {
1730 object_info: Some(PbObjectInfo::Table(
1731 ObjectModel(table.clone(), obj.clone().unwrap(), streaming_job.clone()).into(),
1732 )),
1733 })
1734 .collect_vec();
1735 let notification_op = NotificationOperation::Update;
1736 let mut updated_user_info = vec![];
1737 let mut need_grant_default_privileges = true;
1738
1739 match job_type {
1740 ObjectType::Table => {
1741 let (table, obj) = Table::find_by_id(job_id.as_mv_table_id())
1742 .find_also_related(Object)
1743 .one(txn)
1744 .await?
1745 .ok_or_else(|| MetaError::catalog_id_not_found("table", job_id))?;
1746 if let Some(source_id) = table.optional_associated_source_id {
1747 let (src, obj) = Source::find_by_id(source_id)
1748 .find_also_related(Object)
1749 .one(txn)
1750 .await?
1751 .ok_or_else(|| MetaError::catalog_id_not_found("source", source_id))?;
1752 objects.push(PbObject {
1753 object_info: Some(PbObjectInfo::Source(
1754 ObjectModel(src, obj.unwrap(), None).into(),
1755 )),
1756 });
1757 }
1758 objects.push(PbObject {
1759 object_info: Some(PbObjectInfo::Table(
1760 ObjectModel(table, obj.unwrap(), streaming_job).into(),
1761 )),
1762 });
1763 }
1764 ObjectType::Sink => {
1765 let (sink, obj) = Sink::find_by_id(job_id.as_sink_id())
1766 .find_also_related(Object)
1767 .one(txn)
1768 .await?
1769 .ok_or_else(|| MetaError::catalog_id_not_found("sink", job_id))?;
1770 if sink.name.starts_with(ICEBERG_SINK_PREFIX) {
1771 need_grant_default_privileges = false;
1772 }
1773 objects.push(PbObject {
1774 object_info: Some(PbObjectInfo::Sink(
1775 ObjectModel(sink, obj.unwrap(), streaming_job).into(),
1776 )),
1777 });
1778 }
1779 ObjectType::Index => {
1780 need_grant_default_privileges = false;
1781 let (index, obj) = Index::find_by_id(job_id.as_index_id())
1782 .find_also_related(Object)
1783 .one(txn)
1784 .await?
1785 .ok_or_else(|| MetaError::catalog_id_not_found("index", job_id))?;
1786 {
1787 let (table, obj) = Table::find_by_id(index.index_table_id)
1788 .find_also_related(Object)
1789 .one(txn)
1790 .await?
1791 .ok_or_else(|| {
1792 MetaError::catalog_id_not_found("table", index.index_table_id)
1793 })?;
1794 objects.push(PbObject {
1795 object_info: Some(PbObjectInfo::Table(
1796 ObjectModel(table, obj.unwrap(), streaming_job.clone()).into(),
1797 )),
1798 });
1799 }
1800
1801 let primary_table_privileges = UserPrivilege::find()
1804 .filter(
1805 user_privilege::Column::Oid
1806 .eq(index.primary_table_id)
1807 .and(user_privilege::Column::Action.eq(Action::Select)),
1808 )
1809 .all(txn)
1810 .await?;
1811 if !primary_table_privileges.is_empty() {
1812 let index_state_table_ids: Vec<TableId> = Table::find()
1813 .select_only()
1814 .column(table::Column::TableId)
1815 .filter(
1816 table::Column::BelongsToJobId
1817 .eq(job_id)
1818 .or(table::Column::TableId.eq(index.index_table_id)),
1819 )
1820 .into_tuple()
1821 .all(txn)
1822 .await?;
1823 let mut new_privileges = vec![];
1824 for privilege in &primary_table_privileges {
1825 for state_table_id in &index_state_table_ids {
1826 new_privileges.push(user_privilege::ActiveModel {
1827 id: Default::default(),
1828 oid: Set(state_table_id.as_object_id()),
1829 user_id: Set(privilege.user_id),
1830 action: Set(Action::Select),
1831 dependent_id: Set(privilege.dependent_id),
1832 granted_by: Set(privilege.granted_by),
1833 with_grant_option: Set(privilege.with_grant_option),
1834 });
1835 }
1836 }
1837 UserPrivilege::insert_many(new_privileges).exec(txn).await?;
1838
1839 updated_user_info = list_user_info_by_ids(
1840 primary_table_privileges.into_iter().map(|p| p.user_id),
1841 txn,
1842 )
1843 .await?;
1844 }
1845
1846 objects.push(PbObject {
1847 object_info: Some(PbObjectInfo::Index(
1848 ObjectModel(index, obj.unwrap(), streaming_job).into(),
1849 )),
1850 });
1851 }
1852 ObjectType::Source => {
1853 let (source, obj) = Source::find_by_id(job_id.as_shared_source_id())
1854 .find_also_related(Object)
1855 .one(txn)
1856 .await?
1857 .ok_or_else(|| MetaError::catalog_id_not_found("source", job_id))?;
1858 objects.push(PbObject {
1859 object_info: Some(PbObjectInfo::Source(
1860 ObjectModel(source, obj.unwrap(), None).into(),
1861 )),
1862 });
1863 }
1864 _ => unreachable!("invalid job type: {:?}", job_type),
1865 }
1866
1867 if need_grant_default_privileges {
1868 updated_user_info = grant_default_privileges_automatically(txn, job_id).await?;
1869 }
1870
1871 let dependencies =
1872 list_object_dependencies_by_object_id(txn, job_id.as_object_id()).await?;
1873
1874 Ok((notification_op, objects, updated_user_info, dependencies))
1875 }
1876
1877 pub async fn finish_replace_streaming_job(
1878 &self,
1879 tmp_id: JobId,
1880 streaming_job: StreamingJob,
1881 replace_upstream: FragmentReplaceUpstream,
1882 sink_into_table_context: SinkIntoTableContext,
1883 drop_table_connector_ctx: Option<&DropTableConnectorContext>,
1884 auto_refresh_schema_sinks: Option<Vec<FinishAutoRefreshSchemaSinkContext>>,
1885 ) -> MetaResult<NotificationVersion> {
1886 let inner = self.inner.write().await;
1887 let txn = inner.db.begin().await?;
1888
1889 let (objects, delete_notification_objs, old_fragment_ids, new_fragment_ids) =
1890 Self::finish_replace_streaming_job_inner(
1891 tmp_id,
1892 replace_upstream,
1893 sink_into_table_context,
1894 &txn,
1895 streaming_job,
1896 drop_table_connector_ctx,
1897 auto_refresh_schema_sinks,
1898 )
1899 .await?;
1900
1901 txn.commit().await?;
1902
1903 let notification_manager = self.env.notification_manager();
1905 notification_manager.notify_serving_fragment_mapping_delete(
1906 old_fragment_ids.iter().map(|id| *id as _).collect(),
1907 );
1908 notification_manager.notify_serving_fragment_mapping_update(
1909 new_fragment_ids.iter().map(|id| *id as _).collect(),
1910 );
1911
1912 let mut version = self
1913 .notify_frontend(
1914 NotificationOperation::Update,
1915 NotificationInfo::ObjectGroup(PbObjectGroup {
1916 objects,
1917 dependencies: vec![],
1918 }),
1919 )
1920 .await;
1921
1922 if let Some((user_infos, to_drop_objects)) = delete_notification_objs {
1923 self.notify_users_update(user_infos).await;
1924 version = self
1925 .notify_frontend(
1926 NotificationOperation::Delete,
1927 build_object_group_for_delete(to_drop_objects),
1928 )
1929 .await;
1930 }
1931
1932 Ok(version)
1933 }
1934
1935 fn update_iceberg_source_columns(
1936 original_source_columns: &[ColumnCatalog],
1937 original_row_id_index: Option<usize>,
1938 new_table_columns: &[ColumnCatalog],
1939 ) -> (Vec<ColumnCatalog>, Option<usize>) {
1940 let row_id_column_name = original_row_id_index
1941 .and_then(|idx| original_source_columns.get(idx))
1942 .map(|col| col.name().to_owned());
1943 let mut next_column_id = max_column_id(original_source_columns).next();
1944 let existing_columns: HashMap<String, ColumnCatalog> = original_source_columns
1945 .iter()
1946 .cloned()
1947 .map(|col| (col.name().to_owned(), col))
1948 .collect();
1949
1950 let mut new_columns = Vec::new();
1951 for table_col in new_table_columns
1952 .iter()
1953 .filter(|col| !col.is_rw_sys_column())
1954 {
1955 let mut source_col_name = table_col.name().to_owned();
1956 if source_col_name == ROW_ID_COLUMN_NAME {
1957 source_col_name = RISINGWAVE_ICEBERG_ROW_ID.to_owned();
1958 }
1959
1960 if let Some(existing) = existing_columns.get(&source_col_name) {
1961 new_columns.push(existing.clone());
1962 } else {
1963 let mut new_col = table_col.clone();
1964 new_col.column_desc.name = source_col_name;
1965 new_col.column_desc.column_id = next_column_id;
1966 next_column_id = next_column_id.next();
1967 new_columns.push(new_col);
1968 }
1969 }
1970
1971 let mut seen_names: HashSet<String> = new_columns
1972 .iter()
1973 .map(|col| col.name().to_owned())
1974 .collect();
1975 for col in original_source_columns
1976 .iter()
1977 .filter(|col| col.is_iceberg_hidden_column())
1978 {
1979 if seen_names.insert(col.name().to_owned()) {
1980 new_columns.push(col.clone());
1981 }
1982 }
1983
1984 let new_row_id_index = row_id_column_name
1985 .as_ref()
1986 .and_then(|name| new_columns.iter().position(|col| col.name() == name));
1987
1988 (new_columns, new_row_id_index)
1989 }
1990
1991 pub async fn finish_replace_streaming_job_inner(
1992 tmp_id: JobId,
1993 replace_upstream: FragmentReplaceUpstream,
1994 SinkIntoTableContext {
1995 updated_sink_catalogs,
1996 }: SinkIntoTableContext,
1997 txn: &DatabaseTransaction,
1998 streaming_job: StreamingJob,
1999 drop_table_connector_ctx: Option<&DropTableConnectorContext>,
2000 auto_refresh_schema_sinks: Option<Vec<FinishAutoRefreshSchemaSinkContext>>,
2001 ) -> MetaResult<(
2002 Vec<PbObject>,
2003 Option<(Vec<PbUserInfo>, Vec<PartialObject>)>,
2004 Vec<FragmentId>,
2005 Vec<FragmentId>,
2006 )> {
2007 let original_job_id = streaming_job.id();
2008 let job_type = streaming_job.job_type();
2009
2010 let old_fragment_ids: Vec<FragmentId> = Fragment::find()
2012 .select_only()
2013 .column(fragment::Column::FragmentId)
2014 .filter(fragment::Column::JobId.eq(original_job_id))
2015 .into_tuple()
2016 .all(txn)
2017 .await?;
2018 let new_fragment_ids: Vec<FragmentId> = Fragment::find()
2019 .select_only()
2020 .column(fragment::Column::FragmentId)
2021 .filter(fragment::Column::JobId.eq(tmp_id))
2022 .into_tuple()
2023 .all(txn)
2024 .await?;
2025
2026 let mut index_item_rewriter = None;
2027 let mut updated_iceberg_source_id: Option<SourceId> = None;
2028
2029 match streaming_job {
2031 StreamingJob::Table(_, table, _table_job_type) => {
2032 let original_column_catalogs =
2033 get_table_columns(txn, original_job_id.as_mv_table_id()).await?;
2034 let schema_changed = original_column_catalogs.to_protobuf() != table.columns;
2035 let is_iceberg = table
2036 .engine
2037 .and_then(|engine| PbEngine::try_from(engine).ok())
2038 == Some(PbEngine::Iceberg);
2039 if is_iceberg && schema_changed {
2040 let iceberg_source_name = format!("{}{}", ICEBERG_SOURCE_PREFIX, table.name);
2041 let source = Source::find()
2042 .inner_join(Object)
2043 .filter(
2044 object::Column::DatabaseId
2045 .eq(table.database_id)
2046 .and(object::Column::SchemaId.eq(table.schema_id))
2047 .and(source::Column::Name.eq(&iceberg_source_name)),
2048 )
2049 .one(txn)
2050 .await?
2051 .ok_or_else(|| {
2052 MetaError::catalog_id_not_found("source", iceberg_source_name)
2053 })?;
2054
2055 let source_id = source.source_id;
2056 let source_version = source.version;
2057 let source_columns: Vec<ColumnCatalog> = source
2058 .columns
2059 .to_protobuf()
2060 .into_iter()
2061 .map(ColumnCatalog::from)
2062 .collect();
2063 let source_row_id_index = source
2064 .row_id_index
2065 .and_then(|idx| usize::try_from(idx).ok());
2066 let table_columns: Vec<ColumnCatalog> = table
2067 .columns
2068 .iter()
2069 .cloned()
2070 .map(ColumnCatalog::from)
2071 .collect();
2072 let (updated_columns, updated_row_id_index) =
2073 Self::update_iceberg_source_columns(
2074 &source_columns,
2075 source_row_id_index,
2076 &table_columns,
2077 );
2078 let updated_columns: Vec<PbColumnCatalog> = updated_columns
2079 .into_iter()
2080 .map(|col| col.to_protobuf())
2081 .collect();
2082
2083 let mut source_active = source.into_active_model();
2084 source_active.columns = Set(ColumnCatalogArray::from(updated_columns));
2085 source_active.row_id_index = Set(updated_row_id_index.map(|idx| idx as i32));
2086 source_active.version = Set(source_version + 1);
2087 source_active.update(txn).await?;
2088 updated_iceberg_source_id = Some(source_id);
2089 }
2090
2091 index_item_rewriter = Some({
2092 let original_columns = original_column_catalogs
2093 .to_protobuf()
2094 .into_iter()
2095 .map(|c| c.column_desc.unwrap())
2096 .collect_vec();
2097 let new_columns = table
2098 .columns
2099 .iter()
2100 .map(|c| c.column_desc.clone().unwrap())
2101 .collect_vec();
2102
2103 IndexItemRewriter {
2104 original_columns,
2105 new_columns,
2106 }
2107 });
2108
2109 for sink_id in updated_sink_catalogs {
2111 Sink::update(sink::ActiveModel {
2112 sink_id: Set(sink_id as _),
2113 original_target_columns: Set(Some(original_column_catalogs.clone())),
2114 ..Default::default()
2115 })
2116 .exec(txn)
2117 .await?;
2118 }
2119 let mut table = table::ActiveModel::from(table);
2121 if let Some(drop_table_connector_ctx) = drop_table_connector_ctx
2122 && drop_table_connector_ctx.to_change_streaming_job_id == original_job_id
2123 {
2124 table.optional_associated_source_id = Set(None);
2126 }
2127
2128 Table::update(table).exec(txn).await?;
2129 }
2130 StreamingJob::Source(source) => {
2131 let source = source::ActiveModel::from(source);
2133 Source::update(source).exec(txn).await?;
2134 }
2135 StreamingJob::MaterializedView(table) => {
2136 let table = table::ActiveModel::from(table);
2138 Table::update(table).exec(txn).await?;
2139 }
2140 _ => unreachable!(
2141 "invalid streaming job type: {:?}",
2142 streaming_job.job_type_str()
2143 ),
2144 }
2145
2146 async fn finish_fragments(
2147 txn: &DatabaseTransaction,
2148 tmp_id: JobId,
2149 original_job_id: JobId,
2150 replace_upstream: FragmentReplaceUpstream,
2151 ) -> MetaResult<()> {
2152 let fragment_info: Vec<(FragmentId, I32Array)> = Fragment::find()
2156 .select_only()
2157 .columns([
2158 fragment::Column::FragmentId,
2159 fragment::Column::StateTableIds,
2160 ])
2161 .filter(fragment::Column::JobId.eq(tmp_id))
2162 .into_tuple()
2163 .all(txn)
2164 .await?;
2165 for (fragment_id, state_table_ids) in fragment_info {
2166 for state_table_id in state_table_ids.into_inner() {
2167 let state_table_id = TableId::new(state_table_id as _);
2168 Table::update(table::ActiveModel {
2169 table_id: Set(state_table_id),
2170 fragment_id: Set(Some(fragment_id)),
2171 ..Default::default()
2173 })
2174 .exec(txn)
2175 .await?;
2176 }
2177 }
2178
2179 Fragment::delete_many()
2181 .filter(fragment::Column::JobId.eq(original_job_id))
2182 .exec(txn)
2183 .await?;
2184 Fragment::update_many()
2185 .col_expr(fragment::Column::JobId, SimpleExpr::from(original_job_id))
2186 .filter(fragment::Column::JobId.eq(tmp_id))
2187 .exec(txn)
2188 .await?;
2189
2190 for (fragment_id, fragment_replace_map) in replace_upstream {
2193 let (fragment_id, mut stream_node) =
2194 Fragment::find_by_id(fragment_id as FragmentId)
2195 .select_only()
2196 .columns([fragment::Column::FragmentId, fragment::Column::StreamNode])
2197 .into_tuple::<(FragmentId, StreamNode)>()
2198 .one(txn)
2199 .await?
2200 .map(|(id, node)| (id, node.to_protobuf()))
2201 .ok_or_else(|| MetaError::catalog_id_not_found("fragment", fragment_id))?;
2202
2203 visit_stream_node_mut(&mut stream_node, |body| {
2204 if let PbNodeBody::Merge(m) = body
2205 && let Some(new_fragment_id) =
2206 fragment_replace_map.get(&m.upstream_fragment_id)
2207 {
2208 m.upstream_fragment_id = *new_fragment_id;
2209 }
2210 });
2211 Fragment::update(fragment::ActiveModel {
2212 fragment_id: Set(fragment_id),
2213 stream_node: Set(StreamNode::from(&stream_node)),
2214 ..Default::default()
2215 })
2216 .exec(txn)
2217 .await?;
2218 }
2219
2220 Object::delete_by_id(tmp_id).exec(txn).await?;
2222
2223 Ok(())
2224 }
2225
2226 finish_fragments(txn, tmp_id, original_job_id, replace_upstream).await?;
2227
2228 let mut objects = vec![];
2230 match job_type {
2231 StreamingJobType::Table(_) | StreamingJobType::MaterializedView => {
2232 let (table, table_obj) = Table::find_by_id(original_job_id.as_mv_table_id())
2233 .find_also_related(Object)
2234 .one(txn)
2235 .await?
2236 .ok_or_else(|| MetaError::catalog_id_not_found("object", original_job_id))?;
2237 let streaming_job = streaming_job::Entity::find_by_id(table.job_id())
2238 .one(txn)
2239 .await?;
2240 objects.push(PbObject {
2241 object_info: Some(PbObjectInfo::Table(
2242 ObjectModel(table, table_obj.unwrap(), streaming_job).into(),
2243 )),
2244 })
2245 }
2246 StreamingJobType::Source => {
2247 let (source, source_obj) =
2248 Source::find_by_id(original_job_id.as_shared_source_id())
2249 .find_also_related(Object)
2250 .one(txn)
2251 .await?
2252 .ok_or_else(|| {
2253 MetaError::catalog_id_not_found("object", original_job_id)
2254 })?;
2255 objects.push(PbObject {
2256 object_info: Some(PbObjectInfo::Source(
2257 ObjectModel(source, source_obj.unwrap(), None).into(),
2258 )),
2259 })
2260 }
2261 _ => unreachable!("invalid streaming job type for replace: {:?}", job_type),
2262 }
2263
2264 if let Some(source_id) = updated_iceberg_source_id {
2265 let (source, source_obj) = Source::find_by_id(source_id)
2266 .find_also_related(Object)
2267 .one(txn)
2268 .await?
2269 .ok_or_else(|| MetaError::catalog_id_not_found("source", source_id))?;
2270 objects.push(PbObject {
2271 object_info: Some(PbObjectInfo::Source(
2272 ObjectModel(source, source_obj.unwrap(), None).into(),
2273 )),
2274 });
2275 }
2276
2277 if let Some(expr_rewriter) = index_item_rewriter {
2278 let index_items: Vec<(IndexId, ExprNodeArray)> = Index::find()
2279 .select_only()
2280 .columns([index::Column::IndexId, index::Column::IndexItems])
2281 .filter(index::Column::PrimaryTableId.eq(original_job_id))
2282 .into_tuple()
2283 .all(txn)
2284 .await?;
2285 for (index_id, nodes) in index_items {
2286 let mut pb_nodes = nodes.to_protobuf();
2287 pb_nodes
2288 .iter_mut()
2289 .for_each(|x| expr_rewriter.rewrite_expr(x));
2290 let index = index::ActiveModel {
2291 index_id: Set(index_id),
2292 index_items: Set(pb_nodes.into()),
2293 ..Default::default()
2294 }
2295 .update(txn)
2296 .await?;
2297 let (index_obj, streaming_job) = Object::find_by_id(index.index_id)
2298 .find_also_related(streaming_job::Entity)
2299 .one(txn)
2300 .await?
2301 .ok_or_else(|| MetaError::catalog_id_not_found("object", index.index_id))?;
2302 objects.push(PbObject {
2303 object_info: Some(PbObjectInfo::Index(
2304 ObjectModel(index, index_obj, streaming_job).into(),
2305 )),
2306 });
2307 }
2308 }
2309
2310 if let Some(sinks) = auto_refresh_schema_sinks {
2311 for finish_sink_context in sinks {
2312 finish_fragments(
2313 txn,
2314 finish_sink_context.tmp_sink_id.as_job_id(),
2315 finish_sink_context.original_sink_id.as_job_id(),
2316 Default::default(),
2317 )
2318 .await?;
2319 let (mut sink, sink_obj) = Sink::find_by_id(finish_sink_context.original_sink_id)
2320 .find_also_related(Object)
2321 .one(txn)
2322 .await?
2323 .ok_or_else(|| MetaError::catalog_id_not_found("sink", original_job_id))?;
2324 let sink_streaming_job =
2325 streaming_job::Entity::find_by_id(sink.sink_id.as_job_id())
2326 .one(txn)
2327 .await?;
2328 let columns = ColumnCatalogArray::from(finish_sink_context.columns);
2329 Sink::update(sink::ActiveModel {
2330 sink_id: Set(finish_sink_context.original_sink_id),
2331 columns: Set(columns.clone()),
2332 ..Default::default()
2333 })
2334 .exec(txn)
2335 .await?;
2336 sink.columns = columns;
2337 objects.push(PbObject {
2338 object_info: Some(PbObjectInfo::Sink(
2339 ObjectModel(sink, sink_obj.unwrap(), sink_streaming_job.clone()).into(),
2340 )),
2341 });
2342 if let Some(new_log_store_table) = finish_sink_context.new_log_store_table {
2343 let log_store_table_id = new_log_store_table.id;
2344 let new_log_store_table_columns: ColumnCatalogArray =
2345 new_log_store_table.columns.clone().into();
2346 let new_log_store_table_value_indices =
2347 new_log_store_table.value_indices.clone();
2348 let (mut table, table_obj) = Table::find_by_id(log_store_table_id)
2349 .find_also_related(Object)
2350 .one(txn)
2351 .await?
2352 .ok_or_else(|| MetaError::catalog_id_not_found("table", original_job_id))?;
2353 Table::update(table::ActiveModel {
2354 table_id: Set(log_store_table_id),
2355 columns: Set(new_log_store_table_columns.clone()),
2356 value_indices: Set(new_log_store_table_value_indices.clone().into()),
2357 ..Default::default()
2358 })
2359 .exec(txn)
2360 .await?;
2361 table.columns = new_log_store_table_columns;
2362 table.value_indices = new_log_store_table_value_indices.into();
2363 objects.push(PbObject {
2364 object_info: Some(PbObjectInfo::Table(
2365 ObjectModel(table, table_obj.unwrap(), sink_streaming_job.clone())
2366 .into(),
2367 )),
2368 });
2369 }
2370 }
2371 }
2372
2373 let mut notification_objs: Option<(Vec<PbUserInfo>, Vec<PartialObject>)> = None;
2374 if let Some(drop_table_connector_ctx) = drop_table_connector_ctx {
2375 notification_objs =
2376 Some(Self::drop_table_associated_source(txn, drop_table_connector_ctx).await?);
2377 }
2378
2379 Ok((
2380 objects,
2381 notification_objs,
2382 old_fragment_ids,
2383 new_fragment_ids,
2384 ))
2385 }
2386
2387 pub async fn try_abort_replacing_streaming_job(
2389 &self,
2390 tmp_job_id: JobId,
2391 tmp_sink_ids: Option<Vec<ObjectId>>,
2392 ) -> MetaResult<()> {
2393 let inner = self.inner.write().await;
2394 let txn = inner.db.begin().await?;
2395
2396 let mut all_job_ids: Vec<ObjectId> = vec![tmp_job_id.into()];
2398 if let Some(ref sink_ids) = tmp_sink_ids {
2399 all_job_ids.extend(sink_ids.iter().copied());
2400 }
2401 let abort_fragment_ids: Vec<FragmentId> = Fragment::find()
2402 .select_only()
2403 .column(fragment::Column::FragmentId)
2404 .filter(fragment::Column::JobId.is_in(all_job_ids))
2405 .into_tuple()
2406 .all(&txn)
2407 .await?;
2408
2409 Object::delete_by_id(tmp_job_id).exec(&txn).await?;
2410 if let Some(tmp_sink_ids) = tmp_sink_ids {
2411 for tmp_sink_id in tmp_sink_ids {
2412 Object::delete_by_id(tmp_sink_id).exec(&txn).await?;
2413 }
2414 }
2415 txn.commit().await?;
2416
2417 self.env
2419 .notification_manager()
2420 .notify_serving_fragment_mapping_delete(
2421 abort_fragment_ids.iter().map(|id| *id as _).collect(),
2422 );
2423
2424 Ok(())
2425 }
2426
2427 pub async fn update_source_rate_limit_by_source_id(
2430 &self,
2431 source_id: SourceId,
2432 rate_limit: Option<u32>,
2433 ) -> MetaResult<(HashSet<JobId>, HashMap<FragmentId, PbStreamNode>)> {
2434 let inner = self.inner.read().await;
2435 let txn = inner.db.begin().await?;
2436
2437 {
2438 let active_source = source::ActiveModel {
2439 source_id: Set(source_id),
2440 rate_limit: Set(rate_limit.map(|v| v as i32)),
2441 ..Default::default()
2442 };
2443 Source::update(active_source).exec(&txn).await?;
2444 }
2445
2446 let (source, obj) = Source::find_by_id(source_id)
2447 .find_also_related(Object)
2448 .one(&txn)
2449 .await?
2450 .ok_or_else(|| {
2451 MetaError::catalog_id_not_found(ObjectType::Source.as_str(), source_id)
2452 })?;
2453
2454 let is_fs_source = source.with_properties.inner_ref().is_new_fs_connector();
2455 let streaming_job_ids: Vec<JobId> =
2456 if let Some(table_id) = source.optional_associated_table_id {
2457 vec![table_id.as_job_id()]
2458 } else if let Some(source_info) = &source.source_info
2459 && source_info.to_protobuf().is_shared()
2460 {
2461 vec![source_id.as_share_source_job_id()]
2462 } else {
2463 ObjectDependency::find()
2464 .select_only()
2465 .column(object_dependency::Column::UsedBy)
2466 .filter(object_dependency::Column::Oid.eq(source_id))
2467 .into_tuple()
2468 .all(&txn)
2469 .await?
2470 };
2471
2472 if streaming_job_ids.is_empty() {
2473 return Err(MetaError::invalid_parameter(format!(
2474 "source id {source_id} not used by any streaming job"
2475 )));
2476 }
2477
2478 let fragments: Vec<(FragmentId, JobId, i32, StreamNode)> = Fragment::find()
2479 .select_only()
2480 .columns([
2481 fragment::Column::FragmentId,
2482 fragment::Column::JobId,
2483 fragment::Column::FragmentTypeMask,
2484 fragment::Column::StreamNode,
2485 ])
2486 .filter(fragment::Column::JobId.is_in(streaming_job_ids))
2487 .into_tuple()
2488 .all(&txn)
2489 .await?;
2490 let mut fragments = fragments
2491 .into_iter()
2492 .map(|(id, job_id, mask, stream_node)| {
2493 (
2494 id,
2495 job_id,
2496 FragmentTypeMask::from(mask as u32),
2497 stream_node.to_protobuf(),
2498 )
2499 })
2500 .collect_vec();
2501
2502 fragments.retain_mut(|(_, _, fragment_type_mask, stream_node)| {
2503 let mut found = false;
2504 if fragment_type_mask.contains(FragmentTypeFlag::Source) {
2505 visit_stream_node_mut(stream_node, |node| {
2506 if let PbNodeBody::Source(node) = node
2507 && let Some(node_inner) = &mut node.source_inner
2508 && node_inner.source_id == source_id
2509 {
2510 node_inner.rate_limit = rate_limit;
2511 found = true;
2512 }
2513 });
2514 }
2515 if is_fs_source {
2516 visit_stream_node_mut(stream_node, |node| {
2519 if let PbNodeBody::StreamFsFetch(node) = node {
2520 fragment_type_mask.add(FragmentTypeFlag::FsFetch);
2521 if let Some(node_inner) = &mut node.node_inner
2522 && node_inner.source_id == source_id
2523 {
2524 node_inner.rate_limit = rate_limit;
2525 found = true;
2526 }
2527 }
2528 });
2529 }
2530 found
2531 });
2532
2533 assert!(
2534 !fragments.is_empty(),
2535 "source id should be used by at least one fragment"
2536 );
2537
2538 let (fragment_nodes, job_ids): (HashMap<FragmentId, PbStreamNode>, HashSet<JobId>) =
2539 fragments
2540 .iter()
2541 .map(|(fragment_id, job_id, _, stream_node)| {
2542 ((*fragment_id, stream_node.clone()), *job_id)
2543 })
2544 .unzip();
2545
2546 for (fragment_id, _, fragment_type_mask, stream_node) in fragments {
2547 Fragment::update(fragment::ActiveModel {
2548 fragment_id: Set(fragment_id),
2549 fragment_type_mask: Set(fragment_type_mask.into()),
2550 stream_node: Set(StreamNode::from(&stream_node)),
2551 ..Default::default()
2552 })
2553 .exec(&txn)
2554 .await?;
2555 }
2556
2557 txn.commit().await?;
2558
2559 let relation_info = PbObjectInfo::Source(ObjectModel(source, obj.unwrap(), None).into());
2560 let _version = self
2561 .notify_frontend(
2562 NotificationOperation::Update,
2563 NotificationInfo::ObjectGroup(PbObjectGroup {
2564 objects: vec![PbObject {
2565 object_info: Some(relation_info),
2566 }],
2567 dependencies: vec![],
2568 }),
2569 )
2570 .await;
2571
2572 Ok((job_ids, fragment_nodes))
2573 }
2574
2575 pub async fn mutate_fragments_by_job_id(
2578 &self,
2579 job_id: JobId,
2580 mut fragments_mutation_fn: impl FnMut(FragmentTypeMask, &mut PbStreamNode) -> MetaResult<bool>,
2582 err_msg: &'static str,
2584 ) -> MetaResult<HashMap<FragmentId, PbStreamNode>> {
2585 let inner = self.inner.read().await;
2586 let txn = inner.db.begin().await?;
2587
2588 let fragments: Vec<(FragmentId, i32, StreamNode)> = Fragment::find()
2589 .select_only()
2590 .columns([
2591 fragment::Column::FragmentId,
2592 fragment::Column::FragmentTypeMask,
2593 fragment::Column::StreamNode,
2594 ])
2595 .filter(fragment::Column::JobId.eq(job_id))
2596 .into_tuple()
2597 .all(&txn)
2598 .await?;
2599 let mut fragments = fragments
2600 .into_iter()
2601 .map(|(id, mask, stream_node)| {
2602 (id, FragmentTypeMask::from(mask), stream_node.to_protobuf())
2603 })
2604 .collect_vec();
2605
2606 let fragments = fragments
2607 .iter_mut()
2608 .map(|(_, fragment_type_mask, stream_node)| {
2609 fragments_mutation_fn(*fragment_type_mask, stream_node)
2610 })
2611 .collect::<MetaResult<Vec<bool>>>()?
2612 .into_iter()
2613 .zip_eq_debug(std::mem::take(&mut fragments))
2614 .filter_map(|(keep, fragment)| if keep { Some(fragment) } else { None })
2615 .collect::<Vec<_>>();
2616
2617 if fragments.is_empty() {
2618 return Err(MetaError::invalid_parameter(format!(
2619 "job id {job_id}: {}",
2620 err_msg
2621 )));
2622 }
2623
2624 let fragment_nodes = fragments
2625 .iter()
2626 .map(|(id, _, stream_node)| (*id, stream_node.clone()))
2627 .collect();
2628 for (id, _, stream_node) in fragments {
2629 Fragment::update(fragment::ActiveModel {
2630 fragment_id: Set(id),
2631 stream_node: Set(StreamNode::from(&stream_node)),
2632 ..Default::default()
2633 })
2634 .exec(&txn)
2635 .await?;
2636 }
2637
2638 txn.commit().await?;
2639
2640 Ok(fragment_nodes)
2641 }
2642
2643 async fn mutate_fragment_by_fragment_id(
2644 &self,
2645 fragment_id: FragmentId,
2646 mut fragment_mutation_fn: impl FnMut(FragmentTypeMask, &mut PbStreamNode) -> MetaResult<bool>,
2647 err_msg: &'static str,
2648 ) -> MetaResult<PbStreamNode> {
2649 let inner = self.inner.read().await;
2650 let txn = inner.db.begin().await?;
2651
2652 let (fragment_type_mask, stream_node): (i32, StreamNode) =
2653 Fragment::find_by_id(fragment_id)
2654 .select_only()
2655 .columns([
2656 fragment::Column::FragmentTypeMask,
2657 fragment::Column::StreamNode,
2658 ])
2659 .into_tuple()
2660 .one(&txn)
2661 .await?
2662 .ok_or_else(|| MetaError::catalog_id_not_found("fragment", fragment_id))?;
2663 let mut pb_stream_node = stream_node.to_protobuf();
2664 let fragment_type_mask = FragmentTypeMask::from(fragment_type_mask);
2665
2666 if !fragment_mutation_fn(fragment_type_mask, &mut pb_stream_node)? {
2667 return Err(MetaError::invalid_parameter(format!(
2668 "fragment id {fragment_id}: {}",
2669 err_msg
2670 )));
2671 }
2672
2673 Fragment::update(fragment::ActiveModel {
2674 fragment_id: Set(fragment_id),
2675 stream_node: Set(StreamNode::from(&pb_stream_node)),
2676 ..Default::default()
2677 })
2678 .exec(&txn)
2679 .await?;
2680
2681 txn.commit().await?;
2682
2683 Ok(pb_stream_node)
2684 }
2685
2686 pub async fn update_backfill_orders_by_job_id(
2687 &self,
2688 job_id: JobId,
2689 backfill_orders: Option<BackfillOrders>,
2690 ) -> MetaResult<()> {
2691 let inner = self.inner.write().await;
2692 let txn = inner.db.begin().await?;
2693
2694 ensure_job_not_canceled(job_id, &txn).await?;
2695
2696 streaming_job::ActiveModel {
2697 job_id: Set(job_id),
2698 backfill_orders: Set(backfill_orders),
2699 ..Default::default()
2700 }
2701 .update(&txn)
2702 .await?;
2703
2704 txn.commit().await?;
2705
2706 Ok(())
2707 }
2708
2709 pub async fn update_backfill_rate_limit_by_job_id(
2712 &self,
2713 job_id: JobId,
2714 rate_limit: Option<u32>,
2715 ) -> MetaResult<HashMap<FragmentId, PbStreamNode>> {
2716 let update_backfill_rate_limit =
2717 |fragment_type_mask: FragmentTypeMask, stream_node: &mut PbStreamNode| {
2718 let mut found = false;
2719 if fragment_type_mask
2720 .contains_any(FragmentTypeFlag::backfill_rate_limit_fragments())
2721 {
2722 visit_stream_node_mut(stream_node, |node| match node {
2723 PbNodeBody::StreamCdcScan(node) => {
2724 node.rate_limit = rate_limit;
2725 found = true;
2726 }
2727 PbNodeBody::StreamScan(node) => {
2728 node.rate_limit = rate_limit;
2729 found = true;
2730 }
2731 PbNodeBody::SourceBackfill(node) => {
2732 node.rate_limit = rate_limit;
2733 found = true;
2734 }
2735 _ => {}
2736 });
2737 }
2738 Ok(found)
2739 };
2740
2741 self.mutate_fragments_by_job_id(
2742 job_id,
2743 update_backfill_rate_limit,
2744 "stream scan node or source node not found",
2745 )
2746 .await
2747 }
2748
2749 pub async fn update_sink_rate_limit_by_job_id(
2752 &self,
2753 sink_id: SinkId,
2754 rate_limit: Option<u32>,
2755 ) -> MetaResult<HashMap<FragmentId, PbStreamNode>> {
2756 let update_sink_rate_limit =
2757 |fragment_type_mask: FragmentTypeMask, stream_node: &mut PbStreamNode| {
2758 let mut found = Ok(false);
2759 if fragment_type_mask.contains_any(FragmentTypeFlag::sink_rate_limit_fragments()) {
2760 visit_stream_node_mut(stream_node, |node| {
2761 if found.is_err() {
2762 return;
2763 }
2764 match update_sink_node_rate_limit(node, rate_limit) {
2765 Ok(true) => found = Ok(true),
2766 Ok(false) => {}
2767 Err(err) => found = Err(err),
2768 }
2769 });
2770 }
2771 found
2772 };
2773
2774 self.mutate_fragments_by_job_id(
2775 sink_id.as_job_id(),
2776 update_sink_rate_limit,
2777 "sink node not found",
2778 )
2779 .await
2780 }
2781
2782 pub async fn update_dml_rate_limit_by_job_id(
2783 &self,
2784 job_id: JobId,
2785 rate_limit: Option<u32>,
2786 ) -> MetaResult<HashMap<FragmentId, PbStreamNode>> {
2787 let update_dml_rate_limit =
2788 |fragment_type_mask: FragmentTypeMask, stream_node: &mut PbStreamNode| {
2789 let mut found = false;
2790 if fragment_type_mask.contains_any(FragmentTypeFlag::dml_rate_limit_fragments()) {
2791 visit_stream_node_mut(stream_node, |node| {
2792 if let PbNodeBody::Dml(node) = node {
2793 node.rate_limit = rate_limit;
2794 found = true;
2795 }
2796 });
2797 }
2798 Ok(found)
2799 };
2800
2801 self.mutate_fragments_by_job_id(job_id, update_dml_rate_limit, "dml node not found")
2802 .await
2803 }
2804
2805 pub async fn update_source_props_by_source_id(
2806 &self,
2807 source_id: SourceId,
2808 alter_props: BTreeMap<String, String>,
2809 alter_secret_refs: BTreeMap<String, PbSecretRef>,
2810 skip_alter_on_fly_check: bool,
2811 ) -> MetaResult<WithOptionsSecResolved> {
2812 let inner = self.inner.read().await;
2813 let txn = inner.db.begin().await?;
2814
2815 let (source, _obj) = Source::find_by_id(source_id)
2816 .find_also_related(Object)
2817 .one(&txn)
2818 .await?
2819 .ok_or_else(|| {
2820 MetaError::catalog_id_not_found(ObjectType::Source.as_str(), source_id)
2821 })?;
2822 let connector = source.with_properties.0.get_connector().unwrap();
2823 let is_shared_source = source.is_shared();
2824
2825 let mut dep_source_job_ids: Vec<JobId> = Vec::new();
2826 if !is_shared_source {
2827 dep_source_job_ids = ObjectDependency::find()
2829 .select_only()
2830 .column(object_dependency::Column::UsedBy)
2831 .filter(object_dependency::Column::Oid.eq(source_id))
2832 .into_tuple()
2833 .all(&txn)
2834 .await?;
2835 }
2836
2837 if let Some(new_connector) = alter_props.get(UPSTREAM_SOURCE_KEY)
2839 && new_connector != &connector
2840 {
2841 return Err(MetaError::invalid_parameter(format!(
2842 "Cannot change connector type from '{}' to '{}'. Drop and recreate the source instead.",
2843 connector, new_connector
2844 )));
2845 }
2846
2847 if !skip_alter_on_fly_check {
2849 let prop_keys: Vec<String> = alter_props
2850 .keys()
2851 .chain(alter_secret_refs.keys())
2852 .cloned()
2853 .collect();
2854 risingwave_connector::allow_alter_on_fly_fields::check_source_allow_alter_on_fly_fields(
2855 &connector, &prop_keys,
2856 )?;
2857 }
2858
2859 let mut options_with_secret = WithOptionsSecResolved::new(
2860 source.with_properties.0.clone(),
2861 source
2862 .secret_ref
2863 .map(|secret_ref| secret_ref.to_protobuf())
2864 .unwrap_or_default(),
2865 );
2866 let (to_add_secret_dep, to_remove_secret_dep) =
2867 options_with_secret.handle_update(alter_props, alter_secret_refs)?;
2868
2869 tracing::info!(
2870 "applying new properties to source: source_id={}, options_with_secret={:?}",
2871 source_id,
2872 options_with_secret
2873 );
2874 let _ = ConnectorProperties::extract(options_with_secret.clone(), true)?;
2876 let mut associate_table_id = None;
2879
2880 let mut preferred_id = source_id.as_object_id();
2884 let rewrite_sql = {
2885 let definition = source.definition.clone();
2886
2887 let [mut stmt]: [_; 1] = Parser::parse_sql(&definition)
2888 .map_err(|e| {
2889 MetaError::from(MetaErrorInner::Connector(ConnectorError::from(
2890 anyhow!(e).context("Failed to parse source definition SQL"),
2891 )))
2892 })?
2893 .try_into()
2894 .unwrap();
2895
2896 async fn format_with_option_secret_resolved(
2910 txn: &DatabaseTransaction,
2911 options_with_secret: &WithOptionsSecResolved,
2912 ) -> MetaResult<Vec<SqlOption>> {
2913 let mut options = Vec::new();
2914 for (k, v) in options_with_secret.as_plaintext() {
2915 let sql_option = SqlOption::try_from((k, &format!("'{}'", v)))
2916 .map_err(|e| MetaError::invalid_parameter(e.to_report_string()))?;
2917 options.push(sql_option);
2918 }
2919 for (k, v) in options_with_secret.as_secret() {
2920 if let Some(secret_model) = Secret::find_by_id(v.secret_id).one(txn).await? {
2921 let sql_option =
2922 SqlOption::try_from((k, &format!("SECRET {}", secret_model.name)))
2923 .map_err(|e| MetaError::invalid_parameter(e.to_report_string()))?;
2924 options.push(sql_option);
2925 } else {
2926 return Err(MetaError::catalog_id_not_found("secret", v.secret_id));
2927 }
2928 }
2929 Ok(options)
2930 }
2931
2932 match &mut stmt {
2933 Statement::CreateSource { stmt } => {
2934 stmt.with_properties.0 =
2935 format_with_option_secret_resolved(&txn, &options_with_secret).await?;
2936 }
2937 Statement::CreateTable { with_options, .. } => {
2938 *with_options =
2939 format_with_option_secret_resolved(&txn, &options_with_secret).await?;
2940 associate_table_id = source.optional_associated_table_id;
2941 preferred_id = associate_table_id.unwrap().as_object_id();
2942 }
2943 _ => unreachable!(),
2944 }
2945
2946 stmt.to_string()
2947 };
2948
2949 {
2950 if !to_add_secret_dep.is_empty() {
2953 ObjectDependency::insert_many(to_add_secret_dep.into_iter().map(|secret_id| {
2954 object_dependency::ActiveModel {
2955 oid: Set(secret_id.into()),
2956 used_by: Set(preferred_id),
2957 ..Default::default()
2958 }
2959 }))
2960 .exec(&txn)
2961 .await?;
2962 }
2963 if !to_remove_secret_dep.is_empty() {
2966 let _ = ObjectDependency::delete_many()
2967 .filter(
2968 object_dependency::Column::Oid
2969 .is_in(to_remove_secret_dep)
2970 .and(object_dependency::Column::UsedBy.eq(preferred_id)),
2971 )
2972 .exec(&txn)
2973 .await?;
2974 }
2975 }
2976
2977 let active_source_model = source::ActiveModel {
2978 source_id: Set(source_id),
2979 definition: Set(rewrite_sql.clone()),
2980 with_properties: Set(options_with_secret.as_plaintext().clone().into()),
2981 secret_ref: Set((!options_with_secret.as_secret().is_empty())
2982 .then(|| SecretRef::from(options_with_secret.as_secret().clone()))),
2983 ..Default::default()
2984 };
2985 Source::update(active_source_model).exec(&txn).await?;
2986
2987 if let Some(associate_table_id) = associate_table_id {
2988 let active_table_model = table::ActiveModel {
2990 table_id: Set(associate_table_id),
2991 definition: Set(rewrite_sql),
2992 ..Default::default()
2993 };
2994 Table::update(active_table_model).exec(&txn).await?;
2995 }
2996
2997 let to_check_job_ids = vec![if let Some(associate_table_id) = associate_table_id {
2998 associate_table_id.as_job_id()
3000 } else {
3001 source_id.as_share_source_job_id()
3002 }]
3003 .into_iter()
3004 .chain(dep_source_job_ids)
3005 .collect_vec();
3006
3007 update_connector_props_fragments(
3009 &txn,
3010 to_check_job_ids,
3011 FragmentTypeFlag::Source,
3012 |node, found| {
3013 if let PbNodeBody::Source(node) = node
3014 && let Some(source_inner) = &mut node.source_inner
3015 {
3016 source_inner.with_properties = options_with_secret.as_plaintext().clone();
3017 source_inner.secret_refs = options_with_secret.as_secret().clone();
3018 *found = true;
3019 }
3020 },
3021 is_shared_source,
3022 )
3023 .await?;
3024
3025 let mut to_update_objs = Vec::with_capacity(2);
3026 let (source, obj) = Source::find_by_id(source_id)
3027 .find_also_related(Object)
3028 .one(&txn)
3029 .await?
3030 .ok_or_else(|| {
3031 MetaError::catalog_id_not_found(ObjectType::Source.as_str(), source_id)
3032 })?;
3033 to_update_objs.push(PbObject {
3034 object_info: Some(PbObjectInfo::Source(
3035 ObjectModel(source, obj.unwrap(), None).into(),
3036 )),
3037 });
3038
3039 if let Some(associate_table_id) = associate_table_id {
3040 let (table, obj) = Table::find_by_id(associate_table_id)
3041 .find_also_related(Object)
3042 .one(&txn)
3043 .await?
3044 .ok_or_else(|| MetaError::catalog_id_not_found("table", associate_table_id))?;
3045 let streaming_job = streaming_job::Entity::find_by_id(table.job_id())
3046 .one(&txn)
3047 .await?;
3048 to_update_objs.push(PbObject {
3049 object_info: Some(PbObjectInfo::Table(
3050 ObjectModel(table, obj.unwrap(), streaming_job).into(),
3051 )),
3052 });
3053 }
3054
3055 txn.commit().await?;
3056
3057 self.notify_frontend(
3058 NotificationOperation::Update,
3059 NotificationInfo::ObjectGroup(PbObjectGroup {
3060 objects: to_update_objs,
3061 dependencies: vec![],
3062 }),
3063 )
3064 .await;
3065
3066 Ok(options_with_secret)
3067 }
3068
3069 pub async fn update_sink_props_by_sink_id(
3070 &self,
3071 sink_id: SinkId,
3072 props: BTreeMap<String, String>,
3073 ) -> MetaResult<HashMap<String, String>> {
3074 let inner = self.inner.read().await;
3075 let txn = inner.db.begin().await?;
3076
3077 let (sink, _obj) = Sink::find_by_id(sink_id)
3078 .find_also_related(Object)
3079 .one(&txn)
3080 .await?
3081 .ok_or_else(|| MetaError::catalog_id_not_found(ObjectType::Sink.as_str(), sink_id))?;
3082 validate_sink_props(&sink, &props)?;
3083 let definition = sink.definition.clone();
3084 let [mut stmt]: [_; 1] = Parser::parse_sql(&definition)
3085 .map_err(|e| SinkError::Config(anyhow!(e)))?
3086 .try_into()
3087 .unwrap();
3088 if let Statement::CreateSink { stmt } = &mut stmt {
3089 update_stmt_with_props(&mut stmt.with_properties.0, &props)?;
3090 } else {
3091 panic!("definition is not a create sink statement")
3092 }
3093 let mut new_config = sink.properties.clone().into_inner();
3094 new_config.extend(props.clone());
3095
3096 let definition = stmt.to_string();
3097 let active_sink = sink::ActiveModel {
3098 sink_id: Set(sink_id),
3099 properties: Set(risingwave_meta_model::Property(new_config.clone())),
3100 definition: Set(definition),
3101 ..Default::default()
3102 };
3103 Sink::update(active_sink).exec(&txn).await?;
3104
3105 update_sink_fragment_props(&txn, sink_id, new_config).await?;
3106 let (sink, obj) = Sink::find_by_id(sink_id)
3107 .find_also_related(Object)
3108 .one(&txn)
3109 .await?
3110 .ok_or_else(|| MetaError::catalog_id_not_found(ObjectType::Sink.as_str(), sink_id))?;
3111 let streaming_job = streaming_job::Entity::find_by_id(sink.sink_id.as_job_id())
3112 .one(&txn)
3113 .await?;
3114 txn.commit().await?;
3115 let relation_infos = vec![PbObject {
3116 object_info: Some(PbObjectInfo::Sink(
3117 ObjectModel(sink, obj.unwrap(), streaming_job).into(),
3118 )),
3119 }];
3120
3121 let _version = self
3122 .notify_frontend(
3123 NotificationOperation::Update,
3124 NotificationInfo::ObjectGroup(PbObjectGroup {
3125 objects: relation_infos,
3126 dependencies: vec![],
3127 }),
3128 )
3129 .await;
3130
3131 Ok(props.into_iter().collect())
3132 }
3133
3134 pub async fn update_iceberg_table_props_by_table_id(
3135 &self,
3136 table_id: TableId,
3137 props: BTreeMap<String, String>,
3138 alter_iceberg_table_props: Option<
3139 risingwave_pb::meta::alter_connector_props_request::PbExtraOptions,
3140 >,
3141 ) -> MetaResult<(HashMap<String, String>, SinkId)> {
3142 let risingwave_pb::meta::alter_connector_props_request::PbExtraOptions::AlterIcebergTableIds(AlterIcebergTableIds { sink_id, source_id }) = alter_iceberg_table_props.
3143 ok_or_else(|| MetaError::invalid_parameter("alter_iceberg_table_props is required"))?;
3144 let inner = self.inner.read().await;
3145 let txn = inner.db.begin().await?;
3146
3147 let (sink, _obj) = Sink::find_by_id(sink_id)
3148 .find_also_related(Object)
3149 .one(&txn)
3150 .await?
3151 .ok_or_else(|| MetaError::catalog_id_not_found(ObjectType::Sink.as_str(), sink_id))?;
3152 validate_sink_props(&sink, &props)?;
3153
3154 let definition = sink.definition.clone();
3155 let [mut stmt]: [_; 1] = Parser::parse_sql(&definition)
3156 .map_err(|e| SinkError::Config(anyhow!(e)))?
3157 .try_into()
3158 .unwrap();
3159 if let Statement::CreateTable {
3160 with_options,
3161 engine,
3162 ..
3163 } = &mut stmt
3164 {
3165 if !matches!(engine, Engine::Iceberg) {
3166 return Err(SinkError::Config(anyhow!(
3167 "only iceberg table can be altered as sink"
3168 ))
3169 .into());
3170 }
3171 update_stmt_with_props(with_options, &props)?;
3172 } else {
3173 panic!("definition is not a create iceberg table statement")
3174 }
3175 let mut new_config = sink.properties.clone().into_inner();
3176 new_config.extend(props.clone());
3177
3178 let definition = stmt.to_string();
3179 let active_sink = sink::ActiveModel {
3180 sink_id: Set(sink_id),
3181 properties: Set(risingwave_meta_model::Property(new_config.clone())),
3182 definition: Set(definition.clone()),
3183 ..Default::default()
3184 };
3185 let active_source = source::ActiveModel {
3186 source_id: Set(source_id),
3187 definition: Set(definition.clone()),
3188 ..Default::default()
3189 };
3190 let active_table = table::ActiveModel {
3191 table_id: Set(table_id),
3192 definition: Set(definition),
3193 ..Default::default()
3194 };
3195 Sink::update(active_sink).exec(&txn).await?;
3196 Source::update(active_source).exec(&txn).await?;
3197 Table::update(active_table).exec(&txn).await?;
3198
3199 update_sink_fragment_props(&txn, sink_id, new_config).await?;
3200
3201 let (sink, sink_obj) = Sink::find_by_id(sink_id)
3202 .find_also_related(Object)
3203 .one(&txn)
3204 .await?
3205 .ok_or_else(|| MetaError::catalog_id_not_found(ObjectType::Sink.as_str(), sink_id))?;
3206 let sink_streaming_job = streaming_job::Entity::find_by_id(sink.sink_id.as_job_id())
3207 .one(&txn)
3208 .await?;
3209 let (source, source_obj) = Source::find_by_id(source_id)
3210 .find_also_related(Object)
3211 .one(&txn)
3212 .await?
3213 .ok_or_else(|| {
3214 MetaError::catalog_id_not_found(ObjectType::Source.as_str(), source_id)
3215 })?;
3216 let (table, table_obj) = Table::find_by_id(table_id)
3217 .find_also_related(Object)
3218 .one(&txn)
3219 .await?
3220 .ok_or_else(|| MetaError::catalog_id_not_found(ObjectType::Table.as_str(), table_id))?;
3221 let table_streaming_job = streaming_job::Entity::find_by_id(table.job_id())
3222 .one(&txn)
3223 .await?;
3224 txn.commit().await?;
3225 let relation_infos = vec![
3226 PbObject {
3227 object_info: Some(PbObjectInfo::Sink(
3228 ObjectModel(sink, sink_obj.unwrap(), sink_streaming_job).into(),
3229 )),
3230 },
3231 PbObject {
3232 object_info: Some(PbObjectInfo::Source(
3233 ObjectModel(source, source_obj.unwrap(), None).into(),
3234 )),
3235 },
3236 PbObject {
3237 object_info: Some(PbObjectInfo::Table(
3238 ObjectModel(table, table_obj.unwrap(), table_streaming_job).into(),
3239 )),
3240 },
3241 ];
3242 let _version = self
3243 .notify_frontend(
3244 NotificationOperation::Update,
3245 NotificationInfo::ObjectGroup(PbObjectGroup {
3246 objects: relation_infos,
3247 dependencies: vec![],
3248 }),
3249 )
3250 .await;
3251
3252 Ok((props.into_iter().collect(), sink_id))
3253 }
3254
3255 pub async fn update_connection_and_dependent_objects_props(
3257 &self,
3258 connection_id: ConnectionId,
3259 alter_props: BTreeMap<String, String>,
3260 alter_secret_refs: BTreeMap<String, PbSecretRef>,
3261 ) -> MetaResult<(
3262 WithOptionsSecResolved, Vec<(SourceId, HashMap<String, String>)>, Vec<(SinkId, HashMap<String, String>)>, )> {
3266 let inner = self.inner.read().await;
3267 let txn = inner.db.begin().await?;
3268
3269 let dependent_sources: Vec<SourceId> = Source::find()
3271 .select_only()
3272 .column(source::Column::SourceId)
3273 .filter(source::Column::ConnectionId.eq(connection_id))
3274 .into_tuple()
3275 .all(&txn)
3276 .await?;
3277
3278 let dependent_sinks: Vec<SinkId> = Sink::find()
3279 .select_only()
3280 .column(sink::Column::SinkId)
3281 .filter(sink::Column::ConnectionId.eq(connection_id))
3282 .into_tuple()
3283 .all(&txn)
3284 .await?;
3285
3286 let (connection_catalog, _obj) = Connection::find_by_id(connection_id)
3287 .find_also_related(Object)
3288 .one(&txn)
3289 .await?
3290 .ok_or_else(|| {
3291 MetaError::catalog_id_not_found(ObjectType::Connection.as_str(), connection_id)
3292 })?;
3293
3294 let prop_keys: Vec<String> = alter_props
3296 .keys()
3297 .chain(alter_secret_refs.keys())
3298 .cloned()
3299 .collect();
3300
3301 let connection_type_str = pb_connection_type_to_connection_type(
3303 &connection_catalog.params.to_protobuf().connection_type(),
3304 )
3305 .ok_or_else(|| MetaError::invalid_parameter("Unspecified connection type"))?;
3306
3307 risingwave_connector::allow_alter_on_fly_fields::check_connection_allow_alter_on_fly_fields(
3308 connection_type_str, &prop_keys,
3309 )?;
3310
3311 let connection_pb = connection_catalog.params.to_protobuf();
3312 let mut connection_options_with_secret = WithOptionsSecResolved::new(
3313 connection_pb.properties.into_iter().collect(),
3314 connection_pb.secret_refs.into_iter().collect(),
3315 );
3316
3317 let (to_add_secret_dep, to_remove_secret_dep) = connection_options_with_secret
3318 .handle_update(alter_props.clone(), alter_secret_refs.clone())?;
3319
3320 tracing::debug!(
3321 "applying new properties to connection and dependents: connection_id={}, sources={:?}, sinks={:?}",
3322 connection_id,
3323 dependent_sources,
3324 dependent_sinks
3325 );
3326
3327 {
3329 let conn_params_pb = risingwave_pb::catalog::ConnectionParams {
3330 connection_type: connection_pb.connection_type,
3331 properties: connection_options_with_secret
3332 .as_plaintext()
3333 .clone()
3334 .into_iter()
3335 .collect(),
3336 secret_refs: connection_options_with_secret
3337 .as_secret()
3338 .clone()
3339 .into_iter()
3340 .collect(),
3341 };
3342 let connection = PbConnection {
3343 id: connection_id as _,
3344 info: Some(risingwave_pb::catalog::connection::Info::ConnectionParams(
3345 conn_params_pb,
3346 )),
3347 ..Default::default()
3348 };
3349 validate_connection(&connection).await?;
3350 }
3351
3352 if !to_add_secret_dep.is_empty() {
3354 ObjectDependency::insert_many(to_add_secret_dep.into_iter().map(|secret_id| {
3355 object_dependency::ActiveModel {
3356 oid: Set(secret_id.into()),
3357 used_by: Set(connection_id.as_object_id()),
3358 ..Default::default()
3359 }
3360 }))
3361 .exec(&txn)
3362 .await?;
3363 }
3364 if !to_remove_secret_dep.is_empty() {
3365 let _ = ObjectDependency::delete_many()
3366 .filter(
3367 object_dependency::Column::Oid
3368 .is_in(to_remove_secret_dep)
3369 .and(object_dependency::Column::UsedBy.eq(connection_id.as_object_id())),
3370 )
3371 .exec(&txn)
3372 .await?;
3373 }
3374
3375 let updated_connection_params = risingwave_pb::catalog::ConnectionParams {
3377 connection_type: connection_pb.connection_type,
3378 properties: connection_options_with_secret
3379 .as_plaintext()
3380 .clone()
3381 .into_iter()
3382 .collect(),
3383 secret_refs: connection_options_with_secret
3384 .as_secret()
3385 .clone()
3386 .into_iter()
3387 .collect(),
3388 };
3389 let active_connection_model = connection::ActiveModel {
3390 connection_id: Set(connection_id),
3391 params: Set(ConnectionParams::from(&updated_connection_params)),
3392 ..Default::default()
3393 };
3394 Connection::update(active_connection_model)
3395 .exec(&txn)
3396 .await?;
3397
3398 let mut updated_sources_with_props: Vec<(SourceId, HashMap<String, String>)> = Vec::new();
3400
3401 if !dependent_sources.is_empty() {
3402 let sources_with_objs = Source::find()
3404 .find_also_related(Object)
3405 .filter(source::Column::SourceId.is_in(dependent_sources.iter().cloned()))
3406 .all(&txn)
3407 .await?;
3408
3409 let mut source_updates = Vec::new();
3411 let mut fragment_updates: Vec<DependentSourceFragmentUpdate> = Vec::new();
3412
3413 for (source, _obj) in sources_with_objs {
3414 let source_id = source.source_id;
3415
3416 let mut source_options_with_secret = WithOptionsSecResolved::new(
3417 source.with_properties.0.clone(),
3418 source
3419 .secret_ref
3420 .clone()
3421 .map(|secret_ref| secret_ref.to_protobuf())
3422 .unwrap_or_default(),
3423 );
3424 let (source_to_add_secret_dep, source_to_remove_secret_dep) =
3425 source_options_with_secret
3426 .handle_update(alter_props.clone(), alter_secret_refs.clone())?;
3427
3428 let _ = ConnectorProperties::extract(source_options_with_secret.clone(), true)?;
3430
3431 let source_used_by_id = source
3434 .optional_associated_table_id
3435 .map(|table_id| table_id.as_object_id())
3436 .unwrap_or_else(|| source_id.as_object_id());
3437 if !source_to_add_secret_dep.is_empty() {
3438 ObjectDependency::insert_many(source_to_add_secret_dep.into_iter().map(
3439 |secret_id| object_dependency::ActiveModel {
3440 oid: Set(secret_id.into()),
3441 used_by: Set(source_used_by_id),
3442 ..Default::default()
3443 },
3444 ))
3445 .exec(&txn)
3446 .await?;
3447 }
3448 if !source_to_remove_secret_dep.is_empty() {
3449 let _ = ObjectDependency::delete_many()
3450 .filter(
3451 object_dependency::Column::Oid
3452 .is_in(source_to_remove_secret_dep)
3453 .and(object_dependency::Column::UsedBy.eq(source_used_by_id)),
3454 )
3455 .exec(&txn)
3456 .await?;
3457 }
3458
3459 let active_source = source::ActiveModel {
3461 source_id: Set(source_id),
3462 with_properties: Set(Property(
3463 source_options_with_secret.as_plaintext().clone(),
3464 )),
3465 secret_ref: Set((!source_options_with_secret.as_secret().is_empty()).then(
3466 || {
3467 risingwave_meta_model::SecretRef::from(
3468 source_options_with_secret.as_secret().clone(),
3469 )
3470 },
3471 )),
3472 ..Default::default()
3473 };
3474 source_updates.push(active_source);
3475
3476 let is_shared_source = source.is_shared();
3481 let mut dep_source_job_ids: Vec<JobId> = Vec::new();
3482 if !is_shared_source {
3483 dep_source_job_ids = ObjectDependency::find()
3484 .select_only()
3485 .column(object_dependency::Column::UsedBy)
3486 .filter(object_dependency::Column::Oid.eq(source_id))
3487 .into_tuple()
3488 .all(&txn)
3489 .await?;
3490 }
3491
3492 let base_job_id =
3493 if let Some(associate_table_id) = source.optional_associated_table_id {
3494 associate_table_id.as_job_id()
3495 } else {
3496 source_id.as_share_source_job_id()
3497 };
3498 let job_ids = vec![base_job_id]
3499 .into_iter()
3500 .chain(dep_source_job_ids)
3501 .collect_vec();
3502
3503 fragment_updates.push(DependentSourceFragmentUpdate {
3504 job_ids,
3505 with_properties: source_options_with_secret.as_plaintext().clone(),
3506 secret_refs: source_options_with_secret.as_secret().clone(),
3507 is_shared_source,
3508 });
3509
3510 let complete_source_props = LocalSecretManager::global()
3512 .fill_secrets(
3513 source_options_with_secret.as_plaintext().clone(),
3514 source_options_with_secret.as_secret().clone(),
3515 )
3516 .map_err(MetaError::from)?
3517 .into_iter()
3518 .collect::<HashMap<String, String>>();
3519 updated_sources_with_props.push((source_id, complete_source_props));
3520 }
3521
3522 for source_update in source_updates {
3523 Source::update(source_update).exec(&txn).await?;
3524 }
3525
3526 for DependentSourceFragmentUpdate {
3528 job_ids,
3529 with_properties,
3530 secret_refs,
3531 is_shared_source,
3532 } in fragment_updates
3533 {
3534 update_connector_props_fragments(
3535 &txn,
3536 job_ids,
3537 FragmentTypeFlag::Source,
3538 |node, found| {
3539 if let PbNodeBody::Source(node) = node
3540 && let Some(source_inner) = &mut node.source_inner
3541 {
3542 source_inner.with_properties = with_properties.clone();
3543 source_inner.secret_refs = secret_refs.clone();
3544 *found = true;
3545 }
3546 },
3547 is_shared_source,
3548 )
3549 .await?;
3550 }
3551 }
3552
3553 let mut updated_sinks_with_props: Vec<(SinkId, HashMap<String, String>)> = Vec::new();
3555
3556 if !dependent_sinks.is_empty() {
3557 let sinks_with_objs = Sink::find()
3559 .find_also_related(Object)
3560 .filter(sink::Column::SinkId.is_in(dependent_sinks.iter().cloned()))
3561 .all(&txn)
3562 .await?;
3563
3564 let mut sink_updates = Vec::new();
3566 let mut sink_fragment_updates = Vec::new();
3567
3568 for (sink, _obj) in sinks_with_objs {
3569 let sink_id = sink.sink_id;
3570
3571 match sink.properties.inner_ref().get(CONNECTOR_TYPE_KEY) {
3573 Some(connector) => {
3574 let connector_type = connector.to_lowercase();
3575 check_sink_allow_alter_on_fly_fields(&connector_type, &prop_keys)
3576 .map_err(|e| SinkError::Config(anyhow!(e)))?;
3577
3578 match_sink_name_str!(
3579 connector_type.as_str(),
3580 SinkType,
3581 {
3582 let mut new_sink_props = sink.properties.0.clone();
3583 new_sink_props.extend(alter_props.clone());
3584 SinkType::validate_alter_config(&new_sink_props)
3585 },
3586 |sink: &str| Err(SinkError::Config(anyhow!(
3587 "unsupported sink type {}",
3588 sink
3589 )))
3590 )?
3591 }
3592 None => {
3593 return Err(SinkError::Config(anyhow!(
3594 "connector not specified when alter sink"
3595 ))
3596 .into());
3597 }
3598 };
3599
3600 let mut new_sink_props = sink.properties.0.clone();
3601 new_sink_props.extend(alter_props.clone());
3602
3603 let active_sink = sink::ActiveModel {
3605 sink_id: Set(sink_id),
3606 properties: Set(risingwave_meta_model::Property(new_sink_props.clone())),
3607 ..Default::default()
3608 };
3609 sink_updates.push(active_sink);
3610
3611 sink_fragment_updates.push((sink_id, new_sink_props.clone()));
3613
3614 let complete_sink_props: HashMap<String, String> =
3616 new_sink_props.into_iter().collect();
3617 updated_sinks_with_props.push((sink_id, complete_sink_props));
3618 }
3619
3620 for sink_update in sink_updates {
3622 Sink::update(sink_update).exec(&txn).await?;
3623 }
3624
3625 for (sink_id, new_sink_props) in sink_fragment_updates {
3627 update_connector_props_fragments(
3628 &txn,
3629 vec![sink_id.as_job_id()],
3630 FragmentTypeFlag::Sink,
3631 |node, found| {
3632 if let PbNodeBody::Sink(node) = node
3633 && let Some(sink_desc) = &mut node.sink_desc
3634 && sink_desc.id == sink_id.as_raw_id()
3635 {
3636 sink_desc.properties = new_sink_props.clone();
3637 *found = true;
3638 }
3639 },
3640 true,
3641 )
3642 .await?;
3643 }
3644 }
3645
3646 let mut updated_objects = Vec::new();
3648
3649 let (connection, obj) = Connection::find_by_id(connection_id)
3651 .find_also_related(Object)
3652 .one(&txn)
3653 .await?
3654 .ok_or_else(|| {
3655 MetaError::catalog_id_not_found(ObjectType::Connection.as_str(), connection_id)
3656 })?;
3657 updated_objects.push(PbObject {
3658 object_info: Some(PbObjectInfo::Connection(
3659 ObjectModel(connection, obj.unwrap(), None).into(),
3660 )),
3661 });
3662
3663 for source_id in &dependent_sources {
3665 let (source, obj) = Source::find_by_id(*source_id)
3666 .find_also_related(Object)
3667 .one(&txn)
3668 .await?
3669 .ok_or_else(|| {
3670 MetaError::catalog_id_not_found(ObjectType::Source.as_str(), *source_id)
3671 })?;
3672 updated_objects.push(PbObject {
3673 object_info: Some(PbObjectInfo::Source(
3674 ObjectModel(source, obj.unwrap(), None).into(),
3675 )),
3676 });
3677 }
3678
3679 for sink_id in &dependent_sinks {
3681 let (sink, obj) = Sink::find_by_id(*sink_id)
3682 .find_also_related(Object)
3683 .one(&txn)
3684 .await?
3685 .ok_or_else(|| {
3686 MetaError::catalog_id_not_found(ObjectType::Sink.as_str(), *sink_id)
3687 })?;
3688 let streaming_job = streaming_job::Entity::find_by_id(sink.sink_id.as_job_id())
3689 .one(&txn)
3690 .await?;
3691 updated_objects.push(PbObject {
3692 object_info: Some(PbObjectInfo::Sink(
3693 ObjectModel(sink, obj.unwrap(), streaming_job).into(),
3694 )),
3695 });
3696 }
3697
3698 txn.commit().await?;
3700
3701 if !updated_objects.is_empty() {
3703 self.notify_frontend(
3704 NotificationOperation::Update,
3705 NotificationInfo::ObjectGroup(PbObjectGroup {
3706 objects: updated_objects,
3707 dependencies: vec![],
3708 }),
3709 )
3710 .await;
3711 }
3712
3713 Ok((
3714 connection_options_with_secret,
3715 updated_sources_with_props,
3716 updated_sinks_with_props,
3717 ))
3718 }
3719
3720 pub async fn update_fragment_rate_limit_by_fragment_id(
3721 &self,
3722 fragment_id: FragmentId,
3723 throttle_type: ThrottleType,
3724 rate_limit: Option<u32>,
3725 ) -> MetaResult<PbStreamNode> {
3726 let update_rate_limit = |fragment_type_mask: FragmentTypeMask,
3727 stream_node: &mut PbStreamNode| {
3728 let mut found = Ok(false);
3729 match throttle_type {
3730 ThrottleType::Source => {
3731 visit_stream_node_mut(stream_node, |node| match node {
3732 PbNodeBody::Source(node) => {
3733 if let Some(node_inner) = &mut node.source_inner {
3734 node_inner.rate_limit = rate_limit;
3735 found = Ok(true);
3736 }
3737 }
3738 PbNodeBody::StreamFsFetch(node) => {
3739 if let Some(node_inner) = &mut node.node_inner {
3740 node_inner.rate_limit = rate_limit;
3741 found = Ok(true);
3742 }
3743 }
3744 _ => {}
3745 });
3746 }
3747 ThrottleType::Backfill => {
3748 if fragment_type_mask
3749 .contains_any(FragmentTypeFlag::backfill_rate_limit_fragments())
3750 {
3751 visit_stream_node_mut(stream_node, |node| match node {
3752 PbNodeBody::StreamCdcScan(node) => {
3753 node.rate_limit = rate_limit;
3754 found = Ok(true);
3755 }
3756 PbNodeBody::StreamScan(node) => {
3757 node.rate_limit = rate_limit;
3758 found = Ok(true);
3759 }
3760 PbNodeBody::SourceBackfill(node) => {
3761 node.rate_limit = rate_limit;
3762 found = Ok(true);
3763 }
3764 _ => {}
3765 });
3766 }
3767 }
3768 ThrottleType::Sink => {
3769 if fragment_type_mask
3770 .contains_any(FragmentTypeFlag::sink_rate_limit_fragments())
3771 {
3772 visit_stream_node_mut(stream_node, |node| {
3773 if found.is_err() {
3774 return;
3775 }
3776 match update_sink_node_rate_limit(node, rate_limit) {
3777 Ok(true) => found = Ok(true),
3778 Ok(false) => {}
3779 Err(err) => found = Err(err),
3780 }
3781 });
3782 }
3783 }
3784 ThrottleType::Dml => {
3785 if fragment_type_mask.contains_any(FragmentTypeFlag::dml_rate_limit_fragments())
3786 {
3787 visit_stream_node_mut(stream_node, |node| {
3788 if let PbNodeBody::Dml(node) = node {
3789 node.rate_limit = rate_limit;
3790 found = Ok(true);
3791 }
3792 });
3793 }
3794 }
3795 ThrottleType::Unspecified => {}
3796 }
3797 found
3798 };
3799 self.mutate_fragment_by_fragment_id(
3800 fragment_id,
3801 update_rate_limit,
3802 "rate limit node not found",
3803 )
3804 .await
3805 }
3806
3807 pub async fn list_rate_limits(&self) -> MetaResult<Vec<RateLimitInfo>> {
3810 let inner = self.inner.read().await;
3811 let txn = inner.db.begin().await?;
3812
3813 let fragments: Vec<(FragmentId, JobId, i32, StreamNode)> = Fragment::find()
3814 .select_only()
3815 .columns([
3816 fragment::Column::FragmentId,
3817 fragment::Column::JobId,
3818 fragment::Column::FragmentTypeMask,
3819 fragment::Column::StreamNode,
3820 ])
3821 .filter(FragmentTypeMask::intersects_any(
3822 FragmentTypeFlag::rate_limit_fragments(),
3823 ))
3824 .into_tuple()
3825 .all(&txn)
3826 .await?;
3827
3828 let mut rate_limits = Vec::new();
3829 for (fragment_id, job_id, fragment_type_mask, stream_node) in fragments {
3830 let stream_node = stream_node.to_protobuf();
3831 visit_stream_node_body(&stream_node, |node| {
3832 let mut rate_limit = None;
3833 let mut node_name = None;
3834
3835 match node {
3836 PbNodeBody::Source(node) => {
3838 if let Some(node_inner) = &node.source_inner {
3839 rate_limit = node_inner.rate_limit;
3840 node_name = Some("SOURCE");
3841 }
3842 }
3843 PbNodeBody::StreamFsFetch(node) => {
3844 if let Some(node_inner) = &node.node_inner {
3845 rate_limit = node_inner.rate_limit;
3846 node_name = Some("FS_FETCH");
3847 }
3848 }
3849 PbNodeBody::SourceBackfill(node) => {
3851 rate_limit = node.rate_limit;
3852 node_name = Some("SOURCE_BACKFILL");
3853 }
3854 PbNodeBody::StreamScan(node) => {
3855 rate_limit = node.rate_limit;
3856 node_name = Some("STREAM_SCAN");
3857 }
3858 PbNodeBody::StreamCdcScan(node) => {
3859 rate_limit = node.rate_limit;
3860 node_name = Some("STREAM_CDC_SCAN");
3861 }
3862 PbNodeBody::Sink(node) => {
3863 rate_limit = node.rate_limit;
3864 node_name = Some("SINK");
3865 }
3866 _ => {}
3867 }
3868
3869 if let Some(rate_limit) = rate_limit {
3870 rate_limits.push(RateLimitInfo {
3871 fragment_id,
3872 job_id,
3873 fragment_type_mask: fragment_type_mask as u32,
3874 rate_limit,
3875 node_name: node_name.unwrap().to_owned(),
3876 });
3877 }
3878 });
3879 }
3880
3881 Ok(rate_limits)
3882 }
3883}
3884
3885fn validate_sink_props(sink: &sink::Model, props: &BTreeMap<String, String>) -> MetaResult<()> {
3886 match sink.properties.inner_ref().get(CONNECTOR_TYPE_KEY) {
3888 Some(connector) => {
3889 let connector_type = connector.to_lowercase();
3890 let field_names: Vec<String> = props.keys().cloned().collect();
3891 check_sink_allow_alter_on_fly_fields(&connector_type, &field_names)
3892 .map_err(|e| SinkError::Config(anyhow!(e)))?;
3893
3894 match_sink_name_str!(
3895 connector_type.as_str(),
3896 SinkType,
3897 {
3898 let mut new_props = sink.properties.0.clone();
3899 new_props.extend(props.clone());
3900 SinkType::validate_alter_config(&new_props)
3901 },
3902 |sink: &str| Err(SinkError::Config(anyhow!("unsupported sink type {}", sink)))
3903 )?
3904 }
3905 None => {
3906 return Err(
3907 SinkError::Config(anyhow!("connector not specified when alter sink")).into(),
3908 );
3909 }
3910 };
3911 Ok(())
3912}
3913
3914fn update_stmt_with_props(
3915 with_properties: &mut Vec<SqlOption>,
3916 props: &BTreeMap<String, String>,
3917) -> MetaResult<()> {
3918 let mut new_sql_options = with_properties
3919 .iter()
3920 .map(|sql_option| (&sql_option.name, sql_option))
3921 .collect::<IndexMap<_, _>>();
3922 let add_sql_options = props
3923 .iter()
3924 .map(|(k, v)| SqlOption::try_from((k, v)))
3925 .collect::<Result<Vec<SqlOption>, ParserError>>()
3926 .map_err(|e| SinkError::Config(anyhow!(e)))?;
3927 new_sql_options.extend(
3928 add_sql_options
3929 .iter()
3930 .map(|sql_option| (&sql_option.name, sql_option)),
3931 );
3932 *with_properties = new_sql_options.into_values().cloned().collect();
3933 Ok(())
3934}
3935
3936async fn update_sink_fragment_props(
3937 txn: &DatabaseTransaction,
3938 sink_id: SinkId,
3939 props: BTreeMap<String, String>,
3940) -> MetaResult<()> {
3941 let fragments: Vec<(FragmentId, i32, StreamNode)> = Fragment::find()
3942 .select_only()
3943 .columns([
3944 fragment::Column::FragmentId,
3945 fragment::Column::FragmentTypeMask,
3946 fragment::Column::StreamNode,
3947 ])
3948 .filter(fragment::Column::JobId.eq(sink_id))
3949 .into_tuple()
3950 .all(txn)
3951 .await?;
3952 let fragments = fragments
3953 .into_iter()
3954 .filter(|(_, fragment_type_mask, _)| {
3955 *fragment_type_mask & FragmentTypeFlag::Sink as i32 != 0
3956 })
3957 .filter_map(|(id, _, stream_node)| {
3958 let mut stream_node = stream_node.to_protobuf();
3959 let mut found = false;
3960 visit_stream_node_mut(&mut stream_node, |node| {
3961 if let PbNodeBody::Sink(node) = node
3962 && let Some(sink_desc) = &mut node.sink_desc
3963 && sink_desc.id == sink_id
3964 {
3965 sink_desc.properties.extend(props.clone());
3966 found = true;
3967 }
3968 });
3969 if found { Some((id, stream_node)) } else { None }
3970 })
3971 .collect_vec();
3972 assert!(
3973 !fragments.is_empty(),
3974 "sink id should be used by at least one fragment"
3975 );
3976 for (id, stream_node) in fragments {
3977 Fragment::update(fragment::ActiveModel {
3978 fragment_id: Set(id),
3979 stream_node: Set(StreamNode::from(&stream_node)),
3980 ..Default::default()
3981 })
3982 .exec(txn)
3983 .await?;
3984 }
3985 Ok(())
3986}
3987
3988pub struct SinkIntoTableContext {
3989 pub updated_sink_catalogs: Vec<SinkId>,
3992}
3993
3994pub struct FinishAutoRefreshSchemaSinkContext {
3995 pub tmp_sink_id: SinkId,
3996 pub original_sink_id: SinkId,
3997 pub columns: Vec<PbColumnCatalog>,
3998 pub new_log_store_table: Option<Box<PbTable>>,
3999}
4000
4001async fn update_connector_props_fragments<F>(
4002 txn: &DatabaseTransaction,
4003 job_ids: Vec<JobId>,
4004 expect_flag: FragmentTypeFlag,
4005 mut alter_stream_node_fn: F,
4006 is_shared_source: bool,
4007) -> MetaResult<()>
4008where
4009 F: FnMut(&mut PbNodeBody, &mut bool),
4010{
4011 let fragments: Vec<(FragmentId, StreamNode)> = Fragment::find()
4012 .select_only()
4013 .columns([fragment::Column::FragmentId, fragment::Column::StreamNode])
4014 .filter(
4015 fragment::Column::JobId
4016 .is_in(job_ids.clone())
4017 .and(FragmentTypeMask::intersects(expect_flag)),
4018 )
4019 .into_tuple()
4020 .all(txn)
4021 .await?;
4022 let fragments = fragments
4023 .into_iter()
4024 .filter_map(|(id, stream_node)| {
4025 let mut stream_node = stream_node.to_protobuf();
4026 let mut found = false;
4027 visit_stream_node_mut(&mut stream_node, |node| {
4028 alter_stream_node_fn(node, &mut found);
4029 });
4030 if found { Some((id, stream_node)) } else { None }
4031 })
4032 .collect_vec();
4033 if is_shared_source || job_ids.len() > 1 {
4034 assert!(
4038 !fragments.is_empty(),
4039 "job ids {:?} (type: {:?}) should be used by at least one fragment",
4040 job_ids,
4041 expect_flag
4042 );
4043 }
4044
4045 for (id, stream_node) in fragments {
4046 Fragment::update(fragment::ActiveModel {
4047 fragment_id: Set(id),
4048 stream_node: Set(StreamNode::from(&stream_node)),
4049 ..Default::default()
4050 })
4051 .exec(txn)
4052 .await?;
4053 }
4054
4055 Ok(())
4056}