Skip to content

GH-46371: [C++][Parquet] Parquet Variant decoding tools - #46372

Closed
mapleFU wants to merge 34 commits into
apache:mainfrom
mapleFU:variant-cpp-decoder-tools
Closed

GH-46371: [C++][Parquet] Parquet Variant decoding tools#46372
mapleFU wants to merge 34 commits into
apache:mainfrom
mapleFU:variant-cpp-decoder-tools

Conversation

@mapleFU

@mapleFUmapleFU commented May 9, 2025

Copy link
Copy Markdown
Member

Rationale for this change

This patch supports tool to decode the parquet variant.

What changes are included in this PR?

This patch supports tool to decode the parquet variant.

Are these changes tested?

Yes. I uses parquet-testings. Some problems is listed here: apache/parquet-testing#79

I can also add some hand-written tests after interface is agreed.

Are there any user-facing changes?

Yes, this adds interfaces for decode variant.

@github-actions

Copy link
Copy Markdown

Thanks for opening a pull request!

If this is not a minor PR. Could you open an issue for this pull request on GitHub? https://github.com/apache/arrow/issues/new/choose

Opening GitHub issues ahead of time contributes to the Openness of the Apache Arrow project.

Then could you also rename the pull request title in the following format?

GH-${GITHUB_ISSUE_ID}: [${COMPONENT}] ${SUMMARY}

or

MINOR: [${COMPONENT}] ${SUMMARY}

See also:

@mapleFUmapleFU changed the title [C++][Parquet] Parquet Variant decoding toolsGH-46371: [C++][Parquet] Parquet Variant decoding toolsMay 9, 2025
@github-actions

Copy link
Copy Markdown

⚠️ GitHub issue #46371has been automatically assigned in GitHub to PR creator.

Comment threadcpp/src/parquet/variant_test.cpp Outdated
Comment threadcpp/src/parquet/variant.h Outdated
@github-actionsgithub-actionsBot added awaiting committer review Awaiting committer review and removed awaiting review Awaiting review labels May 13, 2025
Comment threadcpp/src/parquet/variant.h
/// \defgroup ValueAccessors
/// @{

// Note: Null doesn't need visitor.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I don't know should we just return an arrow's Scalar, it would be easy to use but in-efficient.

Comment threadcpp/src/parquet/variant.h Outdated
Comment threadcpp/src/parquet/variant.cc Outdated
Comment threadcpp/src/parquet/variant.cc Outdated
Comment threadcpp/src/parquet/variant.h Outdated
Comment on lines 156 to 160

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Currently, getInt64 only supports read from int64, which is too strict for integer. I think we can also uses some way to allow getInt64 to get some "smaller types" like int32, int16, int8.

Comment threadcpp/src/parquet/variant.h Outdated

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Currently I didn't check utf-8 here.

Comment threadcpp/src/parquet/variant.h Outdated

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Currently, getDouble only supports read from getFloat, which is too strict for. Maybe we can also uses some way to allow getDouble get other types

Comment threadcpp/src/parquet/variant.cc Outdated

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I think should we use a extra function here like "Validate", or just checks them here?

Comment threadcpp/src/parquet/variant.cc Outdated
@mapleFU
mapleFU marked this pull request as ready for review May 14, 2025 08:10
@mapleFU
mapleFU requested a review from wgtmac as a code ownerMay 14, 2025 08:10
@mapleFU
mapleFUforce-pushed the variant-cpp-decoder-tools branch from fb59842 to da142a6CompareMay 14, 2025 08:10
@mapleFU

Copy link
Copy Markdown
MemberAuthor

@emkornfield@wgtmac@pitrou@zeroshade

This patch add some basic variant decoding tools. Some thoughts:

How would the interface for visiting variant like? The simplist way is cast <metadata, value> pairs to a ptr<::arrow::Scalar>, but this is too slow and needs to read whole data. We can also wraps a std::variant, but I think it's also slow and needs to dynamic dispatch the visitor. Here I just add visitor for every type. And currently, getInt64 would only supports read int64. Any idea is welcome

