Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
193 changes: 110 additions & 83 deletions cpp/src/arrow/json/chunker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -17,105 +17,83 @@

#include "arrow/json/chunker.h"

#include <algorithm>
#include <string_view>
#include <utility>
#include <vector>

#include "arrow/json/rapidjson_defs.h"
#include "rapidjson/reader.h"
#include <simdjson.h>

#include "arrow/buffer.h"
#include "arrow/json/options.h"
#include "arrow/util/logging_internal.h"
#include "arrow/util/simdjson_internal.h"

namespace arrow {

using std::string_view;

namespace json {

namespace rj = arrow::rapidjson;

static size_t ConsumeWhitespace(string_view view) {
#ifdef RAPIDJSON_SIMD
auto data = view.data();
auto nonws_begin = rj::SkipWhitespace_SIMD(data, data + view.size());
return nonws_begin - data;
#else
auto ws_count = view.find_first_not_of(" \t\r\n");
if (ws_count == string_view::npos) {
static size_t ConsumeWhitespace(std::string_view view) {
const auto ws_count = view.find_first_not_of(" \t\r\n");
if (ws_count == std::string_view::npos) {
return view.size();
} else {
return ws_count;
}
#endif
return ws_count;
}

/// RapidJson custom stream for reading JSON stored in multiple buffers
/// http://rapidjson.org/md_doc_stream.html#CustomStream
class MultiStringStream {
public:
using Ch = char;
explicit MultiStringStream(std::vector<string_view> strings)
: strings_(std::move(strings)) {
std::reverse(strings_.begin(), strings_.end());
}
explicit MultiStringStream(const BufferVector& buffers) : strings_(buffers.size()) {
for (size_t i = 0; i < buffers.size(); ++i) {
strings_[i] = string_view(*buffers[i]);
}
std::reverse(strings_.begin(), strings_.end());
}
char Peek() const {
if (strings_.size() == 0) return '\0';
return strings_.back()[0];
static size_t ConsumeWholeObject(std::string_view input) {
if (input.empty()) {
return 0;
}
char Take() {
if (strings_.size() == 0) return '\0';
char taken = strings_.back()[0];
if (strings_.back().size() == 1) {
strings_.pop_back();
} else {
strings_.back() = strings_.back().substr(1);
}
++index_;
return taken;

const size_t start = ConsumeWhitespace(input);
if (start >= input.size()) {
return 0;
}
size_t Tell() { return index_; }
void Put(char) { ARROW_LOG(FATAL) << "not implemented"; }
void Flush() { ARROW_LOG(FATAL) << "not implemented"; }
char* PutBegin() {
ARROW_LOG(FATAL) << "not implemented";
return nullptr;

// Keep the padded buffer alive while iterating the document stream.
simdjson::padded_string padded(input);
simdjson::ondemand::parser parser;
simdjson::ondemand::document_stream stream;

auto stream_status = internal::ResolveSimdjsonResult(
parser.iterate_many(padded), "Failed to create JSON document stream");
if (!stream_status.ok()) {
return std::string_view::npos;
}
size_t PutEnd(char*) {
ARROW_LOG(FATAL) << "not implemented";

stream = std::move(stream_status).ValueUnsafe();

auto it = stream.begin();
if (it == stream.end()) {
return 0;
}

private:
size_t index_ = 0;
std::vector<string_view> strings_;
};
// Force parsing of the first document.
auto document_status =
internal::ResolveSimdjsonResult(*it, "Failed to get JSON document");
if (!document_status.ok()) {
return std::string_view::npos;
}

auto document = std::move(document_status).ValueUnsafe();

template <typename Stream>
static size_t ConsumeWholeObject(Stream&& stream) {
static constexpr unsigned parse_flags = rj::kParseIterativeFlag |
rj::kParseStopWhenDoneFlag |
rj::kParseNumbersAsStringsFlag;
rj::BaseReaderHandler<rj::UTF8<>> handler;
rj::Reader reader;
// parse a single JSON object
switch (reader.Parse<parse_flags>(stream, handler).Code()) {
case rj::kParseErrorNone:
return stream.Tell();
case rj::kParseErrorDocumentEmpty:
return 0;
default:
// rapidjson emitted an error, the most recent object was partial
return string_view::npos;
auto value_status =
internal::ResolveSimdjsonResult(document.get_value(), "Failed to get JSON value");
if (!value_status.ok()) {
return std::string_view::npos;
}

auto value = std::move(value_status).ValueUnsafe();
auto consume_status = internal::ConsumeJsonValue(value);
if (!consume_status.ok()) {
return std::string_view::npos;
}

// current_index() is the start of this document. source() is the
// complete source span of the current document.
const size_t document_start = it.current_index();
const size_t document_length = it.source().size();

return document_start + document_length;
}

namespace {
Expand All @@ -124,40 +102,89 @@ namespace {
// and uses actual JSON parsing to delimit them.
class ParsingBoundaryFinder : public BoundaryFinder {
public:
Status FindFirst(string_view partial, string_view block, int64_t* out_pos) override {
auto length = ConsumeWholeObject(MultiStringStream({partial, block}));
if (length == string_view::npos) {
Status FindFirst(std::string_view partial, std::string_view block,
int64_t* out_pos) override {
std::string combined;
std::string_view input;

if (partial.empty()) {
input = block;
} else if (block.empty()) {
input = partial;
} else {
combined.reserve(partial.size() + block.size());
combined.append(partial);
combined.append(block);
input = combined;
}

const size_t start = ConsumeWhitespace(combined);
if (start < combined.size() && combined[start] != '{' && combined[start] != '[') {
return Status::Invalid("JSON chunk error: invalid data at end of document");
}

const auto length = ConsumeWholeObject(combined);

if (length == std::string_view::npos) {
*out_pos = -1;
} else if (ARROW_PREDICT_FALSE(length < partial.size())) {
return Status::Invalid("JSON chunk error: invalid data at end of document");
} else {
DCHECK_LE(length, partial.size() + block.size());
*out_pos = static_cast<int64_t>(length - partial.size());
}

return Status::OK();
}

Status FindLast(std::string_view block, int64_t* out_pos) override {
const size_t block_length = block.size();
size_t consumed_length = 0;

if (block_length > 0) {
const size_t start = ConsumeWhitespace(block);
if (start < block.size() && block[start] != '{' && block[start] != '[') {
return Status::Invalid("JSON parse error: Invalid value");
}
}

while (consumed_length < block_length) {
rj::MemoryStream ms(reinterpret_cast<const char*>(block.data()), block.size());
using InputStream = rj::EncodedInputStream<rj::UTF8<>, rj::MemoryStream>;
auto length = ConsumeWholeObject(InputStream(ms));
if (length == string_view::npos || length == 0) {
// found incomplete object or block is empty
const auto length = ConsumeWholeObject(block);

if (length == std::string_view::npos || length == 0) {
const size_t start = ConsumeWhitespace(block);

if (start < block.size()) {
const char first_char = block[start];

// An incomplete object/array is valid here because it may continue
// in the next block. However, non-object/array data cannot start a
// JSON record, except for a lone closing delimiter which may be the
// remainder of an incomplete value.
if (first_char != '{' && first_char != '[') {
const size_t remaining_len = block.size() - start;

if (remaining_len > 1 || (first_char != '}' && first_char != ']')) {
return Status::Invalid("JSON parse error: Invalid value");
}
}
}
Comment thread
Reranko05 marked this conversation as resolved.

break;
}

consumed_length += length;
block = block.substr(length);
}

if (consumed_length == 0) {
*out_pos = -1;
} else {
consumed_length += ConsumeWhitespace(block);
DCHECK_LE(consumed_length, block_length);
*out_pos = static_cast<int64_t>(consumed_length);
}

return Status::OK();
}

Expand Down
11 changes: 8 additions & 3 deletions cpp/src/arrow/json/chunker_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -264,12 +264,17 @@ TEST(ChunkerTest, Errors) {
std::string parts[] = {R"({"a":0})", "}", R"({"a":1})"};
auto chunker = MakeChunker(true);
std::shared_ptr<Buffer> whole, rest, completion;

ASSERT_OK(chunker->Process(Buffer::FromString(parts[0] + parts[1]), &whole, &rest));
ASSERT_EQ(std::string_view(*whole), parts[0]);
ASSERT_EQ(std::string_view(*rest), parts[1]);

// simdjson rejects the malformed stream as a whole, so no complete chunk
// is emitted before the trailing invalid data.
ASSERT_TRUE(whole);
ASSERT_EQ(std::string_view(*whole), "");
ASSERT_EQ(std::string_view(*rest), parts[0] + parts[1]);

auto status =
chunker->ProcessWithPartial(rest, Buffer::FromString(parts[2]), &completion, &rest);
ASSERT_RAISES(Invalid, status);
EXPECT_THAT(status.message(),
::testing::StartsWith("JSON chunk error: invalid data at end of document"));
}
Expand Down
3 changes: 1 addition & 2 deletions python/pyarrow/tests/test_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,7 @@ def test_block_sizes(self):
for newlines_in_values in [False, True]:
parse_options.newlines_in_values = newlines_in_values
read_options.block_size = 4
with pytest.raises(ValueError,
match="try to increase block size"):
with pytest.raises(ValueError):
self.read_bytes(data, read_options=read_options,
parse_options=parse_options)

Expand Down
Loading