Skip to content

Commit 87ed963

Browse files
fix(serde): make DataFile JSON compatible with Java Iceberg (#975)
1 parent ed6608d commit 87ed963

3 files changed

Lines changed: 470 additions & 52 deletions

File tree

‎src/iceberg/json_serde.cc‎

Lines changed: 225 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,13 @@
1818
*/
1919

2020
#include <algorithm>
21+
#include <concepts>
2122
#include <cstdint>
2223
#include <format>
24+
#include <limits>
2325
#include <map>
2426
#include <regex>
27+
#include <string>
2528
#include <unordered_set>
2629
#include <utility>
2730

@@ -269,7 +272,58 @@ constexpr std::string_view kContentSizeInBytes = "content-size-in-bytes";
269272
constexpr std::string_view kMapKeys = "keys";
270273
constexpr std::string_view kMapValues = "values";
271274

272-
template <typename Value>
275+
template <std::signed_integral Integer>
276+
Result<Integer> IntegerFromJson(const nlohmann::json& json,
277+
std::string_view description) {
278+
if (!json.is_number_integer()) {
279+
return JsonParseError("{} must be an integer, but is {}", description,
280+
json.type_name());
281+
}
282+
if (json.is_number_unsigned()) {
283+
const auto value = json.get<uint64_t>();
284+
if (value > static_cast<uint64_t>(std::numeric_limits<Integer>::max())) {
285+
return JsonParseError("{} integer is out of range: {}", description,
286+
SafeDumpJson(json));
287+
}
288+
return static_cast<Integer>(value);
289+
}
290+
291+
const auto value = json.get<int64_t>();
292+
if (value < static_cast<int64_t>(std::numeric_limits<Integer>::min()) ||
293+
value > static_cast<int64_t>(std::numeric_limits<Integer>::max())) {
294+
return JsonParseError("{} integer is out of range: {}", description,
295+
SafeDumpJson(json));
296+
}
297+
return static_cast<Integer>(value);
298+
}
299+
300+
template <std::signed_integral Integer>
301+
Result<Integer> GetJsonInteger(const nlohmann::json& json, std::string_view key) {
302+
ICEBERG_ASSIGN_OR_RAISE(auto value, GetJsonValue<nlohmann::json>(json, key));
303+
return IntegerFromJson<Integer>(value, std::format("'{}'", key));
304+
}
305+
306+
template <std::signed_integral Integer>
307+
Result<std::vector<Integer>> IntegerVectorFromJson(const nlohmann::json& json,
308+
std::string_view key) {
309+
ICEBERG_ASSIGN_OR_RAISE(auto values_json, GetJsonValue<nlohmann::json>(json, key));
310+
if (!values_json.is_array()) {
311+
return JsonParseError("'{}' must be an array, but is {}", key,
312+
values_json.type_name());
313+
}
314+
315+
std::vector<Integer> values;
316+
values.reserve(values_json.size());
317+
for (const auto& value_json : values_json) {
318+
ICEBERG_ASSIGN_OR_RAISE(
319+
auto value,
320+
IntegerFromJson<Integer>(value_json, std::format("'{}' element", key)));
321+
values.push_back(value);
322+
}
323+
return values;
324+
}
325+
326+
template <std::signed_integral Value>
273327
Result<std::map<int32_t, Value>> KeyValueMapFromJson(const nlohmann::json& json,
274328
std::string_view key) {
275329
std::map<int32_t, Value> result;
@@ -278,16 +332,27 @@ Result<std::map<int32_t, Value>> KeyValueMapFromJson(const nlohmann::json& json,
278332
}
279333

280334
ICEBERG_ASSIGN_OR_RAISE(auto map_json, GetJsonValue<nlohmann::json>(json, key));
281-
ICEBERG_ASSIGN_OR_RAISE(auto keys,
282-
GetJsonValue<std::vector<int32_t>>(map_json, kMapKeys));
283-
ICEBERG_ASSIGN_OR_RAISE(auto values,
284-
GetJsonValue<std::vector<Value>>(map_json, kMapValues));
285-
if (keys.size() != values.size()) {
335+
ICEBERG_ASSIGN_OR_RAISE(auto keys_json,
336+
GetJsonValue<nlohmann::json>(map_json, kMapKeys));
337+
ICEBERG_ASSIGN_OR_RAISE(auto values_json,
338+
GetJsonValue<nlohmann::json>(map_json, kMapValues));
339+
if (!keys_json.is_array() || !values_json.is_array()) {
340+
return JsonParseError("'{}' map keys and values must be arrays", key);
341+
}
342+
if (keys_json.size() != values_json.size()) {
286343
return JsonParseError("'{}' map keys and values have different lengths", key);
287344
}
288345

289-
for (size_t i = 0; i < keys.size(); ++i) {
290-
result[keys[i]] = std::move(values[i]);
346+
for (size_t i = 0; i < keys_json.size(); ++i) {
347+
ICEBERG_ASSIGN_OR_RAISE(
348+
auto field_id,
349+
IntegerFromJson<int32_t>(keys_json[i], std::format("'{}' map key", key)));
350+
ICEBERG_ASSIGN_OR_RAISE(
351+
auto value,
352+
IntegerFromJson<Value>(values_json[i], std::format("'{}' map value", key)));
353+
if (!result.emplace(field_id, value).second) {
354+
return JsonParseError("'{}' map contains duplicate key {}", key, field_id);
355+
}
291356
}
292357
return result;
293358
}
@@ -310,6 +375,118 @@ void SetKeyValueMap(nlohmann::json& json, std::string_view key,
310375
json[key] = {{kMapKeys, std::move(keys)}, {kMapValues, std::move(values)}};
311376
}
312377

378+
std::string BytesToHex(const std::vector<uint8_t>& bytes) {
379+
std::string hex;
380+
hex.reserve(bytes.size() * 2);
381+
for (uint8_t byte : bytes) {
382+
hex += std::format("{:02X}", byte);
383+
}
384+
return hex;
385+
}
386+
387+
Result<std::map<int32_t, std::vector<uint8_t>>> BytesMapFromJson(
388+
const nlohmann::json& json, std::string_view key) {
389+
std::map<int32_t, std::vector<uint8_t>> result;
390+
if (!json.contains(key) || json.at(key).is_null()) {
391+
return result;
392+
}
393+
394+
ICEBERG_ASSIGN_OR_RAISE(auto map_json, GetJsonValue<nlohmann::json>(json, key));
395+
ICEBERG_ASSIGN_OR_RAISE(auto keys_json,
396+
GetJsonValue<nlohmann::json>(map_json, kMapKeys));
397+
ICEBERG_ASSIGN_OR_RAISE(auto values_json,
398+
GetJsonValue<nlohmann::json>(map_json, kMapValues));
399+
if (!keys_json.is_array() || !values_json.is_array()) {
400+
return JsonParseError("'{}' map keys and values must be arrays", key);
401+
}
402+
if (keys_json.size() != values_json.size()) {
403+
return JsonParseError("'{}' map keys and values have different lengths", key);
404+
}
405+
406+
for (size_t i = 0; i < keys_json.size(); ++i) {
407+
ICEBERG_ASSIGN_OR_RAISE(
408+
auto field_id,
409+
IntegerFromJson<int32_t>(keys_json[i], std::format("'{}' map key", key)));
410+
if (!values_json[i].is_string()) {
411+
return JsonParseError("'{}' map value must be a string, but is {}", key,
412+
values_json[i].type_name());
413+
}
414+
ICEBERG_ASSIGN_OR_RAISE(
415+
auto bytes, StringUtils::HexStringToBytes(values_json[i].get<std::string>()));
416+
if (!result.emplace(field_id, std::move(bytes)).second) {
417+
return JsonParseError("'{}' map contains duplicate key {}", key, field_id);
418+
}
419+
}
420+
return result;
421+
}
422+
423+
void SetBytesMap(nlohmann::json& json, std::string_view key,
424+
const std::map<int32_t, std::vector<uint8_t>>& map) {
425+
if (map.empty()) {
426+
return;
427+
}
428+
429+
std::vector<int32_t> keys;
430+
std::vector<std::string> values;
431+
keys.reserve(map.size());
432+
values.reserve(map.size());
433+
for (const auto& [field_id, value] : map) {
434+
keys.push_back(field_id);
435+
values.push_back(BytesToHex(value));
436+
}
437+
json[key] = {{kMapKeys, std::move(keys)}, {kMapValues, std::move(values)}};
438+
}
439+
440+
Result<Literal> PartitionLiteralFromJson(const nlohmann::json* value,
441+
const SchemaField& field) {
442+
if (!field.type() || !field.type()->is_primitive()) {
443+
return InvalidSchema("Partition field {} must have a primitive type",
444+
field.field_id());
445+
}
446+
447+
auto primitive_type = internal::checked_pointer_cast<PrimitiveType>(field.type());
448+
if (value == nullptr || value->is_null()) {
449+
return Literal::Null(std::move(primitive_type));
450+
}
451+
return LiteralFromJson(*value, primitive_type.get());
452+
}
453+
454+
Result<PartitionValues> PartitionValuesFromJson(const nlohmann::json& json,
455+
const StructType& partition_type) {
456+
const auto& fields = partition_type.fields();
457+
std::vector<Literal> values;
458+
values.reserve(fields.size());
459+
460+
if (json.is_array()) {
461+
if (json.size() != fields.size()) {
462+
return JsonParseError("Invalid partition data size: expected = {}, actual = {}",
463+
fields.size(), json.size());
464+
}
465+
for (size_t pos = 0; pos < fields.size(); ++pos) {
466+
ICEBERG_ASSIGN_OR_RAISE(auto value,
467+
PartitionLiteralFromJson(&json[pos], fields[pos]));
468+
values.push_back(std::move(value));
469+
}
470+
} else if (json.is_object()) {
471+
if (json.size() > fields.size()) {
472+
return JsonParseError("Invalid partition data size: expected <= {}, actual = {}",
473+
fields.size(), json.size());
474+
}
475+
for (const auto& field : fields) {
476+
const auto it = json.find(std::to_string(field.field_id()));
477+
const nlohmann::json* value = it == json.end() ? nullptr : &it.value();
478+
ICEBERG_ASSIGN_OR_RAISE(auto literal, PartitionLiteralFromJson(value, field));
479+
values.push_back(std::move(literal));
480+
}
481+
} else {
482+
return JsonParseError(
483+
"Invalid partition data for content file: expected array or object ({})",
484+
SafeDumpJson(json));
485+
}
486+
487+
return PartitionValues(std::move(values));
488+
}
489+
313490
Result<nlohmann::json> DataFileToJsonUnchecked(const DataFile& data_file) {
314491
nlohmann::json json;
315492
switch (data_file.content) {
@@ -327,7 +504,7 @@ Result<nlohmann::json> DataFileToJsonUnchecked(const DataFile& data_file) {
327504
json[kFileFormat] = ToString(data_file.file_format);
328505

329506
if (!data_file.partition_spec_id.has_value()) {
330-
return ValidationFailed("Cannot serialize REST content file without 'spec-id'");
507+
return ValidationFailed("Cannot serialize content file without 'spec-id'");
331508
}
332509
json[kSpecId] = data_file.partition_spec_id.value();
333510

@@ -345,11 +522,11 @@ Result<nlohmann::json> DataFileToJsonUnchecked(const DataFile& data_file) {
345522
SetKeyValueMap(json, kValueCounts, data_file.value_counts);
346523
SetKeyValueMap(json, kNullValueCounts, data_file.null_value_counts);
347524
SetKeyValueMap(json, kNanValueCounts, data_file.nan_value_counts);
348-
SetKeyValueMap(json, kLowerBounds, data_file.lower_bounds);
349-
SetKeyValueMap(json, kUpperBounds, data_file.upper_bounds);
525+
SetBytesMap(json, kLowerBounds, data_file.lower_bounds);
526+
SetBytesMap(json, kUpperBounds, data_file.upper_bounds);
350527

351528
if (!data_file.key_metadata.empty()) {
352-
json[kKeyMetadata] = data_file.key_metadata;
529+
json[kKeyMetadata] = BytesToHex(data_file.key_metadata);
353530
}
354531
if (!data_file.split_offsets.empty()) {
355532
json[kSplitOffsets] = data_file.split_offsets;
@@ -2134,11 +2311,13 @@ Result<DataFile> DataFileFromJson(
21342311
DataFile data_file;
21352312

21362313
ICEBERG_ASSIGN_OR_RAISE(auto content_str, GetJsonValue<std::string>(json, kContent));
2137-
if (content_str == kContentData) {
2314+
if (content_str == kContentData || content_str == "DATA") {
21382315
data_file.content = DataFile::Content::kData;
2139-
} else if (content_str == kContentPositionDeletes) {
2316+
} else if (content_str == kContentPositionDeletes ||
2317+
content_str == "POSITION_DELETES") {
21402318
data_file.content = DataFile::Content::kPositionDeletes;
2141-
} else if (content_str == kContentEqualityDeletes) {
2319+
} else if (content_str == kContentEqualityDeletes ||
2320+
content_str == "EQUALITY_DELETES") {
21422321
data_file.content = DataFile::Content::kEqualityDeletes;
21432322
} else {
21442323
return JsonParseError("Unknown data file content: {}", content_str);
@@ -2149,37 +2328,25 @@ Result<DataFile> DataFileFromJson(
21492328
ICEBERG_ASSIGN_OR_RAISE(auto format_str, GetJsonValue<std::string>(json, kFileFormat));
21502329
ICEBERG_ASSIGN_OR_RAISE(data_file.file_format, FileFormatTypeFromString(format_str));
21512330

2152-
ICEBERG_ASSIGN_OR_RAISE(auto spec_id, GetJsonValue<int32_t>(json, kSpecId));
2331+
ICEBERG_ASSIGN_OR_RAISE(auto spec_id, GetJsonInteger<int32_t>(json, kSpecId));
21532332
data_file.partition_spec_id = spec_id;
21542333

2155-
ICEBERG_ASSIGN_OR_RAISE(auto partition_vals,
2156-
GetJsonValue<nlohmann::json>(json, kPartition));
2157-
if (!partition_vals.is_array()) {
2158-
return JsonParseError("PartitionValues must be a JSON array: {}",
2159-
SafeDumpJson(partition_vals));
2160-
}
2161-
std::vector<Literal> literals;
21622334
auto it = partition_spec_by_id.find(spec_id);
2163-
if (it == partition_spec_by_id.end()) {
2335+
if (it == partition_spec_by_id.end() || !it->second) {
21642336
return JsonParseError("Invalid partition spec id: {}", spec_id);
21652337
}
2166-
ICEBERG_ASSIGN_OR_RAISE(auto struct_type, it->second->PartitionType(schema));
2167-
auto fields = struct_type->fields();
2168-
if (partition_vals.size() != fields.size()) {
2169-
return JsonParseError("Invalid partition data size: expected = {}, actual = {}",
2170-
fields.size(), partition_vals.size());
2171-
}
2172-
for (size_t pos = 0; pos < fields.size(); ++pos) {
2173-
ICEBERG_ASSIGN_OR_RAISE(
2174-
auto literal, LiteralFromJson(partition_vals[pos], fields[pos].type().get()));
2175-
literals.push_back(std::move(literal));
2338+
if (json.contains(kPartition)) {
2339+
ICEBERG_ASSIGN_OR_RAISE(auto partition_vals,
2340+
GetJsonValue<nlohmann::json>(json, kPartition));
2341+
ICEBERG_ASSIGN_OR_RAISE(auto struct_type, it->second->PartitionType(schema));
2342+
ICEBERG_ASSIGN_OR_RAISE(data_file.partition,
2343+
PartitionValuesFromJson(partition_vals, *struct_type));
21762344
}
2177-
data_file.partition = PartitionValues(std::move(literals));
21782345

21792346
ICEBERG_ASSIGN_OR_RAISE(data_file.record_count,
2180-
GetJsonValue<int64_t>(json, kRecordCount));
2347+
GetJsonInteger<int64_t>(json, kRecordCount));
21812348
ICEBERG_ASSIGN_OR_RAISE(data_file.file_size_in_bytes,
2182-
GetJsonValue<int64_t>(json, kFileSizeInBytes));
2349+
GetJsonInteger<int64_t>(json, kFileSizeInBytes));
21832350

21842351
ICEBERG_ASSIGN_OR_RAISE(data_file.column_sizes,
21852352
KeyValueMapFromJson<int64_t>(json, kColumnSizes));
@@ -2189,42 +2356,42 @@ Result<DataFile> DataFileFromJson(
21892356
KeyValueMapFromJson<int64_t>(json, kNullValueCounts));
21902357
ICEBERG_ASSIGN_OR_RAISE(data_file.nan_value_counts,
21912358
KeyValueMapFromJson<int64_t>(json, kNanValueCounts));
2192-
ICEBERG_ASSIGN_OR_RAISE(data_file.lower_bounds,
2193-
KeyValueMapFromJson<std::vector<uint8_t>>(json, kLowerBounds));
2194-
ICEBERG_ASSIGN_OR_RAISE(data_file.upper_bounds,
2195-
KeyValueMapFromJson<std::vector<uint8_t>>(json, kUpperBounds));
2359+
ICEBERG_ASSIGN_OR_RAISE(data_file.lower_bounds, BytesMapFromJson(json, kLowerBounds));
2360+
ICEBERG_ASSIGN_OR_RAISE(data_file.upper_bounds, BytesMapFromJson(json, kUpperBounds));
21962361

21972362
if (json.contains(kKeyMetadata) && !json.at(kKeyMetadata).is_null()) {
2363+
ICEBERG_ASSIGN_OR_RAISE(auto key_metadata,
2364+
GetJsonValue<std::string>(json, kKeyMetadata));
21982365
ICEBERG_ASSIGN_OR_RAISE(data_file.key_metadata,
2199-
GetJsonValue<std::vector<uint8_t>>(json, kKeyMetadata));
2366+
StringUtils::HexStringToBytes(key_metadata));
22002367
}
22012368
if (json.contains(kSplitOffsets) && !json.at(kSplitOffsets).is_null()) {
22022369
ICEBERG_ASSIGN_OR_RAISE(data_file.split_offsets,
2203-
GetJsonValue<std::vector<int64_t>>(json, kSplitOffsets));
2370+
IntegerVectorFromJson<int64_t>(json, kSplitOffsets));
22042371
}
22052372
if (json.contains(kEqualityIds) && !json.at(kEqualityIds).is_null()) {
22062373
ICEBERG_ASSIGN_OR_RAISE(data_file.equality_ids,
2207-
GetJsonValue<std::vector<int32_t>>(json, kEqualityIds));
2374+
IntegerVectorFromJson<int32_t>(json, kEqualityIds));
22082375
}
22092376
if (json.contains(kSortOrderId) && !json.at(kSortOrderId).is_null()) {
22102377
ICEBERG_ASSIGN_OR_RAISE(data_file.sort_order_id,
2211-
GetJsonValue<int32_t>(json, kSortOrderId));
2378+
GetJsonInteger<int32_t>(json, kSortOrderId));
22122379
}
22132380
if (json.contains(kFirstRowId) && !json.at(kFirstRowId).is_null()) {
22142381
ICEBERG_ASSIGN_OR_RAISE(data_file.first_row_id,
2215-
GetJsonValue<int64_t>(json, kFirstRowId));
2382+
GetJsonInteger<int64_t>(json, kFirstRowId));
22162383
}
22172384
if (json.contains(kReferencedDataFile) && !json.at(kReferencedDataFile).is_null()) {
22182385
ICEBERG_ASSIGN_OR_RAISE(data_file.referenced_data_file,
22192386
GetJsonValue<std::string>(json, kReferencedDataFile));
22202387
}
22212388
if (json.contains(kContentOffset) && !json.at(kContentOffset).is_null()) {
22222389
ICEBERG_ASSIGN_OR_RAISE(data_file.content_offset,
2223-
GetJsonValue<int64_t>(json, kContentOffset));
2390+
GetJsonInteger<int64_t>(json, kContentOffset));
22242391
}
22252392
if (json.contains(kContentSizeInBytes) && !json.at(kContentSizeInBytes).is_null()) {
22262393
ICEBERG_ASSIGN_OR_RAISE(data_file.content_size_in_bytes,
2227-
GetJsonValue<int64_t>(json, kContentSizeInBytes));
2394+
GetJsonInteger<int64_t>(json, kContentSizeInBytes));
22282395
}
22292396

22302397
return data_file;
@@ -2254,6 +2421,16 @@ Result<nlohmann::json> ToJson(
22542421
partition_type->fields().empty() ? "unpartitioned" : "partitioned",
22552422
data_file.partition.num_fields() == 0 ? "unpartitioned" : "partitioned");
22562423
}
2424+
for (size_t pos = 0; pos < partition_type->fields().size(); ++pos) {
2425+
const auto& literal = data_file.partition.values()[pos];
2426+
const auto& expected_type = partition_type->fields()[pos].type();
2427+
if (!literal.IsNull() && (!literal.type() || *literal.type() != *expected_type)) {
2428+
return ValidationFailed(
2429+
"Invalid partition value type at position {}: expected = {}, actual = {}", pos,
2430+
expected_type->ToString(),
2431+
literal.type() ? literal.type()->ToString() : "unknown");
2432+
}
2433+
}
22572434
return DataFileToJsonUnchecked(data_file);
22582435
}
22592436

0 commit comments

Comments
 (0)