Skip to content

Commit d287f03

Browse files
test: expand map blob read coverage
1 parent a1b3061 commit d287f03

31 files changed

Lines changed: 400 additions & 0 deletions

src/paimon/format/blob/blob_file_batch_reader_test.cpp

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
#include "paimon/format/blob/blob_file_batch_reader.h"
2020

21+
#include <algorithm>
2122
#include <string_view>
2223

2324
#include "arrow/api.h"
@@ -156,6 +157,28 @@ class BlobFileBatchReaderTest : public testing::Test, public ::testing::WithPara
156157
return std::string(value->data(), value->size());
157158
}
158159

160+
void CheckMapBlobReadFails(const std::string& file_bytes,
161+
const std::shared_ptr<arrow::DataType>& key_type,
162+
const std::string& expected_message) {
163+
auto dir = paimon::test::UniqueTestDirectory::Create();
164+
ASSERT_TRUE(dir);
165+
const std::string file_path = dir->Str() + "/corrupt-map.blob";
166+
std::shared_ptr<FileSystem> file_system = std::make_shared<LocalFileSystem>();
167+
ASSERT_OK(file_system->WriteFile(file_path, file_bytes, /*overwrite=*/true));
168+
169+
auto map_type = arrow::map(key_type, BlobUtils::ToArrowField("value", /*nullable=*/true));
170+
auto schema = arrow::schema({arrow::field("blob_map", map_type)});
171+
::ArrowSchema c_schema;
172+
ASSERT_TRUE(arrow::ExportSchema(*schema, &c_schema).ok());
173+
ASSERT_OK_AND_ASSIGN(std::shared_ptr<InputStream> input, file_system->Open(file_path));
174+
ASSERT_OK_AND_ASSIGN(std::unique_ptr<BlobFileBatchReader> reader,
175+
BlobFileBatchReader::Create(
176+
input, /*batch_size=*/1, /*blob_as_descriptor=*/false,
177+
/*emit_placeholder_sentinel=*/false, pool_, GetArrowPool(pool_)));
178+
ASSERT_OK(reader->SetReadSchema(&c_schema, nullptr, std::nullopt));
179+
ASSERT_NOK_WITH_MSG(reader->NextBatch(), expected_message);
180+
}
181+
159182
private:
160183
std::string blob_field_name_;
161184
std::shared_ptr<MemoryPool> pool_;
@@ -413,6 +436,51 @@ TEST_F(BlobFileBatchReaderTest, RejectsNullMapKey) {
413436
ASSERT_NOK_WITH_MSG(reader->NextBatch(), "MAP<..., BLOB> keys cannot be null");
414437
}
415438

