Skip to content

Commit 37376c2

Browse files
committed
feat: support catalog labels on tables loaded from REST catalogs
Parse the optional `labels` field of the REST load-table response and expose it through Table::labels(), mirroring apache/iceberg#18045 and live on the Table instance instead of TableMetadata. Part of #938.
1 parent 274d1bb commit 37376c2

12 files changed

Lines changed: 415 additions & 18 deletions

‎src/iceberg/catalog/rest/json_serde.cc‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
#include "iceberg/expression/json_serde_internal.h"
3434
#include "iceberg/file_format.h"
3535
#include "iceberg/json_serde_internal.h"
36+
#include "iceberg/labels.h"
3637
#include "iceberg/manifest/manifest_entry.h"
3738
#include "iceberg/partition_spec.h"
3839
#include "iceberg/schema.h"
@@ -72,6 +73,10 @@ constexpr std::string_view kDestination = "destination";
7273
constexpr std::string_view kMetadata = "metadata";
7374
constexpr std::string_view kConfig = "config";
7475
constexpr std::string_view kStorageCredentials = "storage-credentials";
76+
constexpr std::string_view kLabels = "labels";
77+
constexpr std::string_view kObjectLabels = "object-labels";
78+
constexpr std::string_view kFields = "fields";
79+
constexpr std::string_view kFieldId = "field-id";
7580
constexpr std::string_view kPrefix = "prefix";
7681
constexpr std::string_view kIdentifiers = "identifiers";
7782
constexpr std::string_view kOverrides = "overrides";
@@ -474,6 +479,59 @@ Result<RenameTableRequest> RenameTableRequestFromJson(const nlohmann::json& json
474479
return request;
475480
}
476481

482+
nlohmann::json ToJson(const FieldLabel& field_label) {
483+
nlohmann::json json;
484+
json[kFieldId] = field_label.field_id;
485+
json[kLabels] = field_label.labels;
486+
return json;
487+
}
488+
489+
Result<FieldLabel> FieldLabelFromJson(const nlohmann::json& json) {
490+
if (!json.is_object()) {
491+
return JsonParseError("Cannot parse field label from non-object: {}",
492+
SafeDumpJson(json));
493+
}
494+
FieldLabel field_label;
495+
ICEBERG_ASSIGN_OR_RAISE(field_label.field_id, GetJsonInteger<int32_t>(json, kFieldId));
496+
ICEBERG_ASSIGN_OR_RAISE(field_label.labels,
497+
GetJsonValue<decltype(field_label.labels)>(json, kLabels));
498+
return field_label;
499+
}
500+
501+
nlohmann::json ToJson(const Labels& labels) {
502+
nlohmann::json json = nlohmann::json::object();
503+
if (!labels.object_labels.empty()) {
504+
json[kObjectLabels] = labels.object_labels;
505+
}
506+
if (!labels.fields.empty()) {
507+
nlohmann::json fields = nlohmann::json::array();
508+
for (const auto& field_label : labels.fields) {
509+
fields.push_back(ToJson(field_label));
510+
}
511+
json[kFields] = std::move(fields);
512+
}
513+
return json;
514+
}
515+
516+
// A non-object value is treated as absent labels rather than failing the load.
517+
Result<Labels> LabelsFromJson(const nlohmann::json& json) {
518+
Labels labels;
519+
ICEBERG_ASSIGN_OR_RAISE(
520+
labels.object_labels,
521+
GetJsonValueOrDefault<decltype(labels.object_labels)>(json, kObjectLabels));
522+
if (auto it = json.find(kFields); it != json.end() && !it->is_null()) {
523+
if (!it->is_array()) {
524+
return JsonParseError("Cannot parse '{}' from non-array: {}", kFields,
525+
SafeDumpJson(*it));
526+
}
527+
for (const auto& entry : *it) {
528+
ICEBERG_ASSIGN_OR_RAISE(auto field_label, FieldLabelFromJson(entry));
529+
labels.fields.push_back(std::move(field_label));
530+
}
531+
}
532+
return labels;
533+
}
534+
477535
// LoadTableResult (used by CreateTableResponse, LoadTableResponse)
478536
Result<nlohmann::json> ToJson(const LoadTableResult& result) {
479537
nlohmann::json json;
@@ -488,6 +546,9 @@ Result<nlohmann::json> ToJson(const LoadTableResult& result) {
488546
}
489547
json[kStorageCredentials] = std::move(creds);
490548
}
549+
if (!result.labels.empty()) {
550+
json[kLabels] = ToJson(result.labels);
551+
}
491552
return json;
492553
}
493554

