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::any::Any;
21use std::collections::HashMap;
22use std::fmt::Debug;
23use std::sync::Arc;
24use std::sync::RwLock;
25
26use async_trait::async_trait;
27use datafusion::catalog::{CatalogProvider, MemorySchemaProvider, SchemaProvider};
28use datafusion::common::plan_datafusion_err;
29use datafusion::datasource::TableProvider;
30use datafusion::error::Result as DFResult;
31use paimon::catalog::{Catalog, Identifier};
32
33use crate::error::to_datafusion_error;
34use crate::runtime::{await_with_runtime, block_on_with_runtime};
35use crate::system_tables;
36use crate::table::PaimonTableProvider;
37use crate::DynamicOptions;
38
39/// Provides an interface to manage and access multiple schemas (databases)
40/// within a Paimon [`Catalog`].
41///
42/// This provider uses lazy loading - databases and tables are fetched
43/// on-demand from the catalog, ensuring data is always fresh.
44pub struct PaimonCatalogProvider {
45    /// Reference to the Paimon catalog.
46    catalog: Arc<dyn Catalog>,
47    /// Session-scoped dynamic options shared with the SQL context.
48    dynamic_options: DynamicOptions,
49    /// Temporary in-memory tables and views stored in MemorySchemaProvider per database.
50    ///
51    /// Uses `RwLock` with poison recovery (`unwrap_or_else(|e| e.into_inner())`) throughout.
52    /// This is a deliberate choice: since temp tables are session-scoped and non-critical,
53    /// it is preferable to continue with potentially stale data after a panic rather than
54    /// propagate the panic to all subsequent operations. The worst case is a temp table
55    /// becoming invisible or stale, which is recoverable by re-registering it.
56    temp_tables: Arc<RwLock<HashMap<String, Arc<MemorySchemaProvider>>>>,
57}
58
59impl Debug for PaimonCatalogProvider {
60    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61        f.debug_struct("PaimonCatalogProvider").finish()
62    }
63}
64
65impl PaimonCatalogProvider {
66    /// Creates a new [`PaimonCatalogProvider`].
67    ///
68    /// For standalone use without `SET`/`RESET` support.
69    /// When used via [`SQLContext`], the handler creates the provider
70    /// internally with shared dynamic options.
71    pub fn new(catalog: Arc<dyn Catalog>) -> Self {
72        PaimonCatalogProvider {
73            catalog,
74            dynamic_options: Default::default(),
75            temp_tables: Arc::new(RwLock::new(HashMap::new())),
76        }
77    }
78
79    pub(crate) fn with_dynamic_options(
80        catalog: Arc<dyn Catalog>,
81        dynamic_options: DynamicOptions,
82    ) -> Self {
83        PaimonCatalogProvider {
84            catalog,
85            dynamic_options,
86            temp_tables: Arc::new(RwLock::new(HashMap::new())),
87        }
88    }
89}
90
91impl CatalogProvider for PaimonCatalogProvider {
92    fn as_any(&self) -> &dyn Any {
93        self
94    }
95
96    fn schema_names(&self) -> Vec<String> {
97        let catalog = Arc::clone(&self.catalog);
98        block_on_with_runtime(
99            async move {
100                catalog.list_databases().await.unwrap_or_else(|e| {
101                    log::error!("failed to list databases: {e}");
102                    vec![]
103                })
104            },
105            "paimon catalog access thread panicked",
106        )
107    }
108
109    fn schema(&self, name: &str) -> Option<Arc<dyn SchemaProvider>> {
110        let catalog = Arc::clone(&self.catalog);
111        let dynamic_options = Arc::clone(&self.dynamic_options);
112        let name = name.to_string();
113
114        let temp_provider = {
115            let databases = self.temp_tables.read().unwrap_or_else(|e| e.into_inner());
116            databases.get(&name).cloned()
117        };
118
119        block_on_with_runtime(
120            async move {
121                match catalog.get_database(&name).await {
122                    Ok(_) => Some(Arc::new(PaimonSchemaProvider::new(
123                        Arc::clone(&catalog),
124                        name,
125                        dynamic_options,
126                        temp_provider,
127                    )) as Arc<dyn SchemaProvider>),
128                    Err(paimon::Error::DatabaseNotExist { .. }) => {
129                        if temp_provider.is_some() {
130                            Some(Arc::new(PaimonSchemaProvider::new(
131                                Arc::clone(&catalog),
132                                name,
133                                dynamic_options,
134                                temp_provider,
135                            )) as Arc<dyn SchemaProvider>)
136                        } else {
137                            None
138                        }
139                    }
140                    Err(e) => {
141                        log::error!("failed to get database '{}': {e}", name);
142                        None
143                    }
144                }
145            },
146            "paimon catalog access thread panicked",
147        )
148    }
149
150    fn register_schema(
151        &self,
152        name: &str,
153        _schema: Arc<dyn SchemaProvider>,
154    ) -> DFResult<Option<Arc<dyn SchemaProvider>>> {
155        let catalog = Arc::clone(&self.catalog);
156        let dynamic_options = Arc::clone(&self.dynamic_options);
157        let name = name.to_string();
158        block_on_with_runtime(
159            async move {
160                catalog
161                    .create_database(&name, false, HashMap::new())
162                    .await
163                    .map_err(to_datafusion_error)?;
164                Ok(Some(Arc::new(PaimonSchemaProvider::new(
165                    Arc::clone(&catalog),
166                    name,
167                    dynamic_options,
168                    None,
169                )) as Arc<dyn SchemaProvider>))
170            },
171            "paimon catalog access thread panicked",
172        )
173    }
174
175    fn deregister_schema(
176        &self,
177        name: &str,
178        cascade: bool,
179    ) -> DFResult<Option<Arc<dyn SchemaProvider>>> {
180        let catalog = Arc::clone(&self.catalog);
181        let dynamic_options = Arc::clone(&self.dynamic_options);
182        let name = name.to_string();
183        block_on_with_runtime(
184            async move {
185                catalog
186                    .drop_database(&name, false, cascade)
187                    .await
188                    .map_err(to_datafusion_error)?;
189                Ok(Some(Arc::new(PaimonSchemaProvider::new(
190                    Arc::clone(&catalog),
191                    name,
192                    dynamic_options,
193                    None,
194                )) as Arc<dyn SchemaProvider>))
195            },
196            "paimon catalog access thread panicked",
197        )
198    }
199}
200
201impl PaimonCatalogProvider {
202    /// Registers a temporary table or view in the specified database.
203    /// Creates the database if it does not exist.
204    ///
205    /// Returns an error if a temp table with the same name already exists in
206    /// the same database. Logs a warning if the name shadows a real Paimon table.
207    pub fn register_temp_table(
208        &self,
209        database: &str,
210        table_name: &str,
211        table: Arc<dyn TableProvider>,
212    ) -> DFResult<()> {
213        // Warn if this shadows a real Paimon table (outside the lock — not critical)
214        let catalog = Arc::clone(&self.catalog);
215        let db = database.to_string();
216        let tbl = table_name.to_string();
217        let identifier = Identifier::new(db, tbl);
218        if let Ok(true) = block_on_with_runtime(
219            async move {
220                match catalog.get_table(&identifier).await {
221                    Ok(_) => Ok::<bool, paimon::Error>(true),
222                    Err(paimon::Error::TableNotExist { .. }) => Ok(false),
223                    Err(_) => Ok(false),
224                }
225            },
226            "paimon catalog access thread panicked",
227        ) {
228            log::warn!(
229                "Temporary table '{database}.{table_name}' shadows an existing Paimon table"
230            );
231        }
232
233        // Atomically check-then-register under a single write lock to avoid TOCTOU
234        let mut databases = self.temp_tables.write().unwrap_or_else(|e| e.into_inner());
235        let mem_database = databases
236            .entry(database.to_string())
237            .or_insert_with(|| Arc::new(MemorySchemaProvider::new()));
238
239        // register_table returns Ok(Some(old_table)) if the name already existed
240        let old = mem_database.register_table(table_name.to_string(), table)?;
241        if old.is_some() {
242            return Err(plan_datafusion_err!(
243                "Temporary table '{database}.{table_name}' already exists"
244            ));
245        }
246        Ok(())
247    }
248
249    /// Deregisters a temporary table or view from the specified database.
250    pub fn deregister_temp_table(
251        &self,
252        database: &str,
253        table_name: &str,
254    ) -> DFResult<Option<Arc<dyn TableProvider>>> {
255        let databases = self.temp_tables.read().unwrap_or_else(|e| e.into_inner());
256        let mem_database = databases
257            .get(database)
258            .ok_or_else(|| plan_datafusion_err!("Unknown temp database '{database}'"))?;
259        mem_database.deregister_table(table_name)
260    }
261
262    /// Returns whether a temp table database exists with the given name.
263    pub fn has_temp_table_database(&self, name: &str) -> bool {
264        self.temp_tables
265            .read()
266            .unwrap_or_else(|e| e.into_inner())
267            .contains_key(name)
268    }
269
270    /// Returns whether a temp table with the given name exists in the specified database.
271    pub fn temp_table_exist(&self, database: &str, table_name: &str) -> bool {
272        let databases = self.temp_tables.read().unwrap_or_else(|e| e.into_inner());
273        databases
274            .get(database)
275            .is_some_and(|db| db.table_exist(table_name))
276    }
277}
278
279/// Represents a [`SchemaProvider`] for the Paimon [`Catalog`], managing
280/// access to table providers within a specific database.
281///
282/// Tables are loaded lazily when accessed via the `table()` method.
283pub struct PaimonSchemaProvider {
284    /// Reference to the Paimon catalog.
285    catalog: Arc<dyn Catalog>,
286    /// Database name this schema represents.
287    database: String,
288    /// Session-scoped dynamic options shared with the SQL context.
289    dynamic_options: DynamicOptions,
290    /// Optional temporary in-memory provider for temp tables and views.
291    temp_provider: Option<Arc<MemorySchemaProvider>>,
292}
293
294impl Debug for PaimonSchemaProvider {
295    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296        f.debug_struct("PaimonSchemaProvider")
297            .field("database", &self.database)
298            .field("has_temp_provider", &self.temp_provider.is_some())
299            .finish()
300    }
301}
302
303impl PaimonSchemaProvider {
304    /// Creates a new [`PaimonSchemaProvider`] with shared dynamic options.
305    pub fn new(
306        catalog: Arc<dyn Catalog>,
307        database: String,
308        dynamic_options: DynamicOptions,
309        temp_provider: Option<Arc<MemorySchemaProvider>>,
310    ) -> Self {
311        PaimonSchemaProvider {
312            catalog,
313            database,
314            dynamic_options,
315            temp_provider,
316        }
317    }
318}
319
320#[async_trait]
321impl SchemaProvider for PaimonSchemaProvider {
322    fn as_any(&self) -> &dyn Any {
323        self
324    }
325
326    fn table_names(&self) -> Vec<String> {
327        let catalog = Arc::clone(&self.catalog);
328        let database = self.database.clone();
329        let mut names = block_on_with_runtime(
330            {
331                let db = database.clone();
332                async move {
333                    match catalog.list_tables(&db).await {
334                        Ok(names) => names,
335                        Err(e) => {
336                            log::error!("failed to list tables in '{}': {e}", db);
337                            vec![]
338                        }
339                    }
340                }
341            },
342            "paimon catalog access thread panicked",
343        );
344
345        if let Some(temp) = &self.temp_provider {
346            names.extend(temp.table_names());
347        }
348
349        let mut seen = std::collections::HashSet::new();
350        names.retain(|name| seen.insert(name.clone()));
351
352        names
353    }
354
355    async fn table(&self, name: &str) -> DFResult<Option<Arc<dyn TableProvider>>> {
356        if let Some(temp) = &self.temp_provider {
357            if let Some(table) = temp.table(name).await? {
358                return Ok(Some(table));
359            }
360        }
361
362        let (base, system_name) = system_tables::split_object_name(name);
363        if let Some(system_name) = system_name {
364            return await_with_runtime(system_tables::load(
365                Arc::clone(&self.catalog),
366                self.database.clone(),
367                base.to_string(),
368                system_name.to_string(),
369            ))
370            .await;
371        }
372
373        let catalog = Arc::clone(&self.catalog);
374        let dynamic_options = Arc::clone(&self.dynamic_options);
375        let identifier = Identifier::new(self.database.clone(), base);
376        await_with_runtime(async move {
377            match catalog.get_table(&identifier).await {
378                Ok(table) => {
379                    let opts = dynamic_options.read().unwrap().clone();
380                    let table = if opts.is_empty() {
381                        table
382                    } else {
383                        table.copy_with_options(opts)
384                    };
385                    let provider = PaimonTableProvider::try_new(table)?;
386                    Ok(Some(Arc::new(provider) as Arc<dyn TableProvider>))
387                }
388                Err(paimon::Error::TableNotExist { .. }) => Ok(None),
389                Err(e) => Err(to_datafusion_error(e)),
390            }
391        })
392        .await
393    }
394
395    fn table_exist(&self, name: &str) -> bool {
396        if let Some(temp) = &self.temp_provider {
397            if temp.table_exist(name) {
398                return true;
399            }
400        }
401
402        let (base, system_name) = system_tables::split_object_name(name);
403        if let Some(system_name) = system_name {
404            if !system_tables::is_registered(system_name) {
405                return false;
406            }
407        }
408
409        let catalog = Arc::clone(&self.catalog);
410        let identifier = Identifier::new(self.database.clone(), base.to_string());
411        block_on_with_runtime(
412            async move {
413                match catalog.get_table(&identifier).await {
414                    Ok(_) => true,
415                    Err(paimon::Error::TableNotExist { .. }) => false,
416                    Err(e) => {
417                        log::error!("failed to check table '{}': {e}", identifier);
418                        false
419                    }
420                }
421            },
422            "paimon catalog access thread panicked",
423        )
424    }
425
426    fn register_table(
427        &self,
428        _name: String,
429        table: Arc<dyn TableProvider>,
430    ) -> DFResult<Option<Arc<dyn TableProvider>>> {
431        // DataFusion calls register_table after table creation, so we just
432        // acknowledge it here.
433        Ok(Some(table))
434    }
435
436    fn deregister_table(&self, name: &str) -> DFResult<Option<Arc<dyn TableProvider>>> {
437        let catalog = Arc::clone(&self.catalog);
438        let identifier = Identifier::new(self.database.clone(), name);
439        block_on_with_runtime(
440            async move {
441                // Try to get the table first so we can return it.
442                let table = match catalog.get_table(&identifier).await {
443                    Ok(t) => t,
444                    Err(paimon::Error::TableNotExist { .. }) => return Ok(None),
445                    Err(e) => return Err(to_datafusion_error(e)),
446                };
447                let provider = PaimonTableProvider::try_new(table)?;
448                catalog
449                    .drop_table(&identifier, false)
450                    .await
451                    .map_err(to_datafusion_error)?;
452                Ok(Some(Arc::new(provider) as Arc<dyn TableProvider>))
453            },
454            "paimon catalog access thread panicked",
455        )
456    }
457}