Skip to content

Commit ad221b7

Browse files
authored
feat: support catalog labels on tables loaded from REST catalogs (#960)
Parse the optional `labels` field of the REST load-table response and expose it through Table::labels(), mirroring apache/iceberg#18045 and apache/iceberg#18046. Labels are catalog-provided rather than table state, so they live on the Table instance instead of TableMetadata. Part of #938.
1 parent 57ec6b8 commit ad221b7

16 files changed

Lines changed: 552 additions & 38 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";
@@ -488,6 +493,59 @@ Result<RenameTableRequest> RenameTableRequestFromJson(const nlohmann::json& json
488493
return request;
489494
}
490495

496+
nlohmann::json ToJson(const FieldLabel& field_label) {
497+
nlohmann::json json;
498+
json[kFieldId] = field_label.field_id;
499+
json[kLabels] = field_label.labels;
500+
return json;
501+
}
502+
503+
Result<FieldLabel> FieldLabelFromJson(const nlohmann::json& json) {
504+
if (!json.is_object()) {
505+
return JsonParseError("Cannot parse field label from non-object: {}",
506+
SafeDumpJson(json));
507+
}
508+
FieldLabel field_label;
509+
ICEBERG_ASSIGN_OR_RAISE(field_label.field_id, GetJsonInteger<int32_t>(json, kFieldId));
510+
ICEBERG_ASSIGN_OR_RAISE(field_label.labels,
511+
GetJsonValue<decltype(field_label.labels)>(json, kLabels));
512+
return field_label;
513+
}
514+
515+
nlohmann::json ToJson(const Labels& labels) {
516+
nlohmann::json json = nlohmann::json::object();
517+
if (!labels.object_labels.empty()) {
518+
json[kObjectLabels] = labels.object_labels;
519+
}
520+
if (!labels.fields.empty()) {
521+
nlohmann::json fields = nlohmann::json::array();
522+
for (const auto& field_label : labels.fields) {
523+
fields.push_back(ToJson(field_label));
524+
}
525+
json[kFields] = std::move(fields);
526+
}
527+
return json;
528+
}
529+
530+
// A non-object value is treated as absent labels rather than failing the load.
531+
Result<Labels> LabelsFromJson(const nlohmann::json& json) {
532+
Labels labels;
533+
ICEBERG_ASSIGN_OR_RAISE(
534+
labels.object_labels,
535+
GetJsonValueOrDefault<decltype(labels.object_labels)>(json, kObjectLabels));
536+
if (auto it = json.find(kFields); it != json.end() && !it->is_null()) {
537+
if (!it->is_array()) {
538+
return JsonParseError("Cannot parse '{}' from non-array: {}", kFields,
539+
SafeDumpJson(*it));
540+
}
541+
for (const auto& entry : *it) {
542+
ICEBERG_ASSIGN_OR_RAISE(auto field_label, FieldLabelFromJson(entry));
543+
labels.fields.push_back(std::move(field_label));
544+
}
545+
}
546+
return labels;
547+
}
548+
491549
// LoadTableResult (used by CreateTableResponse, LoadTableResponse)
492550
Result<nlohmann::json> ToJson(const LoadTableResult& result) {
493551
nlohmann::json json;
@@ -502,6 +560,9 @@ Result<nlohmann::json> ToJson(const LoadTableResult& result) {
502560
}
503561
json[kStorageCredentials] = std::move(creds);
504562
}
563+
if (!result.labels.empty()) {
564+
json[kLabels] = ToJson(result.labels);
565+
}
505566
return json;
506567
}
507568

@@ -515,6 +576,9 @@ Result<LoadTableResult> LoadTableResultFromJson(const nlohmann::json& json) {
515576
ICEBERG_ASSIGN_OR_RAISE(result.config,
516577
GetJsonValueOrDefault<decltype(result.config)>(json, kConfig));
517578
ICEBERG_ASSIGN_OR_RAISE(result.storage_credentials, StorageCredentialsFromJson(json));
579+
if (auto it = json.find(kLabels); it != json.end() && !it->is_null()) {
580+
ICEBERG_ASSIGN_OR_RAISE(result.labels, LabelsFromJson(*it));
581+
}
518582
ICEBERG_RETURN_UNEXPECTED(result.Validate());
519583
return result;
520584
}
@@ -1003,5 +1067,7 @@ ICEBERG_DEFINE_FROM_JSON(CommitTableResponse)
10031067
ICEBERG_DEFINE_FROM_JSON(OAuthTokenResponse)
10041068
ICEBERG_DEFINE_FROM_JSON(PlanTableScanRequest)
10051069
ICEBERG_DEFINE_FROM_JSON(FetchScanTasksRequest)
1070+
ICEBERG_DEFINE_FROM_JSON(FieldLabel)
1071+
ICEBERG_DEFINE_FROM_JSON(Labels)
10061072

