Skip to content

Commit b818560

Browse files
authored
GH-45334: [C++][Acero] Fix swiss join overflow issues in row offset calculation for fixed length and null masks (#45336)
### Rationale for this change #45334 ### What changes are included in this PR? 1. An all-mighty test case that can effectively reveal all the bugs mentioned in the issue; 2. Other than directly fixing the bugs (actually simply casting to 64-bit somewhere in the multiplication will do), I did some refinement to the buffer accessors of the row table, in order to eliminate more potential similar issues (which I believe do exist): 1. `null_masks()` -> `null_masks(row_id)` which does overflow-safe indexing inside; 2. `is_null(row_id, col_pos)` which does overflow-safe indexing and directly gets the bit of the column; 3. `data(1)` -> `fixed_length_rows(row_id)` which first asserts the row table being fixed-length, then does overflow-safe indexing inside; 4. `data(2)` -> `var_length_rows()` which only asserts the row table being var-length. It is supposed to be paired by the `offsets()` (which is already 64-bit by #43389 ); 5. The `data(0/1/2)` members are made private. 3. The AVX2 specializations are fixed individually by using 64-bit multiplication and indexing. ### Are these changes tested? Yes. ### Are there any user-facing changes? None. * GitHub Issue: #45334 Authored-by: Rossi Sun <zanmato1984@gmail.com> Signed-off-by: Rossi Sun <zanmato1984@gmail.com>
1 parent ac1e7ec commit b818560

14 files changed

+313
-198
lines changed

cpp/src/arrow/acero/hash_join_node_test.cc

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3449,5 +3449,104 @@ TEST(HashJoin, LARGE_MEMORY_TEST(BuildSideOver4GBVarLength)) {
34493449
num_batches_left * num_rows_per_batch_left * num_batches_right);
34503450
}
34513451