439+
TEST_F(BlobFileBatchReaderTest, RejectsCorruptMapPayloadMetadata) {
440+
// The Java golden's first bin starts with the outer BLOB magic (4 bytes), followed by a
441+
// 45-byte MAP payload: header [4, 13), key/value data [13, 35), indexes [35, 41), and the
442+
// two index lengths [41, 49). Mutating the payload does not require updating the outer CRC,
443+
// which BlobFileBatchReader intentionally does not validate.
444+
const std::string golden = MapBlobGoldenBytes();
445+
446+
std::string corrupted = golden;
447+
corrupted[4] = 0;
448+
CheckMapBlobReadFails(corrupted, arrow::utf8(), "invalid MAP<..., BLOB> payload magic number");
449+
450+
corrupted = golden;
451+
corrupted[8] = 2;
452+
CheckMapBlobReadFails(corrupted, arrow::utf8(), "unsupported MAP<..., BLOB> payload version");
453+
454+
corrupted = golden;
455+
std::fill(corrupted.begin() + 9, corrupted.begin() + 13, static_cast<char>(0xFF));
456+
CheckMapBlobReadFails(corrupted, arrow::utf8(), "invalid MAP<..., BLOB> entry count");
457+
458+
corrupted = golden;
459+
corrupted[41] = static_cast<char>(0xFF);
460+
CheckMapBlobReadFails(corrupted, arrow::utf8(), "invalid MAP<..., BLOB> key index length");
461+
462+
corrupted = golden;
463+
corrupted[9] = 2;
464+
CheckMapBlobReadFails(corrupted, arrow::utf8(), "entry count does not match key index length");
465+
466+
CheckMapBlobReadFails(golden, arrow::int32(), "invalid MAP<..., BLOB> fixed-width key length");
467+
468+
corrupted = golden;
469+
corrupted[38] = 0x0C;
470+
corrupted[39] = 0x0B;
471+
CheckMapBlobReadFails(corrupted, arrow::utf8(), "value lengths exceed the payload data length");
472+
473+
corrupted = golden;
474+
corrupted[38] = 0x08;
475+
corrupted[39] = 0x07;
476+
CheckMapBlobReadFails(corrupted, arrow::utf8(),
477+
"key/value lengths do not match the payload data length");
478+
479+
corrupted = golden;
480+
std::copy_n(corrupted.begin() + 13, 5, corrupted.begin() + 18);
481+
CheckMapBlobReadFails(corrupted, arrow::utf8(), "payload: duplicate key");
482+
}
483+
416484
TEST_P(BlobFileBatchReaderTest, TestPushdownBitmap) {
417485
std::string test_data_path = paimon::test::GetDataDir() + "/db_with_blob.db/table_with_blob/";
418486
auto dir = paimon::test::UniqueTestDirectory::Create();

test/inte/blob_table_inte_test.cpp

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,6 +535,50 @@ class BlobTableInteTest : public testing::Test, public ::testing::WithParamInter
535535
});
536536
}
537537

