Skip to content

Commit fcac3df

Browse files
waruto210thisisniczanmato1984
authored
GH-48072: [C++][Acero] fix a bug of materialize boolean (#48073)
### Rationale for this change Use the correct offset when accessing boolean values from the buffer. ### What changes are included in this PR? In `unmaterialized_table_internal.h`, use the correct offset when accessing boolean values from the buffer. Also, add a unit test to regress this bug. ### Are these changes tested? Yes ### Are there any user-facing changes? No **This PR contains a "Critical Fix".** Yes, fix a bug that caused incorrect data * GitHub Issue: #48072 Lead-authored-by: jsonwu <[email protected]> Co-authored-by: Nic Crane <[email protected]> Co-authored-by: Rossi Sun <[email protected]> Signed-off-by: Nic Crane <[email protected]>
1 parent a0e64ef commit fcac3df

2 files changed

Lines changed: 120 additions & 1 deletion

File tree

‎cpp/src/arrow/acero/sorted_merge_node_test.cc‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,22 @@
1616
// under the License.
1717

1818
#include <gtest/gtest.h>
19+
#include <limits>
20+
#include <memory>
21+
#include <vector>
1922

2023
#include "arrow/acero/exec_plan.h"
2124
#include "arrow/acero/map_node.h"
2225
#include "arrow/acero/options.h"
2326
#include "arrow/acero/test_nodes.h"
2427
#include "arrow/array/builder_base.h"
28+
#include "arrow/array/builder_primitive.h"
2529
#include "arrow/array/concatenate.h"
2630
#include "arrow/compute/ordering.h"
31+
#include "arrow/record_batch.h"
2732
#include "arrow/result.h"
2833
#include "arrow/scalar.h"
34+
#include "arrow/status.h"
2935
#include "arrow/table.h"
3036
#include "arrow/testing/generator.h"
3137
#include "arrow/testing/gtest_util.h"
@@ -83,4 +89,114 @@ TEST(SortedMergeNode, Basic) {
8389
AssertArraysEqual(*expected_ts, *output_ts);
8490
}
8591

92+
TEST(SortedMergeNode, TestSortedMergeTwoInputsWithBool) {
93+
const int64_t row_count = (16 << 10); // 16k rows per input
94+
// Not a multiple of 8, so that batches start at non-byte-aligned offsets
95+
const int64_t batch_size = 1001;
96+
97+
// Create schema with int column A and bool column B
98+
auto test_schema = arrow::schema(
99+
{arrow::field("col_a", arrow::int32()), arrow::field("col_b", arrow::boolean())});
100+
101+
// Helper lambda to create table source with specific pattern
102+
auto make_source = [&](int64_t cnt, int offset) -> arrow::Result<Declaration> {
103+
// Create column A (int) - values from offset to offset+cnt-1
104+
arrow::Int32Builder col_a_builder;
105+
std::vector<int32_t> col_a_values;
106+
col_a_values.reserve(cnt);
107+
for (int64_t i = 0; i < cnt; ++i) {
108+
col_a_values.push_back(static_cast<int32_t>(offset + i));
109+
}
110+
ARROW_RETURN_NOT_OK(col_a_builder.AppendValues(col_a_values));
111+
std::shared_ptr<arrow::Array> col_a_arr;
112+
ARROW_RETURN_NOT_OK(col_a_builder.Finish(&col_a_arr));
113+
114+
// Create column B (bool) - pattern: null if col_a % 7 == 0, otherwise
115+
// true if col_a % 5 == 0, false otherwise
116+
arrow::BooleanBuilder col_b_builder;
117+
for (int64_t i = 0; i < cnt; ++i) {
118+
int32_t a_value = static_cast<int32_t>(offset + i);
119+
if (a_value % 7 == 0) {
120+
ARROW_RETURN_NOT_OK(col_b_builder.AppendNull());
121+
continue;
122+
}
123+
bool b_value = (a_value % 5 == 0);
124+
ARROW_RETURN_NOT_OK(col_b_builder.Append(b_value));
125+
}
126+
std::shared_ptr<arrow::Array> col_b_arr;
127+
ARROW_RETURN_NOT_OK(col_b_builder.Finish(&col_b_arr));
128+
129+
auto table = arrow::Table::Make(test_schema, {col_a_arr, col_b_arr});
130+
auto table_source =
131+
Declaration("table_source", TableSourceNodeOptions(table, batch_size));
132+
return table_source;
133+
};
134+
135+
ASSERT_OK_AND_ASSIGN(auto source1, make_source(row_count, 0));
136+
ASSERT_OK_AND_ASSIGN(auto source2, make_source(row_count, 8192));
137+
138+
// Create sorted merge by column A
139+
auto ops = OrderByNodeOptions(compute::Ordering({compute::SortKey("col_a")}));
140+
Declaration sorted_merge{"sorted_merge", {source1, source2}, ops};
141+
142+
// Execute plan and collect result
143+
ASSERT_OK_AND_ASSIGN(auto result_table,
144+
arrow::acero::DeclarationToTable(sorted_merge, false));
145+
146+
ASSERT_TRUE(result_table != nullptr);
147+
148+
// Verify results
149+
auto col_a = result_table->GetColumnByName("col_a");
150+
auto col_b = result_table->GetColumnByName("col_b");
151+
ASSERT_TRUE(col_a != nullptr);
152+
ASSERT_TRUE(col_b != nullptr);
153+
154+
// Verify sorting and bool values
155+
int32_t last_a_value = std::numeric_limits<int32_t>::min();
156+
int64_t total_rows_checked = 0;
157+
158+
for (int i = 0; i < col_a->num_chunks(); i++) {
159+
auto a_chunk = std::static_pointer_cast<arrow::Int32Array>(col_a->chunk(i));
160+
auto b_chunk = std::static_pointer_cast<arrow::BooleanArray>(col_b->chunk(i));
161+
162+
ASSERT_EQ(a_chunk->length(), b_chunk->length())
163+
<< "Column A and B must have same length in chunk " << i;
164+
165+
for (int64_t j = 0; j < a_chunk->length(); j++) {
166+
ASSERT_FALSE(a_chunk->IsNull(j)) << "Column A should not have null values";
167+
168+
int32_t a_value = a_chunk->Value(j);
169+
170+
// Verify sorting by column A
171+
ASSERT_GE(a_value, last_a_value)
172+
<< "Values not sorted at chunk " << i << ", row " << j
173+
<< ": current=" << a_value << ", last=" << last_a_value;
174+
last_a_value = a_value;
175+
176+
// Verify bool validity: should be null if a_value % 7 == 0
177+
bool expected_b_null = (a_value % 7 == 0);
178+
ASSERT_EQ(b_chunk->IsNull(j), expected_b_null)
179+
<< "Bool validity incorrect at chunk " << i << ", row " << j
180+
<< ": col_a=" << a_value;
181+
182+
if (!expected_b_null) {
183+
// Verify bool value correctness: should be true if a_value % 5 == 0
184+
bool b_value = b_chunk->Value(j);
185+
bool expected_b_value = (a_value % 5 == 0);
186+
ASSERT_EQ(b_value, expected_b_value)
187+
<< "Bool value incorrect at chunk " << i << ", row " << j
188+
<< ": col_a=" << a_value << ", col_b=" << b_value
189+
<< ", expected=" << expected_b_value;
190+
}
191+
192+
total_rows_checked++;
193+
}
194+
}
195+
196+
ASSERT_EQ(last_a_value, 24575);
197+
198+
ASSERT_EQ(total_rows_checked, row_count * 2)
199+
<< "Expected " << row_count * 2 << " rows after merge";
200+
}
201+
86202
} // namespace arrow::acero

‎cpp/src/arrow/acero/unmaterialized_table_internal.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,10 @@ class UnmaterializedCompositeTable {
172172
builder.UnsafeAppendNull();
173173
return Status::OK();
174174
}
175-
builder.UnsafeAppend(bit_util::GetBit(source->template GetValues<uint8_t>(1), row));
175+
176+
const int64_t bit_offset = source->offset + static_cast<int64_t>(row);
177+
builder.UnsafeAppend(
178+
bit_util::GetBit(source->template GetValues<uint8_t>(1, 0), bit_offset));
176179
return Status::OK();
177180
}
178181

0 commit comments

Comments
 (0)