Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cc7f851
add Validate method to array and implementation for ListArray
emkornfield Apr 13, 2016
01c50be
Add utility methods for managing null bitmap directly to ArrayBuilder
emkornfield Apr 13, 2016
3895d34
Refactor list builder to use ArrayBuilders bitmap methods and a separ…
emkornfield Apr 13, 2016
20f984b
refactor primitive builders to use parent builders bitmap
emkornfield Apr 13, 2016
1374485
augment python unittest to have null element in list
emkornfield Apr 13, 2016
45e41c0
Make BufferBuilder more useable for appending primitives
emkornfield Apr 13, 2016
5f87aef
Refactor ipc-adapter-test to make it paramaterizable. add unit test …
emkornfield Apr 13, 2016
61b0481
small fixes to naming/style for c++ and potential bugs
emkornfield Apr 13, 2016
a2e1e52
native popcount
emkornfield Apr 13, 2016
39c57ed
add potentially useful methods for generative arrays to ipc test-common
emkornfield Apr 13, 2016
aa0602c
add potentially useful pool factories to test utils
emkornfield Apr 13, 2016
8e464b5
Fixes per tidy and lint
emkornfield Apr 13, 2016
53d37bc
filter out ipc-adapter-test from tidy
emkornfield Apr 13, 2016
8ab5315
make clang tidy ignore a little bit less hacky
emkornfield Apr 17, 2016
e71810b
make Resize and Init virtual on builder
emkornfield Apr 17, 2016
2e6c477
add missing RETURN_NOT_OK
emkornfield Apr 17, 2016
3b219a1
Make append is_null parameter is_valid for api consistency
emkornfield Apr 18, 2016
10e6651
add in maximum recursion depth, surfaced possible recursion issue wit…
emkornfield Apr 18, 2016
be04b3e
add unit tests for zero length row batches and non-null batches. fix…
emkornfield Apr 18, 2016
8982723
remaining style cleanup
emkornfield Apr 18, 2016
5e15815
fix make lint
emkornfield Apr 18, 2016
6e57728
make format fixes
emkornfield Apr 18, 2016
7789205
make clang-format-3.7 happy
emkornfield Apr 18, 2016
0af558b
remove a now unnecessary NOLINT, but mostly to trigger another travis…
emkornfield Apr 18, 2016
0c5162d
another format fix
emkornfield Apr 18, 2016
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -562,10 +562,10 @@ endif()
if (${CLANG_TIDY_FOUND})
# runs clang-tidy and attempts to fix any warning automatically
add_custom_target(clang-tidy ${BUILD_SUPPORT_DIR}/run-clang-tidy.sh ${CLANG_TIDY_BIN} ${CMAKE_BINARY_DIR}/compile_commands.json 1
`find ${CMAKE_CURRENT_SOURCE_DIR}/src -name \\*.cc | sed -e '/_generated/g'`)
`find ${CMAKE_CURRENT_SOURCE_DIR}/src -name \\*.cc |grep -v ipc-adapter-test.cc | sed -e '/_generated/g'`)
# runs clang-tidy and exits with a non-zero exit code if any errors are found.
add_custom_target(check-clang-tidy ${BUILD_SUPPORT_DIR}/run-clang-tidy.sh ${CLANG_TIDY_BIN} ${CMAKE_BINARY_DIR}/compile_commands.json
0 `find ${CMAKE_CURRENT_SOURCE_DIR}/src -name \\*.cc | sed -e '/_generated/g'`)
0 `find ${CMAKE_CURRENT_SOURCE_DIR}/src -name \\*.cc |grep -v ipc-adapter-test.cc | sed -e '/_generated/g'`)

endif()

Expand Down
5 changes: 5 additions & 0 deletions cpp/src/arrow/array.cc
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
#include <cstdint>

#include "arrow/util/buffer.h"
#include "arrow/util/status.h"

