Skip to main content

risingwave_meta/controller/catalog/
test.rs

1// Copyright 2024 RisingWave Labs
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#[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            // Use create_job_catalog to trigger construct_sink_cycle_check_query for regression
385            // testing purpose to ensure no circular issue causing infinite recursion when
386            // cte_referencing
387            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        // Ensure the test sinks were created
440        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        // Ensure no object_dependency created for (target_table, sink) which could cause circular
453        // issue
454        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        // Check that the cascade drop successfully dropped
486        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        // Sanity checks that sources were not dropped
495        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            // Ensures the replacement failed because a referring streaming job is still creating.
506            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        // Ensures the incoming sink is still creating, which should block replacement.
558        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        // Ensures the replacement targets the upstream MV that the creating sink depends on.
569        assert_eq!(replacement.id(), target_mv_id);
570
571        // Ensures replacement rejects the upstream MV while its referring sink is creating.
572        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        // Ensures the rejection error reports the expected concurrency reason.
577        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        // Ensures the incoming sink starts as a creating job before the test marks it created.
630        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        // Ensures the persisted sink row is the sink created by this test.
646        assert_eq!(sink_model.job_id, sink_id);
647        // Ensures a fully created incoming sink does not block upstream MV replacement.
648        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        // Ensures the replacement targets the upstream MV that the created sink depends on.
660        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        // Ensures replacement creates a distinct temporary job instead of reusing the original MV id.
667        assert_ne!(tmp_model.job_id, source_mv_id);
668        // Ensures the temporary replacement job is created but not finished yet.
669        assert_eq!(tmp_model.job_status, JobStatus::Initial);
670
671        let inner = mgr.inner.read().await;
672        let db = &inner.db;
673        // Ensures the created sink still depends on the upstream MV being replaced.
674        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        // Ensures replacement records the temporary job as a dependent of the original MV.
683        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        // A foreground table and its internal table are both visible while creating.
794        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        // A foreground index and its index table are both visible while creating.
840        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        // A creating shared source is included in restart snapshots as well.
885        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(&current_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        // Connect properties are only used to validate the descriptor, never persisted.
2107        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}