10071073
} // 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: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
#include "iceberg/catalog/rest/rest_util.h"
4848
#include "iceberg/catalog/rest/types.h"
4949
#include "iceberg/json_serde_internal.h"
50+
#include "iceberg/labels.h"
5051
#include "iceberg/logging/log_level.h"
5152
#include "iceberg/logging/log_macros.h"
5253
#include "iceberg/logging/logger.h"
@@ -984,6 +985,11 @@ Result<std::shared_ptr<Table>> RestCatalog::MakeTableFromLoadResult(
984985
std::shared_ptr<auth::AuthSession> contextual_session) {
985986
auto table_config = std::move(result.config);
986987
auto storage_credentials = std::move(result.storage_credentials);
988+
// Labels are usually absent; keep the table free of an empty Labels allocation.
989+
std::unique_ptr<Labels> labels;
990+
if (!result.labels.empty()) {
991+
labels = std::make_unique<Labels>(std::move(result.labels));
992+
}
987993
// Before the FileIO: refreshing its credentials reuses the table session.
988994
ICEBERG_ASSIGN_OR_RAISE(
989995
auto table_session,
@@ -997,7 +1003,7 @@ Result<std::shared_ptr<Table>> RestCatalog::MakeTableFromLoadResult(
9971003
return MakeTable(identifier, std::move(result.metadata),
9981004
std::move(result.metadata_location), std::move(table_io),
9991005
std::move(table_catalog), std::move(reporter), table_config,
1000-
std::move(table_session));
1006+
std::move(table_session), std::move(labels));
10011007
}
10021008

10031009
Result<std::shared_ptr<Table>> RestCatalog::MakeTableFromCommitResponse(
@@ -1012,15 +1018,15 @@ Result<std::shared_ptr<Table>> RestCatalog::MakeTableFromCommitResponse(
10121018
return MakeTable(identifier, std::move(response.metadata),
10131019
std::move(response.metadata_location), std::move(table_io),
10141020
std::move(table_catalog), std::move(reporter), table_config,
1015-
std::move(table_session));
1021+
std::move(table_session), /*labels=*/nullptr);
10161022
}
10171023

10181024
Result<std::shared_ptr<Table>> RestCatalog::MakeTable(
10191025
const TableIdentifier& identifier, std::shared_ptr<TableMetadata> metadata,
10201026
std::string metadata_location, std::shared_ptr<FileIO> table_io,
10211027
std::shared_ptr<Catalog> table_catalog, std::shared_ptr<MetricsReporter> reporter,
10221028
const std::unordered_map<std::string, std::string>& table_config,
1023-
std::shared_ptr<auth::AuthSession> table_session) {
1029+
std::shared_ptr<auth::AuthSession> table_session, std::unique_ptr<Labels> labels) {
10241030
// Table config overrides client config.
10251031
ICEBERG_ASSIGN_OR_RAISE(auto server_mode,
10261032
RestCatalogProperties::ScanPlanningModeFrom(table_config));
@@ -1068,12 +1074,13 @@ Result<std::shared_ptr<Table>> RestCatalog::MakeTable(
10681074
return RestTable::Make(identifier, std::move(metadata), std::move(metadata_location),
10691075
std::move(table_io), std::move(table_catalog),
10701076
RestTableName(name_, identifier), std::move(reporter),
1071-
std::move(rest_ctx));
1077+
std::move(rest_ctx), std::move(labels));
10721078
}
10731079

10741080
return Table::Make(identifier, std::move(metadata), std::move(metadata_location),
10751081
std::move(table_io), std::move(table_catalog),
1076-
RestTableName(name_, identifier), std::move(reporter));
1082+
RestTableName(name_, identifier), std::move(reporter),
1083+
std::move(labels));
10771084
}
10781085