538+
Result<std::shared_ptr<arrow::MapArray>> NormalizeMapBlobValues(
539+
const std::shared_ptr<arrow::MapArray>& map_array, bool blob_as_descriptor) const {
540+
const auto& values = checked_cast<const arrow::LargeBinaryArray&>(*map_array->items());
541+
auto fs = std::make_shared<LocalFileSystem>();
542+
arrow::LargeBinaryBuilder builder;
543+
for (int64_t i = 0; i < values.length(); ++i) {
544+
if (values.IsNull(i)) {
545+
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.AppendNull());
546+
continue;
547+
}
548+
std::string_view stored = values.GetView(i);
549+
if (!blob_as_descriptor) {
550+
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Append(stored));
551+
continue;
552+
}
553+
PAIMON_ASSIGN_OR_RAISE(
554+
std::unique_ptr<Blob> blob,
555+
Blob::FromDescriptor(stored.data(), static_cast<int64_t>(stored.size())));
556+
PAIMON_ASSIGN_OR_RAISE(PAIMON_UNIQUE_PTR<Bytes> data, blob->ToData(fs, pool_));
557+
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Append(data->data(), data->size()));
558+
}
559+
std::shared_ptr<arrow::Array> normalized_values;
560+
PAIMON_RETURN_NOT_OK_FROM_ARROW(builder.Finish(&normalized_values));
561+
return std::make_shared<arrow::MapArray>(map_array->type(), map_array->length(),
562+
map_array->value_offsets(), map_array->keys(),
563+
normalized_values, map_array->null_bitmap(),
564+
map_array->null_count(), map_array->offset());
565+
}
566+
567+
void CheckMapBlobColumn(const std::shared_ptr<arrow::StructArray>& rows,
568+
const std::string& field_name, const std::string& expected_json,
569+
bool blob_as_descriptor) const {
570+
auto map_array =
571+
std::dynamic_pointer_cast<arrow::MapArray>(rows->GetFieldByName(field_name));
572+
ASSERT_TRUE(map_array) << field_name;
573+
ASSERT_OK_AND_ASSIGN(auto normalized,
574+
NormalizeMapBlobValues(map_array, blob_as_descriptor));
575+
auto expected = arrow::ipc::internal::json::ArrayFromJSON(map_array->type(), expected_json)
576+
.ValueOrDie();
577+
ASSERT_TRUE(expected->Equals(normalized))
578+
<< field_name << " expected: " << expected->ToString()
579+
<< " actual: " << normalized->ToString();
580+
}
581+
538582
/// Verify DataFileMeta properties from a scan plan.
539583
/// Each vector element corresponds to one expected DataFileMeta (ordered by file index).
540584
static void VerifyDataFileMetas(
@@ -4334,4 +4378,111 @@ TEST_P(BlobTableInteTest, TestReadBlobDescriptorFieldFromJava) {
43344378
ASSERT_TRUE(resolved->Equals(expected_with_rk));
43354379
}
43364380

4381+
TEST_P(BlobTableInteTest, TestReadMapBlobTableFromJava) {
4382+
if (GetParam() != "parquet") {
4383+
GTEST_SKIP() << "the Java fixture uses Parquet";
4384+
}
4385+
const std::string table_path = GetDataDir() + "/parquet/map_blob_java.db/map_blob_java";
4386+
const std::vector<std::string> read_fields = {"id",
4387+
"string_payloads",
4388+
"boolean_payloads",
4389+
"tinyint_payloads",
4390+
"smallint_payloads",
4391+
"int_payloads",
4392+
"bigint_payloads",
4393+
"date_payloads",
4394+
"binary_payloads",
4395+
"compact_decimal_payloads",
4396+
"large_decimal_payloads"};
4397+
4398+
for (int64_t snapshot_id : {1, 3}) {
4399+
ScanContextBuilder scan_builder(table_path);
4400+
scan_builder.AddOption(Options::SCAN_SNAPSHOT_ID, std::to_string(snapshot_id));
4401+
ASSERT_OK_AND_ASSIGN(auto scan_context, scan_builder.Finish());
4402+
ASSERT_OK_AND_ASSIGN(auto table_scan, TableScan::Create(std::move(scan_context)));
4403+
ASSERT_OK_AND_ASSIGN(auto plan, table_scan->CreatePlan());
4404+
4405+
size_t string_layer_count = 0;
4406+
for (const auto& split : plan->Splits()) {
4407+
auto data_split = std::dynamic_pointer_cast<DataSplitImpl>(split);
4408+
ASSERT_TRUE(data_split);
4409+
for (const auto& file : data_split->DataFiles()) {
4410+
if (file->write_cols ==
4411+
std::optional<std::vector<std::string>>({"string_payloads"})) {
4412+
++string_layer_count;
4413+
}
4414+
}
4415+
}
4416+
ASSERT_EQ(snapshot_id == 1 ? 1 : 3, string_layer_count);
4417+
4418+
for (bool blob_as_descriptor : {false, true}) {
4419+
std::map<std::string, std::string> read_options = {
4420+
{Options::BLOB_AS_DESCRIPTOR, blob_as_descriptor ? "true" : "false"}};
4421+
ASSERT_OK_AND_ASSIGN(auto result, ReadTable(table_path, read_fields, plan,
4422+
/*predicate=*/nullptr, read_options));
4423+
ASSERT_TRUE(result);
4424+
auto combined = arrow::Concatenate(result->chunks()).ValueOrDie();
4425+
auto rows = std::dynamic_pointer_cast<arrow::StructArray>(combined);
4426+
ASSERT_TRUE(rows);
4427+
ASSERT_EQ(4, rows->length());
4428+
const auto& ids = checked_cast<const arrow::Int32Array&>(*rows->GetFieldByName("id"));
4429+
for (int64_t i = 0; i < ids.length(); ++i) {
4430+
ASSERT_EQ(i + 1, ids.Value(i));
4431+
}
4432+
4433+
CheckMapBlobColumn(
4434+
rows, "string_payloads",
4435+
R"json([[["", "string-empty"], ["alpha", "string-alpha"]], [], null, [["omega", "string-omega"]]])json",
4436+
blob_as_descriptor);
4437+
CheckMapBlobColumn(
4438+
rows, "boolean_payloads",
4439+
R"json([[[false, "bool-false"], [true, "bool-true"]], null, null, null])json",
4440+
blob_as_descriptor);
4441+
CheckMapBlobColumn(
4442+
rows, "tinyint_payloads",
4443+
R"json([[[-128, "tiny-min"], [-1, "tiny-negative"], [127, "tiny-max"]], null, null, null])json",
4444+
blob_as_descriptor);
4445+
CheckMapBlobColumn(
4446+
rows, "smallint_payloads",
4447+
R"json([[[-32768, "small-min"], [-1, "small-negative"], [32767, "small-max"]], null, null, null])json",
4448+
blob_as_descriptor);
4449+
CheckMapBlobColumn(
4450+
rows, "int_payloads",
4451+
R"json([[[-2147483648, "int-min"], [-1, "int-negative"], [2147483647, "int-max"]], null, null, null])json",
4452+
blob_as_descriptor);
4453+
CheckMapBlobColumn(
4454+
rows, "bigint_payloads",
4455+
R"json([[[-9223372036854775808, "big-min"], [-1, "big-negative"], [9223372036854775807, "big-max"]], null, null, null])json",
4456+
blob_as_descriptor);
4457+
CheckMapBlobColumn(
4458+
rows, "date_payloads",
4459+
R"json([[[-1, "date-negative"], [0, "date-epoch"]], null, null, null])json",
4460+
blob_as_descriptor);
4461+
CheckMapBlobColumn(
4462+
rows, "compact_decimal_payloads",
4463+
R"json([[["-99999999.99", "compact-negative"], ["99999999.99", "compact-positive"]], null, null, null])json",
4464+
blob_as_descriptor);
4465+
CheckMapBlobColumn(
4466+
rows, "large_decimal_payloads",
4467+
R"json([[["-999999999999999999.99", "large-negative"], ["999999999999999999.99", "large-positive"]], null, null, null])json",
4468+
blob_as_descriptor);
4469+
4470+
auto binary_map =
4471+
std::dynamic_pointer_cast<arrow::MapArray>(rows->GetFieldByName("binary_payloads"));
4472+
ASSERT_TRUE(binary_map);
4473+
ASSERT_OK_AND_ASSIGN(auto normalized_binary,
4474+
NormalizeMapBlobValues(binary_map, blob_as_descriptor));
4475+
const auto& binary_keys =
4476+
checked_cast<const arrow::BinaryArray&>(*normalized_binary->keys());
4477+
const auto& binary_values =
4478+
checked_cast<const arrow::LargeBinaryArray&>(*normalized_binary->items());
4479+
ASSERT_EQ(2, normalized_binary->value_length(0));
4480+
ASSERT_EQ("", binary_keys.GetString(0));
4481+
ASSERT_EQ(std::string("\0\xff\1\2", 4), binary_keys.GetString(1));
4482+
ASSERT_EQ("binary-empty", binary_values.GetString(0));
4483+
ASSERT_EQ("binary-bytes", binary_values.GetString(1));
4484+
}
4485+
}
4486+
}
4487+
43374488
} // namespace paimon::test
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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, software
13+
distributed under the License is distributed on an "AS IS" BASIS,
14+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
See the License for the specific language governing permissions and
16+
limitations under the License.
17+
-->
18+
19+
# Java MAP&lt;K, BLOB&gt; fixture
20+
21+
Generated by Apache Paimon Java at commit `a176eba1c6f9b0402eceea641bf435b05976470b`.
22+
23+
Snapshot 1 contains a full write. Snapshots 2 and 3 each add a partial write containing
24+
`BlobMapPlaceholder` values for `string_payloads`, so snapshot 3 exercises fallback across three
25+
sequence layers. The remaining map columns cover BOOLEAN, TINYINT, SMALLINT, INT, BIGINT, DATE,
26+
BINARY, compact DECIMAL, and large DECIMAL keys, including negative and boundary values.

0 commit comments

Comments
 (0)