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