-
Notifications
You must be signed in to change notification settings - Fork 846
feat: stabilize field IDs across schema evolution #8658
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 18 commits
08bebbe
2964690
7e46c86
7d59c81
2b45dbe
8851484
bb42780
d199a9a
3b041d1
981ff8e
41867ac
4a17fc2
d4e8ab6
aff26e7
3a560ff
d952779
4ced65b
399260c
6fde30f
678e855
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. praise: this looks a lot better! Nice work. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -221,16 +221,51 @@ Assigned IDs with parent relationships: | |
| Note: A `parent_id` of -1 indicates a top-level field. For nested fields, `parent_id` references the ID of the parent field. Child fields reference their parent via `parent_id` rather than being stored as separate "children" arrays in the protobuf message (though the Rust in-memory representation maintains a children vector for convenience). | ||
|
|
||
| **New field assignment (incremental):** | ||
| When fields are added later (e.g., through schema evolution), they receive the next available ID | ||
| incrementally. This preserves the history of field additions. | ||
| When fields are added later (e.g., through schema evolution), they receive incrementally assigned | ||
| IDs. On a dataset with the stable field-ID contract activated, allocation starts immediately after | ||
| the manifest's persistent `max_allocated_field_id`; IDs retired by drop or replacement are never | ||
| reused. | ||
|
|
||
| Field-ID metadata supplied on an incoming Arrow schema is not an allocation authority. When that | ||
| schema is merged into an activated dataset, IDs for newly introduced logical fields are cleared and | ||
| assigned by the dataset allocator; callers cannot select or reserve IDs through Arrow metadata. | ||
|
Xuanwo marked this conversation as resolved.
Outdated
|
||
|
|
||
| The presence of `Manifest.max_allocated_field_id` is the activation marker. If it is absent, the | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question(blocking): what's the difference between setting this field and setting the writer flag? What happens if you set this field but not the writer flag? What happens if you set the writer flag, but not this field?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The field stores allocator state, while the flag gates writers; both must be set together, and either mismatch is invalid. |
||
| dataset has legacy allocation semantics and an implementation may derive the next ID from fields | ||
| still referenced by the current snapshot. Activation initializes the high-water mark from the | ||
| maximum non-negative ID referenced by the canonical manifest schema, base data files, and overlay | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion: Worth documenting the restore consequence here: once activated, |
||
| files. Activation provides a forward guarantee only; it cannot reconstruct identities that were | ||
| dropped or reused in older snapshots. | ||
|
Xuanwo marked this conversation as resolved.
Outdated
|
||
|
|
||
| New datasets activate this contract in their initial manifest and set the corresponding reader and | ||
| writer feature bits. The reader bit is a compatibility fence for released runtimes whose generic | ||
| commit path did not enforce unknown writer bits; stable field IDs do not otherwise change how fields | ||
| are read. Existing legacy datasets remain unchanged until an explicit migration commit. Explicit | ||
| migration sets only the writer bit and requires operators to retire older writers before activation. | ||
| The reader fence, once set by automatic activation, is retained by every later commit, clone, and | ||
| restore in that branch ancestry. | ||
|
|
||
| Activation is one-way within a branch ancestry. After migration, restore cannot target a version | ||
| from before activation because that version does not carry the high-water mark needed to preserve | ||
| retired identities. | ||
|
Xuanwo marked this conversation as resolved.
Outdated
|
||
|
|
||
| ### Field ID Properties | ||
|
|
||
| - **Immutable**: Once assigned, a field's ID never changes | ||
| - **Immutable after activation**: An identity keeps its ID throughout the activated branch ancestry | ||
| - **Unique**: Each field within a table has a unique ID | ||
| - **Stable**: IDs are preserved across schema evolution operations | ||
| - **Never reused after activation**: Dropped and replaced identities permanently retire their IDs | ||
| - **Monotonic**: New identities are allocated densely above the persistent high-water mark | ||
| - **Sparse**: Field IDs may not form a contiguous sequence after schema evolution | ||
|
Xuanwo marked this conversation as resolved.
Outdated
|
||
|
|
||
| The guarantee is scoped to one dataset branch ancestry. Field IDs are not globally unique across | ||
| datasets or independently evolving branches. A persistent cross-dataset or cross-branch reference | ||
| must carry the corresponding dataset and ancestry identity. | ||
|
|
||
| Two branches may allocate the same integer after their common ancestor. A future branch-merge | ||
| operation must reject the merge when both sides created post-ancestor identities whose bindings | ||
| differ; it must not silently choose one binding, renumber already-persisted fields, or merge them by | ||
| name. Branch-local collision avoidance requires a separate allocator design. | ||
|
Xuanwo marked this conversation as resolved.
Outdated
|
||
|
|
||
| ### Using Field IDs | ||
|
|
||
| When referencing fields internally within the format, use the field ids rather than field names or positions. | ||
|
|
@@ -296,13 +331,29 @@ The complete schema is represented as a collection of top-level fields plus meta | |
| Field IDs enable efficient schema evolution: | ||
|
|
||
| - **Add Column**: Assign a new field ID and add to schema | ||
| - **Drop Column**: Remove field from schema; its ID may be reused in some systems | ||
| - **Drop Column**: Remove the field and permanently retire its ID after activation | ||
|
Xuanwo marked this conversation as resolved.
Outdated
|
||
| - **Rename Column**: Change field name; ID remains the same | ||
| - **Reorder Columns**: Change field order in schema; IDs remain the same | ||
| - **Type Evolution**: Data type can be changed. This might require rewriting the column in the data, depending on how the type was changed. | ||
| - **Metadata or Nullability Change**: Preserve the field ID | ||
| - **Type Replacement**: Allocate a new field ID and retire the old identity | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. question(non-blocking): is this new behavior or just a description of existing behavior? I wonder if this is really desireable.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is existing cast behavior: changing the logical type creates a new field identity, so it receives a new ID. |
||
| - **Overwrite**: Preserve compatible logical identities; allocate new IDs for added fields and type | ||
| replacements | ||
|
|
||
| The use of field IDs ensures that data files can be correctly interpreted even as the schema changes over time. | ||
|
|
||
| ### Blob Identity Namespace | ||
|
|
||
| A Blob column's canonical logical fields in the manifest schema participate in stable field-ID | ||
| allocation. The top-level Blob field is the public binding identity; logical children already | ||
| present in the manifest schema are also allocated and retired normally. | ||
|
|
||
| Blob writer-prepared fields and stored descriptor fields are representation details. Synthetic | ||
| children such as `kind`, `blob_id`, `blob_size`, `position`, `size`, and `blob_uri` do not enter the | ||
| dataset field-ID namespace unless they are part of the canonical manifest schema or a | ||
| `DataFile.fields` mapping. Their IDs may remain `-1` or use a file-local namespace, and they do not | ||
| advance `max_allocated_field_id`. The `blob_id` value identifies a sidecar object and is unrelated | ||
| to schema field IDs. | ||
|
Xuanwo marked this conversation as resolved.
Outdated
|
||
|
|
||
| ## Example Schemas | ||
|
|
||
| The examples below use a simplified representation of the field structure. In the actual protobuf format, `type` refers to the field type enum (PARENT/REPEATED/LEAF) and `logical_type` contains the data type string representation. | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.