Skip to main content

risingwave_frontend/binder/relation/
table_or_source.rs

1// Copyright 2022 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 std::collections::BTreeMap;
16use std::sync::Arc;
17
18use itertools::Itertools;
19use risingwave_common::acl::AclMode;
20use risingwave_common::bail_not_implemented;
21use risingwave_common::catalog::{
22    Engine, Field, debug_assert_column_ids_distinct, is_system_schema,
23};
24use risingwave_common::session_config::USER_NAME_WILD_CARD;
25use risingwave_connector::WithPropertiesExt;
26use risingwave_connector::sink::catalog::SinkCatalog;
27use risingwave_connector::sink::iceberg::IcebergMetadataTableType;
28use risingwave_pb::secret::PbSecretRef;
29use risingwave_pb::user::grant_privilege::PbObject;
30use risingwave_sqlparser::ast::{AsOf, ObjectName, Statement, TableAlias};
31use risingwave_sqlparser::parser::Parser;
32use thiserror_ext::AsReport;
33
34use super::BoundShare;
35use crate::binder::relation::BoundShareInput;
36use crate::binder::{BindFor, Binder, Relation};
37use crate::catalog::root_catalog::SchemaPath;
38use crate::catalog::source_catalog::SourceCatalog;
39use crate::catalog::system_catalog::SystemTableCatalog;
40use crate::catalog::table_catalog::{TableCatalog, TableType};
41use crate::catalog::view_catalog::ViewCatalog;
42use crate::catalog::{CatalogError, CatalogResult, DatabaseId, IndexCatalog, TableId};
43use crate::error::ErrorCode::PermissionDenied;
44use crate::error::{ErrorCode, Result, RwError};
45use crate::handler::privilege::ObjectCheckItem;
46
47#[derive(Debug, Clone)]
48pub struct BoundBaseTable {
49    pub table_id: TableId,
50    pub table_catalog: Arc<TableCatalog>,
51    pub table_indexes: Vec<Arc<IndexCatalog>>,
52    pub as_of: Option<AsOf>,
53}
54
55#[derive(Debug, Clone)]
56pub struct BoundSystemTable {
57    pub table_id: TableId,
58    pub sys_table_catalog: Arc<SystemTableCatalog>,
59}
60
61#[derive(Debug, Clone)]
62pub struct BoundIcebergMetadataTable {
63    pub metadata_type: IcebergMetadataTableType,
64    pub properties: BTreeMap<String, String>,
65    pub secret_refs: BTreeMap<String, PbSecretRef>,
66    pub as_of: Option<AsOf>,
67}
68
69enum IcebergMetadataBaseRelation {
70    Table(Arc<TableCatalog>),
71    Source(SourceCatalog, bool),
72    Sink(Arc<SinkCatalog>),
73}
74
75#[derive(Debug, Clone)]
76pub struct BoundSource {
77    pub catalog: SourceCatalog,
78    pub as_of: Option<AsOf>,
79}
80
81impl BoundSource {
82    pub fn is_shareable_cdc_connector(&self) -> bool {
83        self.catalog.with_properties.is_shareable_cdc_connector()
84    }
85
86    pub fn is_shared(&self) -> bool {
87        self.catalog.info.is_shared()
88    }
89}
90
91impl Binder {
92    pub fn bind_catalog_relation_by_object_name(
93        &mut self,
94        object_name: &ObjectName,
95        bind_creating_relations: bool,
96    ) -> Result<Relation> {
97        let (schema_name, table_name) =
98            Binder::resolve_schema_qualified_name(&self.db_name, object_name)?;
99        self.bind_catalog_relation_by_name(
100            None,
101            schema_name.as_deref(),
102            &table_name,
103            None,
104            None,
105            bind_creating_relations,
106        )
107    }
108
109    /// Binds table or source, or logical view according to what we get from the catalog.
110    pub fn bind_catalog_relation_by_name(
111        &mut self,
112        db_name: Option<&str>,
113        schema_name: Option<&str>,
114        table_name: &str,
115        alias: Option<&TableAlias>,
116        as_of: Option<&AsOf>,
117        bind_creating_relations: bool,
118    ) -> Result<Relation> {
119        // define some helper functions converting catalog to bound relation
120        let resolve_sys_table_relation = |sys_table_catalog: &Arc<SystemTableCatalog>| {
121            let table = BoundSystemTable {
122                table_id: sys_table_catalog.id(),
123                sys_table_catalog: sys_table_catalog.clone(),
124            };
125            (
126                Relation::SystemTable(Box::new(table)),
127                sys_table_catalog
128                    .columns
129                    .iter()
130                    .map(|c| (c.is_hidden, Field::from(&c.column_desc)))
131                    .collect_vec(),
132            )
133        };
134
135        // check db_name if exists first
136        if let Some(db_name) = db_name {
137            let _ = self.catalog.get_database_by_name(db_name)?;
138        }
139
140        // start to bind
141        let (ret, columns) = {
142            match schema_name {
143                Some(schema_name) => {
144                    let db_name = db_name.unwrap_or(&self.db_name).to_owned();
145                    let schema_path = SchemaPath::Name(schema_name);
146                    if is_system_schema(schema_name) {
147                        if let Ok(sys_table_catalog) =
148                            self.catalog
149                                .get_sys_table_by_name(&db_name, schema_name, table_name)
150                        {
151                            resolve_sys_table_relation(sys_table_catalog)
152                        } else if let Ok((view_catalog, _)) =
153                            self.catalog
154                                .get_view_by_name(&db_name, schema_path, table_name)
155                        {
156                            self.resolve_view_relation(&view_catalog.clone())?
157                        } else {
158                            bail_not_implemented!(
159                                issue = 1695,
160                                r###"{}.{} is not supported, please use `SHOW` commands for now.
161`SHOW TABLES`,
162`SHOW MATERIALIZED VIEWS`,
163`DESCRIBE <table>`,
164`SHOW COLUMNS FROM [table]`
165"###,
166                                schema_name,
167                                table_name
168                            );
169                        }
170                    } else if let Some(source_catalog) =
171                        self.temporary_source_manager.get_source(table_name)
172                    // don't care about the database and schema
173                    {
174                        self.resolve_source_relation(&source_catalog.clone(), as_of, true)?
175                    } else if let Ok((table_catalog, schema_name)) = self
176                        .catalog
177                        .get_any_table_by_name(&db_name, schema_path, table_name)
178                        && (bind_creating_relations
179                            || table_catalog.is_internal_table()
180                            || table_catalog.is_created())
181                    {
182                        self.resolve_table_relation(
183                            table_catalog.clone(),
184                            &db_name,
185                            schema_name,
186                            as_of,
187                        )?
188                    } else if let Ok((source_catalog, _)) =
189                        self.catalog
190                            .get_source_by_name(&db_name, schema_path, table_name)
191                    {
192                        self.resolve_source_relation(&source_catalog.clone(), as_of, false)?
193                    } else if let Ok((view_catalog, _)) =
194                        self.catalog
195                            .get_view_by_name(&db_name, schema_path, table_name)
196                    {
197                        self.resolve_view_relation(&view_catalog.clone())?
198                    } else if let Some(table_catalog) =
199                        self.staging_catalog_manager.get_table(table_name)
200                    {
201                        // don't care about the database and schema
202                        self.resolve_table_relation(
203                            table_catalog.clone().into(),
204                            &db_name,
205                            schema_name,
206                            as_of,
207                        )?
208                    } else {
209                        self.resolve_iceberg_metadata_relation(
210                            &db_name,
211                            Some(schema_name),
212                            table_name,
213                            as_of,
214                        )?
215                    }
216                }
217                None => (|| {
218                    // If schema is not specified, db must be unspecified.
219                    // So we should always use current database here.
220                    assert!(db_name.is_none());
221                    let db_name = self.db_name.clone();
222                    let user_name = self.auth_context.user_name.clone();
223
224                    for path in self.search_path.path() {
225                        if is_system_schema(path)
226                            && let Ok(sys_table_catalog) = self
227                                .catalog
228                                .get_sys_table_by_name(&db_name, path, table_name)
229                        {
230                            return Ok(resolve_sys_table_relation(sys_table_catalog));
231                        } else {
232                            let schema_name = if path == USER_NAME_WILD_CARD {
233                                &user_name
234                            } else {
235                                &path.clone()
236                            };
237
238                            if let Ok(schema) =
239                                self.catalog.get_schema_by_name(&db_name, schema_name)
240                            {
241                                if let Some(source_catalog) =
242                                    self.temporary_source_manager.get_source(table_name)
243                                // don't care about the database and schema
244                                {
245                                    return self.resolve_source_relation(
246                                        &source_catalog.clone(),
247                                        as_of,
248                                        true,
249                                    );
250                                } else if let Some(table_catalog) =
251                                    schema.get_any_table_by_name(table_name)
252                                    && (bind_creating_relations
253                                        || table_catalog.is_internal_table()
254                                        || table_catalog.is_created())
255                                {
256                                    return self.resolve_table_relation(
257                                        table_catalog.clone(),
258                                        &db_name,
259                                        schema_name,
260                                        as_of,
261                                    );
262                                } else if let Some(source_catalog) =
263                                    schema.get_source_by_name(table_name)
264                                {
265                                    return self.resolve_source_relation(
266                                        &source_catalog.clone(),
267                                        as_of,
268                                        false,
269                                    );
270                                } else if let Some(view_catalog) =
271                                    schema.get_view_by_name(table_name)
272                                {
273                                    return self.resolve_view_relation(&view_catalog.clone());
274                                } else if let Some(table_catalog) =
275                                    self.staging_catalog_manager.get_table(table_name)
276                                {
277                                    // don't care about the database and schema
278                                    return self.resolve_table_relation(
279                                        table_catalog.clone().into(),
280                                        &db_name,
281                                        schema_name,
282                                        as_of,
283                                    );
284                                }
285                            }
286                        }
287                    }
288
289                    self.resolve_iceberg_metadata_relation(&db_name, None, table_name, as_of)
290                })()?,
291            }
292        };
293
294        self.bind_table_to_context(
295            columns,
296            table_name.to_owned(),
297            schema_name.map(|s| s.to_owned()),
298            alias,
299        )?;
300        Ok(ret)
301    }
302
303    fn resolve_iceberg_metadata_relation(
304        &mut self,
305        db_name: &str,
306        schema_name: Option<&str>,
307        relation_name: &str,
308        as_of: Option<&AsOf>,
309    ) -> Result<(Relation, Vec<(bool, Field)>)> {
310        let Some((base_name, suffix)) = relation_name.rsplit_once('$') else {
311            return Err(CatalogError::not_found("table or source", relation_name).into());
312        };
313        let Some(metadata_type) = IcebergMetadataTableType::from_suffix(suffix) else {
314            return Err(CatalogError::not_found("table or source", relation_name).into());
315        };
316        if base_name.is_empty() {
317            return Err(CatalogError::not_found("table or source", relation_name).into());
318        }
319
320        if metadata_type == IcebergMetadataTableType::Snapshots && as_of.is_some() {
321            return Err(ErrorCode::BindError(
322                "time travel is only supported for Iceberg manifests and files metadata relations"
323                    .to_owned(),
324            )
325            .into());
326        }
327        if matches!(
328            as_of,
329            Some(AsOf::ProcessTime | AsOf::ProcessTimeWithInterval(_))
330        ) {
331            bail_not_implemented!(
332                "As Of ProcessTime() is not supported for Iceberg metadata relations."
333            );
334        }
335
336        let (base_relation, resolved_schema_name) = if let Some(source) =
337            self.temporary_source_manager.get_source(base_name)
338        {
339            (
340                IcebergMetadataBaseRelation::Source(source.clone(), true),
341                None,
342            )
343        } else {
344            let schema_path = self.bind_schema_path(schema_name);
345            let Some((base_relation, resolved_schema_name)) =
346                schema_path.try_find(|schema_name| -> CatalogResult<_> {
347                    let schema = self.catalog.get_schema_by_name(db_name, schema_name)?;
348                    Ok(schema
349                        .get_created_table_by_name(base_name)
350                        .map(|table| IcebergMetadataBaseRelation::Table(table.clone()))
351                        .or_else(|| {
352                            schema.get_source_by_name(base_name).map(|source| {
353                                IcebergMetadataBaseRelation::Source(source.as_ref().clone(), false)
354                            })
355                        })
356                        .or_else(|| {
357                            schema
358                                .get_created_sink_by_name(base_name)
359                                .map(|sink| IcebergMetadataBaseRelation::Sink(sink.clone()))
360                        }))
361                })?
362            else {
363                return Err(
364                    CatalogError::not_found("Iceberg table, source, or sink", base_name).into(),
365                );
366            };
367            (base_relation, Some(resolved_schema_name.to_owned()))
368        };
369
370        let (properties, secret_refs) = match base_relation {
371            IcebergMetadataBaseRelation::Table(table) => {
372                self.check_privilege(
373                    ObjectCheckItem::new(
374                        table.owner,
375                        AclMode::Select,
376                        table.name.clone(),
377                        table.id(),
378                    ),
379                    table.database_id,
380                )?;
381                if table.engine() != Engine::Iceberg {
382                    return Err(ErrorCode::BindError(format!(
383                        "metadata relation \"{relation_name}\" requires an Iceberg engine table, source, or sink, but table \"{base_name}\" uses {:?}",
384                        table.engine()
385                    ))
386                    .into());
387                }
388                self.included_relations.insert(table.id().as_object_id());
389
390                let sink_name = table.iceberg_sink_name().ok_or_else(|| {
391                    ErrorCode::CatalogError(
392                        format!("no Iceberg sink found for table \"{base_name}\"").into(),
393                    )
394                })?;
395                let sink = self
396                    .catalog
397                    .get_created_sink_by_name(
398                        db_name,
399                        SchemaPath::Name(
400                            resolved_schema_name
401                                .as_deref()
402                                .expect("catalog tables always have a schema"),
403                        ),
404                        &sink_name,
405                    )
406                    .map_err(|_| {
407                        ErrorCode::CatalogError(
408                            format!(
409                                "Iceberg sink \"{sink_name}\" not found for table \"{base_name}\""
410                            )
411                            .into(),
412                        )
413                    })?
414                    .0
415                    .clone();
416                (sink.properties.clone(), sink.secret_refs.clone())
417            }
418            IcebergMetadataBaseRelation::Source(source, is_temporary) => {
419                if !is_temporary {
420                    self.check_privilege(
421                        ObjectCheckItem::new(
422                            source.owner,
423                            AclMode::Select,
424                            source.name.clone(),
425                            source.id,
426                        ),
427                        source.database_id,
428                    )?;
429                }
430                if !source.is_iceberg_connector() {
431                    return Err(ErrorCode::BindError(format!(
432                        "metadata relation \"{relation_name}\" requires an Iceberg source, but source \"{base_name}\" uses a different connector"
433                    ))
434                    .into());
435                }
436                self.included_relations.insert(source.id.as_object_id());
437                source.with_properties.into_parts()
438            }
439            IcebergMetadataBaseRelation::Sink(sink) => {
440                self.check_privilege(
441                    ObjectCheckItem::new(sink.owner, AclMode::Select, sink.name.clone(), sink.id),
442                    sink.database_id,
443                )?;
444                if !sink.properties.is_iceberg_connector() {
445                    return Err(ErrorCode::BindError(format!(
446                        "metadata relation \"{relation_name}\" requires an Iceberg sink, but sink \"{base_name}\" uses a different connector"
447                    ))
448                    .into());
449                }
450                self.included_relations.insert(sink.id.as_object_id());
451                (sink.properties.clone(), sink.secret_refs.clone())
452            }
453        };
454
455        let columns = metadata_type
456            .schema()
457            .fields
458            .into_iter()
459            .map(|field| (false, field))
460            .collect();
461        Ok((
462            Relation::IcebergMetadataTable(Box::new(BoundIcebergMetadataTable {
463                metadata_type,
464                properties,
465                secret_refs,
466                as_of: as_of.cloned(),
467            })),
468            columns,
469        ))
470    }
471
472    pub(crate) fn check_privilege(
473        &self,
474        item: ObjectCheckItem,
475        database_id: DatabaseId,
476    ) -> Result<()> {
477        // security invoker is disabled for view, ignore privilege check.
478        if self.context.disable_security_invoker {
479            return Ok(());
480        }
481
482        match self.bind_for {
483            BindFor::Stream | BindFor::Batch => {
484                // reject sources for cross-db access
485                if matches!(self.bind_for, BindFor::Stream)
486                    && self.database_id != database_id
487                    && matches!(item.object, PbObject::SourceId(_))
488                {
489                    return Err(PermissionDenied(format!(
490                        "SOURCE \"{}\" is not allowed for cross-db access",
491                        item.name
492                    ))
493                    .into());
494                }
495                if let Some(user) = self.user.get_user_by_name(&self.auth_context.user_name) {
496                    if user.is_super || user.id == item.owner {
497                        return Ok(());
498                    }
499                    if !user.has_privilege(item.object, item.mode) {
500                        return Err(PermissionDenied(item.error_message()).into());
501                    }
502
503                    // check CONNECT privilege for cross-db access
504                    if self.database_id != database_id
505                        && !user.has_privilege(database_id, AclMode::Connect)
506                    {
507                        let db_name = self.catalog.get_database_by_id(database_id)?.name.clone();
508
509                        return Err(PermissionDenied(format!(
510                            "permission denied for database \"{db_name}\""
511                        ))
512                        .into());
513                    }
514                } else {
515                    return Err(PermissionDenied("Session user is invalid".to_owned()).into());
516                }
517            }
518            BindFor::Ddl | BindFor::System => {
519                // do nothing.
520            }
521        }
522        Ok(())
523    }
524
525    fn resolve_table_relation(
526        &mut self,
527        table_catalog: Arc<TableCatalog>,
528        db_name: &str,
529        schema_name: &str,
530        as_of: Option<&AsOf>,
531    ) -> Result<(Relation, Vec<(bool, Field)>)> {
532        let table_id = table_catalog.id();
533        let columns = table_catalog
534            .columns
535            .iter()
536            .map(|c| (c.is_hidden, Field::from(&c.column_desc)))
537            .collect_vec();
538        self.check_privilege(
539            ObjectCheckItem::new(
540                table_catalog.owner,
541                AclMode::Select,
542                table_catalog.name.clone(),
543                table_id,
544            ),
545            table_catalog.database_id,
546        )?;
547        self.included_relations.insert(table_id.as_object_id());
548
549        let table_indexes = self.resolve_table_indexes(db_name, schema_name, table_id)?;
550
551        let table = BoundBaseTable {
552            table_id,
553            table_catalog,
554            table_indexes,
555            as_of: as_of.cloned(),
556        };
557
558        Ok::<_, RwError>((Relation::BaseTable(Box::new(table)), columns))
559    }
560
561    fn resolve_source_relation(
562        &mut self,
563        source_catalog: &SourceCatalog,
564        as_of: Option<&AsOf>,
565        is_temporary: bool,
566    ) -> Result<(Relation, Vec<(bool, Field)>)> {
567        debug_assert_column_ids_distinct(&source_catalog.columns);
568        if !is_temporary {
569            self.check_privilege(
570                ObjectCheckItem::new(
571                    source_catalog.owner,
572                    AclMode::Select,
573                    source_catalog.name.clone(),
574                    source_catalog.id,
575                ),
576                source_catalog.database_id,
577            )?;
578        }
579        self.included_relations
580            .insert(source_catalog.id.as_object_id());
581        Ok((
582            Relation::Source(Box::new(BoundSource {
583                catalog: source_catalog.clone(),
584                as_of: as_of.cloned(),
585            })),
586            source_catalog
587                .columns
588                .iter()
589                .map(|c| (c.is_hidden, Field::from(&c.column_desc)))
590                .collect_vec(),
591        ))
592    }
593
594    fn resolve_view_relation(
595        &mut self,
596        view_catalog: &ViewCatalog,
597    ) -> Result<(Relation, Vec<(bool, Field)>)> {
598        if !view_catalog.is_system_view() {
599            self.check_privilege(
600                ObjectCheckItem::new(
601                    view_catalog.owner,
602                    AclMode::Select,
603                    view_catalog.name.clone(),
604                    view_catalog.id,
605                ),
606                view_catalog.database_id,
607            )?;
608        }
609
610        let ast = Parser::parse_sql(&view_catalog.sql)
611            .expect("a view's sql should be parsed successfully");
612        let Statement::Query(query) = Itertools::exactly_one(ast.into_iter())
613            .expect("a view should contain only one statement")
614        else {
615            unreachable!("a view should contain a query statement");
616        };
617        let query = self.bind_query_for_view(&query).map_err(|e| {
618            ErrorCode::BindError(format!(
619                "failed to bind view {}, sql: {}\nerror: {}",
620                view_catalog.name,
621                view_catalog.sql,
622                e.as_report()
623            ))
624        })?;
625
626        let columns = view_catalog.columns.clone();
627
628        if !itertools::equal(
629            query.schema().fields().iter().map(|f| &f.data_type),
630            view_catalog.columns.iter().map(|f| &f.data_type),
631        ) {
632            return Err(ErrorCode::BindError(format!(
633                "failed to bind view {}. The SQL's schema is different from catalog's schema sql: {}, bound schema: {:?}, catalog schema: {:?}",
634                view_catalog.name, view_catalog.sql, query.schema(), columns
635            )).into());
636        }
637
638        let share_id = match self.shared_views.get(&view_catalog.id) {
639            Some(share_id) => *share_id,
640            None => {
641                let share_id = self.next_share_id();
642                self.shared_views.insert(view_catalog.id, share_id);
643                self.included_relations
644                    .insert(view_catalog.id.as_object_id());
645                share_id
646            }
647        };
648        Ok((
649            Relation::Share(Box::new(BoundShare {
650                share_id,
651                input: BoundShareInput::Query(query),
652            })),
653            columns.iter().map(|c| (false, c.clone())).collect_vec(),
654        ))
655    }
656
657    fn resolve_table_indexes(
658        &self,
659        db_name: &str,
660        schema_name: &str,
661        table_id: TableId,
662    ) -> Result<Vec<Arc<IndexCatalog>>> {
663        let schema = self.catalog.get_schema_by_name(db_name, schema_name)?;
664        assert!(
665            schema.get_table_by_id(table_id).is_some() || table_id.is_placeholder(),
666            "table {table_id} not found in {db_name}.{schema_name}"
667        );
668
669        Ok(schema.get_created_indexes_by_table_id(table_id))
670    }
671
672    pub(crate) fn bind_table(
673        &mut self,
674        schema_name: Option<&str>,
675        table_name: &str,
676    ) -> Result<BoundBaseTable> {
677        let db_name = &self.db_name;
678        let schema_path = self.bind_schema_path(schema_name);
679        let (table_catalog, schema_name) =
680            self.catalog
681                .get_created_table_by_name(db_name, schema_path, table_name)?;
682        let table_catalog = table_catalog.clone();
683
684        let table_id = table_catalog.id();
685        let table_indexes = self.resolve_table_indexes(db_name, schema_name, table_id)?;
686
687        let columns = table_catalog.columns.clone();
688
689        self.bind_table_to_context(
690            columns
691                .iter()
692                .map(|c| (c.is_hidden, (&c.column_desc).into())),
693            table_name.to_owned(),
694            Some(schema_name.to_owned()),
695            None,
696        )?;
697
698        Ok(BoundBaseTable {
699            table_id,
700            table_catalog,
701            table_indexes,
702            as_of: None,
703        })
704    }
705
706    pub(crate) fn check_for_dml(table: &TableCatalog, is_insert: bool) -> Result<()> {
707        let table_name = &table.name;
708        match table.table_type() {
709            TableType::Table => {}
710            TableType::Index | TableType::VectorIndex => {
711                return Err(ErrorCode::InvalidInputSyntax(format!(
712                    "cannot change index \"{table_name}\""
713                ))
714                .into());
715            }
716            TableType::MaterializedView => {
717                return Err(ErrorCode::InvalidInputSyntax(format!(
718                    "cannot change materialized view \"{table_name}\""
719                ))
720                .into());
721            }
722            TableType::Internal => {
723                return Err(ErrorCode::InvalidInputSyntax(format!(
724                    "cannot change internal table \"{table_name}\""
725                ))
726                .into());
727            }
728        }
729
730        if table.append_only && !is_insert {
731            return Err(ErrorCode::BindError(
732                "append-only table does not support update or delete".to_owned(),
733            )
734            .into());
735        }
736
737        Ok(())
738    }
739}