blob: e22363df5e70d09f34b2ded87ab17be5d7b36ec2 [file]
// 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 <sstream>
#include <boost/functional/hash.hpp>
#include "kudu/gutil/strings/escaping.h"
#include "runtime/collection-value.h"
#include "runtime/date-value.h"
#include "runtime/raw-value.h"
#include "runtime/raw-value.inline.h"
#include "runtime/string-value.inline.h"
#include "runtime/tuple.h"
#include "udf/udf-internal.h"
#include "util/ubsan.h"
#include "common/names.h"
namespace impala {
using impala_udf::StructVal;
const int RawValue::ASCII_PRECISION;
constexpr double RawValue::CANONICAL_DOUBLE_NAN;
constexpr float RawValue::CANONICAL_FLOAT_NAN;
constexpr double RawValue::CANONICAL_DOUBLE_ZERO;
constexpr float RawValue::CANONICAL_FLOAT_ZERO;
void RawValue::PrintValueAsBytes(const void* value, const ColumnType& type,
stringstream* stream) {
if (value == NULL) return;
const char* chars = reinterpret_cast<const char*>(value);
const StringValue* string_val = NULL;
switch (type.type) {
case TYPE_BOOLEAN:
stream->write(chars, sizeof(bool));
return;
case TYPE_TINYINT:
stream->write(chars, sizeof(int8_t));
break;
case TYPE_SMALLINT:
stream->write(chars, sizeof(int16_t));
break;
case TYPE_INT:
stream->write(chars, sizeof(int32_t));
break;
case TYPE_DATE:
stream->write(chars, sizeof(DateValue));
break;
case TYPE_BIGINT:
stream->write(chars, sizeof(int64_t));
break;
case TYPE_FLOAT:
stream->write(chars, sizeof(float));
break;
case TYPE_DOUBLE:
stream->write(chars, sizeof(double));
break;
case TYPE_STRING:
case TYPE_VARCHAR:
string_val = reinterpret_cast<const StringValue*>(value);
stream->write(string_val->Ptr(), string_val->Len());
break;
case TYPE_TIMESTAMP:
stream->write(chars, TimestampValue::Size());
break;
case TYPE_CHAR:
stream->write(chars, type.len);
break;
case TYPE_DECIMAL:
stream->write(chars, type.GetByteSize());
break;
default:
DCHECK(false) << "bad RawValue::PrintValue() type: " << type.DebugString();
}
}
void RawValue::PrintValue(const void* value, const ColumnType& type, int scale,
string* str) {
if (value == NULL) {
*str = NullLiteral(true);
return;
}
const StringValue* string_val = NULL;
bool val;
string tmp;
// Special case types that we can print more efficiently without using a stringstream
switch (type.type) {
case TYPE_BOOLEAN:
val = *reinterpret_cast<const bool*>(value);
*str = (val ? "true" : "false");
return;
case TYPE_STRING:
case TYPE_VARCHAR:
string_val = reinterpret_cast<const StringValue*>(value);
tmp.assign(string_val->Ptr(), string_val->Len());
str->swap(tmp);
return;
case TYPE_CHAR:
*str = string(reinterpret_cast<const char*>(value), type.len);
return;
case TYPE_TIMESTAMP:
*str = reinterpret_cast<const TimestampValue*>(value)->ToString();
return;
case TYPE_DATE:
*str = reinterpret_cast<const DateValue*>(value)->ToString();
return;
default:
break;
}
stringstream out;
out.precision(ASCII_PRECISION);
PrintValue(value, type, scale, &out);
*str = out.str();
}
void RawValue::WriteNonNullPrimitive(const void* value, void* dst, const ColumnType& type,
MemPool* pool) {
DCHECK(value != NULL);
switch (type.type) {
case TYPE_NULL:
break;
case TYPE_BOOLEAN:
// Unlike the other scalar types, bool has a limited set of valid values, so if
// 'dst' is uninitialized memory and happens to point to a value that is not a valid
// bool, then dereferencing it via *reinterpret_cast<bool*>(dst) is undefined
// behavior.
memcpy(dst, value, sizeof(bool));
break;
case TYPE_TINYINT:
*reinterpret_cast<int8_t*>(dst) = *reinterpret_cast<const int8_t*>(value);
break;
case TYPE_SMALLINT:
*reinterpret_cast<int16_t*>(dst) = *reinterpret_cast<const int16_t*>(value);
break;
case TYPE_INT:
*reinterpret_cast<int32_t*>(dst) = *reinterpret_cast<const int32_t*>(value);
break;
case TYPE_DATE:
*reinterpret_cast<DateValue*>(dst) = *reinterpret_cast<const DateValue*>(value);
break;
case TYPE_BIGINT:
*reinterpret_cast<int64_t*>(dst) = *reinterpret_cast<const int64_t*>(value);
break;
case TYPE_FLOAT:
*reinterpret_cast<float*>(dst) = *reinterpret_cast<const float*>(value);
break;
case TYPE_DOUBLE:
*reinterpret_cast<double*>(dst) = *reinterpret_cast<const double*>(value);
break;
case TYPE_TIMESTAMP:
*reinterpret_cast<TimestampValue*>(dst) =
*reinterpret_cast<const TimestampValue*>(value);
break;
case TYPE_STRING:
case TYPE_VARCHAR: {
const StringValue* src = reinterpret_cast<const StringValue*>(value);
StringValue* dest = reinterpret_cast<StringValue*>(dst);
dest->Assign(*src);
if (type.type == TYPE_VARCHAR) DCHECK_LE(dest->Len(), type.len);
if (pool != NULL) {
// Note: if this changes to TryAllocate(), SlotDescriptor::CodegenWriteToSlot()
// will need to reflect this change as well (the codegen'd Allocate() call is
// actually generated in SlotDescriptor::CodegenWriteStringOrCollectionToSlot()).
dest->Assign(reinterpret_cast<char*>(pool->Allocate(dest->Len())), dest->Len());
Ubsan::MemCpy(dest->Ptr(), src->Ptr(), dest->Len());
}
break;
}
case TYPE_CHAR:
DCHECK_EQ(type.type, TYPE_CHAR);
memcpy(dst, value, type.len);
break;
case TYPE_DECIMAL:
memcpy(dst, value, type.GetByteSize());
break;
case TYPE_ARRAY:
case TYPE_MAP: {
// Collections should be handled by a different Write() function within this class.
DCHECK(false);
break;
}
case TYPE_STRUCT: {
// Structs should be handled by a different Write() function within this class.
DCHECK(false);
break;
}
default:
DCHECK(false) << "RawValue::WriteNonNullPrimitive(): bad type: "
<< type.DebugString();
}
}
void RawValue::Write(const void* value, Tuple* tuple, const SlotDescriptor* slot_desc,
MemPool* pool) {
RawValue::Write<false>(value, tuple, slot_desc, pool, nullptr, nullptr);
}
template <bool COLLECT_VAR_LEN_VALS>
void RawValue::Write(const void* value, Tuple* tuple, const SlotDescriptor* slot_desc,
MemPool* pool, std::vector<StringValue*>* string_values,
std::vector<std::pair<CollectionValue*, int64_t>>* collection_values) {
if (value == nullptr) {
if (slot_desc->type().IsStructType()) {
tuple->SetStructToNull(slot_desc);
} else if (slot_desc->type().IsVariantType()) {
// A variant is null as a whole. Mirror SetStructToNull: set the variant's null bit
// and each (nullable) child's null bit, so downstream var-len collection (which
// checks per-child null bits, e.g. the sorter's CollectNonNullNonSmallVarSlots)
// correctly skips the children.
tuple->SetNull(slot_desc->null_indicator_offset());
DCHECK(slot_desc->children_tuple_descriptor() != nullptr);
for (SlotDescriptor* child : slot_desc->children_tuple_descriptor()->slots()) {
tuple->SetNull(child->null_indicator_offset());
}
} else {
tuple->SetNull(slot_desc->null_indicator_offset());
}
} else {
RawValue::WriteNonNull<COLLECT_VAR_LEN_VALS>(value, tuple, slot_desc, pool,
string_values, collection_values);
}
}
template <bool COLLECT_VAR_LEN_VALS>
void RawValue::WriteNonNull(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool,
vector<StringValue*>* string_values,
vector<pair<CollectionValue*, int64_t>>* collection_values) {
DCHECK(value != nullptr && tuple != nullptr && slot_desc != nullptr);
if (COLLECT_VAR_LEN_VALS) {
DCHECK(string_values != nullptr);
DCHECK(collection_values != nullptr);
}
if (slot_desc->type().IsStructType()) {
WriteStruct<COLLECT_VAR_LEN_VALS>(value, tuple, slot_desc, pool,
string_values, collection_values);
} else if (slot_desc->type().IsCollectionType()) {
WriteCollection<COLLECT_VAR_LEN_VALS>(value, tuple, slot_desc, pool,
string_values, collection_values);
} else if (slot_desc->type().IsVariantType()) {
WriteVariant<COLLECT_VAR_LEN_VALS>(value, tuple, slot_desc, pool, string_values);
} else {
WritePrimitiveCollectVarlen<COLLECT_VAR_LEN_VALS>(value, tuple, slot_desc, pool,
string_values);
}
}
template <bool COLLECT_VAR_LEN_VALS>
void RawValue::WriteStruct(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool, vector<StringValue*>* string_values,
vector<pair<CollectionValue*, int64_t>>* collection_values) {
DCHECK(tuple != nullptr);
DCHECK(slot_desc->type().IsStructType());
DCHECK(slot_desc->children_tuple_descriptor() != nullptr);
if (value == nullptr) {
tuple->SetStructToNull(slot_desc);
return;
}
const StructVal* src = reinterpret_cast<const StructVal*>(value);
const TupleDescriptor* children_tuple_desc = slot_desc->children_tuple_descriptor();
DCHECK_EQ(src->num_children, children_tuple_desc->slots().size());
for (int i = 0; i < src->num_children; ++i) {
SlotDescriptor* child_slot = children_tuple_desc->slots()[i];
uint8_t* src_child = src->ptr[i];
// TODO IMPALA-12160: Handle collections in structs.
if (child_slot->type().IsStructType()) {
// Recursive call in case of nested structs.
WriteStruct<COLLECT_VAR_LEN_VALS>(src_child, tuple, child_slot, pool,
string_values, collection_values);
} else if (src_child == nullptr) {
tuple->SetNull(child_slot->null_indicator_offset());
} else {
WritePrimitiveCollectVarlen<COLLECT_VAR_LEN_VALS>(src_child, tuple, child_slot,
pool, string_values);
}
}
}
template <bool COLLECT_VAR_LEN_VALS>
void RawValue::WriteVariant(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool, vector<StringValue*>* string_values) {
DCHECK(value != nullptr && tuple != nullptr && slot_desc != nullptr);
DCHECK(slot_desc->type().IsVariantType());
DCHECK(slot_desc->children_tuple_descriptor() != nullptr);
// Unlike a struct, a variant value is not a StructVal: 'value' points at the 24-byte
// variant slot in the source tuple (two adjacent StringValues: metadata + value). Child
// slot offsets are absolute in the master tuple (same as struct children), so each
// child's position within 'value' is child->tuple_offset() - slot_desc->tuple_offset()
// (0 for metadata, sizeof(StringValue) for value).
// TODO(variant_get): this assumes the source is always a materialized scan slot. When
// VARIANT becomes a first-class expression type (a VariantVal ABI, letting functions
// such as variant_get() produce VARIANT), this must also handle a VariantVal source.
const TupleDescriptor* children_tuple_desc = slot_desc->children_tuple_descriptor();
const uint8_t* src_base = reinterpret_cast<const uint8_t*>(value);
for (SlotDescriptor* child_slot : children_tuple_desc->slots()) {
// For unshredded variants the children (metadata, value) are always present when the
// variant itself is non-null, and per-child null info is not reachable from the raw
// slot pointer. Revisit when shredded variants (with nullable typed children) land.
DCHECK(child_slot->type().IsVarLenStringType())
<< "Unexpected variant child type: " << child_slot->type().DebugString();
const void* src_child =
src_base + (child_slot->tuple_offset() - slot_desc->tuple_offset());
WritePrimitiveCollectVarlen<COLLECT_VAR_LEN_VALS>(src_child, tuple, child_slot, pool,
string_values);
}
}
template <bool COLLECT_VAR_LEN_VALS>
void RawValue::WriteCollection(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool, vector<StringValue*>* string_values,
vector<pair<CollectionValue*, int64_t>>* collection_values) {
DCHECK(slot_desc->type().IsCollectionType());
void* dst = tuple->GetSlot(slot_desc->tuple_offset());
const CollectionValue* src = reinterpret_cast<const CollectionValue*>(value);
CollectionValue* dest = reinterpret_cast<CollectionValue*>(dst);
dest->num_tuples = src->num_tuples;
int64_t byte_size = dest->ByteSize(*slot_desc->children_tuple_descriptor());
if (pool != nullptr) {
// If 'dest' and 'src' point to the same address, assigning the address of the newly
// allocated buffer to 'dest->ptr' will also overwrite 'src->ptr', and the memcpy will
// be from the destination to the destination.
DCHECK_NE(dest, src);
// Note: if this changes to TryAllocate(), SlotDescriptor::CodegenWriteToSlot() will
// need to reflect this change as well (the codegen'd Allocate() call is actually
// generated in SlotDescriptor::CodegenWriteStringOrCollectionToSlot()).
dest->ptr = reinterpret_cast<uint8_t*>(pool->Allocate(byte_size));
Ubsan::MemCpy(dest->ptr, src->ptr, byte_size);
} else {
dest->ptr = src->ptr;
}
// We only need to recurse if this is a deep copy (pool != nullptr) OR if we collect
// var-len values.
if (pool != nullptr || COLLECT_VAR_LEN_VALS) {
WriteCollectionChildren<COLLECT_VAR_LEN_VALS>(*dest, *src, *slot_desc, pool,
string_values, collection_values);
}
if (COLLECT_VAR_LEN_VALS) {
DCHECK(string_values != nullptr);
DCHECK(collection_values != nullptr);
collection_values->push_back(std::make_pair(dest, byte_size));
}
}
template <bool COLLECT_VAR_LEN_VALS>
void RawValue::WriteCollectionChildren(const CollectionValue& dest,
const CollectionValue& src, const SlotDescriptor& collection_slot_desc, MemPool* pool,
vector<StringValue*>* string_values,
vector<pair<CollectionValue*, int64_t>>* collection_values) {
DCHECK_EQ(src.num_tuples, dest.num_tuples);
const TupleDescriptor* child_tuple_desc =
collection_slot_desc.children_tuple_descriptor();
DCHECK(child_tuple_desc != nullptr);
for (int i = 0; i < dest.num_tuples; i++) {
Tuple* child_src_tuple = reinterpret_cast<Tuple*>(
src.ptr + i * child_tuple_desc->byte_size());
Tuple* child_dest_tuple = reinterpret_cast<Tuple*>(
dest.ptr + i * child_tuple_desc->byte_size());
for (const SlotDescriptor* string_slot_desc : child_tuple_desc->string_slots()) {
WriteCollectionVarlenChild<COLLECT_VAR_LEN_VALS>(child_dest_tuple, child_src_tuple,
string_slot_desc, pool, string_values, collection_values);
}
for (const SlotDescriptor* collection_slot_desc
: child_tuple_desc->collection_slots()) {
WriteCollectionVarlenChild<COLLECT_VAR_LEN_VALS>(child_dest_tuple, child_src_tuple,
collection_slot_desc, pool, string_values, collection_values);
}
}
}
template <bool COLLECT_VAR_LEN_VALS>
void RawValue::WriteCollectionVarlenChild(Tuple* child_dest_tuple, Tuple* child_src_tuple,
const SlotDescriptor* slot_desc, MemPool* pool, vector<StringValue*>* string_values,
vector<pair<CollectionValue*, int64_t>>* collection_values ) {
DCHECK(slot_desc != nullptr);
DCHECK(slot_desc->type().IsVarLenStringType() || slot_desc->type().IsCollectionType());
if (!child_dest_tuple->IsNull(slot_desc->null_indicator_offset())) {
// The fixed length part of the child (the pointer and the length / number of tuples)
// is already in the destination tuple, copied there as the var-len data of the
// parent. We continue the recursion for two things:
// 1. deep-copying the var-len data of the child
// 2. collecting var-len slots.
// At least one of these is true, otherwise we never get here. The called recursive
// function will once again set the length (always unnecessary) and the pointer
// (unnecessary if we're not deep-copying, only collecting). This is not costly enough
// to justify complicating the code. Note, however, that although at this point the
// source and destination slots hold the same value (pointer and length / number of
// tuples), we take 'child_value', the source in the recursive call, from the source
// tuple, because in case of deep-copying, the pointer of the destination slot will be
// re-assigned to the newly allocated buffer, and if we took 'child_value' from the
// destination slot, the 'source' pointer and the 'destination' pointer would be the
// same, meaning the 'source' pointer would also be overwritten before we copied the
// data it pointed to.
void* child_value = child_src_tuple->GetSlot(slot_desc->tuple_offset());
WriteNonNull<COLLECT_VAR_LEN_VALS>(child_value, child_dest_tuple,
slot_desc, pool, string_values, collection_values);
}
}
template <bool COLLECT_VAR_LEN_VALS>
void RawValue::WritePrimitiveCollectVarlen(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool, vector<StringValue*>* string_values) {
DCHECK(value != nullptr && tuple != nullptr && slot_desc != nullptr);
void* dst = tuple->GetSlot(slot_desc->tuple_offset());
WriteNonNullPrimitive(value, dst, slot_desc->type(), pool);
if constexpr (COLLECT_VAR_LEN_VALS) {
DCHECK(string_values != nullptr);
if (slot_desc->type().IsVarLenStringType()) {
StringValue* str_value = reinterpret_cast<StringValue*>(dst);
if (!str_value->IsSmall()) string_values->push_back(str_value);
} else if (slot_desc->type().IsCollectionType()) {
DCHECK(false) << "Collections should be handled in WriteCollection.";
}
}
}
void RawValue::PrintValue(
const void* value, const ColumnType& type, int scale, std::stringstream* stream,
bool quote_val) {
if (value == NULL) {
*stream << NullLiteral(true);
return;
}
int old_precision = stream->precision();
std::ios_base::fmtflags old_flags = stream->flags();
if (scale > -1) {
stream->precision(scale);
// Setting 'fixed' causes precision to set the number of digits printed after the
// decimal (by default it sets the maximum number of digits total).
*stream << std::fixed;
}
const StringValue* string_val = NULL;
switch (type.type) {
case TYPE_BOOLEAN: {
bool val = *reinterpret_cast<const bool*>(value);
*stream << (val ? "true" : "false");
break;
}
case TYPE_TINYINT:
// Extra casting for chars since they should not be interpreted as ASCII.
*stream << static_cast<int>(*reinterpret_cast<const int8_t*>(value));
break;
case TYPE_SMALLINT: *stream << *reinterpret_cast<const int16_t*>(value); break;
case TYPE_INT: *stream << *reinterpret_cast<const int32_t*>(value); break;
case TYPE_BIGINT: *stream << *reinterpret_cast<const int64_t*>(value); break;
case TYPE_FLOAT: {
float val = *reinterpret_cast<const float*>(value);
if (LIKELY(std::isfinite(val))) {
*stream << val;
} else if (std::isinf(val)) {
// 'Infinity' is Java's text representation of inf. By staying close to Java, we
// allow Hive to read text tables containing non-finite values produced by
// Impala. (The same logic applies to 'NaN', below).
*stream << (val < 0 ? "-Infinity" : "Infinity");
} else if (std::isnan(val)) {
*stream << "NaN";
}
} break;
case TYPE_DOUBLE: {
double val = *reinterpret_cast<const double*>(value);
if (LIKELY(std::isfinite(val))) {
*stream << val;
} else if (std::isinf(val)) {
// See TYPE_FLOAT for rationale.
*stream << (val < 0 ? "-Infinity" : "Infinity");
} else if (std::isnan(val)) {
*stream << "NaN";
}
} break;
case TYPE_VARCHAR:
case TYPE_STRING:
string_val = reinterpret_cast<const StringValue*>(value);
if (type.type == TYPE_VARCHAR) DCHECK(string_val->Len() <= type.len);
if (quote_val) {
string str(string_val->Ptr(), string_val->Len());
str = strings::Utf8SafeCEscape(str);
*stream << "\"";
stream->write(str.c_str(), str.size());
*stream << "\"";
} else {
stream->write(string_val->Ptr(), string_val->Len());
}
break;
case TYPE_TIMESTAMP:
if (quote_val) *stream << "\"";
*stream << *reinterpret_cast<const TimestampValue*>(value);
if (quote_val) *stream << "\"";
break;
case TYPE_CHAR:
if (quote_val) {
string str(reinterpret_cast<const char*>(value), type.len);
str = strings::Utf8SafeCEscape(str);
*stream << "\"";
stream->write(str.c_str(), str.size());
*stream << "\"";
} else {
stream->write(reinterpret_cast<const char*>(value), type.len);
}
break;
case TYPE_DECIMAL:
switch (type.GetByteSize()) {
case 4:
*stream << reinterpret_cast<const Decimal4Value*>(value)->ToString(type);
break;
case 8:
*stream << reinterpret_cast<const Decimal8Value*>(value)->ToString(type);
break;
case 16:
*stream << reinterpret_cast<const Decimal16Value*>(value)->ToString(type);
break;
default: DCHECK(false) << type;
}
break;
case TYPE_DATE: {
if (quote_val) *stream << "\"";
*stream << *reinterpret_cast<const DateValue*>(value);
if (quote_val) *stream << "\"";
} break;
case TYPE_FIXED_UDA_INTERMEDIATE: {
// This is always a binary type, so escape invalid unicode characters to make it
// printable.
string intermed_str(reinterpret_cast<const char*>(value), type.len);
intermed_str = strings::Utf8SafeCEscape(intermed_str);
if (quote_val) *stream << "\"";
stream->write(intermed_str.c_str(), intermed_str.size());
if (quote_val) *stream << "\"";
} break;
default: DCHECK(false) << "Unknown type: " << type;
}
stream->precision(old_precision);
// Undo setting stream to fixed
stream->flags(old_flags);
}
template void RawValue::Write<true>(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool,
std::vector<StringValue*>* string_values,
std::vector<std::pair<CollectionValue*, int64_t>>* collection_values);
template void RawValue::Write<false>(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool,
std::vector<StringValue*>* string_values,
std::vector<std::pair<CollectionValue*, int64_t>>* collection_values);
template void RawValue::WriteNonNull<true>(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool,
std::vector<StringValue*>* string_values,
std::vector<std::pair<CollectionValue*, int64_t>>* collection_values);
template void RawValue::WriteNonNull<false>(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool,
std::vector<StringValue*>* string_values,
std::vector<std::pair<CollectionValue*, int64_t>>* collection_values);
template void RawValue::WriteStruct<true>(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool,
std::vector<StringValue*>* string_values,
std::vector<std::pair<CollectionValue*, int64_t>>* collection_values);
template void RawValue::WriteStruct<false>(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool,
std::vector<StringValue*>* string_values,
std::vector<std::pair<CollectionValue*, int64_t>>* collection_values);
template void RawValue::WriteVariant<true>(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool,
std::vector<StringValue*>* string_values);
template void RawValue::WriteVariant<false>(const void* value, Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool,
std::vector<StringValue*>* string_values);
template void RawValue::WritePrimitiveCollectVarlen<true>(const void* value,
Tuple* tuple, const SlotDescriptor* slot_desc, MemPool* pool,
std::vector<StringValue*>* string_values);
template void RawValue::WritePrimitiveCollectVarlen<false>(const void* value,
Tuple* tuple,
const SlotDescriptor* slot_desc, MemPool* pool,
std::vector<StringValue*>* string_values);
}