Skip to content

GH-49704: [C++] Read and write DictionaryEncoding.custom_metadata for dictionary value types - #53994

Draft
rustyconover wants to merge 2 commits into
apache:mainfrom
rustyconover:gh-49704-dictionary-value-metadata
Draft

rustyconover wants to merge 2 commits into
apache:mainfrom
rustyconover:gh-49704-dictionary-value-metadata

Conversation

@rustyconover

@rustyconover rustyconover commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Note

Depends on #54959, which holds the format change, split out at review request. This PR is stacked on it: please review only the second commit, GH-49704: [C++] .... Once #54959 merges, this will be rebased onto main.

Rationale for this change

In IPC a dictionary-encoded column is a single Field whose type is the value type. The only metadata slot was Field.custom_metadata, so dictionary<values=extension<X>> and extension<X, storage=dictionary<...>> serialized identically. Arrow C++ reads both as the latter, so pyarrow cannot read back its own stream of pa.dictionary(pa.int32(), pa.uuid()): Invalid storage type for UuidType: dictionary<...>.

#54959 adds an optional custom_metadata to DictionaryEncoding for the dictionary value type (format version 1.6). This PR is the C++ reference implementation. Companion PRs:

What changes are included in this PR?

  • Generated code: Schema_generated.h regenerated with flatc 24.3.6, limited to the new member.
  • C++: the IPC writer and reader, and the integration JSON reader and writer ("dictionary": {..., "metadata": [...]}).
    • The writer puts a dictionary value extension type's ARROW:extension:* keys in DictionaryEncoding.custom_metadata.
    • The reader applies them to the value type before wrapping it in the dictionary type, then applies Field.custom_metadata as before.
    • C++ applies only the extension keys: its DictionaryType cannot hold value-type metadata, so other keys and unregistered extension names are not preserved. That is an implementation limitation, not a spec rule.
  • Archery: a new dictionary_extension case, run for C++ only for now.
    • Go is skipped until the companion arrow-go PR lands, because integration CI builds arrow-go from main. With that PR, C++ ⇄ Go passes over IPC.
    • Rust is skipped because arrow-rs cannot represent a dictionary of an extension type (Field metadata for dictionary-encoded extension types is lost arrow-rs#7982). It reads the case correctly but cannot produce it, and archery has no producer-only skip.
  • Not included: the Ruby flatbuffers bindings (ruby/red-arrow-format/.../dictionary_encoding.rb) were not regenerated, because no Ruby toolchain was available. They need rake flat_buffers:generate.
  • Follow-ups:
    • DictionaryFieldMapper and DictionaryCollector (ipc/dictionary.cc) don't look through extension value types, so dictionaries nested inside extension dictionary values are not supported.
    • A Parquet ARROW:schema round-trip test.

Are these changes tested?

Yes.

  • C++ IPC tests:
    • schema and record batch round trips, including nested types and the file footer;
    • the flatbuffer layout;
    • delta and replacement dictionaries of an extension value type;
    • the current C++ limitation that non-extension keys and unregistered extension names are dropped on read;
    • legacy field-level metadata, which behaves as before.
  • Also: JSON integration tests, archery C++ IPC and C Data runs, and a pyarrow regression test.
  • Cross-implementation, run manually: C++ ⇄ Go passes with the companion arrow-go PR, and arrow-rs reads the C++ and Go output in every format.

Are there any user-facing changes?

Yes:

  • C++ and pyarrow now round-trip a dictionary whose value type is an extension type (e.g. pa.dictionary(pa.int32(), pa.uuid())) through IPC.
  • The C++/pyarrow IPC output for such columns changes: the extension keys move from Field.custom_metadata to DictionaryEncoding.custom_metadata.
    • Readers that predate this change, including older C++/pyarrow, read such columns as a plain dictionary of the storage type and lose the extension type.
    • That includes types like arrow.opaque, which older readers used to keep, though with the wrong nesting (as an extension type with dictionary storage).
  • Extension types whose storage type is a dictionary are serialized as before.
  • There is deliberately no fallback for data written by existing writers. A field-level arrow.uuid on a dictionary still fails to read, as it does today.

Was AI used for this PR?

PR code and description written by:

  • Human
  • AI

Reviewed before submission by:

  • Human
  • AI
  • Not reviewed

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

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

@github-actions github-actions Bot added the awaiting review Awaiting review label Oct 16, 2026
@rustyconover
rustyconover force-pushed the gh-49704-dictionary-value-metadata branch from 401634e to 0cd8838 Compare October 16, 2026 17:28
@github-actions github-actions Bot added the awaiting review Awaiting review label Oct 16, 2026
rustyconover pushed a commit to rustyconover/arrow-nanoarrow that referenced this pull request Oct 16, 2026
…ionary values

Implements the draft Arrow format 1.6 change from apache/arrow#49704
(apache/arrow#53994): an optional custom_metadata on DictionaryEncoding
that holds the metadata of the dictionary value type. It is the IPC
counterpart of ArrowSchema.dictionary->metadata, and Field.custom_metadata
corresponds to the parent ArrowSchema's metadata.

- flatcc_generated.h is regenerated with dev/update_fbs.sh (flatcc
  e3e44836) from apache/arrow main plus the draft Schema.fbs change.
- The encoder writes schema->dictionary->metadata (all keys) to
  DictionaryEncoding.custom_metadata, omitted when it is NULL or empty,
  and keeps writing schema->metadata to Field.custom_metadata. Before,
  the value metadata was dropped.
- The decoder sets schema->dictionary->metadata from
  DictionaryEncoding.custom_metadata and keeps Field.custom_metadata on
  the parent schema, as before. This covers nested dictionaries and
  footer decoding. Footer encoding carries the member when the caller
  fills footer.dictionaries.
- The integration JSON reader and writer support the optional
  "metadata" of a field's "dictionary" object, and the JSON comparison
  compares the dictionary value metadata, including nested dictionaries.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
if (encoding->custom_metadata() != nullptr) {
ARROW_ASSIGN_OR_RAISE(auto value_metadata,
internal::GetKeyValueMetadata(encoding->custom_metadata()));
RETURN_NOT_OK(MaybeDeserializeExtensionType(value_metadata.get(), &type));

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 am wondering whether there would be a way of retaining this value_metadata even if this is a not known extension type. So the reader could retain it and a new writer would write it. I am thinking on the use case of data passing through:
app knows extension type -> app does not know extension type -> app knows extension type

Maybe not entirely related and just an implementation detail that can be done as a follow up.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Hi @raulcd,

Do you think we need to make a different IPC reader options flag for that? Where we can do extension type pass through?

Rusty

@github-actions github-actions Bot added awaiting changes Awaiting changes and removed awaiting review Awaiting review labels Oct 19, 2026
@pitrou

pitrou commented Oct 22, 2026

Copy link
Copy Markdown
Member

@rustyconover Can you split the Format changes into a separate PR to make them easier to review? Thanks!

@rustyconover

Copy link
Copy Markdown
Collaborator Author

@rustyconover Can you split the Format changes into a separate PR to make them easier to review? Thanks!

Sure I can and I will! thanks @pitrou

rustyconover and others added 2 commits October 22, 2026 12:50
… value type

In IPC, a dictionary-encoded field is a single Field whose type is the
dictionary value type, and the only metadata slot was
Field.custom_metadata. As a result, dictionary<values=extension<X>> and
extension<X, storage=dictionary<...>> were serialized identically.

Add an optional custom_metadata member at the end of the
DictionaryEncoding table. It holds the custom metadata of the dictionary
value type, with the same semantics as Field.custom_metadata (including
the reserved ARROW:extension:* keys), and is the IPC counterpart of
ArrowSchema.dictionary->metadata in the C Data Interface. Document it in
the columnar format (format version 1.6), the versioning history, the C
Data Interface, and the integration JSON format, which gains a matching
optional "metadata" member in the "dictionary" object.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ta for dictionary value types

Implement the DictionaryEncoding.custom_metadata format addition in C++.
The IPC writer now puts the extension keys of a dictionary value type
there, and the reader deserializes the value extension type from it
before wrapping it in the dictionary type and applying
Field.custom_metadata as before. Regenerate Schema_generated.h, support
the "metadata" member of the "dictionary" object in the integration JSON
reader and writer, and add a "dictionary_extension" archery case, which
C++ and Go produce and consume (Go skips the C schema format, Rust is
skipped as it can read but not write the case).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@rustyconover rustyconover changed the title GH-49704: [Format][C++] Add custom_metadata to DictionaryEncoding for the dictionary value type GH-49704: [C++] Read and write DictionaryEncoding.custom_metadata for dictionary value types Oct 22, 2026
@rustyconover
rustyconover force-pushed the gh-49704-dictionary-value-metadata branch from 0cd8838 to b267de8 Compare October 22, 2026 16:56
@github-actions github-actions Bot added awaiting change review Awaiting change review and removed awaiting changes Awaiting changes labels Oct 22, 2026
@rustyconover

Copy link
Copy Markdown
Collaborator Author

@pitrou Done: the format change is now in #54959 (issue #54958). This PR is stacked on it and contains only the C++ implementation, so please review just the second commit.

Note that #54959 was auto-closed by the 3-open-PR limit for contributors without repository access; I've asked on it for it to be reopened.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants