1#[cfg(test)]
16mod tests {
17 use std::collections::BTreeMap;
18
19 use risingwave_common::catalog::{FragmentTypeFlag, FragmentTypeMask};
20 use risingwave_common::hash::VirtualNode;
21 use risingwave_meta_model::FragmentId;
22 use risingwave_meta_model::fragment::DistributionType;
23 use risingwave_meta_model::table::HandleConflictBehavior;
24 use risingwave_pb::catalog::subscription::SubscriptionState;
25 use risingwave_pb::catalog::{PbSinkType, StreamSourceInfo};
26 use risingwave_pb::common::{HostAddress, WorkerNode, WorkerType, worker_node};
27 use risingwave_pb::meta::SubscribeType;
28 use risingwave_pb::meta::table_fragments::fragment::PbFragmentDistributionType;
29 use risingwave_pb::plan_common::ExternalTableDesc;
30 use risingwave_pb::stream_plan::stream_node::PbNodeBody;
31 use risingwave_pb::stream_plan::{PbStreamNode, StreamScanNode, StreamScanType};
32 use tokio::sync::{mpsc, oneshot};
33
34 use crate::barrier::Command;
35 use crate::controller::catalog::*;
36 use crate::manager::{LocalNotification, MetaOpts, WorkerKey};
37 use crate::model::{Fragment, FragmentDownstreamRelation};
38 use crate::serving::ServingVnodeMapping;
39
40 const TEST_DATABASE_ID: DatabaseId = DatabaseId::new(1);
41 const TEST_SCHEMA_ID: SchemaId = SchemaId::new(2);
42 const TEST_OWNER_ID: UserId = UserId::new(1);
43
44 async fn insert_test_table(
45 txn: &DatabaseTransaction,
46 table_id: TableId,
47 name: &str,
48 table_type: TableType,
49 belongs_to_job_id: Option<JobId>,
50 definition: &str,
51 ) -> MetaResult<()> {
52 table::ActiveModel {
53 table_id: Set(table_id),
54 name: Set(name.to_owned()),
55 optional_associated_source_id: Set(None),
56 table_type: Set(table_type),
57 belongs_to_job_id: Set(belongs_to_job_id),
58 columns: Set(vec![].into()),
59 pk: Set(vec![].into()),
60 distribution_key: Set(Vec::<i32>::new().into()),
61 stream_key: Set(Vec::<i32>::new().into()),
62 append_only: Set(false),
63 fragment_id: Set(None),
64 vnode_col_index: Set(None),
65 row_id_index: Set(None),
66 value_indices: Set(Vec::<i32>::new().into()),
67 definition: Set(definition.to_owned()),
68 handle_pk_conflict_behavior: Set(HandleConflictBehavior::NoCheck),
69 version_column_indices: Set(None),
70 read_prefix_len_hint: Set(0),
71 watermark_indices: Set(Vec::<i32>::new().into()),
72 dist_key_in_pk: Set(Vec::<i32>::new().into()),
73 dml_fragment_id: Set(None),
74 cardinality: Set(None),
75 cleaned_by_watermark: Set(false),
76 description: Set(None),
77 version: Set(None),
78 retention_seconds: Set(None),
79 cdc_table_id: Set(None),
80 vnode_count: Set(1),
81 webhook_info: Set(None),
82 engine: Set(None),
83 clean_watermark_index_in_pk: Set(None),
84 clean_watermark_indices: Set(None),
85 refreshable: Set(false),
86 vector_index_info: Set(None),
87 cdc_table_type: Set(None),
88 }
89 .insert(txn)
90 .await?;
91 Ok(())
92 }
93
94 async fn insert_test_fragment(
95 txn: &DatabaseTransaction,
96 fragment_id: FragmentId,
97 job_id: JobId,
98 state_table_ids: TableIdArray,
99 ) -> MetaResult<()> {
100 fragment::ActiveModel {
101 fragment_id: Set(fragment_id),
102 job_id: Set(job_id),
103 fragment_type_mask: Set(0),
104 distribution_type: Set(fragment::DistributionType::Hash),
105 stream_node: Set(StreamNode::from(&PbStreamNode::default())),
106 state_table_ids: Set(state_table_ids),
107 upstream_fragment_id: Set(I32Array::default()),
108 vnode_count: Set(1),
109 parallelism: Set(None),
110 }
111 .insert(txn)
112 .await?;
113 Ok(())
114 }
115
116 async fn insert_test_streaming_job(
117 txn: &DatabaseTransaction,
118 name: &str,
119 has_result_table: bool,
120 policy: Option<CacheRefillPolicy>,
121 ) -> MetaResult<(JobId, Option<TableId>, TableId)> {
122 let object_type = if has_result_table {
123 ObjectType::Table
124 } else {
125 ObjectType::Sink
126 };
127 let job_id = CatalogController::create_object(
128 txn,
129 object_type,
130 TEST_OWNER_ID,
131 Some(TEST_SCHEMA_ID.as_object_id()),
132 )
133 .await?
134 .oid
135 .as_job_id();
136 let result_table_id = has_result_table.then_some(job_id.as_mv_table_id());
137 if let Some(table_id) = result_table_id {
138 insert_test_table(txn, table_id, name, TableType::MaterializedView, None, "").await?;
139 }
140
141 let internal_table_id = CatalogController::create_object(
142 txn,
143 ObjectType::Table,
144 TEST_OWNER_ID,
145 Some(job_id.as_object_id()),
146 )
147 .await?
148 .oid
149 .as_table_id();
150 insert_test_table(
151 txn,
152 internal_table_id,
153 &format!("__internal_{name}"),
154 TableType::Internal,
155 Some(job_id),
156 "",
157 )
158 .await?;
159
160 insert_test_streaming_job_model(txn, job_id, policy).await?;
161
162 Ok((job_id, result_table_id, internal_table_id))
163 }
164
165 async fn insert_test_streaming_job_model(
166 txn: &DatabaseTransaction,
167 job_id: JobId,
168 policy: Option<CacheRefillPolicy>,
169 ) -> MetaResult<()> {
170 streaming_job::ActiveModel {
171 job_id: Set(job_id),
172 job_status: Set(JobStatus::Created),
173 create_type: Set(CreateType::Foreground),
174 timezone: Set(None),
175 config_override: Set(policy.map(|policy| {
176 format!(
177 "[streaming.developer]\ncache_refill_policy = \"{}\"\n",
178 policy
179 )
180 })),
181 adaptive_parallelism_strategy: Set(None),
182 parallelism: Set(StreamingParallelism::Adaptive),
183 backfill_parallelism: Set(None),
184 backfill_adaptive_parallelism_strategy: Set(None),
185 backfill_orders: Set(None),
186 max_parallelism: Set(1),
187 specific_resource_group: Set(None),
188 is_serverless_backfill: Set(false),
189 refresh_interval_sec: Set(None),
190 }
191 .insert(txn)
192 .await?;
193
194 Ok(())
195 }
196
197 #[tokio::test]
198 async fn test_cancel_creating_job_includes_belonging_streaming_jobs() -> MetaResult<()> {
199 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
200 let mut inner = mgr.inner.write().await;
201 let txn = inner.db.begin().await?;
202
203 let table_job_id = CatalogController::create_object(
204 &txn,
205 ObjectType::Table,
206 TEST_OWNER_ID,
207 Some(TEST_SCHEMA_ID.as_object_id()),
208 )
209 .await?
210 .oid
211 .as_job_id();
212 insert_test_table(
213 &txn,
214 table_job_id.as_mv_table_id(),
215 "cancel_table",
216 TableType::Table,
217 None,
218 "",
219 )
220 .await?;
221 let sink_job_id = CatalogController::create_object(
222 &txn,
223 ObjectType::Sink,
224 TEST_OWNER_ID,
225 Some(table_job_id.as_object_id()),
226 )
227 .await?
228 .oid
229 .as_job_id();
230 Sink::insert(sink::ActiveModel::from(PbSink {
231 id: sink_job_id.as_sink_id(),
232 schema_id: TEST_SCHEMA_ID,
233 database_id: TEST_DATABASE_ID,
234 name: "cancel_sink".to_owned(),
235 owner: TEST_OWNER_ID as _,
236 sink_type: PbSinkType::AppendOnly as i32,
237 ..Default::default()
238 }))
239 .exec(&txn)
240 .await?;
241 for job_id in [table_job_id, sink_job_id] {
242 insert_test_streaming_job_model(&txn, job_id, None).await?;
243 StreamingJob::update(streaming_job::ActiveModel {
244 job_id: Set(job_id),
245 job_status: Set(JobStatus::Creating),
246 ..Default::default()
247 })
248 .exec(&txn)
249 .await?;
250 }
251
252 let table_state_id = TableId::new(1000);
253 let sink_state_id = TableId::new(1001);
254 insert_test_fragment(
255 &txn,
256 FragmentId::new(100),
257 table_job_id,
258 TableIdArray(vec![table_state_id]),
259 )
260 .await?;
261 insert_test_fragment(
262 &txn,
263 FragmentId::new(101),
264 sink_job_id,
265 TableIdArray(vec![sink_state_id]),
266 )
267 .await?;
268 let (table_finish_tx, table_finish_rx) = oneshot::channel();
269 inner.register_finish_notifier(TEST_DATABASE_ID, table_job_id, table_finish_tx);
270 let (sink_finish_tx, sink_finish_rx) = oneshot::channel();
271 inner.register_finish_notifier(TEST_DATABASE_ID, sink_job_id, sink_finish_tx);
272 txn.commit().await?;
273 drop(inner);
274
275 let abort_result = mgr
276 .try_abort_creating_streaming_job(table_job_id, true)
277 .await?;
278 assert!(abort_result.aborted);
279 assert_eq!(
280 abort_result.aborted_sink_ids,
281 vec![sink_job_id.as_sink_id()]
282 );
283 let cancel_info = abort_result
284 .cancel_info
285 .expect("cancelled table job should have cleanup information");
286 assert_eq!(
287 cancel_info
288 .streaming_job_ids
289 .iter()
290 .copied()
291 .collect::<HashSet<_>>(),
292 HashSet::from([table_job_id, sink_job_id])
293 );
294 assert_eq!(
295 cancel_info
296 .state_table_ids
297 .iter()
298 .copied()
299 .collect::<HashSet<_>>(),
300 HashSet::from([table_state_id, sink_state_id])
301 );
302 let Command::DropStreamingJobs {
303 streaming_job_ids,
304 unregistered_state_table_ids,
305 ..
306 } = cancel_info.command
307 else {
308 unreachable!()
309 };
310 assert_eq!(
311 streaming_job_ids,
312 HashSet::from([table_job_id, sink_job_id])
313 );
314 assert_eq!(
315 unregistered_state_table_ids,
316 HashSet::from([table_state_id, sink_state_id])
317 );
318
319 for finish_rx in [table_finish_rx, sink_finish_rx] {
320 let err = finish_rx
321 .await
322 .expect("aborted job should notify its finish waiter")
323 .expect_err("aborted job should not finish successfully");
324 assert!(err.contains("cancelled"));
325 }
326 let db = &mgr.inner.read().await.db;
327 assert!(Object::find_by_id(table_job_id).one(db).await?.is_none());
328 assert!(Object::find_by_id(sink_job_id).one(db).await?.is_none());
329
330 Ok(())
331 }
332
333 #[tokio::test]
334 async fn test_create_multiple_sinks_into_same_table_and_drop_table() -> MetaResult<()> {
335 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
336 let inner = mgr.inner.write().await;
337 let txn = inner.db.begin().await?;
338 let (_, Some(target_table_id), _) =
339 insert_test_streaming_job(&txn, "mvt", true, None).await?
340 else {
341 unreachable!()
342 };
343 let (mv1_id, Some(_), _) = insert_test_streaming_job(&txn, "mv1", true, None).await? else {
344 unreachable!()
345 };
346 let (mv2_id, Some(_), _) = insert_test_streaming_job(&txn, "mv2", true, None).await? else {
347 unreachable!()
348 };
349 txn.commit().await?;
350 drop(inner);
351
352 let mut sink_ids = Vec::new();
353 let test_sink_tuples = [("s1", mv1_id), ("s2", mv2_id)];
354
355 fn assert_incoming_sink_drop_error<T>(error: &MetaError, test_sink_tuples: &[(&str, T)]) {
356 let message = error.to_string();
357
358 assert!(
359 message.contains("sink") && message.contains("depends on it"),
360 "expected an incoming-sink dependency error, got: {message}"
361 );
362
363 assert!(
364 test_sink_tuples
365 .iter()
366 .all(|(sink_name, _)| message.contains(*sink_name)),
367 "expected the error to mention all incoming sinks, got: {message}"
368 );
369 }
370
371 for (name, source) in test_sink_tuples {
372 let mut job = crate::manager::StreamingJob::Sink(
373 PbSink {
374 name: name.to_owned(),
375 database_id: TEST_DATABASE_ID,
376 schema_id: TEST_SCHEMA_ID,
377 owner: TEST_OWNER_ID as _,
378 target_table: Some(target_table_id),
379 sink_type: PbSinkType::AppendOnly as i32,
380 ..Default::default()
381 },
382 None,
383 );
384 tokio::time::timeout(
388 std::time::Duration::from_secs(3),
389 mgr.create_job_catalog(
390 &mut job,
391 &crate::model::StreamContext::default(),
392 &None,
393 1,
394 HashSet::from([source.as_object_id()]),
395 risingwave_pb::ddl_service::streaming_job_resource_type::ResourceType::Regular(
396 true,
397 ),
398 &None,
399 None,
400 None,
401 None,
402 None,
403 ),
404 )
405 .await
406 .expect("creating a second sink into the same table should not hang")?;
407 sink_ids.push(job.id().as_object_id());
408 }
409 let owned_sink_name = "owned_sink_without_iceberg_prefix";
410 let mut owned_sink = crate::manager::StreamingJob::Sink(
411 PbSink {
412 name: owned_sink_name.to_owned(),
413 database_id: TEST_DATABASE_ID,
414 schema_id: TEST_SCHEMA_ID,
415 owner: TEST_OWNER_ID as _,
416 target_table: Some(target_table_id),
417 sink_type: PbSinkType::AppendOnly as i32,
418 ..Default::default()
419 },
420 Some(target_table_id),
421 );
422 mgr.create_job_catalog(
423 &mut owned_sink,
424 &crate::model::StreamContext::default(),
425 &None,
426 1,
427 HashSet::from([mv1_id.as_object_id()]),
428 risingwave_pb::ddl_service::streaming_job_resource_type::ResourceType::Regular(true),
429 &None,
430 None,
431 None,
432 None,
433 None,
434 )
435 .await?;
436 let owned_sink_id = owned_sink.id().as_object_id();
437 sink_ids.push(owned_sink_id);
438
439 assert_eq!(sink_ids.len(), test_sink_tuples.len() + 1);
441
442 let inner = mgr.inner.read().await;
443 for sink_id in &sink_ids {
444 streaming_job::ActiveModel {
445 job_id: Set(sink_id.as_job_id()),
446 job_status: Set(JobStatus::Created),
447 ..Default::default()
448 }
449 .update(&inner.db)
450 .await?;
451 }
452 let object_dependency_count = ObjectDependency::find()
455 .filter(object_dependency::Column::Oid.eq(target_table_id.as_object_id()))
456 .filter(object_dependency::Column::UsedBy.is_in(sink_ids.clone()))
457 .count(&inner.db)
458 .await?;
459 assert_eq!(object_dependency_count, 0);
460 assert_eq!(
461 Object::find_by_id(owned_sink_id)
462 .one(&inner.db)
463 .await?
464 .unwrap()
465 .belong_to_oid,
466 Some(target_table_id.as_object_id())
467 );
468 drop(inner);
469
470 let error = mgr
471 .drop_object(ObjectType::Table, target_table_id, DropMode::Restrict)
472 .await
473 .expect_err("RESTRICT drop should fail for a table with incoming sinks");
474 assert_incoming_sink_drop_error(&error, &test_sink_tuples);
475 assert!(
476 !error.to_string().contains(owned_sink_name),
477 "an owned incoming sink should not prevent a RESTRICT drop"
478 );
479 mgr.drop_object(ObjectType::Table, target_table_id, DropMode::Cascade)
480 .await
481 .expect("CASCADE drop should succeed");
482
483 let inner = mgr.inner.read().await;
484 let db = &inner.db;
485 assert!(Object::find_by_id(target_table_id).one(db).await?.is_none());
487 assert_eq!(
488 Object::find()
489 .filter(object::Column::Oid.is_in(sink_ids))
490 .count(db)
491 .await?,
492 0
493 );
494 assert!(Object::find_by_id(mv1_id).one(db).await?.is_some());
496 assert!(Object::find_by_id(mv2_id).one(db).await?.is_some());
497
498 Ok(())
499 }
500
501 #[tokio::test]
502 async fn test_replace_upstream_object_rejects_creating_incoming_sink() -> MetaResult<()> {
503 fn assert_replace_concurrency_error(error: &MetaError) {
504 let message = error.to_string();
505 assert!(
507 message.contains("referenced by some creating jobs"),
508 "expected a replace concurrency error, got: {message}"
509 );
510 }
511
512 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
513 let inner = mgr.inner.write().await;
514 let txn = inner.db.begin().await?;
515 let (target_mv_id, Some(target_mv_table_id), _) =
516 insert_test_streaming_job(&txn, "target_mv", true, None).await?
517 else {
518 unreachable!()
519 };
520 let (source_mv_id, Some(_), _) =
521 insert_test_streaming_job(&txn, "source_mv", true, None).await?
522 else {
523 unreachable!()
524 };
525 txn.commit().await?;
526 drop(inner);
527
528 let mut sink = crate::manager::StreamingJob::Sink(
529 PbSink {
530 name: "creating_sink".to_owned(),
531 database_id: TEST_DATABASE_ID,
532 schema_id: TEST_SCHEMA_ID,
533 owner: TEST_OWNER_ID as _,
534 target_table: Some(target_mv_table_id),
535 sink_type: PbSinkType::AppendOnly as i32,
536 ..Default::default()
537 },
538 None,
539 );
540 let creating_sink = mgr
541 .create_job_catalog(
542 &mut sink,
543 &crate::model::StreamContext::default(),
544 &None,
545 1,
546 HashSet::from([source_mv_id.as_object_id()]),
547 risingwave_pb::ddl_service::streaming_job_resource_type::ResourceType::Regular(
548 true,
549 ),
550 &None,
551 None,
552 None,
553 None,
554 None,
555 )
556 .await?;
557 assert_ne!(creating_sink.job_status, JobStatus::Created);
559
560 let replacement = crate::manager::StreamingJob::MaterializedView(PbTable {
561 id: target_mv_table_id,
562 name: "target_mv".to_owned(),
563 database_id: TEST_DATABASE_ID,
564 schema_id: TEST_SCHEMA_ID,
565 owner: TEST_OWNER_ID as _,
566 ..Default::default()
567 });
568 assert_eq!(replacement.id(), target_mv_id);
570
571 let error = mgr
573 .create_job_catalog_for_replace(&replacement, None, None, None)
574 .await
575 .expect_err("replacement should reject a creating incoming sink");
576 assert_replace_concurrency_error(&error);
578
579 Ok(())
580 }
581
582 #[tokio::test]
583 async fn test_replace_upstream_object_with_created_incoming_sink() -> MetaResult<()> {
584 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
585 let inner = mgr.inner.write().await;
586 let txn = inner.db.begin().await?;
587 let (_, Some(target_table_id), _) =
588 insert_test_streaming_job(&txn, "target_table", true, None).await?
589 else {
590 unreachable!()
591 };
592 let (source_mv_id, Some(source_mv_table_id), _) =
593 insert_test_streaming_job(&txn, "source_mv", true, None).await?
594 else {
595 unreachable!()
596 };
597 txn.commit().await?;
598 drop(inner);
599
600 let mut sink = crate::manager::StreamingJob::Sink(
601 PbSink {
602 name: "created_sink".to_owned(),
603 database_id: TEST_DATABASE_ID,
604 schema_id: TEST_SCHEMA_ID,
605 owner: TEST_OWNER_ID as _,
606 target_table: Some(target_table_id),
607 sink_type: PbSinkType::AppendOnly as i32,
608 ..Default::default()
609 },
610 None,
611 );
612 let creating_sink = mgr
613 .create_job_catalog(
614 &mut sink,
615 &crate::model::StreamContext::default(),
616 &None,
617 1,
618 HashSet::from([source_mv_id.as_object_id()]),
619 risingwave_pb::ddl_service::streaming_job_resource_type::ResourceType::Regular(
620 true,
621 ),
622 &None,
623 None,
624 None,
625 None,
626 None,
627 )
628 .await?;
629 assert_ne!(creating_sink.job_status, JobStatus::Created);
631
632 let sink_id = sink.id();
633 let inner = mgr.inner.read().await;
634 streaming_job::ActiveModel {
635 job_id: Set(sink_id),
636 job_status: Set(JobStatus::Created),
637 ..Default::default()
638 }
639 .update(&inner.db)
640 .await?;
641 let sink_model = risingwave_meta_model::prelude::StreamingJob::find_by_id(sink_id)
642 .one(&inner.db)
643 .await?
644 .expect("sink should exist");
645 assert_eq!(sink_model.job_id, sink_id);
647 assert_eq!(sink_model.job_status, JobStatus::Created);
649 drop(inner);
650
651 let replacement = crate::manager::StreamingJob::MaterializedView(PbTable {
652 id: source_mv_table_id,
653 name: "source_mv".to_owned(),
654 database_id: TEST_DATABASE_ID,
655 schema_id: TEST_SCHEMA_ID,
656 owner: TEST_OWNER_ID as _,
657 ..Default::default()
658 });
659 assert_eq!(replacement.id(), source_mv_id);
661
662 let tmp_model = mgr
663 .create_job_catalog_for_replace(&replacement, None, None, None)
664 .await?;
665
666 assert_ne!(tmp_model.job_id, source_mv_id);
668 assert_eq!(tmp_model.job_status, JobStatus::Initial);
670
671 let inner = mgr.inner.read().await;
672 let db = &inner.db;
673 assert_eq!(
675 ObjectDependency::find()
676 .filter(object_dependency::Column::Oid.eq(source_mv_id.as_object_id()))
677 .filter(object_dependency::Column::UsedBy.eq(sink_id.as_object_id()))
678 .count(db)
679 .await?,
680 1
681 );
682 assert_eq!(
684 ObjectDependency::find()
685 .filter(object_dependency::Column::Oid.eq(source_mv_id.as_object_id()))
686 .filter(object_dependency::Column::UsedBy.eq(tmp_model.job_id.as_object_id()))
687 .count(db)
688 .await?,
689 1
690 );
691
692 Ok(())
693 }
694
695 #[tokio::test]
696 async fn test_table_refill_catalog_snapshot_classifies_table_identity() -> MetaResult<()> {
697 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
698 let inner = mgr.inner.write().await;
699 let txn = inner.db.begin().await?;
700
701 let (mv_job, Some(mv_result), mv_internal) =
702 insert_test_streaming_job(&txn, "mv_both", true, Some(CacheRefillPolicy::Both)).await?
703 else {
704 unreachable!()
705 };
706 let (default_job, Some(_default_result), default_internal) =
707 insert_test_streaming_job(&txn, "mv_default", true, None).await?
708 else {
709 unreachable!()
710 };
711 let (sink_job, None, sink_internal) = insert_test_streaming_job(
712 &txn,
713 "sink_streaming",
714 false,
715 Some(CacheRefillPolicy::Streaming),
716 )
717 .await?
718 else {
719 unreachable!()
720 };
721
722 let result_fragment = FragmentId::new(100);
723 let internal_fragment = FragmentId::new(101);
724 let sink_fragment = FragmentId::new(102);
725 for (fragment_id, job_id, table_ids) in [
726 (result_fragment, mv_job, vec![mv_result, mv_internal]),
727 (internal_fragment, default_job, vec![default_internal]),
728 (sink_fragment, sink_job, vec![sink_internal]),
729 ] {
730 insert_test_fragment(&txn, fragment_id, job_id, TableIdArray(table_ids)).await?;
731 }
732 txn.commit().await?;
733 drop(inner);
734
735 let serving_infos = mgr.fragment_serving_infos().await?;
736 assert_eq!(serving_infos.len(), 3);
737 assert_eq!(
738 serving_infos[&result_fragment].result_table_id,
739 Some(mv_result)
740 );
741 assert_eq!(serving_infos[&internal_fragment].result_table_id, None);
742 assert_eq!(serving_infos[&sink_fragment].result_table_id, None);
743
744 let policies = mgr.table_cache_refill_policies_snapshot().await?;
745 assert_eq!(
746 policies
747 .table_policies
748 .into_iter()
749 .map(|policy| (policy.table_id, policy.policy))
750 .collect::<HashMap<_, _>>(),
751 HashMap::from([(
752 mv_result.as_raw_id(),
753 CacheRefillPolicy::Both.to_protobuf() as i32,
754 )])
755 );
756 assert_eq!(
757 policies
758 .internal_table_policies
759 .into_iter()
760 .map(|policy| (policy.table_id, policy.policy))
761 .collect::<HashMap<_, _>>(),
762 HashMap::from([
763 (
764 mv_internal.as_raw_id(),
765 CacheRefillPolicy::Both.to_protobuf() as i32,
766 ),
767 (
768 sink_internal.as_raw_id(),
769 CacheRefillPolicy::Streaming.to_protobuf() as i32,
770 ),
771 ])
772 );
773
774 Ok(())
775 }
776
777 #[tokio::test]
778 async fn test_foreground_creating_catalog_lifecycle() -> MetaResult<()> {
779 let env = MetaSrvEnv::for_test().await;
780 let (tx, mut notification_rx) = mpsc::unbounded_channel();
781 env.notification_manager().insert_sender(
782 SubscribeType::Frontend,
783 WorkerKey(HostAddress {
784 host: "localhost".to_owned(),
785 port: 1234,
786 }),
787 tx,
788 );
789 let mgr = CatalogController::new(env).await?;
790 let inner = mgr.inner.write().await;
791 let txn = inner.db.begin().await?;
792
793 let (job_id, Some(table_id), internal_table_id) =
795 insert_test_streaming_job(&txn, "creating_table", true, None).await?
796 else {
797 unreachable!()
798 };
799 let associated_source_id = CatalogController::create_object(
800 &txn,
801 ObjectType::Source,
802 TEST_OWNER_ID,
803 Some(TEST_SCHEMA_ID.as_object_id()),
804 )
805 .await?
806 .oid
807 .as_source_id();
808 Source::insert(source::ActiveModel::from(PbSource {
809 id: associated_source_id,
810 schema_id: TEST_SCHEMA_ID,
811 database_id: TEST_DATABASE_ID,
812 name: "creating_table_source".to_owned(),
813 owner: TEST_OWNER_ID as _,
814 optional_associated_table_id: Some(
815 risingwave_pb::catalog::source::OptionalAssociatedTableId::AssociatedTableId(
816 table_id,
817 ),
818 ),
819 ..Default::default()
820 }))
821 .exec(&txn)
822 .await?;
823 table::ActiveModel {
824 table_id: Set(table_id),
825 table_type: Set(TableType::Table),
826 optional_associated_source_id: Set(Some(associated_source_id)),
827 ..Default::default()
828 }
829 .update(&txn)
830 .await?;
831 streaming_job::ActiveModel {
832 job_id: Set(job_id),
833 job_status: Set(JobStatus::Initial),
834 ..Default::default()
835 }
836 .update(&txn)
837 .await?;
838
839 let (_primary_job_id, Some(primary_table_id), _) =
841 insert_test_streaming_job(&txn, "primary_table", true, None).await?
842 else {
843 unreachable!()
844 };
845 let index_job_id = CatalogController::create_object(
846 &txn,
847 ObjectType::Index,
848 TEST_OWNER_ID,
849 Some(TEST_SCHEMA_ID.as_object_id()),
850 )
851 .await?
852 .oid
853 .as_job_id();
854 let index_table_id = index_job_id.as_mv_table_id();
855 insert_test_table(
856 &txn,
857 index_table_id,
858 "creating_index",
859 TableType::Index,
860 None,
861 "",
862 )
863 .await?;
864 index::ActiveModel {
865 index_id: Set(index_job_id.as_index_id()),
866 name: Set("creating_index".to_owned()),
867 index_table_id: Set(index_table_id),
868 primary_table_id: Set(primary_table_id),
869 index_items: Set(vec![].into()),
870 index_column_properties: Set(None),
871 index_columns_len: Set(0),
872 }
873 .insert(&txn)
874 .await?;
875 insert_test_streaming_job_model(&txn, index_job_id, None).await?;
876 streaming_job::ActiveModel {
877 job_id: Set(index_job_id),
878 job_status: Set(JobStatus::Initial),
879 ..Default::default()
880 }
881 .update(&txn)
882 .await?;
883
884 let source_job_id = CatalogController::create_object(
886 &txn,
887 ObjectType::Source,
888 TEST_OWNER_ID,
889 Some(TEST_SCHEMA_ID.as_object_id()),
890 )
891 .await?
892 .oid
893 .as_job_id();
894 Source::insert(source::ActiveModel::from(PbSource {
895 id: source_job_id.as_shared_source_id(),
896 schema_id: TEST_SCHEMA_ID,
897 database_id: TEST_DATABASE_ID,
898 name: "creating_shared_source".to_owned(),
899 owner: TEST_OWNER_ID as _,
900 info: Some(StreamSourceInfo {
901 cdc_source_job: true,
902 ..Default::default()
903 }),
904 ..Default::default()
905 }))
906 .exec(&txn)
907 .await?;
908 insert_test_streaming_job_model(&txn, source_job_id, None).await?;
909 streaming_job::ActiveModel {
910 job_id: Set(source_job_id),
911 job_status: Set(JobStatus::Initial),
912 ..Default::default()
913 }
914 .update(&txn)
915 .await?;
916
917 let sink_job_id = CatalogController::create_object(
918 &txn,
919 ObjectType::Sink,
920 TEST_OWNER_ID,
921 Some(TEST_SCHEMA_ID.as_object_id()),
922 )
923 .await?
924 .oid
925 .as_job_id();
926 Sink::insert(sink::ActiveModel::from(PbSink {
927 id: sink_job_id.as_sink_id(),
928 schema_id: TEST_SCHEMA_ID,
929 database_id: TEST_DATABASE_ID,
930 name: "creating_sink".to_owned(),
931 owner: TEST_OWNER_ID as _,
932 sink_type: PbSinkType::AppendOnly as i32,
933 ..Default::default()
934 }))
935 .exec(&txn)
936 .await?;
937 insert_test_streaming_job_model(&txn, sink_job_id, None).await?;
938 streaming_job::ActiveModel {
939 job_id: Set(sink_job_id),
940 job_status: Set(JobStatus::Initial),
941 ..Default::default()
942 }
943 .update(&txn)
944 .await?;
945
946 txn.commit().await?;
947
948 let (catalog, _) = inner.snapshot().await?;
949 assert!(!catalog.2.iter().any(|table| table.id == table_id));
950 assert!(!catalog.2.iter().any(|table| table.id == internal_table_id));
951 assert!(!catalog.2.iter().any(|table| table.id == index_table_id));
952 assert!(
953 !catalog
954 .3
955 .iter()
956 .any(|source| source.id == associated_source_id)
957 );
958 assert!(
959 !catalog
960 .3
961 .iter()
962 .any(|source| source.id == source_job_id.as_shared_source_id())
963 );
964 assert!(
965 !catalog
966 .6
967 .iter()
968 .any(|index| index.id == index_job_id.as_index_id())
969 );
970 assert!(
971 !catalog
972 .4
973 .iter()
974 .any(|sink| sink.id == sink_job_id.as_sink_id())
975 );
976
977 drop(inner);
978
979 let downstreams = FragmentDownstreamRelation::new();
980 let mut add_notifications = vec![];
981 for creating_job_id in [job_id, index_job_id, source_job_id, sink_job_id] {
982 mgr.post_collect_job_fragments(creating_job_id, &downstreams, None, None, None, true)
983 .await?;
984 let notification = notification_rx
985 .recv()
986 .await
987 .expect("frontend should receive a creating notification")
988 .expect("creating notification should be valid");
989 assert_eq!(notification.operation(), NotificationOperation::Add);
990 let object_group = match notification.info {
991 Some(NotificationInfo::ObjectGroup(object_group)) => object_group,
992 other => panic!("unexpected notification: {other:?}"),
993 };
994 add_notifications.push(object_group);
995 }
996
997 assert!(add_notifications[0].objects.iter().any(|object| matches!(
998 &object.object_info,
999 Some(PbObjectInfo::Table(table)) if table.id == table_id
1000 )));
1001 assert!(add_notifications[0].objects.iter().any(|object| matches!(
1002 &object.object_info,
1003 Some(PbObjectInfo::Table(table)) if table.id == internal_table_id
1004 )));
1005 assert!(add_notifications[0].objects.iter().any(|object| matches!(
1006 &object.object_info,
1007 Some(PbObjectInfo::Source(source)) if source.id == associated_source_id
1008 )));
1009 assert!(add_notifications[1].objects.iter().any(|object| matches!(
1010 &object.object_info,
1011 Some(PbObjectInfo::Table(table)) if table.id == index_table_id
1012 )));
1013 assert!(add_notifications[1].objects.iter().any(|object| matches!(
1014 &object.object_info,
1015 Some(PbObjectInfo::Index(index)) if index.id == index_job_id.as_index_id()
1016 )));
1017 assert!(add_notifications[2].objects.iter().any(|object| matches!(
1018 &object.object_info,
1019 Some(PbObjectInfo::Source(source)) if source.id == source_job_id.as_shared_source_id()
1020 )));
1021 assert!(add_notifications[3].objects.iter().any(|object| matches!(
1022 &object.object_info,
1023 Some(PbObjectInfo::Sink(sink)) if sink.id == sink_job_id.as_sink_id()
1024 )));
1025
1026 let inner = mgr.inner.write().await;
1027 let (catalog, _) = inner.snapshot().await?;
1028 assert!(catalog.2.iter().any(|table| table.id == table_id));
1029 assert!(catalog.2.iter().any(|table| table.id == internal_table_id));
1030 assert!(catalog.2.iter().any(|table| table.id == index_table_id));
1031 assert!(
1032 catalog
1033 .3
1034 .iter()
1035 .any(|source| source.id == source_job_id.as_shared_source_id())
1036 );
1037 assert!(
1038 catalog
1039 .6
1040 .iter()
1041 .any(|index| index.id == index_job_id.as_index_id())
1042 );
1043 assert!(
1044 catalog
1045 .4
1046 .iter()
1047 .any(|sink| sink.id == sink_job_id.as_sink_id())
1048 );
1049
1050 let txn = inner.db.begin().await?;
1051 let (operation, _, _, _) = mgr.finish_streaming_job_inner(&txn, job_id).await?;
1052 assert_eq!(operation, NotificationOperation::Update);
1053 let (operation, _, _, _) = mgr.finish_streaming_job_inner(&txn, index_job_id).await?;
1054 assert_eq!(operation, NotificationOperation::Update);
1055 let (operation, _, _, _) = mgr.finish_streaming_job_inner(&txn, source_job_id).await?;
1056 assert_eq!(operation, NotificationOperation::Update);
1057 let (operation, _, _, _) = mgr.finish_streaming_job_inner(&txn, sink_job_id).await?;
1058 assert_eq!(operation, NotificationOperation::Update);
1059 txn.commit().await?;
1060
1061 Ok(())
1062 }
1063
1064 #[tokio::test]
1065 async fn test_alter_streaming_job_cache_refill_policy_notifies_hummock() -> MetaResult<()> {
1066 let env = MetaSrvEnv::for_test().await;
1067 let (tx, mut rx) = mpsc::unbounded_channel();
1068 env.notification_manager().insert_sender(
1069 SubscribeType::Hummock,
1070 WorkerKey(HostAddress {
1071 host: "localhost".to_owned(),
1072 port: 1234,
1073 }),
1074 tx,
1075 );
1076 let mgr = CatalogController::new(env).await?;
1077
1078 let inner = mgr.inner.write().await;
1079 let txn = inner.db.begin().await?;
1080 let (_job, Some(result_table_id), internal_table_id) =
1081 insert_test_streaming_job(&txn, "mv_cache_refill", true, None).await?
1082 else {
1083 unreachable!()
1084 };
1085 insert_test_fragment(
1086 &txn,
1087 FragmentId::new(200),
1088 result_table_id.as_job_id(),
1089 TableIdArray(vec![result_table_id, internal_table_id]),
1090 )
1091 .await?;
1092 txn.commit().await?;
1093 drop(inner);
1094
1095 mgr.alter_streaming_job_config(
1096 result_table_id.as_job_id(),
1097 HashMap::from([(
1098 STREAMING_CACHE_REFILL_POLICY_CONFIG_PATH.to_owned(),
1099 "\"both\"".to_owned(),
1100 )]),
1101 vec![],
1102 )
1103 .await?;
1104
1105 let response = rx
1106 .recv()
1107 .await
1108 .expect("should receive hummock notification")
1109 .expect("notification should be ok");
1110 assert_eq!(response.operation(), NotificationOperation::Update);
1111 let info = response.info;
1112 let Some(NotificationInfo::TableRefillRuntimeConfig(config)) = info else {
1113 panic!("unexpected notification: {:?}", info);
1114 };
1115 assert!(config.serving_table_vnode_mappings.is_none());
1116 let policies = config
1117 .table_cache_refill_policies
1118 .expect("policy snapshot should be present");
1119 assert_eq!(
1120 policies
1121 .table_policies
1122 .into_iter()
1123 .map(|policy| (policy.table_id, policy.policy))
1124 .collect::<HashMap<_, _>>(),
1125 HashMap::from([(
1126 result_table_id.as_raw_id(),
1127 CacheRefillPolicy::Both.to_protobuf() as i32,
1128 )])
1129 );
1130 assert_eq!(
1131 policies
1132 .internal_table_policies
1133 .into_iter()
1134 .map(|policy| (policy.table_id, policy.policy))
1135 .collect::<HashMap<_, _>>(),
1136 HashMap::from([(
1137 internal_table_id.as_raw_id(),
1138 CacheRefillPolicy::Both.to_protobuf() as i32,
1139 )])
1140 );
1141
1142 Ok(())
1143 }
1144
1145 #[tokio::test]
1146 async fn test_prepare_streaming_job_cache_refill_policy_notifies_hummock() -> MetaResult<()> {
1147 let env = MetaSrvEnv::for_test().await;
1148 let (tx, mut rx) = mpsc::unbounded_channel();
1149 env.notification_manager().insert_sender(
1150 SubscribeType::Hummock,
1151 WorkerKey(HostAddress {
1152 host: "localhost".to_owned(),
1153 port: 1234,
1154 }),
1155 tx,
1156 );
1157 let (local_notification_tx, mut local_notification_rx) = mpsc::unbounded_channel();
1158 env.notification_manager()
1159 .insert_local_sender(local_notification_tx);
1160 let mgr = CatalogController::new(env).await?;
1161
1162 let inner = mgr.inner.write().await;
1163 let txn = inner.db.begin().await?;
1164 let (job_id, Some(result_table_id), internal_table_id) = insert_test_streaming_job(
1165 &txn,
1166 "mv_initial_cache_refill",
1167 true,
1168 Some(CacheRefillPolicy::Both),
1169 )
1170 .await?
1171 else {
1172 unreachable!()
1173 };
1174 let (unprepared_job_id, Some(unprepared_result_table_id), unprepared_internal_table_id) =
1175 insert_test_streaming_job(
1176 &txn,
1177 "mv_unprepared_cache_refill",
1178 true,
1179 Some(CacheRefillPolicy::Serving),
1180 )
1181 .await?
1182 else {
1183 unreachable!()
1184 };
1185 for job_id in [job_id, unprepared_job_id] {
1186 streaming_job::ActiveModel {
1187 job_id: Set(job_id),
1188 job_status: Set(JobStatus::Initial),
1189 ..Default::default()
1190 }
1191 .update(&txn)
1192 .await?;
1193 }
1194 txn.commit().await?;
1195 drop(inner);
1196
1197 let fragments = [Fragment {
1198 fragment_id: FragmentId::new(300),
1199 fragment_type_mask: FragmentTypeMask::default(),
1200 distribution_type: PbFragmentDistributionType::Hash,
1201 state_table_ids: vec![],
1202 maybe_vnode_count: Some(1),
1203 nodes: PbStreamNode::default(),
1204 }];
1205 mgr.prepare_streaming_job(
1206 job_id,
1207 || fragments.iter(),
1208 &FragmentDownstreamRelation::default(),
1209 true,
1210 None,
1211 None,
1212 )
1213 .await?;
1214
1215 let local_notification = local_notification_rx
1216 .try_recv()
1217 .expect("should receive serving fragment mapping notification");
1218 let LocalNotification::ServingFragmentMappingsUpsert(fragment_ids) = local_notification
1219 else {
1220 panic!(
1221 "unexpected local notification before hummock notification: {:?}",
1222 local_notification
1223 );
1224 };
1225 assert_eq!(fragment_ids, vec![FragmentId::new(300).as_raw_id()]);
1226
1227 let response = rx
1228 .recv()
1229 .await
1230 .expect("should receive hummock notification")
1231 .expect("notification should be ok");
1232 assert_eq!(response.operation(), NotificationOperation::Update);
1233 let info = response.info;
1234 let Some(NotificationInfo::TableRefillRuntimeConfig(config)) = info else {
1235 panic!("unexpected notification: {:?}", info);
1236 };
1237 assert!(config.serving_table_vnode_mappings.is_none());
1238 let policies = config
1239 .table_cache_refill_policies
1240 .expect("policy snapshot should be present");
1241 let table_policies = policies
1242 .table_policies
1243 .into_iter()
1244 .map(|policy| (policy.table_id, policy.policy))
1245 .collect::<HashMap<_, _>>();
1246 assert_eq!(
1247 table_policies,
1248 HashMap::from([(
1249 result_table_id.as_raw_id(),
1250 CacheRefillPolicy::Both.to_protobuf() as i32,
1251 )])
1252 );
1253 assert!(!table_policies.contains_key(&unprepared_result_table_id.as_raw_id()));
1254 let internal_table_policies = policies
1255 .internal_table_policies
1256 .into_iter()
1257 .map(|policy| (policy.table_id, policy.policy))
1258 .collect::<HashMap<_, _>>();
1259 assert_eq!(
1260 internal_table_policies,
1261 HashMap::from([(
1262 internal_table_id.as_raw_id(),
1263 CacheRefillPolicy::Both.to_protobuf() as i32,
1264 )])
1265 );
1266 assert!(!internal_table_policies.contains_key(&unprepared_internal_table_id.as_raw_id()));
1267
1268 Ok(())
1269 }
1270
1271 async fn insert_dirty_creating_job_with_fragment(
1272 mgr: &CatalogController,
1273 fragment_id: FragmentId,
1274 vnode_count: i32,
1275 fragment_type_mask: FragmentTypeMask,
1276 ) -> MetaResult<(JobId, TableId)> {
1277 let inner = mgr.inner.write().await;
1278 let txn = inner.db.begin().await?;
1279 let job_obj = CatalogController::create_object(
1280 &txn,
1281 ObjectType::Table,
1282 TEST_OWNER_ID,
1283 Some(TEST_SCHEMA_ID.as_object_id()),
1284 )
1285 .await?;
1286 let job_id = job_obj.oid.as_job_id();
1287 let table_id = job_id.as_mv_table_id();
1288 insert_test_table(
1289 &txn,
1290 table_id,
1291 "mv_dirty_serving_mapping",
1292 TableType::MaterializedView,
1293 None,
1294 "CREATE MATERIALIZED VIEW mv_dirty_serving_mapping AS SELECT 1",
1295 )
1296 .await?;
1297 table::ActiveModel {
1298 table_id: Set(table_id),
1299 engine: Set(Some(table::Engine::Hummock)),
1300 ..Default::default()
1301 }
1302 .update(&txn)
1303 .await?;
1304 streaming_job::ActiveModel {
1305 job_id: Set(job_id),
1306 job_status: Set(JobStatus::Creating),
1307 create_type: Set(CreateType::Foreground),
1308 timezone: Set(None),
1309 config_override: Set(None),
1310 adaptive_parallelism_strategy: Set(None),
1311 parallelism: Set(StreamingParallelism::Adaptive),
1312 backfill_parallelism: Set(None),
1313 backfill_adaptive_parallelism_strategy: Set(None),
1314 backfill_orders: Set(None),
1315 max_parallelism: Set(1),
1316 specific_resource_group: Set(None),
1317 is_serverless_backfill: Set(false),
1318 refresh_interval_sec: Set(None),
1319 }
1320 .insert(&txn)
1321 .await?;
1322 fragment::ActiveModel {
1323 fragment_id: Set(fragment_id),
1324 job_id: Set(job_id),
1325 fragment_type_mask: Set(fragment_type_mask.into()),
1326 distribution_type: Set(DistributionType::Hash),
1327 stream_node: Set(StreamNode::default()),
1328 state_table_ids: Set(Vec::<TableId>::new().into()),
1329 upstream_fragment_id: Set(Vec::<i32>::new().into()),
1330 vnode_count: Set(vnode_count),
1331 parallelism: Set(None),
1332 }
1333 .insert(&txn)
1334 .await?;
1335 txn.commit().await?;
1336 drop(inner);
1337
1338 Ok((job_id, table_id))
1339 }
1340
1341 #[tokio::test]
1342 async fn test_dirty_cleanup_reconcile_removes_stale_serving_vnode_mapping() -> MetaResult<()> {
1343 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
1344 let fragment_id = FragmentId::new(42);
1345 insert_dirty_creating_job_with_fragment(
1346 &mgr,
1347 fragment_id,
1348 VirtualNode::COUNT_FOR_TEST as i32,
1349 FragmentTypeMask::from(FragmentTypeFlag::Values as u32),
1350 )
1351 .await?;
1352
1353 let worker = WorkerNode {
1354 id: WorkerId::new(1),
1355 r#type: WorkerType::ComputeNode.into(),
1356 host: Some(HostAddress {
1357 host: "localhost".to_owned(),
1358 port: 1,
1359 }),
1360 state: worker_node::State::Running as i32,
1361 property: Some(worker_node::Property {
1362 is_serving: true,
1363 parallelism: 1,
1364 ..Default::default()
1365 }),
1366 ..Default::default()
1367 };
1368 let serving_vnode_mapping = ServingVnodeMapping::default();
1369 let initial_snapshot = mgr.fragment_serving_infos().await?;
1370 serving_vnode_mapping.upsert(&initial_snapshot, std::slice::from_ref(&worker), None);
1371 assert!(serving_vnode_mapping.all().contains_key(&fragment_id));
1372
1373 mgr.clean_dirty_creating_jobs(Some(TEST_DATABASE_ID))
1374 .await?;
1375 let current_snapshot = mgr.fragment_serving_infos().await?;
1376 assert!(!current_snapshot.contains_key(&fragment_id));
1377
1378 serving_vnode_mapping.reconcile(¤t_snapshot, &[worker], None);
1379 assert!(!serving_vnode_mapping.all().contains_key(&fragment_id));
1380
1381 Ok(())
1382 }
1383
1384 #[tokio::test]
1385 async fn test_clean_dirty_creating_jobs_keeps_job_without_values_fragment() -> MetaResult<()> {
1386 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
1387 let fragment_id = FragmentId::new(43);
1388 let (job_id, table_id) = insert_dirty_creating_job_with_fragment(
1389 &mgr,
1390 fragment_id,
1391 1,
1392 FragmentTypeMask::empty(),
1393 )
1394 .await?;
1395
1396 let cleaned = mgr
1397 .clean_dirty_creating_jobs(Some(TEST_DATABASE_ID))
1398 .await?;
1399 assert!(cleaned.streaming_job_ids.is_empty());
1400
1401 let inner = mgr.inner.read().await;
1402 assert!(Object::find_by_id(job_id).one(&inner.db).await?.is_some());
1403 assert!(
1404 StreamingJob::find_by_id(job_id)
1405 .one(&inner.db)
1406 .await?
1407 .is_some()
1408 );
1409 assert!(Table::find_by_id(table_id).one(&inner.db).await?.is_some());
1410
1411 Ok(())
1412 }
1413
1414 #[tokio::test]
1415 async fn test_clean_dirty_creating_jobs_cleans_foreground_job_in_legacy_mode() -> MetaResult<()>
1416 {
1417 let mut opts = MetaOpts::test(false);
1418 opts.clean_all_foreground_jobs_on_recovery = true;
1419 let mgr = CatalogController::new(MetaSrvEnv::for_test_opts(opts, |_| ()).await).await?;
1420 let (job_id, table_id) = insert_dirty_creating_job_with_fragment(
1421 &mgr,
1422 FragmentId::new(44),
1423 1,
1424 FragmentTypeMask::empty(),
1425 )
1426 .await?;
1427
1428 let cleaned = mgr
1429 .clean_dirty_creating_jobs(Some(TEST_DATABASE_ID))
1430 .await?;
1431 assert_eq!(cleaned.streaming_job_ids, vec![job_id]);
1432
1433 let db = &mgr.inner.read().await.db;
1434 assert!(Object::find_by_id(job_id).one(db).await?.is_none());
1435 assert!(StreamingJob::find_by_id(job_id).one(db).await?.is_none());
1436 assert!(Table::find_by_id(table_id).one(db).await?.is_none());
1437
1438 Ok(())
1439 }
1440
1441 #[tokio::test]
1442 async fn test_database_func() -> MetaResult<()> {
1443 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
1444 let pb_database = PbDatabase {
1445 name: "db1".to_owned(),
1446 owner: TEST_OWNER_ID as _,
1447 ..Default::default()
1448 };
1449 mgr.create_database(pb_database).await?;
1450
1451 let database_id: DatabaseId = Database::find()
1452 .select_only()
1453 .column(database::Column::DatabaseId)
1454 .filter(database::Column::Name.eq("db1"))
1455 .into_tuple()
1456 .one(&mgr.inner.read().await.db)
1457 .await?
1458 .unwrap();
1459
1460 mgr.alter_name(ObjectType::Database, database_id, "db2")
1461 .await?;
1462 let database = Database::find_by_id(database_id)
1463 .one(&mgr.inner.read().await.db)
1464 .await?
1465 .unwrap();
1466 assert_eq!(database.name, "db2");
1467
1468 let schema_id: SchemaId = Schema::find()
1469 .inner_join(Object)
1470 .select_only()
1471 .column(schema::Column::SchemaId)
1472 .filter(object::Column::DatabaseId.eq(database_id))
1473 .into_tuple()
1474 .one(&mgr.inner.read().await.db)
1475 .await?
1476 .unwrap();
1477 mgr.create_view(
1478 PbView {
1479 schema_id,
1480 database_id,
1481 name: "cross_db_upstream".to_owned(),
1482 owner: TEST_OWNER_ID as _,
1483 sql: "CREATE VIEW cross_db_upstream AS SELECT 1".to_owned(),
1484 ..Default::default()
1485 },
1486 HashSet::new(),
1487 )
1488 .await?;
1489 let upstream_id: ViewId = View::find()
1490 .inner_join(Object)
1491 .select_only()
1492 .column(view::Column::ViewId)
1493 .filter(
1494 object::Column::DatabaseId
1495 .eq(database_id)
1496 .and(view::Column::Name.eq("cross_db_upstream")),
1497 )
1498 .into_tuple()
1499 .one(&mgr.inner.read().await.db)
1500 .await?
1501 .unwrap();
1502
1503 let inner = mgr.inner.write().await;
1504 let txn = inner.db.begin().await?;
1505 let (dependent_job_id, Some(dependent_table_id), _) =
1506 insert_test_streaming_job(&txn, "cross_db_dependent", true, None).await?
1507 else {
1508 unreachable!()
1509 };
1510 ObjectDependency::insert(object_dependency::ActiveModel {
1511 oid: Set(upstream_id.as_object_id()),
1512 used_by: Set(dependent_job_id.as_object_id()),
1513 ..Default::default()
1514 })
1515 .exec(&txn)
1516 .await?;
1517 txn.commit().await?;
1518 drop(inner);
1519
1520 assert!(
1521 mgr.drop_object(ObjectType::Database, database_id, DropMode::Cascade)
1522 .await
1523 .is_err()
1524 );
1525 mgr.drop_object(ObjectType::Table, dependent_table_id, DropMode::Cascade)
1526 .await?;
1527 mgr.drop_object(ObjectType::Database, database_id, DropMode::Cascade)
1528 .await?;
1529
1530 Ok(())
1531 }
1532
1533 #[tokio::test]
1534 async fn test_schema_func() -> MetaResult<()> {
1535 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
1536 let pb_schema = PbSchema {
1537 database_id: TEST_DATABASE_ID,
1538 name: "schema1".to_owned(),
1539 owner: TEST_OWNER_ID as _,
1540 ..Default::default()
1541 };
1542 mgr.create_schema(pb_schema.clone()).await?;
1543 assert!(mgr.create_schema(pb_schema).await.is_err());
1544
1545 let schema_id: SchemaId = Schema::find()
1546 .select_only()
1547 .column(schema::Column::SchemaId)
1548 .filter(schema::Column::Name.eq("schema1"))
1549 .into_tuple()
1550 .one(&mgr.inner.read().await.db)
1551 .await?
1552 .unwrap();
1553
1554 mgr.alter_name(ObjectType::Schema, schema_id, "schema2")
1555 .await?;
1556 let schema = Schema::find_by_id(schema_id)
1557 .one(&mgr.inner.read().await.db)
1558 .await?
1559 .unwrap();
1560 assert_eq!(schema.name, "schema2");
1561 mgr.drop_object(ObjectType::Schema, schema_id, DropMode::Restrict)
1562 .await?;
1563
1564 Ok(())
1565 }
1566
1567 #[tokio::test]
1568 async fn test_create_view() -> MetaResult<()> {
1569 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
1570 let pb_view = PbView {
1571 schema_id: TEST_SCHEMA_ID,
1572 database_id: TEST_DATABASE_ID,
1573 name: "view".to_owned(),
1574 owner: TEST_OWNER_ID as _,
1575 sql: "CREATE VIEW view AS SELECT 1".to_owned(),
1576 ..Default::default()
1577 };
1578 mgr.create_view(pb_view.clone(), HashSet::new()).await?;
1579 assert!(mgr.create_view(pb_view, HashSet::new()).await.is_err());
1580
1581 let view = View::find().one(&mgr.inner.read().await.db).await?.unwrap();
1582 mgr.drop_object(ObjectType::View, view.view_id, DropMode::Cascade)
1583 .await?;
1584 assert!(
1585 View::find_by_id(view.view_id)
1586 .one(&mgr.inner.read().await.db)
1587 .await?
1588 .is_none()
1589 );
1590
1591 Ok(())
1592 }
1593
1594 #[tokio::test]
1595 async fn test_object_belong_to_cascade() -> MetaResult<()> {
1596 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
1597 mgr.create_schema(PbSchema {
1598 database_id: TEST_DATABASE_ID,
1599 name: "belong_to_target".to_owned(),
1600 owner: TEST_OWNER_ID as _,
1601 ..Default::default()
1602 })
1603 .await?;
1604 let target_schema_id: SchemaId = Schema::find()
1605 .select_only()
1606 .column(schema::Column::SchemaId)
1607 .filter(schema::Column::Name.eq("belong_to_target"))
1608 .into_tuple()
1609 .one(&mgr.inner.read().await.db)
1610 .await?
1611 .unwrap();
1612 let txn = mgr.inner.read().await.db.begin().await?;
1613
1614 let mv_obj = CatalogController::create_object(
1615 &txn,
1616 ObjectType::Table,
1617 TEST_OWNER_ID,
1618 Some(TEST_SCHEMA_ID.as_object_id()),
1619 )
1620 .await?;
1621 assert_eq!(mv_obj.belong_to_oid, Some(TEST_SCHEMA_ID.as_object_id()));
1622 assert_eq!(mv_obj.database_id, Some(TEST_DATABASE_ID));
1623 assert_eq!(mv_obj.schema_id, Some(TEST_SCHEMA_ID));
1624 let job_id = mv_obj.oid.as_job_id();
1625 let mv_table_id = job_id.as_mv_table_id();
1626 insert_test_table(
1627 &txn,
1628 mv_table_id,
1629 "mv_belong_to",
1630 TableType::MaterializedView,
1631 None,
1632 "CREATE MATERIALIZED VIEW mv_belong_to AS SELECT 1",
1633 )
1634 .await?;
1635
1636 let internal_obj = CatalogController::create_object(
1637 &txn,
1638 ObjectType::Table,
1639 TEST_OWNER_ID,
1640 Some(job_id.as_object_id()),
1641 )
1642 .await?;
1643 assert_eq!(internal_obj.belong_to_oid, Some(job_id.as_object_id()));
1644 assert_eq!(internal_obj.database_id, Some(TEST_DATABASE_ID));
1645 assert_eq!(internal_obj.schema_id, Some(TEST_SCHEMA_ID));
1646 let internal_table_id = internal_obj.oid.as_table_id();
1647 insert_test_table(
1648 &txn,
1649 internal_table_id,
1650 "__internal_mv_belong_to",
1651 TableType::Internal,
1652 Some(job_id),
1653 "",
1654 )
1655 .await?;
1656 let nested_obj = CatalogController::create_object(
1657 &txn,
1658 ObjectType::Table,
1659 TEST_OWNER_ID,
1660 Some(internal_table_id.as_object_id()),
1661 )
1662 .await?;
1663 txn.commit().await?;
1664
1665 assert!(
1666 mgr.alter_schema(ObjectType::Sink, job_id.as_object_id(), target_schema_id,)
1667 .await
1668 .is_err()
1669 );
1670 mgr.alter_schema(ObjectType::Table, job_id.as_object_id(), target_schema_id)
1671 .await?;
1672
1673 let db = &mgr.inner.read().await.db;
1674 let belonging_object_ids = get_belong_objects(db, job_id.as_object_id())
1675 .await?
1676 .into_iter()
1677 .map(|object| object.oid)
1678 .collect::<HashSet<_>>();
1679 assert_eq!(
1680 belonging_object_ids,
1681 HashSet::from([internal_table_id.as_object_id(), nested_obj.oid])
1682 );
1683 let moved_objects = Object::find()
1684 .filter(object::Column::Oid.is_in([
1685 job_id.as_object_id(),
1686 internal_table_id.as_object_id(),
1687 nested_obj.oid,
1688 ]))
1689 .all(db)
1690 .await?;
1691 assert!(
1692 moved_objects
1693 .iter()
1694 .all(|object| object.schema_id == Some(target_schema_id))
1695 );
1696 assert_eq!(
1697 Object::find_by_id(internal_table_id)
1698 .one(db)
1699 .await?
1700 .unwrap()
1701 .belong_to_oid,
1702 Some(job_id.as_object_id())
1703 );
1704 assert_eq!(
1705 Object::find_by_id(nested_obj.oid)
1706 .one(db)
1707 .await?
1708 .unwrap()
1709 .belong_to_oid,
1710 Some(internal_table_id.as_object_id())
1711 );
1712
1713 Object::delete_by_id(job_id).exec(db).await?;
1714
1715 assert!(Object::find_by_id(job_id).one(db).await?.is_none());
1716 assert!(
1717 Object::find_by_id(internal_table_id)
1718 .one(db)
1719 .await?
1720 .is_none()
1721 );
1722 assert!(Table::find_by_id(mv_table_id).one(db).await?.is_none());
1723 assert!(
1724 Table::find_by_id(internal_table_id)
1725 .one(db)
1726 .await?
1727 .is_none()
1728 );
1729 assert!(Object::find_by_id(nested_obj.oid).one(db).await?.is_none());
1730
1731 Ok(())
1732 }
1733
1734 #[tokio::test]
1735 async fn test_alter_internal_table_schema_rejected() -> MetaResult<()> {
1736 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
1737 mgr.create_schema(PbSchema {
1738 database_id: TEST_DATABASE_ID,
1739 name: "internal_table_alter_target".to_owned(),
1740 owner: TEST_OWNER_ID as _,
1741 ..Default::default()
1742 })
1743 .await?;
1744 let target_schema_id: SchemaId = Schema::find()
1745 .select_only()
1746 .column(schema::Column::SchemaId)
1747 .filter(schema::Column::Name.eq("internal_table_alter_target"))
1748 .into_tuple()
1749 .one(&mgr.inner.read().await.db)
1750 .await?
1751 .unwrap();
1752
1753 let txn = mgr.inner.read().await.db.begin().await?;
1754 let parent_obj = CatalogController::create_object(
1755 &txn,
1756 ObjectType::Table,
1757 TEST_OWNER_ID,
1758 Some(TEST_SCHEMA_ID.as_object_id()),
1759 )
1760 .await?;
1761 let parent_job_id = parent_obj.oid.as_job_id();
1762 insert_test_table(
1763 &txn,
1764 parent_job_id.as_mv_table_id(),
1765 "internal_table_parent",
1766 TableType::MaterializedView,
1767 None,
1768 "",
1769 )
1770 .await?;
1771 let internal_obj = CatalogController::create_object(
1772 &txn,
1773 ObjectType::Table,
1774 TEST_OWNER_ID,
1775 Some(parent_job_id.as_object_id()),
1776 )
1777 .await?;
1778 let internal_table_id = internal_obj.oid.as_table_id();
1779 insert_test_table(
1780 &txn,
1781 internal_table_id,
1782 "__internal_table_alter_target",
1783 TableType::Internal,
1784 Some(parent_job_id),
1785 "",
1786 )
1787 .await?;
1788 txn.commit().await?;
1789
1790 for new_schema in [TEST_SCHEMA_ID, target_schema_id] {
1791 assert!(
1792 mgr.alter_schema(
1793 ObjectType::Table,
1794 internal_table_id.as_object_id(),
1795 new_schema,
1796 )
1797 .await
1798 .is_err()
1799 );
1800 }
1801
1802 let internal_obj = Object::find_by_id(internal_table_id)
1803 .one(&mgr.inner.read().await.db)
1804 .await?
1805 .unwrap();
1806 assert_eq!(internal_obj.schema_id, Some(TEST_SCHEMA_ID));
1807 assert_eq!(
1808 internal_obj.belong_to_oid,
1809 Some(parent_job_id.as_object_id())
1810 );
1811
1812 Ok(())
1813 }
1814
1815 #[tokio::test]
1816 async fn test_alter_table_schema_moves_indexes_but_not_subscriptions() -> MetaResult<()> {
1817 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
1818 mgr.create_schema(PbSchema {
1819 database_id: TEST_DATABASE_ID,
1820 name: "alter_table_target".to_owned(),
1821 owner: TEST_OWNER_ID as _,
1822 ..Default::default()
1823 })
1824 .await?;
1825 let target_schema_id: SchemaId = Schema::find()
1826 .select_only()
1827 .column(schema::Column::SchemaId)
1828 .filter(schema::Column::Name.eq("alter_table_target"))
1829 .into_tuple()
1830 .one(&mgr.inner.read().await.db)
1831 .await?
1832 .unwrap();
1833
1834 let txn = mgr.inner.read().await.db.begin().await?;
1835 let table_obj = CatalogController::create_object(
1836 &txn,
1837 ObjectType::Table,
1838 TEST_OWNER_ID,
1839 Some(TEST_SCHEMA_ID.as_object_id()),
1840 )
1841 .await?;
1842 let table_id = table_obj.oid.as_table_id();
1843 insert_test_table(
1844 &txn,
1845 table_id,
1846 "mv_with_index_and_subscription",
1847 TableType::MaterializedView,
1848 None,
1849 "CREATE MATERIALIZED VIEW mv_with_index_and_subscription AS SELECT 1",
1850 )
1851 .await?;
1852
1853 let index_obj = CatalogController::create_object(
1854 &txn,
1855 ObjectType::Index,
1856 TEST_OWNER_ID,
1857 Some(TEST_SCHEMA_ID.as_object_id()),
1858 )
1859 .await?;
1860 let index_id = index_obj.oid.as_index_id();
1861 let index_table_id = index_id.as_object_id().as_table_id();
1862 insert_test_table(
1863 &txn,
1864 index_table_id,
1865 "idx_mv_with_index_and_subscription_table",
1866 TableType::Index,
1867 None,
1868 "",
1869 )
1870 .await?;
1871 index::ActiveModel {
1872 index_id: Set(index_id),
1873 name: Set("idx_mv_with_index_and_subscription".to_owned()),
1874 index_table_id: Set(index_table_id),
1875 primary_table_id: Set(table_id),
1876 index_items: Set(Vec::<risingwave_pb::expr::ExprNode>::new().into()),
1877 index_column_properties: Set(None),
1878 index_columns_len: Set(0),
1879 }
1880 .insert(&txn)
1881 .await?;
1882
1883 let index_internal_obj = CatalogController::create_object(
1884 &txn,
1885 ObjectType::Table,
1886 TEST_OWNER_ID,
1887 Some(index_id.as_object_id()),
1888 )
1889 .await?;
1890 let index_internal_table_id = index_internal_obj.oid.as_table_id();
1891 insert_test_table(
1892 &txn,
1893 index_internal_table_id,
1894 "__internal_idx_mv_with_index_and_subscription",
1895 TableType::Internal,
1896 Some(index_id.as_job_id()),
1897 "",
1898 )
1899 .await?;
1900 txn.commit().await?;
1901
1902 let mut subscription = PbSubscription {
1903 name: "subscription_in_original_schema".to_owned(),
1904 definition: "CREATE SUBSCRIPTION subscription_in_original_schema FROM mv_with_index_and_subscription".to_owned(),
1905 retention_seconds: 86400,
1906 database_id: TEST_DATABASE_ID,
1907 schema_id: TEST_SCHEMA_ID,
1908 dependent_table_id: table_id,
1909 owner: TEST_OWNER_ID as _,
1910 subscription_state: SubscriptionState::Created as _,
1911 ..Default::default()
1912 };
1913 mgr.create_subscription_catalog(&mut subscription).await?;
1914
1915 {
1916 let inner = mgr.inner.read().await;
1917 assert_eq!(
1918 Object::find_by_id(index_id)
1919 .one(&inner.db)
1920 .await?
1921 .unwrap()
1922 .belong_to_oid,
1923 Some(TEST_SCHEMA_ID.as_object_id())
1924 );
1925 assert_eq!(
1926 Object::find_by_id(subscription.id)
1927 .one(&inner.db)
1928 .await?
1929 .unwrap()
1930 .belong_to_oid,
1931 Some(TEST_SCHEMA_ID.as_object_id())
1932 );
1933 }
1934
1935 mgr.alter_schema(ObjectType::Table, table_id.as_object_id(), target_schema_id)
1936 .await?;
1937
1938 let db = &mgr.inner.read().await.db;
1939 for object_id in [table_id.as_object_id(), index_id.as_object_id()] {
1940 let object = Object::find_by_id(object_id).one(db).await?.unwrap();
1941 assert_eq!(object.schema_id, Some(target_schema_id));
1942 assert_eq!(object.belong_to_oid, Some(target_schema_id.as_object_id()));
1943 }
1944 let index_internal_object = Object::find_by_id(index_internal_table_id)
1945 .one(db)
1946 .await?
1947 .unwrap();
1948 assert_eq!(index_internal_object.schema_id, Some(target_schema_id));
1949 assert_eq!(
1950 index_internal_object.belong_to_oid,
1951 Some(index_id.as_object_id())
1952 );
1953
1954 let subscription_object = Object::find_by_id(subscription.id).one(db).await?.unwrap();
1955 assert_eq!(subscription_object.schema_id, Some(TEST_SCHEMA_ID));
1956 assert_eq!(
1957 subscription_object.belong_to_oid,
1958 Some(TEST_SCHEMA_ID.as_object_id())
1959 );
1960
1961 Ok(())
1962 }
1963
1964 #[tokio::test]
1965 async fn test_create_function() -> MetaResult<()> {
1966 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
1967 let test_data_type = risingwave_pb::data::DataType {
1968 type_name: risingwave_pb::data::data_type::TypeName::Int32 as _,
1969 ..Default::default()
1970 };
1971 let arg_types = vec![test_data_type.clone()];
1972 let pb_function = PbFunction {
1973 schema_id: TEST_SCHEMA_ID,
1974 database_id: TEST_DATABASE_ID,
1975 name: "test_function".to_owned(),
1976 owner: TEST_OWNER_ID as _,
1977 arg_types,
1978 return_type: Some(test_data_type.clone()),
1979 language: "python".to_owned(),
1980 kind: Some(risingwave_pb::catalog::function::Kind::Scalar(
1981 Default::default(),
1982 )),
1983 ..Default::default()
1984 };
1985 mgr.create_function(pb_function.clone()).await?;
1986 assert!(mgr.create_function(pb_function).await.is_err());
1987
1988 let function = Function::find()
1989 .inner_join(Object)
1990 .filter(
1991 object::Column::DatabaseId
1992 .eq(TEST_DATABASE_ID)
1993 .and(object::Column::SchemaId.eq(TEST_SCHEMA_ID))
1994 .add(function::Column::Name.eq("test_function")),
1995 )
1996 .one(&mgr.inner.read().await.db)
1997 .await?
1998 .unwrap();
1999 assert_eq!(function.return_type.to_protobuf(), test_data_type);
2000 assert_eq!(function.arg_types.to_protobuf().len(), 1);
2001 assert_eq!(function.language, "python");
2002
2003 mgr.create_schema(PbSchema {
2004 database_id: TEST_DATABASE_ID,
2005 name: "function_target".to_owned(),
2006 owner: TEST_OWNER_ID as _,
2007 ..Default::default()
2008 })
2009 .await?;
2010 let target_schema_id: SchemaId = Schema::find()
2011 .select_only()
2012 .column(schema::Column::SchemaId)
2013 .filter(schema::Column::Name.eq("function_target"))
2014 .into_tuple()
2015 .one(&mgr.inner.read().await.db)
2016 .await?
2017 .unwrap();
2018 mgr.alter_schema(
2019 ObjectType::Function,
2020 function.function_id.as_object_id(),
2021 target_schema_id,
2022 )
2023 .await?;
2024 assert_eq!(
2025 Object::find_by_id(function.function_id)
2026 .one(&mgr.inner.read().await.db)
2027 .await?
2028 .unwrap()
2029 .schema_id,
2030 Some(target_schema_id)
2031 );
2032
2033 mgr.drop_object(
2034 ObjectType::Function,
2035 function.function_id,
2036 DropMode::Restrict,
2037 )
2038 .await?;
2039 assert!(
2040 Object::find_by_id(function.function_id)
2041 .one(&mgr.inner.read().await.db)
2042 .await?
2043 .is_none()
2044 );
2045
2046 Ok(())
2047 }
2048
2049 #[tokio::test]
2050 async fn test_create_cdc_table_source_dependency() -> MetaResult<()> {
2051 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
2052 let (shared_source_id, _) = mgr
2053 .create_source(
2054 PbSource {
2055 schema_id: TEST_SCHEMA_ID,
2056 database_id: TEST_DATABASE_ID,
2057 name: "shared_cdc".to_owned(),
2058 owner: TEST_OWNER_ID,
2059 info: Some(StreamSourceInfo {
2060 cdc_source_job: true,
2061 ..Default::default()
2062 }),
2063 ..Default::default()
2064 },
2065 None,
2066 )
2067 .await?;
2068
2069 let (table_source_id, _) = mgr
2070 .create_source(
2071 PbSource {
2072 schema_id: TEST_SCHEMA_ID,
2073 database_id: TEST_DATABASE_ID,
2074 name: "orders_cdc".to_owned(),
2075 owner: TEST_OWNER_ID,
2076 info: Some(StreamSourceInfo {
2077 external_table: Some(ExternalTableDesc {
2078 source_id: shared_source_id,
2079 table_name: "mydb.orders".to_owned(),
2080 connect_properties: BTreeMap::from([(
2081 "hostname".to_owned(),
2082 "mysql-host".to_owned(),
2083 )]),
2084 ..Default::default()
2085 }),
2086 ..Default::default()
2087 }),
2088 ..Default::default()
2089 },
2090 None,
2091 )
2092 .await?;
2093
2094 let table_source = Source::find_by_id(table_source_id)
2095 .one(&mgr.inner.read().await.db)
2096 .await?
2097 .unwrap();
2098 let external_table = table_source
2099 .source_info
2100 .unwrap()
2101 .to_protobuf()
2102 .external_table
2103 .unwrap();
2104 assert_eq!(external_table.source_id, shared_source_id);
2105 assert_eq!(external_table.table_id, table_source_id.as_cdc_table_id());
2106 assert!(external_table.connect_properties.is_empty());
2108 assert!(external_table.secret_refs.is_empty());
2109
2110 let dependency_count = ObjectDependency::find()
2111 .filter(object_dependency::Column::Oid.eq(shared_source_id.as_object_id()))
2112 .filter(object_dependency::Column::UsedBy.eq(table_source_id.as_object_id()))
2113 .count(&mgr.inner.read().await.db)
2114 .await?;
2115 assert_eq!(dependency_count, 1);
2116
2117 Ok(())
2118 }
2119
2120 #[tokio::test]
2121 async fn test_alter_relation_rename() -> MetaResult<()> {
2122 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
2123 let pb_source = PbSource {
2124 schema_id: TEST_SCHEMA_ID,
2125 database_id: TEST_DATABASE_ID,
2126 name: "s1".to_owned(),
2127 owner: TEST_OWNER_ID as _,
2128 definition: r#"CREATE SOURCE s1 (v1 int) with (
2129 connector = 'kafka',
2130 topic = 'kafka_alter',
2131 properties.bootstrap.server = 'message_queue:29092',
2132 scan.startup.mode = 'earliest'
2133) FORMAT PLAIN ENCODE JSON"#
2134 .to_owned(),
2135 info: Some(StreamSourceInfo {
2136 ..Default::default()
2137 }),
2138 ..Default::default()
2139 };
2140 mgr.create_source(pb_source, None).await?;
2141 let source_id: SourceId = Source::find()
2142 .select_only()
2143 .column(source::Column::SourceId)
2144 .filter(source::Column::Name.eq("s1"))
2145 .into_tuple()
2146 .one(&mgr.inner.read().await.db)
2147 .await?
2148 .unwrap();
2149
2150 let pb_view = PbView {
2151 schema_id: TEST_SCHEMA_ID,
2152 database_id: TEST_DATABASE_ID,
2153 name: "view_1".to_owned(),
2154 owner: TEST_OWNER_ID as _,
2155 sql: "CREATE VIEW view_1 AS SELECT v1 FROM s1".to_owned(),
2156 ..Default::default()
2157 };
2158 mgr.create_view(pb_view, HashSet::from([source_id.as_object_id()]))
2159 .await?;
2160 let view_id: ViewId = View::find()
2161 .select_only()
2162 .column(view::Column::ViewId)
2163 .filter(view::Column::Name.eq("view_1"))
2164 .into_tuple()
2165 .one(&mgr.inner.read().await.db)
2166 .await?
2167 .unwrap();
2168
2169 mgr.alter_name(ObjectType::Source, source_id, "s2").await?;
2170 let source = Source::find_by_id(source_id)
2171 .one(&mgr.inner.read().await.db)
2172 .await?
2173 .unwrap();
2174 assert_eq!(source.name, "s2");
2175 assert_eq!(
2176 source.definition,
2177 "CREATE SOURCE s2 (v1 INT) WITH (\
2178 connector = 'kafka', \
2179 topic = 'kafka_alter', \
2180 properties.bootstrap.server = 'message_queue:29092', \
2181 scan.startup.mode = 'earliest'\
2182) FORMAT PLAIN ENCODE JSON"
2183 );
2184
2185 let view = View::find_by_id(view_id)
2186 .one(&mgr.inner.read().await.db)
2187 .await?
2188 .unwrap();
2189 assert_eq!(
2190 view.definition,
2191 "CREATE VIEW view_1 AS SELECT v1 FROM s2 AS s1"
2192 );
2193
2194 mgr.drop_object(ObjectType::Source, source_id, DropMode::Cascade)
2195 .await?;
2196 assert!(
2197 View::find_by_id(view_id)
2198 .one(&mgr.inner.read().await.db)
2199 .await?
2200 .is_none()
2201 );
2202
2203 Ok(())
2204 }
2205
2206 #[tokio::test]
2207 async fn test_cancel_creating_table_deletes_associated_source() -> MetaResult<()> {
2208 let env = MetaSrvEnv::for_test().await;
2209 let (tx, mut notification_rx) = mpsc::unbounded_channel();
2210 env.notification_manager().insert_sender(
2211 SubscribeType::Frontend,
2212 WorkerKey(HostAddress {
2213 host: "localhost".to_owned(),
2214 port: 1234,
2215 }),
2216 tx,
2217 );
2218 let mgr = CatalogController::new(env).await?;
2219
2220 let mut inner = mgr.inner.write().await;
2221 let txn = inner.db.begin().await?;
2222 let obj = CatalogController::create_object(
2223 &txn,
2224 ObjectType::Table,
2225 TEST_OWNER_ID,
2226 Some(TEST_SCHEMA_ID.as_object_id()),
2227 )
2228 .await?;
2229 let job_id = obj.oid.as_job_id();
2230 let source_obj = CatalogController::create_object(
2231 &txn,
2232 ObjectType::Source,
2233 TEST_OWNER_ID,
2234 Some(job_id.as_object_id()),
2235 )
2236 .await?;
2237 Source::insert(source::ActiveModel::from(PbSource {
2238 id: source_obj.oid.as_source_id(),
2239 schema_id: TEST_SCHEMA_ID,
2240 database_id: TEST_DATABASE_ID,
2241 name: "source_abort_initial".to_owned(),
2242 owner: TEST_OWNER_ID as _,
2243 ..Default::default()
2244 }))
2245 .exec(&txn)
2246 .await?;
2247
2248 table::ActiveModel {
2249 table_id: Set(obj.oid.as_table_id()),
2250 name: Set("table_abort_initial".to_owned()),
2251 optional_associated_source_id: Set(Some(source_obj.oid.as_source_id())),
2252 table_type: Set(TableType::Table),
2253 belongs_to_job_id: Set(None),
2254 columns: Set(vec![].into()),
2255 pk: Set(vec![].into()),
2256 distribution_key: Set(Vec::<i32>::new().into()),
2257 stream_key: Set(Vec::<i32>::new().into()),
2258 append_only: Set(false),
2259 fragment_id: Set(None),
2260 vnode_col_index: Set(None),
2261 row_id_index: Set(None),
2262 value_indices: Set(Vec::<i32>::new().into()),
2263 definition: Set("CREATE TABLE table_abort_initial (v1 INT)".to_owned()),
2264 handle_pk_conflict_behavior: Set(HandleConflictBehavior::NoCheck),
2265 version_column_indices: Set(None),
2266 read_prefix_len_hint: Set(0),
2267 watermark_indices: Set(Vec::<i32>::new().into()),
2268 dist_key_in_pk: Set(Vec::<i32>::new().into()),
2269 dml_fragment_id: Set(None),
2270 cardinality: Set(None),
2271 cleaned_by_watermark: Set(false),
2272 description: Set(None),
2273 version: Set(None),
2274 retention_seconds: Set(None),
2275 cdc_table_id: Set(None),
2276 vnode_count: Set(1),
2277 webhook_info: Set(None),
2278 engine: Set(None),
2279 clean_watermark_index_in_pk: Set(None),
2280 clean_watermark_indices: Set(None),
2281 refreshable: Set(false),
2282 vector_index_info: Set(None),
2283 cdc_table_type: Set(None),
2284 }
2285 .insert(&txn)
2286 .await?;
2287
2288 let internal_obj = CatalogController::create_object(
2289 &txn,
2290 ObjectType::Table,
2291 TEST_OWNER_ID,
2292 Some(job_id.as_object_id()),
2293 )
2294 .await?;
2295 let internal_table_id = internal_obj.oid.as_table_id();
2296 insert_test_table(
2297 &txn,
2298 internal_table_id,
2299 "__internal_mv_abort_initial",
2300 TableType::Internal,
2301 Some(job_id),
2302 "",
2303 )
2304 .await?;
2305
2306 streaming_job::ActiveModel {
2307 job_id: Set(job_id),
2308 job_status: Set(JobStatus::Creating),
2309 create_type: Set(CreateType::Foreground),
2310 timezone: Set(None),
2311 config_override: Set(None),
2312 adaptive_parallelism_strategy: Set(None),
2313 parallelism: Set(StreamingParallelism::Adaptive),
2314 backfill_parallelism: Set(None),
2315 backfill_adaptive_parallelism_strategy: Set(None),
2316 backfill_orders: Set(None),
2317 max_parallelism: Set(1),
2318 specific_resource_group: Set(None),
2319 is_serverless_backfill: Set(false),
2320 refresh_interval_sec: Set(None),
2321 }
2322 .insert(&txn)
2323 .await?;
2324
2325 let (tx, rx) = oneshot::channel();
2326 inner.register_finish_notifier(TEST_DATABASE_ID, job_id, tx);
2327 txn.commit().await?;
2328 drop(inner);
2329
2330 let abort_result = mgr.try_abort_creating_streaming_job(job_id, true).await?;
2331 assert!(abort_result.aborted);
2332 assert_eq!(abort_result.database_id, Some(TEST_DATABASE_ID));
2333
2334 let err = rx
2335 .await
2336 .expect("finish notifier should be notified")
2337 .expect_err("creating job cancellation should fail the create wait");
2338 assert!(err.contains("cancelled"));
2339
2340 let db = &mgr.inner.read().await.db;
2341 assert!(Object::find_by_id(job_id).one(db).await?.is_none());
2342 assert!(StreamingJob::find_by_id(job_id).one(db).await?.is_none());
2343 assert!(
2344 Table::find_by_id(job_id.as_mv_table_id())
2345 .one(db)
2346 .await?
2347 .is_none()
2348 );
2349 assert!(
2350 Object::find_by_id(internal_table_id)
2351 .one(db)
2352 .await?
2353 .is_none()
2354 );
2355 assert!(
2356 Table::find_by_id(internal_table_id)
2357 .one(db)
2358 .await?
2359 .is_none()
2360 );
2361 assert!(
2362 mgr.inner
2363 .read()
2364 .await
2365 .dropped_tables
2366 .contains_key(&internal_table_id)
2367 );
2368 assert!(
2369 Source::find_by_id(source_obj.oid.as_source_id())
2370 .one(db)
2371 .await?
2372 .is_none()
2373 );
2374
2375 let notification = notification_rx
2376 .recv()
2377 .await
2378 .expect("frontend should receive an abort notification")
2379 .expect("abort notification should be valid");
2380 assert_eq!(notification.operation(), NotificationOperation::Delete);
2381 let object_group = match notification.info {
2382 Some(NotificationInfo::ObjectGroup(object_group)) => object_group,
2383 other => panic!("unexpected notification: {other:?}"),
2384 };
2385 assert!(object_group.objects.iter().any(|object| matches!(
2386 &object.object_info,
2387 Some(PbObjectInfo::Table(table)) if table.id == job_id.as_mv_table_id()
2388 )));
2389 assert!(object_group.objects.iter().any(|object| matches!(
2390 &object.object_info,
2391 Some(PbObjectInfo::Source(source)) if source.id == source_obj.oid.as_source_id()
2392 )));
2393
2394 Ok(())
2395 }
2396
2397 #[tokio::test]
2398 async fn test_failed_foreground_creating_job_is_preserved() -> MetaResult<()> {
2399 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
2400 let (job_id, table_id) = insert_dirty_creating_job_with_fragment(
2401 &mgr,
2402 FragmentId::new(45),
2403 1,
2404 FragmentTypeMask::empty(),
2405 )
2406 .await?;
2407
2408 let abort_result = mgr.try_abort_creating_streaming_job(job_id, false).await?;
2409 assert!(!abort_result.aborted);
2410 assert_eq!(abort_result.database_id, Some(TEST_DATABASE_ID));
2411
2412 let db = &mgr.inner.read().await.db;
2413 assert!(Object::find_by_id(job_id).one(db).await?.is_some());
2414 assert!(StreamingJob::find_by_id(job_id).one(db).await?.is_some());
2415 assert!(Table::find_by_id(table_id).one(db).await?.is_some());
2416
2417 Ok(())
2418 }
2419
2420 #[tokio::test]
2421 async fn test_failed_created_job_is_preserved() -> MetaResult<()> {
2422 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
2423 let (job_id, table_id) = insert_dirty_creating_job_with_fragment(
2424 &mgr,
2425 FragmentId::new(46),
2426 1,
2427 FragmentTypeMask::empty(),
2428 )
2429 .await?;
2430
2431 {
2432 let inner = mgr.inner.read().await;
2433 streaming_job::ActiveModel {
2434 job_id: Set(job_id),
2435 job_status: Set(JobStatus::Created),
2436 ..Default::default()
2437 }
2438 .update(&inner.db)
2439 .await?;
2440 }
2441
2442 let abort_result = mgr.try_abort_creating_streaming_job(job_id, false).await?;
2443 assert!(!abort_result.aborted);
2444 assert_eq!(abort_result.database_id, Some(TEST_DATABASE_ID));
2445 let db = &mgr.inner.read().await.db;
2446 assert!(Object::find_by_id(job_id).one(db).await?.is_some());
2447 assert!(StreamingJob::find_by_id(job_id).one(db).await?.is_some());
2448 assert!(Table::find_by_id(table_id).one(db).await?.is_some());
2449
2450 Ok(())
2451 }
2452
2453 #[tokio::test]
2454 async fn test_clean_dirty_creating_jobs_records_dropped_tables_for_per_db_recovery()
2455 -> MetaResult<()> {
2456 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
2457
2458 let inner = mgr.inner.write().await;
2459 let txn = inner.db.begin().await?;
2460 let mv_obj = CatalogController::create_object(
2461 &txn,
2462 ObjectType::Table,
2463 TEST_OWNER_ID,
2464 Some(TEST_SCHEMA_ID.as_object_id()),
2465 )
2466 .await?;
2467 let job_id = mv_obj.oid.as_job_id();
2468 let mv_table_id = job_id.as_mv_table_id();
2469 insert_test_table(
2470 &txn,
2471 mv_table_id,
2472 "mv_dirty",
2473 TableType::MaterializedView,
2474 None,
2475 "CREATE MATERIALIZED VIEW mv_dirty AS SELECT 1",
2476 )
2477 .await?;
2478
2479 let internal_obj = CatalogController::create_object(
2480 &txn,
2481 ObjectType::Table,
2482 TEST_OWNER_ID,
2483 Some(job_id.as_object_id()),
2484 )
2485 .await?;
2486 let internal_table_id = internal_obj.oid.as_table_id();
2487 insert_test_table(
2488 &txn,
2489 internal_table_id,
2490 "__internal_mv_dirty",
2491 TableType::Internal,
2492 Some(job_id),
2493 "",
2494 )
2495 .await?;
2496
2497 streaming_job::ActiveModel {
2498 job_id: Set(job_id),
2499 job_status: Set(JobStatus::Initial),
2500 create_type: Set(CreateType::Foreground),
2501 timezone: Set(None),
2502 config_override: Set(None),
2503 adaptive_parallelism_strategy: Set(None),
2504 parallelism: Set(StreamingParallelism::Adaptive),
2505 backfill_parallelism: Set(None),
2506 backfill_adaptive_parallelism_strategy: Set(None),
2507 backfill_orders: Set(None),
2508 max_parallelism: Set(1),
2509 specific_resource_group: Set(None),
2510 is_serverless_backfill: Set(false),
2511 refresh_interval_sec: Set(None),
2512 }
2513 .insert(&txn)
2514 .await?;
2515 txn.commit().await?;
2516 drop(inner);
2517
2518 let cleaned = mgr
2519 .clean_dirty_creating_jobs(Some(TEST_DATABASE_ID))
2520 .await?;
2521 assert_eq!(cleaned.streaming_job_ids, vec![job_id]);
2522 assert!(cleaned.source_ids.is_empty());
2523 let mut dropped_table_ids = cleaned.dropped_table_ids;
2524 dropped_table_ids.sort_unstable();
2525 assert_eq!(dropped_table_ids, vec![mv_table_id, internal_table_id]);
2526
2527 let inner = mgr.inner.read().await;
2528 assert!(inner.dropped_tables.contains_key(&mv_table_id));
2529 assert!(inner.dropped_tables.contains_key(&internal_table_id));
2530 assert!(Object::find_by_id(job_id).one(&inner.db).await?.is_none());
2531 assert!(
2532 Object::find_by_id(internal_table_id)
2533 .one(&inner.db)
2534 .await?
2535 .is_none()
2536 );
2537 assert!(
2538 StreamingJob::find_by_id(job_id)
2539 .one(&inner.db)
2540 .await?
2541 .is_none()
2542 );
2543 assert!(
2544 Table::find_by_id(mv_table_id)
2545 .one(&inner.db)
2546 .await?
2547 .is_none()
2548 );
2549 assert!(
2550 Table::find_by_id(internal_table_id)
2551 .one(&inner.db)
2552 .await?
2553 .is_none()
2554 );
2555
2556 Ok(())
2557 }
2558
2559 #[tokio::test]
2560 async fn test_clean_dirty_creating_jobs_notifies_serving_mapping_fragment_delete()
2561 -> MetaResult<()> {
2562 let env = MetaSrvEnv::for_test().await;
2563 let (local_notification_tx, mut local_notification_rx) = mpsc::unbounded_channel();
2564 env.notification_manager()
2565 .insert_local_sender(local_notification_tx);
2566 let mgr = CatalogController::new(env).await?;
2567 let fragment_id = FragmentId::new(3);
2568 let (job_id, mv_table_id) = insert_dirty_creating_job_with_fragment(
2569 &mgr,
2570 fragment_id,
2571 1,
2572 FragmentTypeMask::from(FragmentTypeFlag::Values as u32),
2573 )
2574 .await?;
2575
2576 assert!(
2577 mgr.fragment_serving_infos()
2578 .await?
2579 .contains_key(&fragment_id)
2580 );
2581
2582 let cleaned = mgr
2583 .clean_dirty_creating_jobs(Some(TEST_DATABASE_ID))
2584 .await?;
2585 assert_eq!(cleaned.streaming_job_ids, vec![job_id]);
2586
2587 let inner = mgr.inner.read().await;
2588 assert!(Object::find_by_id(job_id).one(&inner.db).await?.is_none());
2589 assert!(
2590 StreamingJob::find_by_id(job_id)
2591 .one(&inner.db)
2592 .await?
2593 .is_none()
2594 );
2595 assert!(
2596 Table::find_by_id(mv_table_id)
2597 .one(&inner.db)
2598 .await?
2599 .is_none()
2600 );
2601 drop(inner);
2602 assert!(
2603 !mgr.fragment_serving_infos()
2604 .await?
2605 .contains_key(&fragment_id)
2606 );
2607
2608 let notification = local_notification_rx.try_recv().expect(
2609 "dirty-job cleanup must notify the serving mapping worker about deleted fragments",
2610 );
2611 match notification {
2612 LocalNotification::ServingFragmentMappingsDelete(fragment_ids) => {
2613 assert_eq!(fragment_ids, vec![fragment_id]);
2614 }
2615 notification => panic!("unexpected local notification: {notification:?}"),
2616 }
2617
2618 Ok(())
2619 }
2620
2621 #[tokio::test]
2622 async fn test_abort_creating_subscription_commits_delete() -> MetaResult<()> {
2623 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
2624 let pb_view = PbView {
2625 schema_id: TEST_SCHEMA_ID,
2626 database_id: TEST_DATABASE_ID,
2627 name: "subscription_dep_view".to_owned(),
2628 owner: TEST_OWNER_ID as _,
2629 sql: "CREATE VIEW subscription_dep_view AS SELECT 1".to_owned(),
2630 ..Default::default()
2631 };
2632 mgr.create_view(pb_view, HashSet::new()).await?;
2633
2634 let view_id: ViewId = View::find()
2635 .select_only()
2636 .column(view::Column::ViewId)
2637 .filter(view::Column::Name.eq("subscription_dep_view"))
2638 .into_tuple()
2639 .one(&mgr.inner.read().await.db)
2640 .await?
2641 .unwrap();
2642
2643 let mut pb_subscription = PbSubscription {
2644 name: "subscription_to_abort".to_owned(),
2645 definition: "CREATE SUBSCRIPTION subscription_to_abort FROM subscription_dep_view"
2646 .to_owned(),
2647 retention_seconds: 86400,
2648 database_id: TEST_DATABASE_ID,
2649 schema_id: TEST_SCHEMA_ID,
2650 dependent_table_id: view_id.as_object_id().as_table_id(),
2651 owner: TEST_OWNER_ID as _,
2652 subscription_state: SubscriptionState::Init as _,
2653 ..Default::default()
2654 };
2655 mgr.create_subscription_catalog(&mut pb_subscription)
2656 .await?;
2657
2658 mgr.try_abort_creating_subscription(pb_subscription.id)
2659 .await?;
2660
2661 assert!(
2662 Subscription::find_by_id(pb_subscription.id)
2663 .one(&mgr.inner.read().await.db)
2664 .await?
2665 .is_none()
2666 );
2667
2668 Ok(())
2669 }
2670
2671 #[tokio::test]
2672 async fn test_drop_table_cascade_drops_dependent_subscription() -> MetaResult<()> {
2673 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
2674
2675 let inner = mgr.inner.write().await;
2676 let txn = inner.db.begin().await?;
2677 let table_obj = CatalogController::create_object(
2678 &txn,
2679 ObjectType::Table,
2680 TEST_OWNER_ID,
2681 Some(TEST_SCHEMA_ID.as_object_id()),
2682 )
2683 .await?;
2684 let table_id = table_obj.oid.as_table_id();
2685 insert_test_table(
2686 &txn,
2687 table_id,
2688 "subscription_dep_table",
2689 TableType::Table,
2690 None,
2691 "CREATE TABLE subscription_dep_table (v1 INT)",
2692 )
2693 .await?;
2694 txn.commit().await?;
2695 drop(inner);
2696
2697 let mut pb_subscription = PbSubscription {
2698 name: "subscription_to_drop_with_table".to_owned(),
2699 definition:
2700 "CREATE SUBSCRIPTION subscription_to_drop_with_table FROM subscription_dep_table"
2701 .to_owned(),
2702 retention_seconds: 86400,
2703 database_id: TEST_DATABASE_ID,
2704 schema_id: TEST_SCHEMA_ID,
2705 dependent_table_id: table_id,
2706 owner: TEST_OWNER_ID as _,
2707 subscription_state: SubscriptionState::Created as _,
2708 ..Default::default()
2709 };
2710 mgr.create_subscription_catalog(&mut pb_subscription)
2711 .await?;
2712
2713 mgr.drop_object(ObjectType::Table, table_id, DropMode::Cascade)
2714 .await?;
2715
2716 let db = &mgr.inner.read().await.db;
2717 assert!(Table::find_by_id(table_id).one(db).await?.is_none());
2718 assert!(
2719 Object::find_by_id(table_id.as_object_id())
2720 .one(db)
2721 .await?
2722 .is_none()
2723 );
2724 assert!(
2725 Subscription::find_by_id(pb_subscription.id)
2726 .one(db)
2727 .await?
2728 .is_none()
2729 );
2730 assert!(
2731 Object::find_by_id(pb_subscription.id.as_object_id())
2732 .one(db)
2733 .await?
2734 .is_none()
2735 );
2736
2737 Ok(())
2738 }
2739
2740 #[tokio::test]
2741 async fn test_get_table_change_log_truncate_info() -> MetaResult<()> {
2742 let mgr = CatalogController::new(MetaSrvEnv::for_test().await).await?;
2743 let pb_view = PbView {
2744 schema_id: TEST_SCHEMA_ID,
2745 database_id: TEST_DATABASE_ID,
2746 name: "change_log_upstream".to_owned(),
2747 owner: TEST_OWNER_ID as _,
2748 sql: "CREATE VIEW change_log_upstream AS SELECT 1".to_owned(),
2749 ..Default::default()
2750 };
2751 mgr.create_view(pb_view, HashSet::new()).await?;
2752 let upstream_table_id: TableId = View::find()
2753 .select_only()
2754 .column(view::Column::ViewId)
2755 .filter(view::Column::Name.eq("change_log_upstream"))
2756 .into_tuple::<ViewId>()
2757 .one(&mgr.inner.read().await.db)
2758 .await?
2759 .unwrap()
2760 .as_object_id()
2761 .as_table_id();
2762 let mut subscription = PbSubscription {
2763 name: "change_log_subscription".to_owned(),
2764 definition: "CREATE SUBSCRIPTION change_log_subscription FROM change_log_upstream"
2765 .to_owned(),
2766 retention_seconds: 123,
2767 database_id: TEST_DATABASE_ID,
2768 schema_id: TEST_SCHEMA_ID,
2769 dependent_table_id: upstream_table_id,
2770 owner: TEST_OWNER_ID as _,
2771 subscription_state: SubscriptionState::Created as _,
2772 ..Default::default()
2773 };
2774 mgr.create_subscription_catalog(&mut subscription).await?;
2775
2776 let inner = mgr.inner.write().await;
2777 let txn = inner.db.begin().await?;
2778 let (job_id, _, state_table_id) =
2779 insert_test_streaming_job(&txn, "snapshot_job", true, None).await?;
2780 let mut job = streaming_job::Entity::find_by_id(job_id)
2781 .one(&txn)
2782 .await?
2783 .unwrap()
2784 .into_active_model();
2785 job.job_status = Set(JobStatus::Creating);
2786 job.update(&txn).await?;
2787 fragment::ActiveModel {
2788 fragment_id: Set(FragmentId::new(100)),
2789 job_id: Set(job_id),
2790 fragment_type_mask: Set(FragmentTypeFlag::SnapshotBackfillStreamScan as i32),
2791 distribution_type: Set(fragment::DistributionType::Hash),
2792 stream_node: Set(StreamNode::from(&PbStreamNode {
2793 node_body: Some(PbNodeBody::StreamScan(Box::new(StreamScanNode {
2794 table_id: upstream_table_id,
2795 stream_scan_type: StreamScanType::SnapshotBackfill as i32,
2796 snapshot_backfill_epoch: None,
2797 ..Default::default()
2798 }))),
2799 ..Default::default()
2800 })),
2801 state_table_ids: Set(vec![state_table_id].into()),
2802 upstream_fragment_id: Set(I32Array::default()),
2803 vnode_count: Set(1),
2804 parallelism: Set(None),
2805 }
2806 .insert(&txn)
2807 .await?;
2808 txn.commit().await?;
2809 drop(inner);
2810
2811 let truncate_info = mgr.get_table_change_log_truncate_info().await?;
2812 assert_eq!(
2813 truncate_info.subscription_retention_seconds,
2814 HashMap::from([(upstream_table_id, 123)])
2815 );
2816 assert_eq!(truncate_info.independent_jobs.len(), 1);
2817 let independent_job = &truncate_info.independent_jobs[0];
2818 assert_eq!(independent_job.job_id, job_id);
2819 assert_eq!(
2820 independent_job.state_table_ids,
2821 HashSet::from([state_table_id])
2822 );
2823 assert_eq!(
2824 independent_job.upstream_table_snapshot_epochs,
2825 HashMap::from([(upstream_table_id, None)])
2826 );
2827
2828 Ok(())
2829 }
2830}