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