GH-50830: [C++][Parquet] Use JsonWriter for LogicalType::ToJSON() - #50877
GH-50830: [C++][Parquet] Use JsonWriter for LogicalType::ToJSON()#50877NathanChung4 wants to merge 4 commits into
Conversation
I changed the 8 ToJSON() functions so that they now use JsonWriter instead of the old hand build strings. I deleted WriteCrsKeyAndValue since it was no longer needed, and I updated the test files to match the changes made in types.cc
There was a problem hiding this comment.
Pull request overview
Refactors Parquet C++ LogicalType::ToJSON() implementations to consistently use ::arrow::json::JsonWriter rather than manual string/stream construction, resulting in compact JSON output and reduced risk of producing invalid JSON for unescaped values.
Changes:
- Replaced hand-built JSON in 8
LogicalType::Impl::*::ToJSON()implementations withJsonWriter-based construction. - Removed the now-unneeded
WriteCrsKeyAndValuehelper and simplified CRS emission toStringField. - Updated Parquet schema and reader tests to match the new compact JSON formatting.
Reviewed changes
Copilot reviewed 1 out of 3 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| cpp/src/parquet/types.cc | Converts LogicalType::ToJSON() methods to JsonWriter and removes manual/stream JSON helper code. |
| cpp/src/parquet/schema_test.cc | Updates expected ToJSON() strings for logical types to match compact JSON output. |
| cpp/src/parquet/reader_test.cc | Updates expected JSON substrings containing embedded logical-type JSON to match compact formatting. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| writer.StringField("Type", "Decimal"); | ||
| writer.Key("precision"); | ||
| writer.Int(precision_); | ||
| writer.Key("scale"); |
There was a problem hiding this comment.
Could we add an IntField() helper to JsonWriter and use it here, similar to StringField() and BoolField()? It would make these conversions a bit cleaner.
There was a problem hiding this comment.
Hey Reranko05, thank you for the suggestion and I totally agree with you. I have committed the proposed change, please let me know if you want to change anything or anything
Added IntField() to JsonWriter per Reranko05's PR review suggestion. Updated the 3 different functions: Decimal, Int, and Variant that had the Key() + Int() two call pattern
|
@kou Can you review this when you have time? |
| { "Id": "0", "Name": "id", "PhysicalType": "INT32", "ConvertedType": "NONE", "LogicalType": {"Type": "None"} }, | ||
| { "Id": "1", "Name": "bool_col", "PhysicalType": "BOOLEAN", "ConvertedType": "NONE", "LogicalType": {"Type": "None"} }, | ||
| { "Id": "2", "Name": "tinyint_col", "PhysicalType": "INT32", "ConvertedType": "NONE", "LogicalType": {"Type": "None"} }, | ||
| { "Id": "3", "Name": "smallint_col", "PhysicalType": "INT32", "ConvertedType": "NONE", "LogicalType": {"Type": "None"} }, | ||
| { "Id": "4", "Name": "int_col", "PhysicalType": "INT32", "ConvertedType": "NONE", "LogicalType": {"Type": "None"} }, | ||
| { "Id": "5", "Name": "bigint_col", "PhysicalType": "INT64", "ConvertedType": "NONE", "LogicalType": {"Type": "None"} }, | ||
| { "Id": "6", "Name": "float_col", "PhysicalType": "FLOAT", "ConvertedType": "NONE", "LogicalType": {"Type": "None"} }, | ||
| { "Id": "7", "Name": "double_col", "PhysicalType": "DOUBLE", "ConvertedType": "NONE", "LogicalType": {"Type": "None"} }, | ||
| { "Id": "8", "Name": "date_string_col", "PhysicalType": "BYTE_ARRAY", "ConvertedType": "NONE", "LogicalType": {"Type": "None"} }, | ||
| { "Id": "9", "Name": "string_col", "PhysicalType": "BYTE_ARRAY", "ConvertedType": "NONE", "LogicalType": {"Type": "None"} }, | ||
| { "Id": "10", "Name": "timestamp_col", "PhysicalType": "INT96", "ConvertedType": "NONE", "LogicalType": {"Type": "None"} } | ||
| { "Id": "0", "Name": "id", "PhysicalType": "INT32", "ConvertedType": "NONE", "LogicalType": {"Type":"None"} }, | ||
| { "Id": "1", "Name": "bool_col", "PhysicalType": "BOOLEAN", "ConvertedType": "NONE", "LogicalType": {"Type":"None"} }, | ||
| { "Id": "2", "Name": "tinyint_col", "PhysicalType": "INT32", "ConvertedType": "NONE", "LogicalType": {"Type":"None"} }, | ||
| { "Id": "3", "Name": "smallint_col", "PhysicalType": "INT32", "ConvertedType": "NONE", "LogicalType": {"Type":"None"} }, | ||
| { "Id": "4", "Name": "int_col", "PhysicalType": "INT32", "ConvertedType": "NONE", "LogicalType": {"Type":"None"} }, | ||
| { "Id": "5", "Name": "bigint_col", "PhysicalType": "INT64", "ConvertedType": "NONE", "LogicalType": {"Type":"None"} }, | ||
| { "Id": "6", "Name": "float_col", "PhysicalType": "FLOAT", "ConvertedType": "NONE", "LogicalType": {"Type":"None"} }, | ||
| { "Id": "7", "Name": "double_col", "PhysicalType": "DOUBLE", "ConvertedType": "NONE", "LogicalType": {"Type":"None"} }, | ||
| { "Id": "8", "Name": "date_string_col", "PhysicalType": "BYTE_ARRAY", "ConvertedType": "NONE", "LogicalType": {"Type":"None"} }, | ||
| { "Id": "9", "Name": "string_col", "PhysicalType": "BYTE_ARRAY", "ConvertedType": "NONE", "LogicalType": {"Type":"None"} }, | ||
| { "Id": "10", "Name": "timestamp_col", "PhysicalType": "INT96", "ConvertedType": "NONE", "LogicalType": {"Type":"None"} } |
There was a problem hiding this comment.
Could you use JsonWriter in
arrow/cpp/src/parquet/printer.cc
Lines 254 to 435 in f64e90a
It seems that we can use https://github.com/simdjson/simdjson/blob/master/doc/builder.md#pretty-formatted-fractured-json for it.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 6 changed files in this pull request and generated no new comments.
Suppressed comments (3)
cpp/src/parquet/reader_test.cc:1169
- These tests now assert exact pretty-printed JSON bytes. Since the formatting is produced by simdjson::fractured_json_string (and may change across simdjson updates), the tests can become brittle even if the JSON content is semantically unchanged. Consider parsing + asserting key fields, or normalizing both strings (e.g., minify/canonicalize) before comparison.
std::string json_content = ReadFromLocalFile("nested_lists.snappy.parquet");
ASSERT_EQ(json_output, json_content);
}
cpp/src/parquet/printer.cc:429
- JSONPrint now builds JSON and then reparses it via GetPrettyString() to pretty-print. For large file metadata this adds a full extra parse + allocation pass; consider either (a) emitting compact JSON via GetString() here, or (b) making pretty-print optional (flag/arg) so callers can avoid the extra work when not needed.
writer.EndObject();
PARQUET_ASSIGN_OR_THROW(std::string pretty_json, writer.GetPrettyString());
stream << pretty_json << "\n";
cpp/src/arrow/json/json_writer_internal.h:66
- New JsonWriter APIs (IntField / GetPrettyString) are now relied on by Parquet code, but there are no unit tests covering their behavior. Adding small gtest coverage would help catch formatting or type-regression issues early (especially since GetPrettyString depends on simdjson pretty-print behavior).
void StringField(std::string_view key, std::string_view value);
void BoolField(std::string_view key, bool value);
void IntField(std::string_view key, int32_t value);
Result<std::string_view> GetString() const;
Result<std::string> GetPrettyString(
const simdjson::fractured_json_options& options = {}) const;
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 6 out of 7 changed files in this pull request and generated no new comments.
Suppressed comments (2)
cpp/src/parquet/printer.cc:270
JSONPrint()now builds the entire JSON document in-memory and then pretty-prints it (GetPrettyString()), which duplicates the JSON (builder buffer + pretty string) before writing tostream. For large files with many row groups/columns, this is a potentially significant memory regression compared to the previous streaming implementation.
Consider making pretty-printing optional (defaulting to compact GetString()), or preserving a streaming approach for large outputs.
void ParquetFilePrinter::JSONPrint(std::ostream& stream, std::list<int> selected_columns,
const char* filename) {
const FileMetaData* file_metadata = fileReader->metadata().get();
::arrow::json::JsonWriter writer;
writer.StartObject();
writer.StringField("FileName", filename);
writer.StringField("Version", ParquetVersionToString(file_metadata->version()));
writer.StringField("CreatedBy", file_metadata->created_by());
writer.StringField("TotalRows", std::to_string(file_metadata->num_rows()));
writer.StringField("NumberOfRowGroups",
std::to_string(file_metadata->num_row_groups()));
writer.StringField(
"NumberOfRealColumns",
std::to_string(file_metadata->schema()->group_node()->field_count()));
writer.StringField("NumberOfColumns", std::to_string(file_metadata->num_columns()));
cpp/src/parquet/reader_test.cc:1192
JSONOutputcompares the entire pretty-printed JSON string, but the expectedColumnsentries here look inconsistent with howParquetFilePrinter::JSONPrint()now writes fields (writer order is Id/Name/PhysicalType/ConvertedType/LogicalType, andGetPrettyString()formatting is unlikely to emit the aligned spacing + reordered keys shown). This is likely to make the test fail and/or be very brittle across simdjson pretty-printer changes.
Consider either regenerating the golden string from the new output, or (preferably) parsing json_content and asserting on the relevant fields/values instead of full-string equality.
std::string json_output = R"###({
"FileName": "alltypes_plain.parquet",
"Version": "1.0",
"CreatedBy": "impala version 1.3.0-INTERNAL (build 8a48ddb1eff84592b3fc06bc6f51ec120e1fffc9)",
"TotalRows": "8",
"NumberOfRowGroups": "1",
"NumberOfRealColumns": "11",
"NumberOfColumns": "11",
"Columns": [
{ "ConvertedType": "NONE", "Id": "0" , "LogicalType": { "Type": "None" }, "Name": "id" , "PhysicalType": "INT32" },
{ "ConvertedType": "NONE", "Id": "1" , "LogicalType": { "Type": "None" }, "Name": "bool_col" , "PhysicalType": "BOOLEAN" },
{ "ConvertedType": "NONE", "Id": "2" , "LogicalType": { "Type": "None" }, "Name": "tinyint_col" , "PhysicalType": "INT32" },
{ "ConvertedType": "NONE", "Id": "3" , "LogicalType": { "Type": "None" }, "Name": "smallint_col" , "PhysicalType": "INT32" },
{ "ConvertedType": "NONE", "Id": "4" , "LogicalType": { "Type": "None" }, "Name": "int_col" , "PhysicalType": "INT32" },
{ "ConvertedType": "NONE", "Id": "5" , "LogicalType": { "Type": "None" }, "Name": "bigint_col" , "PhysicalType": "INT64" },
{ "ConvertedType": "NONE", "Id": "6" , "LogicalType": { "Type": "None" }, "Name": "float_col" , "PhysicalType": "FLOAT" },
{ "ConvertedType": "NONE", "Id": "7" , "LogicalType": { "Type": "None" }, "Name": "double_col" , "PhysicalType": "DOUBLE" },
{ "ConvertedType": "NONE", "Id": "8" , "LogicalType": { "Type": "None" }, "Name": "date_string_col", "PhysicalType": "BYTE_ARRAY" },
{ "ConvertedType": "NONE", "Id": "9" , "LogicalType": { "Type": "None" }, "Name": "string_col" , "PhysicalType": "BYTE_ARRAY" },
{ "ConvertedType": "NONE", "Id": "10", "LogicalType": { "Type": "None" }, "Name": "timestamp_col" , "PhysicalType": "INT96" }
],
Rationale for this change
This change was made because it refactored ToJSON() to build JSON through a proper writer instead of hand-built strings, which removes a correctness risk. It also aligns the codebase's ongoing transition away from manual JSON construction.
What changes are included in this PR?
This PR focused on changing the 8 ToJSON() functions from hand built strings to using JsonWriter. In addition, the test files (schema_test.cc, reader_test.cc) were also updated to reflect these changes. Finally, WriteCrsKeyAndValue were no longer needed so it was deleted.
Are these changes tested?
These changes are tested and verified. There was a parquet-schema-test in which 41/41 testcases passed. There was a parquet-reader-test in which 149/154 passed and 5 were skipped as they were unrelated. A Full ctest -R "^parquet-": 11/11 suites passed. And finally the pre-commit (C++ Format + C++ Lint) came out clean.
Are there any user-facing changes?
Yes, there are user-facing changes. ToJSON() now produces compact JSON instead of spaced JSON. It's the same valid JSON, but different exact bytes:
AI Disclosure
Per the AI-generated code guidance: the 8 ToJSON() transitions to JsonWriter, test file changes, the merge conflict resolutions, and the test were produced with Claude Code, and reviewed and verified by me. Correctness was checked by rebuilding and running the relevant test after every single function conversion, not just once at the end. In addition, catching two mistakes early via failing tests (the Decimal attempt that dropped fields, the Int attempt that did the same).
JsonWriterconsistently inLogicalType::ToJSON()#50830