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