Skip to content

Commit 0b67bb4

Browse files
authored
fix(data issue): fix dictionary encoded binary read as empty (#373)
1 parent 7470e4c commit 0b67bb4

31 files changed

Lines changed: 617 additions & 119 deletions

‎docs/source/user_guide/compaction.rst‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,10 @@ one is read as ordinary values, and the writer takes both. A high-cardinality
122122
column that started dictionary-encoded and fell back to plain encoding therefore
123123
does not qualify, even though it still carries a dictionary page. ``BINARY`` is
124124
not forwarded although Parquet stores it in the same physical type and
125-
dictionary-encodes it the same way, because the value accessors cannot read a
126-
``BINARY`` dictionary. The rewrite also overrides the option back to ``false``
127-
when the table writes a format other than Parquet, when
125+
dictionary-encodes it the same way; this option only selects ``STRING`` columns.
126+
A file with a serialized Arrow dictionary schema can independently produce a
127+
``BINARY`` dictionary, which the value accessors also support. The rewrite also
128+
overrides the option back to ``false`` when the table writes a format other than Parquet, when
128129
``parquet.enable-dictionary`` is ``false`` because the writer would only expand
129130
the values again, or when variant/map shredding is configured because those
130131
writers reshape each batch against a fixed physical schema. Setting the option

‎src/paimon/common/data/columnar/columnar_utils.h‎

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,15 @@ class ColumnarUtils {
7373
dict_index = indices->Value(pos);
7474
}
7575
assert(dict_index >= 0);
76-
if (value_type_id == arrow::Type::type::STRING) {
76+
if (value_type_id == arrow::Type::type::STRING ||
77+
value_type_id == arrow::Type::type::BINARY) {
7778
auto dictionary =
78-
checked_cast<arrow::StringArray*>(typed_array->dictionary().get());
79+
checked_cast<arrow::BinaryArray*>(typed_array->dictionary().get());
7980
return dictionary->GetView(dict_index);
80-
} else if (value_type_id == arrow::Type::type::LARGE_STRING) {
81+
} else if (value_type_id == arrow::Type::type::LARGE_STRING ||
82+
value_type_id == arrow::Type::type::LARGE_BINARY) {
8183
auto dictionary =
82-
checked_cast<arrow::LargeStringArray*>(typed_array->dictionary().get());
84+
checked_cast<arrow::LargeBinaryArray*>(typed_array->dictionary().get());
8385
return dictionary->GetView(dict_index);
8486
}
8587
assert(false);

‎src/paimon/common/data/columnar/columnar_utils_test.cpp‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
#include "paimon/common/data/columnar/columnar_utils.h"
1919

2020
#include <string>
21+
#include <vector>
2122

2223
#include "arrow/api.h"
2324
#include "arrow/array/array_dict.h"
@@ -53,4 +54,51 @@ TEST(ColumnarUtilsTest, TestGetViewAndBytesOfDict) {
5354
ASSERT_EQ("foo", std::string(ColumnarUtils::GetView(dict_array.get(), 4)));
5455
}
5556

57+
template <typename ArrowType>
58+
class ColumnarUtilsBinaryDictionaryTest : public ::testing::Test {};
59+
60+
using BinaryDictionaryTypes = ::testing::Types<arrow::BinaryType, arrow::LargeBinaryType>;
61+
TYPED_TEST_SUITE(ColumnarUtilsBinaryDictionaryTest, BinaryDictionaryTypes);
62+
63+
TYPED_TEST(ColumnarUtilsBinaryDictionaryTest, GetViewAndBytes) {
64+
auto pool = GetDefaultPool();
65+
const std::vector<std::string> values = {std::string("\x00\xff\x80", 3), "",
66+
std::string("a\0b", 3)};
67+
typename arrow::TypeTraits<TypeParam>::BuilderType builder;
68+
ASSERT_TRUE(builder.Append("unused").ok());
69+
for (const auto& value : values) {
70+
ASSERT_TRUE(builder.Append(value).ok());
71+
}
72+
std::shared_ptr<arrow::Array> dictionary;
73+
ASSERT_TRUE(builder.Finish(&dictionary).ok());
74+
dictionary = dictionary->Slice(1);
75+
76+
const std::vector<std::shared_ptr<arrow::DataType>> index_types = {
77+
arrow::int8(), arrow::int16(), arrow::int32(), arrow::int64()};
78+
const std::vector<int32_t> expected_indices = {0, 1, 2, 0, -1, 2};
79+
for (const auto& index_type : index_types) {
80+
SCOPED_TRACE(index_type->ToString());
81+
auto indices =
82+
arrow::ipc::internal::json::ArrayFromJSON(index_type, "[0, 1, 2, 0, null, 2]")
83+
.ValueOrDie();
84+
auto dict_array = arrow::DictionaryArray::FromArrays(indices, dictionary).ValueOrDie();
85+
ASSERT_TRUE(dict_array->ValidateFull().ok());
86+
for (int32_t offset : {0, 1}) {
87+
SCOPED_TRACE(offset);
88+
auto sliced = dict_array->Slice(offset);
89+
for (int32_t pos = 0; pos < sliced->length(); ++pos) {
90+
SCOPED_TRACE(pos);
91+
int32_t index = expected_indices[offset + pos];
92+
ASSERT_EQ(index == -1, sliced->IsNull(pos));
93+
if (index == -1) {
94+
continue;
95+
}
96+
ASSERT_EQ(values[index], ColumnarUtils::GetView(sliced.get(), pos));
97+
auto bytes = ColumnarUtils::GetBytes<TypeParam>(sliced.get(), pos, pool.get());
98+
ASSERT_EQ(Bytes(values[index], pool.get()), *bytes);
99+
}
100+
}
101+
}
102+
}
103+
56104
} // namespace paimon::test

‎src/paimon/common/predicate/literal_converter.cpp‎

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -214,17 +214,26 @@ Result<std::vector<Literal>> LiteralConverter::ConvertLiteralsFromArray(const ar
214214
auto* dict_type = checked_cast<arrow::DictionaryType*>(dict_array.type().get());
215215
auto value_type_id = dict_type->value_type()->id();
216216
auto index_type_id = dict_type->index_type()->id();
217-
if (value_type_id == arrow::Type::type::STRING &&
217+
if ((value_type_id == arrow::Type::type::STRING ||
218+
value_type_id == arrow::Type::type::BINARY) &&
218219
index_type_id == arrow::Type::type::INT32) {
219-
return GetLiteralFromDictionaryArray<arrow::StringArray, arrow::Int32Array>(
220-
dict_array, FieldType::STRING, own_data);
221-
} else if (value_type_id == arrow::Type::type::LARGE_STRING &&
220+
FieldType literal_type = value_type_id == arrow::Type::type::STRING
221+
? FieldType::STRING
222+
: FieldType::BINARY;
223+
return GetLiteralFromDictionaryArray<arrow::BinaryArray, arrow::Int32Array>(
224+
dict_array, literal_type, own_data);
225+
} else if ((value_type_id == arrow::Type::type::LARGE_STRING ||
226+
value_type_id == arrow::Type::type::LARGE_BINARY) &&
222227
index_type_id == arrow::Type::type::INT64) {
223-
return GetLiteralFromDictionaryArray<arrow::LargeStringArray, arrow::Int64Array>(
224-
dict_array, FieldType::STRING, own_data);
228+
FieldType literal_type = value_type_id == arrow::Type::type::LARGE_STRING
229+
? FieldType::STRING
230+
: FieldType::BINARY;
231+
return GetLiteralFromDictionaryArray<arrow::LargeBinaryArray, arrow::Int64Array>(
232+
dict_array, literal_type, own_data);
225233
} else {
226234
return Status::Invalid(
227-
"only support [STRING, INT32] or [LARGE_STRING, INT64] for DictionaryArray");
235+
"only support [STRING|BINARY, INT32] or "
236+
"[LARGE_STRING|LARGE_BINARY, INT64] for DictionaryArray");
228237
}
229238
}
230239
default:

‎src/paimon/common/predicate/literal_converter.h‎

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -134,17 +134,8 @@ class PAIMON_EXPORT LiteralConverter {
134134
literals.emplace_back(literal_type);
135135
} else {
136136
int64_t dict_index = indices->Value(i);
137-
if constexpr (std::is_same_v<DictArrayType, arrow::StringArray>) {
138-
int32_t length = 0;
139-
const uint8_t* value = dictionary->GetValue(dict_index, &length);
140-
literals.emplace_back(literal_type, reinterpret_cast<const char*>(value),
141-
length, own_data);
142-
} else {
143-
int64_t length = 0;
144-
const uint8_t* value = dictionary->GetValue(dict_index, &length);
145-
literals.emplace_back(literal_type, reinterpret_cast<const char*>(value),
146-
length, own_data);
147-
}
137+
auto value = dictionary->GetView(dict_index);
138+
literals.emplace_back(literal_type, value.data(), value.size(), own_data);
148139
}
149140
}
150141
return literals;

