Skip to content

GH-49704: [Format][C++] Add custom_metadata to DictionaryEncoding for the dictionary value type - #53994

Draft
rustyconover wants to merge 1 commit into
apache:mainfrom
rustyconover:gh-49704-dictionary-value-metadata
Draft

rustyconover wants to merge 1 commit into
apache:mainfrom
rustyconover:gh-49704-dictionary-value-metadata

Conversation

@rustyconover

@rustyconover rustyconover commented Oct 16, 2026 •

Copy link
Copy Markdown

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<...>. The dev@ thread (https://lists.apache.org/thread/4xj0yrl2vn0lbrv39p3dvd28okvkjxyz) reached lazy consensus on adding KeyValue metadata for the value type to DictionaryEncoding (option 1).

This is a format change. It needs a [VOTE] on dev@ and two reference implementations, so this PR is a draft. Companion PRs:

What changes are included in this PR?

  • Format: an optional custom_metadata: [KeyValue] appended to DictionaryEncoding. It is the custom_metadata of the dictionary value type, with the same semantics as Field.custom_metadata (including the reserved ARROW:extension:* keys) but applied to the dictionary values. It is the IPC counterpart of ArrowSchema.dictionary->metadata in the C Data Interface.
    • Writers should put a value extension type's ARROW:extension:name/ARROW:extension:metadata there rather than in Field.custom_metadata.
    • Readers apply it to the value type before wrapping it in the dictionary type, then apply 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.
    • Format version 1.6, recorded in the Schema.fbs history and the Columnar, Versioning and Integration docs. MetadataVersion stays V5, and the file footer and Flight need no other change.
  • 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": [...]}).
  • 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 over IPC 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 (Claude Code)

Reviewed before submission by:

  • Human (maintainer)
  • AI (AI reviewer)
  • Not reviewed

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

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

…ng for the dictionary value type

In IPC, a dictionary-encoded field is a single Field whose type is the
dictionary value type, so dictionary<values=extension<X>> and
extension<X, storage=dictionary<...>> were serialized identically, with
the extension keys in Field.custom_metadata.

Add an optional custom_metadata member at the end of the
DictionaryEncoding table holding the metadata of the dictionary value
type (format version 1.6). The C++ 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. The integration JSON
format gains a matching optional "metadata" member in the "dictionary"
object, and 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 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>
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.

1 participant