10791086
} // namespace iceberg::rest

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
#include "iceberg/catalog/rest/type_fwd.h"
3333
#include "iceberg/catalog/session_catalog.h"
3434
#include "iceberg/catalog/session_context.h"
35+
#include "iceberg/labels.h"
3536
#include "iceberg/result.h"
3637
#include "iceberg/storage_credential.h"
3738
#include "iceberg/type_fwd.h"
@@ -214,7 +215,8 @@ class ICEBERG_REST_EXPORT RestCatalog final
214215
std::string metadata_location, std::shared_ptr<FileIO> table_io,
215216
std::shared_ptr<Catalog> table_catalog, std::shared_ptr<MetricsReporter> reporter,
216217
const std::unordered_map<std::string, std::string>& table_config,
217-
std::shared_ptr<auth::AuthSession> table_session);
218+
std::shared_ptr<auth::AuthSession> table_session,
219+
std::unique_ptr<Labels> labels = nullptr);
218220

219221
RestCatalogProperties config_;
220222
std::shared_ptr<FileIO> file_io_;

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
#include <utility>
2424

2525
#include "iceberg/catalog/rest/rest_table_scan.h"
26+
#include "iceberg/labels.h"
2627
#include "iceberg/result.h"
2728
#include "iceberg/table_metadata.h"
2829
#include "iceberg/util/macros.h"
@@ -33,9 +34,10 @@ RestTable::RestTable(TableIdentifier identifier, std::shared_ptr<TableMetadata>
3334
std::string metadata_location, std::shared_ptr<FileIO> io,
3435
std::shared_ptr<Catalog> catalog, std::string full_name,
3536
std::shared_ptr<MetricsReporter> reporter,
36-
RestScanContext rest_context)
37+
RestScanContext rest_context, std::unique_ptr<Labels> labels)
3738
: Table(std::move(identifier), std::move(metadata), std::move(metadata_location),
38-
std::move(io), std::move(catalog), std::move(full_name), std::move(reporter)),
39+
std::move(io), std::move(catalog), std::move(full_name), std::move(reporter),
40+
std::move(labels)),
3941
rest_context_(std::make_shared<const RestScanContext>(std::move(rest_context))) {}
4042

4143
RestTable::~RestTable() = default;
@@ -44,14 +46,15 @@ Result<std::shared_ptr<RestTable>> RestTable::Make(
4446
TableIdentifier identifier, std::shared_ptr<TableMetadata> metadata,
4547
std::string metadata_location, std::shared_ptr<FileIO> io,
4648
std::shared_ptr<Catalog> catalog, std::string full_name,
47-
std::shared_ptr<MetricsReporter> reporter, RestScanContext rest_context) {
49+
std::shared_ptr<MetricsReporter> reporter, RestScanContext rest_context,
50+
std::unique_ptr<Labels> labels) {
4851
if (metadata == nullptr) {
4952
return InvalidArgument("Metadata cannot be null");
5053
}
51-
return std::shared_ptr<RestTable>(
52-
new RestTable(std::move(identifier), std::move(metadata),
53-
std::move(metadata_location), std::move(io), std::move(catalog),
54-
std::move(full_name), std::move(reporter), std::move(rest_context)));
54+
return std::shared_ptr<RestTable>(new RestTable(
55+
std::move(identifier), std::move(metadata), std::move(metadata_location),
56+
std::move(io), std::move(catalog), std::move(full_name), std::move(reporter),
57+
std::move(rest_context), std::move(labels)));
5558
}
5659

5760
Result<std::unique_ptr<DataTableScanBuilder>> RestTable::NewScan() const {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ class ICEBERG_REST_EXPORT RestTable final : public Table {
4141
TableIdentifier identifier, std::shared_ptr<TableMetadata> metadata,
4242
std::string metadata_location, std::shared_ptr<FileIO> io,
4343
std::shared_ptr<Catalog> catalog, std::string full_name,
44-
std::shared_ptr<MetricsReporter> reporter, RestScanContext rest_context);
44+
std::shared_ptr<MetricsReporter> reporter, RestScanContext rest_context,
45+
std::unique_ptr<Labels> labels = nullptr);
4546

4647
~RestTable() override;
4748

@@ -57,7 +58,8 @@ class ICEBERG_REST_EXPORT RestTable final : public Table {
5758
RestTable(TableIdentifier identifier, std::shared_ptr<TableMetadata> metadata,
5859
std::string metadata_location, std::shared_ptr<FileIO> io,
5960
std::shared_ptr<Catalog> catalog, std::string full_name,
60-
std::shared_ptr<MetricsReporter> reporter, RestScanContext rest_context);
61+
std::shared_ptr<MetricsReporter> reporter, RestScanContext rest_context,
62+
std::unique_ptr<Labels> labels);
6163

6264
std::shared_ptr<const RestScanContext> rest_context_;
6365
};

‎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

0 commit comments

Comments
 (0)