@@ -501,6 +562,9 @@ Result<LoadTableResult> LoadTableResultFromJson(const nlohmann::json& json) {
501562
ICEBERG_ASSIGN_OR_RAISE(result.config,
502563
GetJsonValueOrDefault<decltype(result.config)>(json, kConfig));
503564
ICEBERG_ASSIGN_OR_RAISE(result.storage_credentials, StorageCredentialsFromJson(json));
565+
if (auto it = json.find(kLabels); it != json.end() && !it->is_null()) {
566+
ICEBERG_ASSIGN_OR_RAISE(result.labels, LabelsFromJson(*it));
567+
}
504568
ICEBERG_RETURN_UNEXPECTED(result.Validate());
505569
return result;
506570
}
@@ -983,5 +1047,7 @@ ICEBERG_DEFINE_FROM_JSON(CommitTableResponse)
9831047
ICEBERG_DEFINE_FROM_JSON(OAuthTokenResponse)
9841048
ICEBERG_DEFINE_FROM_JSON(PlanTableScanRequest)
9851049
ICEBERG_DEFINE_FROM_JSON(FetchScanTasksRequest)
1050+
ICEBERG_DEFINE_FROM_JSON(FieldLabel)
1051+
ICEBERG_DEFINE_FROM_JSON(Labels)
9861052

9871053
} // namespace iceberg::rest

‎src/iceberg/catalog/rest/json_serde_internal.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ ICEBERG_DECLARE_JSON_SERDE(ListTablesResponse)
6060
ICEBERG_DECLARE_JSON_SERDE(RegisterTableRequest)
6161
ICEBERG_DECLARE_JSON_SERDE(RenameTableRequest)
6262
ICEBERG_DECLARE_JSON_SERDE(OAuthTokenResponse)
63+
ICEBERG_DECLARE_JSON_SERDE(FieldLabel)
64+
ICEBERG_DECLARE_JSON_SERDE(Labels)
6365

6466
#undef ICEBERG_DECLARE_JSON_SERDE
6567

‎src/iceberg/catalog/rest/rest_catalog.cc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -975,7 +975,7 @@ Result<std::shared_ptr<Table>> RestCatalog::MakeTableFromLoadResult(
975975
return Table::Make(identifier, std::move(result.metadata),
976976
std::move(result.metadata_location), std::move(table_io),
977977
std::move(table_catalog), RestTableName(name_, identifier),
978-
std::move(reporter));
978+
std::move(reporter), std::move(result.labels));
979979
}
980980