3452+
// GH-45334: The row ids of the matching rows on the right side (the build side) are very
3453+
// big, causing the index calculation overflow.
3454+
TEST(HashJoin, BuildSideLargeRowIds) {
3455+
GTEST_SKIP() << "Test disabled due to excessively time and resource consuming, "
3456+
"for local debugging only.";
3457+
3458+
// A fair amount of match rows to trigger both SIMD and non-SIMD code paths.
3459+
const int64_t num_match_rows = 35;
3460+
const int64_t num_rows_per_match_batch = 35;
3461+
const int64_t num_match_batches = num_match_rows / num_rows_per_match_batch;
3462+
3463+
const int64_t num_unmatch_rows_large = 720898048;
3464+
const int64_t num_rows_per_unmatch_batch_large = 352001;
3465+
const int64_t num_unmatch_batches_large =
3466+
num_unmatch_rows_large / num_rows_per_unmatch_batch_large;
3467+
3468+
auto schema_small =
3469+
schema({field("small_key", int64()), field("small_payload", int64())});
3470+
auto schema_large =
3471+
schema({field("large_key", int64()), field("large_payload", int64())});
3472+
3473+
// A carefully chosen key value which hashes to 0xFFFFFFFE, making the match rows to be
3474+
// placed at higher address of the row table.
3475+
const int64_t match_key = 289339070;
3476+
const int64_t match_payload = 42;
3477+
3478+
// Match arrays of length num_rows_per_match_batch.
3479+
ASSERT_OK_AND_ASSIGN(
3480+
auto match_key_arr,
3481+
Constant(MakeScalar(match_key))->Generate(num_rows_per_match_batch));
3482+
ASSERT_OK_AND_ASSIGN(
3483+
auto match_payload_arr,
3484+
Constant(MakeScalar(match_payload))->Generate(num_rows_per_match_batch));
3485+
// Append 1 row of null to trigger null processing code paths.
3486+
ASSERT_OK_AND_ASSIGN(auto null_arr, MakeArrayOfNull(int64(), 1));
3487+
ASSERT_OK_AND_ASSIGN(match_key_arr, Concatenate({match_key_arr, null_arr}));
3488+
ASSERT_OK_AND_ASSIGN(match_payload_arr, Concatenate({match_payload_arr, null_arr}));
3489+
// Match batch.
3490+
ExecBatch match_batch({match_key_arr, match_payload_arr}, num_rows_per_match_batch + 1);
3491+
3492+
// Small batch.
3493+
ExecBatch batch_small = match_batch;
3494+
3495+
// Large unmatch batches.
3496+
const int64_t seed = 42;
3497+
std::vector<ExecBatch> unmatch_batches_large;
3498+
unmatch_batches_large.reserve(num_unmatch_batches_large);
3499+
ASSERT_OK_AND_ASSIGN(auto unmatch_payload_arr_large,
3500+
MakeArrayOfNull(int64(), num_rows_per_unmatch_batch_large));
3501+
int64_t unmatch_range_per_batch =
3502+
(std::numeric_limits<int64_t>::max() - match_key) / num_unmatch_batches_large;
3503+
for (int i = 0; i < num_unmatch_batches_large; ++i) {
3504+
auto unmatch_key_arr_large = RandomArrayGenerator(seed).Int64(
3505+
num_rows_per_unmatch_batch_large,
3506+
/*min=*/match_key + 1 + i * unmatch_range_per_batch,
3507+
/*max=*/match_key + 1 + (i + 1) * unmatch_range_per_batch);
3508+
unmatch_batches_large.push_back(
3509+
ExecBatch({unmatch_key_arr_large, unmatch_payload_arr_large},
3510+
num_rows_per_unmatch_batch_large));
3511+
}
3512+
// Large match batch.
3513+
ExecBatch match_batch_large = match_batch;
3514+
3515+
// Batches with schemas.
3516+
auto batches_small = BatchesWithSchema{
3517+
std::vector<ExecBatch>(num_match_batches, batch_small), schema_small};
3518+
auto batches_large = BatchesWithSchema{std::move(unmatch_batches_large), schema_large};
3519+
for (int i = 0; i < num_match_batches; i++) {
3520+
batches_large.batches.push_back(match_batch_large);
3521+
}
3522+
3523+
Declaration source_small{
3524+
"exec_batch_source",
3525+
ExecBatchSourceNodeOptions(batches_small.schema, batches_small.batches)};
3526+
Declaration source_large{
3527+
"exec_batch_source",
3528+
ExecBatchSourceNodeOptions(batches_large.schema, batches_large.batches)};
3529+
3530+
HashJoinNodeOptions join_opts(JoinType::INNER, /*left_keys=*/{"small_key"},
3531+
/*right_keys=*/{"large_key"});
3532+
Declaration join{
3533+
"hashjoin", {std::move(source_small), std::move(source_large)}, join_opts};
3534+
3535+
// Join should emit num_match_rows * num_match_rows rows.
3536+
ASSERT_OK_AND_ASSIGN(auto batches_result, DeclarationToExecBatches(std::move(join)));
3537+
Declaration result{"exec_batch_source",
3538+
ExecBatchSourceNodeOptions(std::move(batches_result.schema),
3539+
std::move(batches_result.batches))};
3540+
AssertRowCountEq(result, num_match_rows * num_match_rows);
3541+
3542+
// All rows should be match_key/payload.
3543+
auto predicate = and_({equal(field_ref("small_key"), literal(match_key)),
3544+
equal(field_ref("small_payload"), literal(match_payload)),
3545+
equal(field_ref("large_key"), literal(match_key)),
3546+
equal(field_ref("large_payload"), literal(match_payload))});
3547+
Declaration filter{"filter", {result}, FilterNodeOptions{std::move(predicate)}};
3548+
AssertRowCountEq(std::move(filter), num_match_rows * num_match_rows);
3549+
}
3550+
34523551
} // namespace acero
34533552
} // namespace arrow