namespace arrow {

Expand Down Expand Up @@ -47,6 +48,10 @@ bool Array::EqualsExact(const Array& other) const {
return true;
}

Status Array::Validate() const {
return Status::OK();
}

bool NullArray::Equals(const std::shared_ptr<Array>& arr) const {
if (this == arr.get()) { return true; }
if (Type::NA != arr->type_enum()) { return false; }
Expand Down
6 changes: 5 additions & 1 deletion cpp/src/arrow/array.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
namespace arrow {

class Buffer;
class Status;

// Immutable data array with some logical type and some length. Any memory is
// owned by the respective Buffer instance (or its parents).
Expand All @@ -39,7 +40,7 @@ class Array {
Array(const std::shared_ptr<DataType>& type, int32_t length, int32_t null_count = 0,
const std::shared_ptr<Buffer>& null_bitmap = nullptr);

virtual ~Array() {}
virtual ~Array() = default;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside: I tried to find definitive documentation on when to use = default vs = 0 in C++11, if you have any pointers

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding is default == {}, and destructor = 0, should be used rarely. From: http://en.cppreference.com/w/cpp/language/destructor "A destructor may be declared pure virtual, for example in a base class which needs to be made abstract, but has no other suitable functions that could be declared pure virtual. Such destructor must have a definition, since all base class destructors are always called when the derived class is destroyed" Although the second sentence seems to contradict the first.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed to default purely for style (I likely should have done this as a separate PR and discussed), for some reason it reads more elegantly to me.


// Determine if a slot is null. For inner loops. Does *not* boundscheck
bool IsNull(int i) const {
Expand All @@ -58,6 +59,9 @@ class Array {

bool EqualsExact(const Array& arr) const;
virtual bool Equals(const std::shared_ptr<Array>& arr) const = 0;
// Determines if the array is internally consistent. Defaults to always
// returning Status::OK. This can be an expensive check.
virtual Status Validate() const;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pardon my dumb question, my understanding of Status just seeing its usage in the code I thought it's signaling either an operation succeeded or failed, but validate seems to imply it's either in a valid state or invalid state (like a bool). Why not just use bool? Or does Status can also encode error information?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status can encode error information.

@wesm wesm Apr 15, 2016

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was some work last year in Impala in which they analyzed the generated x86 instructions to see how to minimize the CPU cycles associated with verifying Status::OK: apache/impala@1afe728


protected:
std::shared_ptr<DataType> type_;
Expand Down
56 changes: 56 additions & 0 deletions cpp/src/arrow/builder.cc
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,25 @@

namespace arrow {

Status ArrayBuilder::AppendToBitmap(bool is_null) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a matter of consistency, should this be is_valid?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, I did it this way because it was closer to how the code previously worked. But I will change it.

if (length_ == capacity_) {
// If the capacity was not already a multiple of 2, do so here
// TODO(emkornfield) doubling isn't great default allocation practice
// see https://github.com/facebook/folly/blob/master/folly/docs/FBVector.md
// fo discussion
RETURN_NOT_OK(Resize(util::next_power2(capacity_ + 1)));
}
UnsafeAppendToBitmap(is_null);
return Status::OK();
}

Status ArrayBuilder::AppendToBitmap(const uint8_t* valid_bytes, int32_t length) {
Reserve(length);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reserve may return error status

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops. I wonder how hard it would be write a clang-tidy rule to always ensure methods with a Status field are checked.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


UnsafeAppendToBitmap(valid_bytes, length);
return Status::OK();
}

Status ArrayBuilder::Init(int32_t capacity) {
capacity_ = capacity;
int32_t to_alloc = util::ceil_byte(capacity) / 8;
Expand All @@ -36,6 +55,7 @@ Status ArrayBuilder::Init(int32_t capacity) {
}

Status ArrayBuilder::Resize(int32_t new_bits) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By the way I was thinking it would be nice to make Resize and Init virtual methods, I think it would reduce code repetition and potential bugs for classes the don't have any reason to override Reserve. Thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good to me, go ahead

if (!null_bitmap_) { return Init(new_bits); }
int32_t new_bytes = util::ceil_byte(new_bits) / 8;
int32_t old_bytes = null_bitmap_->size();
RETURN_NOT_OK(null_bitmap_->Resize(new_bytes));
Expand All @@ -56,10 +76,46 @@ Status ArrayBuilder::Advance(int32_t elements) {

Status ArrayBuilder::Reserve(int32_t elements) {
if (length_ + elements > capacity_) {
// TODO(emkornfield) power of 2 growth is potentially suboptimal

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aside: should we do 1.5x growth everywhere (this is the folly approach IIRC -- is there more research on this subject?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes this is what folly uses. Seems like 1.5 edges out 2 on the very light survey done here: https://en.wikipedia.org/wiki/Dynamic_array#Growth_factor

Ideally, we would benchmark once we have some real data in place. In the absence of that 1.5 seems like a good default.

int32_t new_capacity = util::next_power2(length_ + elements);
return Resize(new_capacity);
}
return Status::OK();
}

Status ArrayBuilder::SetNotNull(int32_t length) {
RETURN_NOT_OK(Reserve(length));
UnsafeSetNotNull(length);
return Status::OK();
}

void ArrayBuilder::UnsafeAppendToBitmap(bool is_null) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same question as above

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I think so. I will make the change.

if (is_null) {
++null_count_;
} else {
util::set_bit(null_bitmap_data_, length_);
}
++length_;
}

void ArrayBuilder::UnsafeAppendToBitmap(const uint8_t* valid_bytes, int32_t length) {
if (valid_bytes == nullptr) {
UnsafeSetNotNull(length);
return;
}
for (int32_t i = 0; i < length; ++i) {
// TODO(emkornfield) Optimize for large values of length?
AppendToBitmap(valid_bytes[i] == 0);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be UnsafeAppendToBitmap? This presumes that the user has called Reserve(...) beforehand

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correct. I will change.

}
}

void ArrayBuilder::UnsafeSetNotNull(int32_t length) {
const int32_t new_length = length + length_;
// TODO(emkornfield) Optimize for large values of length?
for (int32_t i = length_; i < new_length; ++i) {
util::set_bit(null_bitmap_data_, i);
}
length_ = new_length;
}

} // namespace arrow
29 changes: 26 additions & 3 deletions cpp/src/arrow/builder.h
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ class PoolBuffer;

static constexpr int32_t MIN_BUILDER_CAPACITY = 1 << 5;

// Base class for all data array builders
// Base class for all data array builders.
// This class provides a facilities for incrementally building the null bitmap
// (see Append methods) and as a side effect the current number of slots and
// the null count.
class ArrayBuilder {
public:
explicit ArrayBuilder(MemoryPool* pool, const TypePtr& type)
Expand All @@ -46,7 +49,7 @@ class ArrayBuilder {
length_(0),
capacity_(0) {}

virtual ~ArrayBuilder() {}
virtual ~ArrayBuilder() = default;

// For nested types. Since the objects are owned by this class instance, we
// skip shared pointers and just return a raw pointer
Expand All @@ -58,6 +61,14 @@ class ArrayBuilder {
int32_t null_count() const { return null_count_; }
int32_t capacity() const { return capacity_; }

// Append to null bitmap
Status AppendToBitmap(bool is_null);
// Vector append. Treat each zero byte as a null. If valid_bytes is null
// assume all of length bits are valid.
Status AppendToBitmap(const uint8_t* valid_bytes, int32_t length);
// Set the next length bits to not null (i.e. valid).
Status SetNotNull(int32_t length);

// Allocates requires memory at this level, but children need to be
// initialized independently
Status Init(int32_t capacity);
Expand All @@ -75,7 +86,7 @@ class ArrayBuilder {
const std::shared_ptr<PoolBuffer>& null_bitmap() const { return null_bitmap_; }

// Creates new array object to hold the contents of the builder and transfers
// ownership of the data
// ownership of the data. This resets all variables on the builder.
virtual std::shared_ptr<Array> Finish() = 0;

const std::shared_ptr<DataType>& type() const { return type_; }
Expand All @@ -97,6 +108,18 @@ class ArrayBuilder {
// Child value array builders. These are owned by this class
std::vector<std::unique_ptr<ArrayBuilder>> children_;

//
// Unsafe operations (don't check capacity/don't resize)
//

// Append to null bitmap.
void UnsafeAppendToBitmap(bool is_null);
// Vector append. Treat each zero byte as a nullzero. If valid_bytes is null
// assume all of length bits are valid.
void UnsafeAppendToBitmap(const uint8_t* valid_bytes, int32_t length);
// Set the next length bits to not null (i.e. valid).
void UnsafeSetNotNull(int32_t length);

private:
DISALLOW_COPY_AND_ASSIGN(ArrayBuilder);
};
Expand Down
94 changes: 71 additions & 23 deletions cpp/src/arrow/ipc/adapter.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

#include <cstdint>
#include <cstring>
#include <sstream>
#include <vector>

#include "arrow/array.h"
Expand All @@ -31,6 +32,7 @@
#include "arrow/type.h"
#include "arrow/types/construct.h"
#include "arrow/types/primitive.h"
#include "arrow/types/list.h"
#include "arrow/util/buffer.h"
#include "arrow/util/logging.h"
#include "arrow/util/status.h"
Expand Down Expand Up @@ -63,32 +65,56 @@ static bool IsPrimitive(const DataType* type) {
}
}

static bool IsListType(const DataType* type) {
DCHECK(type != nullptr);
switch (type->type) {
// TODO(emkornfield) grouping like this are used in a few places in the
// code consider using pattern like:
// http://stackoverflow.com/questions/26784685/c-macro-for-calling-function-based-on-enum-type
//
// TODO(emkornfield) Fix type systems so these are all considered lists and
// the types behave the same way?
// case Type::BINARY:
// case Type::CHAR:
case Type::LIST:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See also @jacques-n comment on the mailing list. Having logical types for String (UTF8) and Binary makes sense, but internally these are represented in memory using the same memory layout as List. So how we implement them in C++ should be whatever is the most convenient and high performance thing, probably

// see todo on common types
// case Type::STRING:
// case Type::VARCHAR:
return true;
default:
return false;
}
}

// ----------------------------------------------------------------------
// Row batch write path

Status VisitArray(const Array* arr, std::vector<flatbuf::FieldNode>* field_nodes,
std::vector<std::shared_ptr<Buffer>>* buffers) {
if (IsPrimitive(arr->type().get())) {
const PrimitiveArray* prim_arr = static_cast<const PrimitiveArray*>(arr);

field_nodes->push_back(
flatbuf::FieldNode(prim_arr->length(), prim_arr->null_count()));
DCHECK(arr);
DCHECK(field_nodes);
// push back all common elements
field_nodes->push_back(flatbuf::FieldNode(arr->length(), arr->null_count()));
if (arr->null_count() > 0) {
buffers->push_back(arr->null_bitmap());
} else {
// Push a dummy zero-length buffer, not to be copied
buffers->push_back(std::make_shared<Buffer>(nullptr, 0));
}

if (prim_arr->null_count() > 0) {
buffers->push_back(prim_arr->null_bitmap());
} else {
// Push a dummy zero-length buffer, not to be copied
buffers->push_back(std::make_shared<Buffer>(nullptr, 0));
}
const auto arr_type = arr->type().get();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Declare this type

if (IsPrimitive(arr_type)) {
const PrimitiveArray* prim_arr = static_cast<const PrimitiveArray*>(arr);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auto is appropriate here, as below

buffers->push_back(prim_arr->data());
} else if (arr->type_enum() == Type::LIST) {
// TODO(wesm)
return Status::NotImplemented("List type");
} else if (IsListType(arr_type)) {
const ListArray* list_arr = static_cast<const ListArray*>(arr);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

auto is OK here

buffers->push_back(list_arr->offset_buffer());
// TODO(emkornfield) limit recursion depth

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Impala limited to 32K depth IIRC, but at minimum we should track the depth and have a static limit.

RETURN_NOT_OK(VisitArray(list_arr->values().get(), field_nodes, buffers));
} else if (arr->type_enum() == Type::STRUCT) {
// TODO(wesm)
return Status::NotImplemented("Struct type");
}

return Status::OK();
}

Expand Down Expand Up @@ -214,7 +240,7 @@ class RowBatchReader::Impl {
// Traverse the flattened record batch metadata and reassemble the
// corresponding array containers
Status NextArray(const Field* field, std::shared_ptr<Array>* out) {
const std::shared_ptr<DataType>& type = field->type;
const TypePtr& type = field->type;

// pop off a field
if (field_index_ >= num_flattened_fields_) {
Expand All @@ -226,15 +252,17 @@ class RowBatchReader::Impl {
// we can skip that buffer without reading from shared memory
FieldMetadata field_meta = metadata_->field(field_index_++);

// extract null_bitmap which is common to all arrays
std::shared_ptr<Buffer> null_bitmap;
if (field_meta.null_count == 0) {
null_bitmap = nullptr;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this redundant?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. The way the spec is currently worded it says it is optional on whether null_count == 0 implies a null pointer, and that the presence of it should be communicated in the metadata. I need to check Message.fbs, but I'm not sure it currently allows for communicating whether the bitmap is included. Here we make the decision to never include the null bitmap because the data-structure is redundant. I will add a comment explaining this.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah sorry, I meant that the nullary ctor for shared_ptr sets nullptr (versus having to do so explicitly)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes good point.

++buffer_index_;
} else {
RETURN_NOT_OK(GetBuffer(buffer_index_++, &null_bitmap));
}

if (IsPrimitive(type.get())) {
std::shared_ptr<Buffer> null_bitmap;
std::shared_ptr<Buffer> data;
if (field_meta.null_count == 0) {
null_bitmap = nullptr;
++buffer_index_;
} else {
RETURN_NOT_OK(GetBuffer(buffer_index_++, &null_bitmap));
}
if (field_meta.length > 0) {
RETURN_NOT_OK(GetBuffer(buffer_index_++, &data));
} else {
Expand All @@ -243,6 +271,26 @@ class RowBatchReader::Impl {
return MakePrimitiveArray(
type, field_meta.length, data, field_meta.null_count, null_bitmap, out);
}
if (IsListType(type.get())) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

else if?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will check what clang-tidy says. In other cases it liked not having else if when the original "if" block always returned. If we prefer to allow either I can look into turning off that particular check.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either else if, or I also think having more consistency in spacing will be better, so extra space after closing braces.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prior block is executed if IsPrimitive(...) is true -- in theory they should be mutually exclusive

std::shared_ptr<Buffer> offsets;
if (field_meta.length > 0) {
RETURN_NOT_OK(GetBuffer(buffer_index_++, &offsets));
} else {
offsets.reset(new Buffer(nullptr, 0));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is interesting -- I suppose with length-0 ListArray instances, there is no need to have any offsets allocated at all (versus a length-1 array with a single element 0).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I wasn't sure, its not mentioned in the spec. I'll send another e-mail out to the ML about this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After further consideration I think we should always serialize the offsets. I don't think it is worth consuming code have to special case for 0 length lists.

}
const int num_children = type->num_children();
if (num_children != 1) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could skip the local variable creation (to avoid an extra type->num_children() below), but I'm ambivalent.

std::stringstream ss;
ss << "Field: " << field->ToString()
<< " has wrong number of children:" << num_children;
return Status::Invalid(ss.str());
}
std::shared_ptr<Array> values_array;
// TODO(emkornfield): limit recursion depth?
RETURN_NOT_OK(NextArray(type->child(0).get(), &values_array));
return MakeListArray(type, field_meta.length, offsets, values_array,
field_meta.null_count, null_bitmap, out);
}
return Status::NotImplemented("Non-primitive types not complete yet");
}

Expand Down
Loading