Skip to main content

risingwave_meta/controller/catalog/
util.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
15use risingwave_common::catalog::FragmentTypeMask;
16
17use super::*;
18use crate::controller::fragment::FragmentTypeMaskExt;
19use crate::controller::utils::load_streaming_jobs_by_ids;
20
21pub(crate) async fn prepare_object_models_for_schema_change(
22    txn: &DatabaseTransaction,
23    object_models: &mut [PbObjectInfo],
24    database_id: DatabaseId,
25    new_schema: SchemaId,
26) -> MetaResult<()> {
27    // Names live in the type-specific catalog tables, not in `object`. Relations also share a
28    // namespace across object types, so preserve the existing type-specific duplicate checks.
29    let index_ids = object_models
30        .iter()
31        .filter_map(|object_info| match object_info {
32            PbObjectInfo::Index(index) => Some(index.id.as_object_id().as_table_id()),
33            _ => None,
34        })
35        .collect::<HashSet<_>>();
36
37    for object_info in object_models {
38        match object_info {
39            PbObjectInfo::Table(table) => table.schema_id = new_schema,
40            PbObjectInfo::Source(source) => source.schema_id = new_schema,
41            PbObjectInfo::Sink(sink) => sink.schema_id = new_schema,
42            PbObjectInfo::View(view) => view.schema_id = new_schema,
43            PbObjectInfo::Index(index) => index.schema_id = new_schema,
44            PbObjectInfo::Function(function) => function.schema_id = new_schema,
45            PbObjectInfo::Connection(connection) => connection.schema_id = new_schema,
46            PbObjectInfo::Subscription(subscription) => subscription.schema_id = new_schema,
47            PbObjectInfo::Secret(secret) => secret.schema_id = new_schema,
48            PbObjectInfo::Database(_) | PbObjectInfo::Schema(_) => {}
49        }
50
51        match object_info {
52            PbObjectInfo::Table(table) if !index_ids.contains(&table.id) => {
53                check_relation_name_duplicate(&table.name, database_id, new_schema, txn).await?;
54            }
55            PbObjectInfo::Source(source) => {
56                check_relation_name_duplicate(&source.name, database_id, new_schema, txn).await?;
57            }
58            PbObjectInfo::Sink(sink) => {
59                check_relation_name_duplicate(&sink.name, database_id, new_schema, txn).await?;
60            }
61            PbObjectInfo::Index(index) => {
62                check_relation_name_duplicate(&index.name, database_id, new_schema, txn).await?;
63            }
64            PbObjectInfo::View(view) => {
65                check_relation_name_duplicate(&view.name, database_id, new_schema, txn).await?;
66            }
67            PbObjectInfo::Subscription(subscription) => {
68                check_relation_name_duplicate(&subscription.name, database_id, new_schema, txn)
69                    .await?;
70                check_subscription_name_duplicate(subscription, txn).await?;
71            }
72            PbObjectInfo::Function(function) => {
73                check_function_signature_duplicate(function, txn).await?;
74            }
75            PbObjectInfo::Connection(connection) => {
76                check_connection_name_duplicate(connection, txn).await?;
77            }
78            PbObjectInfo::Secret(secret) => {
79                check_secret_name_duplicate(secret, txn).await?;
80            }
81            PbObjectInfo::Database(_) | PbObjectInfo::Schema(_) | PbObjectInfo::Table(_) => {}
82        }
83    }
84
85    Ok(())
86}
87
88pub(crate) async fn load_object_models(
89    txn: &DatabaseTransaction,
90    objects: &[object::Model],
91) -> MetaResult<Vec<PbObjectInfo>> {
92    let mut object_infos = vec![];
93
94    let table_ids = objects
95        .iter()
96        .filter(|object| object.obj_type == ObjectType::Table)
97        .map(|object| object.oid.as_table_id())
98        .collect_vec();
99    let table_objs = Table::find()
100        .find_also_related(Object)
101        .filter(table::Column::TableId.is_in(table_ids))
102        .all(txn)
103        .await?;
104    let streaming_jobs =
105        load_streaming_jobs_by_ids(txn, table_objs.iter().map(|(table, _)| table.job_id())).await?;
106    for (table, table_obj) in table_objs {
107        let streaming_job = streaming_jobs.get(&table.job_id()).cloned();
108        object_infos.push(PbObjectInfo::Table(
109            ObjectModel(table, table_obj.unwrap(), streaming_job).into(),
110        ));
111    }
112
113    let index_ids = objects
114        .iter()
115        .filter(|object| object.obj_type == ObjectType::Index)
116        .map(|object| object.oid.as_index_id())
117        .collect_vec();
118    let index_table_objs = Table::find()
119        .find_also_related(Object)
120        .filter(
121            table::Column::TableId
122                .is_in(index_ids.iter().map(|id| id.as_object_id().as_table_id())),
123        )
124        .all(txn)
125        .await?;
126    let index_streaming_jobs = load_streaming_jobs_by_ids(
127        txn,
128        index_table_objs.iter().map(|(table, _)| table.job_id()),
129    )
130    .await?;
131    for (table, table_obj) in index_table_objs {
132        let streaming_job = index_streaming_jobs.get(&table.job_id()).cloned();
133        object_infos.push(PbObjectInfo::Table(
134            ObjectModel(table, table_obj.unwrap(), streaming_job).into(),
135        ));
136    }
137    let index_objs = Index::find()
138        .find_also_related(Object)
139        .filter(index::Column::IndexId.is_in(index_ids))
140        .all(txn)
141        .await?;
142    for (index, index_obj) in index_objs {
143        let streaming_job = index_streaming_jobs
144            .get(&index.index_id.as_job_id())
145            .cloned();
146        object_infos.push(PbObjectInfo::Index(
147            ObjectModel(index, index_obj.unwrap(), streaming_job).into(),
148        ));
149    }
150
151    let source_ids = objects
152        .iter()
153        .filter(|object| object.obj_type == ObjectType::Source)
154        .map(|object| object.oid.as_source_id())
155        .collect_vec();
156    for (source, source_obj) in Source::find()
157        .find_also_related(Object)
158        .filter(source::Column::SourceId.is_in(source_ids))
159        .all(txn)
160        .await?
161    {
162        object_infos.push(PbObjectInfo::Source(
163            ObjectModel(source, source_obj.unwrap(), None).into(),
164        ));
165    }
166
167    let sink_ids = objects
168        .iter()
169        .filter(|object| object.obj_type == ObjectType::Sink)
170        .map(|object| object.oid.as_sink_id())
171        .collect_vec();
172    let sink_objs = Sink::find()
173        .find_also_related(Object)
174        .filter(sink::Column::SinkId.is_in(sink_ids))
175        .all(txn)
176        .await?;
177    let sink_streaming_jobs = load_streaming_jobs_by_ids(
178        txn,
179        sink_objs.iter().map(|(sink, _)| sink.sink_id.as_job_id()),
180    )
181    .await?;
182    for (sink, sink_obj) in sink_objs {
183        let streaming_job = sink_streaming_jobs.get(&sink.sink_id.as_job_id()).cloned();
184        object_infos.push(PbObjectInfo::Sink(
185            ObjectModel(sink, sink_obj.unwrap(), streaming_job).into(),
186        ));
187    }
188
189    let subscription_ids = objects
190        .iter()
191        .filter(|object| object.obj_type == ObjectType::Subscription)
192        .map(|object| object.oid.as_subscription_id())
193        .collect_vec();
194    for (subscription, subscription_obj) in Subscription::find()
195        .find_also_related(Object)
196        .filter(subscription::Column::SubscriptionId.is_in(subscription_ids))
197        .all(txn)
198        .await?
199    {
200        object_infos.push(PbObjectInfo::Subscription(
201            ObjectModel(subscription, subscription_obj.unwrap(), None).into(),
202        ));
203    }
204
205    let view_ids = objects
206        .iter()
207        .filter(|object| object.obj_type == ObjectType::View)
208        .map(|object| object.oid.as_view_id())
209        .collect_vec();
210    for (view, view_obj) in View::find()
211        .find_also_related(Object)
212        .filter(view::Column::ViewId.is_in(view_ids))
213        .all(txn)
214        .await?
215    {
216        object_infos.push(PbObjectInfo::View(
217            ObjectModel(view, view_obj.unwrap(), None).into(),
218        ));
219    }
220
221    let function_ids = objects
222        .iter()
223        .filter(|object| object.obj_type == ObjectType::Function)
224        .map(|object| object.oid.as_function_id())
225        .collect_vec();
226    for (function, function_obj) in Function::find()
227        .find_also_related(Object)
228        .filter(function::Column::FunctionId.is_in(function_ids))
229        .all(txn)
230        .await?
231    {
232        object_infos.push(PbObjectInfo::Function(
233            ObjectModel(function, function_obj.unwrap(), None).into(),
234        ));
235    }
236
237    let connection_ids = objects
238        .iter()
239        .filter(|object| object.obj_type == ObjectType::Connection)
240        .map(|object| object.oid.as_connection_id())
241        .collect_vec();
242    for (connection, connection_obj) in Connection::find()
243        .find_also_related(Object)
244        .filter(connection::Column::ConnectionId.is_in(connection_ids))
245        .all(txn)
246        .await?
247    {
248        object_infos.push(PbObjectInfo::Connection(
249            ObjectModel(connection, connection_obj.unwrap(), None).into(),
250        ));
251    }
252
253    let secret_ids = objects
254        .iter()
255        .filter(|object| object.obj_type == ObjectType::Secret)
256        .map(|object| object.oid.as_secret_id())
257        .collect_vec();
258    for (secret, secret_obj) in Secret::find()
259        .find_also_related(Object)
260        .filter(secret::Column::SecretId.is_in(secret_ids))
261        .all(txn)
262        .await?
263    {
264        object_infos.push(PbObjectInfo::Secret(
265            ObjectModel(secret, secret_obj.unwrap(), None).into(),
266        ));
267    }
268
269    let database_ids = objects
270        .iter()
271        .filter(|object| object.obj_type == ObjectType::Database)
272        .map(|object| object.oid.as_database_id())
273        .collect_vec();
274    for (database, database_obj) in Database::find()
275        .find_also_related(Object)
276        .filter(database::Column::DatabaseId.is_in(database_ids))
277        .all(txn)
278        .await?
279    {
280        object_infos.push(PbObjectInfo::Database(
281            ObjectModel(database, database_obj.unwrap(), None).into(),
282        ));
283    }
284
285    let schema_ids = objects
286        .iter()
287        .filter(|object| object.obj_type == ObjectType::Schema)
288        .map(|object| object.oid.as_schema_id())
289        .collect_vec();
290    for (schema, schema_obj) in Schema::find()
291        .find_also_related(Object)
292        .filter(schema::Column::SchemaId.is_in(schema_ids))
293        .all(txn)
294        .await?
295    {
296        object_infos.push(PbObjectInfo::Schema(
297            ObjectModel(schema, schema_obj.unwrap(), None).into(),
298        ));
299    }
300
301    Ok(object_infos)
302}
303
304pub(crate) async fn update_internal_tables(
305    txn: &DatabaseTransaction,
306    object_id: ObjectId,
307    column: object::Column,
308    new_value: impl Into<Value>,
309    objects_to_notify: &mut Vec<PbObjectInfo>,
310) -> MetaResult<()> {
311    let internal_tables = get_internal_tables_by_id(object_id.as_job_id(), txn).await?;
312
313    if !internal_tables.is_empty() {
314        Object::update_many()
315            .col_expr(column, SimpleExpr::Value(new_value.into()))
316            .filter(object::Column::Oid.is_in(internal_tables.clone()))
317            .exec(txn)
318            .await?;
319
320        let table_objs = Table::find()
321            .find_also_related(Object)
322            .filter(table::Column::TableId.is_in(internal_tables))
323            .all(txn)
324            .await?;
325        let streaming_jobs =
326            load_streaming_jobs_by_ids(txn, table_objs.iter().map(|(table, _)| table.job_id()))
327                .await?;
328        for (table, table_obj) in table_objs {
329            let job_id = table.job_id();
330            let streaming_job = streaming_jobs.get(&job_id).cloned();
331            objects_to_notify.push(PbObjectInfo::Table(
332                ObjectModel(table, table_obj.unwrap(), streaming_job).into(),
333            ));
334        }
335    }
336    Ok(())
337}
338
339impl CatalogController {
340    pub(crate) async fn init(&self) -> MetaResult<()> {
341        self.table_catalog_cdc_table_id_update().await?;
342        Ok(())
343    }
344
345    /// Fill in the `cdc_table_id` field for Table with empty `cdc_table_id` and parent Source job.
346    /// NOTES: We assume Table with a parent Source job is a CDC table
347    pub(crate) async fn table_catalog_cdc_table_id_update(&self) -> MetaResult<()> {
348        let inner = self.inner.read().await;
349        let txn = inner.db.begin().await?;
350
351        // select Tables which cdc_table_id is empty and has a parent Source job
352        let table_and_source_id: Vec<(TableId, String, SourceId)> = Table::find()
353            .join(JoinType::InnerJoin, table::Relation::ObjectDependency.def())
354            .join(
355                JoinType::InnerJoin,
356                object_dependency::Relation::Source.def(),
357            )
358            .select_only()
359            .columns([table::Column::TableId, table::Column::Definition])
360            .columns([source::Column::SourceId])
361            .filter(
362                table::Column::TableType.eq(TableType::Table).and(
363                    table::Column::CdcTableId
364                        .is_null()
365                        .or(table::Column::CdcTableId.eq("")),
366                ),
367            )
368            .into_tuple()
369            .all(&txn)
370            .await?;
371
372        // return directly if the result set is empty.
373        if table_and_source_id.is_empty() {
374            return Ok(());
375        }
376
377        info!(table_and_source_id = ?table_and_source_id, "cdc table with empty cdc_table_id");
378
379        let mut cdc_table_ids = HashMap::new();
380        for (table_id, definition, source_id) in table_and_source_id {
381            match extract_external_table_name_from_definition(&definition) {
382                None => {
383                    tracing::warn!(
384                        %table_id,
385                        definition,
386                        "failed to extract cdc table name from table definition.",
387                    )
388                }
389                Some(external_table_name) => {
390                    cdc_table_ids.insert(
391                        table_id,
392                        build_cdc_table_id(source_id, &external_table_name),
393                    );
394                }
395            }
396        }
397
398        for (table_id, cdc_table_id) in cdc_table_ids {
399            Table::update(table::ActiveModel {
400                table_id: Set(table_id as _),
401                cdc_table_id: Set(Some(cdc_table_id)),
402                ..Default::default()
403            })
404            .exec(&txn)
405            .await?;
406        }
407        txn.commit().await?;
408        Ok(())
409    }
410
411    pub(crate) async fn log_cleaned_dirty_jobs(
412        &self,
413        dirty_objs: &[PartialObject],
414        txn: &DatabaseTransaction,
415    ) -> MetaResult<()> {
416        // Record cleaned streaming jobs in event logs.
417        let mut dirty_table_ids = vec![];
418        let mut dirty_source_ids = vec![];
419        let mut dirty_sink_ids = vec![];
420        for dirty_job_obj in dirty_objs {
421            let job_id = dirty_job_obj.oid;
422            let job_type = dirty_job_obj.obj_type;
423            match job_type {
424                ObjectType::Table | ObjectType::Index => dirty_table_ids.push(job_id),
425                ObjectType::Source => dirty_source_ids.push(job_id),
426                ObjectType::Sink => dirty_sink_ids.push(job_id),
427                _ => unreachable!("unexpected streaming job type"),
428            }
429        }
430
431        let mut event_logs = vec![];
432        if !dirty_table_ids.is_empty() {
433            let table_info: Vec<(TableId, String, String)> = Table::find()
434                .select_only()
435                .columns([
436                    table::Column::TableId,
437                    table::Column::Name,
438                    table::Column::Definition,
439                ])
440                .filter(table::Column::TableId.is_in(dirty_table_ids))
441                .into_tuple()
442                .all(txn)
443                .await?;
444            for (table_id, name, definition) in table_info {
445                let event = risingwave_pb::meta::event_log::EventDirtyStreamJobClear {
446                    id: table_id.as_job_id(),
447                    name,
448                    definition,
449                    error: "clear during recovery".to_owned(),
450                };
451                event_logs.push(risingwave_pb::meta::event_log::Event::DirtyStreamJobClear(
452                    event,
453                ));
454            }
455        }
456        if !dirty_source_ids.is_empty() {
457            let source_info: Vec<(SourceId, String, String)> = Source::find()
458                .select_only()
459                .columns([
460                    source::Column::SourceId,
461                    source::Column::Name,
462                    source::Column::Definition,
463                ])
464                .filter(source::Column::SourceId.is_in(dirty_source_ids))
465                .into_tuple()
466                .all(txn)
467                .await?;
468            for (source_id, name, definition) in source_info {
469                let event = risingwave_pb::meta::event_log::EventDirtyStreamJobClear {
470                    id: source_id.as_share_source_job_id(),
471                    name,
472                    definition,
473                    error: "clear during recovery".to_owned(),
474                };
475                event_logs.push(risingwave_pb::meta::event_log::Event::DirtyStreamJobClear(
476                    event,
477                ));
478            }
479        }
480        if !dirty_sink_ids.is_empty() {
481            let sink_info: Vec<(SinkId, String, String)> = Sink::find()
482                .select_only()
483                .columns([
484                    sink::Column::SinkId,
485                    sink::Column::Name,
486                    sink::Column::Definition,
487                ])
488                .filter(sink::Column::SinkId.is_in(dirty_sink_ids))
489                .into_tuple()
490                .all(txn)
491                .await?;
492            for (sink_id, name, definition) in sink_info {
493                let event = risingwave_pb::meta::event_log::EventDirtyStreamJobClear {
494                    id: sink_id.as_job_id(),
495                    name,
496                    definition,
497                    error: "clear during recovery".to_owned(),
498                };
499                event_logs.push(risingwave_pb::meta::event_log::Event::DirtyStreamJobClear(
500                    event,
501                ));
502            }
503        }
504        self.env.event_log_manager_ref().add_event_logs(event_logs);
505        Ok(())
506    }
507
508    pub(crate) async fn clean_dirty_sink_downstreams(txn: &DatabaseTransaction) -> MetaResult<()> {
509        // clean incoming sink from (table)
510        // clean upstream fragment ids from (fragment)
511        // clean stream node from (fragment)
512        // clean upstream actor ids from (actor)
513
514        // The cleanup of fragment and StreamNode is to maintain compatibility with old versions of data. For the
515        // current sink-into-table implementation, there is no need to restore the contents of StreamNode, because the
516        // `UpstreamSinkUnion` operator does not persist any data, but relies on refill during recovery.
517
518        let all_fragment_ids: Vec<FragmentId> = Fragment::find()
519            .select_only()
520            .column(fragment::Column::FragmentId)
521            .into_tuple()
522            .all(txn)
523            .await?;
524
525        let all_fragment_ids: HashSet<_> = all_fragment_ids.into_iter().collect();
526
527        let all_sink_into_tables: Vec<Option<TableId>> = Sink::find()
528            .select_only()
529            .column(sink::Column::TargetTable)
530            .filter(sink::Column::TargetTable.is_not_null())
531            .into_tuple()
532            .all(txn)
533            .await?;
534
535        let mut table_with_incoming_sinks: HashSet<TableId> = HashSet::new();
536        for target_table_id in all_sink_into_tables {
537            table_with_incoming_sinks.insert(target_table_id.expect("filter by non null"));
538        }
539
540        // no need to update, returning
541        if table_with_incoming_sinks.is_empty() {
542            return Ok(());
543        }
544
545        for table_id in table_with_incoming_sinks {
546            tracing::info!("cleaning dirty table sink downstream table {}", table_id);
547
548            let fragments: Vec<(FragmentId, StreamNode)> = Fragment::find()
549                .select_only()
550                .columns(vec![
551                    fragment::Column::FragmentId,
552                    fragment::Column::StreamNode,
553                ])
554                .filter(fragment::Column::JobId.eq(table_id).and(
555                    // dirty downstream should be materialize fragment of table
556                    FragmentTypeMask::intersects(FragmentTypeFlag::Mview),
557                ))
558                .into_tuple()
559                .all(txn)
560                .await?;
561
562            for (fragment_id, stream_node) in fragments {
563                {
564                    let mut dirty_upstream_fragment_ids = HashSet::new();
565
566                    let mut pb_stream_node = stream_node.to_protobuf();
567
568                    visit_stream_node_cont_mut(&mut pb_stream_node, |node| {
569                        if let Some(NodeBody::Union(_)) = node.node_body {
570                            node.input.retain_mut(|input| match &mut input.node_body {
571                                Some(NodeBody::Project(_)) => {
572                                    let body = Itertools::exactly_one(input.input.iter()).unwrap();
573                                    let Some(NodeBody::Merge(merge_node)) = &body.node_body else {
574                                        unreachable!("expect merge node");
575                                    };
576                                    if all_fragment_ids.contains(&(merge_node.upstream_fragment_id))
577                                    {
578                                        true
579                                    } else {
580                                        dirty_upstream_fragment_ids
581                                            .insert(merge_node.upstream_fragment_id);
582                                        false
583                                    }
584                                }
585                                Some(NodeBody::Merge(merge_node)) => {
586                                    if all_fragment_ids.contains(&(merge_node.upstream_fragment_id))
587                                    {
588                                        true
589                                    } else {
590                                        dirty_upstream_fragment_ids
591                                            .insert(merge_node.upstream_fragment_id);
592                                        false
593                                    }
594                                }
595                                _ => false,
596                            });
597                        }
598                        true
599                    });
600
601                    tracing::info!(
602                        "cleaning dirty table sink fragment {:?} from downstream fragment {}",
603                        dirty_upstream_fragment_ids,
604                        fragment_id
605                    );
606
607                    if !dirty_upstream_fragment_ids.is_empty() {
608                        tracing::info!(
609                            "fixing dirty stream node in downstream fragment {}",
610                            fragment_id
611                        );
612                        Fragment::update_many()
613                            .col_expr(
614                                fragment::Column::StreamNode,
615                                StreamNode::from(&pb_stream_node).into(),
616                            )
617                            .filter(fragment::Column::FragmentId.eq(fragment_id))
618                            .exec(txn)
619                            .await?;
620                    }
621                }
622            }
623        }
624
625        Ok(())
626    }
627
628    pub async fn has_any_streaming_jobs(&self) -> MetaResult<bool> {
629        let inner = self.inner.read().await;
630        let count = streaming_job::Entity::find().count(&inner.db).await?;
631        Ok(count > 0)
632    }
633
634    pub async fn find_creating_streaming_job_ids(
635        &self,
636        infos: Vec<PbCreatingJobInfo>,
637    ) -> MetaResult<Vec<ObjectId>> {
638        let inner = self.inner.read().await;
639
640        type JobKey = (DatabaseId, SchemaId, String);
641
642        // Index table is already included if we still assign the same name for index table as the index.
643        let creating_tables: Vec<(ObjectId, String, DatabaseId, SchemaId)> = Table::find()
644            .select_only()
645            .columns([table::Column::TableId, table::Column::Name])
646            .columns([object::Column::DatabaseId, object::Column::SchemaId])
647            .join(JoinType::InnerJoin, table::Relation::Object1.def())
648            .join(JoinType::InnerJoin, object::Relation::StreamingJob.def())
649            .filter(streaming_job::Column::JobStatus.eq(JobStatus::Creating))
650            .into_tuple()
651            .all(&inner.db)
652            .await?;
653        let creating_sinks: Vec<(ObjectId, String, DatabaseId, SchemaId)> = Sink::find()
654            .select_only()
655            .columns([sink::Column::SinkId, sink::Column::Name])
656            .columns([object::Column::DatabaseId, object::Column::SchemaId])
657            .join(JoinType::InnerJoin, sink::Relation::Object.def())
658            .join(JoinType::InnerJoin, object::Relation::StreamingJob.def())
659            .filter(streaming_job::Column::JobStatus.eq(JobStatus::Creating))
660            .into_tuple()
661            .all(&inner.db)
662            .await?;
663        let creating_subscriptions: Vec<(ObjectId, String, DatabaseId, SchemaId)> =
664            Subscription::find()
665                .select_only()
666                .columns([
667                    subscription::Column::SubscriptionId,
668                    subscription::Column::Name,
669                ])
670                .columns([object::Column::DatabaseId, object::Column::SchemaId])
671                .join(JoinType::InnerJoin, subscription::Relation::Object.def())
672                .join(JoinType::InnerJoin, object::Relation::StreamingJob.def())
673                .filter(streaming_job::Column::JobStatus.eq(JobStatus::Creating))
674                .into_tuple()
675                .all(&inner.db)
676                .await?;
677
678        let mut job_mapping: HashMap<JobKey, ObjectId> = creating_tables
679            .into_iter()
680            .chain(creating_sinks)
681            .chain(creating_subscriptions)
682            .map(|(id, name, database_id, schema_id)| ((database_id, schema_id, name), id))
683            .collect();
684
685        Ok(infos
686            .into_iter()
687            .flat_map(|info| job_mapping.remove(&(info.database_id, info.schema_id, info.name)))
688            .collect())
689    }
690}