cpp/src/arrow/acero/swiss_join.cc

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -477,14 +477,15 @@ void RowArrayMerge::CopyFixedLength(RowTableImpl* target, const RowTableImpl& so
477477
const int64_t* source_rows_permutation) {
478478
int64_t num_source_rows = source.length();
479479

480-
int64_t fixed_length = target->metadata().fixed_length;
480+
uint32_t fixed_length = target->metadata().fixed_length;
481481

482482
// Permutation of source rows is optional. Without permutation all that is
483483
// needed is memcpy.
484484
//
485485
if (!source_rows_permutation) {
486-
memcpy(target->mutable_data(1) + fixed_length * first_target_row_id, source.data(1),
487-
fixed_length * num_source_rows);
486+
DCHECK_LE(first_target_row_id, std::numeric_limits<uint32_t>::max());
487+
memcpy(target->mutable_fixed_length_rows(static_cast<uint32_t>(first_target_row_id)),
488+
source.fixed_length_rows(/*row_id=*/0), fixed_length * num_source_rows);
488489
} else {
489490
// Row length must be a multiple of 64-bits due to enforced alignment.
490491
// Loop for each output row copying a fixed number of 64-bit words.
@@ -494,10 +495,13 @@ void RowArrayMerge::CopyFixedLength(RowTableImpl* target, const RowTableImpl& so
494495
int64_t num_words_per_row = fixed_length / sizeof(uint64_t);
495496
for (int64_t i = 0; i < num_source_rows; ++i) {
496497
int64_t source_row_id = source_rows_permutation[i];
498+
DCHECK_LE(source_row_id, std::numeric_limits<uint32_t>::max());
497499
const uint64_t* source_row_ptr = reinterpret_cast<const uint64_t*>(
498-
source.data(1) + fixed_length * source_row_id);
500+
source.fixed_length_rows(static_cast<uint32_t>(source_row_id)));
501+
int64_t target_row_id = first_target_row_id + i;
502+
DCHECK_LE(target_row_id, std::numeric_limits<uint32_t>::max());
499503
uint64_t* target_row_ptr = reinterpret_cast<uint64_t*>(
500-
target->mutable_data(1) + fixed_length * (first_target_row_id + i));
504+
target->mutable_fixed_length_rows(static_cast<uint32_t>(target_row_id)));
501505

502506
for (int64_t word = 0; word < num_words_per_row; ++word) {
503507
target_row_ptr[word] = source_row_ptr[word];
@@ -529,16 +533,16 @@ void RowArrayMerge::CopyVaryingLength(RowTableImpl* target, const RowTableImpl&
529533

530534
// We can simply memcpy bytes of rows if their order has not changed.
531535
//
532-
memcpy(target->mutable_data(2) + target_offsets[first_target_row_id], source.data(2),
533-
source_offsets[num_source_rows] - source_offsets[0]);
536+
memcpy(target->mutable_var_length_rows() + target_offsets[first_target_row_id],
537+
source.var_length_rows(), source_offsets[num_source_rows] - source_offsets[0]);
534538
} else {
535539
int64_t target_row_offset = first_target_row_offset;
536-
uint64_t* target_row_ptr =
537-
reinterpret_cast<uint64_t*>(target->mutable_data(2) + target_row_offset);
540+
uint64_t* target_row_ptr = reinterpret_cast<uint64_t*>(
541+
target->mutable_var_length_rows() + target_row_offset);
538542
for (int64_t i = 0; i < num_source_rows; ++i) {
539543
int64_t source_row_id = source_rows_permutation[i];
540544
const uint64_t* source_row_ptr = reinterpret_cast<const uint64_t*>(
541-
source.data(2) + source_offsets[source_row_id]);
545+
source.var_length_rows() + source_offsets[source_row_id]);
542546
int64_t length = source_offsets[source_row_id + 1] - source_offsets[source_row_id];
543547
// Though the row offset is 64-bit, the length of a single row must be 32-bit as
544548
// required by current row table implementation.
@@ -564,14 +568,18 @@ void RowArrayMerge::CopyNulls(RowTableImpl* target, const RowTableImpl& source,
564568
const int64_t* source_rows_permutation) {
565569
int64_t num_source_rows = source.length();
566570
int num_bytes_per_row = target->metadata().null_masks_bytes_per_row;
567-
uint8_t* target_nulls = target->null_masks() + num_bytes_per_row * first_target_row_id;
571+
DCHECK_LE(first_target_row_id, std::numeric_limits<uint32_t>::max());
572+
uint8_t* target_nulls =
573+
target->mutable_null_masks(static_cast<uint32_t>(first_target_row_id));
568574
if (!source_rows_permutation) {
569-
memcpy(target_nulls, source.null_masks(), num_bytes_per_row * num_source_rows);
575+
memcpy(target_nulls, source.null_masks(/*row_id=*/0),
576+
num_bytes_per_row * num_source_rows);
570577
} else {
571-
for (int64_t i = 0; i < num_source_rows; ++i) {
578+
for (uint32_t i = 0; i < num_source_rows; ++i) {
572579
int64_t source_row_id = source_rows_permutation[i];
580+
DCHECK_LE(source_row_id, std::numeric_limits<uint32_t>::max());
573581
const uint8_t* source_nulls =
574-
source.null_masks() + num_bytes_per_row * source_row_id;
582+
source.null_masks(static_cast<uint32_t>(source_row_id));
575583
for (int64_t byte = 0; byte < num_bytes_per_row; ++byte) {
576584
*target_nulls++ = *source_nulls++;
577585
}

cpp/src/arrow/acero/swiss_join_avx2.cc

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
// under the License.
1717

1818
#include "arrow/acero/swiss_join_internal.h"
19+
#include "arrow/compute/row/row_util_avx2_internal.h"
1920
#include "arrow/util/bit_util.h"
2021
#include "arrow/util/simd.h"
2122

@@ -46,7 +47,7 @@ int RowArrayAccessor::Visit_avx2(const RowTableImpl& rows, int column_id, int nu
4647

4748
if (!is_fixed_length_column) {
4849
int varbinary_column_id = VarbinaryColumnId(rows.metadata(), column_id);
49-
const uint8_t* row_ptr_base = rows.data(2);
50+
const uint8_t* row_ptr_base = rows.var_length_rows();
5051
const RowTableImpl::offset_type* row_offsets = rows.offsets();
5152
auto row_offsets_i64 =
5253
reinterpret_cast<const arrow::util::int64_for_gather_t*>(row_offsets);
@@ -172,7 +173,7 @@ int RowArrayAccessor::Visit_avx2(const RowTableImpl& rows, int column_id, int nu
172173
if (is_fixed_length_row) {
173174
// Case 3: This is a fixed length column in fixed length row
174175
//
175-
const uint8_t* row_ptr_base = rows.data(1);
176+
const uint8_t* row_ptr_base = rows.fixed_length_rows(/*row_id=*/0);
176177
for (int i = 0; i < num_rows / kUnroll; ++i) {
177178
// Load 8 32-bit row ids.
178179
__m256i row_id =
@@ -197,7 +198,7 @@ int RowArrayAccessor::Visit_avx2(const RowTableImpl& rows, int column_id, int nu
197198
} else {
198199
// Case 4: This is a fixed length column in varying length row
199200
//
200-
const uint8_t* row_ptr_base = rows.data(2);
201+
const uint8_t* row_ptr_base = rows.var_length_rows();
201202
const RowTableImpl::offset_type* row_offsets = rows.offsets();
202203
auto row_offsets_i64 =
203204
reinterpret_cast<const arrow::util::int64_for_gather_t*>(row_offsets);
@@ -237,31 +238,12 @@ int RowArrayAccessor::VisitNulls_avx2(const RowTableImpl& rows, int column_id,
237238
//
238239
constexpr int kUnroll = 8;
239240

240-
const uint8_t* null_masks = rows.null_masks();
241-
__m256i null_bits_per_row =
242-
_mm256_set1_epi32(8 * rows.metadata().null_masks_bytes_per_row);
243-
__m256i pos_after_encoding =
244-
_mm256_set1_epi32(rows.metadata().pos_after_encoding(column_id));
241+
uint32_t pos_after_encoding = rows.metadata().pos_after_encoding(column_id);
245242
for (int i = 0; i < num_rows / kUnroll; ++i) {
246243
__m256i row_id = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(row_ids) + i);
247-
__m256i bit_id = _mm256_mullo_epi32(row_id, null_bits_per_row);
248-
bit_id = _mm256_add_epi32(bit_id, pos_after_encoding);
249-
__m256i bytes = _mm256_i32gather_epi32(reinterpret_cast<const int*>(null_masks),
250-
_mm256_srli_epi32(bit_id, 3), 1);
251-
__m256i bit_in_word = _mm256_sllv_epi32(
252-
_mm256_set1_epi32(1), _mm256_and_si256(bit_id, _mm256_set1_epi32(7)));
253-
// `result` will contain one 32-bit word per tested null bit, either 0xffffffff if the
254-
// null bit was set or 0 if it was unset.
255-
__m256i result =
256-
_mm256_cmpeq_epi32(_mm256_and_si256(bytes, bit_in_word), bit_in_word);
257-
// NB: Be careful about sign-extension when casting the return value of
258-
// _mm256_movemask_epi8 (signed 32-bit) to unsigned 64-bit, which will pollute the
259-
// higher bits of the following OR.
260-
uint32_t null_bytes_lo = static_cast<uint32_t>(
261-
_mm256_movemask_epi8(_mm256_cvtepi32_epi64(_mm256_castsi256_si128(result))));
262-
uint64_t null_bytes_hi =
263-
_mm256_movemask_epi8(_mm256_cvtepi32_epi64(_mm256_extracti128_si256(result, 1)));
264-
uint64_t null_bytes = null_bytes_lo | (null_bytes_hi << 32);
244+
__m256i null32 = GetNullBitInt32(rows, pos_after_encoding, row_id);
245+
null32 = _mm256_cmpeq_epi32(null32, _mm256_set1_epi32(1));
246+
uint64_t null_bytes = arrow::compute::Cmp32To8(null32);
265247

266248
process_8_values_fn(i * kUnroll, null_bytes);
267249
}

cpp/src/arrow/acero/swiss_join_internal.h

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ class RowArrayAccessor {
7272

7373
if (!is_fixed_length_column) {
7474
int varbinary_column_id = VarbinaryColumnId(rows.metadata(), column_id);
75-
const uint8_t* row_ptr_base = rows.data(2);
75+
const uint8_t* row_ptr_base = rows.var_length_rows();
7676
const RowTableImpl::offset_type* row_offsets = rows.offsets();
7777
uint32_t field_offset_within_row, field_length;
7878

@@ -108,22 +108,21 @@ class RowArrayAccessor {
108108
if (field_length == 0) {
109109
field_length = 1;
110110
}
111-
uint32_t row_length = rows.metadata().fixed_length;
112111

113112
bool is_fixed_length_row = rows.metadata().is_fixed_length;
114113
if (is_fixed_length_row) {
115114
// Case 3: This is a fixed length column in a fixed length row
116115
//
117-
const uint8_t* row_ptr_base = rows.data(1) + field_offset_within_row;
118116
for (int i = 0; i < num_rows; ++i) {
119117
uint32_t row_id = row_ids[i];
120-
const uint8_t* row_ptr = row_ptr_base + row_length * row_id;
118+
const uint8_t* row_ptr =
119+
rows.fixed_length_rows(row_id) + field_offset_within_row;
121120
process_value_fn(i, row_ptr, field_length);
122121
}
123122
} else {
124123
// Case 4: This is a fixed length column in a varying length row
125124
//
126-
const uint8_t* row_ptr_base = rows.data(2) + field_offset_within_row;
125+
const uint8_t* row_ptr_base = rows.var_length_rows() + field_offset_within_row;
127126
const RowTableImpl::offset_type* row_offsets = rows.offsets();
128127
for (int i = 0; i < num_rows; ++i) {
129128
uint32_t row_id = row_ids[i];
@@ -142,13 +141,10 @@ class RowArrayAccessor {
142141
template <class PROCESS_VALUE_FN>
143142
static void VisitNulls(const RowTableImpl& rows, int column_id, int num_rows,
144143
const uint32_t* row_ids, PROCESS_VALUE_FN process_value_fn) {
145-
const uint8_t* null_masks = rows.null_masks();
146-
uint32_t null_mask_num_bytes = rows.metadata().null_masks_bytes_per_row;
147144
uint32_t pos_after_encoding = rows.metadata().pos_after_encoding(column_id);
148145
for (int i = 0; i < num_rows; ++i) {
149146
uint32_t row_id = row_ids[i];
150-
int64_t bit_id = row_id * null_mask_num_bytes * 8 + pos_after_encoding;
151-
process_value_fn(i, bit_util::GetBit(null_masks, bit_id) ? 0xff : 0);
147+
process_value_fn(i, rows.is_null(row_id, pos_after_encoding) ? 0xff : 0);
152148
}
153149
}
154150

cpp/src/arrow/compute/row/compare_internal.cc

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,10 @@ void KeyCompare::NullUpdateColumnToRow(uint32_t id_col, uint32_t num_rows_to_com
5555

5656
if (!col.data(0)) {
5757
// Remove rows from the result for which the column value is a null
58-
const uint8_t* null_masks = rows.null_masks();
59-
uint32_t null_mask_num_bytes = rows.metadata().null_masks_bytes_per_row;
6058
for (uint32_t i = num_processed; i < num_rows_to_compare; ++i) {
6159
uint32_t irow_left = use_selection ? sel_left_maybe_null[i] : i;
6260
uint32_t irow_right = left_to_right_map[irow_left];
63-
int64_t bitid = irow_right * null_mask_num_bytes * 8 + null_bit_id;
64-
match_bytevector[i] &= (bit_util::GetBit(null_masks, bitid) ? 0 : 0xff);
61+
match_bytevector[i] &= (rows.is_null(irow_right, null_bit_id) ? 0 : 0xff);
6562
}
6663
} else if (!rows.has_any_nulls(ctx)) {
6764
// Remove rows from the result for which the column value on left side is
@@ -74,15 +71,12 @@ void KeyCompare::NullUpdateColumnToRow(uint32_t id_col, uint32_t num_rows_to_com
7471
bit_util::GetBit(non_nulls, irow_left + col.bit_offset(0)) ? 0xff : 0;
7572
}
7673
} else {
77-
const uint8_t* null_masks = rows.null_masks();
78-
uint32_t null_mask_num_bytes = rows.metadata().null_masks_bytes_per_row;
7974
const uint8_t* non_nulls = col.data(0);
8075
ARROW_DCHECK(non_nulls);
8176
for (uint32_t i = num_processed; i < num_rows_to_compare; ++i) {
8277
uint32_t irow_left = use_selection ? sel_left_maybe_null[i] : i;
8378
uint32_t irow_right = left_to_right_map[irow_left];
84-
int64_t bitid_right = irow_right * null_mask_num_bytes * 8 + null_bit_id;
85-
int right_null = bit_util::GetBit(null_masks, bitid_right) ? 0xff : 0;
79+
int right_null = rows.is_null(irow_right, null_bit_id) ? 0xff : 0;
8680
int left_null =
8781
bit_util::GetBit(non_nulls, irow_left + col.bit_offset(0)) ? 0 : 0xff;
8882
match_bytevector[i] |= left_null & right_null;
@@ -101,7 +95,7 @@ void KeyCompare::CompareBinaryColumnToRowHelper(
10195
if (is_fixed_length) {
10296
uint32_t fixed_length = rows.metadata().fixed_length;
10397
const uint8_t* rows_left = col.data(1);
104-
const uint8_t* rows_right = rows.data(1);
98+
const uint8_t* rows_right = rows.fixed_length_rows(/*row_id=*/0);
10599
for (uint32_t i = first_row_to_compare; i < num_rows_to_compare; ++i) {
106100
uint32_t irow_left = use_selection ? sel_left_maybe_null[i] : i;
107101
// irow_right is used to index into row data so promote to the row offset type.
@@ -113,7 +107,7 @@ void KeyCompare::CompareBinaryColumnToRowHelper(
113107
} else {
114108
const uint8_t* rows_left = col.data(1);
115109
const RowTableImpl::offset_type* offsets_right = rows.offsets();
116-
const uint8_t* rows_right = rows.data(2);
110+
const uint8_t* rows_right = rows.var_length_rows();
117111
for (uint32_t i = first_row_to_compare; i < num_rows_to_compare; ++i) {
118112
uint32_t irow_left = use_selection ? sel_left_maybe_null[i] : i;
119113
uint32_t irow_right = left_to_right_map[irow_left];
@@ -246,7 +240,7 @@ void KeyCompare::CompareVarBinaryColumnToRowHelper(
246240
const uint32_t* offsets_left = col.offsets();
247241
const RowTableImpl::offset_type* offsets_right = rows.offsets();
248242
const uint8_t* rows_left = col.data(2);
249-
const uint8_t* rows_right = rows.data(2);
243+
const uint8_t* rows_right = rows.var_length_rows();
250244
for (uint32_t i = first_row_to_compare; i < num_rows_to_compare; ++i) {
251245
uint32_t irow_left = use_selection ? sel_left_maybe_null[i] : i;
252246
uint32_t irow_right = left_to_right_map[irow_left];

0 commit comments

Comments
 (0)