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