‎src/paimon/common/predicate/literal_converter_test.cpp‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,59 @@ TEST_F(LiteralConverterTest, TestDictType) {
449449
Literal(FieldType::STRING, "foo", 3), Literal(FieldType::STRING)}));
450450
}
451451

452+
TEST_F(LiteralConverterTest, TestDictionaryValueAndIndexTypes) {
453+
const std::vector<std::pair<std::shared_ptr<arrow::DataType>, std::shared_ptr<arrow::DataType>>>
454+
types = {{arrow::utf8(), arrow::int32()},
455+
{arrow::binary(), arrow::int32()},
456+
{arrow::large_utf8(), arrow::int64()},
457+
{arrow::large_binary(), arrow::int64()}};
458+
for (const auto& [value_type, index_type] : types) {
459+
SCOPED_TRACE(value_type->ToString());
460+
const FieldType literal_type =
461+
arrow::is_string(value_type->id()) ? FieldType::STRING : FieldType::BINARY;
462+
auto dictionary = arrow::ipc::internal::json::ArrayFromJSON(
463+
value_type, R"(["unused", "a\u0000b", "", "middle", "tail"])")
464+
.ValueOrDie()
465+
->Slice(1);
466+
const std::vector<Literal> expected = {
467+
Literal(literal_type, "a\0b", 3), Literal(literal_type),
468+
Literal(literal_type, "", 0), Literal(literal_type, "middle", 6),
469+
Literal(literal_type, "tail", 4), Literal(literal_type, "a\0b", 3)};
470+
SCOPED_TRACE(index_type->ToString());
471+
auto indices =
472+
arrow::ipc::internal::json::ArrayFromJSON(index_type, "[3, 0, null, 1, 2, 3, 0]")
473+
.ValueOrDie();
474+
auto array = arrow::DictionaryArray::FromArrays(indices, dictionary).ValueOrDie();
475+
ASSERT_TRUE(array->ValidateFull().ok());
476+
auto sliced = array->Slice(1);
477+
for (bool own_data : {false, true}) {
478+
SCOPED_TRACE(own_data);
479+
ASSERT_OK_AND_ASSIGN(std::vector<Literal> actual,
480+
LiteralConverter::ConvertLiteralsFromArray(*sliced, own_data));
481+
ASSERT_EQ(expected, actual);
482+
}
483+
}
484+
}
485+
486+
TEST_F(LiteralConverterTest, TestBinaryDictionaryPreservesBytes) {
487+
const std::string bytes("\x00\xff\x80", 3);
488+
arrow::BinaryBuilder builder;
489+
ASSERT_TRUE(builder.Append(bytes).ok());
490+
std::shared_ptr<arrow::Array> dictionary;
491+
ASSERT_TRUE(builder.Finish(&dictionary).ok());
492+
auto indices =
493+
arrow::ipc::internal::json::ArrayFromJSON(arrow::int32(), "[0, null, 0]").ValueOrDie();
494+
auto array = arrow::DictionaryArray::FromArrays(indices, dictionary).ValueOrDie();
495+
ASSERT_OK_AND_ASSIGN(std::vector<Literal> actual,
496+
LiteralConverter::ConvertLiteralsFromArray(*array, /*own_data=*/true));
497+
array.reset();
498+
dictionary.reset();
499+
ASSERT_EQ(actual,
500+
std::vector<Literal>({Literal(FieldType::BINARY, bytes.data(), bytes.size()),
501+
Literal(FieldType::BINARY),
502+
Literal(FieldType::BINARY, bytes.data(), bytes.size())}));
503+
}
504+
452505
TEST_F(LiteralConverterTest, TestLiteralsToArray) {
453506
// Every writable field type converts to an array and back, the null literal included, so each
454507
// case asserts `ConvertLiteralsToArray` and `ConvertLiteralsFromArray` agree on every type.

‎src/paimon/common/predicate/null_false_leaf_binary_function.cpp‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -98,12 +98,15 @@ std::optional<FieldType> ArrayFieldType(const arrow::Array& array) {
9898
const std::shared_ptr<arrow::DataType>& type = array.type();
9999
if (type->id() == arrow::Type::DICTIONARY) {
100100
const auto& dict_type = checked_cast<const arrow::DictionaryType&>(*type);
101-
const bool is_string = dict_type.value_type()->id() == arrow::Type::STRING &&
102-
dict_type.index_type()->id() == arrow::Type::INT32;
103-
const bool is_large_string = dict_type.value_type()->id() == arrow::Type::LARGE_STRING &&
104-
dict_type.index_type()->id() == arrow::Type::INT64;
105-
if (is_string || is_large_string) {
106-
return FieldType::STRING;
101+
const arrow::Type::type value_type = dict_type.value_type()->id();
102+
const arrow::Type::type index_type = dict_type.index_type()->id();
103+
if ((value_type == arrow::Type::STRING || value_type == arrow::Type::BINARY) &&
104+
index_type == arrow::Type::INT32) {
105+
return value_type == arrow::Type::STRING ? FieldType::STRING : FieldType::BINARY;
106+
}
107+
if ((value_type == arrow::Type::LARGE_STRING || value_type == arrow::Type::LARGE_BINARY) &&
108+
index_type == arrow::Type::INT64) {
109+
return value_type == arrow::Type::LARGE_STRING ? FieldType::STRING : FieldType::BINARY;
107110
}
108111
return std::nullopt;
109112
}

‎src/paimon/common/predicate/null_false_leaf_binary_function_test.cpp‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,28 @@ TEST_F(NullFalseLeafBinaryFunctionTest, TestLargeStringDictionary) {
339339
std::vector<char>({0, 1, 0, 0}));
340340
}
341341

342+
TEST_F(NullFalseLeafBinaryFunctionTest, TestBinaryDictionary) {
343+
const std::vector<std::pair<std::shared_ptr<arrow::DataType>, std::shared_ptr<arrow::DataType>>>
344+
types = {{arrow::binary(), arrow::int32()}, {arrow::large_binary(), arrow::int64()}};
345+
for (const auto& [value_type, index_type] : types) {
346+
SCOPED_TRACE(value_type->ToString());
347+
auto dictionary =
348+
arrow::ipc::internal::json::ArrayFromJSON(value_type, R"(["a\u0000b", "a", "", null])")
349+
.ValueOrDie();
350+
SCOPED_TRACE(index_type->ToString());
351+
auto indices =
352+
arrow::ipc::internal::json::ArrayFromJSON(index_type, "[1, 0, null, 1, 2, 3, 0]")
353+
.ValueOrDie();
354+
auto array = arrow::DictionaryArray::FromArrays(indices, dictionary).ValueOrDie()->Slice(1);
355+
const auto literal = BinaryLiteral(std::string("a\0b", 3));
356+
ASSERT_EQ(Eval(Equal::Instance(), literal, array), std::vector<char>({1, 0, 0, 0, 0, 1}));
357+
ASSERT_EQ(Eval(LessThan::Instance(), literal, array),
358+
std::vector<char>({0, 0, 1, 1, 0, 0}));
359+
ASSERT_EQ(Eval(NotEqual::Instance(), BinaryLiteral(""), array),
360+
std::vector<char>({1, 0, 1, 0, 0, 1}));
361+
}
362+
}
363+
342364
TEST_F(NullFalseLeafBinaryFunctionTest, TestDictionaryWithNullValue) {
343365
// A kernel decodes the dictionary, so a row pointing at a null dictionary value becomes a null
344366
// row and is false for every comparison. The row by row path reads the slot the index names

‎src/paimon/core/casting/casting_utils.cpp‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,21 @@
2323
#include "paimon/common/utils/checked_cast.h"
2424

2525
namespace paimon {
26+
Result<std::shared_ptr<arrow::Array>> CastingUtils::DecodeDictionary(
27+
const std::shared_ptr<arrow::Array>& array, arrow::MemoryPool* pool) {
28+
if (array->type_id() != arrow::Type::DICTIONARY) {
29+
return array;
30+
}
31+
const auto& dictionary_type = checked_cast<const arrow::DictionaryType&>(*array->type());
32+
std::shared_ptr<arrow::DataType> value_type = dictionary_type.value_type();
33+
/// We're not casting `LARGE_BINARY` to `BINARY` since `BLOB` specifically expects
34+
/// `LARGE_BINARY`. See also in `src/paimon/common/utils/field_type_utils.h`.
35+
if (value_type->id() == arrow::Type::LARGE_STRING) {
36+
value_type = arrow::utf8();
37+
}
38+
return Cast(array, value_type, arrow::compute::CastOptions::Safe(), pool);
39+
}
40+
2641
Result<std::shared_ptr<arrow::Array>> CastingUtils::Cast(
2742
const std::shared_ptr<arrow::Array>& src_array,
2843
const std::shared_ptr<arrow::DataType>& target_type, const arrow::compute::CastOptions& options,

‎src/paimon/core/casting/casting_utils.h‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ class PAIMON_EXPORT CastingUtils {
4747
const std::shared_ptr<arrow::DataType>& target_type,
4848
const arrow::compute::CastOptions& options, arrow::MemoryPool* pool);
4949

50+
/// Decodes a dictionary to its value type, normalizing LARGE_STRING to STRING for readers
51+
/// that widen string offsets. Other value types, including binary offsets, are preserved.
52+
/// Returns a non-dictionary array unchanged.
53+
static Result<std::shared_ptr<arrow::Array>> DecodeDictionary(
54+
const std::shared_ptr<arrow::Array>& array, arrow::MemoryPool* pool);
55+
5056
template <typename SrcScalar, typename SrcDataType, typename TargetScalar,
5157
typename TargetDataType>
5258
static Result<Literal> Cast(const Literal& literal,

0 commit comments

Comments
 (0)