1use 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 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 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 if let Some(db_name) = db_name {
137 let _ = self.catalog.get_database_by_name(db_name)?;
138 }
139
140 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 {
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 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 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 {
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 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::ProcessTimeBroadcast | 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 if self.context.disable_security_invoker {
479 return Ok(());
480 }
481
482 match self.bind_for {
483 BindFor::Stream | BindFor::Batch => {
484 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 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 }
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 source_catalog.validate_iceberg_update_source()?;
568 debug_assert_column_ids_distinct(&source_catalog.columns);
569 if !is_temporary {
570 self.check_privilege(
571 ObjectCheckItem::new(
572 source_catalog.owner,
573 AclMode::Select,
574 source_catalog.name.clone(),
575 source_catalog.id,
576 ),
577 source_catalog.database_id,
578 )?;
579 }
580 self.included_relations
581 .insert(source_catalog.id.as_object_id());
582 Ok((
583 Relation::Source(Box::new(BoundSource {
584 catalog: source_catalog.clone(),
585 as_of: as_of.cloned(),
586 })),
587 source_catalog
588 .columns
589 .iter()
590 .map(|c| (c.is_hidden, Field::from(&c.column_desc)))
591 .collect_vec(),
592 ))
593 }
594
595 fn resolve_view_relation(
596 &mut self,
597 view_catalog: &ViewCatalog,
598 ) -> Result<(Relation, Vec<(bool, Field)>)> {
599 if !view_catalog.is_system_view() {
600 self.check_privilege(
601 ObjectCheckItem::new(
602 view_catalog.owner,
603 AclMode::Select,
604 view_catalog.name.clone(),
605 view_catalog.id,
606 ),
607 view_catalog.database_id,
608 )?;
609 }
610
611 let ast = Parser::parse_sql(&view_catalog.sql).map_err(|_| {
614 ErrorCode::BindError(format!(
615 "failed to parse the stored definition of view {}",
616 view_catalog.name
617 ))
618 })?;
619 let Statement::Query(query) = Itertools::exactly_one(ast.into_iter())
620 .expect("a view should contain only one statement")
621 else {
622 unreachable!("a view should contain a query statement");
623 };
624 let query = self.bind_query_for_view(&query).map_err(|e| {
625 ErrorCode::BindError(format!(
626 "failed to bind view {}, sql: {}\nerror: {}",
627 view_catalog.name,
628 view_catalog.sql,
629 e.as_report()
630 ))
631 })?;
632
633 let columns = view_catalog.columns.clone();
634
635 if !itertools::equal(
636 query.schema().fields().iter().map(|f| &f.data_type),
637 view_catalog.columns.iter().map(|f| &f.data_type),
638 ) {
639 return Err(ErrorCode::BindError(format!(
640 "failed to bind view {}. The SQL's schema is different from catalog's schema sql: {}, bound schema: {:?}, catalog schema: {:?}",
641 view_catalog.name, view_catalog.sql, query.schema(), columns
642 )).into());
643 }
644
645 let share_id = match self.shared_views.get(&view_catalog.id) {
646 Some(share_id) => *share_id,
647 None => {
648 let share_id = self.next_share_id();
649 self.shared_views.insert(view_catalog.id, share_id);
650 self.included_relations
651 .insert(view_catalog.id.as_object_id());
652 share_id
653 }
654 };
655 Ok((
656 Relation::Share(Box::new(BoundShare {
657 share_id,
658 input: BoundShareInput::Query(query),
659 })),
660 columns.iter().map(|c| (false, c.clone())).collect_vec(),
661 ))
662 }
663
664 fn resolve_table_indexes(
665 &self,
666 db_name: &str,
667 schema_name: &str,
668 table_id: TableId,
669 ) -> Result<Vec<Arc<IndexCatalog>>> {
670 let schema = self.catalog.get_schema_by_name(db_name, schema_name)?;
671 assert!(
672 schema.get_table_by_id(table_id).is_some() || table_id.is_placeholder(),
673 "table {table_id} not found in {db_name}.{schema_name}"
674 );
675
676 Ok(schema.get_created_indexes_by_table_id(table_id))
677 }
678
679 pub(crate) fn bind_table(
680 &mut self,
681 schema_name: Option<&str>,
682 table_name: &str,
683 ) -> Result<BoundBaseTable> {
684 let db_name = &self.db_name;
685 let schema_path = self.bind_schema_path(schema_name);
686 let (table_catalog, schema_name) =
687 self.catalog
688 .get_created_table_by_name(db_name, schema_path, table_name)?;
689 let table_catalog = table_catalog.clone();
690
691 let table_id = table_catalog.id();
692 let table_indexes = self.resolve_table_indexes(db_name, schema_name, table_id)?;
693
694 let columns = table_catalog.columns.clone();
695
696 self.bind_table_to_context(
697 columns
698 .iter()
699 .map(|c| (c.is_hidden, (&c.column_desc).into())),
700 table_name.to_owned(),
701 Some(schema_name.to_owned()),
702 None,
703 )?;
704
705 Ok(BoundBaseTable {
706 table_id,
707 table_catalog,
708 table_indexes,
709 as_of: None,
710 })
711 }
712
713 pub(crate) fn check_for_dml(table: &TableCatalog, is_insert: bool) -> Result<()> {
714 let table_name = &table.name;
715 match table.table_type() {
716 TableType::Table => {}
717 TableType::Index | TableType::VectorIndex => {
718 return Err(ErrorCode::InvalidInputSyntax(format!(
719 "cannot change index \"{table_name}\""
720 ))
721 .into());
722 }
723 TableType::MaterializedView => {
724 return Err(ErrorCode::InvalidInputSyntax(format!(
725 "cannot change materialized view \"{table_name}\""
726 ))
727 .into());
728 }
729 TableType::Internal => {
730 return Err(ErrorCode::InvalidInputSyntax(format!(
731 "cannot change internal table \"{table_name}\""
732 ))
733 .into());
734 }
735 }
736
737 if table.append_only && !is_insert {
738 return Err(ErrorCode::BindError(
739 "append-only table does not support update or delete".to_owned(),
740 )
741 .into());
742 }
743
744 Ok(())
745 }
746}
747
748#[cfg(test)]
749mod tests {
750 use risingwave_common::catalog::{DEFAULT_SUPER_USER_ID, SYS_CATALOG_START_ID};
751
752 use super::*;
753 use crate::binder::test_utils::mock_binder;
754
755 #[tokio::test]
756 async fn test_view_with_unparsable_sql() {
757 let view = ViewCatalog {
758 id: (SYS_CATALOG_START_ID as u32).into(),
760 name: "v".to_owned(),
761 schema_id: 0.into(),
762 database_id: 0.into(),
763 owner: DEFAULT_SUPER_USER_ID,
764 properties: Default::default(),
765 sql: "SELECT 'secret' FROM".to_owned(),
766 columns: vec![],
767 created_at_epoch: None,
768 created_at_cluster_version: None,
769 };
770 let error = mock_binder()
771 .resolve_view_relation(&view)
772 .unwrap_err()
773 .to_report_string();
774 assert!(error.contains("view v"), "{error}");
775 assert!(!error.contains("secret"), "{error}");
776 }
777}