blob: 166d59a487f389c08e413f7ec374721217e2ffd0 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Catalog API for Apache Paimon.
//!
//! Design aligns with [Paimon Java Catalog](https://github.com/apache/paimon/blob/master/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java)
//! and follows API patterns from Apache Iceberg Rust.
mod database;
mod factory;
mod filesystem;
mod partition_listing;
mod rest;
use std::collections::HashMap;
use std::fmt;
use crate::{Error, Result};
pub use database::*;
pub use factory::*;
pub use filesystem::*;
pub use partition_listing::list_partitions_from_file_system;
pub use rest::*;
use serde::{Deserialize, Serialize};
/// Splitter for system table names (e.g. `table$snapshots`).
pub const SYSTEM_TABLE_SPLITTER: &str = "$";
/// Prefix for branch in object name (e.g. `table$branch_foo`).
pub const SYSTEM_BRANCH_PREFIX: &str = "branch_";
/// Default main branch name.
pub const DEFAULT_MAIN_BRANCH: &str = "main";
/// Database value when the database is not known; [`Identifier::full_name`] returns only the object.
pub const UNKNOWN_DATABASE: &str = "unknown";
/// Database property key for custom location. Not allowed for filesystem catalog.
/// See [Catalog.DB_LOCATION_PROP](https://github.com/apache/paimon/blob/release-1.3/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java).
pub const DB_LOCATION_PROP: &str = "location";
/// Suffix for database directory names in the filesystem (e.g. `mydb.db`).
/// See [Catalog.DB_SUFFIX](https://github.com/apache/paimon/blob/release-1.3/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java).
pub const DB_SUFFIX: &str = ".db";
// ======================= Identifier ===============================
/// Identifies a catalog object (e.g. a table) by database and object name.
///
/// Corresponds to [org.apache.paimon.catalog.Identifier](https://github.com/apache/paimon/blob/release-1.3/paimon-api/src/main/java/org/apache/paimon/catalog/Identifier.java).
/// The object name may be a table name or a qualified name like `table$branch_foo` or
/// `table$snapshots` for system tables.
#[derive(Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Identifier {
/// Database name.
database: String,
/// Object name (table name, or table$branch$system for system tables).
object: String,
}
impl Identifier {
/// Create an identifier from database and object name.
pub fn new(database: impl Into<String>, object: impl Into<String>) -> Self {
Self {
database: database.into(),
object: object.into(),
}
}
/// Validate this identifier's database and object names.
pub(crate) fn validate(&self) -> Result<()> {
Self::validate_database_name(&self.database)?;
Self::validate_object_name(&self.object)
}
/// Validate a database name for path-safe catalog use.
pub(crate) fn validate_database_name(name: &str) -> Result<()> {
validate_identifier_name("database", name)
}
/// Validate an object name for path-safe catalog use.
pub(crate) fn validate_object_name(name: &str) -> Result<()> {
validate_identifier_name("object", name)
}
/// Database name.
pub fn database(&self) -> &str {
&self.database
}
/// Full object name (table name, or with branch/system suffix).
pub fn object(&self) -> &str {
&self.object
}
/// Full name: when database is [`UNKNOWN_DATABASE`], returns only the object;
/// otherwise returns `database.object`.
pub fn full_name(&self) -> String {
if self.database == UNKNOWN_DATABASE {
self.object.clone()
} else {
format!("{}.{}", self.database, self.object)
}
}
}
fn validate_identifier_name(kind: &str, name: &str) -> Result<()> {
let invalid = if name.trim().is_empty() {
Some("cannot be empty or whitespace")
} else if matches!(name, "." | "..") {
Some("cannot be '.' or '..'")
} else if name.contains('/') || name.contains('\\') {
Some("cannot contain path separators")
} else if name.chars().any(char::is_control) {
Some("cannot contain control characters")
} else {
None
};
if let Some(reason) = invalid {
return Err(Error::IdentifierInvalid {
message: format!("{kind} name {reason}: {name:?}"),
});
}
Ok(())
}
impl fmt::Display for Identifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.full_name())
}
}
impl fmt::Debug for Identifier {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Identifier")
.field("database", &self.database)
.field("object", &self.object)
.finish()
}
}
// ======================= Catalog trait ===============================
use async_trait::async_trait;
use crate::api::PagedList;
use crate::spec::{Partition, Schema, SchemaChange};
use crate::table::Table;
/// Catalog API for reading and writing metadata (databases, tables) in Paimon.
///
/// Corresponds to [org.apache.paimon.catalog.Catalog](https://github.com/apache/paimon/blob/release-1.3/paimon-core/src/main/java/org/apache/paimon/catalog/Catalog.java).
#[async_trait]
pub trait Catalog: Send + Sync {
// ======================= database methods ===============================
/// List names of all databases in this catalog.
///
/// # Errors
/// Implementations may return other errors (e.g. I/O or backend-specific).
async fn list_databases(&self) -> Result<Vec<String>>;
/// Create a database.
///
/// * `ignore_if_exists` - if true, do nothing when the database already exists;
/// if false, return [`crate::Error::DatabaseAlreadyExist`].
///
/// # Errors
/// * [`crate::Error::DatabaseAlreadyExist`] - database already exists when `ignore_if_exists` is false.
async fn create_database(
&self,
name: &str,
ignore_if_exists: bool,
properties: HashMap<String, String>,
) -> Result<()>;
/// Get a database by name.
///
/// # Errors
/// * [`crate::Error::DatabaseNotExist`] - database does not exist.
async fn get_database(&self, name: &str) -> Result<Database>;
/// Drop a database.
///
/// * `ignore_if_not_exists` - if true, do nothing when the database does not exist.
/// * `cascade` - if true, delete all tables in the database then delete the database;
/// if false, return [`crate::Error::DatabaseNotEmpty`] when not empty.
///
/// # Errors
/// * [`crate::Error::DatabaseNotExist`] - database does not exist when `ignore_if_not_exists` is false.
/// * [`crate::Error::DatabaseNotEmpty`] - database is not empty when `cascade` is false.
async fn drop_database(
&self,
name: &str,
ignore_if_not_exists: bool,
cascade: bool,
) -> Result<()>;
// ======================= table methods ===============================
/// Get table metadata for the given identifier.
///
/// # Errors
/// * [`crate::Error::DatabaseNotExist`] - database in identifier does not exist.
/// * [`crate::Error::TableNotExist`] - table does not exist.
async fn get_table(&self, identifier: &Identifier) -> Result<Table>;
/// List table names in a database. System tables are not listed.
///
/// # Errors
/// * [`crate::Error::DatabaseNotExist`] - database does not exist.
async fn list_tables(&self, database_name: &str) -> Result<Vec<String>>;
/// Create a table.
///
/// * `ignore_if_exists` - if true, do nothing when the table already exists;
/// if false, return [`crate::Error::TableAlreadyExist`].
///
/// # Errors
/// * [`crate::Error::DatabaseNotExist`] - database in identifier does not exist.
/// * [`crate::Error::TableAlreadyExist`] - table already exists when `ignore_if_exists` is false.
async fn create_table(
&self,
identifier: &Identifier,
creation: Schema,
ignore_if_exists: bool,
) -> Result<()>;
/// Drop a table. System tables cannot be dropped.
///
/// # Errors
/// * [`crate::Error::TableNotExist`] - table does not exist when `ignore_if_not_exists` is false.
async fn drop_table(&self, identifier: &Identifier, ignore_if_not_exists: bool) -> Result<()>;
/// Rename a table.
///
/// # Errors
/// * [`crate::Error::TableNotExist`] - source table does not exist when `ignore_if_not_exists` is false.
/// * [`crate::Error::TableAlreadyExist`] - target table already exists.
async fn rename_table(
&self,
from: &Identifier,
to: &Identifier,
ignore_if_not_exists: bool,
) -> Result<()>;
/// Apply schema changes to a table.
///
/// # Errors
/// * [`crate::Error::TableNotExist`] - table does not exist when `ignore_if_not_exists` is false.
/// * [`crate::Error::ColumnAlreadyExist`] - adding a column that already exists.
/// * [`crate::Error::ColumnNotExist`] - altering or dropping a column that does not exist.
async fn alter_table(
&self,
identifier: &Identifier,
changes: Vec<SchemaChange>,
ignore_if_not_exists: bool,
) -> Result<()>;
/// List partitions for a table.
///
/// Default impl scans the table's manifest entries via
/// [`list_partitions_from_file_system`], matching Java
/// `AbstractCatalog.listPartitions`. Catalogs with metastore-tracked
/// partitions (e.g. `RESTCatalog`) override to return audit fields too.
async fn list_partitions(&self, identifier: &Identifier) -> Result<Vec<Partition>> {
let table = self.get_table(identifier).await?;
list_partitions_from_file_system(&table).await
}
/// Like [`Self::list_partitions`] but paged. Default impl ignores
/// `max_results` and `page_token`, returning all partitions in a single page.
/// Catalogs that need true pagination (e.g. `RESTCatalog`) override this.
async fn list_partitions_paged(
&self,
identifier: &Identifier,
_max_results: Option<u32>,
_page_token: Option<&str>,
) -> Result<PagedList<Partition>> {
Ok(PagedList::new(
self.list_partitions(identifier).await?,
None,
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_identifier_validate_should_reject_path_control_names() {
for (database, object) in [
("", "table"),
(" ", "table"),
(".", "table"),
("..", "table"),
("../escaped", "table"),
("db\\escaped", "table"),
("db\nescaped", "table"),
("db", ""),
("db", " "),
("db", "."),
("db", ".."),
("db", "../escaped"),
("db", "nested/table"),
("db", "nested\\table"),
("db", "table\0name"),
] {
let result = Identifier::new(database, object).validate();
assert!(
matches!(result, Err(Error::IdentifierInvalid { .. })),
"expected invalid identifier for database={database:?}, object={object:?}, got {result:?}"
);
}
}
#[test]
fn test_identifier_validate_should_allow_system_suffix_and_unicode_names() {
let identifier = Identifier::new("analytics", "orders$snapshots");
identifier.validate().unwrap();
assert_eq!(identifier.database(), "analytics");
assert_eq!(identifier.object(), "orders$snapshots");
let identifier = Identifier::new("数据", "订单");
identifier.validate().unwrap();
assert_eq!(identifier.database(), "数据");
assert_eq!(identifier.object(), "订单");
}
}