@mapleFU
mapleFUforce-pushed the variant-cpp-decoder-tools branch from da142a6 to 54681c4CompareMay 14, 2025 08:20
#include <arrow/testing/gtest_util.h>
#include <arrow/util/base64.h>

#include <boost/uuid/uuid.hpp>

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

use boost just for testing

@mapleFU

Copy link
Copy Markdown
MemberAuthor

Is it possible to manually add unit tests covering scenarios such as null values, UUID, etc.?

@xxubai I've added some hand-written binary fmt for testing..

@mapleFU
mapleFU requested review from emkornfield and pitrouMay 21, 2025 08:40
@pitrou

Copy link
Copy Markdown
Member

Please let me find the time to digest the Variant spec and review this. :)

@mapleFU

Copy link
Copy Markdown
MemberAuthor

Thanks! The read/write implementation will not be too complex without shredding. However design the proper interface would be a little challenge here

/// \brief Get the metadata id for a given key.
/// From the discussion in ML:
/// https://lists.apache.org/thread/b68tjmrjmy64mbv9dknpmqs28vnzjj96 if
/// !sorted_and_unique(), the metadata key is not guaranteed to be unique, so we use a

@xxubaixxubaiMay 22, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I check the Iceberg implementation just return the first matched string index(https://github.com/apache/iceberg/blob/1911c94ea605a3d3f10a1994b046f00a5e9fdceb/api/src/main/java/org/apache/iceberg/variants/SerializedMetadata.java#L88-L102).

Kindly ask: Why we need to return a vector here?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Assume a {"a": {"a" : 1 } } here. And metadata is duplicate ( standard not require it's unique if !sorted )

The metadata is: "a": 0, "a": 1. Assume inner object gets "a", if only return field_id = 0, it cannot get any info from the inner object, which requires 1 as field_id

@xxubaixxubaiMay 22, 2025

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

IMO. In Iceberg and Arrow implementation it returns first matched variant value in a object. But it's different in parquet-java which returns the latest pushed value.

This is a bit confusing for me — please correct me if I misunderstood.

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

Refer to: https://lists.apache.org/thread/b68tjmrjmy64mbv9dknpmqs28vnzjj96

Keys may appear in nested objects, but cannot appear in the same object. So the first example, {"a": {"a": 1}} is allowed. The second example, {"a": 1, "a": 2} is not allowed.

This parquet-java test prevent from the key in same object. But

If sorted_strings is set to 1, strings in the dictionary must be unique and sorted in lexicographic order. If the value is set to 0, readers may not make any assumptions about string order or uniqueness.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Okay. So for object variant, is it acceptable that different reader implementations return a different value if there are duplicate keys in the dictionary?

@mapleFUmapleFUMay 22, 2025

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

I don't know, but I think whether the writer promises that it doesn't produce value like this, whether the reader should pay the effort. Otherwise it's a bug

For example, {"a": {"a": 1}} reads "a" in inner object, it has field_id: 1 in field-id list. But the metadata returns 0 for "a", so it returns "not exists" for get "a". In this implementation it should get "field_ids: [0, 1]", and find "1" in it's field-id list.

I think most of the scenerio we don't need care about this, so uses SmallVector here. And if sorted_and_unique, it can have an optimization on find the keys

if (offset_sz < kMinimalOffsetSizeBytes || offset_sz > kMaximumOffsetSizeBytes) {
throw ParquetException("Invalid Variant metadata: invalid offset size: " +
std::to_string(offset_sz));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

offset_sz already checked in readLittleEndianU32?

 ARROW_DCHECK_LE(size, 4);
ARROW_DCHECK_GE(size, 1);

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

DCHECK means debug check, it should be regarded as an requires assertion rather than runtime dynamically check

"Invalid Variant metadata: offset out of range: " +
std::to_string((dictionary_size_ + kHeaderSizeBytes) * offset_sz) + " > " +
std::to_string(metadata_.size()));
}

@mapleFUmapleFUMay 23, 2025

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

In metadata I delayed the validating offsets to getter, it can also put here to validate all offsets are monotonic and the final offset is equal to the metadata size boundary

@pitrou

pitrou commented May 27, 2025

Copy link
Copy Markdown
Member

Since there seems to be a controversy about a good API for this, how about we start with something lower-level, such as a SAX-like parser (i.e. event-driven)? Then we can build up a higher-level API on top of it once we know which kind of API would be efficient?

An example of event-driven API is in RapidJSON, another is in nlohmann/json
(of course, we should use more modern C++ features)

case VariantBasicType::Primitive: {
auto primitive_type =
static_cast<VariantPrimitiveType>(value_[0] >> kValueHeaderBitShift);
auto primitive_type = static_cast<VariantPrimitiveType>(valueHeader());

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.

any reason not to push the static_cast<VariantPrimitiveType> call into the valueHeader method?

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.

I'm a bit confused. valueHeader seems to be just doing >> kValueHeaderBitShift, which means that the primitive type would be valueHeader() & 0x3f, right?

Copy link
Copy Markdown
MemberAuthor

Choose a reason for hiding this comment

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

if it's not primitive type, value_header might be length for string, or be used as field_offset_length etc

@github-actionsgithub-actionsBot added awaiting changes Awaiting changes and removed awaiting committer review Awaiting committer review labels May 27, 2025
@mapleFU

Copy link
Copy Markdown
MemberAuthor

Since there seems to be a controversy about a good API for this, how about we start with something lower-level, such as a SAX-like parser (i.e. event-driven)? Then we can build up a higher-level API on top of it once we know which kind of API would be efficient?

@pitrou SAX style parser means consume the whole token and parsing it to a variant object. It's ok when we'd like to parse the whole object 🤔, however, maybe it would be a bit expansive when we want to visit one or multiple columns? Currently I just implement a Variant object for this

@pitrou

Copy link
Copy Markdown
Member

@pitrou SAX style parser means consume the whole token and parsing it to a variant object.

It does not. See https://rapidjson.org/md_doc_sax.html

@mapleFU

Copy link
Copy Markdown
MemberAuthor

Emm i mean, for writer it might be good, for reader, get a key not requires visit/parse the whole binary with Key("...") api?

@pitrou

Copy link
Copy Markdown
Member

How would you avoid parsing the whole binary?

@mapleFU

Copy link
Copy Markdown
MemberAuthor

Emm it's by the design of parquet variant format. In memory there is VariantObject, VariantObject::Get(key) would search key in metadata, find field_id, and search for field_id in object variant. These operations are operated directed in variant binary format

@wgtmac

Copy link
Copy Markdown
Member

How would you avoid parsing the whole binary?

The variant spec has provided sufficient metadata (dictionary of all keys and offset to any value) to jump into a key at arbitrary nesting level without decoding irrelevant binary data. It is already a parsed binary. This is something that XML or JSON texts cannot do.

@pitrou

Copy link
Copy Markdown
Member

Thanks for the explanation @wgtmac . I retract my suggestion then.

public:
VariantValue(std::string_view metadata, std::string_view value);

VariantBasicType getBasicType() const;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nit: style in this class looks off. either GetBasicType or get_basic_type

@github-actions

Copy link
Copy Markdown

Thank you for your contribution. Unfortunately, this pull request has been marked as stale because it has had no activity in the past 365 days. Please remove the stale label or comment below, or this PR will be closed in 14 days. Feel free to re-open this if it has been closed in error. If you do not have repository permissions to reopen the PR, please tag a maintainer.

@github-actionsgithub-actionsBot added the Status: stale-warning Issues and PRs flagged as stale which are due to be closed if no indication otherwise label Jun 4, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting changesAwaiting changesComponent: C++Component: ParquetStatus: stale-warningIssues and PRs flagged as stale which are due to be closed if no indication otherwise

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants

@mapleFU@xxubai@pitrou@wgtmac@zeroshade@scovich@emkornfield