Skip to content

Commit fc9bc0e

Browse files
committed
fix: align rest table update serde with java/spec
1 parent 701d39c commit fc9bc0e

15 files changed

Lines changed: 500 additions & 17 deletions

src/iceberg/catalog/rest/json_serde.cc

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -903,13 +903,21 @@ Result<CommitTableRequest> CommitTableRequestFromJson(const nlohmann::json& json
903903

904904
ICEBERG_ASSIGN_OR_RAISE(auto requirements_json,
905905
GetJsonValue<nlohmann::json>(json, kRequirements));
906+
if (!requirements_json.is_array()) {
907+
return JsonParseError("Expected '{}' to be an array, got {}", kRequirements,
908+
SafeDumpJson(requirements_json));
909+
}
906910
for (const auto& req_json : requirements_json) {
907911
ICEBERG_ASSIGN_OR_RAISE(auto requirement, TableRequirementFromJson(req_json));
908912
request.requirements.push_back(std::move(requirement));
909913
}
910914

911915
ICEBERG_ASSIGN_OR_RAISE(auto updates_json,
912916
GetJsonValue<nlohmann::json>(json, kUpdates));
917+
if (!updates_json.is_array()) {
918+
return JsonParseError("Expected '{}' to be an array, got {}", kUpdates,
919+
SafeDumpJson(updates_json));
920+
}
913921
for (const auto& update_json : updates_json) {
914922
ICEBERG_ASSIGN_OR_RAISE(auto update, TableUpdateFromJson(update_json));
915923
request.updates.push_back(std::move(update));
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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/encryption/encrypted_key.h
23+
/// Encrypted table key metadata.
24+
25+
#include <optional>
26+
#include <string>
27+
#include <unordered_map>
28+
29+
#include "iceberg/iceberg_export.h"
30+
31+
namespace iceberg {
32+
33+
/// \brief Represents an encrypted table key entry.
34+
struct ICEBERG_EXPORT EncryptedKey {
35+
std::string key_id;
36+
std::string encrypted_key_metadata;
37+
std::optional<std::string> encrypted_by_id = std::nullopt;
38+
std::unordered_map<std::string, std::string> properties;
39+
40+
friend bool operator==(const EncryptedKey&, const EncryptedKey&) = default;
41+
};
42+
43+
} // namespace iceberg

src/iceberg/encryption/meson.build

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
install_headers(['encrypted_key.h'], subdir: 'iceberg/encryption')

src/iceberg/json_serde.cc

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
#include "iceberg/table_update.h"
4444
#include "iceberg/transform.h"
4545
#include "iceberg/type.h"
46+
#include "iceberg/util/base64.h"
4647
#include "iceberg/util/checked_cast.h"
4748
#include "iceberg/util/formatter.h" // IWYU pragma: keep
4849
#include "iceberg/util/json_util_internal.h"
@@ -90,6 +91,8 @@ constexpr std::string_view kValueId = "value-id";
9091
constexpr std::string_view kRequired = "required";
9192
constexpr std::string_view kElementRequired = "element-required";
9293
constexpr std::string_view kValueRequired = "value-required";
94+
constexpr std::string_view kEncryptedKeyMetadata = "encrypted-key-metadata";
95+
constexpr std::string_view kEncryptedById = "encrypted-by-id";
9396

9497
// Snapshot constants
9598
constexpr std::string_view kSpecId = "spec-id";
@@ -166,6 +169,7 @@ constexpr std::string_view kRefs = "refs";
166169
constexpr std::string_view kStatistics = "statistics";
167170
constexpr std::string_view kPartitionStatistics = "partition-statistics";
168171
constexpr std::string_view kNextRowId = "next-row-id";
172+
constexpr std::string_view kEncryptionKeys = "encryption-keys";
169173
constexpr std::string_view kMetadataFile = "metadata-file";
170174
constexpr std::string_view kStatisticsPath = "statistics-path";
171175
constexpr std::string_view kFileSizeInBytes = "file-size-in-bytes";
@@ -196,6 +200,8 @@ constexpr std::string_view kActionRemoveStatistics = "remove-statistics";
196200
constexpr std::string_view kActionSetPartitionStatistics = "set-partition-statistics";
197201
constexpr std::string_view kActionRemovePartitionStatistics =
198202
"remove-partition-statistics";
203+
constexpr std::string_view kActionAddEncryptionKey = "add-encryption-key";
204+
constexpr std::string_view kActionRemoveEncryptionKey = "remove-encryption-key";
199205

200206
// TableUpdate field constants
201207
constexpr std::string_view kUUID = "uuid";
@@ -210,6 +216,9 @@ constexpr std::string_view kRefName = "ref-name";
210216
constexpr std::string_view kRef = "ref";
211217
constexpr std::string_view kUpdates = "updates";
212218
constexpr std::string_view kRemovals = "removals";
219+
constexpr std::string_view kUpdated = "updated";
220+
constexpr std::string_view kRemoved = "removed";
221+
constexpr std::string_view kEncryptionKey = "encryption-key";
213222

214223
// TableRequirement type constants
215224
constexpr std::string_view kRequirementAssertDoesNotExist = "assert-create";
@@ -851,6 +860,41 @@ Result<MetadataLogEntry> MetadataLogEntryFromJson(const nlohmann::json& json) {
851860
return metadata_log_entry;
852861
}
853862

863+
nlohmann::json ToJson(const EncryptedKey& encrypted_key) {
864+
nlohmann::json json;
865+
json[kKeyId] = encrypted_key.key_id;
866+
json[kEncryptedKeyMetadata] = Base64::Encode(encrypted_key.encrypted_key_metadata);
867+
SetOptionalField(json, kEncryptedById, encrypted_key.encrypted_by_id);
868+
if (!encrypted_key.properties.empty()) {
869+
json[kProperties] = encrypted_key.properties;
870+
}
871+
return json;
872+
}
873+
874+
Result<EncryptedKey> EncryptedKeyFromJson(const nlohmann::json& json) {
875+
using StringMap = std::unordered_map<std::string, std::string>;
876+
877+
if (!json.is_object()) {
878+
return JsonParseError("Invalid encryption key, must be non-null object: {}",
879+
SafeDumpJson(json));
880+
}
881+
882+
ICEBERG_ASSIGN_OR_RAISE(auto key_id, GetJsonValue<std::string>(json, kKeyId));
883+
ICEBERG_ASSIGN_OR_RAISE(auto encoded_metadata,
884+
GetJsonValue<std::string>(json, kEncryptedKeyMetadata));
885+
ICEBERG_ASSIGN_OR_RAISE(auto encrypted_key_metadata, Base64::Decode(encoded_metadata));
886+
ICEBERG_ASSIGN_OR_RAISE(auto encrypted_by_id,
887+
GetJsonValueOptional<std::string>(json, kEncryptedById));
888+
ICEBERG_ASSIGN_OR_RAISE(auto properties,
889+
GetJsonValueOrDefault<StringMap>(json, kProperties));
890+
return EncryptedKey{
891+
.key_id = std::move(key_id),
892+
.encrypted_key_metadata = std::move(encrypted_key_metadata),
893+
.encrypted_by_id = std::move(encrypted_by_id),
894+
.properties = std::move(properties),
895+
};
896+
}
897+
854898
nlohmann::json ToJson(const TableMetadata& table_metadata) {
855899
nlohmann::json json;
856900

@@ -917,6 +961,9 @@ nlohmann::json ToJson(const TableMetadata& table_metadata) {
917961
json[kSnapshots] = ToJsonList(table_metadata.snapshots);
918962
json[kStatistics] = ToJsonList(table_metadata.statistics);
919963
json[kPartitionStatistics] = ToJsonList(table_metadata.partition_statistics);
964+
if (!table_metadata.encryption_keys.empty()) {
965+
json[kEncryptionKeys] = ToJsonList(table_metadata.encryption_keys);
966+
}
920967
json[kSnapshotLog] = ToJsonList(table_metadata.snapshot_log);
921968
json[kMetadataLog] = ToJsonList(table_metadata.metadata_log);
922969

@@ -1182,6 +1229,9 @@ Result<std::unique_ptr<TableMetadata>> TableMetadataFromJson(const nlohmann::jso
11821229
table_metadata->partition_statistics,
11831230
FromJsonList<PartitionStatisticsFile>(json, kPartitionStatistics,
11841231
PartitionStatisticsFileFromJson));
1232+
ICEBERG_ASSIGN_OR_RAISE(
1233+
table_metadata->encryption_keys,
1234+
FromJsonList<EncryptedKey>(json, kEncryptionKeys, EncryptedKeyFromJson));
11851235
ICEBERG_ASSIGN_OR_RAISE(
11861236
table_metadata->snapshot_log,
11871237
FromJsonList<SnapshotLogEntry>(json, kSnapshotLog, SnapshotLogEntryFromJson));
@@ -1486,6 +1536,18 @@ nlohmann::json ToJson(const TableUpdate& update) {
14861536
json[kSnapshotId] = u.snapshot_id();
14871537
break;
14881538
}
1539+
case TableUpdate::Kind::kAddEncryptionKey: {
1540+
const auto& u = internal::checked_cast<const table::AddEncryptionKey&>(update);
1541+
json[kAction] = kActionAddEncryptionKey;
1542+
json[kEncryptionKey] = ToJson(u.key());
1543+
break;
1544+
}
1545+
case TableUpdate::Kind::kRemoveEncryptionKey: {
1546+
const auto& u = internal::checked_cast<const table::RemoveEncryptionKey&>(update);
1547+
json[kAction] = kActionRemoveEncryptionKey;
1548+
json[kKeyId] = u.key_id();
1549+
break;
1550+
}
14891551
}
14901552
return json;
14911553
}
@@ -1506,7 +1568,6 @@ nlohmann::json ToJson(const TableRequirement& requirement) {
15061568
const auto& r =
15071569
internal::checked_cast<const table::AssertRefSnapshotID&>(requirement);
15081570
json[kType] = kRequirementAssertRefSnapshotID;
1509-
// REST spec names this field "ref", not "ref-name".
15101571
json[kRef] = r.ref_name();
15111572
if (r.snapshot_id().has_value()) {
15121573
json[kSnapshotId] = r.snapshot_id().value();
@@ -1570,8 +1631,10 @@ Result<std::unique_ptr<TableUpdate>> TableUpdateFromJson(const nlohmann::json& j
15701631
ICEBERG_ASSIGN_OR_RAISE(auto schema_json,
15711632
GetJsonValue<nlohmann::json>(json, kSchema));
15721633
ICEBERG_ASSIGN_OR_RAISE(auto parsed_schema, SchemaFromJson(schema_json));
1573-
ICEBERG_ASSIGN_OR_RAISE(auto last_column_id,
1574-
GetJsonValue<int32_t>(json, kLastColumnId));
1634+
ICEBERG_ASSIGN_OR_RAISE(auto highest_field_id, parsed_schema->HighestFieldId());
1635+
ICEBERG_ASSIGN_OR_RAISE(
1636+
auto last_column_id,
1637+
GetJsonValueOrDefault<int32_t>(json, kLastColumnId, highest_field_id));
15751638
return std::make_unique<table::AddSchema>(std::move(parsed_schema), last_column_id);
15761639
}
15771640
if (action == kActionSetCurrentSchema) {
@@ -1650,12 +1713,17 @@ Result<std::unique_ptr<TableUpdate>> TableUpdateFromJson(const nlohmann::json& j
16501713
}
16511714
if (action == kActionSetProperties) {
16521715
using StringMap = std::unordered_map<std::string, std::string>;
1653-
ICEBERG_ASSIGN_OR_RAISE(auto updates, GetJsonValue<StringMap>(json, kUpdates));
1716+
ICEBERG_ASSIGN_OR_RAISE(auto updates,
1717+
json.contains(kUpdates) || !json.contains(kUpdated)
1718+
? GetJsonValue<StringMap>(json, kUpdates)
1719+
: GetJsonValue<StringMap>(json, kUpdated));
16541720
return std::make_unique<table::SetProperties>(std::move(updates));
16551721
}
16561722
if (action == kActionRemoveProperties) {
16571723
ICEBERG_ASSIGN_OR_RAISE(auto removals_vec,
1658-
GetJsonValue<std::vector<std::string>>(json, kRemovals));
1724+
json.contains(kRemovals) || !json.contains(kRemoved)
1725+
? GetJsonValue<std::vector<std::string>>(json, kRemovals)
1726+
: GetJsonValue<std::vector<std::string>>(json, kRemoved));
16591727
std::unordered_set<std::string> removals(
16601728
std::make_move_iterator(removals_vec.begin()),
16611729
std::make_move_iterator(removals_vec.end()));
@@ -1688,6 +1756,17 @@ Result<std::unique_ptr<TableUpdate>> TableUpdateFromJson(const nlohmann::json& j
16881756
ICEBERG_ASSIGN_OR_RAISE(auto snapshot_id, GetJsonValue<int64_t>(json, kSnapshotId));
16891757
return std::make_unique<table::RemovePartitionStatistics>(snapshot_id);
16901758
}
1759+
if (action == kActionAddEncryptionKey) {
1760+
if (!json.contains(kEncryptionKey)) {
1761+
return JsonParseError("Invalid encryption key, must be non-null object: null");
1762+
}
1763+
ICEBERG_ASSIGN_OR_RAISE(auto key, EncryptedKeyFromJson(json.at(kEncryptionKey)));
1764+
return std::make_unique<table::AddEncryptionKey>(std::move(key));
1765+
}
1766+
if (action == kActionRemoveEncryptionKey) {
1767+
ICEBERG_ASSIGN_OR_RAISE(auto key_id, GetJsonValue<std::string>(json, kKeyId));
1768+
return std::make_unique<table::RemoveEncryptionKey>(std::move(key_id));
1769+
}
16911770

16921771
return JsonParseError("Unknown table update action: {}", action);
16931772
}

src/iceberg/json_serde_internal.h

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
#include <nlohmann/json_fwd.hpp>
3030

31+
#include "iceberg/encryption/encrypted_key.h"
3132
#include "iceberg/result.h"
3233
#include "iceberg/statistics_file.h"
3334
#include "iceberg/table_metadata.h"
@@ -283,6 +284,18 @@ ICEBERG_EXPORT nlohmann::json ToJson(const MetadataLogEntry& metadata_log_entry)
283284
ICEBERG_EXPORT Result<MetadataLogEntry> MetadataLogEntryFromJson(
284285
const nlohmann::json& json);
285286

287+
/// \brief Serializes an `EncryptedKey` object to JSON.
288+
///
289+
/// \param encrypted_key The `EncryptedKey` object to be serialized.
290+
/// \return A JSON object representing the `EncryptedKey`.
291+
ICEBERG_EXPORT nlohmann::json ToJson(const EncryptedKey& encrypted_key);
292+
293+
/// \brief Deserializes a JSON object into an `EncryptedKey` object.
294+
///
295+
/// \param json The JSON object representing an `EncryptedKey`.
296+
/// \return An `EncryptedKey` object or an error if the conversion fails.
297+
ICEBERG_EXPORT Result<EncryptedKey> EncryptedKeyFromJson(const nlohmann::json& json);
298+
286299
/// \brief Serializes a `TableMetadata` object to JSON.
287300
///
288301
/// \param table_metadata The `TableMetadata` object to be serialized.

src/iceberg/meson.build

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ install_headers(
287287
subdir('catalog')
288288
subdir('data')
289289
subdir('deletes')
290+
subdir('encryption')
290291
subdir('expression')
291292
subdir('manifest')
292293
subdir('metrics')

0 commit comments

Comments
 (0)