This is an automated email from the ASF dual-hosted git repository.
pitrou pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/main by this push:
new 6c2c8c7514 GH-50567: [C++] Introduce JsonWriter and migrate
integration JSON writer (#50568)
6c2c8c7514 is described below
commit 6c2c8c751430026625d12627d8c4f56e6ab80355
Author: Aaditya Srinivasan <[email protected]>
AuthorDate: Tue Jul 28 20:57:36 2026 +0530
GH-50567: [C++] Introduce JsonWriter and migrate integration JSON writer
(#50568)
### What changes are included?
This PR introduces a reusable `JsonWriter` wrapper around simdjson's JSON
builder API and migrates the integration JSON writer to use it instead of
RapidJSON.
Specifically, this PR:
- Adds a reusable `JsonWriter` abstraction in `arrow/json`.
- Migrates the integration JSON writer implementation to `JsonWriter`.
- Updates the integration tests to use `JsonWriter`.
- Adds unit tests for `JsonWriter`.
This is part of the incremental migration from RapidJSON to simdjson.
### Performance
I compared the serialization performance of
`IntegrationJsonWriter::WriteRecordBatch()` + `Finish()` using a temporary
benchmark with a 1M-row `RecordBatch`. The benchmark was run 10 times on both
`main` and this branch.
| Branch | Average Time |
|--------|-------------:|
| `main` | 2157.4 ms |
| This PR | 2153.6 ms |
No measurable performance regression was observed.
### Are these changes tested?
Yes.
I added unit tests for `JsonWriter` and verified that both JSON and
integration tests pass locally:
- `arrow-json-test`
- `arrow-json-integration-test`
### Are there any user-facing changes?
No.
* GitHub Issue: #50567
Authored-by: Aaditya Srinivasan <[email protected]>
Signed-off-by: Antoine Pitrou <[email protected]>
---
ci/docker/ubuntu-24.04-cpp.dockerfile | 1 +
cpp/cmake_modules/ThirdpartyToolchain.cmake | 2 +-
cpp/src/arrow/CMakeLists.txt | 4 +-
cpp/src/arrow/integration/CMakeLists.txt | 7 +-
cpp/src/arrow/integration/json_integration.cc | 35 +++--
cpp/src/arrow/integration/json_integration_test.cc | 12 +-
cpp/src/arrow/integration/json_internal.cc | 45 +++---
cpp/src/arrow/integration/json_internal.h | 17 ++-
cpp/src/arrow/integration/meson.build | 7 +-
cpp/src/arrow/json/CMakeLists.txt | 6 +-
cpp/src/arrow/json/json_writer_internal.cc | 118 ++++++++++++++++
cpp/src/arrow/json/json_writer_internal.h | 66 +++++++++
cpp/src/arrow/json/json_writer_internal_test.cc | 152 +++++++++++++++++++++
cpp/src/arrow/json/meson.build | 1 +
cpp/src/arrow/json/reader_test.cc | 12 +-
cpp/src/arrow/meson.build | 3 +-
dev/tasks/tasks.yml | 2 +-
17 files changed, 421 insertions(+), 69 deletions(-)
diff --git a/ci/docker/ubuntu-24.04-cpp.dockerfile
b/ci/docker/ubuntu-24.04-cpp.dockerfile
index ce2a30d980..00cc8796c5 100644
--- a/ci/docker/ubuntu-24.04-cpp.dockerfile
+++ b/ci/docker/ubuntu-24.04-cpp.dockerfile
@@ -221,4 +221,5 @@ ENV absl_SOURCE=BUNDLED \
PARQUET_BUILD_EXECUTABLES=ON \
PATH=/usr/lib/ccache/:$PATH \
PYTHON=python3 \
+ simdjson_SOURCE=BUNDLED \
xsimd_SOURCE=BUNDLED
diff --git a/cpp/cmake_modules/ThirdpartyToolchain.cmake
b/cpp/cmake_modules/ThirdpartyToolchain.cmake
index 4bef30f7df..9fc1e3d0aa 100644
--- a/cpp/cmake_modules/ThirdpartyToolchain.cmake
+++ b/cpp/cmake_modules/ThirdpartyToolchain.cmake
@@ -2843,7 +2843,7 @@ function(build_simdjson)
endfunction()
if(ARROW_WITH_SIMDJSON)
- set(ARROW_SIMDJSON_REQUIRED_VERSION "3.0.0")
+ set(ARROW_SIMDJSON_REQUIRED_VERSION "4.0.0")
resolve_dependency(simdjson
FORCE_ANY_NEWER_VERSION
TRUE
diff --git a/cpp/src/arrow/CMakeLists.txt b/cpp/src/arrow/CMakeLists.txt
index 0b21fec605..64fb4d12a5 100644
--- a/cpp/src/arrow/CMakeLists.txt
+++ b/cpp/src/arrow/CMakeLists.txt
@@ -733,7 +733,8 @@ if(ARROW_BUILD_INTEGRATION OR ARROW_BUILD_TESTS)
arrow_add_object_library(ARROW_INTEGRATION integration/json_integration.cc
integration/json_internal.cc)
foreach(ARROW_INTEGRATION_TARGET ${ARROW_INTEGRATION_TARGETS})
- target_link_libraries(${ARROW_INTEGRATION_TARGET} PRIVATE RapidJSON)
+ target_link_libraries(${ARROW_INTEGRATION_TARGET} PRIVATE RapidJSON
+
simdjson::simdjson)
endforeach()
else()
set(ARROW_INTEGRATION_TARGET_SHARED)
@@ -1039,6 +1040,7 @@ if(ARROW_JSON)
json/chunker.cc
json/converter.cc
json/from_string.cc
+ json/json_writer_internal.cc
json/object_parser.cc
json/object_writer.cc
json/parser.cc
diff --git a/cpp/src/arrow/integration/CMakeLists.txt
b/cpp/src/arrow/integration/CMakeLists.txt
index 267d0adf11..8374d6de10 100644
--- a/cpp/src/arrow/integration/CMakeLists.txt
+++ b/cpp/src/arrow/integration/CMakeLists.txt
@@ -21,12 +21,17 @@ arrow_install_all_headers("arrow/integration")
# - an executable that can be called to answer integration test requests
# - a self-(unit)test for the C++ side of integration testing
if(ARROW_BUILD_TESTS)
- add_arrow_test(json_integration_test EXTRA_LINK_LIBS RapidJSON
${GFLAGS_LIBRARIES})
+ add_arrow_test(json_integration_test
+ EXTRA_LINK_LIBS
+ RapidJSON
+ simdjson::simdjson
+ ${GFLAGS_LIBRARIES})
add_dependencies(arrow-integration arrow-json-integration-test)
elseif(ARROW_BUILD_INTEGRATION)
add_executable(arrow-json-integration-test json_integration_test.cc)
target_link_libraries(arrow-json-integration-test
RapidJSON
+ simdjson::simdjson
${ARROW_TEST_LINK_LIBS}
${GFLAGS_LIBRARIES}
${ARROW_GTEST_GTEST})
diff --git a/cpp/src/arrow/integration/json_integration.cc
b/cpp/src/arrow/integration/json_integration.cc
index f1844bcbda..d7c0fa59e4 100644
--- a/cpp/src/arrow/integration/json_integration.cc
+++ b/cpp/src/arrow/integration/json_integration.cc
@@ -27,6 +27,7 @@
#include "arrow/integration/json_internal.h"
#include "arrow/io/file.h"
#include "arrow/ipc/dictionary.h"
+#include "arrow/json/json_writer_internal.h"
#include "arrow/record_batch.h"
#include "arrow/result.h"
#include "arrow/status.h"
@@ -36,6 +37,8 @@
using arrow::ipc::DictionaryFieldMapper;
using arrow::ipc::DictionaryMemo;
+using JsonWriter = arrow::json::JsonWriter;
+
namespace arrow::internal::integration {
// ----------------------------------------------------------------------
@@ -44,13 +47,10 @@ namespace arrow::internal::integration {
class IntegrationJsonWriter::Impl {
public:
explicit Impl(const std::shared_ptr<Schema>& schema)
- : schema_(schema), mapper_(*schema), first_batch_written_(false) {
- writer_.reset(new RjWriter(string_buffer_));
- }
-
+ : schema_(schema), mapper_(*schema), first_batch_written_(false) {}
Status Start() {
- writer_->StartObject();
- RETURN_NOT_OK(json::WriteSchema(*schema_, mapper_, writer_.get()));
+ writer_.StartObject();
+ RETURN_NOT_OK(json::WriteSchema(*schema_, mapper_, &writer_));
return Status::OK();
}
@@ -59,26 +59,26 @@ class IntegrationJsonWriter::Impl {
// Write dictionaries, if any
if (!dictionaries.empty()) {
- writer_->Key("dictionaries");
- writer_->StartArray();
+ writer_.Key("dictionaries");
+ writer_.StartArray();
for (const auto& entry : dictionaries) {
- RETURN_NOT_OK(json::WriteDictionary(entry.first, entry.second,
writer_.get()));
+ RETURN_NOT_OK(json::WriteDictionary(entry.first, entry.second,
&writer_));
}
- writer_->EndArray();
+ writer_.EndArray();
}
// Record batches
- writer_->Key("batches");
- writer_->StartArray();
+ writer_.Key("batches");
+ writer_.StartArray();
first_batch_written_ = true;
return Status::OK();
}
Result<std::string> Finish() {
- writer_->EndArray(); // Record batches
- writer_->EndObject();
+ writer_.EndArray(); // Record batches
+ writer_.EndObject();
- return string_buffer_.GetString();
+ return std::string(writer_.GetString());
}
Status WriteRecordBatch(const RecordBatch& batch) {
@@ -87,7 +87,7 @@ class IntegrationJsonWriter::Impl {
if (!first_batch_written_) {
RETURN_NOT_OK(FirstRecordBatch(batch));
}
- return json::WriteRecordBatch(batch, writer_.get());
+ return json::WriteRecordBatch(batch, &writer_);
}
private:
@@ -96,8 +96,7 @@ class IntegrationJsonWriter::Impl {
bool first_batch_written_;
- rj::StringBuffer string_buffer_;
- std::unique_ptr<RjWriter> writer_;
+ JsonWriter writer_;
};
IntegrationJsonWriter::IntegrationJsonWriter(const std::shared_ptr<Schema>&
schema) {
diff --git a/cpp/src/arrow/integration/json_integration_test.cc
b/cpp/src/arrow/integration/json_integration_test.cc
index 23105e8ca1..e7a55d13e0 100644
--- a/cpp/src/arrow/integration/json_integration_test.cc
+++ b/cpp/src/arrow/integration/json_integration_test.cc
@@ -38,6 +38,7 @@
#include "arrow/ipc/reader.h"
#include "arrow/ipc/test_common.h"
#include "arrow/ipc/writer.h"
+#include "arrow/json/json_writer_internal.h"
#include "arrow/pretty_print.h"
#include "arrow/status.h"
#include "arrow/testing/builder.h"
@@ -723,8 +724,7 @@ static const char* json_example6 = R"example(
)example";
void TestSchemaRoundTrip(const std::shared_ptr<Schema>& schema) {
- rj::StringBuffer sb;
- rj::Writer<rj::StringBuffer> writer(sb);
+ arrow::json::JsonWriter writer;
DictionaryFieldMapper mapper(*schema);
@@ -732,7 +732,7 @@ void TestSchemaRoundTrip(const std::shared_ptr<Schema>&
schema) {
ASSERT_OK(json::WriteSchema(*schema, mapper, &writer));
writer.EndObject();
- std::string json_schema = sb.GetString();
+ std::string json_schema(writer.GetString());
rj::Document d;
// Pass explicit size to avoid ASAN issues with
@@ -748,12 +748,11 @@ void TestSchemaRoundTrip(const std::shared_ptr<Schema>&
schema) {
void TestArrayRoundTrip(const Array& array) {
static std::string name = "dummy";
- rj::StringBuffer sb;
- rj::Writer<rj::StringBuffer> writer(sb);
+ arrow::json::JsonWriter writer;
ASSERT_OK(json::WriteArray(name, array, &writer));
- std::string array_as_json = sb.GetString();
+ std::string array_as_json(writer.GetString());
rj::Document d;
// Pass explicit size to avoid ASAN issues with
@@ -768,7 +767,6 @@ void TestArrayRoundTrip(const Array& array) {
json::ReadArray(default_memory_pool(), d, ::arrow::field(name,
array.type())));
ASSERT_OK(result_array->ValidateFull());
- // std::cout << array_as_json << std::endl;
CompareArraysDetailed(0, *result_array, array);
}
diff --git a/cpp/src/arrow/integration/json_internal.cc
b/cpp/src/arrow/integration/json_internal.cc
index 1e57be51b0..d0baad67e1 100644
--- a/cpp/src/arrow/integration/json_internal.cc
+++ b/cpp/src/arrow/integration/json_internal.cc
@@ -36,6 +36,7 @@
#include "arrow/array/builder_time.h"
#include "arrow/extension_type.h"
#include "arrow/ipc/dictionary.h"
+#include "arrow/json/json_writer_internal.h"
#include "arrow/record_batch.h"
#include "arrow/result.h"
#include "arrow/scalar.h"
@@ -64,6 +65,8 @@ using arrow::ipc::DictionaryFieldMapper;
using arrow::ipc::DictionaryMemo;
using arrow::ipc::internal::FieldPosition;
+using JsonWriter = arrow::json::JsonWriter;
+
namespace arrow::internal::integration::json {
namespace {
@@ -118,7 +121,7 @@ Result<std::string_view> GetStringView(const rj::Value&
str) {
class SchemaWriter {
public:
explicit SchemaWriter(const Schema& schema, const DictionaryFieldMapper&
mapper,
- RjWriter* writer)
+ JsonWriter* writer)
: schema_(schema), mapper_(mapper), writer_(writer) {}
Status Write() {
@@ -460,7 +463,7 @@ class SchemaWriter {
private:
const Schema& schema_;
const DictionaryFieldMapper& mapper_;
- RjWriter* writer_;
+ JsonWriter* writer_;
};
Status SchemaWriter::VisitType(const DataType& type) {
@@ -469,7 +472,7 @@ Status SchemaWriter::VisitType(const DataType& type) {
class ArrayWriter {
public:
- ArrayWriter(const std::string& name, const Array& array, RjWriter* writer)
+ ArrayWriter(const std::string& name, const Array& array, JsonWriter* writer)
: name_(name), array_(array), writer_(writer) {}
Status Write() { return VisitArray(name_, array_); }
@@ -490,11 +493,7 @@ class ArrayWriter {
return Status::OK();
}
- void WriteRawNumber(std::string_view v) {
- // Avoid RawNumber() as it misleadingly adds quotes
- // (see https://github.com/Tencent/rapidjson/pull/1155)
- writer_->RawValue(v.data(), v.size(), rj::kNumberType);
- }
+ void WriteRawNumber(std::string_view v) { writer_->RawValue(v); }
template <typename ArrayType, typename TypeClass = typename
ArrayType::TypeClass,
typename CType = typename TypeClass::c_type>
@@ -522,11 +521,10 @@ class ArrayWriter {
for (int64_t i = 0; i < arr.length(); ++i) {
if (arr.IsValid(i)) {
fmt(arr.Value(i), [&](std::string_view repr) {
- writer_->String(repr.data(), static_cast<rj::SizeType>(repr.size()));
+ writer_->String(std::string_view(repr.data(), repr.size()));
});
} else {
- writer_->String(null_string.data(),
- static_cast<rj::SizeType>(null_string.size()));
+ writer_->String(std::string_view(null_string.data(),
null_string.size()));
}
}
}
@@ -553,7 +551,7 @@ class ArrayWriter {
if constexpr (Type::is_utf8) {
// UTF8 string, write as is
auto view = arr.GetView(i);
- writer_->String(view.data(), static_cast<rj::SizeType>(view.size()));
+ writer_->String(std::string_view(view.data(), view.size()));
} else {
// Binary, encode to hexadecimal.
writer_->String(HexEncode(arr.GetView(i)));
@@ -598,7 +596,7 @@ class ArrayWriter {
const Decimal32 value(arr.GetValue(i));
writer_->String(value.ToIntegerString());
} else {
- writer_->String(null_string, sizeof(null_string));
+ writer_->String(std::string_view(null_string));
}
}
}
@@ -610,7 +608,7 @@ class ArrayWriter {
const Decimal64 value(arr.GetValue(i));
writer_->String(value.ToIntegerString());
} else {
- writer_->String(null_string, sizeof(null_string));
+ writer_->String(std::string_view(null_string));
}
}
}
@@ -622,7 +620,7 @@ class ArrayWriter {
const Decimal128 value(arr.GetValue(i));
writer_->String(value.ToIntegerString());
} else {
- writer_->String(null_string, sizeof(null_string));
+ writer_->String(std::string_view(null_string));
}
}
}
@@ -634,7 +632,7 @@ class ArrayWriter {
const Decimal256 value(arr.GetValue(i));
writer_->String(value.ToIntegerString());
} else {
- writer_->String(null_string, sizeof(null_string));
+ writer_->String(std::string_view(null_string));
}
}
}
@@ -670,7 +668,7 @@ class ArrayWriter {
// them exactly.
::arrow::internal::StringFormatter<typename CTypeTraits<T>::ArrowType>
formatter;
auto append = [this](std::string_view v) {
- writer_->String(v.data(), static_cast<rj::SizeType>(v.size()));
+ writer_->String(std::string_view(v.data(), v.size()));
return Status::OK();
};
for (int i = 0; i < length; ++i) {
@@ -692,7 +690,8 @@ class ArrayWriter {
if (s.is_inline()) {
writer_->Key("INLINED");
if constexpr (ArrayType::TypeClass::is_utf8) {
- writer_->String(reinterpret_cast<const char*>(s.inline_data()),
s.size());
+ writer_->String(
+ std::string_view(reinterpret_cast<const char*>(s.inline_data()),
s.size()));
} else {
writer_->String(HexEncode(s.inline_data(), s.size()));
}
@@ -863,7 +862,7 @@ class ArrayWriter {
private:
const std::string& name_;
const Array& array_;
- RjWriter* writer_;
+ JsonWriter* writer_;
};
Result<TimeUnit::type> GetUnitFromString(const std::string& unit_str) {
@@ -2035,13 +2034,13 @@ Result<std::shared_ptr<RecordBatch>> ReadRecordBatch(
}
Status WriteSchema(const Schema& schema, const DictionaryFieldMapper& mapper,
- RjWriter* json_writer) {
+ JsonWriter* json_writer) {
SchemaWriter converter(schema, mapper, json_writer);
return converter.Write();
}
Status WriteDictionary(int64_t id, const std::shared_ptr<Array>& dictionary,
- RjWriter* writer) {
+ JsonWriter* writer) {
writer->StartObject();
writer->Key("id");
writer->Int(static_cast<int32_t>(id));
@@ -2055,7 +2054,7 @@ Status WriteDictionary(int64_t id, const
std::shared_ptr<Array>& dictionary,
return Status::OK();
}
-Status WriteRecordBatch(const RecordBatch& batch, RjWriter* writer) {
+Status WriteRecordBatch(const RecordBatch& batch, JsonWriter* writer) {
writer->StartObject();
writer->Key("count");
writer->Int(static_cast<int32_t>(batch.num_rows()));
@@ -2076,7 +2075,7 @@ Status WriteRecordBatch(const RecordBatch& batch,
RjWriter* writer) {
return Status::OK();
}
-Status WriteArray(const std::string& name, const Array& array, RjWriter*
json_writer) {
+Status WriteArray(const std::string& name, const Array& array, JsonWriter*
json_writer) {
ArrayWriter converter(name, array, json_writer);
return converter.Write();
}
diff --git a/cpp/src/arrow/integration/json_internal.h
b/cpp/src/arrow/integration/json_internal.h
index ac5bd2069d..b87c102ca9 100644
--- a/cpp/src/arrow/integration/json_internal.h
+++ b/cpp/src/arrow/integration/json_internal.h
@@ -36,9 +36,12 @@
#include "arrow/util/visibility.h"
namespace rj = arrow::rapidjson;
-using RjWriter = rj::Writer<rj::StringBuffer>;
-using RjArray = rj::Value::ConstArray;
using RjObject = rj::Value::ConstObject;
+using RjArray = rj::Value::ConstArray;
+
+namespace arrow::json {
+class JsonWriter;
+} // namespace arrow::json
#define RETURN_NOT_FOUND(TOK, NAME, PARENT) \
if (NAME == (PARENT).MemberEnd()) { \
@@ -77,20 +80,20 @@ using RjObject = rj::Value::ConstObject;
namespace arrow::internal::integration::json {
-/// \brief Append integration test Schema format to rapidjson writer
+/// \brief Append integration test Schema format to JSON writer
ARROW_EXPORT
Status WriteSchema(const Schema& schema, const ipc::DictionaryFieldMapper&
mapper,
- RjWriter* writer);
+ arrow::json::JsonWriter*);
ARROW_EXPORT
Status WriteDictionary(int64_t id, const std::shared_ptr<Array>& dictionary,
- RjWriter* writer);
+ arrow::json::JsonWriter*);
ARROW_EXPORT
-Status WriteRecordBatch(const RecordBatch& batch, RjWriter* writer);
+Status WriteRecordBatch(const RecordBatch& batch, arrow::json::JsonWriter*);
ARROW_EXPORT
-Status WriteArray(const std::string& name, const Array& array, RjWriter*
writer);
+Status WriteArray(const std::string& name, const Array& array,
arrow::json::JsonWriter*);
ARROW_EXPORT
Result<std::shared_ptr<Schema>> ReadSchema(const rj::Value& json_obj,
MemoryPool* pool,
diff --git a/cpp/src/arrow/integration/meson.build
b/cpp/src/arrow/integration/meson.build
index 6437c380bb..17b6ea7449 100644
--- a/cpp/src/arrow/integration/meson.build
+++ b/cpp/src/arrow/integration/meson.build
@@ -20,7 +20,12 @@ install_headers(['json_integration.h'])
exc = executable(
'arrow-json-integration-test',
sources: ['json_integration_test.cc'],
- dependencies: [arrow_test_dep_no_main, rapidjson_dep, gflags_dep],
+ dependencies: [
+ arrow_test_dep_no_main,
+ rapidjson_dep,
+ simdjson_dep,
+ gflags_dep,
+ ],
)
arrow_c_data_integration_lib = library(
diff --git a/cpp/src/arrow/json/CMakeLists.txt
b/cpp/src/arrow/json/CMakeLists.txt
index b5b47c0ed3..e7719c771c 100644
--- a/cpp/src/arrow/json/CMakeLists.txt
+++ b/cpp/src/arrow/json/CMakeLists.txt
@@ -21,13 +21,15 @@ add_arrow_test(test
chunker_test.cc
converter_test.cc
from_string_test.cc
+ json_writer_internal_test.cc
+ object_parser_test.cc
parser_test.cc
reader_test.cc
- object_parser_test.cc
PREFIX
"arrow-json"
EXTRA_LINK_LIBS
- RapidJSON)
+ RapidJSON
+ simdjson::simdjson)
add_arrow_benchmark(parser_benchmark
PREFIX
diff --git a/cpp/src/arrow/json/json_writer_internal.cc
b/cpp/src/arrow/json/json_writer_internal.cc
new file mode 100644
index 0000000000..11fe70a741
--- /dev/null
+++ b/cpp/src/arrow/json/json_writer_internal.cc
@@ -0,0 +1,118 @@
+// 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 "arrow/json/json_writer_internal.h"
+
+namespace arrow::json {
+
+void JsonWriter::StartObject() {
+ MaybeComma();
+ builder_.start_object();
+ needs_comma_ = false;
+}
+
+void JsonWriter::EndObject() {
+ builder_.end_object();
+ needs_comma_ = true;
+}
+
+void JsonWriter::StartArray() {
+ MaybeComma();
+ builder_.start_array();
+ needs_comma_ = false;
+}
+
+void JsonWriter::EndArray() {
+ builder_.end_array();
+ needs_comma_ = true;
+}
+
+void JsonWriter::Key(std::string_view key) {
+ MaybeComma();
+ builder_.escape_and_append_with_quotes(key);
+ builder_.append_colon();
+ needs_comma_ = false;
+}
+
+void JsonWriter::String(std::string_view value) {
+ MaybeComma();
+ builder_.escape_and_append_with_quotes(value);
+ needs_comma_ = true;
+}
+
+void JsonWriter::RawValue(std::string_view value) {
+ MaybeComma();
+ builder_.append_raw(value);
+ needs_comma_ = true;
+}
+
+void JsonWriter::Bool(bool value) {
+ MaybeComma();
+ builder_.append(value);
+ needs_comma_ = true;
+}
+
+void JsonWriter::Int(int32_t value) {
+ MaybeComma();
+ builder_.append(value);
+ needs_comma_ = true;
+}
+
+void JsonWriter::Int64(int64_t value) {
+ MaybeComma();
+ builder_.append(value);
+ needs_comma_ = true;
+}
+
+void JsonWriter::Uint(uint32_t value) {
+ MaybeComma();
+ builder_.append(value);
+ needs_comma_ = true;
+}
+
+void JsonWriter::Uint64(uint64_t value) {
+ MaybeComma();
+ builder_.append(value);
+ needs_comma_ = true;
+}
+
+void JsonWriter::Double(double value) {
+ MaybeComma();
+ builder_.append(value);
+ needs_comma_ = true;
+}
+
+void JsonWriter::Null() {
+ MaybeComma();
+ builder_.append_null();
+ needs_comma_ = true;
+}
+
+std::string_view JsonWriter::GetString() const { return
builder_.view().value(); }
+
+void JsonWriter::Clear() {
+ builder_.clear();
+ needs_comma_ = false;
+}
+
+void JsonWriter::MaybeComma() {
+ if (needs_comma_) {
+ builder_.append_comma();
+ }
+}
+
+} // namespace arrow::json
diff --git a/cpp/src/arrow/json/json_writer_internal.h
b/cpp/src/arrow/json/json_writer_internal.h
new file mode 100644
index 0000000000..e4d6e49885
--- /dev/null
+++ b/cpp/src/arrow/json/json_writer_internal.h
@@ -0,0 +1,66 @@
+// 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.
+
+#pragma once
+
+#include <simdjson.h>
+
+#include <cstdint>
+#include <string_view>
+
+#include "arrow/util/visibility.h"
+
+namespace arrow::json {
+
+class ARROW_EXPORT JsonWriter {
+ public:
+ JsonWriter() = default;
+
+ void StartObject();
+ void EndObject();
+
+ void StartArray();
+ void EndArray();
+
+ void Key(std::string_view key);
+
+ void String(std::string_view value);
+ void RawValue(std::string_view value);
+ void Bool(bool value);
+
+ void Int(int32_t value);
+ void Int64(int64_t value);
+
+ void Uint(uint32_t value);
+ void Uint64(uint64_t value);
+
+ void Double(double value);
+
+ void Null();
+
+ std::string_view GetString() const;
+
+ void Clear();
+
+ private:
+ void MaybeComma();
+
+ simdjson::builder::string_builder builder_;
+ bool needs_comma_ = false;
+};
+
+} // namespace arrow::json
diff --git a/cpp/src/arrow/json/json_writer_internal_test.cc
b/cpp/src/arrow/json/json_writer_internal_test.cc
new file mode 100644
index 0000000000..01e6d7ecb8
--- /dev/null
+++ b/cpp/src/arrow/json/json_writer_internal_test.cc
@@ -0,0 +1,152 @@
+// 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 <gtest/gtest.h>
+
+#include "arrow/json/json_writer_internal.h"
+
+namespace arrow::json {
+
+TEST(JsonWriter, SimpleObject) {
+ JsonWriter writer;
+
+ writer.StartObject();
+ writer.Key("a");
+ writer.Int(42);
+ writer.Key("b");
+ writer.String("hello");
+ writer.EndObject();
+
+ EXPECT_EQ(writer.GetString(), R"({"a":42,"b":"hello"})");
+}
+
+TEST(JsonWriter, Array) {
+ JsonWriter writer;
+
+ writer.StartArray();
+ writer.Int(1);
+ writer.Int(2);
+ writer.Int(3);
+ writer.EndArray();
+
+ EXPECT_EQ(writer.GetString(), "[1,2,3]");
+}
+
+TEST(JsonWriter, NestedObject) {
+ JsonWriter writer;
+
+ writer.StartObject();
+
+ writer.Key("child");
+ writer.StartObject();
+ writer.Key("x");
+ writer.Bool(true);
+ writer.EndObject();
+
+ writer.EndObject();
+
+ EXPECT_EQ(writer.GetString(), R"({"child":{"x":true}})");
+}
+
+TEST(JsonWriter, NullValue) {
+ JsonWriter writer;
+
+ writer.StartObject();
+ writer.Key("value");
+ writer.Null();
+ writer.EndObject();
+
+ EXPECT_EQ(writer.GetString(), R"({"value":null})");
+}
+
+TEST(JsonWriter, DoubleValue) {
+ JsonWriter writer;
+
+ writer.StartObject();
+ writer.Key("pi");
+ writer.Double(3.14);
+ writer.EndObject();
+
+ EXPECT_EQ(writer.GetString(), R"({"pi":3.14})");
+}
+
+TEST(JsonWriter, UnsignedValues) {
+ JsonWriter writer;
+
+ writer.StartObject();
+ writer.Key("u32");
+ writer.Uint(42);
+ writer.Key("u64");
+ writer.Uint64(1234567890123ULL);
+ writer.EndObject();
+
+ EXPECT_EQ(writer.GetString(), R"({"u32":42,"u64":1234567890123})");
+}
+
+TEST(JsonWriter, Int64Value) {
+ JsonWriter writer;
+
+ writer.StartObject();
+ writer.Key("i64");
+ writer.Int64(-1234567890123LL);
+ writer.EndObject();
+
+ EXPECT_EQ(writer.GetString(), R"({"i64":-1234567890123})");
+}
+
+TEST(JsonWriter, Clear) {
+ JsonWriter writer;
+
+ writer.StartObject();
+ writer.Key("a");
+ writer.Int(1);
+ writer.EndObject();
+
+ writer.Clear();
+
+ writer.StartArray();
+ writer.Int(5);
+ writer.EndArray();
+
+ EXPECT_EQ(writer.GetString(), "[5]");
+}
+
+TEST(JsonWriter, RawValue) {
+ JsonWriter writer;
+
+ writer.StartObject();
+ writer.Key("number");
+ writer.RawValue("123.456");
+ writer.EndObject();
+
+ ASSERT_EQ(writer.GetString(), R"({"number":123.456})");
+}
+
+TEST(JsonWriter, StringWithExplicitLength) {
+ JsonWriter writer;
+
+ const char value[] = {'a', 'b', 'c', 'd', 'e'};
+
+ writer.StartObject();
+ writer.Key("value");
+ writer.String(std::string_view(value, 3));
+ writer.EndObject();
+
+ ASSERT_EQ(writer.GetString(), R"({"value":"abc"})");
+}
+
+} // namespace arrow::json
diff --git a/cpp/src/arrow/json/meson.build b/cpp/src/arrow/json/meson.build
index bc1567df9e..a2aa69ecf7 100644
--- a/cpp/src/arrow/json/meson.build
+++ b/cpp/src/arrow/json/meson.build
@@ -24,6 +24,7 @@ exc = executable(
'from_string_test.cc',
'parser_test.cc',
'reader_test.cc',
+ 'json_writer_internal_test.cc',
],
dependencies: [arrow_test_dep, rapidjson_dep],
)
diff --git a/cpp/src/arrow/json/reader_test.cc
b/cpp/src/arrow/json/reader_test.cc
index 98bdac96c2..2aca602ae9 100644
--- a/cpp/src/arrow/json/reader_test.cc
+++ b/cpp/src/arrow/json/reader_test.cc
@@ -39,7 +39,7 @@ namespace json {
using std::string_view;
-using internal::checked_cast;
+using arrow::internal::checked_cast;
static Result<std::shared_ptr<Table>> ReadToTable(std::string json,
const ReadOptions&
read_options,
@@ -597,7 +597,7 @@ class StreamingReaderTestBase {
return out;
}
- internal::Executor* executor_ = nullptr;
+ arrow::internal::Executor* executor_ = nullptr;
ParseOptions parse_options_ = ParseOptions::Defaults();
ReadOptions read_options_ = ReadOptions::Defaults();
io::IOContext io_context_ = io::default_io_context();
@@ -965,7 +965,7 @@ TEST_F(AsyncStreamingReaderTest, AsyncReentrancy) {
ASSERT_FINISHES_OK_AND_ASSIGN(auto results, All(std::move(futures)));
EXPECT_EQ(reader->bytes_processed(), expected.json_size);
- ASSERT_OK_AND_ASSIGN(auto batches,
internal::UnwrapOrRaise(std::move(results)));
+ ASSERT_OK_AND_ASSIGN(auto batches,
arrow::internal::UnwrapOrRaise(std::move(results)));
AssertBatchSequenceEquals(expected.batches, batches);
}
@@ -989,7 +989,7 @@ TEST_F(AsyncStreamingReaderTest, FuturesOutliveReader) {
}
ASSERT_FINISHES_OK_AND_ASSIGN(auto results, All(std::move(futures)));
- ASSERT_OK_AND_ASSIGN(auto batches,
internal::UnwrapOrRaise(std::move(results)));
+ ASSERT_OK_AND_ASSIGN(auto batches,
arrow::internal::UnwrapOrRaise(std::move(results)));
AssertBatchSequenceEquals(expected.batches, batches);
}
@@ -1009,7 +1009,7 @@ TEST_F(AsyncStreamingReaderTest, StressBufferedReads) {
}
ASSERT_FINISHES_OK_AND_ASSIGN(auto results, All(std::move(futures)));
- ASSERT_OK_AND_ASSIGN(auto batches, internal::UnwrapOrRaise(results));
+ ASSERT_OK_AND_ASSIGN(auto batches, arrow::internal::UnwrapOrRaise(results));
AssertBatchSequenceEquals(expected.batches, batches);
}
@@ -1023,7 +1023,7 @@ TEST_F(AsyncStreamingReaderTest,
StressSharedIoAndCpuExecutor) {
read_options_.block_size = expected.block_size;
// Force the serial -> parallel pipeline to contend for a single thread
- ASSERT_OK_AND_ASSIGN(auto thread_pool, internal::ThreadPool::Make(1));
+ ASSERT_OK_AND_ASSIGN(auto thread_pool, arrow::internal::ThreadPool::Make(1));
io_context_ = io::IOContext(thread_pool.get());
executor_ = thread_pool.get();
diff --git a/cpp/src/arrow/meson.build b/cpp/src/arrow/meson.build
index 831bc12180..ef044f6c42 100644
--- a/cpp/src/arrow/meson.build
+++ b/cpp/src/arrow/meson.build
@@ -510,13 +510,14 @@ if needs_json
'extension/opaque.cc',
'extension/tensor_internal.cc',
'extension/variable_shape_tensor.cc',
- 'json/options.cc',
'json/chunked_builder.cc',
'json/chunker.cc',
'json/converter.cc',
'json/from_string.cc',
+ 'json/json_writer_internal.cc',
'json/object_parser.cc',
'json/object_writer.cc',
+ 'json/options.cc',
'json/parser.cc',
'json/reader.cc',
],
diff --git a/dev/tasks/tasks.yml b/dev/tasks/tasks.yml
index 89d239a0a2..af8b319dca 100644
--- a/dev/tasks/tasks.yml
+++ b/dev/tasks/tasks.yml
@@ -645,7 +645,7 @@ tasks:
ci: github
template: docker-tests/github.linux.yml
params:
- flags: '-e ARROW_DEPENDENCY_SOURCE=SYSTEM -e ARROW_GCS=OFF -e
xsimd_SOURCE=BUNDLED'
+ flags: '-e ARROW_DEPENDENCY_SOURCE=SYSTEM -e ARROW_GCS=OFF -e
simdjson_SOURCE=BUNDLED -e xsimd_SOURCE=BUNDLED'
image: ubuntu-r-only-r
test-r-dev-duckdb: