Skip to main content

paimon_datafusion/
catalog.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Paimon catalog integration for DataFusion.
19
20use std::collections::{HashMap, HashSet, VecDeque};
21use std::fmt::Debug;
22use std::sync::Arc;
23use std::sync::RwLock;
24
25use async_trait::async_trait;
26use datafusion::catalog::{CatalogProvider, MemorySchemaProvider, SchemaProvider};
27use datafusion::common::{plan_datafusion_err, Column};
28use datafusion::datasource::{TableProvider, TableType};
29use datafusion::error::Result as DFResult;
30use datafusion::execution::SessionState;
31use datafusion::logical_expr::{expr_fn::cast, Expr, LogicalPlan, LogicalPlanBuilder};
32use datafusion::sql::planner::IdentNormalizer;
33use datafusion::sql::sqlparser::ast::{Ident, ObjectName, Query, Statement, Visit, Visitor};
34use datafusion::sql::sqlparser::dialect::GenericDialect;
35use datafusion::sql::sqlparser::parser::Parser;
36use paimon::catalog::{Catalog, Identifier, View};
37
38use crate::error::to_datafusion_error;
39use crate::runtime::{await_with_runtime, block_on_with_runtime};
40use crate::system_tables;
41use crate::table::PaimonTableProvider;
42use crate::{BlobReaderRegistry, DynamicOptions};
43
44pub(crate) type SessionStateProvider = Arc<dyn Fn() -> Option<SessionState> + Send + Sync>;
45
46/// Provides an interface to manage and access multiple schemas (databases)
47/// within a Paimon [`Catalog`].
48///
49/// This provider uses lazy loading - databases and tables are fetched
50/// on-demand from the catalog, ensuring data is always fresh.
51pub struct PaimonCatalogProvider {
52    catalog_name: Option<String>,
53    /// Reference to the Paimon catalog.
54    catalog: Arc<dyn Catalog>,
55    /// Session-scoped dynamic options shared with the SQL context.
56    dynamic_options: DynamicOptions,
57    /// Temporary in-memory tables and views stored in MemorySchemaProvider per database.
58    ///
59    /// Uses `RwLock` with poison recovery (`unwrap_or_else(|e| e.into_inner())`) throughout.
60    /// This is a deliberate choice: since temp tables are session-scoped and non-critical,
61    /// it is preferable to continue with potentially stale data after a panic rather than
62    /// propagate the panic to all subsequent operations. The worst case is a temp table
63    /// becoming invisible or stale, which is recoverable by re-registering it.
64    temp_tables: Arc<RwLock<HashMap<String, Arc<MemorySchemaProvider>>>>,
65    blob_reader_registry: BlobReaderRegistry,
66    session_state: Option<SessionStateProvider>,
67    schema_force_view_types: bool,
68}
69
70impl Debug for PaimonCatalogProvider {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("PaimonCatalogProvider").finish()
73    }
74}
75
76impl PaimonCatalogProvider {
77    /// Creates a new [`PaimonCatalogProvider`].
78    pub fn new(
79        catalog_name: Option<String>,
80        catalog: Arc<dyn Catalog>,
81        dynamic_options: DynamicOptions,
82        blob_reader_registry: BlobReaderRegistry,
83        session_state: Option<SessionStateProvider>,
84    ) -> Self {
85        PaimonCatalogProvider {
86            catalog_name,
87            catalog,
88            dynamic_options,
89            temp_tables: Arc::new(RwLock::new(HashMap::new())),
90            blob_reader_registry,
91            session_state,
92            schema_force_view_types: true,
93        }
94    }
95
96    /// Configure whether table schemas use Arrow view types when available.
97    ///
98    /// Disable this for consumers that cannot operate on Arrow view arrays. This changes the
99    /// schema exposed to DataFusion, so query operators above the table scan will use the classic
100    /// Arrow types as well.
101    pub fn with_schema_force_view_types(mut self, schema_force_view_types: bool) -> Self {
102        self.schema_force_view_types = schema_force_view_types;
103        self
104    }
105}
106
107impl CatalogProvider for PaimonCatalogProvider {
108    fn schema_names(&self) -> Vec<String> {
109        let catalog = Arc::clone(&self.catalog);
110        block_on_with_runtime(
111            async move {
112                catalog.list_databases().await.unwrap_or_else(|e| {
113                    log::error!("failed to list databases: {e}");
114                    vec![]
115                })
116            },
117            "paimon catalog access thread panicked",
118        )
119    }
120
121    fn schema(&self, name: &str) -> Option<Arc<dyn SchemaProvider>> {
122        let catalog = Arc::clone(&self.catalog);
123        let dynamic_options = Arc::clone(&self.dynamic_options);
124        let blob_reader_registry = self.blob_reader_registry.clone();
125        let catalog_name = self.catalog_name.clone();
126        let session_state = self.session_state.clone();
127        let schema_force_view_types = self.schema_force_view_types;
128        let name = name.to_string();
129
130        let temp_provider = {
131            let databases = self.temp_tables.read().unwrap_or_else(|e| e.into_inner());
132            databases.get(&name).cloned()
133        };
134
135        block_on_with_runtime(
136            async move {
137                match catalog.get_database(&name).await {
138                    Ok(_) => Some(Arc::new(
139                        PaimonSchemaProvider::new(
140                            catalog_name,
141                            Arc::clone(&catalog),
142                            name,
143                            dynamic_options,
144                            temp_provider,
145                            blob_reader_registry,
146                            session_state,
147                        )
148                        .with_schema_force_view_types(schema_force_view_types),
149                    ) as Arc<dyn SchemaProvider>),
150                    Err(paimon::Error::DatabaseNotExist { .. }) => {
151                        if temp_provider.is_some() {
152                            Some(Arc::new(
153                                PaimonSchemaProvider::new(
154                                    catalog_name,
155                                    Arc::clone(&catalog),
156                                    name,
157                                    dynamic_options,
158                                    temp_provider,
159                                    blob_reader_registry,
160                                    session_state,
161                                )
162                                .with_schema_force_view_types(schema_force_view_types),
163                            ) as Arc<dyn SchemaProvider>)
164                        } else {
165                            None
166                        }
167                    }
168                    Err(e) => {
169                        log::error!("failed to get database '{}': {e}", name);
170                        None
171                    }
172                }
173            },
174            "paimon catalog access thread panicked",
175        )
176    }
177
178    fn register_schema(
179        &self,
180        name: &str,
181        _schema: Arc<dyn SchemaProvider>,
182    ) -> DFResult<Option<Arc<dyn SchemaProvider>>> {
183        let catalog = Arc::clone(&self.catalog);
184        let dynamic_options = Arc::clone(&self.dynamic_options);
185        let blob_reader_registry = self.blob_reader_registry.clone();
186        let catalog_name = self.catalog_name.clone();
187        let session_state = self.session_state.clone();
188        let schema_force_view_types = self.schema_force_view_types;
189        let name = name.to_string();
190        block_on_with_runtime(
191            async move {
192                catalog
193                    .create_database(&name, false, HashMap::new())
194                    .await
195                    .map_err(to_datafusion_error)?;
196                Ok(Some(Arc::new(
197                    PaimonSchemaProvider::new(
198                        catalog_name,
199                        Arc::clone(&catalog),
200                        name,
201                        dynamic_options,
202                        None,
203                        blob_reader_registry,
204                        session_state,
205                    )
206                    .with_schema_force_view_types(schema_force_view_types),
207                ) as Arc<dyn SchemaProvider>))
208            },
209            "paimon catalog access thread panicked",
210        )
211    }
212
213    fn deregister_schema(
214        &self,
215        name: &str,
216        cascade: bool,
217    ) -> DFResult<Option<Arc<dyn SchemaProvider>>> {
218        let catalog = Arc::clone(&self.catalog);
219        let dynamic_options = Arc::clone(&self.dynamic_options);
220        let blob_reader_registry = self.blob_reader_registry.clone();
221        let catalog_name = self.catalog_name.clone();
222        let session_state = self.session_state.clone();
223        let schema_force_view_types = self.schema_force_view_types;
224        let name = name.to_string();
225        block_on_with_runtime(
226            async move {
227                catalog
228                    .drop_database(&name, false, cascade)
229                    .await
230                    .map_err(to_datafusion_error)?;
231                Ok(Some(Arc::new(
232                    PaimonSchemaProvider::new(
233                        catalog_name,
234                        Arc::clone(&catalog),
235                        name,
236                        dynamic_options,
237                        None,
238                        blob_reader_registry,
239                        session_state,
240                    )
241                    .with_schema_force_view_types(schema_force_view_types),
242                ) as Arc<dyn SchemaProvider>))
243            },
244            "paimon catalog access thread panicked",
245        )
246    }
247}
248
249impl PaimonCatalogProvider {
250    /// Registers a temporary table or view in the specified database.
251    /// Creates the database if it does not exist.
252    ///
253    /// Returns an error if a temp table with the same name already exists in
254    /// the same database. Logs a warning if the name shadows a real Paimon table.
255    pub fn register_temp_table(
256        &self,
257        database: &str,
258        table_name: &str,
259        table: Arc<dyn TableProvider>,
260    ) -> DFResult<()> {
261        // Warn if this shadows a real Paimon table (outside the lock — not critical)
262        let catalog = Arc::clone(&self.catalog);
263        let db = database.to_string();
264        let tbl = table_name.to_string();
265        let identifier = Identifier::new(db, tbl);
266        if let Ok(true) = block_on_with_runtime(
267            async move {
268                match catalog.get_table(&identifier).await {
269                    Ok(_) => Ok::<bool, paimon::Error>(true),
270                    Err(paimon::Error::TableNotExist { .. }) => Ok(false),
271                    Err(_) => Ok(false),
272                }
273            },
274            "paimon catalog access thread panicked",
275        ) {
276            log::warn!(
277                "Temporary table '{database}.{table_name}' shadows an existing Paimon table"
278            );
279        }
280
281        // Atomically check-then-register under a single write lock to avoid TOCTOU
282        let mut databases = self.temp_tables.write().unwrap_or_else(|e| e.into_inner());
283        let mem_database = databases
284            .entry(database.to_string())
285            .or_insert_with(|| Arc::new(MemorySchemaProvider::new()));
286
287        // register_table returns Ok(Some(old_table)) if the name already existed
288        let old = mem_database.register_table(table_name.to_string(), table)?;
289        if old.is_some() {
290            return Err(plan_datafusion_err!(
291                "Temporary table '{database}.{table_name}' already exists"
292            ));
293        }
294        Ok(())
295    }
296
297    /// Deregisters a temporary table or view from the specified database.
298    pub fn deregister_temp_table(
299        &self,
300        database: &str,
301        table_name: &str,
302    ) -> DFResult<Option<Arc<dyn TableProvider>>> {
303        let databases = self.temp_tables.read().unwrap_or_else(|e| e.into_inner());
304        let mem_database = databases
305            .get(database)
306            .ok_or_else(|| plan_datafusion_err!("Unknown temp database '{database}'"))?;
307        mem_database.deregister_table(table_name)
308    }
309
310    /// Returns whether a temp table database exists with the given name.
311    pub fn has_temp_table_database(&self, name: &str) -> bool {
312        self.temp_tables
313            .read()
314            .unwrap_or_else(|e| e.into_inner())
315            .contains_key(name)
316    }
317
318    /// Returns whether a temp table with the given name exists in the specified database.
319    pub fn temp_table_exist(&self, database: &str, table_name: &str) -> bool {
320        let databases = self.temp_tables.read().unwrap_or_else(|e| e.into_inner());
321        databases
322            .get(database)
323            .is_some_and(|db| db.table_exist(table_name))
324    }
325}
326
327/// Represents a [`SchemaProvider`] for the Paimon [`Catalog`], managing
328/// access to table providers within a specific database.
329///
330/// Tables are loaded lazily when accessed via the `table()` method.
331pub struct PaimonSchemaProvider {
332    catalog_name: Option<String>,
333    /// Reference to the Paimon catalog.
334    catalog: Arc<dyn Catalog>,
335    /// Database name this schema represents.
336    database: String,
337    /// Session-scoped dynamic options shared with the SQL context.
338    dynamic_options: DynamicOptions,
339    /// Optional temporary in-memory provider for temp tables and views.
340    temp_provider: Option<Arc<MemorySchemaProvider>>,
341    /// Table types populated together with `table_names` for metadata-only lookups.
342    catalog_table_types: RwLock<HashMap<String, TableType>>,
343    blob_reader_registry: BlobReaderRegistry,
344    session_state: Option<SessionStateProvider>,
345    schema_force_view_types: bool,
346}
347
348impl Debug for PaimonSchemaProvider {
349    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
350        f.debug_struct("PaimonSchemaProvider")
351            .field("database", &self.database)
352            .field("has_temp_provider", &self.temp_provider.is_some())
353            .finish()
354    }
355}
356
357impl PaimonSchemaProvider {
358    /// Creates a new [`PaimonSchemaProvider`].
359    pub fn new(
360        catalog_name: Option<String>,
361        catalog: Arc<dyn Catalog>,
362        database: String,
363        dynamic_options: DynamicOptions,
364        temp_provider: Option<Arc<MemorySchemaProvider>>,
365        blob_reader_registry: BlobReaderRegistry,
366        session_state: Option<SessionStateProvider>,
367    ) -> Self {
368        PaimonSchemaProvider {
369            catalog_name,
370            catalog,
371            database,
372            dynamic_options,
373            temp_provider,
374            catalog_table_types: RwLock::new(HashMap::new()),
375            blob_reader_registry,
376            session_state,
377            schema_force_view_types: true,
378        }
379    }
380
381    fn with_schema_force_view_types(mut self, schema_force_view_types: bool) -> Self {
382        self.schema_force_view_types = schema_force_view_types;
383        self
384    }
385}
386
387#[async_trait]
388impl SchemaProvider for PaimonSchemaProvider {
389    fn table_names(&self) -> Vec<String> {
390        let catalog = Arc::clone(&self.catalog);
391        let database = self.database.clone();
392        let (mut names, views) = block_on_with_runtime(
393            {
394                let db = database.clone();
395                async move {
396                    let names = match catalog.list_tables(&db).await {
397                        Ok(names) => names,
398                        Err(e) => {
399                            log::error!("failed to list tables in '{}': {e}", db);
400                            vec![]
401                        }
402                    };
403                    let views = match catalog.list_views(&db).await {
404                        Ok(views) => views,
405                        Err(paimon::Error::Unsupported { .. }) => vec![],
406                        Err(error) => {
407                            log::error!("failed to list views in '{}': {error}", db);
408                            vec![]
409                        }
410                    };
411                    (names, views)
412                }
413            },
414            "paimon catalog access thread panicked",
415        );
416
417        let mut catalog_table_types = HashMap::with_capacity(names.len() + views.len());
418        for name in &names {
419            catalog_table_types.insert(name.clone(), TableType::Base);
420        }
421        for view in views {
422            catalog_table_types
423                .entry(view.clone())
424                .or_insert(TableType::View);
425            names.push(view);
426        }
427        *self
428            .catalog_table_types
429            .write()
430            .unwrap_or_else(|e| e.into_inner()) = catalog_table_types;
431
432        if let Some(temp) = &self.temp_provider {
433            names.extend(temp.table_names());
434        }
435
436        let mut seen = std::collections::HashSet::new();
437        names.retain(|name| seen.insert(name.clone()));
438
439        names
440    }
441
442    async fn table(&self, name: &str) -> DFResult<Option<Arc<dyn TableProvider>>> {
443        if let Some(temp) = &self.temp_provider {
444            if let Some(table) = temp.table(name).await? {
445                return Ok(Some(table));
446            }
447        }
448
449        let object = system_tables::parse_object_name_for_datafusion(name)?;
450        if let Some(system_name) = object.system_table().map(str::to_string) {
451            return await_with_runtime(system_tables::load(
452                Arc::clone(&self.catalog),
453                self.database.clone(),
454                object,
455                system_name,
456            ))
457            .await;
458        }
459
460        let catalog = Arc::clone(&self.catalog);
461        let dynamic_options = Arc::clone(&self.dynamic_options);
462        let blob_reader_registry = self.blob_reader_registry.clone();
463        let catalog_name = self.catalog_name.clone();
464        let session_state = self.session_state.clone();
465        let schema_force_view_types = self.schema_force_view_types;
466        let identifier = Identifier::new(self.database.clone(), object.table().to_string());
467        let branch = object.branch().map(str::to_string);
468        await_with_runtime(async move {
469            match catalog.get_table(&identifier).await {
470                Ok(mut table) => {
471                    if let Some(branch) = branch.as_deref() {
472                        table = table
473                            .copy_with_branch(branch)
474                            .await
475                            .map_err(to_datafusion_error)?;
476                    }
477                    let opts = dynamic_options.read().unwrap().clone();
478                    let provider = if opts.is_empty() {
479                        PaimonTableProvider::try_new_with_blob_reader_registry(
480                            table,
481                            blob_reader_registry,
482                        )?
483                    } else {
484                        let table_definition = crate::table::build_table_definition(&table).ok();
485                        // Dynamic options may select a historical snapshot
486                        // (e.g. `SET 'paimon.scan.version'`); switch to its
487                        // schema so planning sees the snapshot's columns.
488                        let table = table
489                            .copy_with_time_travel(opts)
490                            .await
491                            .map_err(to_datafusion_error)?;
492                        PaimonTableProvider::try_new_with_blob_reader_registry_and_definition(
493                            table,
494                            blob_reader_registry,
495                            table_definition,
496                        )?
497                    }
498                    .with_schema_force_view_types(schema_force_view_types)?;
499                    Ok(Some(Arc::new(provider) as Arc<dyn TableProvider>))
500                }
501                Err(paimon::Error::TableNotExist { .. }) => {
502                    if branch.is_some() {
503                        return Ok(None);
504                    }
505                    // DataFusion preloads every relation name before planning, including
506                    // registered table functions. Do not reinterpret a missing UDTF name as
507                    // a REST view; the planner will resolve it through the UDTF registry.
508                    if session_state
509                        .as_ref()
510                        .and_then(|provider| provider())
511                        .is_some_and(|state| state.table_functions().contains_key(identifier.object()))
512                    {
513                        return Ok(None);
514                    }
515                    let view = match catalog.get_view(&identifier).await {
516                        Ok(view) => view,
517                        Err(paimon::Error::ViewNotExist { .. })
518                        | Err(paimon::Error::Unsupported { .. }) => return Ok(None),
519                        Err(error) => return Err(to_datafusion_error(error)),
520                    };
521                    let catalog_name = catalog_name.ok_or_else(|| {
522                        plan_datafusion_err!(
523                            "REST catalog view '{}' requires a session-aware catalog provider",
524                            identifier.full_name()
525                        )
526                    })?;
527                    validate_view_dependencies(&catalog, &catalog_name, &view).await?;
528                    let mut state = session_state
529                        .and_then(|provider| provider())
530                        .ok_or_else(|| {
531                            plan_datafusion_err!(
532                                "DataFusion session is unavailable while planning REST catalog view '{}'",
533                                identifier.full_name()
534                            )
535                        })?;
536                    state.config_mut().options_mut().catalog.default_catalog =
537                        catalog_name.clone();
538                    state.config_mut().options_mut().catalog.default_schema =
539                        identifier.database().to_string();
540                    let catalogs =
541                        HashMap::from([(catalog_name.clone(), Arc::clone(&catalog))]);
542                    let query = crate::sql_function::expand_sql(
543                        view.query_for("datafusion"),
544                        &catalogs,
545                        &catalog_name,
546                        identifier.database(),
547                    )
548                    .await?;
549                    let plan = state.create_logical_plan(&query).await?;
550                    let plan = enforce_view_schema(plan, &view)?;
551                    Ok(Some(Arc::new(datafusion::datasource::ViewTable::new(
552                        plan,
553                        Some(query),
554                    )) as Arc<dyn TableProvider>))
555                }
556                Err(e) => Err(to_datafusion_error(e)),
557            }
558        })
559        .await
560    }
561
562    async fn table_type(&self, name: &str) -> DFResult<Option<TableType>> {
563        if let Some(temp) = &self.temp_provider {
564            if let Some(table_type) = temp.table_type(name).await? {
565                return Ok(Some(table_type));
566            }
567        }
568
569        if let Some(table_type) = self
570            .catalog_table_types
571            .read()
572            .unwrap_or_else(|e| e.into_inner())
573            .get(name)
574        {
575            return Ok(Some(*table_type));
576        }
577
578        self.table(name)
579            .await
580            .map(|table| table.map(|table| table.table_type()))
581    }
582
583    fn table_exist(&self, name: &str) -> bool {
584        if let Some(temp) = &self.temp_provider {
585            if temp.table_exist(name) {
586                return true;
587            }
588        }
589
590        let object = match system_tables::parse_object_name_for_datafusion(name) {
591            Ok(object) => object,
592            Err(e) => {
593                log::error!("failed to parse Paimon object name '{name}': {e}");
594                return false;
595            }
596        };
597        if let Some(system_name) = object.system_table() {
598            if !system_tables::is_registered(system_name) {
599                return false;
600            }
601        }
602
603        let catalog = Arc::clone(&self.catalog);
604        let identifier = Identifier::new(self.database.clone(), object.table().to_string());
605        let branch = object.branch().map(str::to_string);
606        let is_branches_table = object
607            .system_table()
608            .is_some_and(|name| name.eq_ignore_ascii_case("branches"));
609        block_on_with_runtime(
610            async move {
611                match catalog.get_table(&identifier).await {
612                    Ok(table) => {
613                        if let Some(branch) = branch.as_deref() {
614                            if is_branches_table {
615                                return true;
616                            }
617                            table.copy_with_branch(branch).await.is_ok()
618                        } else {
619                            true
620                        }
621                    }
622                    Err(paimon::Error::TableNotExist { .. }) => {
623                        if branch.is_some() {
624                            return false;
625                        }
626                        match catalog.get_view(&identifier).await {
627                            Ok(_) => true,
628                            Err(paimon::Error::ViewNotExist { .. })
629                            | Err(paimon::Error::Unsupported { .. }) => false,
630                            Err(error) => {
631                                log::error!("failed to check view '{}': {error}", identifier);
632                                false
633                            }
634                        }
635                    }
636                    Err(e) => {
637                        log::error!("failed to check table '{}': {e}", identifier);
638                        false
639                    }
640                }
641            },
642            "paimon catalog access thread panicked",
643        )
644    }
645
646    fn register_table(
647        &self,
648        _name: String,
649        table: Arc<dyn TableProvider>,
650    ) -> DFResult<Option<Arc<dyn TableProvider>>> {
651        // DataFusion calls register_table after table creation, so we just
652        // acknowledge it here.
653        Ok(Some(table))
654    }
655
656    fn deregister_table(&self, name: &str) -> DFResult<Option<Arc<dyn TableProvider>>> {
657        let catalog = Arc::clone(&self.catalog);
658        let identifier = Identifier::new(self.database.clone(), name);
659        block_on_with_runtime(
660            async move {
661                // Try to get the table first so we can return it.
662                let table = match catalog.get_table(&identifier).await {
663                    Ok(t) => t,
664                    Err(paimon::Error::TableNotExist { .. }) => return Ok(None),
665                    Err(e) => return Err(to_datafusion_error(e)),
666                };
667                let provider = PaimonTableProvider::try_new(table)?;
668                catalog
669                    .drop_table(&identifier, false)
670                    .await
671                    .map_err(to_datafusion_error)?;
672                Ok(Some(Arc::new(provider) as Arc<dyn TableProvider>))
673            },
674            "paimon catalog access thread panicked",
675        )
676    }
677}
678
679fn enforce_view_schema(plan: LogicalPlan, view: &View) -> DFResult<LogicalPlan> {
680    let declared_fields = view.schema().fields();
681    let actual_fields = plan.schema().fields();
682    if actual_fields.len() != declared_fields.len() {
683        return Err(plan_datafusion_err!(
684            "REST catalog view '{}' declares {} fields but its query returns {}",
685            view.full_name(),
686            declared_fields.len(),
687            actual_fields.len()
688        ));
689    }
690
691    let expressions = declared_fields
692        .iter()
693        .enumerate()
694        .map(|(index, declared)| {
695            let (qualifier, actual) = plan.schema().qualified_field(index);
696            let column = match qualifier {
697                Some(qualifier) => Column::new(Some(qualifier.clone()), actual.name()),
698                None => Column::new_unqualified(actual.name()),
699            };
700            let target_type = paimon::arrow::paimon_type_to_arrow(declared.data_type())
701                .map_err(to_datafusion_error)?;
702            Ok(cast(Expr::Column(column), target_type).alias(declared.name()))
703        })
704        .collect::<DFResult<Vec<_>>>()?;
705
706    LogicalPlanBuilder::from(plan).project(expressions)?.build()
707}
708
709const MAX_VIEW_DEPENDENCIES: usize = 64;
710
711async fn validate_view_dependencies(
712    catalog: &Arc<dyn Catalog>,
713    catalog_name: &str,
714    root: &View,
715) -> DFResult<()> {
716    let mut queue = VecDeque::from([root.clone()]);
717    let mut loaded = HashSet::from([root.identifier().clone()]);
718    let mut dependencies = HashMap::<Identifier, Vec<Identifier>>::new();
719
720    while let Some(view) = queue.pop_front() {
721        let candidates = view_relation_identifiers(&view, catalog_name)?;
722        let mut view_dependencies = Vec::new();
723        for identifier in candidates {
724            match catalog.get_table(&identifier).await {
725                Ok(_) => continue,
726                Err(paimon::Error::TableNotExist { .. })
727                | Err(paimon::Error::Unsupported { .. }) => {}
728                Err(error) => return Err(to_datafusion_error(error)),
729            }
730
731            let dependency = match catalog.get_view(&identifier).await {
732                Ok(view) => view,
733                Err(paimon::Error::ViewNotExist { .. })
734                | Err(paimon::Error::Unsupported { .. }) => continue,
735                Err(error) => return Err(to_datafusion_error(error)),
736            };
737            view_dependencies.push(identifier.clone());
738            if loaded.insert(identifier) {
739                if loaded.len() > MAX_VIEW_DEPENDENCIES {
740                    return Err(plan_datafusion_err!(
741                        "REST catalog view '{}' exceeds the dependency limit of {}",
742                        root.full_name(),
743                        MAX_VIEW_DEPENDENCIES
744                    ));
745                }
746                queue.push_back(dependency);
747            }
748        }
749        dependencies.insert(view.identifier().clone(), view_dependencies);
750
751        if let Some(cycle) = find_view_dependency_cycle(&dependencies) {
752            let path = cycle
753                .iter()
754                .map(Identifier::full_name)
755                .collect::<Vec<_>>()
756                .join(" -> ");
757            return Err(plan_datafusion_err!(
758                "recursive REST catalog view dependency detected: {path}"
759            ));
760        }
761    }
762    Ok(())
763}
764
765fn view_relation_identifiers(view: &View, catalog_name: &str) -> DFResult<Vec<Identifier>> {
766    let statements =
767        Parser::parse_sql(&GenericDialect {}, view.query_for("datafusion")).map_err(|error| {
768            plan_datafusion_err!(
769                "Invalid SQL for REST catalog view '{}': {error}",
770                view.full_name()
771            )
772        })?;
773    if statements.len() != 1 {
774        return Err(plan_datafusion_err!(
775            "REST catalog view '{}' must contain exactly one SQL statement",
776            view.full_name()
777        ));
778    }
779    if !matches!(statements.first(), Some(Statement::Query(_))) {
780        return Err(plan_datafusion_err!(
781            "REST catalog view '{}' must contain a single read-only query",
782            view.full_name()
783        ));
784    }
785
786    let mut visitor = ViewRelationVisitor::new(catalog_name, view.identifier().database());
787    let _: std::ops::ControlFlow<()> = statements.visit(&mut visitor);
788    Ok(visitor.identifiers)
789}
790
791type SqlIdentifierKey = String;
792
793struct QueryCteScope {
794    visible: HashSet<SqlIdentifierKey>,
795    cte_query_visibility: HashMap<usize, HashSet<SqlIdentifierKey>>,
796}
797
798struct ViewRelationVisitor<'a> {
799    catalog_name: &'a str,
800    current_database: &'a str,
801    scopes: Vec<QueryCteScope>,
802    identifiers: Vec<Identifier>,
803}
804
805impl<'a> ViewRelationVisitor<'a> {
806    fn new(catalog_name: &'a str, current_database: &'a str) -> Self {
807        Self {
808            catalog_name,
809            current_database,
810            scopes: Vec::new(),
811            identifiers: Vec::new(),
812        }
813    }
814}
815
816impl Visitor for ViewRelationVisitor<'_> {
817    type Break = ();
818
819    fn pre_visit_query(&mut self, query: &Query) -> std::ops::ControlFlow<Self::Break> {
820        let query_address = query as *const Query as usize;
821        let inherited = self
822            .scopes
823            .last()
824            .map(|scope| {
825                scope
826                    .cte_query_visibility
827                    .get(&query_address)
828                    .unwrap_or(&scope.visible)
829                    .clone()
830            })
831            .unwrap_or_default();
832        let mut visible = inherited.clone();
833        let mut cte_query_visibility = HashMap::new();
834
835        if let Some(with) = &query.with {
836            let local_ctes = with
837                .cte_tables
838                .iter()
839                .map(|cte| sql_identifier_key(&cte.alias.name))
840                .collect::<Vec<_>>();
841            if with.recursive {
842                visible.extend(local_ctes);
843                for cte in &with.cte_tables {
844                    cte_query_visibility
845                        .insert(cte.query.as_ref() as *const Query as usize, visible.clone());
846                }
847            } else {
848                for (cte, alias) in with.cte_tables.iter().zip(local_ctes) {
849                    cte_query_visibility
850                        .insert(cte.query.as_ref() as *const Query as usize, visible.clone());
851                    visible.insert(alias);
852                }
853            }
854        }
855
856        self.scopes.push(QueryCteScope {
857            visible,
858            cte_query_visibility,
859        });
860        std::ops::ControlFlow::Continue(())
861    }
862
863    fn post_visit_query(&mut self, _query: &Query) -> std::ops::ControlFlow<Self::Break> {
864        self.scopes.pop();
865        std::ops::ControlFlow::Continue(())
866    }
867
868    fn pre_visit_relation(&mut self, relation: &ObjectName) -> std::ops::ControlFlow<Self::Break> {
869        let is_cte = match relation.0.as_slice() {
870            [part] => part.as_ident().is_some_and(|identifier| {
871                self.scopes
872                    .last()
873                    .is_some_and(|scope| scope.visible.contains(&sql_identifier_key(identifier)))
874            }),
875            _ => false,
876        };
877        if !is_cte {
878            if let Some(identifier) =
879                relation_identifier(relation, self.catalog_name, self.current_database)
880            {
881                self.identifiers.push(identifier);
882            }
883        }
884        std::ops::ControlFlow::Continue(())
885    }
886}
887
888fn sql_identifier_key(identifier: &Ident) -> SqlIdentifierKey {
889    IdentNormalizer::default().normalize(identifier.clone())
890}
891
892fn relation_identifier(
893    relation: &ObjectName,
894    catalog_name: &str,
895    current_database: &str,
896) -> Option<Identifier> {
897    let parts = relation
898        .0
899        .iter()
900        .map(|part| part.as_ident().map(sql_identifier_key))
901        .collect::<Option<Vec<_>>>()?;
902    match parts.as_slice() {
903        [object] => Some(Identifier::new(current_database, object.as_str())),
904        [database, object] => Some(Identifier::new(database.as_str(), object.as_str())),
905        [catalog, database, object] if catalog == catalog_name => {
906            Some(Identifier::new(database.as_str(), object.as_str()))
907        }
908        _ => None,
909    }
910}
911
912fn find_view_dependency_cycle(
913    dependencies: &HashMap<Identifier, Vec<Identifier>>,
914) -> Option<Vec<Identifier>> {
915    fn visit(
916        identifier: &Identifier,
917        dependencies: &HashMap<Identifier, Vec<Identifier>>,
918        finished: &mut HashSet<Identifier>,
919        path: &mut Vec<Identifier>,
920    ) -> Option<Vec<Identifier>> {
921        if let Some(start) = path.iter().position(|entry| entry == identifier) {
922            let mut cycle = path[start..].to_vec();
923            cycle.push(identifier.clone());
924            return Some(cycle);
925        }
926        if finished.contains(identifier) {
927            return None;
928        }
929
930        path.push(identifier.clone());
931        if let Some(next_identifiers) = dependencies.get(identifier) {
932            for next in next_identifiers {
933                if let Some(cycle) = visit(next, dependencies, finished, path) {
934                    return Some(cycle);
935                }
936            }
937        }
938        path.pop();
939        finished.insert(identifier.clone());
940        None
941    }
942
943    let mut finished = HashSet::new();
944    for identifier in dependencies.keys() {
945        if let Some(cycle) = visit(identifier, dependencies, &mut finished, &mut Vec::new()) {
946            return Some(cycle);
947        }
948    }
949    None
950}
951
952#[cfg(test)]
953mod tests {
954    use super::*;
955
956    #[test]
957    fn relation_identifiers_follow_datafusion_normalization() {
958        let relation = ObjectName(vec![
959            datafusion::sql::sqlparser::ast::ObjectNamePart::Identifier(Ident::new("PAIMON")),
960            datafusion::sql::sqlparser::ast::ObjectNamePart::Identifier(Ident::new("DEFAULT")),
961            datafusion::sql::sqlparser::ast::ObjectNamePart::Identifier(Ident::new("ANSWER_VIEW")),
962        ]);
963
964        assert_eq!(
965            relation_identifier(&relation, "paimon", "unused"),
966            Some(Identifier::new("default", "answer_view"))
967        );
968    }
969}