981981
Result<std::shared_ptr<Table>> RestCatalog::MakeTableFromCommitResponse(

‎src/iceberg/catalog/rest/types.cc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ bool CreateTableRequest::operator==(const CreateTableRequest& other) const {
8787

8888
bool LoadTableResult::operator==(const LoadTableResult& other) const {
8989
if (metadata_location != other.metadata_location || config != other.config ||
90-
storage_credentials != other.storage_credentials) {
90+
storage_credentials != other.storage_credentials || labels != other.labels) {
9191
return false;
9292
}
9393

‎src/iceberg/catalog/rest/types.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
#include "iceberg/catalog/rest/endpoint.h"
3131
#include "iceberg/catalog/rest/iceberg_rest_export.h"
32+
#include "iceberg/labels.h"
3233
#include "iceberg/result.h"
3334
#include "iceberg/storage_credential.h"
3435
#include "iceberg/table_identifier.h"
@@ -188,6 +189,8 @@ struct ICEBERG_REST_EXPORT LoadTableResult {
188189
std::unordered_map<std::string, std::string> config;
189190
/// \brief Vended storage credentials, one per URI prefix; empty if none.
190191
std::vector<StorageCredential> storage_credentials;
192+
/// \brief Catalog-provided labels; empty if none.
193+
Labels labels;
191194

192195
/// \brief Validates the LoadTableResult.
193196
Status Validate() const {

‎src/iceberg/cpp20_compatibility_internal.h‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
#include "iceberg/inspect/history_table.h"
8282
#include "iceberg/inspect/metadata_table.h"
8383
#include "iceberg/inspect/snapshots_table.h"
84+
#include "iceberg/labels.h"
8485
#include "iceberg/location_provider.h"
8586
#include "iceberg/logging/cerr_logger.h"
8687
#include "iceberg/logging/log_level.h"

‎src/iceberg/labels.h‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
#pragma once
21+
22+
/// \file iceberg/labels.h
23+
/// \brief Catalog-provided labels for tables and their fields.
24+
25+
#include <cstdint>
26+
#include <string>
27+
#include <unordered_map>
28+
#include <vector>
29+
30+
#include "iceberg/iceberg_export.h"
31+
32+
namespace iceberg {
33+
34+
/// \brief Catalog-provided labels for a single field, identified by its field ID.
35+
struct ICEBERG_EXPORT FieldLabel {
36+
int32_t field_id = 0;
37+
std::unordered_map<std::string, std::string> labels;
38+
39+
bool operator==(const FieldLabel&) const = default;
40+
};
41+
42+
/// \brief Optional catalog-provided labels returned when a table is loaded.
43+
///
44+
/// Labels are advisory enrichment, not table state: they are not persisted with the
45+
/// table and may be absent.
46+
struct ICEBERG_EXPORT Labels {
47+
std::unordered_map<std::string, std::string> object_labels;
48+
std::vector<FieldLabel> fields;
49+
50+
/// \brief Returns true when there are neither object-level nor field-level labels.
51+
bool empty() const { return object_labels.empty() && fields.empty(); }
52+
53+
bool operator==(const Labels&) const = default;
54+
};
55+
56+
} // namespace iceberg

‎src/iceberg/table.cc‎

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -56,13 +56,11 @@
5656

5757
namespace iceberg {
5858

59-
Result<std::shared_ptr<Table>> Table::Make(TableIdentifier identifier,
60-
std::shared_ptr<TableMetadata> metadata,
61-
std::string metadata_location,
62-
std::shared_ptr<FileIO> io,
63-
std::shared_ptr<Catalog> catalog,
64-
std::string full_name,
65-
std::shared_ptr<MetricsReporter> reporter) {
59+
Result<std::shared_ptr<Table>> Table::Make(
60+
TableIdentifier identifier, std::shared_ptr<TableMetadata> metadata,
61+
std::string metadata_location, std::shared_ptr<FileIO> io,
62+
std::shared_ptr<Catalog> catalog, std::string full_name,
63+
std::shared_ptr<MetricsReporter> reporter, Labels labels) {
6664
if (metadata == nullptr) [[unlikely]] {
6765
return InvalidArgument("Metadata cannot be null");
6866
}
@@ -75,25 +73,27 @@ Result<std::shared_ptr<Table>> Table::Make(TableIdentifier identifier,
7573
if (catalog == nullptr) [[unlikely]] {
7674
return InvalidArgument("Catalog cannot be null");
7775
}
78-
return std::shared_ptr<Table>(new Table(
79-
std::move(identifier), std::move(metadata), std::move(metadata_location),
80-
std::move(io), std::move(catalog), std::move(full_name), std::move(reporter)));
76+
return std::shared_ptr<Table>(new Table(std::move(identifier), std::move(metadata),
77+
std::move(metadata_location), std::move(io),
78+
std::move(catalog), std::move(full_name),
79+
std::move(reporter), std::move(labels)));
8180
}
8281

8382
Table::~Table() = default;
8483

8584
Table::Table(TableIdentifier identifier, std::shared_ptr<TableMetadata> metadata,
8685
std::string metadata_location, std::shared_ptr<FileIO> io,
8786
std::shared_ptr<Catalog> catalog, std::string full_name,
88-
std::shared_ptr<MetricsReporter> reporter)
87+
std::shared_ptr<MetricsReporter> reporter, Labels labels)
8988
: identifier_(std::move(identifier)),
9089
full_name_(full_name.empty() ? identifier_.ToString() : std::move(full_name)),
9190
metadata_(std::move(metadata)),
9291
metadata_location_(std::move(metadata_location)),
9392
io_(std::move(io)),
9493
catalog_(std::move(catalog)),
9594
reporter_(std::move(reporter)),
96-
metadata_cache_(std::make_unique<TableMetadataCache>(metadata_.get())) {}
95+
metadata_cache_(std::make_unique<TableMetadataCache>(metadata_.get())),
96+
labels_(std::move(labels)) {}
9797

9898
const std::string& Table::uuid() const { return metadata_->table_uuid; }
9999

@@ -167,6 +167,8 @@ const std::shared_ptr<Catalog>& Table::catalog() const { return catalog_; }
167167

168168
const std::shared_ptr<MetricsReporter>& Table::reporter() const { return reporter_; }
169169

170+
const Labels& Table::labels() const { return labels_; }
171+
170172
Result<std::unique_ptr<LocationProvider>> Table::location_provider() const {
171173
return LocationProvider::Make(metadata_->location, metadata_->properties);
172174
}

‎src/iceberg/table.h‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <vector>
3030

3131
#include "iceberg/iceberg_export.h"
32+
#include "iceberg/labels.h"
3233
#include "iceberg/snapshot.h"
3334
#include "iceberg/table_identifier.h"
3435
#include "iceberg/type_fwd.h"
@@ -50,11 +51,12 @@ class ICEBERG_EXPORT Table : public std::enable_shared_from_this<Table> {
5051
/// string representation of identifier when empty.
5152
/// \param[in] reporter Optional metrics reporter for this table. Defaults to nullptr
5253
/// (noop).
54+
/// \param[in] labels Catalog-provided labels for this table. Defaults to empty.
5355
static Result<std::shared_ptr<Table>> Make(
5456
TableIdentifier identifier, std::shared_ptr<TableMetadata> metadata,
5557
std::string metadata_location, std::shared_ptr<FileIO> io,
5658
std::shared_ptr<Catalog> catalog, std::string full_name = "",
57-
std::shared_ptr<MetricsReporter> reporter = nullptr);
59+
std::shared_ptr<MetricsReporter> reporter = nullptr, Labels labels = {});
5860

5961
virtual ~Table();
6062

@@ -130,6 +132,13 @@ class ICEBERG_EXPORT Table : public std::enable_shared_from_this<Table> {
130132
/// \brief Returns the metrics reporter for this table.
131133
const std::shared_ptr<MetricsReporter>& reporter() const;
132134

135+
/// \brief Returns the catalog-provided labels for this table.
136+
///
137+
/// Labels come only from the catalog's load, create or register response and are fixed
138+
/// for the life of this table: Refresh() does not update them, and a table returned by
139+
/// Transaction::Commit() has none. Load the table again to get current labels.
140+
const Labels& labels() const;
141+
133142
/// \brief Returns a LocationProvider for this table
134143
Result<std::unique_ptr<LocationProvider>> location_provider() const;
135144

@@ -216,7 +225,7 @@ class ICEBERG_EXPORT Table : public std::enable_shared_from_this<Table> {
216225
Table(TableIdentifier identifier, std::shared_ptr<TableMetadata> metadata,
217226
std::string metadata_location, std::shared_ptr<FileIO> io,
218227
std::shared_ptr<Catalog> catalog, std::string full_name,
219-
std::shared_ptr<MetricsReporter> reporter = nullptr);
228+
std::shared_ptr<MetricsReporter> reporter = nullptr, Labels labels = {});
220229

221230
const TableIdentifier identifier_;
222231
const std::string full_name_;
@@ -226,6 +235,7 @@ class ICEBERG_EXPORT Table : public std::enable_shared_from_this<Table> {
226235
std::shared_ptr<Catalog> catalog_;
227236
std::shared_ptr<MetricsReporter> reporter_;
228237
std::unique_ptr<class TableMetadataCache> metadata_cache_;
238+
const Labels labels_;
229239
};
230240

231241
/// \brief A table created by stage-create and not yet committed.

0 commit comments

Comments
 (0)