blob: fcac61535b8fb08d28f67903a83766e5afaabc2c [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include <lz4.h>
#include "common/init.h"
#include "testutil/gtest-util.h"
#include "runtime/collection-value.h"
#include "runtime/collection-value-builder.h"
#include "runtime/mem-tracker.h"
#include "runtime/raw-value.h"
#include "runtime/raw-value.inline.h"
#include "runtime/row-batch.h"
#include "runtime/tuple-row.h"
#include "service/fe-support.h"
#include "service/frontend.h"
#include "util/stopwatch.h"
#include "testutil/desc-tbl-builder.h"
#include "common/names.h"
using namespace impala;
namespace impala {
const int NUM_ROWS = 20;
const int MAX_STRING_LEN = 10;
const int MAX_ARRAY_LEN = 3;
const int NULL_VALUE_PERCENT = 10;
class RowBatchSerializeTest : public testing::Test {
protected:
ObjectPool pool_;
scoped_ptr<MemTracker> tracker_;
// For computing tuple mem layouts.
scoped_ptr<Frontend> fe_;
virtual void SetUp() {
fe_.reset(new Frontend());
tracker_.reset(new MemTracker());
}
virtual void TearDown() {
pool_.Clear();
tracker_.reset();
fe_.reset();
}
// Serializes and deserializes 'batch', then checks that the deserialized batch is valid
// and has the same contents as 'batch'. If serialization returns an error (e.g. if the
// row batch is too large to serialize), this will return that error.
Status TestRowBatchInternal(const RowDescriptor& row_desc, RowBatch* batch,
bool print_batches, bool full_dedup = false) {
if (print_batches) cout << PrintBatch(batch) << endl;
TRowBatch trow_batch;
RETURN_IF_ERROR(batch->Serialize(&trow_batch, full_dedup));
RowBatch deserialized_batch(&row_desc, trow_batch, tracker_.get());
if (print_batches) cout << PrintBatch(&deserialized_batch) << endl;
EXPECT_EQ(batch->num_rows(), deserialized_batch.num_rows());
for (int row_idx = 0; row_idx < batch->num_rows(); ++row_idx) {
TupleRow* row = batch->GetRow(row_idx);
TupleRow* deserialized_row = deserialized_batch.GetRow(row_idx);
for (int tuple_idx = 0; tuple_idx < row_desc.tuple_descriptors().size(); ++tuple_idx) {
TupleDescriptor* tuple_desc = row_desc.tuple_descriptors()[tuple_idx];
Tuple* tuple = row->GetTuple(tuple_idx);
Tuple* deserialized_tuple = deserialized_row->GetTuple(tuple_idx);
TestTuplesEqual(*tuple_desc, tuple, deserialized_tuple);
}
}
return Status::OK();
}
// Serializes and deserializes 'batch', then checks that the deserialized batch is valid
// and has the same contents as 'batch'. This requires that serialization succeed.
void TestRowBatch(const RowDescriptor& row_desc, RowBatch* batch, bool print_batches,
bool full_dedup = false) {
EXPECT_OK(TestRowBatchInternal(row_desc, batch, print_batches, full_dedup));
}
// Construct a RowBatch with the specified size by creating a single row with
// multiple strings, then test whether this RowBatch can be serialized and
// deserialized successfully. If there is an error during serialization,
// return that error.
Status TestRowBatchLimits(int64_t row_batch_size) {
// tuple: (int, string, string, string)
// This uses three strings so that this test can reach INT_MAX+1 without any
// single string exceeding the 1GB limit on string length (see string-value.h).
DescriptorTblBuilder builder(fe_.get(), &pool_);
builder.DeclareTuple() << TYPE_INT << TYPE_STRING << TYPE_STRING << TYPE_STRING;
DescriptorTbl* desc_tbl = builder.Build();
// Create row descriptor
vector<bool> nullable_tuples(1, false);
vector<TTupleId> tuple_id(1, (TTupleId) 0);
RowDescriptor row_desc(*desc_tbl, tuple_id, nullable_tuples);
EXPECT_EQ(row_desc.tuple_descriptors().size(), 1);
// Create base row
RowBatch* batch = pool_.Add(new RowBatch(&row_desc, 1, tracker_.get()));
int len = row_desc.GetRowSize();
uint8_t* tuple_mem = batch->tuple_data_pool()->Allocate(len);
memset(tuple_mem, 0, len);
// Create one row
TupleRow* row = batch->GetRow(batch->AddRow());
// There is only one TupleDescriptor
TupleDescriptor* tuple_desc = row_desc.tuple_descriptors()[0];
Tuple* tuple = reinterpret_cast<Tuple*>(tuple_mem);
// Write slot 0 (Random Integer)
SlotDescriptor* int_desc = tuple_desc->slots()[0];
WriteValue(tuple, *int_desc, batch->tuple_data_pool());
// The RowBatch has consumed 'len' bytes so far. Need to add row_batch_size - len
// bytes of string data. Split this equally among the three strings with any
// remainder going to the first string.
int64_t size_remaining = row_batch_size - len;
int64_t string1_size = (size_remaining / 3) + (size_remaining % 3);
int64_t string2_size = (size_remaining / 3);
int64_t string3_size = string2_size;
// Write string #1
SlotDescriptor* string1_desc = tuple_desc->slots()[1];
string string1(string1_size, 'a');
StringValue sv1(string1);
RawValue::Write(&sv1, tuple, string1_desc, batch->tuple_data_pool());
// Write string #2
SlotDescriptor* string2_desc = tuple_desc->slots()[2];
string string2(string2_size, 'a');
StringValue sv2(string2);
RawValue::Write(&sv2, tuple, string2_desc, batch->tuple_data_pool());
// Write string #3
SlotDescriptor* string3_desc = tuple_desc->slots()[3];
string string3(string3_size, 'a');
StringValue sv3(string3);
RawValue::Write(&sv3, tuple, string3_desc, batch->tuple_data_pool());
// Done with this row
row->SetTuple(0, tuple);
batch->CommitLastRow();
// See if this RowBatch can be serialized and deserialized
return TestRowBatchInternal(row_desc, batch, false);
}
// Recursively checks that 'deserialized_tuple' is valid and has the same contents as
// 'tuple'. 'deserialized_tuple' should be the result of serializing then deserializing
// 'tuple'.
void TestTuplesEqual(const TupleDescriptor& tuple_desc, Tuple* tuple,
Tuple* deserialized_tuple) {
if (tuple_desc.byte_size() == 0) return;
if (tuple == NULL) {
EXPECT_TRUE(deserialized_tuple == NULL);
return;
}
EXPECT_FALSE(deserialized_tuple == NULL);
for (int slot_idx = 0; slot_idx < tuple_desc.slots().size(); ++slot_idx) {
SlotDescriptor* slot_desc = tuple_desc.slots()[slot_idx];
if (tuple->IsNull(slot_desc->null_indicator_offset())) {
EXPECT_TRUE(deserialized_tuple->IsNull(slot_desc->null_indicator_offset()));
continue;
}
EXPECT_FALSE(deserialized_tuple->IsNull(slot_desc->null_indicator_offset()));
const ColumnType& type = slot_desc->type();
void* slot = tuple->GetSlot(slot_desc->tuple_offset());
void* deserialized_slot = deserialized_tuple->GetSlot(slot_desc->tuple_offset());
if (!type.IsCollectionType()) {
EXPECT_TRUE(RawValue::Eq(slot, deserialized_slot, type));
}
if (type.IsStringType()) {
// Check that serialized and deserialized string values have different pointers.
StringValue* string_value = reinterpret_cast<StringValue*>(slot);
StringValue* deserialized_string_value =
reinterpret_cast<StringValue*>(deserialized_slot);
EXPECT_NE(string_value->ptr, deserialized_string_value->ptr);
}
if (type.IsCollectionType()) {
const TupleDescriptor& item_desc = *slot_desc->collection_item_descriptor();
CollectionValue* coll_value = reinterpret_cast<CollectionValue*>(slot);
CollectionValue* deserialized_coll_value =
reinterpret_cast<CollectionValue*>(deserialized_slot);
EXPECT_EQ(coll_value->num_tuples, deserialized_coll_value->num_tuples);
uint8_t* coll_data = coll_value->ptr;
uint8_t* deserialized_coll_data = deserialized_coll_value->ptr;
for (int i = 0; i < coll_value->num_tuples; ++i) {
TestTuplesEqual(item_desc, reinterpret_cast<Tuple*>(coll_data),
reinterpret_cast<Tuple*>(deserialized_coll_data));
coll_data += item_desc.byte_size();
deserialized_coll_data += item_desc.byte_size();
}
// Check that collection values have different pointers
EXPECT_NE(coll_value->ptr, deserialized_coll_value->ptr);
}
}
}
// Writes a randomized value in 'tuple'.
void WriteValue(Tuple* tuple, const SlotDescriptor& slot_desc, MemPool* pool) {
switch (slot_desc.type().type) {
case TYPE_INT: {
int val = rand();
RawValue::Write(&val, tuple, &slot_desc, pool);
break;
}
case TYPE_STRING: {
// Via http://stackoverflow.com/questions/440133/
// how-do-i-create-a-random-alpha-numeric-string-in-c
static const char chars[] =
"0123456789"
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz";
int len = rand() % (MAX_STRING_LEN + 1);
char buf[MAX_STRING_LEN];
for (int i = 0; i < len; ++i) {
buf[i] = chars[rand() % (sizeof(chars) - 1)];
}
StringValue sv(&buf[0], len);
RawValue::Write(&sv, tuple, &slot_desc, pool);
break;
}
case TYPE_ARRAY: {
const TupleDescriptor* item_desc = slot_desc.collection_item_descriptor();
int array_len = rand() % (MAX_ARRAY_LEN + 1);
CollectionValue cv;
CollectionValueBuilder builder(&cv, *item_desc, pool, NULL, array_len);
Tuple* tuple_mem;
int n;
EXPECT_OK(builder.GetFreeMemory(&tuple_mem, &n));
ASSERT_GE(n, array_len);
memset(tuple_mem, 0, item_desc->byte_size() * array_len);
for (int i = 0; i < array_len; ++i) {
for (int slot_idx = 0; slot_idx < item_desc->slots().size(); ++slot_idx) {
SlotDescriptor* item_slot_desc = item_desc->slots()[slot_idx];
WriteValue(tuple_mem, *item_slot_desc, pool);
}
tuple_mem += item_desc->byte_size();
}
builder.CommitTuples(array_len);
// Array data already lives in 'pool'
RawValue::Write(&cv, tuple, &slot_desc, NULL);
break;
}
default:
ASSERT_TRUE(false) << "NYI: " << slot_desc.type().DebugString();
}
}
// Creates a row batch with randomized values.
RowBatch* CreateRowBatch(const RowDescriptor& row_desc) {
RowBatch* batch = pool_.Add(new RowBatch(&row_desc, NUM_ROWS, tracker_.get()));
int len = row_desc.GetRowSize() * NUM_ROWS;
uint8_t* tuple_mem = batch->tuple_data_pool()->Allocate(len);
memset(tuple_mem, 0, len);
for (int i = 0; i < NUM_ROWS; ++i) {
TupleRow* row = batch->GetRow(batch->AddRow());
for (int tuple_idx = 0; tuple_idx < row_desc.tuple_descriptors().size(); ++tuple_idx) {
TupleDescriptor* tuple_desc = row_desc.tuple_descriptors()[tuple_idx];
// Allocating zero-length tuples in this way means that multiple tuples can have
// same address: an important corner case to test.
Tuple* tuple = reinterpret_cast<Tuple*>(tuple_mem);
for (int slot_idx = 0; slot_idx < tuple_desc->slots().size(); ++slot_idx) {
SlotDescriptor* slot_desc = tuple_desc->slots()[slot_idx];
if (rand() % 100 < NULL_VALUE_PERCENT) {
tuple->SetNull(slot_desc->null_indicator_offset());
} else {
WriteValue(tuple, *slot_desc, batch->tuple_data_pool());
}
}
row->SetTuple(tuple_idx, tuple);
tuple_mem += tuple_desc->byte_size();
}
batch->CommitLastRow();
}
return batch;
}
// Generate num_tuples distinct tuples with randomized values.
void CreateTuples(const TupleDescriptor& tuple_desc, MemPool* pool,
int num_tuples, int null_tuple_percent, int null_value_percent,
vector<Tuple*>* result) {
uint8_t* tuple_mem = pool->Allocate(tuple_desc.byte_size() * num_tuples);
memset(tuple_mem, 0, tuple_desc.byte_size() * num_tuples);
for (int i = 0; i < num_tuples; ++i) {
if (null_tuple_percent > 0 && rand() % 100 < null_tuple_percent) {
result->push_back(NULL);
continue;
}
Tuple* tuple = reinterpret_cast<Tuple*>(tuple_mem);
for (int slot_idx = 0; slot_idx < tuple_desc.slots().size(); ++slot_idx) {
SlotDescriptor* slot_desc = tuple_desc.slots()[slot_idx];
if (null_value_percent > 0 && rand() % 100 < null_value_percent) {
tuple->SetNull(slot_desc->null_indicator_offset());
} else {
WriteValue(tuple, *slot_desc, pool);
}
}
result->push_back(tuple);
tuple_mem += tuple_desc.byte_size();
}
}
// Create a row batch from preconstructed tuples. Each tuple instance for tuple i
// is consecutively repeated repeats[i] times. The tuple instances are used in the
// order provided, starting at the beginning once all are used.
void AddTuplesToRowBatch(int num_rows, const vector<vector<Tuple*>>& tuples,
const vector<int>& repeats, RowBatch* batch) {
int tuples_per_row = batch->row_desc()->tuple_descriptors().size();
ASSERT_EQ(tuples_per_row, tuples.size());
ASSERT_EQ(tuples_per_row, repeats.size());
vector<int> next_tuple(tuples_per_row, 0);
for (int i = 0; i < num_rows; ++i) {
int idx = batch->AddRow();
TupleRow* row = batch->GetRow(idx);
for (int tuple_idx = 0; tuple_idx < tuples_per_row; ++tuple_idx) {
int curr_tuple = next_tuple[tuple_idx];
ASSERT_GT(tuples[tuple_idx].size(), 0);
row->SetTuple(tuple_idx, tuples[tuple_idx][curr_tuple]);
if ((i + 1) % repeats[tuple_idx] == 0) {
next_tuple[tuple_idx] = (curr_tuple + 1) % tuples[tuple_idx].size();
}
}
batch->CommitLastRow();
}
}
// Helper to build a row batch with only one tuple per row.
void AddTuplesToRowBatch(int num_rows, const vector<Tuple*>& tuples, int repeats,
RowBatch* batch) {
vector<vector<Tuple*>> tmp_tuples(1, tuples);
vector<int> tmp_repeats(1, repeats);
AddTuplesToRowBatch(num_rows, tmp_tuples, tmp_repeats, batch);
}
// Helper to access internal state of batch (this class is friend of RowBatch).
bool UseFullDedup(RowBatch* batch) { return batch->UseFullDedup(); }
void TestDupCorrectness(bool full_dedup);
void TestDupRemoval(bool full_dedup);
void TestConsecutiveNulls(bool full_dedup);
void TestZeroLengthTuple(bool full_dedup);
};
TEST_F(RowBatchSerializeTest, Basic) {
// tuple: (int)
DescriptorTblBuilder builder(fe_.get(), &pool_);
builder.DeclareTuple() << TYPE_INT;
DescriptorTbl* desc_tbl = builder.Build();
vector<bool> nullable_tuples(1, false);
vector<TTupleId> tuple_id(1, (TTupleId) 0);
RowDescriptor row_desc(*desc_tbl, tuple_id, nullable_tuples);
ASSERT_EQ(row_desc.tuple_descriptors().size(), 1);
RowBatch* batch = CreateRowBatch(row_desc);
TestRowBatch(row_desc, batch, true);
}
TEST_F(RowBatchSerializeTest, String) {
// tuple: (int, string)
DescriptorTblBuilder builder(fe_.get(), &pool_);
builder.DeclareTuple() << TYPE_INT << TYPE_STRING;
DescriptorTbl* desc_tbl = builder.Build();
vector<bool> nullable_tuples(1, false);
vector<TTupleId> tuple_id(1, (TTupleId) 0);
RowDescriptor row_desc(*desc_tbl, tuple_id, nullable_tuples);
ASSERT_EQ(row_desc.tuple_descriptors().size(), 1);
RowBatch* batch = CreateRowBatch(row_desc);
TestRowBatch(row_desc, batch, true);
}
TEST_F(RowBatchSerializeTest, RowBatchLZ4Success) {
// Inputs up to LZ4_MAX_INPUT_SIZE (0x7E000000) should work
Status status = TestRowBatchLimits(LZ4_MAX_INPUT_SIZE);
cout << status.GetDetail() << endl;
EXPECT_OK(status);
}
TEST_F(RowBatchSerializeTest, RowBatchLZ4TooLarge) {
// Inputs with size LZ4_MAX_INPUT_SIZE + 1 through INT_MAX should get an error from LZ4
Status status;
status = TestRowBatchLimits(LZ4_MAX_INPUT_SIZE + 1);
cout << status.GetDetail() << endl;
EXPECT_EQ(status.code(), TErrorCode::LZ4_COMPRESSION_INPUT_TOO_LARGE);
status = TestRowBatchLimits(INT_MAX);
cout << status.GetDetail() << endl;
EXPECT_EQ(status.code(), TErrorCode::LZ4_COMPRESSION_INPUT_TOO_LARGE);
}
TEST_F(RowBatchSerializeTest, RowBatchTooLarge) {
// RowBatches with size > INT_MAX cannot be serialized
Status status;
status = TestRowBatchLimits(static_cast<int64_t>(INT_MAX) + 1);
cout << status.GetDetail() << endl;
EXPECT_EQ(status.code(), TErrorCode::ROW_BATCH_TOO_LARGE);
}
TEST_F(RowBatchSerializeTest, BasicArray) {
// tuple: (int, string, array<int>)
ColumnType array_type;
array_type.type = TYPE_ARRAY;
array_type.children.push_back(TYPE_INT);
DescriptorTblBuilder builder(fe_.get(), &pool_);
builder.DeclareTuple() << TYPE_INT << TYPE_STRING << array_type;
DescriptorTbl* desc_tbl = builder.Build();
vector<bool> nullable_tuples(1, false);
vector<TTupleId> tuple_id(1, (TTupleId) 0);
RowDescriptor row_desc(*desc_tbl, tuple_id, nullable_tuples);
ASSERT_EQ(row_desc.tuple_descriptors().size(), 1);
RowBatch* batch = CreateRowBatch(row_desc);
TestRowBatch(row_desc, batch, true);
}
TEST_F(RowBatchSerializeTest, StringArray) {
// tuple: (int, string, array<struct<int, string, string>>)
ColumnType struct_type;
struct_type.type = TYPE_STRUCT;
struct_type.children.push_back(TYPE_INT);
struct_type.field_names.push_back("int1");
struct_type.children.push_back(TYPE_STRING);
struct_type.field_names.push_back("string1");
struct_type.children.push_back(TYPE_STRING);
struct_type.field_names.push_back("string2");
ColumnType array_type;
array_type.type = TYPE_ARRAY;
array_type.children.push_back(struct_type);
DescriptorTblBuilder builder(fe_.get(), &pool_);
builder.DeclareTuple() << TYPE_INT << TYPE_STRING << array_type;
DescriptorTbl* desc_tbl = builder.Build();
vector<bool> nullable_tuples(1, false);
vector<TTupleId> tuple_id(1, (TTupleId) 0);
RowDescriptor row_desc(*desc_tbl, tuple_id, nullable_tuples);
ASSERT_EQ(row_desc.tuple_descriptors().size(), 1);
RowBatch* batch = CreateRowBatch(row_desc);
TestRowBatch(row_desc, batch, true);
}
TEST_F(RowBatchSerializeTest, NestedArrays) {
// tuple: (array<struct<array<string>, array<string, string>>>)
ColumnType inner_array_type1;
inner_array_type1.type = TYPE_ARRAY;
inner_array_type1.children.push_back(TYPE_STRING);
ColumnType inner_struct_type;
inner_struct_type.type = TYPE_STRUCT;
inner_struct_type.children.push_back(TYPE_STRING);
inner_struct_type.field_names.push_back("string1");
inner_struct_type.children.push_back(TYPE_STRING);
inner_struct_type.field_names.push_back("string2");
ColumnType inner_array_type2;
inner_array_type2.type = TYPE_ARRAY;
inner_array_type2.children.push_back(inner_struct_type);
ColumnType struct_type;
struct_type.type = TYPE_STRUCT;
struct_type.children.push_back(inner_array_type1);
struct_type.field_names.push_back("array1");
struct_type.children.push_back(inner_array_type2);
struct_type.field_names.push_back("array2");
ColumnType array_type;
array_type.type = TYPE_ARRAY;
array_type.children.push_back(struct_type);
DescriptorTblBuilder builder(fe_.get(), &pool_);
builder.DeclareTuple() << array_type;
DescriptorTbl* desc_tbl = builder.Build();
vector<bool> nullable_tuples(1, false);
vector<TTupleId> tuple_id(1, (TTupleId) 0);
RowDescriptor row_desc(*desc_tbl, tuple_id, nullable_tuples);
ASSERT_EQ(row_desc.tuple_descriptors().size(), 1);
RowBatch* batch = CreateRowBatch(row_desc);
TestRowBatch(row_desc, batch, true);
}
// Test that we get correct result when serializing/deserializing with duplicates
TEST_F(RowBatchSerializeTest, DupCorrectnessAdjacent) {
TestDupCorrectness(false);
}
TEST_F(RowBatchSerializeTest, DupCorrectnessFull) {
TestDupCorrectness(true);
}
void RowBatchSerializeTest::TestDupCorrectness(bool full_dedup) {
// tuples: (int), (string)
DescriptorTblBuilder builder(fe_.get(), &pool_);
builder.DeclareTuple() << TYPE_INT;
builder.DeclareTuple() << TYPE_STRING;
DescriptorTbl* desc_tbl = builder.Build();
vector<bool> nullable_tuples(2, false);
vector<TTupleId> tuple_id;
tuple_id.push_back((TTupleId) 0);
tuple_id.push_back((TTupleId) 1);
RowDescriptor row_desc(*desc_tbl, tuple_id, nullable_tuples);
ASSERT_EQ(row_desc.tuple_descriptors().size(), 2);
int num_rows = 1000;
int distinct_int_tuples = 100;
int distinct_string_tuples = 100;
vector<int> repeats;
// All int dups are non-consecutive
repeats.push_back(1);
// All string dups are consecutive
repeats.push_back(num_rows / distinct_string_tuples + 1);
RowBatch* batch = pool_.Add(new RowBatch(&row_desc, num_rows, tracker_.get()));
vector<vector<Tuple*>> distinct_tuples(2);
CreateTuples(*row_desc.tuple_descriptors()[0], batch->tuple_data_pool(),
distinct_int_tuples, 0, 10, distinct_tuples.data());
CreateTuples(*row_desc.tuple_descriptors()[1], batch->tuple_data_pool(),
distinct_string_tuples, 0, 10, &distinct_tuples[1]);
AddTuplesToRowBatch(num_rows, distinct_tuples, repeats, batch);
TestRowBatch(row_desc, batch, false, full_dedup);
}
TEST_F(RowBatchSerializeTest, DupRemovalAdjacent) {
TestDupRemoval(false);
}
TEST_F(RowBatchSerializeTest, DupRemovalFull) {
TestDupRemoval(true);
}
// Test that tuple deduplication results in the expected reduction in serialized size.
void RowBatchSerializeTest::TestDupRemoval(bool full_dedup) {
// tuples: (int, string)
DescriptorTblBuilder builder(fe_.get(), &pool_);
builder.DeclareTuple() << TYPE_INT << TYPE_STRING;
DescriptorTbl* desc_tbl = builder.Build();
vector<bool> nullable_tuples(1, false);
vector<TTupleId> tuple_id(1, (TTupleId) 0);
RowDescriptor row_desc(*desc_tbl, tuple_id, nullable_tuples);
TupleDescriptor& tuple_desc = *row_desc.tuple_descriptors()[0];
int num_rows = 1000;
int num_distinct_tuples = 100;
// All dups are consecutive
int repeats = num_rows / num_distinct_tuples;
RowBatch* batch = pool_.Add(new RowBatch(&row_desc, num_rows, tracker_.get()));
vector<Tuple*> tuples;
CreateTuples(tuple_desc, batch->tuple_data_pool(), num_distinct_tuples, 0, 10, &tuples);
AddTuplesToRowBatch(num_rows, tuples, repeats, batch);
TRowBatch trow_batch;
EXPECT_OK(batch->Serialize(&trow_batch, full_dedup));
// Serialized data should only have one copy of each tuple.
int64_t total_byte_size = 0; // Total size without duplication
for (int i = 0; i < tuples.size(); ++i) {
total_byte_size += tuples[i]->TotalByteSize(tuple_desc);
}
EXPECT_EQ(total_byte_size, trow_batch.uncompressed_size);
TestRowBatch(row_desc, batch, false, full_dedup);
}
TEST_F(RowBatchSerializeTest, ConsecutiveNullsAdjacent) {
TestConsecutiveNulls(false);
}
TEST_F(RowBatchSerializeTest, ConsecutiveNullsFull) {
TestConsecutiveNulls(true);
}
// Test that deduplication handles NULL tuples correctly.
void RowBatchSerializeTest::TestConsecutiveNulls(bool full_dedup) {
// tuples: (int)
DescriptorTblBuilder builder(fe_.get(), &pool_);
builder.DeclareTuple() << TYPE_INT;
DescriptorTbl* desc_tbl = builder.Build();
vector<bool> nullable_tuples(1, true);
vector<TTupleId> tuple_id(1, (TTupleId) 0);
RowDescriptor row_desc(*desc_tbl, tuple_id, nullable_tuples);
int num_rows = 100;
int num_distinct_tuples = 20;
int repeats = 5;
RowBatch* batch = pool_.Add(new RowBatch(&row_desc, num_rows, tracker_.get()));
vector<Tuple*> tuples;
CreateTuples(*row_desc.tuple_descriptors()[0], batch->tuple_data_pool(),
num_distinct_tuples, 50, 10, &tuples);
AddTuplesToRowBatch(num_rows, tuples, repeats, batch);
TestRowBatch(row_desc, batch, false, full_dedup);
}
TEST_F(RowBatchSerializeTest, ZeroLengthTuples) {
TestZeroLengthTuple(false);
}
TEST_F(RowBatchSerializeTest, ZeroLengthTuplesDedup) {
TestZeroLengthTuple(true);
}
void RowBatchSerializeTest::TestZeroLengthTuple(bool full_dedup) {
// tuples: (int), (string), ()
DescriptorTblBuilder builder(fe_.get(), &pool_);
builder.DeclareTuple() << TYPE_INT;
builder.DeclareTuple() << TYPE_STRING;
builder.DeclareTuple();
DescriptorTbl* desc_tbl = builder.Build();
vector<bool> nullable_tuples(3, false);
vector<TTupleId> tuple_ids;
tuple_ids.push_back((TTupleId) 0);
tuple_ids.push_back((TTupleId) 1);
tuple_ids.push_back((TTupleId) 2);
RowDescriptor row_desc(*desc_tbl, tuple_ids, nullable_tuples);
RowBatch* batch = CreateRowBatch(row_desc);
TestRowBatch(row_desc, batch, false, full_dedup);
}
// Test a pathological case for consecutive deduplication - two large alternating tuples.
// This tests that we are capable of duplicating non-adjacent tuples to produce a compact
// serialized batch with no duplication. It also stresses the serialization logic to
// ensure that it can handle batches with large degrees of duplication without hitting
// any runtime errors.
TEST_F(RowBatchSerializeTest, DedupPathologicalFull) {
// tuples: (int, int array<(string)>)
// Need 3 tuples + array to enable non-adjacent dedup automatically
ColumnType array_type;
array_type.type = TYPE_ARRAY;
array_type.children.push_back(TYPE_STRING);
DescriptorTblBuilder builder(fe_.get(), &pool_);
builder.DeclareTuple() << TYPE_INT;
builder.DeclareTuple() << TYPE_INT;
builder.DeclareTuple() << array_type;
DescriptorTbl* desc_tbl = builder.Build();
vector<bool> nullable_tuples(3, true);
vector<TTupleId> tuple_id;
tuple_id.push_back((TTupleId) 0);
tuple_id.push_back((TTupleId) 1);
tuple_id.push_back((TTupleId) 2);
RowDescriptor row_desc(*desc_tbl, tuple_id, nullable_tuples);
const int num_tuples = 3;
const int num_int_tuples = 2;
const int array_tuple_idx = 2;
int huge_string_size = INT_MAX / 256;
int num_rows = 100000; // Without deduplication this will be gigantic
int num_distinct_array_tuples = 2;
// Build two tuples with huge strings in them
string huge_string;
LOG(INFO) << "Try to resize to " << huge_string_size;
huge_string.resize(huge_string_size, 'z');
vector<vector<Tuple*>> tuples(num_tuples);
vector<int> repeats(num_tuples, 1); // Don't repeat tuples adjacently
int64_t total_byte_size = 0;
RowBatch* batch = pool_.Add(new RowBatch(&row_desc, num_rows, tracker_.get()));
// First two tuples are integers because it doesn't matter for this test.
for (int tuple_idx = 0; tuple_idx < num_int_tuples; ++tuple_idx) {
TupleDescriptor* tuple_desc = row_desc.tuple_descriptors()[tuple_idx];
CreateTuples(*tuple_desc, batch->tuple_data_pool(), num_rows, 0, false,
&tuples[tuple_idx]);
total_byte_size += tuple_desc->byte_size() * num_rows;
}
// The last tuple is a duplicated array with a large string inside.
const TupleDescriptor* array_tuple_desc = row_desc.tuple_descriptors()[array_tuple_idx];
const SlotDescriptor* array_slot_desc = array_tuple_desc->slots()[0];
const TupleDescriptor* array_item_desc = array_slot_desc->collection_item_descriptor();
const SlotDescriptor* string_slot_desc = array_item_desc->slots()[0];
MemPool* pool = batch->tuple_data_pool();
for (int i = 0; i < num_distinct_array_tuples; ++i) {
uint8_t* tuple_mem = pool->Allocate(array_tuple_desc->byte_size());
memset(tuple_mem, 0, array_tuple_desc->byte_size());
Tuple* tuple = reinterpret_cast<Tuple*>(tuple_mem);
CollectionValue cv;
cv.ptr = pool->Allocate(array_item_desc->byte_size());
memset(cv.ptr, 0, array_item_desc->byte_size());
cv.num_tuples = 1;
StringValue huge_string_value(
const_cast<char*>(huge_string.data()), huge_string_size);
RawValue::Write(&huge_string_value, reinterpret_cast<Tuple*>(cv.ptr),
string_slot_desc, NULL);
RawValue::Write(&cv, tuple, array_slot_desc, NULL);
tuples[array_tuple_idx].push_back(tuple);
total_byte_size += array_tuple_desc->byte_size() + cv.ByteSize(*array_item_desc) +
huge_string_size;
}
LOG(INFO) << "Building row batch";
AddTuplesToRowBatch(num_rows, tuples, repeats, batch);
// Full dedup should be automatically enabled because of row batch structure.
EXPECT_TRUE(UseFullDedup(batch));
LOG(INFO) << "Serializing row batch";
TRowBatch trow_batch;
EXPECT_OK(batch->Serialize(&trow_batch));
LOG(INFO) << "Serialized batch size: " << trow_batch.tuple_data.size();
LOG(INFO) << "Serialized batch uncompressed size: " << trow_batch.uncompressed_size;
LOG(INFO) << "Serialized batch expected size: " << total_byte_size;
// Serialized data should only have one copy of each tuple.
EXPECT_EQ(total_byte_size, trow_batch.uncompressed_size);
LOG(INFO) << "Deserializing row batch";
RowBatch deserialized_batch(&row_desc, trow_batch, tracker_.get());
LOG(INFO) << "Verifying row batch";
// Need to do special verification: comparing all duplicate strings is too slow.
EXPECT_EQ(batch->num_rows(), deserialized_batch.num_rows());
for (int row_idx = 0; row_idx < batch->num_rows(); ++row_idx) {
for (int tuple_idx = 0; tuple_idx < num_tuples; ++tuple_idx) {
TupleDescriptor* tuple_desc = row_desc.tuple_descriptors()[tuple_idx];
Tuple* tuple = batch->GetRow(row_idx)->GetTuple(tuple_idx);
Tuple* deserialized_tuple = deserialized_batch.GetRow(row_idx)->GetTuple(tuple_idx);
if (tuple_idx != array_tuple_idx || row_idx < tuples.size()) {
// Compare tuples directly
TestTuplesEqual(*tuple_desc, tuple, deserialized_tuple);
} else {
// This should just be a repeat of a previous tuple - avoid comparison.
int prev_dup_idx = row_idx - num_distinct_array_tuples;
EXPECT_EQ(deserialized_batch.GetRow(prev_dup_idx)->GetTuple(tuple_idx),
deserialized_tuple);
}
}
}
}
}