blob: 5d2bb8238510282f50cdd6e6c7e0eace16b8a0c8 [file]
/*
* Copyright 2026-present Alibaba Inc.
*
* Licensed 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.
*/
#include "paimon/rest/rest_catalog.h"
#include <algorithm>
#include <utility>
#include "arrow/c/bridge.h"
#include "fmt/format.h"
#include "paimon/catalog/table.h"
#include "paimon/catalog_options.h"
#include "paimon/common/utils/arrow/status_utils.h"
#include "paimon/common/utils/checked_cast.h"
#include "paimon/common/utils/rapidjson_util.h"
#include "paimon/common/utils/string_utils.h"
#include "paimon/core/catalog/catalog_utils.h"
#include "paimon/core/core_options.h"
#include "paimon/core/schema/table_schema.h"
#include "paimon/core/table/system/global_system_tables.h"
#include "paimon/core/table/system/system_table.h"
#include "paimon/core/table/system/system_table_schema.h"
#include "paimon/defs.h"
#include "paimon/fs/file_system.h"
#include "paimon/rest/rest_util.h"
#include "rapidjson/document.h"
namespace paimon {
namespace {
constexpr const char kPathOption[] = "path";
// Maps the default branch "main" to "no branch": it is addressed as the bare table. The
// comparison ignores case, as the identifier the Java client sends is built the same way,
// so "MAIN" also resolves to the bare table and cannot address a branch of that name.
// `BranchManager::IsMainBranch`, which names the branch directory of a table, stays
// case-sensitive: this normalization only decides how a table is addressed on the server.
std::optional<std::string> NormalizeBranch(std::optional<std::string> branch) {
if (branch && StringUtils::ToLowerCase(branch.value()) == Identifier::kDefaultMainBranch) {
return std::nullopt;
}
return branch;
}
// Builds the "<table>$branch_<branch>" object name addressing `branch` of `table_name`
// on the rest server.
std::string BranchObjectName(std::string table_name, const std::string& branch) {
table_name.append(Identifier::kSystemTableSplitter);
table_name.append(Identifier::kSystemBranchPrefix);
table_name.append(branch);
return table_name;
}
// Builds the identifier sent to the rest server: the system table suffix is stripped
// while the branch stays in the object name, so the server resolves the branch itself and
// returns the branch's own schema. The path the server reports is the data table root in
// either case; the branch subdirectory is derived downstream from the branch option and
// must not be applied twice (see `ToTableSchema`).
Result<Identifier> ToLoadIdentifier(const Identifier& identifier) {
PAIMON_ASSIGN_OR_RAISE(std::string data_table_name, identifier.GetDataTableName());
PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> branch, identifier.GetBranchName());
branch = NormalizeBranch(std::move(branch));
std::string object_name = branch ? BranchObjectName(std::move(data_table_name), branch.value())
: std::move(data_table_name);
return Identifier(identifier.GetDatabaseName(), object_name);
}
} // namespace
RestCatalog::RestCatalog(std::unique_ptr<RestApi> api, const std::shared_ptr<FileSystem>& fs,
const std::string& warehouse)
: api_(std::move(api)),
fs_(fs),
warehouse_(warehouse),
table_default_options_(RestUtil::ExtractPrefixMap(
api_->GetMergedOptions(), CatalogOptions::TABLE_DEFAULT_OPTION_PREFIX)),
logger_(Logger::GetLogger("RestCatalog")) {}
Result<std::unique_ptr<RestCatalog>> RestCatalog::Create(
const std::string& warehouse, const std::map<std::string, std::string>& options,
const std::shared_ptr<FileSystem>& file_system, const RestHttpClient::Config& http_config) {
PAIMON_ASSIGN_OR_RAISE(
std::unique_ptr<RestApi> api,
RestApi::Create(options, warehouse, /*config_required=*/true, http_config));
PAIMON_ASSIGN_OR_RAISE(CoreOptions core_options,
CoreOptions::FromMap(api->GetMergedOptions(), file_system));
return std::unique_ptr<RestCatalog>(
new RestCatalog(std::move(api), core_options.GetFileSystem(), warehouse));
}
const std::map<std::string, std::string>& RestCatalog::GetOptions() const {
return api_->GetMergedOptions();
}
Status RestCatalog::CreateDatabase(const std::string& name,
const std::map<std::string, std::string>& options,
bool ignore_if_exists) {
PAIMON_RETURN_NOT_OK(CatalogUtils::CheckNotSystemDatabase(name, "createDatabase"));
Status status = api_->CreateDatabase(name, options);
if (status.IsExist() && ignore_if_exists) {
return Status::OK();
}
return status;
}
Result<std::vector<std::string>> RestCatalog::ListDatabases() const {
return api_->ListDatabases();
}
Result<bool> RestCatalog::DatabaseExists(const std::string& db_name) const {
if (CatalogUtils::IsSystemDatabase(db_name)) {
return true;
}
Result<GetDatabaseResponse> response = api_->GetDatabase(db_name);
if (response.ok()) {
return true;
}
if (response.status().IsNotExist()) {
return false;
}
return response.status();
}
Status RestCatalog::DropDatabase(const std::string& name, bool ignore_if_not_exists, bool cascade) {
PAIMON_RETURN_NOT_OK(CatalogUtils::CheckNotSystemDatabase(name, "dropDatabase"));
if (!cascade) {
Result<std::vector<std::string>> tables = ListTables(name);
if (!tables.ok()) {
if (tables.status().IsNotExist() && ignore_if_not_exists) {
return Status::OK();
}
return tables.status();
}
if (!tables.value().empty()) {
return Status::Invalid(
fmt::format("Cannot drop non-empty database {}. Use cascade=true to force.", name));
}
}
Status status = api_->DropDatabase(name);
if (status.IsNotExist() && ignore_if_not_exists) {
return Status::OK();
}
return status;
}
std::string RestCatalog::GetDatabaseLocation(const std::string& db_name) const {
// The virtual "sys" database has no location and is unknown to the server.
if (CatalogUtils::IsSystemDatabase(db_name)) {
return "";
}
Result<GetDatabaseResponse> response = api_->GetDatabase(db_name);
if (!response.ok()) {
PAIMON_LOG_WARN(logger_, "failed to get location of database %s: %s", db_name.c_str(),
response.status().ToString().c_str());
return "";
}
return response.value().GetLocation();
}
Result<std::vector<std::string>> RestCatalog::ListTables(const std::string& db_name) const {
if (CatalogUtils::IsSystemDatabase(db_name)) {
return GlobalSystemTableLoader::GetSupportedTableNames(api_->GetMergedOptions());
}
return api_->ListTables(db_name);
}
Status RestCatalog::CreateTable(const Identifier& identifier, ArrowSchema* c_schema,
const std::vector<std::string>& partition_keys,
const std::vector<std::string>& primary_keys,
const std::map<std::string, std::string>& options,
bool ignore_if_exists) {
PAIMON_RETURN_NOT_OK(CatalogUtils::CheckNotBranch(identifier, "createTable"));
PAIMON_RETURN_NOT_OK(CatalogUtils::CheckNotSystemTable(identifier, "createTable"));
PAIMON_ASSIGN_OR_RAISE_FROM_ARROW(std::shared_ptr<arrow::Schema> schema,
arrow::ImportSchema(c_schema));
std::map<std::string, std::string> effective_options = options;
for (const auto& [key, value] : table_default_options_) {
effective_options.emplace(key, value);
}
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableSchema> table_schema,
TableSchema::Create(TableSchema::FIRST_SCHEMA_ID, schema, partition_keys,
primary_keys, effective_options));
std::string schema_json;
try {
rapidjson::Document doc;
doc.SetObject();
rapidjson::Document::AllocatorType& allocator = doc.GetAllocator();
doc.AddMember(rapidjson::StringRef("fields"),
RapidJsonUtil::SerializeValue(table_schema->Fields(), &allocator).Move(),
allocator);
doc.AddMember(rapidjson::StringRef("partitionKeys"),
RapidJsonUtil::SerializeValue(partition_keys, &allocator).Move(), allocator);
doc.AddMember(rapidjson::StringRef("primaryKeys"),
RapidJsonUtil::SerializeValue(primary_keys, &allocator).Move(), allocator);
doc.AddMember(rapidjson::StringRef("options"),
RapidJsonUtil::SerializeValue(effective_options, &allocator).Move(),
allocator);
schema_json = RestUtil::JsonToString(doc);
} catch (const std::exception& e) {
return Status::SerializationError("failed to serialize create table schema: ", e.what());
}
Status status = api_->CreateTable(identifier, schema_json);
if (status.IsExist() && ignore_if_exists) {
return Status::OK();
}
return status;
}
Result<bool> RestCatalog::TableExists(const Identifier& identifier) const {
if (CatalogUtils::IsSystemDatabase(identifier.GetDatabaseName())) {
return GlobalSystemTableLoader::IsSupported(identifier.GetTableName(),
api_->GetMergedOptions());
}
PAIMON_ASSIGN_OR_RAISE(bool is_system_table, identifier.IsSystemTable());
if (is_system_table) {
PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> system_table_name,
identifier.GetSystemTableName());
if (!system_table_name || !SystemTableLoader::IsSupported(system_table_name.value())) {
return false;
}
}
PAIMON_ASSIGN_OR_RAISE(Identifier load_identifier, ToLoadIdentifier(identifier));
Result<GetTableResponse> response = api_->GetTable(load_identifier);
if (response.ok()) {
return true;
}
if (response.status().IsNotExist()) {
return false;
}
return response.status();
}
Result<std::string> RestCatalog::GetTableLocation(const Identifier& identifier) const {
PAIMON_ASSIGN_OR_RAISE(Identifier load_identifier, ToLoadIdentifier(identifier));
PAIMON_ASSIGN_OR_RAISE(GetTableResponse response, api_->GetTable(load_identifier));
return response.GetPath();
}
Status RestCatalog::DropTable(const Identifier& identifier, bool ignore_if_not_exists) {
PAIMON_RETURN_NOT_OK(CatalogUtils::CheckNotBranch(identifier, "dropTable"));
PAIMON_RETURN_NOT_OK(CatalogUtils::CheckNotSystemTable(identifier, "dropTable"));
Status status = api_->DropTable(identifier);
if (status.IsNotExist() && ignore_if_not_exists) {
return Status::OK();
}
return status;
}
Status RestCatalog::RenameTable(const Identifier& from_table, const Identifier& to_table,
bool ignore_if_not_exists) {
PAIMON_RETURN_NOT_OK(CatalogUtils::CheckNotBranch(from_table, "renameTable"));
PAIMON_RETURN_NOT_OK(CatalogUtils::CheckNotBranch(to_table, "renameTable"));
PAIMON_RETURN_NOT_OK(CatalogUtils::CheckNotSystemTable(from_table, "renameTable"));
PAIMON_RETURN_NOT_OK(CatalogUtils::CheckNotSystemTable(to_table, "renameTable"));
Status status = api_->RenameTable(from_table, to_table);
if (status.IsNotExist() && ignore_if_not_exists) {
return Status::OK();
}
return status;
}
Result<std::unique_ptr<TableSchema>> RestCatalog::ToTableSchema(
const GetTableResponse& response, const std::optional<std::string>& branch) {
std::string table_schema_json;
try {
rapidjson::Document src;
src.Parse(response.GetSchemaJson().c_str());
if (src.HasParseError() || !src.IsObject() || !src.HasMember("fields") ||
!src["fields"].IsArray()) {
return Status::Invalid("invalid table schema json from the rest server");
}
rapidjson::Document out;
out.SetObject();
rapidjson::Document::AllocatorType& allocator = out.GetAllocator();
out.AddMember(rapidjson::StringRef("version"), TableSchema::CURRENT_VERSION, allocator);
out.AddMember(rapidjson::StringRef("id"), response.GetSchemaId(), allocator);
PAIMON_ASSIGN_OR_RAISE(int32_t highest_field_id,
TableSchema::ComputeHighestFieldId(src["fields"]));
rapidjson::Value fields(rapidjson::kArrayType);
fields.CopyFrom(src["fields"], allocator);
out.AddMember(rapidjson::StringRef("highestFieldId"), highest_field_id, allocator);
out.AddMember(rapidjson::StringRef("fields"), fields.Move(), allocator);
// A missing or wrong-typed member fails instead of defaulting to an empty value,
// which would silently change table semantics (an omitted "partitionKeys" would
// load a partitioned table as unpartitioned); Java rejects absent members too.
for (const char* key : {"partitionKeys", "primaryKeys"}) {
if (!src.HasMember(key)) {
return Status::Invalid(fmt::format(
"invalid table schema json from the rest server: missing '{}'", key));
}
if (!src[key].IsArray()) {
return Status::Invalid(fmt::format(
"invalid table schema json from the rest server: '{}' is not an array", key));
}
rapidjson::Value keys(rapidjson::kArrayType);
keys.CopyFrom(src[key], allocator);
out.AddMember(rapidjson::StringRef(key), keys.Move(), allocator);
}
if (!src.HasMember("options")) {
return Status::Invalid(
"invalid table schema json from the rest server: missing 'options'");
}
if (!src["options"].IsObject()) {
return Status::Invalid(
"invalid table schema json from the rest server: 'options' is not an object");
}
auto options_map =
RapidJsonUtil::DeserializeValue<std::map<std::string, std::string>>(src["options"]);
options_map[kPathOption] = response.GetPath();
response.GetAuditFields().PutAuditOptionsTo(&options_map);
if (branch) {
options_map[Options::BRANCH] = branch.value();
}
out.AddMember(rapidjson::StringRef("options"),
RapidJsonUtil::SerializeValue(options_map, &allocator).Move(), allocator);
if (src.HasMember("comment") && src["comment"].IsString()) {
rapidjson::Value comment;
comment.CopyFrom(src["comment"], allocator);
out.AddMember(rapidjson::StringRef("comment"), comment.Move(), allocator);
}
// The server's audit time keeps the conversion deterministic; 0 (the epoch)
// when the server did not report an "updatedAt".
out.AddMember(rapidjson::StringRef("timeMillis"),
response.GetAuditFields().updated_at.value_or(0), allocator);
table_schema_json = RestUtil::JsonToString(out);
} catch (const std::exception& e) {
return Status::Invalid("failed to convert rest table schema: ", e.what());
}
return TableSchema::CreateFromJson(table_schema_json);
}
Result<std::shared_ptr<TableSchema>> RestCatalog::LoadDataTableSchema(
const Identifier& data_identifier, const std::optional<std::string>& branch,
std::string* table_path) const {
PAIMON_ASSIGN_OR_RAISE(GetTableResponse response, api_->GetTable(data_identifier));
if (table_path != nullptr) {
*table_path = response.GetPath();
}
PAIMON_ASSIGN_OR_RAISE(std::unique_ptr<TableSchema> schema, ToTableSchema(response, branch));
return std::shared_ptr<TableSchema>(std::move(schema));
}
Result<std::shared_ptr<Schema>> RestCatalog::LoadTableSchema(const Identifier& identifier) const {
// Serve the global system tables of the "sys" database locally, like
// FileSystemCatalog.
if (CatalogUtils::IsSystemDatabase(identifier.GetDatabaseName())) {
PAIMON_ASSIGN_OR_RAISE(bool supported,
GlobalSystemTableLoader::IsSupported(identifier.GetTableName(),
api_->GetMergedOptions()));
if (!supported) {
return Status::NotExist(fmt::format("{} not exist", identifier.ToString()));
}
GlobalSystemTableContext context;
context.catalog = this;
context.fs = fs_;
context.warehouse = warehouse_;
context.catalog_options = api_->GetMergedOptions();
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<SystemTable> system_table,
GlobalSystemTableLoader::Load(identifier.GetTableName(), context));
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> arrow_schema,
system_table->ArrowSchema());
return std::make_shared<SystemTableSchema>(std::move(arrow_schema));
}
PAIMON_ASSIGN_OR_RAISE(bool is_system_table, identifier.IsSystemTable());
PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> branch, identifier.GetBranchName());
branch = NormalizeBranch(std::move(branch));
PAIMON_ASSIGN_OR_RAISE(Identifier load_identifier, ToLoadIdentifier(identifier));
if (is_system_table) {
PAIMON_ASSIGN_OR_RAISE(std::optional<std::string> system_table_name,
identifier.GetSystemTableName());
if (!system_table_name || !SystemTableLoader::IsSupported(system_table_name.value())) {
return Status::NotExist(fmt::format("{} not exist", identifier.ToString()));
}
std::string table_path;
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<TableSchema> latest_schema,
LoadDataTableSchema(load_identifier, branch, &table_path));
std::map<std::string, std::string> dynamic_options;
if (branch) {
dynamic_options[Options::BRANCH] = branch.value();
}
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<SystemTable> system_table,
SystemTableLoader::Load(system_table_name.value(), fs_, table_path,
latest_schema, dynamic_options));
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<arrow::Schema> arrow_schema,
system_table->ArrowSchema());
return std::make_shared<SystemTableSchema>(std::move(arrow_schema));
}
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<TableSchema> schema,
LoadDataTableSchema(load_identifier, branch, nullptr));
return checked_pointer_cast<Schema>(schema);
}
Result<std::shared_ptr<Table>> RestCatalog::GetTable(const Identifier& identifier) const {
PAIMON_ASSIGN_OR_RAISE(std::shared_ptr<Schema> schema, LoadTableSchema(identifier));
return std::make_shared<Table>(schema, identifier.GetDatabaseName(), identifier.GetTableName());
}
std::string RestCatalog::GetRootPath() const {
return warehouse_;
}
std::shared_ptr<FileSystem> RestCatalog::GetFileSystem() const {
return fs_;
}
Result<std::vector<SnapshotInfo>> RestCatalog::ListSnapshots(const Identifier& identifier,
const std::string& branch) const {
PAIMON_RETURN_NOT_OK(CatalogUtils::CheckNotBranch(identifier, "listSnapshots"));
PAIMON_RETURN_NOT_OK(CatalogUtils::CheckNotSystemTable(identifier, "listSnapshots"));
std::optional<std::string> normalized_branch =
NormalizeBranch(branch.empty() ? std::nullopt : std::make_optional(branch));
std::string object_name =
normalized_branch ? BranchObjectName(identifier.GetTableName(), normalized_branch.value())
: identifier.GetTableName();
Identifier load_identifier(identifier.GetDatabaseName(), object_name);
// The Catalog interface has no pagination, so all pages are fetched; the server
// does not order snapshots across pages while the contract requires ascending ids.
PAIMON_ASSIGN_OR_RAISE(std::vector<Snapshot> snapshots, api_->ListSnapshots(load_identifier));
std::sort(snapshots.begin(), snapshots.end(),
[](const Snapshot& a, const Snapshot& b) { return a.Id() < b.Id(); });
std::vector<SnapshotInfo> result;
result.reserve(snapshots.size());
for (const auto& snapshot : snapshots) {
result.push_back(snapshot.ToSnapshotInfo());
}
return result;
}
} // namespace paimon