From 008b357730afc2db53a8d8ff657850459a689918 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Fri, 18 Sep 2026 16:06:29 -0400 Subject: [PATCH 01/12] docs: explain crate versioning and editions Signed-off-by: "Connor Tsui" --- docs/_static/versioning-proof.pdf | Bin 0 -> 187481 bytes docs/specs/editions.md | 390 +----------- docs/specs/file-format.md | 13 +- docs/specs/index.md | 2 +- docs/specs/versioning-proof.typ | 592 ++++++++++++++++++ docs/specs/versioning.md | 85 +++ .../versioning/arrays-and-compression.md | 167 +++++ docs/specs/versioning/compatibility.md | 131 ++++ docs/specs/versioning/editions.md | 158 +++++ docs/specs/versioning/using-editions.md | 122 ++++ vortex-edition/src/lib.rs | 2 +- 11 files changed, 1271 insertions(+), 391 deletions(-) create mode 100644 docs/_static/versioning-proof.pdf create mode 100644 docs/specs/versioning-proof.typ create mode 100644 docs/specs/versioning.md create mode 100644 docs/specs/versioning/arrays-and-compression.md create mode 100644 docs/specs/versioning/compatibility.md create mode 100644 docs/specs/versioning/editions.md create mode 100644 docs/specs/versioning/using-editions.md diff --git a/docs/_static/versioning-proof.pdf b/docs/_static/versioning-proof.pdf new file mode 100644 index 0000000000000000000000000000000000000000..2f1741210978d625ed3579fc2f474ce64b1a85a8 GIT binary patch literal 187481 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%0Dvfu3BW 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 literal 0 HcmV?d00001 diff --git a/docs/specs/editions.md b/docs/specs/editions.md index 1aa3cfe4e11..ffa20d539ff 100644 --- a/docs/specs/editions.md +++ b/docs/specs/editions.md @@ -1,386 +1,10 @@ -# Editions - -Vortex files contain several kinds of serialized **component**: array encodings, layout encodings, extension dtypes, and -aggregate functions. An **edition** is a named set of their concrete wire IDs. It controls what a writer may put in a -file and, once frozen, identifies its origin library or project and minimum version: the earliest release of that origin -that recognizes every ID in the set. - -Editions belong to independently versioned families and are cumulative within a family. Each edition includes all -components from the preceding edition in that family, plus any additions. A writer selects at most one edition from -each family and may use the union of their component IDs. For example, selecting `core2026.08.3` and -`tensor2026.04.0` allows the core components together with tensor arrays and dtypes. Every family names the origin -library or project whose release versions its editions use. - -The first frozen edition, `core2025.05.0`, contains the components that Vortex `0.36.0` could write. This marks the -start of the Vortex file format's stability guarantee. Every Vortex release from `0.36.0` onward can read -`core2025.05.0`, and later frozen `core` editions extend that guarantee to newer components. - -When a writer selects only frozen editions from one origin, the highest of their minimum library versions is the -earliest release guaranteed to read the resulting file. With multiple origins, the file requires the recorded minimum -version of each. Editions without minimum library versions are drafts and carry no guarantee about their future -compatibility. - -## What an edition contains - -An edition records every component by kind and wire ID. IDs are unique within a kind, but not across kinds: a layout -named `vortex.flat` and an array encoding with the same ID are distinct components. The writer therefore builds and -enforces a separate allowlist for each kind: - -| Kind | What it identifies | Used at | -|-------------|--------------------------------------------|------------------------------| -| `array` | a serialized array representation | array serialization | -| `layout` | the footer's layout tree | layout serialization context | -| `dtype` | extension dtypes nested in the file schema | file writer | -| `aggregate` | zone maps in zoned layouts | layout writer context | - -Writing a component that is absent from the selected editions fails the write. This rule applies to every kind, -including aggregates. Although a zone map is only an optimization and could be dropped, doing so would silently change -the writer's configured pruning behavior. - -Only aggregates that would actually be written are checked. If a column's dtype cannot support an aggregate, the writer -omits it and there is no edition violation. - -An empty allowlist permits no components. Collectively, the selected editions must declare every serialized array ID, -layout encoding, extension dtype, and aggregate function that the writer writes. An array serializer may expose several -wire IDs for one in-memory encoding. The serializer chooses the representation, and the serialization context rejects -the write if the chosen wire ID is not declared by the selected editions. - -For example, `core2026.08.0` declares the aggregate functions that the default writer may store in zone maps: `min`, -`max`, `bounded_min`, `bounded_max`, `nan_count`, and `null_count`. It does not declare `sum`, because the writer does -not store sums in zone maps. File-level statistics use a fixed legacy field for sums rather than a serialized aggregate -function ID, so this allowlist does not apply to them. - -Optional Vortex modules enable their own edition families alongside `core`. Tensor support enables -`tensor2026.04.0`, for example, while Zstd buffer wrapping enables `zstd2026.02.0`. - -## Resolving an unknown-component error - -An unknown-ID error means that the reader does not recognize a serialized component in the file. Find the component's -kind and ID in the [registry](#edition-registry): - -1. **It belongs to a frozen edition.** Upgrade the edition's named origin to at least its minimum library version. -2. **It belongs to a draft edition.** No released reader is guaranteed to support it. Use a build that registers the - component or ask the file's producer which build to use. -3. **It is not in the registry.** The file contains a custom, third-party, or experimental component outside the - editions system. Ask the producer for its implementation and register it with the reader's session. - -Tools that only inspect or copy data can opt in to `allow_unknown`. Unknown array encodings, layout encodings, and -extension dtypes are then preserved as inert representations. An unknown aggregate function disables the affected -zone-map pruning rather than causing the file to be rejected. - -## Writing with an edition - -By default, the Vortex facade targets `core2026.08.3`. A new encoding or serialization feature that is -still evolving gets a new draft edition; later additions create later editions rather than changing an already -published feature set. Each feature advances through its own independently versioned family until it is ready to join -the shared `preview` family. Preview components remain opt-in until they are ready to join `core`. Components supplied -by an optional plugin belong to that plugin's family, such as `tensor`, `zstd`, `spatial`, or `json`. - -Edition configuration belongs to the writer's Vortex session. Registering an edition makes its declaration available to -the session; enabling it allows the writer to use its components. Enabling another edition in the same family replaces -the previous selection. - -You can change the default configuration to: - -- **Target an older `core` edition** when the file must remain readable by an older Vortex deployment. -- **Enable another family** to use components outside `core`. Vortex currently defines `preview`, `tensor`, `zstd`, - `spatial`, and `json` in addition to `core`. - -Sessions created without the Vortex facade must register and enable their editions before writing files. - -For experimental or custom components that do not belong to an edition, the Rust writer exposes -`disable_editions()`. This disables every edition check for that write: every array representation registered in the -session is eligible for compression and serialization, while layouts, extension dtypes, and aggregate functions are -unrestricted. It does not register missing readers, so files written this way have no edition compatibility guarantee. - -Compression and edition compatibility are separate. Compressors produce current in-memory arrays and do not select a -wire ID. The writer maps each allowed serialized ID to its current in-memory encoding and restricts the default -BtrBlocks compressor to schemes producing those encodings. Custom compressors remain unrestricted, with serialization -providing the final compatibility boundary when edition enforcement is enabled. At that boundary, the array plugin -produces an ID, metadata, buffers, and children. The serialization context interns the returned ID and fails the write -if the selected editions do not permit it. A serializer may emit a historical ID when the value satisfies that ID's -frozen contract, but it does not inspect the edition allowlist. Without disabling edition enforcement, a custom layout -or compressor therefore cannot bypass the final wire-ID check. - -## How editions change - -A frozen edition never changes: neither its membership list nor the meaning of its component IDs may be altered. -Introducing a new serialized object or a reader-visible revision requires a new edition; it is never added -retroactively to an existing edition. A component supplied by an optional plugin creates that edition in the plugin's -independently versioned family. - -Core-maintained objects do not enter `core` directly. When an object is ready for users to try and its wire format is -believed complete, it enters a draft edition in an independently versioned family. Publishing that edition is a -format-stability commitment, not the start of format design: the serialized contract should change only when absolutely -necessary to resolve a problem found during testing. After successful testing, the same object ID and wire contract move -into a new `preview` edition for broad opt-in use, and later into a new `core` edition for use by default. - -A new stable `core` or plugin edition may freeze in the release of its origin project in which it first ships. Until -that release is cut, its version is not known and the declaration keeps `min_library_version: None`. After the release -is cut, the declaration is updated with that newly released version, usually during development of the next release. -This backfills the documented minimum library version; it does not delay the freeze or its read-forever compatibility -guarantee. - -A component may later be deprecated, meaning that writers stop using it. Readers must continue to support it, so -deprecation does not invalidate existing files. - -Writer behavior evolves independently from the in-memory representation. A change that an old reader must distinguish -uses a new serialized ID, even when the new deserializer produces the same in-memory array. A serializer may continue -emitting the older ID for values that satisfy its frozen contract; the selected editions validate the ID it emits. - -## How serialized components evolve - -Editions govern serialized components, not in-memory representations. An in-memory representation may gain capabilities -or be replaced without changing an edition. Each in-memory array plugin owns the mapping between that representation and -its wire history: - -- the serialized IDs its deserializer recognizes; -- one serializer that returns the appropriate lossless variant as an ID, metadata, buffers, and children; and -- a deserializer that receives the exact ID found in the file and constructs the current in-memory representation. - -An in-memory representation often has one serialized ID equal to its in-memory encoding ID, but this is only the simple -case. Editions constrain the ID stored in the file, because that is what an old reader can recognize. - -### Reader-visible evolution requires a new ID - -Any new form that an old reader does not already understand uses a new serialized ID. This includes additive metadata or -children when an old reader would accept the ID but reject or misinterpret the new combination. The ID is the capability -tag: readers do not consult the edition or negotiate a separate version while decoding an array. - -Keeping an ID is safe only when the emitted representation remains within that ID's existing frozen contract. A writer -may choose a different but already-valid encoding of the same contract, and a reader may fix a bug or normalize the old -form into a newer in-memory structure. Neither action expands what the wire ID means. - -A new wire ID does not normally require a second in-memory array. The current plugin registers every historical ID, -serializes the current value under the oldest allowed lossless one, and deserializes all of them into the current type. -The old ID remains registered forever. If the compressor and serializer cannot preserve one common in-memory -representation and losslessly downgrade it, the change instead needs a new in-memory array, compressor, and -deserializer. - -Name successive incompatible revisions by appending a version to the same base name: `vortex.foo`, `vortex.foo_v2`, -`vortex.foo_v3`. Do not give successor versions descriptive names. A linear naming scheme keeps the component's -serialized history unambiguous. - -#### Example: multi-part decimals - -`vortex.decimal_byte_parts` entered `core2025.05.0` with each decimal value represented by one signed integer child. Its -metadata includes `lower_part_count`, but readers of this component require that field to be zero. Suppose the in-memory -representation gains support for wide decimals, represented by a signed most-significant part and one or more unsigned -64-bit lower parts: - -- The serializer first tries to construct the old single-signed-child form. If every value can be - represented that way, it emits `vortex.decimal_byte_parts` with `lower_part_count = 0`, even if - the current in-memory array has lower-part children. -- An array that cannot be collapsed into that old form losslessly uses the new - `vortex.decimal_byte_parts.v2` component, initially staged in a draft edition. -- A new reader deserializes both IDs into the same in-memory representation. An older reader reports - `vortex.decimal_byte_parts.v2` as unknown instead of trying to decode a wire format it does not support. -- When targeting an edition that permits only the old ID, serializing a value that can be collapsed succeeds; an - irreducibly multi-part value fails because no lossless downgrade exists. - -#### Example: Pco 8-bit integers - -The historical `vortex.pco` contract does not include `i8` or `u8`; readers implementing that contract must not be -sent an 8-bit Pco payload under the familiar ID. Adding 8-bit support keeps one current in-memory `Pco` array but adds -`vortex.pco.v2` as a serialized component: - -- The single Pco serializer emits `vortex.pco` for the primitive types covered by the old contract, even when both IDs - are permitted. -- For `i8` or `u8`, the earliest lossless form is `vortex.pco.v2`. A target edition without that ID rejects the write. -- The current deserializer registers both IDs. When given `vortex.pco`, it still rejects an 8-bit dtype; understanding - the v2 payload does not silently broaden the frozen v1 contract. -- The Pco compression scheme can sample and construct 8-bit Pco arrays without consulting editions. Wire selection - remains the serializer's responsibility. - -If writing an older edition must succeed for every input, its compression policy must choose an in-memory encoding -whose serializer has a permitted lossless form. It must not disguise the newer Pco form with the old ID. - -### Reading: deserialize into the current representation - -Every component in a frozen edition remains readable. Its deserializer may convert old data directly into the current -in-memory representation rather than preserving a parallel legacy representation. For example, a `vortex.alp` array with -interior patches is read as a `Patched` array around a patch-free ALP array. Similarly, old zone maps, including -`vortex.stats` layouts, are read by the machinery used for modern `vortex.zoned` layouts. - -Readers do not negotiate versions. They resolve the component ID, pass that exact ID to its deserializer, and either -construct the current in-memory array or report an -[unknown-component error](#resolving-an-unknown-component-error). - -A current deserializer must preserve each historical ID's contract. Recognizing a newer ID does not authorize it to -accept the newer metadata, child shape, dtype coverage, or buffer interpretation when the file carries an older ID. +--- +orphan: true +--- -A file contains its array ID, dtype, metadata, children, and buffers. A newer plugin may be registered under both -`vortex.foo` and `vortex.foo_v2`, but an older build is registered only under `vortex.foo`. This is what guarantees that -the older build rejects a v2 file before interpreting its contents. - -### Writing: validate the selected component and writer behavior - -For each in-memory array, the writer calls its plugin's single serializer. The serializer owns the versioning logic and -returns the appropriate lossless variant. It may change metadata, buffers, and children without constructing a legacy -in-memory array. Returning `None` means the array cannot be serialized. The serialization context then interns the -returned ID, failing the write if that ID is not permitted by the selected editions. - -This selection happens recursively after compression. Compressor output therefore remains an in-memory concern: a -compressor does not label its array with an edition or choose a wire version. Layouts, extension dtypes, and aggregates -perform their analogous compatibility checks at their own serialization boundaries. - -### What this means for each kind - -- **Arrays.** The array serialization context permits only wire IDs from the selected editions. The in-memory array's - serializer chooses its lossless representation, and the context rejects it if its ID is not permitted. -- **Layouts.** The layout strategy builds the layout tree at write time. When targeting an older edition, it must use - structures available in that edition, such as plain chunked data in place of newer auxiliary layouts. -- **Extension dtypes.** Before writing any bytes, the file writer recursively validates every extension dtype in the - schema. Readers resolve serialized dtype IDs against the session's dtype registry. -- **Aggregate functions.** Zone maps serialize aggregate function IDs and their options. A zone map containing a - function outside the selected editions fails the write. With `allow_unknown`, readers disable a zone map whose - aggregate function they do not recognize; ignoring a zone map only reduces pruning and does not affect correctness. - -## The `preview` family - -The additive `preview` family is the shared opt-in set for core-maintained components whose serialized contracts have -survived independent testing but are not yet available to the default core writer. Preview currently contains no -components. Adding the first component will create a later preview edition; unrelated work remains in independent -families until it meets the preview compatibility bar. - -## Independently versioned component families - -Components that are ready for focused testing but are not yet ready for the shared preview set advance through their own -families. Optional modules use families such as `tensor`, `zstd`, `spatial`, and `json`. Each family can evolve without -coupling its chronology or selection to unrelated components. - -The wire format is expected to be complete when its first draft edition is published and should change only when -necessary to resolve an issue discovered during testing. If a correction changes what readers must understand, give the -corrected representation a new ID and add a later edition to the same family. Once testing establishes that an object is -ready for broad opt-in use, promote that same ID and serialized contract into a new `preview` edition. Later adoption by -the default writer promotes it into a new `core` edition. - -The default writer does not emit a component merely because its reader understands it. Users opt in by enabling the -edition containing that component. - -## Declaring, freezing, and the edition records - -The default declarations live in `vortex-edition/src/declarations/`, while optional-module declarations live in their -owning crates. Each declared edition is exported as a TOML record under `vortex/editions/`, grouped by family. A record -names the origin library or project whose releases `min_library_version` refers to. Draft records omit that field and -carry no read-forever guarantee. Regenerate the records by running: - -```sh -cargo run -p xtask -- generate-editions -``` - -Changing the declarations follows the edition's lifecycle: - -1. **Create a new family and edition for every new object.** Never add an unrelated serialized - object or reader-visible revision to an existing family. A revision advances the family that - owns its earlier ID. -2. **Publish test-ready work as a draft.** When an object is ready to be tried and its format is - believed complete, give it a wire ID and add it to a draft edition in its family. Change that - format only when necessary to resolve an issue found during testing; a reader-visible - correction gets another ID and a later edition in the same family. -3. **Promote the tested contract to preview.** Once it is ready for broad opt-in use, add the same - object ID and wire contract to a new additive `preview` edition. Promotion must not redesign - the format. -4. **Promote the adopted contract to core.** Once it is ready for use by the default writer, add - the same object ID and wire contract to a new `core` edition with `min_library_version: None` - and regenerate its draft record, then ship it in a release. The edition freezes as part of that - release. Its minimum library version cannot be populated yet because the release version is not - known until the release is cut. -5. **Backfill the released version.** After cutting the release, set `min_library_version` to that - newly released Vortex version — the version that first shipped readers for every member — and - regenerate the records, converting the draft record into a frozen record. This update usually - lands during development of the next release, but it documents the freeze that already - happened; it does not freeze the edition later. -6. **Never touch it again.** A frozen record is immutable: CI (`cargo run -p xtask -- check-editions`) rejects any - change that edits, renames, unfreezes, - or deletes a frozen record, and rejects new editions that do not extend their family's - chronology. To change what writers may emit, declare the next edition instead. - -## Edition registry - -Registry entries list the edition in which each component first appeared. Later editions in the same family inherit all -earlier components. - -### Frozen `core` editions - -#### `core2025.05.0` - -Minimum library version: `0.36.0`. - -- `array`: `fastlanes.bitpacked`, `fastlanes.for`, `vortex.alp`, `vortex.alprd`, `vortex.bool`, - `vortex.bytebool`, `vortex.chunked`, `vortex.constant`, `vortex.datetimeparts`, `vortex.decimal`, - `vortex.decimal_byte_parts`, `vortex.dict`, `vortex.ext`, `vortex.fsst`, `vortex.list`, - `vortex.null`, `vortex.primitive`, `vortex.runend`, `vortex.sparse`, `vortex.struct`, - `vortex.varbin`, `vortex.varbinview`, `vortex.zigzag` -- `layout`: `vortex.chunked`, `vortex.dict`, `vortex.flat`, `vortex.stats`, `vortex.struct` -- `dtype`: `vortex.date`, `vortex.time`, `vortex.timestamp` - -#### `core2025.06.0` - -Minimum library version: `0.40.0`. - -- `array`: `vortex.pco`, `vortex.sequence`, `vortex.zstd` - -#### `core2025.10.0` - -Minimum library version: `0.54.0`. - -- `array`: `fastlanes.rle`, `vortex.fixed_size_list`, `vortex.listview`, `vortex.masked` - -#### `core2026.08.0` - -Minimum library version: `0.84.0`. - -- `layout`: `vortex.zoned` -- `aggregate`: `vortex.bounded_max`, `vortex.bounded_min`, `vortex.max`, `vortex.min`, - `vortex.nan_count`, `vortex.null_count` - -#### `core2026.08.1` - -Minimum library version: `0.84.0`. - -- `array`: `vortex.onpair` - -#### `core2026.08.2` - -Minimum library version: `0.85.0`. - -- `array`: `vortex.map` - -#### `core2026.08.3` - -Minimum library version: `0.85.0`. - -- `array`: `vortex.parquet.variant`, `vortex.variant` -- `dtype`: `vortex.uuid` - -### Editions without a frozen guarantee - -These editions have no minimum library version. Evolving features advance through new draft editions in their own -families. Their formats are expected to remain compatible unless a defect is serious enough to block promotion into -core. Optional plugin families state their own policy. - -#### `preview2026.08.0` - -This edition currently adds no components. - -#### `tensor2026.04.0` - -- `array`: `vortex.tensor.cosine_similarity`, `vortex.tensor.inner_product`, `vortex.tensor.l2_norm`, - `vortex.tensor.l2_normalize` -- `dtype`: `vortex.tensor.fixed_shape_tensor`, `vortex.tensor.vector` - -#### `zstd2026.02.0` - -- `array`: `vortex.zstd_buffers` - -#### `spatial2026.08.0` - -- `dtype`: `vortex.st.box`, `vortex.st.linestring`, `vortex.st.multilinestring`, - `vortex.st.multipoint`, `vortex.st.multipolygon`, `vortex.st.point`, `vortex.st.polygon`, - `vortex.st.wkb` -- `aggregate`: `vortex.st.aabb` +# Editions -#### `json2026.08.0` +The editions specification is now part of [Versioning](versioning.md). -- `dtype`: `vortex.json` +For edition membership and minimum Vortex crate versions, see the +[edition registry](versioning/editions.md#edition-registry). diff --git a/docs/specs/file-format.md b/docs/specs/file-format.md index 04e065ac5f1..fad90980ee1 100644 --- a/docs/specs/file-format.md +++ b/docs/specs/file-format.md @@ -1,9 +1,11 @@ # File Format :::{important} -The Vortex File Format has been considered stable since the release of version 0.36.0. That means that you can expect all -future versions of the Vortex library to be able to read files written by version 0.36.0 or later (up to and including -the version doing the reading). +The Vortex file format's stability guarantee starts with version `0.36.0` of the Vortex Rust crates +and edition `core2025.05.0`. Later versions of those crates retain read support for the components +in frozen editions. +[Versioning](/specs/versioning) explains the guarantee and the requirements for draft and custom +components. ::: :::{seealso} @@ -17,9 +19,8 @@ definition that allows efficiently querying the layout. Other considerations for the Vortex file format include: -* Backwards compatibility, and (coming soon) forwards compatibility. The set of encodings a - writer may put in a file — and the resulting read-compatibility promise — is governed by - [Editions](/specs/editions). +* File compatibility. Editions constrain which serialized components a writer can use. A writer + built with newer Vortex crates can target an older edition that its intended readers support. * Fine-grained encryption. * Efficient access for both local disk and cloud storage. * Minimal overhead reading few columns or rows from wide or long arrays. diff --git a/docs/specs/index.md b/docs/specs/index.md index 12fab8da430..918082e200b 100644 --- a/docs/specs/index.md +++ b/docs/specs/index.md @@ -8,7 +8,7 @@ maxdepth: 2 --- file-format -editions +versioning ipc-format dtype-format scalar-format diff --git a/docs/specs/versioning-proof.typ b/docs/specs/versioning-proof.typ new file mode 100644 index 00000000000..4b54683772a --- /dev/null +++ b/docs/specs/versioning-proof.typ @@ -0,0 +1,592 @@ +// SPDX-License-Identifier: CC-BY-4.0 +// SPDX-FileCopyrightText: Copyright the Vortex contributors + +#set document(title: "Vortex versioning: model and proofs", author: "Vortex contributors") +#set page(paper: "a4", margin: (x: 23mm, y: 22mm), numbering: "1") +#set text(size: 10.5pt) +#set par(justify: false, leading: 0.65em) +#set heading(numbering: "1.1") +#set math.equation(numbering: "(1)") +#set table(inset: 6pt, stroke: 0.4pt + luma(75%)) +#show heading.where(level: 1): set block(above: 1.7em, below: 0.7em) +#show heading.where(level: 2): set block(above: 1.2em, below: 0.5em) + +#align(center)[ + #text(size: 21pt)[Vortex versioning] + #v(0.3em) + #text(size: 14pt)[Model, proof obligations, and compatibility theorems] +] +#v(1em) + +This document proves the compatibility guarantees described in the Versioning documentation. +Under the stated assumptions, every successful write constrained to a set of editions can be read +with the same meaning by any reader that supports those editions, including a reader older than +the writer. + +These are mathematical proofs about a model. They are not a machine-checked proof of the Rust +implementation. The assumptions require correct readers and writers, fixed serialized formats, +and retained support for those formats in later versions of the code. The implementation section +identifies the remaining work. In particular, configuring compression schemes for each target +edition is still planned. + += Compatibility and writer invariants + +I1 through I7 establish compatibility for successful writes and require later versions of the +reader code to preserve it. I8 through I10 require the writer to retain supported targets, select +the oldest suitable format, and avoid recompression solely to meet an edition's constraints. + +#[ +#set enum(numbering: n => [I#n.]) + ++ *A frozen wire contract is immutable.* A typed wire ID always identifies the same valid + forms and their meanings. A reader-visible extension requires a new ID. ++ *Readers implement each supported contract.* Exact-ID dispatch and local decoding accept + every valid form of that ID, preserve its meaning, and compose correctly with decoded children. ++ *Serialization preserves meaning.* Every emitted node satisfies its claimed wire contract, + and its meaning equals the meaning of the input representation. ++ *Edition enforcement covers the whole output.* A successful constrained write contains + only permitted typed IDs, including children, layouts, extension dtypes, and aggregates. ++ *Frozen edition records are immutable and membership is cumulative within a family.* Membership, + origin, and recorded minimum version remain fixed. A later edition adds permissions without + changing the earlier edition. Selecting families takes their union. ++ *Reader evolution preserves historical support.* A later conforming version of a project's code + retains support for its frozen formats, even after writers stop using them. ++ *A frozen edition's origin and minimum version are sound.* That version of the origin's code + supplies readers for every member of the edition. Required plugins must be present and registered. ++ *Supported writer targets remain writable.* A writer retains the behavior needed + to construct permitted representations for the input domain it promises to write to an edition. ++ *Serialization chooses the oldest available lossless form.* Selection depends on the + representation and the plugin's wire history. Edition validation follows selection. The + serializer does not choose a different form by consulting the edition allowlist. ++ *Future scheme configuration is consistent and closed under children.* For a target + edition, estimation, sampling, and full compression use one compatible behavior configuration. + Every produced child is subject to the same capability constraint. +] + +I1 fixes the meaning of a wire ID. I2 constrains readers, I3 constrains serializers, and I4 +constrains which IDs a write can emit. I5 fixes the meaning of an edition name. I6 preserves +support across versions of the reader code, and I7 connects that support to a published version +number. I8 concerns write availability, I9 concerns which valid form is chosen, and I10 prevents +compression from producing a form that must be recompressed solely to meet the edition. None +follows from the others. + += Definitions and scope + +== Symbols + +#table( + columns: (auto, 1fr), + table.header([Symbol], [Meaning]), + [$K$], [Component kinds: array, layout, extension dtype, and aggregate.], + [$u = (k, i)$], [A typed wire ID: kind $k in K$ and identifier string $i$.], + [$C_u$, $phi_u$], [The valid local forms of $u$, and their semantic interpretation.], + [$t$, $U(t)$], [A finite serialized object tree, and all typed IDs occurring in it.], + [$V(t)$], [The semantic meaning of a valid serialized tree.], + [$M_L$, $mu_L$], [Configuration $L$'s in-memory representations and their semantic meanings.], + [$R_L$, $H_L$], [IDs implemented by reader $L$, and IDs its writer can emit.], + [$D_L$, $S_L$], [Recursive reading and serialization for configuration $L$.], + [$e$, $A(e)$], [An edition, and its cumulative set of permitted typed IDs.], + [$E$, $A(E)$], [A selection of editions, and the union of their permissions.], + [$o(e)$, $m(e)$], [The origin project and its minimum code version recorded for a frozen edition.], + [$W_(L,E)$], [A write with configuration $L$ that enforces selection $E$.], + [$bot$], [Failure, rejection, or absence of a result, as specified by the operation.], +) + +An ID is typed because identical strings in different kinds identify different contracts. +In particular, $("array", "vortex.flat")$ and $("layout", "vortex.flat")$ are different IDs. +No ordering is inferred from ID strings. A plugin explicitly orders its own historical forms. + +== Serialized objects and semantic meaning + +A node has the form + +$ t = (u, p, t_1, dots, t_n). $ + +Here $p$ includes all non-child data needed to interpret the node: metadata, buffers, dtype, +length, and any relevant context supplied by its container. Dependencies such as extension +dtypes and serialized aggregate functions are included in the tree even when their concrete +bytes occur elsewhere. A file has a fixed-format root whose children include all versioned +components. That root has a separately assumed stable, correctly implemented contract. + +A concrete file can share children. Unfolding its finite acyclic dependency graph into a finite +tree does not change the argument. Cyclic graphs, malformed references, corrupted bytes, resource +exhaustion, and incompatible file envelopes are outside this compatibility theorem. Finite input +and terminating local routines are explicit premises. Permission to use an ID does not establish +termination or a resource bound. + +A local contract $C_u$ is a predicate on the payload, child interfaces, and child meanings. +Interfaces include the structural facts needed by the parent, such as child count, dtype, and +length. Its interpretation $phi_u$ defines the meaning of each valid local form. Validity and +meaning are defined recursively: + +$ V(t) = phi_(u)(p, V(t_1), dots, V(t_n)). $ + +This expression is defined only if each child is valid and the parent satisfies $C_u$. +The semantic domains are sorted by component kind and logical type. For example, an array means +its typed values and nulls, an extension dtype means its logical interpretation, and an aggregate +means its function and options. A file's meaning includes the declared component behavior as well +as its logical data. This prevents silently dropping a configured aggregate from counting as +serialization of the same file specification. + +The equations omit child interfaces for readability. Local implementations must preserve them +as well as semantic values. An aggregate contract's meaning includes the conditions that make +its use for pruning sound. The proof does not establish an aggregate algorithm's soundness. + +The ID closure is + +$ U(t) = {u} union union.big_(j=1)^n U(t_j). $ + +The fixed-format root contributes no edition-governed ID. An unrecognized dependency is not +removed from $U(t)$ merely because a particular query does not use it. + +== Reader and writer configurations + +A *configuration* $L$ identifies the versions of the Vortex Rust crates and optional plugin code, +the enabled modules, and the registered implementations. A crate version alone does not specify +which components an application can read or write. + +Its memory domain $M_L$ can differ completely from another configuration's domain. The meaning of +$a in M_L$ is $mu_(L)(a)$. The reader set $R_L$ is a set of typed IDs with implementations, not a set +of registered edition declarations. The writer set $H_L$ is defined independently: a historical +ID can be retained only for reading. + +No in-memory version field is assumed. Fields, children, and metadata can distinguish the shapes +handled by one current implementation. A deserializer can construct that implementation directly +or construct another equivalent current representation. Its output need not have the same +in-memory encoding ID as the wire ID that dispatched it. + +== Editions, families, and origin versions + +An edition selection $E$ contains at most one edition per family. Its permission set is + +$ A(E) = union.big_(e in E) A(e). $ + +The union of an empty selection is empty. An empty per-kind allowlist permits no component of that +kind. Within one family, $e <= e'$ implies $A(e) subset.eq A(e')$. There is no chronology comparison +between editions from different families. + +For a frozen edition, $o(e)$ names the *origin*: the project that supplies its component +implementations. The minimum version $m(e)$ refers to that project's code. For origin `vortex`, +this is the shared version of the Vortex Rust crates. Independent plugins can have their own +origins and version numbers. Version ordering is used only within one origin. + +Draft editions have a permission set but no published minimum version or perpetual read-support +guarantee. A stable edition can freeze when its origin publishes the code that supports it. +Recording the minimum version afterward documents that freeze, rather than creating a new one. + +A reader supports a selection when + +$ A(E) subset.eq R_L. $ + +This is only a coverage statement. Correct decoding follows from the local implementation premise +below and an induction, not from this definition alone. Registering an edition declaration changes +which permissions can be selected. It does not enlarge $R_L$, $H_L$, or $M_L$. + += Implementation and publication premises + +== Local reader premise (I2) + +For every $u in R_L$, reader $L$ has an exact-ID decoder $d_(L,u)$. Given a valid local payload +and correctly decoded child representations $a_1, dots, a_n$, this decoder terminates and returns +$a in M_L$ with the required interface and + +$ mu_(L)(a) = phi_(u)(p, mu_(L)(a_1), dots, mu_(L)(a_n)). $ + +The decoder validates the contract of the exact supplied ID. Recognizing a successor ID does not +expand what an older ID permits. In strict reading, dispatch fails when $u in.not R_L$. + +This is a local obligation for each implementation. It is stronger than recognizing a string in a +registry and weaker than assuming the whole-file compatibility result. + +== Local writer premise (I3) + +A successful local serializer for $a in M_L$ returns an ID $u in H_L$, a payload $p$, and child +representations $a_1, dots, a_n$. Their interfaces satisfy $C_u$, and + +$ mu_(L)(a) = phi_(u)(p, mu_(L)(a_1), dots, mu_(L)(a_n)). $ + +Recursive serialization follows a finite, well-founded dependency structure. Each child satisfies +the same premise. Newly constructed children are covered too. Thus local structural adaptation +cannot bypass either the semantic obligation or recursive validation. + +Compression, layout construction, or another preparation step used before serialization has its +own value-preservation obligation. A theorem about serialization of $a$ guarantees the meaning of +$a$. It guarantees the original source values only when preparation preserved them. + +== Constrained-write premise (I4) + +The constrained writer recursively checks every emitted typed ID against $A(E)$. It reports success +only if serialization and every such check succeed. A failure can occur after some bytes have been +written: this premise is about a successfully completed file, not transactional or atomic I/O. +Edition checks must use the serializer's returned wire ID rather than the in-memory encoding ID. + +== Publication premises (I1, I5, I6, I7) + +Once frozen, both $C_u$ and $phi_u$ remain fixed for each published ID. Frozen edition membership, +origin, and recorded minimum version also remain fixed. Later editions are cumulative in their +own families. + +For each frozen $e$, version $m(e)$ of the origin project's code provides conforming implementations +for every member of $A(e)$. Later conforming versions retain that support and its meanings. +The project must preserve this support when publishing code. Increasing a version number alone +does not establish compatibility. When origins are combined, their implementations must be installed +in a compatible host configuration and agree on any shared contracts. Taking the maximum of version +numbers from unrelated origins is undefined. + += Compatibility proofs + +== Lemma 1: recursive reading preserves meaning + +Let $t$ be valid, finite, and acyclic, and let $U(t) subset.eq R_L$. Under I1 and the local reader +premise, $D_(L)(t)$ succeeds and + +$ mu_(L)(D_(L)(t)) = V(t). $ + +*Proof.* Induct on the height of $t$. A leaf has no children. Its ID belongs to $R_L$, so exact-ID +dispatch selects its decoder. The local reader premise returns the leaf's prescribed meaning. +For a non-leaf, each $U(t_j)$ is contained in $U(t)$ and hence in $R_L$. Each child has smaller +height, so the induction hypothesis supplies its correctly decoded representation and interface. +The parent's ID also belongs to $R_L$. Applying @local-reader to those children yields +@wire-meaning. Finite height and terminating local routines complete the induction. #h(1fr)#sym.square.stroked + +This proves closure across component kinds as well as nested arrays. Checking only a root array ID +does not supply the induction hypothesis for its child encodings, extension types, or dependencies. + +== Lemma 2: recursive serialization preserves meaning + +If recursive serialization of $a$ succeeds with tree $t$, the local writer premise implies that +$t$ is valid and $V(t) = mu_(L)(a)$. + +*Proof.* Induct on the finite serialization dependency structure. For a leaf, @local-writer gives +both validity and equality. For a parent, the induction hypotheses replace every child meaning +$mu_(L)(a_j)$ with $V(t_j)$. The local contract establishes parent validity, and @local-writer becomes +@wire-meaning with result $mu_(L)(a)$. #h(1fr)#sym.square.stroked + +#block(breakable: false)[ +== Theorem 1: successful edition-constrained writes are compatible + +Let $W$ be any writer configuration, $L$ any conforming reader, and $E$ a selected edition set. If + +$ W_(W,E)(a) = t != bot quad "and" quad A(E) subset.eq R_L, $ + +then + +$ D_(L)(t) != bot quad "and" quad mu_(L)(D_(L)(t)) = mu_(W)(a). $ +] + +*Proof.* Recursive enforcement gives $U(t) subset.eq A(E)$. Coverage then gives +$U(t) subset.eq R_L$. Lemma 2 establishes validity and $V(t) = mu_(W)(a)$. Lemma 1 gives +$mu_(L)(D_(L)(t)) = V(t)$. Transitivity proves the claim. #h(1fr)#sym.square.stroked + +The writer can use newer Vortex crates than the reader. No premise compares their crate versions, +memory layouts, or compression implementations. The reader needs the emitted contracts, not knowledge +of the writer. Two writers can produce different bytes for equal values while both satisfy this +theorem. Byte-for-byte reproducibility does not follow. + +The theorem is conditional on successful writing. It does not assert that every array has a +permitted form, or that every configured compressor can construct one. + +== Theorem 2: later readers preserve readability + +Suppose $t$ uses frozen IDs, and a conforming reader $L$ reads it under Lemma 1. If $L'$ is a later +conforming configuration retaining those implementations under I6, then $L'$ reads $t$ with the same +meaning, even if $M_L$ and $M_(L')$ differ. + +*Proof.* Retention gives $U(t) subset.eq R_(L')$. I1 preserves the validity and meaning of $t$. +Apply Lemma 1 to $L'$ and to $L$. Both results have meaning $V(t)$. #h(1fr)#sym.square.stroked + +A reader need not convert a historical in-memory object into a current one. It can decode the +historical bytes directly into its current representation. Historical contract support is required. +A separate legacy memory type, version flag, upgrade pass, or recompression pass is not. + +== Theorem 3: frozen edition bounds are sufficient, but conservative + +For each origin $o$ named by a frozen selection, define + +$ b_(o)(E) = max { m(e) : e in E, o(e) = o }. $ + +A compatible reader configuration containing each required origin at version $b_(o)(E)$ or later, +with its implementations registered, reads every successful write constrained to $E$. + +*Proof.* I7 supplies $A(e)$ at $m(e)$ and I6 retains it at the selected later version. Taking the +union gives $A(E) subset.eq R_L$. Apply Theorem 1. #h(1fr)#sym.square.stroked + +This is a sufficient bound computed from the recorded edition minima for that selection, not a +proof of a necessary or globally minimal version of the reader code. A file's actual requirement +is $U(t)$, and I4 gives $U(t) subset.eq A(E)$. For example, selecting an edition that permits v1 and v2 but +emitting only v1 produces a file an appropriately configured v1-only reader can read. An edition's +minimum covers all its members, including ones the file never uses. + +Under I5, replacing an edition by a later one in the same family only enlarges the permission set. +Consequently a previously valid serialized tree remains permitted. This does not prove that a +compressor makes identical choices, nor that the enlarged target retains the same minimum reader. + +== Theorem 4: unsupported and forbidden IDs fail at their boundaries + +If a candidate output $t$ contains $u in.not A(E)$, it cannot be a successful constrained write. +If strict full reading encounters $u in.not R_L$, it fails at dispatch. A writer cannot emit a valid +local form through an implementation it lacks, $u in.not H_L$. + +*Proof.* The first conclusion is the contrapositive of recursive enforcement. The second is the +exact-ID dispatch rule. The third follows from the definition of $H_L$ and the local writer +premise. These are different failures: adding permission cannot supply an implementation, and +adding an implementation cannot supply permission. #h(1fr)#sym.square.stroked + +Unknown-component passthrough does not satisfy Lemma 1's full semantic decoding result. It can +preserve inert bytes for inspection or copying. Ignoring an unknown aggregate can disable pruning +and still allow correct logical data reads. That weaker operation is outside the full-component +interpretation proved here. Disabling editions removes I4, so Theorem 1's edition conclusion no +longer follows, although a specific file can still be readable. + += Representation changes and writer policy + +== Structural adaptation preserves compatibility + +Let a plugin adapt $a$ into another representation $b$ at the serialization boundary, with +$mu_(W)(b) = mu_(W)(a)$. If $S_(W)(b) = t$, Lemma 2 gives + +$ V(t) = mu_(W)(b) = mu_(W)(a). $ + +Thus a lossless structural downgrade preserves the compatibility theorem whenever the resulting +tree is permitted. Alternatively, the plugin can return adapted payload and child parts directly. +The local writer premise yields the same equality without constructing a legacy memory type. + +This result is about meaning. The equality does not prove that the transformation is cheap, that +it avoids decoding, or that it qualifies as structural rather than recompression. Those are +separate operational claims. A plugin must not relabel a newer payload with an older ID unless +the payload actually satisfies that older contract. + +== Oldest available lossless form + +For a current representation $a$, let $Q_(W)(a)$ be the finite set of local wire forms the plugin can +produce losslessly by its supported representation-preserving serialization operations, without +recompression. This set includes only forms the writer implements. Within the relevant wire +history, each form has an explicit chronological rank. I9 requires choosing a form with the +minimum rank in $Q_(W)(a)$, or reporting no serialization if the set is empty. + +*Proposition.* If I9 selects form $q$, no older form in $Q_(W)(a)$ was skipped. If all IDs in the +completed serialization, including those of its children, belong to $A(E)$, the constrained write +passes the edition checks. + +*Proof.* The first claim is the defining property of a minimum in a finite ordered set. The +second is recursive enforcement applied to the actual output. #h(1fr)#sym.square.stroked + +The useful implementation obligation is constructing a sound candidate set and selecting its +minimum. Merely listing supported IDs oldest-first does not establish I9. Historical IDs retained +only for reading need not belong to the writer's candidate set. This policy does not demand a +search through arbitrary recompressions to discover every mathematically possible encoding of the +same values. + +I9 orders formats within one encoding. It does not minimize the required version of the reader code. +A parent that uses an old ID can still have a child using a new ID. The complete tree determines +compatibility. If a selected form is forbidden, the edition check fails. It does not instruct the +serializer to try a newer form. Normal cumulative edition families preserve their earlier members, +but arbitrary custom permission sets need not have that property. + +== Example: decimal parts + +The historical decimal-byte-parts wire form stores one signed integer child. Its metadata field +`lower_part_count` must be zero. A current array can also hold lower-part children for wider values. +The plugin registers both historical and successor IDs and uses one current array implementation. + +Consider decimal values whose scaled integers are 12, 34, and 56. If compression constructs a +single signed child containing those integers, the plugin writes v1 and reuses the child. The +current array implementation's ability to represent wider values does not force this array to use +v2. The existing child can be serialized directly, with no upgrade, downgrade, or recompression. + +If the constructed array contains lower-part children, the current plugin selects v2. The v1 +reader's contract forbids those children even though it recognizes the metadata field's name. +A hypothetical operation that merges children into one integer buffer needs its own value and +cost analysis. The presence of small numerical values alone does not mean serialization already +performs that operation. For an old target, a future scheme must directly construct the old +single-child form when appropriate, or choose another permitted encoding. + +== Retaining old write paths (I8) + +Let $B_(W,E)$ be the source-value domain for which writer $W$ promises to write target $E$. +I8 requires a retained construction procedure that, on that domain, terminates with a +value-preserving representation whose complete serialization is permitted. This is an explicit +availability obligation beyond Theorem 1. + +A writer can satisfy I1 through I7 while deleting every old compression path and rejecting all +writes to an old target. Reader compatibility remains true for successful writes but is then +unhelpful to that writer's users. I8 rules out that regression on its stated domain. It makes no +promise for arbitrary custom arrays or arbitrary source values outside that domain. + += Future scheme configuration and recompression + +== Configuration obligations + +Fix a target selection $E$. A scheme configuration $c$ includes the behavior that affects its +output representation. Each scheme orders its supported behaviors and selects the newest one whose +declared output capabilities are permitted. Distinct algorithms can still compete. Their ordinary +compression decisions are not ordered by wire-version age. + +Let $P(c)$ be a set of typed IDs bounding the complete serialization of any successfully produced +representation under configuration $c$, including every child and fallback. The future contract +requires: + ++ *Capability soundness:* for every successful compression result $a$, serialization terminates + successfully without further compression: $S_(W)(a) = t != bot$, with $U(t) subset.eq P(c)$. ++ *Target admissibility:* $P(c) subset.eq A(E)$. ++ *Phase consistency:* estimation, sample compression, and full compression use the same + behavior configuration $c$. An estimate concerns that behavior. It need not predict the exact + full-input compression ratio. ++ *Recursive closure:* child compressors and fallbacks use admissible configurations, and their + possible IDs are included in $P(c)$. Configuration remains fixed for the write, or an equivalent + coherent snapshot is used. ++ *Semantic preservation:* full compression preserves the input values. This does not follow + from capability declarations. + +Capability soundness is a universal obligation across the full input domain. A sample containing +only narrow decimal values cannot justify using a v1-only claim for a full-input path that can +produce lower-part children. The configured full path must handle that case with a permitted +construction or report failure. An estimate alone cannot prove this property. + +== Theorem 5: configured compression needs no recompression to satisfy editions + +Suppose the obligations above hold and full compression of source $x$ under $c$ succeeds with +representation $a$. Its serialization succeeds without recompression solely to satisfy $E$, passes +all edition checks, and is readable with the meaning of $x$ by every conforming supporting reader. + +*Proof.* Capability soundness provides the terminating serialization $t = S_(W)(a)$ without further +compression. It also gives $U(t) subset.eq P(c)$. Admissibility gives +$P(c) subset.eq A(E)$, hence $U(t) subset.eq A(E)$. All edition checks therefore pass. Semantic +preservation equates the meaning of $a$ with that of $x$. Apply Theorem 1. #h(1fr)#sym.square.stroked + +The theorem does not infer absence of recompression merely from an allowlist: it uses the +explicit construction guarantee in capability soundness. Its practical purpose is to require +schemes to establish that guarantee before compression rather than repair incompatible arrays +later. Stateful or configurable schemes are possible implementations, not a requirement to put a +version field on every array. + +Phase consistency additionally guarantees that candidates are evaluated under the behavior +actually selected for compression. Without it, full compression can still produce permitted output, +but the selection process can estimate one representation and produce another. No theorem here +establishes optimal compression or estimator accuracy. + +Array schemes cover the array dependencies they construct. A complete file also needs admissible +layout, dtype, and aggregate construction. Their permission checks remain independent boundaries. +A caller-supplied array need not have been built under $c$. It can require explicit adaptation, +recompression, or rejection. This theorem does not extend to it without the same premises. + += Implementation locations and remaining work + +These source locations show where the model's requirements apply to the Rust implementation. +They identify the APIs involved, but do not prove that every component satisfies the requirements. + +- `vortex-array/src/array/plugin.rs` separates the in-memory plugin ID from its historical + `serialized_ids`. Serialization returns a concrete ID, metadata, buffers, and children. + Deserialization receives the exact wire ID. +- `vortex-edition/src/lib.rs` and `session.rs` define typed component membership, independently + versioned families, cumulative inclusion, origin metadata, and selection. +- `vortex-btrblocks/src/builder.rs` filters schemes by their declared serialized IDs. General + edition-derived behavior configuration, including phase consistency and recursive capability + coverage, remains an implementation obligation for Theorem 5. +- `encodings/decimal-byte-parts/src/decimal_byte_parts/plugin/` implements the structural example. + With no lower-part children, the plugin selects v1. Otherwise it selects v2. Both deserialize + through the current representation. + +Historical contract tests need to cover validation against the exact wire ID, every component kind, +and recursively created children. Writers must retain the code needed for supported older targets. +Scheme configuration needs sound output declarations, with fixtures covering files produced by +new writers for old readers. These checks supply implementation evidence. The model separately +requires preservation of values, compatibility of successful writes, and the ability to write +every input in the promised domain. + +The intended API direction is recorded in +#link("https://github.com/vortex-data/vortex/pull/9779")[Vortex PR #9779]. Updating the Vortex crates does not by +itself require an upgrade or downgrade of an array, and edition enforcement does not choose the +wire form. + +#pagebreak(weak: true) + += Complete two-format matrix + +This matrix applies the planned scheme configuration to one encoding in an otherwise compatible +file. It covers eight writer, target, and output combinations, with two reader outcomes each. +The wire-format column describes a possible output, not a separate scheme setting. A supported +write means that suitable valid inputs and output shapes exist. It does not promise success for +every input. L1 reading a v1-only E2 file does not establish that L1 supports all of E2. + +#table( + columns: (auto, 1fr), + table.header([Symbol], [Meaning]), + [v1], [The encoding's original serialized format.], + [v2], [A successor serialized format with a distinct wire ID.], + [L1], [Older Vortex crate version that reads and writes v1 only.], + [L2], [Newer Vortex crate version that reads and writes v1 and v2 through one current array + implementation.], + [E1], [Edition permitting v1.], + [E2], [Edition permitting v1 and v2.], + [✓], [Supported for a suitable input and correctly registered implementations.], + [X], [Unsupported or forbidden.], + [N/A], [No successful write, so no reader outcome.], +) + +#text(size: 9pt)[ +#table( + columns: (auto, auto, auto, 1fr, auto, auto), + table.header([Writer], [Target], [Wire form], [Write¹], [L1 reads], [L2 reads]), + [L1], [E1], [v1], [✓], [✓], [✓²], + [L1], [E1], [v2], [X: unsupported and forbidden], [N/A], [N/A], + [L1], [E2³], [v1], [✓], [✓], [✓²], + [L1], [E2³], [v2], [X: unsupported], [N/A], [N/A], + [L2], [E1], [v1], [✓⁴⁵], [✓], [✓²], + [L2], [E1], [v2], [X: forbidden], [N/A], [N/A], + [L2], [E2], [v1], [✓⁴⁵], [✓], [✓²], + [L2], [E2], [v2], [✓⁴], [X: unknown ID], [✓], +) +] + +¹ Success is conditional on representability and successful preparation. Theorem 1 does not make +all inputs writable. A permitted parent is insufficient if any child is forbidden. + +² L2 decodes the historical contract directly into its current implementation. It need not first +construct and upgrade a legacy array. + +³ L1 must know the E2 declaration to select it. Registration grants permission, not a v2 +implementation. A v1-only file can be read by L1 even though E2 also permits v2. + +⁴ Future configurable schemes establish Theorem 5's obligations. Current BtrBlocks filtering uses +schemes' declared serialized output IDs. That filtering is correct for its current declarations. +A scheme that can produce several formats still needs configuration to select compatible behavior. + +⁵ A newer implementation can construct the original single-child decimal form and emit v1 directly. +For E1, a compatible construction must be selected before compression. For E2, the same old form can +arise naturally, and I9 still selects v1. + +#pagebreak(weak: true) + += Counterexamples when an invariant is omitted + +Each example shows a failure that the omitted invariant prevents. These are hypothetical failures, +not claims about current Vortex bugs. + +#table( + columns: (auto, 1fr), + table.header([Omitted invariant], [What can change]), + [I1: immutable contract], [An ID starts interpreting a buffer as unsigned instead of signed. + Old and new readers accept the same bytes but disagree on values.], + [I2: correct local reader], [A registered decoder reverses the child order. All ID checks pass, + but composition changes the values.], + [I3: correct serializer], [A serializer truncates a wide value while claiming a valid old ID. + Every reader consistently returns the wrong value.], + [I4: recursive enforcement], [A permitted dictionary parent contains a forbidden compressed + child. The target reader can dispatch the parent but cannot read the child.], + [I5: fixed edition membership], [An already published edition gains v2. Its name no longer + denotes the permission set users previously selected, even if its minimum version is also + revised to preserve coverage. Alternatively, changing only a minimum version changes the + published deployment promise without changing any file bytes.], + [I6: retained readers], [The origin supplied v1 at its recorded minimum version but a later version + removes it. Updating the reader's code breaks historical files.], + [I7: sound minimum version], [The declared minimum predates the first v2 implementation. + Membership is fixed and readers are additive, yet the promised minimum cannot read all output.], + [I8: retained writer paths], [The writer keeps historical decoders but deletes old construction + behavior. Old files remain readable, while supported old-target writes become unavailable.], + [I9: oldest-form choice], [An array fitting v1 is always emitted as v2. The write can remain safe + for E2, but an otherwise unnecessary newer-reader requirement is introduced.], + [I10: coherent schemes], [Sampling uses the old form, while full compression creates a forbidden + successor or child. Final validation remains safe by rejecting the file, but successful writing + now requires another compression pass or a different construction.], +) diff --git a/docs/specs/versioning.md b/docs/specs/versioning.md new file mode 100644 index 00000000000..26986a773f2 --- /dev/null +++ b/docs/specs/versioning.md @@ -0,0 +1,85 @@ +# Versioning + +These docs explain how Vortex keeps files readable as its Rust implementation changes. They cover +how to write files for older deployments and how to add encoding formats without breaking existing +files.[^proof] + +## The Vortex Rust library + +Vortex's Rust library provides the array types and algorithms that an application uses to compress +and process data **in memory**. It also reads and writes files. The code is split across the +`vortex` crate and supporting crates such as `vortex-array` and `vortex-file`. + +The _library version_, such as `0.85.0`, is the version shared by these crates when they are +published. Changes to the crates follow +[Rust's semantic versioning rules for crates](https://doc.rust-lang.org/cargo/reference/semver.html). +Published versions of the `vortex` crate are on +[crates.io](https://crates.io/crates/vortex/versions), with release notes on +[GitHub](https://github.com/vortex-data/vortex/releases). + +In these docs, a _reader_ is the Vortex code that an application uses to read files. A _writer_ +is the Vortex code it uses to write files. Each uses a particular crate version and the component +implementations registered by that application. One application can use both. + +## Editions + +When writing a file, an application selects an _edition_: a named set of formats it is allowed to +**serialize to disk**. To write files for an older deployment, select an edition whose formats +the Vortex code in that deployment can decode. The application writing the file still uses the +in-memory array types and algorithms provided by its own version of the Vortex crates. + +Editions belong to _families_. The `core` family covers the default writer's formats, while optional +features can have their own families. Edition names use dates: `core2026.08.3` belongs to the `core` +family, `2026.08` gives its year and month, and `3` distinguishes editions in that family and month. +These are Vortex editions, separate from Rust language editions such as Rust 2024. + +Once an edition is _frozen_, its permitted formats and reader requirements stay fixed. New formats +go into later editions. + +## Array plugins + +An _array plugin_ implements the read and write code for an array encoding. It can read an older +serialized format into the current in-memory array type, and write that type in an older format when +the array's structure allows it. **The serialized format and the Rust array type do not have to +change together.** + +For example, a service can update its Vortex crates while keeping the edition it previously +selected for writing. The service uses the new crate version's array implementations, but writes +only formats permitted by that edition. Applications that could decode all those formats before +the update can still decode the output afterward, without updating their own Vortex crates. + +**Encoding changes must be additive:** support for a new serialized format must preserve read +support for the old formats. The +[decimal encoding example](versioning/arrays-and-compression.md#example-decimal-children) shows +one array implementation supporting two serialized formats. + +## Suggested reading order + +After this overview, read the pages in this order: + +1. [Using editions](versioning/using-editions.md): select formats for a writer and find the crate + versions and plugins its readers need. +2. [Arrays and compression](versioning/arrays-and-compression.md): follow a decimal array through + compression, serialization, and reading to see how one implementation supports multiple formats. +3. [Compatibility](versioning/compatibility.md): use the invariants and matrix to work through the + combinations of old and new readers, writers, editions, and serialized formats. +4. [Edition lifecycle and registry](versioning/editions.md): add or revise a format, or look up an + edition's components and minimum reader version. This page is also a reference to return to later. + +```{toctree} +--- +maxdepth: 1 +hidden: true +--- + +versioning/using-editions +versioning/arrays-and-compression +versioning/compatibility +versioning/editions +``` + +The [implementation roadmap](versioning/arrays-and-compression.md#implementation-roadmap) covers +the remaining work, including configuring compression to produce formats permitted by the target +edition. + +[^proof]: For a mathematical treatment, see the [formal proof of the versioning model (PDF)](../_static/versioning-proof.pdf). diff --git a/docs/specs/versioning/arrays-and-compression.md b/docs/specs/versioning/arrays-and-compression.md new file mode 100644 index 00000000000..6db775c4bae --- /dev/null +++ b/docs/specs/versioning/arrays-and-compression.md @@ -0,0 +1,167 @@ +# Arrays and compression + +The edition selected by a writer limits the formats it can put in a file. It does not choose the +Rust array types used to hold the data before writing. This page uses decimal arrays to explain +how the same in-memory type can support old and new serialized formats, and what that requires +from the compressor. + +A _compression scheme_ takes values and constructs an encoded array **in memory**. An _array +plugin_ supplies the code to read and write its serialized formats. **Compression and serialization +are separate operations**, so improving a compression algorithm does not necessarily require a +change to the format it writes. + +An encoded array can contain other arrays, called _children_. For example, dictionary encoding +stores a dictionary of values and an array of codes that refer to those values. Both are children +of the dictionary array, and each can have its own encoding. A reader needs to understand those +child encodings as well as the dictionary encoding. + +## Additive encoding changes + +A serialized array's _wire ID_ tells the reader how to interpret its metadata, buffers, and +children. The plugin responsible for that ID reads the stored data into an array supported by the +current Vortex crates. **The format fixes the meaning of the stored data, not its Rust array type.** +The resulting array must have the same values, data type, and nulls, even if its fields or children +are arranged differently in memory. + +Encoding changes must be _additive_: a new version of the Vortex crates must retain read support +for the encoding's earlier frozen formats when it adds a new format. Each old wire ID keeps the +same meaning. A change that an old reader cannot interpret, such as an additional child or a new +supported data type, needs a new wire ID. **The new reader must support the old format. The old +reader is not required to support the new one.** + +## Example: decimal children + +Decimal values can be represented as integers with a shared scale. For example, the values +`[1.25, 2.50, 3.75]` can be stored as `[125, 250, 375]` with a scale of two decimal places. + +The original decimal-byte-parts format stores these integers in one child array. For wider decimal +values, the current implementation can split each scaled integer across several child arrays. +The same Rust array type handles both cases: one child for the original representation, or +additional children for the wider representation. + +The original format uses `vortex.decimal_byte_parts` and requires one signed integer child. +Its `lower_part_count` metadata field must be zero. The newer `vortex.decimal_byte_parts.v2` format +also permits unsigned lower-part children after the signed most-significant child. + +| Array structure | Serialized ID | Reader that only supports the original format | +|---|---|---| +| One signed integer child, no lower-part children | `vortex.decimal_byte_parts` | Reads the array | +| A signed integer child and additional lower-part children | `vortex.decimal_byte_parts.v2` | Reports an unknown ID | + +For the example values, a child containing `[125, 250, 375]` fits the original format. The serializer +reuses that child and writes the original metadata. **There is no downgrade or recompression.** +A reader using the current Rust array type can read this file directly, with no lower-part +children and no separate upgrade step. An array with lower-part children requires the newer format +because the original format does not permit those children. + +**The array needs no format-version field.** The serializer can determine which format to write +from the children that are present. It does not inspect the values to see whether several children +can be merged into one, even if the values happen to fit in a single integer. + +The current decimal compression scheme constructs only single-child arrays and declares the +original serialized ID. Values too wide for that scheme remain in the standard, uncompressed +decimal representation, called a canonical decimal array. These outputs are compatible with the +existing core editions. The in-memory decimal-byte-parts ID matches the newer wire ID, but no +declared edition currently permits `vortex.decimal_byte_parts.v2` in a file. + +## Compression schemes + +By the time serialization starts, the array already has a particular structure. If the target +edition permits only the original decimal format, the compressor needs to produce a one-child +array or choose another permitted encoding. If compression produces a multi-child array, the +current decimal serializer chooses the newer format, and the write fails the edition check. + +Vortex's default compressor, BtrBlocks, chooses among compression schemes. Each scheme declares +the wire IDs it can produce through `produced_encodings()`. The builder excludes a scheme if any +of those IDs is forbidden by the selected editions. These declarations cover arrays the scheme +constructs directly. Schemes used to compress the children have their own declarations and go +through the same filtering. Custom compressors must also produce permitted formats, and the +writer checks their actual output during serialization. + +The existing schemes and their declarations are correct for the formats they produce today. +**The planned change is to configure a scheme for the selected editions, instead of excluding the +whole scheme when it can also produce a newer format.** For a decimal scheme, this means enabling +additional children only when the selected editions permit their format. + +The scheme must select the newest behavior it supports that produces permitted formats. That +selection must happen before estimating compression ratios or compressing a sample, so that those +estimates describe the same behavior used to compress the full input. Child compression and +fallbacks must also produce permitted formats. + +### Example: extending Pco's supported types + +Pco compresses numeric arrays. Consider a hypothetical extension that adds support for signed and +unsigned 8-bit integers (`i8` and `u8`). The frozen `vortex.pco` format does not support those types, +so the extension needs a new wire ID. + +The serializer can continue to use `vortex.pco` for the original types and select the new ID for +8-bit arrays. The reader must still reject an 8-bit payload labelled with the original ID, even +if its current implementation supports 8-bit values under the new ID. + +For an edition that excludes the new ID, the scheme must disable that Pco behavior before +compression. The compressor can then choose another permitted encoding for the 8-bit input. + +## Serializing an array + +Before the writer can check an array against the selected editions, it needs to know which format +the plugin will write. **The plugin selects the format first. The writer then checks whether the +editions permit it.** + +The plugin's serializer returns a wire ID, metadata, buffers, and children. When several formats +preserve the array's representation without recompression, it selects the oldest one it supports +writing. The selection depends on the array's structure, not the _edition allowlist_ +(the set of permitted IDs). A format retained only for reading is not a candidate for writing. + +The serialization context checks the returned ID against the selected editions, then serializes +and checks the children recursively. Layouts, extension dtypes, and aggregates have their own +checks. For example, if a dictionary array is permitted but the encoding of its values child is +not, the write fails. + +If the selected ID is forbidden, the write fails. The serializer does not retry a newer format, +even if a custom edition permits that newer format and excludes the older one. + +A _lossless structural downgrade_ adapts an array's metadata, buffers, or children to fit an older +serialized format without recompressing its values. A plugin can do this during serialization, +without constructing an old Rust array type. _Recompression_ constructs a different encoding of +the same logical values. If the array cannot fit a permitted format without recompression, the +write path must arrange that compression explicitly or fail. + +## Reading a serialized array + +Reading an old format does not require recreating the in-memory array type used by the original +writer. The reader's plugin can read it directly into the reader's current array implementation, +as it does for the one-child decimal format. + +The plugin declares the serialized IDs it can read. The reader uses the ID in the file to select +a registered plugin, then passes that ID to the plugin. The plugin must apply that ID's format +contract. **Edition selection restricts writing.** A reader can read any format for which it has +a registered implementation, regardless of the selected target editions. + +Some formats need a different arrangement of arrays when read into the current implementation. +ALP, a floating-point encoding, stores values that do not fit its main representation separately +as _patches_. The old ALP format stores those patches inside the ALP array. The current plugin +reads them into a `Patched` parent around a patch-free ALP child. This changes the array tree while +preserving the values, as part of reading the file. It needs no separate upgrade pass. + +## Implementation roadmap + +The writer already rejects serialized formats that the selected editions forbid. The remaining +work is to choose compatible compression behavior early enough to avoid compressing the same data +again just to meet an edition's constraints: + +- **Configure schemes before compression.** Listing every possible output ID excludes an entire + scheme if the target forbids any of them. A configuration can limit the scheme to compatible + outputs. The API for stateful or configurable schemes is not settled. +- **Apply the configuration to children and fallbacks.** Every array produced by those paths must + serialize to permitted IDs, including arrays created when a preferred scheme cannot handle the + input. +- **Retain writer support for older editions.** Older targets can require different layout + strategies or encoding choices. Writers need to make those choices before serialization. +- **Declare new formats in editions.** Multi-part decimal serialization exists, but its ID is not + yet in an edition and the default scheme does not construct it. New formats need the + [testing and promotion process](editions.md#format-testing-and-promotion). +- **Maintain compatibility tests.** Historical fixtures, invalid payloads under old IDs, recursive + edition checks, and configured schemes need coverage. Tests must cover both valid historical + payloads and newer payloads incorrectly labelled with an old wire ID. + +[Next: Compatibility](compatibility.md) diff --git a/docs/specs/versioning/compatibility.md b/docs/specs/versioning/compatibility.md new file mode 100644 index 00000000000..4a548ad0e1e --- /dev/null +++ b/docs/specs/versioning/compatibility.md @@ -0,0 +1,131 @@ +# Compatibility + +**The application writing a file and the application reading it do not need the same Vortex crate +version.** The application writing the file selects editions to limit the serialized formats it +can use. The application reading the file needs code that can decode those formats. + +For example, application A uses Vortex crates `0.85.0` and writes a file with only `core2026.08.0` +selected. Application B uses Vortex crates `0.84.0`, that edition's recorded minimum. With the +required component implementations registered, B can read A's file even though A uses newer crates. + +Here, a _writer_ means the Vortex code that an application uses to write files, with its crate +version, registered implementations, and selected editions. A _reader_ means the Vortex code that +an application uses to read files, with its crate version and registered implementations. +**Older and newer readers or writers refer to crate versions, not editions.** + +To obtain the compatibility guarantee for each selected frozen edition, the application reading +the file must meet that edition's recorded minimum version requirement. It must register the +implementations for every permitted component, including any optional plugins. These are the +[edition's reader requirements](using-editions.md#choosing-a-reader-version). With these +requirements met, it must be able to read any valid file successfully written within the selected +editions' restrictions. It must also understand the file's outer container format. A write can +fail if the selected editions cannot represent the input. + +An application with older Vortex crates can sometimes decode a particular file even if it does +not meet the edition's minimum version. That file can contain only formats implemented by those +older crates. The minimum version guarantees decoding for every format the edition permits, +including formats that this particular file does not use. + +The first frozen edition is `core2025.05.0`. Its components were writable with version `0.36.0` of +the Vortex crates, and later crate versions must retain read support for them. Each subsequent +frozen edition adds the same requirement for its additional components. + +## Compatibility invariants + +A file can contain several serialized formats. Each has an identifier, called a _wire ID_, that +selects the code used to read it. The first five invariants establish which files must remain +readable and what readers and writers must preserve. + +1. **A frozen wire ID has one fixed contract.** Its accepted dtypes, metadata, children, buffers, + options, and interpretation cannot change. An extension that an old reader cannot understand + requires a new ID. +2. **A frozen edition has fixed membership, origin, and minimum version of that origin.** The origin + identifies the project that supplies the component implementations, as described in + [Choosing a reader version](using-editions.md#choosing-a-reader-version). Later editions in the + same family include every earlier component. An edition name must continue to identify the same + formats and reader requirements. +3. **Compression, serialization, and reading must preserve the values, dtype, and nulls.** Each + serializer must produce a valid instance of its chosen format. Each reader must interpret that + format correctly, including its children. A reader must reject an invalid payload under an old + ID even if it supports that payload under a newer ID. +4. **New versions of a component's implementation must retain read support for its frozen formats.** + This includes formats no longer used by writers. Edition selection restricts writing. It does not + restrict the registered formats that a reader can read. +5. **A write with edition checks enabled can succeed only if every serialized component is + permitted.** This includes array children, layouts, nested extension dtypes, and aggregate + functions. Custom writer strategies must obey the same checks. Checking only the root or the + compressor's declared output is not sufficient. + +The remaining invariants govern writing. Read compatibility alone does not require a writer to +keep producing old formats, or to choose an old format when a newer one also fits. + +6. **A writer must retain the ability to produce files for each target edition it supports.** This + does not require retaining every historical writer. +7. **A scheme must select a configuration before estimation, sampling, or full compression.** That + configuration must use the newest supported behavior that produces permitted formats. All three + stages must use that configuration, and child compression must obey the same permissions. + General per-writer scheme configuration is still future work. +8. **A serializer must select the oldest supported writable format that preserves the array's + representation losslessly without recompression.** It selects from the array's structure without + consulting the edition allowlist. The serialization context then checks the returned ID. + Historical formats retained only for reading do not have to remain writable. + +## Compatibility matrix + +The matrix follows one encoding as a new serialized format is added. L1 is the older version of +the Vortex crates, and L2 is the newer version. The application writing the file uses one of these +versions. The two reading columns show what happens when the application reading that file uses +L1 or L2. Both versions are assumed to decode the rest of the file, with the required +implementations registered. + +| Symbol | Definition | +|---|---| +| v1 | The encoding's original serialized format | +| v2 | A newer serialized format with a distinct wire ID | +| L1 | Older crate version: reads and writes only v1 | +| L2 | Newer crate version: reads and writes v1 and v2 through one current array implementation | +| E1 | An edition that permits v1 but not v2 | +| E2 | An edition that permits v1 and v2 | +| ✓ | The operation is supported for suitable inputs | +| X | The operation is unsupported or forbidden | +| N/A | The write cannot succeed, so there is no reader outcome | + +The matrix assumes the planned scheme configuration support.⁴ The serialized-format column shows +a possible output. It is not another setting that the writer chooses independently: the scheme +must construct an array that its crate version can serialize within the selected edition. + +| Crate version used to write | Target edition | Serialized format | Write result¹ | Read with L1 | Read with L2 | +|---|---|---|---|---|---| +| L1 | E1 | v1 | ✓ | ✓ | ✓² | +| L1 | E1 | v2 | X: unsupported and forbidden | N/A | N/A | +| L1 | E2³ | v1 | ✓ | ✓ | ✓² | +| L1 | E2³ | v2 | X: unsupported | N/A | N/A | +| L2 | E1 | v1 | ✓⁴⁵ | ✓ | ✓² | +| L2 | E1 | v2 | X: forbidden | N/A | N/A | +| L2 | E2 | v1 | ✓⁴⁵ | ✓ | ✓² | +| L2 | E2 | v2 | ✓⁴ | X: unknown ID | ✓ | + +Each of the eight rows has two reader outcomes, covering all 16 combinations. Reader outcomes +apply only after a successful write. In particular, L1 can read an E2 file containing only v1, +but it cannot read an E2 file that uses v2. + +¹ An input array can require a format that the target forbids. The plugin can provide a lossless +structural downgrade. If the array requires recompression, the write path must arrange it +explicitly or fail. + +² L2 reads v1 into its current implementation. The plugin adapts the structure only if necessary. +Using a newer version of the Vortex crates does not itself require an array upgrade or conversion. + +³ L1 needs the E2 declaration to select it. Registering the declaration does not add v2 support, +so L1 still writes only v1. + +⁴ Current schemes declare the serialized IDs that they produce, and the builder filters them by +those IDs. General per-writer configuration is not yet implemented. The matrix assumes that the +scheme selects compatible behavior before estimation, sampling, and full compression. Its output +then needs no recompression solely to meet the edition. + +⁵ L2 can construct a decimal array with one integer child and serialize it as v1 without +recompression. Additional lower-part children require v2. See the +[decimal example](arrays-and-compression.md#example-decimal-children). + +[Next: Edition lifecycle and registry](editions.md) diff --git a/docs/specs/versioning/editions.md b/docs/specs/versioning/editions.md new file mode 100644 index 00000000000..c38b3dea5c0 --- /dev/null +++ b/docs/specs/versioning/editions.md @@ -0,0 +1,158 @@ +# Edition lifecycle and registry + +A new serialized format needs testing before Vortex commits to reading it indefinitely. This page +describes how a format becomes part of a frozen edition and lists the formats in each edition. +For writer configuration and reader version requirements, see [Using editions](using-editions.md). + +**Freezing an edition fixes its formats and reader requirements.** Later versions of the code that +implements those formats must retain read support. New formats go into later editions. + +## Format testing and promotion + +A format intended for `core` starts in a dedicated edition family. This lets applications try the +format before it becomes part of the default writer's output. + +The first edition is a _draft_: it has no recorded `min_library_version` and no frozen compatibility +guarantee. A draft format is expected to be complete. If testing reveals a defect whose correction +changes what readers must understand, the correction needs a new wire ID and a later edition. + +After that initial testing, the format can enter a new `preview` edition so that more applications +can opt in to it. A later `core` edition can then include it for default use. **The format and its +wire ID stay the same during promotion.** Only the set of editions that permit it changes. + +The current `preview` edition contains no components. Its first component will go into a new +edition. Optional plugins also have their own families, including `tensor`, `zstd`, `spatial`, and +`json`, which can add editions independently of `core`. + +## Freezing an edition + +Each edition family names an _origin_, the project that supplies its component implementations. +The `core` family uses `vortex` as its origin, so its minimum versions refer to the Vortex Rust +crates. An independent plugin can have its own origin and version numbers. + +A stable edition can freeze when its origin project publishes the code that first supports it. For a +`core` edition, this happens when the Vortex crates are published. Until that crate version is +known, the Rust declaration uses `min_library_version: None`. The version is filled in afterward, +usually while the next crate version is being developed. **Filling in the field records the +original freeze.** The compatibility guarantee applies from the published crate version, even if +the declaration is updated later. An independent plugin follows the same process with its own +project's versions. + +**Deprecating a format does not remove the requirement to read it.** A writer can stop choosing +that format and use other formats permitted by the target edition. Readers must retain support +for the deprecated format because existing files can contain it. + +## Maintaining edition records + +Default edition declarations are in `vortex-edition/src/declarations/`. Optional modules keep +their declarations with their implementation code. The exported TOML records are under +`vortex/editions/`, grouped by family. The family record names the origin. A frozen edition record +gives the minimum version of that origin's code. + +For a new component, declare its own family and draft edition. For a revision, add a later edition +to the family that owns the earlier ID. Promotion adds the tested format to new `preview` and +`core` editions. When an edition freezes, record the first version of its origin's code that +supports every component in the edition. **Do not change a frozen edition's membership or the +contracts of its formats.** + +Regenerate the records with: + +```sh +cargo run -p xtask -- generate-editions +``` + +CI's `check-editions` command rejects changes to frozen records, including renames, unfreezing, and +deletion. It also rejects a new edition that does not follow its family's chronology. Changes to +permitted formats require a later edition. + +## Edition registry + +Each entry lists the components added by that edition. It also permits every component from +earlier editions in the same family, so an entry does not repeat the complete permitted set. + +### Frozen `core` editions + +The origin of every edition below is `vortex`. Each minimum refers to the shared version of the +Vortex Rust crates, including the `vortex` crate. + +#### `core2025.05.0` + +Minimum Vortex Rust crate version: `0.36.0`. + +- `array`: `fastlanes.bitpacked`, `fastlanes.for`, `vortex.alp`, `vortex.alprd`, `vortex.bool`, + `vortex.bytebool`, `vortex.chunked`, `vortex.constant`, `vortex.datetimeparts`, `vortex.decimal`, + `vortex.decimal_byte_parts`, `vortex.dict`, `vortex.ext`, `vortex.fsst`, `vortex.list`, + `vortex.null`, `vortex.primitive`, `vortex.runend`, `vortex.sparse`, `vortex.struct`, + `vortex.varbin`, `vortex.varbinview`, `vortex.zigzag` +- `layout`: `vortex.chunked`, `vortex.dict`, `vortex.flat`, `vortex.stats`, `vortex.struct` +- `dtype`: `vortex.date`, `vortex.time`, `vortex.timestamp` + +#### `core2025.06.0` + +Minimum Vortex Rust crate version: `0.40.0`. + +- `array`: `vortex.pco`, `vortex.sequence`, `vortex.zstd` + +#### `core2025.10.0` + +Minimum Vortex Rust crate version: `0.54.0`. + +- `array`: `fastlanes.rle`, `vortex.fixed_size_list`, `vortex.listview`, `vortex.masked` + +#### `core2026.08.0` + +Minimum Vortex Rust crate version: `0.84.0`. + +- `layout`: `vortex.zoned` +- `aggregate`: `vortex.bounded_max`, `vortex.bounded_min`, `vortex.max`, `vortex.min`, + `vortex.nan_count`, `vortex.null_count` + +#### `core2026.08.1` + +Minimum Vortex Rust crate version: `0.84.0`. + +- `array`: `vortex.onpair` + +#### `core2026.08.2` + +Minimum Vortex Rust crate version: `0.85.0`. + +- `array`: `vortex.map` + +#### `core2026.08.3` + +Minimum Vortex Rust crate version: `0.85.0`. + +- `array`: `vortex.parquet.variant`, `vortex.variant` +- `dtype`: `vortex.uuid` + +### Editions without a frozen guarantee + +These editions have no recorded minimum version of their origin project's code. New formats and +revisions get new draft editions. Vortex-maintained draft formats are expected to remain compatible +unless a defect blocks promotion into `core`. Independent plugin projects state their own policy. + +#### `preview2026.08.0` + +This edition currently adds no components. + +#### `tensor2026.04.0` + +- `array`: `vortex.tensor.cosine_similarity`, `vortex.tensor.inner_product`, `vortex.tensor.l2_norm`, + `vortex.tensor.l2_normalize` +- `dtype`: `vortex.tensor.fixed_shape_tensor`, `vortex.tensor.vector` + +#### `zstd2026.02.0` + +- `array`: `vortex.zstd_buffers` + +#### `spatial2026.08.0` + +- `dtype`: `vortex.st.box`, `vortex.st.linestring`, `vortex.st.multilinestring`, + `vortex.st.multipoint`, `vortex.st.multipolygon`, `vortex.st.point`, `vortex.st.polygon`, + `vortex.st.wkb` +- `aggregate`: `vortex.st.aabb` + +#### `json2026.08.0` + +- `dtype`: `vortex.json` diff --git a/docs/specs/versioning/using-editions.md b/docs/specs/versioning/using-editions.md new file mode 100644 index 00000000000..1c3ac792d13 --- /dev/null +++ b/docs/specs/versioning/using-editions.md @@ -0,0 +1,122 @@ +# Using editions + +To write files for another deployment, select editions whose formats that deployment can read. +This page explains how to configure that selection and find the crate versions and plugins +required to read the output. + +## Selecting edition families + +An _edition family_ groups editions for a related set of formats. For example, `core` covers the +default writer's formats, while `tensor` and `zstd` cover optional features. Keeping these in +separate families lets a writer enable an optional feature without changing its `core` selection. + +**A writer selects at most one edition from each family.** Selecting `core2026.08.3` and +`tensor2026.04.0`, for example, permits every component in either edition. Within a family, later +editions include the components from all earlier editions, so selecting a later edition adds +formats to the permitted set. + +## Components and wire IDs + +Reading a file requires more than decoding its compressed arrays. The reader also needs to +understand how the file is laid out, how to interpret custom data types, and how to use any stored +summaries to skip irrelevant data. Editions cover each of these parts of the file. + +An edition lists _components_, such as array encodings and file layouts. Each component has a kind +and an ID stored in the file, called its _wire ID_. **The kind and ID together identify the +component.** The array and layout encodings named `vortex.chunked`, for example, are distinct +components despite sharing the same ID string. + +_Zone maps_ store summaries such as the minimum and maximum in a group of rows. A reader can use +these to skip a group when no value in it can match a filter. The _aggregate functions_ that compute +these summaries also have wire IDs for their serialized definitions. + +| Kind | What the wire ID identifies | +|---|---| +| `array` | An array's serialized representation | +| `layout` | A node in the file's layout tree | +| `dtype` | An extension dtype: a custom logical data type in the schema | +| `aggregate` | An aggregate function stored in a zone map | + +## Configuring a writer + +A _session_ holds the writer's registered implementations and edition selection. The default +session from the `vortex` crate targets `core2026.08.3`. A session constructed without those defaults +needs its editions registered and enabled before writing. + +_Registering_ an edition makes its declaration available to the session, including which components +it permits. _Enabling_ the edition selects those components for writing. **The component +implementations must be registered separately.** Enabling another edition from the same family +replaces the previous selection. + +To write files for an older deployment, select a `core` edition whose recorded minimum does not +exceed that deployment's Vortex crate version. The deployment must also register the required +component implementations. Optional modules can enable their own families alongside `core`, such +as `tensor2026.04.0` for tensor support or `zstd2026.02.0` for Zstd buffer wrapping. If the selected +editions permit no components, the writer cannot serialize any edition-governed component. + +For custom or experimental formats outside the edition declarations, the Rust writer provides +`disable_editions()`. This disables checks for arrays, layouts, extension dtypes, and aggregate +functions, while still requiring their implementations to be registered. Files written with these +checks disabled have **no edition compatibility guarantee**. + +## Checks during writing + +The final checks apply to what the writer actually serializes. A permitted array encoding can +contain child arrays with other encodings, so the writer must check those children too. + +| Kind | Check | +|---|---| +| Arrays | Check the serializer's returned ID, then serialize and check its children recursively. | +| Layouts | Check every serialized layout ID. The layout strategy must use permitted layouts. | +| Extension dtypes | Check all extension dtypes in the schema, including nested ones, before writing bytes. | +| Aggregate functions | Check every function stored in a zone map against the edition and its format contract. | + +A zone-map aggregate that the edition forbids causes the write to fail. Silently omitting it +changes which filters can use the configured zone map to skip rows. This differs from an aggregate +that does not apply to a column's data type: the writer omits that aggregate, so there is no +serialized component to check. + +For example, `core2026.08.0` declares `min`, `max`, `bounded_min`, `bounded_max`, `nan_count`, and +`null_count`. It does not declare `sum` because zone maps do not store sums. File-level statistics +store sums in a fixed legacy field governed by the enclosing format's contract. + +## Choosing a reader version + +A frozen edition records the minimum version of the code needed to read all its components. +For example, version `0.85.0` of the Vortex crates implements decoding for every format in +`core2026.08.3`. It also retains decoding for the formats in `core2026.08.0`, whose recorded minimum +is `0.84.0`. The application reading the file must register those implementations in its session. + +The edition family names an _origin_, the project that supplies its component implementations. +For `core`, the origin is `vortex`, so the edition's `min_library_version` refers to the shared +[Vortex Rust crate version](../versioning.md#the-vortex-rust-library). An independent plugin can +name a different origin with its own version numbers. + +**Each origin has its own minimum version.** When selected editions share an origin, use a version +of that project's code at or above the highest recorded minimum. When the origins differ, check +each project separately. In particular, check an independent plugin's version even if the Vortex +crates already meet the `core` requirement. The reader must also register the required component +implementations, including any optional plugins. + +The recorded minimum covers every format the edition permits, including ones that a particular +file does not use. An earlier crate version can therefore sometimes read that file, even though +it cannot read every file permitted by the edition. + +## Unknown-component errors + +An unknown-ID error means that the reader has no registered implementation for a component in the +file. Find its kind and ID in the [registry](editions.md#edition-registry): + +1. For a frozen edition, use at least the recorded minimum version of its origin and register any + required optional module. +2. For a draft edition, use a build that implements the component. The draft does not guarantee + support in a published crate version. Ask the producer which build to use. +3. For a component absent from the registry, obtain its implementation from the producer and + register it with the session. + +Inspection and copying tools can use `allow_unknown` to preserve unknown arrays, layouts, and +extension dtypes. The reader retains their serialized data without interpreting it, so these +objects are not available for computation. An unknown aggregate disables the affected zone-map +pruning. Data reads remain correct, but they cannot use that aggregate to skip rows. + +[Next: Arrays and compression](arrays-and-compression.md) diff --git a/vortex-edition/src/lib.rs b/vortex-edition/src/lib.rs index ba2382ec02b..ede2646bb17 100644 --- a/vortex-edition/src/lib.rs +++ b/vortex-edition/src/lib.rs @@ -29,7 +29,7 @@ //! //! The first-party edition declarations live in this crate. The public `vortex` crate //! re-exports them and registers and enables them on the default session. See the published spec at -//! . +//! . pub mod declarations; mod session; From 4704bf50345c3d84cde9af7a702bfb3c8b5afd5f Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Sun, 20 Sep 2026 10:36:02 -0400 Subject: [PATCH 02/12] docs: move versioning proof out of documentation PR Signed-off-by: "Connor Tsui" --- docs/_static/versioning-proof.pdf | Bin 187481 -> 0 bytes docs/specs/versioning-proof.typ | 592 ------------------------------ docs/specs/versioning.md | 4 +- 3 files changed, 1 insertion(+), 595 deletions(-) delete mode 100644 docs/_static/versioning-proof.pdf delete mode 100644 docs/specs/versioning-proof.typ diff --git a/docs/_static/versioning-proof.pdf b/docs/_static/versioning-proof.pdf deleted file mode 100644 index 2f1741210978d625ed3579fc2f474ce64b1a85a8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 187481 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%0Dvfu3BW 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 diff --git a/docs/specs/versioning-proof.typ b/docs/specs/versioning-proof.typ deleted file mode 100644 index 4b54683772a..00000000000 --- a/docs/specs/versioning-proof.typ +++ /dev/null @@ -1,592 +0,0 @@ -// SPDX-License-Identifier: CC-BY-4.0 -// SPDX-FileCopyrightText: Copyright the Vortex contributors - -#set document(title: "Vortex versioning: model and proofs", author: "Vortex contributors") -#set page(paper: "a4", margin: (x: 23mm, y: 22mm), numbering: "1") -#set text(size: 10.5pt) -#set par(justify: false, leading: 0.65em) -#set heading(numbering: "1.1") -#set math.equation(numbering: "(1)") -#set table(inset: 6pt, stroke: 0.4pt + luma(75%)) -#show heading.where(level: 1): set block(above: 1.7em, below: 0.7em) -#show heading.where(level: 2): set block(above: 1.2em, below: 0.5em) - -#align(center)[ - #text(size: 21pt)[Vortex versioning] - #v(0.3em) - #text(size: 14pt)[Model, proof obligations, and compatibility theorems] -] -#v(1em) - -This document proves the compatibility guarantees described in the Versioning documentation. -Under the stated assumptions, every successful write constrained to a set of editions can be read -with the same meaning by any reader that supports those editions, including a reader older than -the writer. - -These are mathematical proofs about a model. They are not a machine-checked proof of the Rust -implementation. The assumptions require correct readers and writers, fixed serialized formats, -and retained support for those formats in later versions of the code. The implementation section -identifies the remaining work. In particular, configuring compression schemes for each target -edition is still planned. - -= Compatibility and writer invariants - -I1 through I7 establish compatibility for successful writes and require later versions of the -reader code to preserve it. I8 through I10 require the writer to retain supported targets, select -the oldest suitable format, and avoid recompression solely to meet an edition's constraints. - -#[ -#set enum(numbering: n => [I#n.]) - -+ *A frozen wire contract is immutable.* A typed wire ID always identifies the same valid - forms and their meanings. A reader-visible extension requires a new ID. -+ *Readers implement each supported contract.* Exact-ID dispatch and local decoding accept - every valid form of that ID, preserve its meaning, and compose correctly with decoded children. -+ *Serialization preserves meaning.* Every emitted node satisfies its claimed wire contract, - and its meaning equals the meaning of the input representation. -+ *Edition enforcement covers the whole output.* A successful constrained write contains - only permitted typed IDs, including children, layouts, extension dtypes, and aggregates. -+ *Frozen edition records are immutable and membership is cumulative within a family.* Membership, - origin, and recorded minimum version remain fixed. A later edition adds permissions without - changing the earlier edition. Selecting families takes their union. -+ *Reader evolution preserves historical support.* A later conforming version of a project's code - retains support for its frozen formats, even after writers stop using them. -+ *A frozen edition's origin and minimum version are sound.* That version of the origin's code - supplies readers for every member of the edition. Required plugins must be present and registered. -+ *Supported writer targets remain writable.* A writer retains the behavior needed - to construct permitted representations for the input domain it promises to write to an edition. -+ *Serialization chooses the oldest available lossless form.* Selection depends on the - representation and the plugin's wire history. Edition validation follows selection. The - serializer does not choose a different form by consulting the edition allowlist. -+ *Future scheme configuration is consistent and closed under children.* For a target - edition, estimation, sampling, and full compression use one compatible behavior configuration. - Every produced child is subject to the same capability constraint. -] - -I1 fixes the meaning of a wire ID. I2 constrains readers, I3 constrains serializers, and I4 -constrains which IDs a write can emit. I5 fixes the meaning of an edition name. I6 preserves -support across versions of the reader code, and I7 connects that support to a published version -number. I8 concerns write availability, I9 concerns which valid form is chosen, and I10 prevents -compression from producing a form that must be recompressed solely to meet the edition. None -follows from the others. - -= Definitions and scope - -== Symbols - -#table( - columns: (auto, 1fr), - table.header([Symbol], [Meaning]), - [$K$], [Component kinds: array, layout, extension dtype, and aggregate.], - [$u = (k, i)$], [A typed wire ID: kind $k in K$ and identifier string $i$.], - [$C_u$, $phi_u$], [The valid local forms of $u$, and their semantic interpretation.], - [$t$, $U(t)$], [A finite serialized object tree, and all typed IDs occurring in it.], - [$V(t)$], [The semantic meaning of a valid serialized tree.], - [$M_L$, $mu_L$], [Configuration $L$'s in-memory representations and their semantic meanings.], - [$R_L$, $H_L$], [IDs implemented by reader $L$, and IDs its writer can emit.], - [$D_L$, $S_L$], [Recursive reading and serialization for configuration $L$.], - [$e$, $A(e)$], [An edition, and its cumulative set of permitted typed IDs.], - [$E$, $A(E)$], [A selection of editions, and the union of their permissions.], - [$o(e)$, $m(e)$], [The origin project and its minimum code version recorded for a frozen edition.], - [$W_(L,E)$], [A write with configuration $L$ that enforces selection $E$.], - [$bot$], [Failure, rejection, or absence of a result, as specified by the operation.], -) - -An ID is typed because identical strings in different kinds identify different contracts. -In particular, $("array", "vortex.flat")$ and $("layout", "vortex.flat")$ are different IDs. -No ordering is inferred from ID strings. A plugin explicitly orders its own historical forms. - -== Serialized objects and semantic meaning - -A node has the form - -$ t = (u, p, t_1, dots, t_n). $ - -Here $p$ includes all non-child data needed to interpret the node: metadata, buffers, dtype, -length, and any relevant context supplied by its container. Dependencies such as extension -dtypes and serialized aggregate functions are included in the tree even when their concrete -bytes occur elsewhere. A file has a fixed-format root whose children include all versioned -components. That root has a separately assumed stable, correctly implemented contract. - -A concrete file can share children. Unfolding its finite acyclic dependency graph into a finite -tree does not change the argument. Cyclic graphs, malformed references, corrupted bytes, resource -exhaustion, and incompatible file envelopes are outside this compatibility theorem. Finite input -and terminating local routines are explicit premises. Permission to use an ID does not establish -termination or a resource bound. - -A local contract $C_u$ is a predicate on the payload, child interfaces, and child meanings. -Interfaces include the structural facts needed by the parent, such as child count, dtype, and -length. Its interpretation $phi_u$ defines the meaning of each valid local form. Validity and -meaning are defined recursively: - -$ V(t) = phi_(u)(p, V(t_1), dots, V(t_n)). $ - -This expression is defined only if each child is valid and the parent satisfies $C_u$. -The semantic domains are sorted by component kind and logical type. For example, an array means -its typed values and nulls, an extension dtype means its logical interpretation, and an aggregate -means its function and options. A file's meaning includes the declared component behavior as well -as its logical data. This prevents silently dropping a configured aggregate from counting as -serialization of the same file specification. - -The equations omit child interfaces for readability. Local implementations must preserve them -as well as semantic values. An aggregate contract's meaning includes the conditions that make -its use for pruning sound. The proof does not establish an aggregate algorithm's soundness. - -The ID closure is - -$ U(t) = {u} union union.big_(j=1)^n U(t_j). $ - -The fixed-format root contributes no edition-governed ID. An unrecognized dependency is not -removed from $U(t)$ merely because a particular query does not use it. - -== Reader and writer configurations - -A *configuration* $L$ identifies the versions of the Vortex Rust crates and optional plugin code, -the enabled modules, and the registered implementations. A crate version alone does not specify -which components an application can read or write. - -Its memory domain $M_L$ can differ completely from another configuration's domain. The meaning of -$a in M_L$ is $mu_(L)(a)$. The reader set $R_L$ is a set of typed IDs with implementations, not a set -of registered edition declarations. The writer set $H_L$ is defined independently: a historical -ID can be retained only for reading. - -No in-memory version field is assumed. Fields, children, and metadata can distinguish the shapes -handled by one current implementation. A deserializer can construct that implementation directly -or construct another equivalent current representation. Its output need not have the same -in-memory encoding ID as the wire ID that dispatched it. - -== Editions, families, and origin versions - -An edition selection $E$ contains at most one edition per family. Its permission set is - -$ A(E) = union.big_(e in E) A(e). $ - -The union of an empty selection is empty. An empty per-kind allowlist permits no component of that -kind. Within one family, $e <= e'$ implies $A(e) subset.eq A(e')$. There is no chronology comparison -between editions from different families. - -For a frozen edition, $o(e)$ names the *origin*: the project that supplies its component -implementations. The minimum version $m(e)$ refers to that project's code. For origin `vortex`, -this is the shared version of the Vortex Rust crates. Independent plugins can have their own -origins and version numbers. Version ordering is used only within one origin. - -Draft editions have a permission set but no published minimum version or perpetual read-support -guarantee. A stable edition can freeze when its origin publishes the code that supports it. -Recording the minimum version afterward documents that freeze, rather than creating a new one. - -A reader supports a selection when - -$ A(E) subset.eq R_L. $ - -This is only a coverage statement. Correct decoding follows from the local implementation premise -below and an induction, not from this definition alone. Registering an edition declaration changes -which permissions can be selected. It does not enlarge $R_L$, $H_L$, or $M_L$. - -= Implementation and publication premises - -== Local reader premise (I2) - -For every $u in R_L$, reader $L$ has an exact-ID decoder $d_(L,u)$. Given a valid local payload -and correctly decoded child representations $a_1, dots, a_n$, this decoder terminates and returns -$a in M_L$ with the required interface and - -$ mu_(L)(a) = phi_(u)(p, mu_(L)(a_1), dots, mu_(L)(a_n)). $ - -The decoder validates the contract of the exact supplied ID. Recognizing a successor ID does not -expand what an older ID permits. In strict reading, dispatch fails when $u in.not R_L$. - -This is a local obligation for each implementation. It is stronger than recognizing a string in a -registry and weaker than assuming the whole-file compatibility result. - -== Local writer premise (I3) - -A successful local serializer for $a in M_L$ returns an ID $u in H_L$, a payload $p$, and child -representations $a_1, dots, a_n$. Their interfaces satisfy $C_u$, and - -$ mu_(L)(a) = phi_(u)(p, mu_(L)(a_1), dots, mu_(L)(a_n)). $ - -Recursive serialization follows a finite, well-founded dependency structure. Each child satisfies -the same premise. Newly constructed children are covered too. Thus local structural adaptation -cannot bypass either the semantic obligation or recursive validation. - -Compression, layout construction, or another preparation step used before serialization has its -own value-preservation obligation. A theorem about serialization of $a$ guarantees the meaning of -$a$. It guarantees the original source values only when preparation preserved them. - -== Constrained-write premise (I4) - -The constrained writer recursively checks every emitted typed ID against $A(E)$. It reports success -only if serialization and every such check succeed. A failure can occur after some bytes have been -written: this premise is about a successfully completed file, not transactional or atomic I/O. -Edition checks must use the serializer's returned wire ID rather than the in-memory encoding ID. - -== Publication premises (I1, I5, I6, I7) - -Once frozen, both $C_u$ and $phi_u$ remain fixed for each published ID. Frozen edition membership, -origin, and recorded minimum version also remain fixed. Later editions are cumulative in their -own families. - -For each frozen $e$, version $m(e)$ of the origin project's code provides conforming implementations -for every member of $A(e)$. Later conforming versions retain that support and its meanings. -The project must preserve this support when publishing code. Increasing a version number alone -does not establish compatibility. When origins are combined, their implementations must be installed -in a compatible host configuration and agree on any shared contracts. Taking the maximum of version -numbers from unrelated origins is undefined. - -= Compatibility proofs - -== Lemma 1: recursive reading preserves meaning - -Let $t$ be valid, finite, and acyclic, and let $U(t) subset.eq R_L$. Under I1 and the local reader -premise, $D_(L)(t)$ succeeds and - -$ mu_(L)(D_(L)(t)) = V(t). $ - -*Proof.* Induct on the height of $t$. A leaf has no children. Its ID belongs to $R_L$, so exact-ID -dispatch selects its decoder. The local reader premise returns the leaf's prescribed meaning. -For a non-leaf, each $U(t_j)$ is contained in $U(t)$ and hence in $R_L$. Each child has smaller -height, so the induction hypothesis supplies its correctly decoded representation and interface. -The parent's ID also belongs to $R_L$. Applying @local-reader to those children yields -@wire-meaning. Finite height and terminating local routines complete the induction. #h(1fr)#sym.square.stroked - -This proves closure across component kinds as well as nested arrays. Checking only a root array ID -does not supply the induction hypothesis for its child encodings, extension types, or dependencies. - -== Lemma 2: recursive serialization preserves meaning - -If recursive serialization of $a$ succeeds with tree $t$, the local writer premise implies that -$t$ is valid and $V(t) = mu_(L)(a)$. - -*Proof.* Induct on the finite serialization dependency structure. For a leaf, @local-writer gives -both validity and equality. For a parent, the induction hypotheses replace every child meaning -$mu_(L)(a_j)$ with $V(t_j)$. The local contract establishes parent validity, and @local-writer becomes -@wire-meaning with result $mu_(L)(a)$. #h(1fr)#sym.square.stroked - -#block(breakable: false)[ -== Theorem 1: successful edition-constrained writes are compatible - -Let $W$ be any writer configuration, $L$ any conforming reader, and $E$ a selected edition set. If - -$ W_(W,E)(a) = t != bot quad "and" quad A(E) subset.eq R_L, $ - -then - -$ D_(L)(t) != bot quad "and" quad mu_(L)(D_(L)(t)) = mu_(W)(a). $ -] - -*Proof.* Recursive enforcement gives $U(t) subset.eq A(E)$. Coverage then gives -$U(t) subset.eq R_L$. Lemma 2 establishes validity and $V(t) = mu_(W)(a)$. Lemma 1 gives -$mu_(L)(D_(L)(t)) = V(t)$. Transitivity proves the claim. #h(1fr)#sym.square.stroked - -The writer can use newer Vortex crates than the reader. No premise compares their crate versions, -memory layouts, or compression implementations. The reader needs the emitted contracts, not knowledge -of the writer. Two writers can produce different bytes for equal values while both satisfy this -theorem. Byte-for-byte reproducibility does not follow. - -The theorem is conditional on successful writing. It does not assert that every array has a -permitted form, or that every configured compressor can construct one. - -== Theorem 2: later readers preserve readability - -Suppose $t$ uses frozen IDs, and a conforming reader $L$ reads it under Lemma 1. If $L'$ is a later -conforming configuration retaining those implementations under I6, then $L'$ reads $t$ with the same -meaning, even if $M_L$ and $M_(L')$ differ. - -*Proof.* Retention gives $U(t) subset.eq R_(L')$. I1 preserves the validity and meaning of $t$. -Apply Lemma 1 to $L'$ and to $L$. Both results have meaning $V(t)$. #h(1fr)#sym.square.stroked - -A reader need not convert a historical in-memory object into a current one. It can decode the -historical bytes directly into its current representation. Historical contract support is required. -A separate legacy memory type, version flag, upgrade pass, or recompression pass is not. - -== Theorem 3: frozen edition bounds are sufficient, but conservative - -For each origin $o$ named by a frozen selection, define - -$ b_(o)(E) = max { m(e) : e in E, o(e) = o }. $ - -A compatible reader configuration containing each required origin at version $b_(o)(E)$ or later, -with its implementations registered, reads every successful write constrained to $E$. - -*Proof.* I7 supplies $A(e)$ at $m(e)$ and I6 retains it at the selected later version. Taking the -union gives $A(E) subset.eq R_L$. Apply Theorem 1. #h(1fr)#sym.square.stroked - -This is a sufficient bound computed from the recorded edition minima for that selection, not a -proof of a necessary or globally minimal version of the reader code. A file's actual requirement -is $U(t)$, and I4 gives $U(t) subset.eq A(E)$. For example, selecting an edition that permits v1 and v2 but -emitting only v1 produces a file an appropriately configured v1-only reader can read. An edition's -minimum covers all its members, including ones the file never uses. - -Under I5, replacing an edition by a later one in the same family only enlarges the permission set. -Consequently a previously valid serialized tree remains permitted. This does not prove that a -compressor makes identical choices, nor that the enlarged target retains the same minimum reader. - -== Theorem 4: unsupported and forbidden IDs fail at their boundaries - -If a candidate output $t$ contains $u in.not A(E)$, it cannot be a successful constrained write. -If strict full reading encounters $u in.not R_L$, it fails at dispatch. A writer cannot emit a valid -local form through an implementation it lacks, $u in.not H_L$. - -*Proof.* The first conclusion is the contrapositive of recursive enforcement. The second is the -exact-ID dispatch rule. The third follows from the definition of $H_L$ and the local writer -premise. These are different failures: adding permission cannot supply an implementation, and -adding an implementation cannot supply permission. #h(1fr)#sym.square.stroked - -Unknown-component passthrough does not satisfy Lemma 1's full semantic decoding result. It can -preserve inert bytes for inspection or copying. Ignoring an unknown aggregate can disable pruning -and still allow correct logical data reads. That weaker operation is outside the full-component -interpretation proved here. Disabling editions removes I4, so Theorem 1's edition conclusion no -longer follows, although a specific file can still be readable. - -= Representation changes and writer policy - -== Structural adaptation preserves compatibility - -Let a plugin adapt $a$ into another representation $b$ at the serialization boundary, with -$mu_(W)(b) = mu_(W)(a)$. If $S_(W)(b) = t$, Lemma 2 gives - -$ V(t) = mu_(W)(b) = mu_(W)(a). $ - -Thus a lossless structural downgrade preserves the compatibility theorem whenever the resulting -tree is permitted. Alternatively, the plugin can return adapted payload and child parts directly. -The local writer premise yields the same equality without constructing a legacy memory type. - -This result is about meaning. The equality does not prove that the transformation is cheap, that -it avoids decoding, or that it qualifies as structural rather than recompression. Those are -separate operational claims. A plugin must not relabel a newer payload with an older ID unless -the payload actually satisfies that older contract. - -== Oldest available lossless form - -For a current representation $a$, let $Q_(W)(a)$ be the finite set of local wire forms the plugin can -produce losslessly by its supported representation-preserving serialization operations, without -recompression. This set includes only forms the writer implements. Within the relevant wire -history, each form has an explicit chronological rank. I9 requires choosing a form with the -minimum rank in $Q_(W)(a)$, or reporting no serialization if the set is empty. - -*Proposition.* If I9 selects form $q$, no older form in $Q_(W)(a)$ was skipped. If all IDs in the -completed serialization, including those of its children, belong to $A(E)$, the constrained write -passes the edition checks. - -*Proof.* The first claim is the defining property of a minimum in a finite ordered set. The -second is recursive enforcement applied to the actual output. #h(1fr)#sym.square.stroked - -The useful implementation obligation is constructing a sound candidate set and selecting its -minimum. Merely listing supported IDs oldest-first does not establish I9. Historical IDs retained -only for reading need not belong to the writer's candidate set. This policy does not demand a -search through arbitrary recompressions to discover every mathematically possible encoding of the -same values. - -I9 orders formats within one encoding. It does not minimize the required version of the reader code. -A parent that uses an old ID can still have a child using a new ID. The complete tree determines -compatibility. If a selected form is forbidden, the edition check fails. It does not instruct the -serializer to try a newer form. Normal cumulative edition families preserve their earlier members, -but arbitrary custom permission sets need not have that property. - -== Example: decimal parts - -The historical decimal-byte-parts wire form stores one signed integer child. Its metadata field -`lower_part_count` must be zero. A current array can also hold lower-part children for wider values. -The plugin registers both historical and successor IDs and uses one current array implementation. - -Consider decimal values whose scaled integers are 12, 34, and 56. If compression constructs a -single signed child containing those integers, the plugin writes v1 and reuses the child. The -current array implementation's ability to represent wider values does not force this array to use -v2. The existing child can be serialized directly, with no upgrade, downgrade, or recompression. - -If the constructed array contains lower-part children, the current plugin selects v2. The v1 -reader's contract forbids those children even though it recognizes the metadata field's name. -A hypothetical operation that merges children into one integer buffer needs its own value and -cost analysis. The presence of small numerical values alone does not mean serialization already -performs that operation. For an old target, a future scheme must directly construct the old -single-child form when appropriate, or choose another permitted encoding. - -== Retaining old write paths (I8) - -Let $B_(W,E)$ be the source-value domain for which writer $W$ promises to write target $E$. -I8 requires a retained construction procedure that, on that domain, terminates with a -value-preserving representation whose complete serialization is permitted. This is an explicit -availability obligation beyond Theorem 1. - -A writer can satisfy I1 through I7 while deleting every old compression path and rejecting all -writes to an old target. Reader compatibility remains true for successful writes but is then -unhelpful to that writer's users. I8 rules out that regression on its stated domain. It makes no -promise for arbitrary custom arrays or arbitrary source values outside that domain. - -= Future scheme configuration and recompression - -== Configuration obligations - -Fix a target selection $E$. A scheme configuration $c$ includes the behavior that affects its -output representation. Each scheme orders its supported behaviors and selects the newest one whose -declared output capabilities are permitted. Distinct algorithms can still compete. Their ordinary -compression decisions are not ordered by wire-version age. - -Let $P(c)$ be a set of typed IDs bounding the complete serialization of any successfully produced -representation under configuration $c$, including every child and fallback. The future contract -requires: - -+ *Capability soundness:* for every successful compression result $a$, serialization terminates - successfully without further compression: $S_(W)(a) = t != bot$, with $U(t) subset.eq P(c)$. -+ *Target admissibility:* $P(c) subset.eq A(E)$. -+ *Phase consistency:* estimation, sample compression, and full compression use the same - behavior configuration $c$. An estimate concerns that behavior. It need not predict the exact - full-input compression ratio. -+ *Recursive closure:* child compressors and fallbacks use admissible configurations, and their - possible IDs are included in $P(c)$. Configuration remains fixed for the write, or an equivalent - coherent snapshot is used. -+ *Semantic preservation:* full compression preserves the input values. This does not follow - from capability declarations. - -Capability soundness is a universal obligation across the full input domain. A sample containing -only narrow decimal values cannot justify using a v1-only claim for a full-input path that can -produce lower-part children. The configured full path must handle that case with a permitted -construction or report failure. An estimate alone cannot prove this property. - -== Theorem 5: configured compression needs no recompression to satisfy editions - -Suppose the obligations above hold and full compression of source $x$ under $c$ succeeds with -representation $a$. Its serialization succeeds without recompression solely to satisfy $E$, passes -all edition checks, and is readable with the meaning of $x$ by every conforming supporting reader. - -*Proof.* Capability soundness provides the terminating serialization $t = S_(W)(a)$ without further -compression. It also gives $U(t) subset.eq P(c)$. Admissibility gives -$P(c) subset.eq A(E)$, hence $U(t) subset.eq A(E)$. All edition checks therefore pass. Semantic -preservation equates the meaning of $a$ with that of $x$. Apply Theorem 1. #h(1fr)#sym.square.stroked - -The theorem does not infer absence of recompression merely from an allowlist: it uses the -explicit construction guarantee in capability soundness. Its practical purpose is to require -schemes to establish that guarantee before compression rather than repair incompatible arrays -later. Stateful or configurable schemes are possible implementations, not a requirement to put a -version field on every array. - -Phase consistency additionally guarantees that candidates are evaluated under the behavior -actually selected for compression. Without it, full compression can still produce permitted output, -but the selection process can estimate one representation and produce another. No theorem here -establishes optimal compression or estimator accuracy. - -Array schemes cover the array dependencies they construct. A complete file also needs admissible -layout, dtype, and aggregate construction. Their permission checks remain independent boundaries. -A caller-supplied array need not have been built under $c$. It can require explicit adaptation, -recompression, or rejection. This theorem does not extend to it without the same premises. - -= Implementation locations and remaining work - -These source locations show where the model's requirements apply to the Rust implementation. -They identify the APIs involved, but do not prove that every component satisfies the requirements. - -- `vortex-array/src/array/plugin.rs` separates the in-memory plugin ID from its historical - `serialized_ids`. Serialization returns a concrete ID, metadata, buffers, and children. - Deserialization receives the exact wire ID. -- `vortex-edition/src/lib.rs` and `session.rs` define typed component membership, independently - versioned families, cumulative inclusion, origin metadata, and selection. -- `vortex-btrblocks/src/builder.rs` filters schemes by their declared serialized IDs. General - edition-derived behavior configuration, including phase consistency and recursive capability - coverage, remains an implementation obligation for Theorem 5. -- `encodings/decimal-byte-parts/src/decimal_byte_parts/plugin/` implements the structural example. - With no lower-part children, the plugin selects v1. Otherwise it selects v2. Both deserialize - through the current representation. - -Historical contract tests need to cover validation against the exact wire ID, every component kind, -and recursively created children. Writers must retain the code needed for supported older targets. -Scheme configuration needs sound output declarations, with fixtures covering files produced by -new writers for old readers. These checks supply implementation evidence. The model separately -requires preservation of values, compatibility of successful writes, and the ability to write -every input in the promised domain. - -The intended API direction is recorded in -#link("https://github.com/vortex-data/vortex/pull/9779")[Vortex PR #9779]. Updating the Vortex crates does not by -itself require an upgrade or downgrade of an array, and edition enforcement does not choose the -wire form. - -#pagebreak(weak: true) - -= Complete two-format matrix - -This matrix applies the planned scheme configuration to one encoding in an otherwise compatible -file. It covers eight writer, target, and output combinations, with two reader outcomes each. -The wire-format column describes a possible output, not a separate scheme setting. A supported -write means that suitable valid inputs and output shapes exist. It does not promise success for -every input. L1 reading a v1-only E2 file does not establish that L1 supports all of E2. - -#table( - columns: (auto, 1fr), - table.header([Symbol], [Meaning]), - [v1], [The encoding's original serialized format.], - [v2], [A successor serialized format with a distinct wire ID.], - [L1], [Older Vortex crate version that reads and writes v1 only.], - [L2], [Newer Vortex crate version that reads and writes v1 and v2 through one current array - implementation.], - [E1], [Edition permitting v1.], - [E2], [Edition permitting v1 and v2.], - [✓], [Supported for a suitable input and correctly registered implementations.], - [X], [Unsupported or forbidden.], - [N/A], [No successful write, so no reader outcome.], -) - -#text(size: 9pt)[ -#table( - columns: (auto, auto, auto, 1fr, auto, auto), - table.header([Writer], [Target], [Wire form], [Write¹], [L1 reads], [L2 reads]), - [L1], [E1], [v1], [✓], [✓], [✓²], - [L1], [E1], [v2], [X: unsupported and forbidden], [N/A], [N/A], - [L1], [E2³], [v1], [✓], [✓], [✓²], - [L1], [E2³], [v2], [X: unsupported], [N/A], [N/A], - [L2], [E1], [v1], [✓⁴⁵], [✓], [✓²], - [L2], [E1], [v2], [X: forbidden], [N/A], [N/A], - [L2], [E2], [v1], [✓⁴⁵], [✓], [✓²], - [L2], [E2], [v2], [✓⁴], [X: unknown ID], [✓], -) -] - -¹ Success is conditional on representability and successful preparation. Theorem 1 does not make -all inputs writable. A permitted parent is insufficient if any child is forbidden. - -² L2 decodes the historical contract directly into its current implementation. It need not first -construct and upgrade a legacy array. - -³ L1 must know the E2 declaration to select it. Registration grants permission, not a v2 -implementation. A v1-only file can be read by L1 even though E2 also permits v2. - -⁴ Future configurable schemes establish Theorem 5's obligations. Current BtrBlocks filtering uses -schemes' declared serialized output IDs. That filtering is correct for its current declarations. -A scheme that can produce several formats still needs configuration to select compatible behavior. - -⁵ A newer implementation can construct the original single-child decimal form and emit v1 directly. -For E1, a compatible construction must be selected before compression. For E2, the same old form can -arise naturally, and I9 still selects v1. - -#pagebreak(weak: true) - -= Counterexamples when an invariant is omitted - -Each example shows a failure that the omitted invariant prevents. These are hypothetical failures, -not claims about current Vortex bugs. - -#table( - columns: (auto, 1fr), - table.header([Omitted invariant], [What can change]), - [I1: immutable contract], [An ID starts interpreting a buffer as unsigned instead of signed. - Old and new readers accept the same bytes but disagree on values.], - [I2: correct local reader], [A registered decoder reverses the child order. All ID checks pass, - but composition changes the values.], - [I3: correct serializer], [A serializer truncates a wide value while claiming a valid old ID. - Every reader consistently returns the wrong value.], - [I4: recursive enforcement], [A permitted dictionary parent contains a forbidden compressed - child. The target reader can dispatch the parent but cannot read the child.], - [I5: fixed edition membership], [An already published edition gains v2. Its name no longer - denotes the permission set users previously selected, even if its minimum version is also - revised to preserve coverage. Alternatively, changing only a minimum version changes the - published deployment promise without changing any file bytes.], - [I6: retained readers], [The origin supplied v1 at its recorded minimum version but a later version - removes it. Updating the reader's code breaks historical files.], - [I7: sound minimum version], [The declared minimum predates the first v2 implementation. - Membership is fixed and readers are additive, yet the promised minimum cannot read all output.], - [I8: retained writer paths], [The writer keeps historical decoders but deletes old construction - behavior. Old files remain readable, while supported old-target writes become unavailable.], - [I9: oldest-form choice], [An array fitting v1 is always emitted as v2. The write can remain safe - for E2, but an otherwise unnecessary newer-reader requirement is introduced.], - [I10: coherent schemes], [Sampling uses the old form, while full compression creates a forbidden - successor or child. Final validation remains safe by rejecting the file, but successful writing - now requires another compression pass or a different construction.], -) diff --git a/docs/specs/versioning.md b/docs/specs/versioning.md index 26986a773f2..cef1907c9bd 100644 --- a/docs/specs/versioning.md +++ b/docs/specs/versioning.md @@ -2,7 +2,7 @@ These docs explain how Vortex keeps files readable as its Rust implementation changes. They cover how to write files for older deployments and how to add encoding formats without breaking existing -files.[^proof] +files. ## The Vortex Rust library @@ -81,5 +81,3 @@ versioning/editions The [implementation roadmap](versioning/arrays-and-compression.md#implementation-roadmap) covers the remaining work, including configuring compression to produce formats permitted by the target edition. - -[^proof]: For a mathematical treatment, see the [formal proof of the versioning model (PDF)](../_static/versioning-proof.pdf). From 9b627a8f38b0500b36921912a41948e5535dc981 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Sun, 20 Sep 2026 11:02:37 -0400 Subject: [PATCH 03/12] docs: organize versioning around guarantees and design Signed-off-by: "Connor Tsui" --- docs/_static/versioning-flow.svg | 88 ++++++ docs/concepts/index.md | 8 +- .../internals/serialization.md | 29 +- docs/getting-started/index.md | 4 + docs/index.md | 2 +- docs/specs/file-format.md | 27 +- docs/specs/versioning.md | 119 ++++---- .../versioning/arrays-and-compression.md | 167 ------------ docs/specs/versioning/compatibility.md | 131 --------- docs/specs/versioning/design.md | 253 ++++++++++++++++++ docs/specs/versioning/editions.md | 172 ++++++------ docs/specs/versioning/using-editions.md | 238 ++++++++-------- 12 files changed, 649 insertions(+), 589 deletions(-) create mode 100644 docs/_static/versioning-flow.svg delete mode 100644 docs/specs/versioning/arrays-and-compression.md delete mode 100644 docs/specs/versioning/compatibility.md create mode 100644 docs/specs/versioning/design.md diff --git a/docs/_static/versioning-flow.svg b/docs/_static/versioning-flow.svg new file mode 100644 index 00000000000..f552351b818 --- /dev/null +++ b/docs/_static/versioning-flow.svg @@ -0,0 +1,88 @@ + + + + Edition checks on the write path, format dispatch on the read path + + The selected edition supplies permitted formats to the default compressor's scheme filter and + to the final serialization checks. Compression produces an in-memory array. Its plugin selects + a wire representation. The writer checks the actual wire IDs, including serialized children, + and rejects forbidden output. A reader uses the stored IDs to find its registered plugins and + construct its own in-memory array, independently of the writer's library version. + + + + + + + + + + + + + Selected edition: permitted formats + + + Writer's library + + + + + + + + + + + + + + Default compressor + In-memory array + Plugin serializer + Check actual wire IDs + + + Filter schemes, then + compress values + Buffers and children + Select wire ID, + metadata, and parts + Include serialized children + Reject forbidden output + + + + + + + + Permitted serialized output + + + Reader's library + + + + + + + File + Decode by stored wire ID + Reader's in-memory array + + + Wire IDs and data + Use registered plugins + Preserve values, types, and nulls + + + + + + + diff --git a/docs/concepts/index.md b/docs/concepts/index.md index 5a2741655cf..fc6961f3bae 100644 --- a/docs/concepts/index.md +++ b/docs/concepts/index.md @@ -23,8 +23,9 @@ scanning without dictating physical layout, allowing the same logical data to use different encodings. **[Arrays](arrays.md)** are the in-memory representation. Unlike Arrow, Vortex arrays can be -*compressed*—an integer array might be bit-packed rather than stored as a flat buffer. Arrays -share the same representation on disk and over the wire, enabling zero-copy I/O. +*compressed*, so an integer array can be bit-packed rather than stored as a flat buffer. +Serialized formats can reuse array buffers for zero-copy I/O, while plugins adapt historical +formats to current array implementations. **[Compute](expressions.md)** functions operate directly on compressed arrays where possible, dispatching to encoding-specific kernels or falling back to canonical implementations. @@ -39,6 +40,9 @@ segment retrieval, FlatBuffer metadata for O(1) schema access, and support for m **[IPC Format](../specs/ipc-format.md)** provides streaming transfer of compressed arrays. +**[Versioning](../specs/versioning/design.md)** explains how library implementations, serialized +formats, and editions evolve while preserving read compatibility. + ## Integrations **Language bindings:** [Rust](https://docs.rs/vortex), [Python](../api/python/index.rst), diff --git a/docs/developer-guide/internals/serialization.md b/docs/developer-guide/internals/serialization.md index 22c73e66d9a..26d88c2edf7 100644 --- a/docs/developer-guide/internals/serialization.md +++ b/docs/developer-guide/internals/serialization.md @@ -1,9 +1,10 @@ # Serialization -Vortex uses the same binary representation for arrays in memory, on disk, and over the wire. -Metadata is stored in FlatBuffers for O(1) field access without parsing, and data buffers are -stored separately with alignment guarantees that enable zero-copy reads. Appropriate padding is -written into Vortex files to ensure that segments can be memory-mapped with correct alignment. +Vortex stores array-tree metadata in FlatBuffers and data buffers separately, with alignment that +enables zero-copy reads. A serializer can reuse an array's buffers while adapting its metadata or +children to a supported wire format. The in-memory array and its serialized representation can +evolve independently, as described in the [versioning design](../../specs/versioning/design.md). +Padding in Vortex files allows segments to be memory-mapped with the required alignment. ## Array Serialization @@ -27,9 +28,10 @@ On the wire, a serialized array is: [padding] [buffer 0] [padding] [buffer 1] ... [flatbuffer] [u32 flatbuffer length] ``` -Deserialization constructs an `ArrayParts` value that holds the FlatBuffer and buffer handles -without copying. The array is then decoded by resolving the array ID through the session's -registry and calling `build()` on the corresponding vtable. +`SerializedArray` holds the serialized tree and buffer handles. Decoding resolves the stored wire ID +through the session's plugin registry and calls the plugin's `deserialize` method with the metadata, +buffers, and children. The plugin validates that wire format and constructs an array supported by +the current implementation. ## IPC Format @@ -107,11 +109,10 @@ bindings, which `build.rs` compiles into `OUT_DIR`. The read/write traits they a ## Zero-Copy Design -The alignment and padding system is designed so that serialized buffers can be used directly -as in-memory arrays without copying. When a segment is read from disk or received over the -network, the I/O subsystem allocates an aligned buffer matching the segment's alignment -requirement. The resulting buffer handle can be used directly by the array without -reallocating or copying the data. +The alignment and padding system allows serialized buffers to be used directly in in-memory arrays +without copying. When a segment is read from disk or received over the network, the I/O subsystem +allocates an aligned buffer matching the segment's alignment requirement. The resulting buffer +handle can be used directly by the array without reallocating or copying the data. -This property holds across all three contexts: in-memory arrays, on-disk file segments, and -over-the-wire IPC messages all use the same layout and alignment conventions. +Reusing buffers does not require the reader's array tree to have the same structure as the serialized +tree. A plugin can adapt a historical format while retaining its data buffers. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index fb7ce0afbe9..3872f952246 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -27,3 +27,7 @@ Rust Quickstart C++ Quickstart Java Quickstart ``` + +For upgrades and files shared between applications, see +[Versioning and compatibility](../specs/versioning.md). It explains the read guarantee and how to +target formats supported by older deployments. diff --git a/docs/index.md b/docs/index.md index 9de33b387b0..31b571ca1a8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -72,7 +72,7 @@ internals. Build and benchmark locally. [ALP](https://github.com/spiraldb/alp) — no decompression needed for many operations. - **Extensible file format**: Zero-allocation reads, FlatBuffer metadata for O(1) column access, - and optional WASM decompression kernels for forward compatibility. + and [editions](specs/versioning.md) for writing formats supported by older readers. - **Query engine integration**: Filter and projection pushdown through the Scan API, with native integrations for DataFusion, DuckDB, Spark, Trino, and Ray. diff --git a/docs/specs/file-format.md b/docs/specs/file-format.md index fad90980ee1..9e8fcdec1ec 100644 --- a/docs/specs/file-format.md +++ b/docs/specs/file-format.md @@ -107,23 +107,18 @@ as of June 2025, it might look as follows. ## Backward Compatibility -Backward compatibility guarantees that any **older** Vortex file can be read by **newer** versions of the Vortex library, -and is expected from all releases of Vortex from version 0.36.0 onwards. +Later Vortex library versions retain read support for frozen formats, beginning with the formats in +`core2025.05.0`, supported from version `0.36.0`. The reader must retain the required component +implementations, including optional plugins. See [Versioning](versioning.md) for the guarantee and +its boundaries. ## Forward Compatibility -:::{warning} -Forward compatibility is not yet implemented, but is planned to ship prior to the 1.0 release. -::: - -Forward compatibility extends the preceding stability guarantee such that **newer** Vortex files can be read by -**older** versions of the Vortex library. - -The intent of this work is to allow us to continue to evolve the Vortex File Format, avoiding calcification -and remaining up-to-date with new compression codecs and layout optimizations -- without breaking existing -readers or requiring lockstep upgrades. +Newer writers can already produce files for older readers by +[selecting editions](versioning/using-editions.md) whose formats those readers support. That allows +applications to upgrade independently while continuing to exchange files in supported formats. -The plan is that at write-time, a minimum supported reader version is declared. Any encodings or layouts added after that minimum -reader version can then be embedded into the file with WebAssembly decompression logic. Old readers are able to decompress new -data (slower than native code, but still with SIMD acceleration) and read the file. New readers are able to make the best use of -these encodings with native decompression logic and additional push-down compute functions (which also provides an incentive to upgrade). +Reading a newly introduced format with an older reader is a separate capability. A proposed approach +embeds WebAssembly decoding logic for new encodings and layouts in the file. An older reader with +the required execution support could then interpret them without a native implementation. This +approach is not implemented and is not part of the edition compatibility guarantee. diff --git a/docs/specs/versioning.md b/docs/specs/versioning.md index cef1907c9bd..544d4c4a2d4 100644 --- a/docs/specs/versioning.md +++ b/docs/specs/versioning.md @@ -1,70 +1,80 @@ -# Versioning +# Versioning and compatibility -These docs explain how Vortex keeps files readable as its Rust implementation changes. They cover -how to write files for older deployments and how to add encoding formats without breaking existing -files. +Vortex retains read support for its frozen serialized formats as the library evolves. An application +can upgrade its Vortex reader without rewriting files that use those formats. Writers can also +restrict their output to formats that an older deployment supports, so the applications producing +and consuming files do not need to upgrade together. -## The Vortex Rust library +An _edition_ names a set of formats that a writer is allowed to put in a file. Once an edition is +_frozen_, that set and its reader requirements stay fixed. The first frozen edition is +`core2025.05.0`, supported from version `0.36.0` of the Vortex Rust library. Later versions retain +read support for its formats and those of subsequent frozen editions. -Vortex's Rust library provides the array types and algorithms that an application uses to compress -and process data **in memory**. It also reads and writes files. The code is split across the -`vortex` crate and supporting crates such as `vortex-array` and `vortex-file`. +This guarantee concerns file compatibility. The library's programming interfaces follow +[Rust's semantic versioning rules](https://doc.rust-lang.org/cargo/reference/semver.html), so an API +change can require application changes even when existing files remain readable. -The _library version_, such as `0.85.0`, is the version shared by these crates when they are -published. Changes to the crates follow -[Rust's semantic versioning rules for crates](https://doc.rust-lang.org/cargo/reference/semver.html). -Published versions of the `vortex` crate are on -[crates.io](https://crates.io/crates/vortex/versions), with release notes on -[GitHub](https://github.com/vortex-data/vortex/releases). +## A newer writer and an older reader -In these docs, a _reader_ is the Vortex code that an application uses to read files. A _writer_ -is the Vortex code it uses to write files. Each uses a particular crate version and the component -implementations registered by that application. One application can use both. +Consider two applications. A service writes files using Vortex `0.85.0`, while a query engine reads +them using Vortex `0.84.0`. These numbers identify the Rust library versions used by each application. -## Editions +The service selects `core2026.08.0` for writing. That edition's recorded minimum reader version is +`0.84.0`, so the query engine meets the version requirement. With the required implementations +registered, it can read valid files successfully written within that edition's restrictions. -When writing a file, an application selects an _edition_: a named set of formats it is allowed to -**serialize to disk**. To write files for an older deployment, select an edition whose formats -the Vortex code in that deployment can decode. The application writing the file still uses the -in-memory array types and algorithms provided by its own version of the Vortex crates. +The service still uses its own library's array implementations and compression algorithms. Its +edition selection limits the formats it serializes. Improvements that preserve those formats do +not require an update to the query engine. -Editions belong to _families_. The `core` family covers the default writer's formats, while optional -features can have their own families. Edition names use dates: `core2026.08.3` belongs to the `core` -family, `2026.08` gives its year and month, and `3` distinguishes editions in that family and month. -These are Vortex editions, separate from Rust language editions such as Rust 2024. +If the service instead selects `core2026.08.3`, the edition permits additional formats and records +a minimum of `0.85.0`. The older query engine is no longer guaranteed to read every file the service +can produce. It can still read a particular file if that file uses only formats it supports. -Once an edition is _frozen_, its permitted formats and reader requirements stay fixed. New formats -go into later editions. +An edition's minimum therefore answers a deployment question: which version supports *all* the +formats this writer is permitted to use? It is not necessarily the earliest version that can read +one particular file. -## Array plugins +## What the guarantee requires -An _array plugin_ implements the read and write code for an array encoding. It can read an older -serialized format into the current in-memory array type, and write that type in an older format when -the array's structure allows it. **The serialized format and the Rust array type do not have to -change together.** +**A successful write with edition checks enabled uses only permitted formats.** A reader that +supports all those formats can read the output. The reader must also understand the enclosing +[file format](file-format.md). -For example, a service can update its Vortex crates while keeping the edition it previously -selected for writing. The service uses the new crate version's array implementations, but writes -only formats permitted by that edition. Applications that could decode all those formats before -the update can still decode the output afterward, without updating their own Vortex crates. +Meeting a minimum library version is part of that requirement. The application must also register +the implementations that read the formats, including any optional plugins. An independent plugin +can have its own versions and compatibility policy. Upgrading Vortex alone does not install it. -**Encoding changes must be additive:** support for a new serialized format must preserve read -support for the old formats. The -[decimal encoding example](versioning/arrays-and-compression.md#example-decimal-children) shows -one array implementation supporting two serialized formats. +Edition selection does not guarantee that every input or custom writing strategy can produce a +permitted file. An array can need a format that the edition forbids, or a custom strategy can choose +an unsupported layout. The write fails when its serialized output violates the selection. -## Suggested reading order +Draft editions have no frozen compatibility guarantee. Custom formats written with edition checks +disabled also fall outside the edition guarantee. The [registry](versioning/editions.md) distinguishes +frozen editions from drafts and lists their formats and recorded minimum versions. -After this overview, read the pages in this order: +## Upgrading a deployment -1. [Using editions](versioning/using-editions.md): select formats for a writer and find the crate - versions and plugins its readers need. -2. [Arrays and compression](versioning/arrays-and-compression.md): follow a decimal array through - compression, serialization, and reading to see how one implementation supports multiple formats. -3. [Compatibility](versioning/compatibility.md): use the invariants and matrix to work through the - combinations of old and new readers, writers, editions, and serialized formats. -4. [Edition lifecycle and registry](versioning/editions.md): add or revise a format, or look up an - edition's components and minimum reader version. This page is also a reference to return to later. +An application upgrading its reader must retain the plugins needed by its existing files. The +updated implementations must continue to decode their frozen formats, including formats that +writers no longer choose. + +An application upgrading its writer must also consider its consumers. The default Vortex session +selects the newest frozen `core` edition, so a library upgrade can change the default output +permissions. To keep serving older readers, explicitly select an edition whose requirements those +readers meet. Change that selection when the readers can support the additional formats. + +Selecting an older edition affects writing. It does not prevent the same application from reading +newer formats that its registered implementations support. + +## Configuration, design, and reference + +- [Using editions](versioning/using-editions.md) shows how to configure a writer, check reader + requirements, and diagnose incompatible output or missing implementations. +- [How Vortex evolves its formats](versioning/design.md) explains the separation between library + releases, serialized formats, and editions through a worked example and compatibility tables. +- [Edition registry](versioning/editions.md) lists the formats and minimum versions, with instructions + for introducing formats and maintaining edition records. ```{toctree} --- @@ -73,11 +83,6 @@ hidden: true --- versioning/using-editions -versioning/arrays-and-compression -versioning/compatibility +versioning/design versioning/editions ``` - -The [implementation roadmap](versioning/arrays-and-compression.md#implementation-roadmap) covers -the remaining work, including configuring compression to produce formats permitted by the target -edition. diff --git a/docs/specs/versioning/arrays-and-compression.md b/docs/specs/versioning/arrays-and-compression.md deleted file mode 100644 index 6db775c4bae..00000000000 --- a/docs/specs/versioning/arrays-and-compression.md +++ /dev/null @@ -1,167 +0,0 @@ -# Arrays and compression - -The edition selected by a writer limits the formats it can put in a file. It does not choose the -Rust array types used to hold the data before writing. This page uses decimal arrays to explain -how the same in-memory type can support old and new serialized formats, and what that requires -from the compressor. - -A _compression scheme_ takes values and constructs an encoded array **in memory**. An _array -plugin_ supplies the code to read and write its serialized formats. **Compression and serialization -are separate operations**, so improving a compression algorithm does not necessarily require a -change to the format it writes. - -An encoded array can contain other arrays, called _children_. For example, dictionary encoding -stores a dictionary of values and an array of codes that refer to those values. Both are children -of the dictionary array, and each can have its own encoding. A reader needs to understand those -child encodings as well as the dictionary encoding. - -## Additive encoding changes - -A serialized array's _wire ID_ tells the reader how to interpret its metadata, buffers, and -children. The plugin responsible for that ID reads the stored data into an array supported by the -current Vortex crates. **The format fixes the meaning of the stored data, not its Rust array type.** -The resulting array must have the same values, data type, and nulls, even if its fields or children -are arranged differently in memory. - -Encoding changes must be _additive_: a new version of the Vortex crates must retain read support -for the encoding's earlier frozen formats when it adds a new format. Each old wire ID keeps the -same meaning. A change that an old reader cannot interpret, such as an additional child or a new -supported data type, needs a new wire ID. **The new reader must support the old format. The old -reader is not required to support the new one.** - -## Example: decimal children - -Decimal values can be represented as integers with a shared scale. For example, the values -`[1.25, 2.50, 3.75]` can be stored as `[125, 250, 375]` with a scale of two decimal places. - -The original decimal-byte-parts format stores these integers in one child array. For wider decimal -values, the current implementation can split each scaled integer across several child arrays. -The same Rust array type handles both cases: one child for the original representation, or -additional children for the wider representation. - -The original format uses `vortex.decimal_byte_parts` and requires one signed integer child. -Its `lower_part_count` metadata field must be zero. The newer `vortex.decimal_byte_parts.v2` format -also permits unsigned lower-part children after the signed most-significant child. - -| Array structure | Serialized ID | Reader that only supports the original format | -|---|---|---| -| One signed integer child, no lower-part children | `vortex.decimal_byte_parts` | Reads the array | -| A signed integer child and additional lower-part children | `vortex.decimal_byte_parts.v2` | Reports an unknown ID | - -For the example values, a child containing `[125, 250, 375]` fits the original format. The serializer -reuses that child and writes the original metadata. **There is no downgrade or recompression.** -A reader using the current Rust array type can read this file directly, with no lower-part -children and no separate upgrade step. An array with lower-part children requires the newer format -because the original format does not permit those children. - -**The array needs no format-version field.** The serializer can determine which format to write -from the children that are present. It does not inspect the values to see whether several children -can be merged into one, even if the values happen to fit in a single integer. - -The current decimal compression scheme constructs only single-child arrays and declares the -original serialized ID. Values too wide for that scheme remain in the standard, uncompressed -decimal representation, called a canonical decimal array. These outputs are compatible with the -existing core editions. The in-memory decimal-byte-parts ID matches the newer wire ID, but no -declared edition currently permits `vortex.decimal_byte_parts.v2` in a file. - -## Compression schemes - -By the time serialization starts, the array already has a particular structure. If the target -edition permits only the original decimal format, the compressor needs to produce a one-child -array or choose another permitted encoding. If compression produces a multi-child array, the -current decimal serializer chooses the newer format, and the write fails the edition check. - -Vortex's default compressor, BtrBlocks, chooses among compression schemes. Each scheme declares -the wire IDs it can produce through `produced_encodings()`. The builder excludes a scheme if any -of those IDs is forbidden by the selected editions. These declarations cover arrays the scheme -constructs directly. Schemes used to compress the children have their own declarations and go -through the same filtering. Custom compressors must also produce permitted formats, and the -writer checks their actual output during serialization. - -The existing schemes and their declarations are correct for the formats they produce today. -**The planned change is to configure a scheme for the selected editions, instead of excluding the -whole scheme when it can also produce a newer format.** For a decimal scheme, this means enabling -additional children only when the selected editions permit their format. - -The scheme must select the newest behavior it supports that produces permitted formats. That -selection must happen before estimating compression ratios or compressing a sample, so that those -estimates describe the same behavior used to compress the full input. Child compression and -fallbacks must also produce permitted formats. - -### Example: extending Pco's supported types - -Pco compresses numeric arrays. Consider a hypothetical extension that adds support for signed and -unsigned 8-bit integers (`i8` and `u8`). The frozen `vortex.pco` format does not support those types, -so the extension needs a new wire ID. - -The serializer can continue to use `vortex.pco` for the original types and select the new ID for -8-bit arrays. The reader must still reject an 8-bit payload labelled with the original ID, even -if its current implementation supports 8-bit values under the new ID. - -For an edition that excludes the new ID, the scheme must disable that Pco behavior before -compression. The compressor can then choose another permitted encoding for the 8-bit input. - -## Serializing an array - -Before the writer can check an array against the selected editions, it needs to know which format -the plugin will write. **The plugin selects the format first. The writer then checks whether the -editions permit it.** - -The plugin's serializer returns a wire ID, metadata, buffers, and children. When several formats -preserve the array's representation without recompression, it selects the oldest one it supports -writing. The selection depends on the array's structure, not the _edition allowlist_ -(the set of permitted IDs). A format retained only for reading is not a candidate for writing. - -The serialization context checks the returned ID against the selected editions, then serializes -and checks the children recursively. Layouts, extension dtypes, and aggregates have their own -checks. For example, if a dictionary array is permitted but the encoding of its values child is -not, the write fails. - -If the selected ID is forbidden, the write fails. The serializer does not retry a newer format, -even if a custom edition permits that newer format and excludes the older one. - -A _lossless structural downgrade_ adapts an array's metadata, buffers, or children to fit an older -serialized format without recompressing its values. A plugin can do this during serialization, -without constructing an old Rust array type. _Recompression_ constructs a different encoding of -the same logical values. If the array cannot fit a permitted format without recompression, the -write path must arrange that compression explicitly or fail. - -## Reading a serialized array - -Reading an old format does not require recreating the in-memory array type used by the original -writer. The reader's plugin can read it directly into the reader's current array implementation, -as it does for the one-child decimal format. - -The plugin declares the serialized IDs it can read. The reader uses the ID in the file to select -a registered plugin, then passes that ID to the plugin. The plugin must apply that ID's format -contract. **Edition selection restricts writing.** A reader can read any format for which it has -a registered implementation, regardless of the selected target editions. - -Some formats need a different arrangement of arrays when read into the current implementation. -ALP, a floating-point encoding, stores values that do not fit its main representation separately -as _patches_. The old ALP format stores those patches inside the ALP array. The current plugin -reads them into a `Patched` parent around a patch-free ALP child. This changes the array tree while -preserving the values, as part of reading the file. It needs no separate upgrade pass. - -## Implementation roadmap - -The writer already rejects serialized formats that the selected editions forbid. The remaining -work is to choose compatible compression behavior early enough to avoid compressing the same data -again just to meet an edition's constraints: - -- **Configure schemes before compression.** Listing every possible output ID excludes an entire - scheme if the target forbids any of them. A configuration can limit the scheme to compatible - outputs. The API for stateful or configurable schemes is not settled. -- **Apply the configuration to children and fallbacks.** Every array produced by those paths must - serialize to permitted IDs, including arrays created when a preferred scheme cannot handle the - input. -- **Retain writer support for older editions.** Older targets can require different layout - strategies or encoding choices. Writers need to make those choices before serialization. -- **Declare new formats in editions.** Multi-part decimal serialization exists, but its ID is not - yet in an edition and the default scheme does not construct it. New formats need the - [testing and promotion process](editions.md#format-testing-and-promotion). -- **Maintain compatibility tests.** Historical fixtures, invalid payloads under old IDs, recursive - edition checks, and configured schemes need coverage. Tests must cover both valid historical - payloads and newer payloads incorrectly labelled with an old wire ID. - -[Next: Compatibility](compatibility.md) diff --git a/docs/specs/versioning/compatibility.md b/docs/specs/versioning/compatibility.md deleted file mode 100644 index 4a548ad0e1e..00000000000 --- a/docs/specs/versioning/compatibility.md +++ /dev/null @@ -1,131 +0,0 @@ -# Compatibility - -**The application writing a file and the application reading it do not need the same Vortex crate -version.** The application writing the file selects editions to limit the serialized formats it -can use. The application reading the file needs code that can decode those formats. - -For example, application A uses Vortex crates `0.85.0` and writes a file with only `core2026.08.0` -selected. Application B uses Vortex crates `0.84.0`, that edition's recorded minimum. With the -required component implementations registered, B can read A's file even though A uses newer crates. - -Here, a _writer_ means the Vortex code that an application uses to write files, with its crate -version, registered implementations, and selected editions. A _reader_ means the Vortex code that -an application uses to read files, with its crate version and registered implementations. -**Older and newer readers or writers refer to crate versions, not editions.** - -To obtain the compatibility guarantee for each selected frozen edition, the application reading -the file must meet that edition's recorded minimum version requirement. It must register the -implementations for every permitted component, including any optional plugins. These are the -[edition's reader requirements](using-editions.md#choosing-a-reader-version). With these -requirements met, it must be able to read any valid file successfully written within the selected -editions' restrictions. It must also understand the file's outer container format. A write can -fail if the selected editions cannot represent the input. - -An application with older Vortex crates can sometimes decode a particular file even if it does -not meet the edition's minimum version. That file can contain only formats implemented by those -older crates. The minimum version guarantees decoding for every format the edition permits, -including formats that this particular file does not use. - -The first frozen edition is `core2025.05.0`. Its components were writable with version `0.36.0` of -the Vortex crates, and later crate versions must retain read support for them. Each subsequent -frozen edition adds the same requirement for its additional components. - -## Compatibility invariants - -A file can contain several serialized formats. Each has an identifier, called a _wire ID_, that -selects the code used to read it. The first five invariants establish which files must remain -readable and what readers and writers must preserve. - -1. **A frozen wire ID has one fixed contract.** Its accepted dtypes, metadata, children, buffers, - options, and interpretation cannot change. An extension that an old reader cannot understand - requires a new ID. -2. **A frozen edition has fixed membership, origin, and minimum version of that origin.** The origin - identifies the project that supplies the component implementations, as described in - [Choosing a reader version](using-editions.md#choosing-a-reader-version). Later editions in the - same family include every earlier component. An edition name must continue to identify the same - formats and reader requirements. -3. **Compression, serialization, and reading must preserve the values, dtype, and nulls.** Each - serializer must produce a valid instance of its chosen format. Each reader must interpret that - format correctly, including its children. A reader must reject an invalid payload under an old - ID even if it supports that payload under a newer ID. -4. **New versions of a component's implementation must retain read support for its frozen formats.** - This includes formats no longer used by writers. Edition selection restricts writing. It does not - restrict the registered formats that a reader can read. -5. **A write with edition checks enabled can succeed only if every serialized component is - permitted.** This includes array children, layouts, nested extension dtypes, and aggregate - functions. Custom writer strategies must obey the same checks. Checking only the root or the - compressor's declared output is not sufficient. - -The remaining invariants govern writing. Read compatibility alone does not require a writer to -keep producing old formats, or to choose an old format when a newer one also fits. - -6. **A writer must retain the ability to produce files for each target edition it supports.** This - does not require retaining every historical writer. -7. **A scheme must select a configuration before estimation, sampling, or full compression.** That - configuration must use the newest supported behavior that produces permitted formats. All three - stages must use that configuration, and child compression must obey the same permissions. - General per-writer scheme configuration is still future work. -8. **A serializer must select the oldest supported writable format that preserves the array's - representation losslessly without recompression.** It selects from the array's structure without - consulting the edition allowlist. The serialization context then checks the returned ID. - Historical formats retained only for reading do not have to remain writable. - -## Compatibility matrix - -The matrix follows one encoding as a new serialized format is added. L1 is the older version of -the Vortex crates, and L2 is the newer version. The application writing the file uses one of these -versions. The two reading columns show what happens when the application reading that file uses -L1 or L2. Both versions are assumed to decode the rest of the file, with the required -implementations registered. - -| Symbol | Definition | -|---|---| -| v1 | The encoding's original serialized format | -| v2 | A newer serialized format with a distinct wire ID | -| L1 | Older crate version: reads and writes only v1 | -| L2 | Newer crate version: reads and writes v1 and v2 through one current array implementation | -| E1 | An edition that permits v1 but not v2 | -| E2 | An edition that permits v1 and v2 | -| ✓ | The operation is supported for suitable inputs | -| X | The operation is unsupported or forbidden | -| N/A | The write cannot succeed, so there is no reader outcome | - -The matrix assumes the planned scheme configuration support.⁴ The serialized-format column shows -a possible output. It is not another setting that the writer chooses independently: the scheme -must construct an array that its crate version can serialize within the selected edition. - -| Crate version used to write | Target edition | Serialized format | Write result¹ | Read with L1 | Read with L2 | -|---|---|---|---|---|---| -| L1 | E1 | v1 | ✓ | ✓ | ✓² | -| L1 | E1 | v2 | X: unsupported and forbidden | N/A | N/A | -| L1 | E2³ | v1 | ✓ | ✓ | ✓² | -| L1 | E2³ | v2 | X: unsupported | N/A | N/A | -| L2 | E1 | v1 | ✓⁴⁵ | ✓ | ✓² | -| L2 | E1 | v2 | X: forbidden | N/A | N/A | -| L2 | E2 | v1 | ✓⁴⁵ | ✓ | ✓² | -| L2 | E2 | v2 | ✓⁴ | X: unknown ID | ✓ | - -Each of the eight rows has two reader outcomes, covering all 16 combinations. Reader outcomes -apply only after a successful write. In particular, L1 can read an E2 file containing only v1, -but it cannot read an E2 file that uses v2. - -¹ An input array can require a format that the target forbids. The plugin can provide a lossless -structural downgrade. If the array requires recompression, the write path must arrange it -explicitly or fail. - -² L2 reads v1 into its current implementation. The plugin adapts the structure only if necessary. -Using a newer version of the Vortex crates does not itself require an array upgrade or conversion. - -³ L1 needs the E2 declaration to select it. Registering the declaration does not add v2 support, -so L1 still writes only v1. - -⁴ Current schemes declare the serialized IDs that they produce, and the builder filters them by -those IDs. General per-writer configuration is not yet implemented. The matrix assumes that the -scheme selects compatible behavior before estimation, sampling, and full compression. Its output -then needs no recompression solely to meet the edition. - -⁵ L2 can construct a decimal array with one integer child and serialize it as v1 without -recompression. Additional lower-part children require v2. See the -[decimal example](arrays-and-compression.md#example-decimal-children). - -[Next: Edition lifecycle and registry](editions.md) diff --git a/docs/specs/versioning/design.md b/docs/specs/versioning/design.md new file mode 100644 index 00000000000..bb725327daa --- /dev/null +++ b/docs/specs/versioning/design.md @@ -0,0 +1,253 @@ +# How Vortex evolves its formats + +A file can outlive the application that wrote it. Its readers can also belong to different services, +with different upgrade schedules. Meanwhile, the library writing those files needs to improve its +compression algorithms and in-memory data structures. Tying every such change to a new file format +would force readers to upgrade even when the stored data could remain the same. + +Vortex separates the implementation used by an application from the serialized formats it reads and +writes. An application selects an _edition_ to limit its writer's output to a known set of formats. +The [versioning overview](../versioning.md) describes the deployment guarantee. This page explains +how those pieces fit together and what their implementations must preserve. + +## What changes independently + +A library release supplies code: array implementations, compression algorithms, readers, and writers. +A serialized format specifies how to interpret stored metadata and buffers. Its _wire ID_ identifies +that contract, including the supported data types and any child arrays. An edition groups these IDs +into a set of permitted formats, giving a writer a named target for compatible output. + +For example, a newer library can improve how it compresses a dictionary's values while keeping the +same dictionary format. It can also change its internal array fields while retaining code to read +old files. Only a change to what a reader must understand requires a new serialized contract. + +The file container has a separate [version tag](../file-format.md#file-specification). It describes +the enclosing format. Component wire IDs describe the arrays and other structures within that +container, so those components can evolve independently. + +## From values to a file and back + +In Vortex, compression produces an encoded array in memory. The default compressor chooses among +_compression schemes_, each of which changes the representation while preserving values, data types, +and nulls. An array can contain buffers and other arrays, called _children_. A dictionary array, for +example, has a child for its values and another for the codes that refer to those values. Each child +can use its own encoding. + +An _array plugin_ supplies serialization and deserialization for an in-memory array representation. +Its serializer returns a wire ID, metadata, buffers, and children. The writer checks that ID and the +serialized children against the selected editions. A reader uses the IDs in the file to find the +registered plugins that interpret those formats. + +```{figure} ../../_static/versioning-flow.svg +:alt: Edition checks constrain writing. Stored wire IDs select the reader's plugins. + +The array path through the default writer and a reader. Edition selection constrains writing. +The reader uses its own library implementations to decode the formats stored in the file. +``` + +This separation lets one current plugin read several historical formats. It also lets a writer use +an older format when the current array's structure fits that format, without constructing an old +version of the Rust array type. + +## Example: decimal children + +Consider the decimal values `[1.25, 2.50, 3.75]`. They can be represented as the integers +`[125, 250, 375]` with a scale of two decimal places. The original decimal-byte-parts format stores +these integers in one signed integer child array. + +The current implementation also supports wider values split across several children: a signed +most-significant part followed by unsigned lower parts. One Rust array type handles both shapes. +The original format's contract permits only the single-child shape, so the additional children +require a new wire ID. + +| Array structure | Wire ID selected by the serializer | +|---|---| +| One signed integer child, no lower parts | `vortex.decimal_byte_parts` | +| A signed integer child with additional lower parts | `vortex.decimal_byte_parts.v2` | + +The current array type's in-memory ID matches the newer wire ID. The writer must therefore check the +serializer's returned ID to determine which format it puts in the file. + +For the example values, the serializer reuses the child containing `[125, 250, 375]` and writes the +original format's metadata. No recompression is needed. The updated reader can decode that file +directly into its current array type, with no lower-part children. + +The reader must still enforce the original contract when it sees the original ID. For that format, +the `lower_part_count` metadata field must be zero and the array must have one signed integer child. +Understanding additional children under the new ID does not make them valid under the old ID. +Otherwise, a new writer could label extended data as the original format and produce a file that +an old reader cannot interpret. + +The serializer chooses from the array's structure. It does not inspect values across several +children to determine whether they could fit in one integer child. An array with lower parts uses +the extended format even if its values happen to be small. There is no need for a format-version +field on the in-memory array to distinguish these cases. + +### Choosing a writable format + +A serializer must choose the oldest supported writable format that preserves the array's +representation without recompression. A plugin can adapt metadata, buffers, or children to fit an +older format. Formats retained only for reading are not candidates for writing. + +The serializer makes that choice before the writer checks edition permissions. If the chosen ID is +forbidden, the write fails. It does not retry a newer format because that format happens to be +permitted. In particular, a custom edition that permits only the newer decimal ID cannot write the +single-child array through this serializer, which selects the original ID. + +This policy preserves older-reader compatibility when the existing representation allows it. +Producing a different encoding of the same values is a compression decision. If an array needs that +work to fit the target edition, the write path must arrange it explicitly or fail. + +Reading can also change the array structure. For example, the old ALP floating-point format stores +exceptional values, called patches, inside the ALP array. The current plugin reads it into a +`Patched` parent around an ALP child without patches. The values stay the same even though the +reader's array tree differs from the stored tree. + +## Compatibility includes the children + +Suppose the decimal serializer selects the original wire ID, but its integer child uses an encoding +that the target edition forbids. Checking only the decimal ID would accept a file that the intended +reader cannot decode. The writer must check every child recursively. + +The same requirement extends beyond arrays. A file also describes its layout, logical types, and +stored summaries used for pruning. Editions cover each of these component kinds: + +| Kind | What its wire ID identifies | +|---|---| +| `array` | An array's serialized representation | +| `layout` | A node in the file's layout tree | +| `dtype` | An extension dtype, which defines a custom logical type | +| `aggregate` | An aggregate function stored in a zone map | + +A _zone map_ stores summaries for a group of rows, such as its minimum and maximum. Readers use those +summaries to skip groups that cannot match a filter. Their aggregate definitions need stable meaning +just as array formats do. The [component checks](editions.md#component-checks) describe the writing +rules for each kind. + +The kind and ID together identify a contract. For example, the array and layout named +`vortex.chunked` are separate components. Supporting one does not imply support for the other. + +## Editions as deployment targets + +Applications need a way to select compatible output without maintaining their own inventory of +every component. A frozen edition gives that inventory a stable name and records a library version +that supports all its members. New formats require a later edition, leaving the earlier target +available to writers serving older deployments. + +An _edition family_ groups editions for related components. Membership is cumulative within a +family: each later edition includes all earlier members. An application can opt into additional +formats by changing its target, while the writer remains free to choose an earlier format. +Permitting a newer format does not require every output to use it. + +The `core` family covers the default writer's formats. Optional features have independent families. +A writer can select one `core` edition and one `tensor` edition, for example, and use the union of +their permitted components. This avoids tying a change in an optional feature to a change in the +application's core target. The reader must satisfy both selections' requirements. + +Each family names an _origin_, the project that supplies its implementations. A frozen edition's +minimum version refers to that origin. For `core`, it is the Vortex Rust library. Independent plugins +can use their own release numbers, so there is no single version comparison that covers every +possible combination. Versions must meet the minimum for each origin, and the implementations must +be registered in the reader. + +An edition declaration supplies permissions, not implementations. Registering a later declaration +with an older library does not teach that library to read or write new formats. Conversely, a +reader with the required implementations does not need the writer's edition selection to interpret +the IDs in a file. + +## Compatibility tables + +Writing and reading answer different questions. Writing checks the format selected by the plugin +against the target's permissions. Reading checks the format actually in the file against the +reader's implementations. + +The following tables use the two decimal formats above. The current core editions permit the +original format. An illustrative later edition permits both. No declared edition currently permits +`vortex.decimal_byte_parts.v2`, so the later edition here is an example, not a selectable release. +Assume the other components of the file are permitted and supported. + +| Serializer output | Target permits original only | Target permits both | +|---|---|---| +| Original format, one signed child | Allowed | Allowed | +| Extended format, additional lower parts | Rejected | Allowed | + +These outcomes depend on the serializer's actual output. The table does not assume that compression +can construct either shape for every input. + +| Format in the file | Reader supporting original only | Reader supporting both | +|---|---|---| +| Original format | Reads | Reads | +| Extended format | Unknown ID | Reads | + +An old writer that supports only the original format cannot produce the extended format, even with +a later edition declaration. A new writer can still produce the original format. This is why the +writer's library version alone cannot determine whether an older reader can read its output. + +The recorded minimum version promises support for an edition's entire permitted set. A file that +uses only a subset can sometimes be read by an earlier version. That possibility does not establish +that the earlier version can read every file produced under the same edition selection. + +## Compatibility invariants + +The examples rely on six rules: + +1. **A frozen wire contract is immutable.** Its valid data types, metadata, buffers, children, + options, and meanings stay fixed. A reader-visible extension requires a new ID. +2. **Compression, serialization, and reading preserve meaning.** Each serializer produces a valid + instance of its chosen contract. Each reader enforces that exact contract. All three operations + preserve values, data types, nulls, and the meaning of other components. +3. **Later implementations retain historical read support.** Frozen formats remain readable even + after writers stop choosing them. Edition selection does not restrict what a reader can read. +4. **Frozen edition records are immutable.** Membership, origin, and recorded minimum stay fixed. + Membership is cumulative within each family. Selecting multiple families takes their union. +5. **Edition enforcement covers the whole output.** Every serialized component must be permitted, + including children and nested dependencies. Compressor declarations alone are insufficient. +6. **Recorded reader requirements are sound.** Each recorded origin version supplies readers for + every member of the edition. Applications must register those implementations. + +Together, these rules establish the relationship: + +```text +IDs used by the file ⊆ IDs permitted by the editions ⊆ IDs supported by the reader +``` + +The IDs here include their component kinds. With valid serialized data, correct implementations, and +support for the enclosing file format, the reader can interpret the output. Retaining those +contracts and implementations preserves that ability across later releases. + +This read guarantee is separate from a writer's ability to produce suitable output. A writer must +retain the behavior needed for the target editions it supports, but it does not need to retain every +historical writing implementation. Its oldest-format selection policy and compression choices +determine how it produces that output. + +## Introducing a format + +A new format needs testing before its implementation takes on the obligation to read it +indefinitely. A format intended for `core` starts in a dedicated edition family, then can enter +`preview` for broader opt-in use, and finally a later `core` edition for default use. Promotion +changes which editions permit the format. Its wire ID and interpretation remain the same. + +A draft edition has no recorded minimum version and no frozen guarantee. A frozen edition records +the first release of its origin that supports all its members. The +[registry maintenance instructions](editions.md#maintaining-edition-records) describe when to record +that release and how to introduce revisions. Deprecating a format can stop writers from choosing +it, but cannot remove the obligation to read existing files. + +## Current compression behavior and its limit + +The default BtrBlocks compressor filters schemes by their declared output wire IDs. A scheme is +excluded if any declared ID is forbidden. Schemes used for child compression go through the same +filtering. Final serialization checks still reject forbidden output, including output from custom +compressors. + +The current decimal scheme produces only single-child arrays and declares the original wire ID. +Values too wide for it remain in the standard uncompressed decimal representation. The multi-child +serializer exists, but the default scheme does not construct those arrays and no edition permits +their newer ID. + +The planned improvement is to configure a scheme's behavior for the selected editions, allowing it +to retain an older mode when its newer mode requires a forbidden format. That configuration needs +to apply consistently to estimation, sampling, full compression, children, and fallbacks. +General per-writer scheme configuration is not implemented, and its API is unsettled. It improves +which compatible representations the compressor can produce. The final output checks already +establish the edition restriction. diff --git a/docs/specs/versioning/editions.md b/docs/specs/versioning/editions.md index c38b3dea5c0..cd81f2a6371 100644 --- a/docs/specs/versioning/editions.md +++ b/docs/specs/versioning/editions.md @@ -1,81 +1,18 @@ -# Edition lifecycle and registry +# Edition registry -A new serialized format needs testing before Vortex commits to reading it indefinitely. This page -describes how a format becomes part of a frozen edition and lists the formats in each edition. -For writer configuration and reader version requirements, see [Using editions](using-editions.md). - -**Freezing an edition fixes its formats and reader requirements.** Later versions of the code that -implements those formats must retain read support. New formats go into later editions. - -## Format testing and promotion - -A format intended for `core` starts in a dedicated edition family. This lets applications try the -format before it becomes part of the default writer's output. - -The first edition is a _draft_: it has no recorded `min_library_version` and no frozen compatibility -guarantee. A draft format is expected to be complete. If testing reveals a defect whose correction -changes what readers must understand, the correction needs a new wire ID and a later edition. - -After that initial testing, the format can enter a new `preview` edition so that more applications -can opt in to it. A later `core` edition can then include it for default use. **The format and its -wire ID stay the same during promotion.** Only the set of editions that permit it changes. - -The current `preview` edition contains no components. Its first component will go into a new -edition. Optional plugins also have their own families, including `tensor`, `zstd`, `spatial`, and -`json`, which can add editions independently of `core`. - -## Freezing an edition - -Each edition family names an _origin_, the project that supplies its component implementations. -The `core` family uses `vortex` as its origin, so its minimum versions refer to the Vortex Rust -crates. An independent plugin can have its own origin and version numbers. - -A stable edition can freeze when its origin project publishes the code that first supports it. For a -`core` edition, this happens when the Vortex crates are published. Until that crate version is -known, the Rust declaration uses `min_library_version: None`. The version is filled in afterward, -usually while the next crate version is being developed. **Filling in the field records the -original freeze.** The compatibility guarantee applies from the published crate version, even if -the declaration is updated later. An independent plugin follows the same process with its own -project's versions. - -**Deprecating a format does not remove the requirement to read it.** A writer can stop choosing -that format and use other formats permitted by the target edition. Readers must retain support -for the deprecated format because existing files can contain it. - -## Maintaining edition records - -Default edition declarations are in `vortex-edition/src/declarations/`. Optional modules keep -their declarations with their implementation code. The exported TOML records are under -`vortex/editions/`, grouped by family. The family record names the origin. A frozen edition record -gives the minimum version of that origin's code. - -For a new component, declare its own family and draft edition. For a revision, add a later edition -to the family that owns the earlier ID. Promotion adds the tested format to new `preview` and -`core` editions. When an edition freezes, record the first version of its origin's code that -supports every component in the edition. **Do not change a frozen edition's membership or the -contracts of its formats.** - -Regenerate the records with: - -```sh -cargo run -p xtask -- generate-editions -``` - -CI's `check-editions` command rejects changes to frozen records, including renames, unfreezing, and -deletion. It also rejects a new edition that does not follow its family's chronology. Changes to -permitted formats require a later edition. - -## Edition registry +This page lists edition membership and reader versions. For configuration, see +[Using editions](using-editions.md). For the reasoning behind frozen formats and cumulative +membership, see [How Vortex evolves its formats](design.md). Each entry lists the components added by that edition. It also permits every component from earlier editions in the same family, so an entry does not repeat the complete permitted set. -### Frozen `core` editions +## Frozen `core` editions The origin of every edition below is `vortex`. Each minimum refers to the shared version of the Vortex Rust crates, including the `vortex` crate. -#### `core2025.05.0` +### `core2025.05.0` Minimum Vortex Rust crate version: `0.36.0`. @@ -87,19 +24,19 @@ Minimum Vortex Rust crate version: `0.36.0`. - `layout`: `vortex.chunked`, `vortex.dict`, `vortex.flat`, `vortex.stats`, `vortex.struct` - `dtype`: `vortex.date`, `vortex.time`, `vortex.timestamp` -#### `core2025.06.0` +### `core2025.06.0` Minimum Vortex Rust crate version: `0.40.0`. - `array`: `vortex.pco`, `vortex.sequence`, `vortex.zstd` -#### `core2025.10.0` +### `core2025.10.0` Minimum Vortex Rust crate version: `0.54.0`. - `array`: `fastlanes.rle`, `vortex.fixed_size_list`, `vortex.listview`, `vortex.masked` -#### `core2026.08.0` +### `core2026.08.0` Minimum Vortex Rust crate version: `0.84.0`. @@ -107,52 +44,125 @@ Minimum Vortex Rust crate version: `0.84.0`. - `aggregate`: `vortex.bounded_max`, `vortex.bounded_min`, `vortex.max`, `vortex.min`, `vortex.nan_count`, `vortex.null_count` -#### `core2026.08.1` +### `core2026.08.1` Minimum Vortex Rust crate version: `0.84.0`. - `array`: `vortex.onpair` -#### `core2026.08.2` +### `core2026.08.2` Minimum Vortex Rust crate version: `0.85.0`. - `array`: `vortex.map` -#### `core2026.08.3` +### `core2026.08.3` Minimum Vortex Rust crate version: `0.85.0`. - `array`: `vortex.parquet.variant`, `vortex.variant` - `dtype`: `vortex.uuid` -### Editions without a frozen guarantee +## Editions without a frozen guarantee These editions have no recorded minimum version of their origin project's code. New formats and revisions get new draft editions. Vortex-maintained draft formats are expected to remain compatible unless a defect blocks promotion into `core`. Independent plugin projects state their own policy. -#### `preview2026.08.0` +### `preview2026.08.0` This edition currently adds no components. -#### `tensor2026.04.0` +### `tensor2026.04.0` - `array`: `vortex.tensor.cosine_similarity`, `vortex.tensor.inner_product`, `vortex.tensor.l2_norm`, `vortex.tensor.l2_normalize` - `dtype`: `vortex.tensor.fixed_shape_tensor`, `vortex.tensor.vector` -#### `zstd2026.02.0` +### `zstd2026.02.0` - `array`: `vortex.zstd_buffers` -#### `spatial2026.08.0` +### `spatial2026.08.0` - `dtype`: `vortex.st.box`, `vortex.st.linestring`, `vortex.st.multilinestring`, `vortex.st.multipoint`, `vortex.st.multipolygon`, `vortex.st.point`, `vortex.st.polygon`, `vortex.st.wkb` - `aggregate`: `vortex.st.aabb` -#### `json2026.08.0` +### `json2026.08.0` - `dtype`: `vortex.json` + +## Component checks + +The writer checks the formats it actually serializes against the selected editions. The checks +cover four kinds of component: + +| Kind | Writing rule | +|---|---| +| Arrays | Check the serializer's returned wire ID and every serialized child recursively. | +| Layouts | Check every serialized layout ID. The writing strategy must use permitted layouts. | +| Extension dtypes | Check all extension dtypes in the schema, including nested ones, before writing bytes. | +| Aggregate functions | Check every function stored in a zone map against the edition and its format contract. | + +A forbidden zone-map aggregate causes the write to fail. Silently omitting it would change which +filters can use the configured zone map to skip rows. An aggregate that does not apply to a column's +data type is different: the writer omits it, so there is no serialized component to check. + +For example, `core2026.08.0` declares `min`, `max`, `bounded_min`, `bounded_max`, `nan_count`, and +`null_count`. It does not declare `sum` because zone maps do not store sums. File-level statistics +store sums in a fixed legacy field governed by the enclosing format's contract. + +## Format testing and promotion + +A format intended for `core` starts in a dedicated edition family. Its first edition is a draft, +with no recorded `min_library_version` and no frozen compatibility guarantee. The format is expected +to be complete. If testing reveals a defect whose correction changes what readers must understand, +the correction needs a new wire ID and a later edition. + +After initial testing, the format can enter a new `preview` edition for broader opt-in use. A later +`core` edition can include it for default use. Promotion preserves the format and its wire ID. Only +the editions that permit it change. The current `preview` edition is empty, so its first component +must go into a new edition. + +Optional plugins can also keep their own families, such as `tensor`, `zstd`, `spatial`, and `json`, +which add editions independently of `core`. + +## Freezing an edition + +A family names its origin, the project that supplies its implementations. A stable edition can +freeze when that project publishes the code that first supports all its members. For `core`, the +origin is `vortex` and the release is a Vortex Rust crate version. Independent plugins follow the +same process using their own versions. + +Until the release version is known, the declaration uses `min_library_version: None`. Once it is +known, the field records that original release, usually while the next release is in development. +Filling in the field documents the freeze. The guarantee applies from the recorded release, even +if the declaration is updated later. + +A frozen edition's membership, origin, and minimum version stay fixed. Deprecating a format can stop +writers from choosing it, but readers must retain support because existing files can contain it. + +## Maintaining edition records + +Default declarations live in `vortex-edition/src/declarations/`. Optional modules keep declarations +with their implementation code. The exported TOML records are under `vortex/editions/`, grouped by +family. Each family names its origin, and each frozen edition records a minimum version of that +origin's code. + +1. For a new component, declare its own family and draft edition. For a revision, add a later + edition to the family that owns the earlier ID. +2. To promote a tested format, add it to new `preview` and `core` editions without changing its ID + or contract. +3. When an edition freezes, record the first release of its origin that supports every permitted + component, including inherited members. +4. Regenerate the records: + + ```sh + cargo run -p xtask -- generate-editions + ``` + +CI's `check-editions` command rejects changes to frozen records, including renames, unfreezing, and +deletion. It also rejects a new edition that does not follow its family's chronology. Changes to +permitted formats require a later edition. diff --git a/docs/specs/versioning/using-editions.md b/docs/specs/versioning/using-editions.md index 1c3ac792d13..fe33619f49d 100644 --- a/docs/specs/versioning/using-editions.md +++ b/docs/specs/versioning/using-editions.md @@ -1,122 +1,120 @@ # Using editions -To write files for another deployment, select editions whose formats that deployment can read. -This page explains how to configure that selection and find the crate versions and plugins -required to read the output. - -## Selecting edition families - -An _edition family_ groups editions for a related set of formats. For example, `core` covers the -default writer's formats, while `tensor` and `zstd` cover optional features. Keeping these in -separate families lets a writer enable an optional feature without changing its `core` selection. - -**A writer selects at most one edition from each family.** Selecting `core2026.08.3` and -`tensor2026.04.0`, for example, permits every component in either edition. Within a family, later -editions include the components from all earlier editions, so selecting a later edition adds -formats to the permitted set. - -## Components and wire IDs - -Reading a file requires more than decoding its compressed arrays. The reader also needs to -understand how the file is laid out, how to interpret custom data types, and how to use any stored -summaries to skip irrelevant data. Editions cover each of these parts of the file. - -An edition lists _components_, such as array encodings and file layouts. Each component has a kind -and an ID stored in the file, called its _wire ID_. **The kind and ID together identify the -component.** The array and layout encodings named `vortex.chunked`, for example, are distinct -components despite sharing the same ID string. - -_Zone maps_ store summaries such as the minimum and maximum in a group of rows. A reader can use -these to skip a group when no value in it can match a filter. The _aggregate functions_ that compute -these summaries also have wire IDs for their serialized definitions. - -| Kind | What the wire ID identifies | -|---|---| -| `array` | An array's serialized representation | -| `layout` | A node in the file's layout tree | -| `dtype` | An extension dtype: a custom logical data type in the schema | -| `aggregate` | An aggregate function stored in a zone map | - -## Configuring a writer - -A _session_ holds the writer's registered implementations and edition selection. The default -session from the `vortex` crate targets `core2026.08.3`. A session constructed without those defaults -needs its editions registered and enabled before writing. - -_Registering_ an edition makes its declaration available to the session, including which components -it permits. _Enabling_ the edition selects those components for writing. **The component -implementations must be registered separately.** Enabling another edition from the same family -replaces the previous selection. - -To write files for an older deployment, select a `core` edition whose recorded minimum does not -exceed that deployment's Vortex crate version. The deployment must also register the required -component implementations. Optional modules can enable their own families alongside `core`, such -as `tensor2026.04.0` for tensor support or `zstd2026.02.0` for Zstd buffer wrapping. If the selected -editions permit no components, the writer cannot serialize any edition-governed component. - -For custom or experimental formats outside the edition declarations, the Rust writer provides -`disable_editions()`. This disables checks for arrays, layouts, extension dtypes, and aggregate -functions, while still requiring their implementations to be registered. Files written with these -checks disabled have **no edition compatibility guarantee**. - -## Checks during writing - -The final checks apply to what the writer actually serializes. A permitted array encoding can -contain child arrays with other encodings, so the writer must check those children too. - -| Kind | Check | -|---|---| -| Arrays | Check the serializer's returned ID, then serialize and check its children recursively. | -| Layouts | Check every serialized layout ID. The layout strategy must use permitted layouts. | -| Extension dtypes | Check all extension dtypes in the schema, including nested ones, before writing bytes. | -| Aggregate functions | Check every function stored in a zone map against the edition and its format contract. | - -A zone-map aggregate that the edition forbids causes the write to fail. Silently omitting it -changes which filters can use the configured zone map to skip rows. This differs from an aggregate -that does not apply to a column's data type: the writer omits that aggregate, so there is no -serialized component to check. - -For example, `core2026.08.0` declares `min`, `max`, `bounded_min`, `bounded_max`, `nan_count`, and -`null_count`. It does not declare `sum` because zone maps do not store sums. File-level statistics -store sums in a fixed legacy field governed by the enclosing format's contract. - -## Choosing a reader version - -A frozen edition records the minimum version of the code needed to read all its components. -For example, version `0.85.0` of the Vortex crates implements decoding for every format in -`core2026.08.3`. It also retains decoding for the formats in `core2026.08.0`, whose recorded minimum -is `0.84.0`. The application reading the file must register those implementations in its session. - -The edition family names an _origin_, the project that supplies its component implementations. -For `core`, the origin is `vortex`, so the edition's `min_library_version` refers to the shared -[Vortex Rust crate version](../versioning.md#the-vortex-rust-library). An independent plugin can -name a different origin with its own version numbers. - -**Each origin has its own minimum version.** When selected editions share an origin, use a version -of that project's code at or above the highest recorded minimum. When the origins differ, check -each project separately. In particular, check an independent plugin's version even if the Vortex -crates already meet the `core` requirement. The reader must also register the required component -implementations, including any optional plugins. - -The recorded minimum covers every format the edition permits, including ones that a particular -file does not use. An earlier crate version can therefore sometimes read that file, even though -it cannot read every file permitted by the edition. - -## Unknown-component errors - -An unknown-ID error means that the reader has no registered implementation for a component in the -file. Find its kind and ID in the [registry](editions.md#edition-registry): - -1. For a frozen edition, use at least the recorded minimum version of its origin and register any - required optional module. -2. For a draft edition, use a build that implements the component. The draft does not guarantee - support in a published crate version. Ask the producer which build to use. -3. For a component absent from the registry, obtain its implementation from the producer and - register it with the session. - -Inspection and copying tools can use `allow_unknown` to preserve unknown arrays, layouts, and -extension dtypes. The reader retains their serialized data without interpreting it, so these -objects are not available for computation. An unknown aggregate disables the affected zone-map -pruning. Data reads remain correct, but they cannot use that aggregate to skip rows. - -[Next: Arrays and compression](arrays-and-compression.md) +To write files for an older deployment, select editions whose formats that deployment supports. +The [versioning overview](../versioning.md) explains the guarantee. This page shows the Rust +configuration API and the requirements to check on each side of a deployment. + +## Configure a writer + +A _session_ holds the registered implementations, edition declarations, and enabled editions. The +following function creates write options targeting `core2026.08.0`, whose recorded minimum reader +version is `0.84.0`: + +```rust +use vortex::VortexSessionDefault; +use vortex::editions::CORE_2026_08_0; +use vortex::editions::EditionSessionExt; +use vortex::error::VortexResult; +use vortex::file::VortexWriteOptions; +use vortex::file::WriteOptionsSessionExt; +use vortex::session::VortexSession; + +fn writer_for_older_readers() -> VortexResult { + let session = VortexSession::default(); + session.enable_edition(CORE_2026_08_0)?; + + Ok(session.write_options()) +} +``` + +Use the returned options' `write` method to write an array stream to an output. The +[Rust quickstart](../../getting-started/rust.rst) covers the input and I/O setup. The example uses +file support from the `vortex` crate. It does not require the consuming application to select the +same edition in its reader session. + +The default session registers the standard implementations and edition declarations. It currently +enables `core2026.08.3`. Calling `enable_edition` replaces the enabled edition from the same family. +Set the selection before starting the write, which captures the permitted formats at that point. + +An edition declaration describes permitted formats. Registering it does not install the code to +read or write those formats. When constructing a session without the defaults, register the required +implementations and declarations, then enable the target editions. Enabling an unregistered edition +returns an error. A selection that permits no components cannot serialize any edition-governed +component. + +## Select optional features + +An _edition family_ groups editions for related formats. The `core` family covers the default +writer's formats. Optional features have their own families, such as `tensor` and `zstd`, so they can +add formats without changing an application's `core` selection. + +A writer selects at most one edition per family. Selecting `core2026.08.0` and `tensor2026.04.0` +permits every component in either edition. Within one family, a later edition includes all earlier +members. Across families, the selections are independent. + +Check the [registry](editions.md#edition-registry) before enabling an optional family. For example, +`tensor2026.04.0` is a draft and has no frozen minimum reader version. Adding it does not extend +`core`'s frozen guarantee to the tensor formats. Both applications need the appropriate tensor +implementations. + +An edition name such as `core2026.08.3` contains its family, year, month, and a number distinguishing +editions in that family and month. These are Vortex editions, separate from Rust language editions. + +## Choose reader versions + +For each selected frozen edition, find its recorded minimum version and its _origin_: the project +that supplies the component implementations. The `core` family's origin is `vortex`, so its +`min_library_version` refers to the shared Vortex Rust crate version. An independent plugin can name +a different origin with its own release numbers. + +For editions with the same origin, use at least the highest recorded minimum. For different +origins, check each project separately. In both cases, register the implementations in the reader. +A sufficiently recent library without a required plugin is not enough. + +For example, `core2026.08.0` records `0.84.0`, while `core2026.08.3` records `0.85.0`. A Vortex reader +using `0.85.0` with the required implementations meets either edition's requirements. The recorded +minimum covers every permitted format, including formats that an individual file does not use. +An older reader can sometimes read that file, but that is insufficient evidence that it supports the +writer's entire target edition. + +## When writing fails + +Edition checks apply to the actual serialized output, including child arrays, layouts, nested +extension dtypes, and stored aggregate functions. An array encoding can be permitted while one of +its children uses a forbidden encoding. The writer rejects that output too. + +The default writer filters compression schemes by the formats they declare. A custom strategy or +compressor is responsible for constructing permitted representations. Selecting an edition does +not automatically reconfigure a custom strategy, and final checks still apply. + +When a write fails because a format is forbidden, choose a permitted representation or strategy. +Alternatively, select a later edition after confirming that the readers meet its requirements. +[The decimal example](design.md#example-decimal-children) shows why an array's structure can require +a newer format even when its values appear suitable for an older one. + +For custom or experimental output, `VortexWriteOptions::disable_editions()` disables the array, +layout, extension-dtype, and aggregate checks. It does not register missing implementations. Files +written this way have no edition compatibility guarantee, so producers and consumers must agree on +the required implementations themselves. + +## When a reader reports an unknown ID + +An unknown-ID error means that the reader has no registered implementation for that component. +Look up its kind and ID in the [registry](editions.md#edition-registry). The kind matters because an +array and a layout can share the same ID string while describing different formats. + +- For a frozen edition, use at least the recorded minimum version of its origin and register the + required optional module. +- For a draft edition, obtain a build that implements the component from the producer. A draft does + not promise support in a published release. +- For a component absent from the registry, obtain its implementation from the producer and register + it with the session. + +Inspection and copying tools can use `allow_unknown` to retain the serialized data of unknown arrays, +layouts, and extension dtypes without interpreting it. Those objects are not available for ordinary +computation. + +With `allow_unknown`, an unknown aggregate disables pruning for the affected zone-map layout. Its +data remains readable if the reader supports the other required formats. Without `allow_unknown`, +the unknown aggregate causes an error. Retaining unknown components or disabling pruning does not +establish full support for the file's formats. From de0576eb1bb1f4e83a5e2e3191c679245bc29753 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Sun, 20 Sep 2026 11:30:05 -0400 Subject: [PATCH 04/12] docs: tighten versioning prose and restore compatibility matrix Signed-off-by: "Connor Tsui" --- docs/getting-started/index.md | 2 +- docs/specs/versioning.md | 42 ++++----- docs/specs/versioning/compatibility.md | 53 +++++++++++ docs/specs/versioning/design.md | 114 +++++++----------------- docs/specs/versioning/editions.md | 21 ++--- docs/specs/versioning/using-editions.md | 38 +++----- 6 files changed, 126 insertions(+), 144 deletions(-) create mode 100644 docs/specs/versioning/compatibility.md diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 3872f952246..27294448866 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -30,4 +30,4 @@ Java Quickstart For upgrades and files shared between applications, see [Versioning and compatibility](../specs/versioning.md). It explains the read guarantee and how to -target formats supported by older deployments. +target formats supported by older Vortex versions. diff --git a/docs/specs/versioning.md b/docs/specs/versioning.md index 544d4c4a2d4..2ad9763f09e 100644 --- a/docs/specs/versioning.md +++ b/docs/specs/versioning.md @@ -1,20 +1,19 @@ # Versioning and compatibility -Vortex retains read support for its frozen serialized formats as the library evolves. An application -can upgrade its Vortex reader without rewriting files that use those formats. Writers can also -restrict their output to formats that an older deployment supports, so the applications producing -and consuming files do not need to upgrade together. +Vortex guarantees **backward compatibility** for its frozen serialized formats: newer library +versions retain read support for files in those formats. Applications can upgrade their readers +without rewriting those files. Newer writers can also select formats that older readers support, +so the applications producing and consuming files do not need to upgrade together. An _edition_ names a set of formats that a writer is allowed to put in a file. Once an edition is _frozen_, that set and its reader requirements stay fixed. The first frozen edition is -`core2025.05.0`, supported from version `0.36.0` of the Vortex Rust library. Later versions retain -read support for its formats and those of subsequent frozen editions. +`core2025.05.0`, supported from version `0.36.0` of the Vortex Rust library. This guarantee concerns file compatibility. The library's programming interfaces follow [Rust's semantic versioning rules](https://doc.rust-lang.org/cargo/reference/semver.html), so an API change can require application changes even when existing files remain readable. -## A newer writer and an older reader +## Writing for older readers Consider two applications. A service writes files using Vortex `0.85.0`, while a query engine reads them using Vortex `0.84.0`. These numbers identify the Rust library versions used by each application. @@ -31,11 +30,10 @@ If the service instead selects `core2026.08.3`, the edition permits additional f a minimum of `0.85.0`. The older query engine is no longer guaranteed to read every file the service can produce. It can still read a particular file if that file uses only formats it supports. -An edition's minimum therefore answers a deployment question: which version supports *all* the -formats this writer is permitted to use? It is not necessarily the earliest version that can read -one particular file. +The recorded minimum covers every format in the edition, including formats that a particular file +does not use. -## What the guarantee requires +## Compatibility requirements **A successful write with edition checks enabled uses only permitted formats.** A reader that supports all those formats can read the output. The reader must also understand the enclosing @@ -53,26 +51,23 @@ Draft editions have no frozen compatibility guarantee. Custom formats written wi disabled also fall outside the edition guarantee. The [registry](versioning/editions.md) distinguishes frozen editions from drafts and lists their formats and recorded minimum versions. -## Upgrading a deployment +## Upgrades -An application upgrading its reader must retain the plugins needed by its existing files. The -updated implementations must continue to decode their frozen formats, including formats that -writers no longer choose. - -An application upgrading its writer must also consider its consumers. The default Vortex session -selects the newest frozen `core` edition, so a library upgrade can change the default output -permissions. To keep serving older readers, explicitly select an edition whose requirements those -readers meet. Change that selection when the readers can support the additional formats. +The default Vortex session selects the newest frozen `core` edition, so a library upgrade can change +the default output permissions. To keep serving older readers, explicitly select an edition whose +requirements those readers meet. Change that selection when the readers can support the additional +formats. Selecting an older edition affects writing. It does not prevent the same application from reading newer formats that its registered implementations support. -## Configuration, design, and reference +## Further reading - [Using editions](versioning/using-editions.md) shows how to configure a writer, check reader requirements, and diagnose incompatible output or missing implementations. -- [How Vortex evolves its formats](versioning/design.md) explains the separation between library - releases, serialized formats, and editions through a worked example and compatibility tables. +- [Versioning design](versioning/design.md) explains wire IDs, serialization, and compatibility rules. +- [Compatibility matrix](versioning/compatibility.md) shows the combinations of writer version, + edition, serialized format, and reader version. - [Edition registry](versioning/editions.md) lists the formats and minimum versions, with instructions for introducing formats and maintaining edition records. @@ -84,5 +79,6 @@ hidden: true versioning/using-editions versioning/design +versioning/compatibility versioning/editions ``` diff --git a/docs/specs/versioning/compatibility.md b/docs/specs/versioning/compatibility.md new file mode 100644 index 00000000000..d7eee4db825 --- /dev/null +++ b/docs/specs/versioning/compatibility.md @@ -0,0 +1,53 @@ +# Compatibility matrix + +This matrix covers backward compatibility and writing for older readers as an encoding gains a new +wire format. The versions and editions are illustrative: + +- The older library reads and writes only the original format. +- The newer library reads and writes both formats through its current array implementation. +- The original edition permits only the original format. The later edition permits both. + +Both readers are assumed to support the rest of the file with the required plugins. **Allowed** +means the writer implements the format and the edition permits it, provided the writer can construct +a suitable representation. Reader results apply only after a successful write. + +| Writer version | Target edition | Format | Write result[^representation] | Older reader | Newer reader | +|---|---|---|---|---|---| +| Older | Original | Original | Allowed | Reads | Reads[^current-array] | +| Older | Original | Extended | Unsupported and forbidden | N/A | N/A | +| Older | Later[^declaration] | Original | Allowed | Reads | Reads[^current-array] | +| Older | Later[^declaration] | Extended | Unsupported | N/A | N/A | +| Newer | Original | Original | Allowed[^compression][^decimal] | Reads | Reads[^current-array] | +| Newer | Original | Extended | Forbidden | N/A | N/A | +| Newer | Later | Original | Allowed[^compression][^decimal] | Reads | Reads[^current-array] | +| Newer | Later | Extended | Allowed[^compression] | Unknown ID | Reads | + +The format column is not a separate writer setting. The serializer selects a format from the array's +structure, and the writer checks its edition permissions. A writer targeting the later edition can +still produce the original format, which both readers can read. + +For the compatibility guarantee and minimum reader versions, see [Versioning](../versioning.md). +The [design](design.md#compatibility-invariants) explains the invariants behind these outcomes. + +[^representation]: An input array can require a format that the target forbids. The plugin can + provide a lossless structural downgrade. If the array requires recompression, the write path must + arrange it explicitly or fail. + +[^current-array]: The newer reader reads the original format into its current implementation. The + plugin adapts the structure only if necessary. Using a newer version of the Vortex crates does + not itself require an array upgrade or conversion. + +[^declaration]: The older writer needs the later edition's declaration to select it. Registering the + declaration does not add support for the extended format, so the older writer still writes only + the original format. + +[^compression]: Current schemes declare the serialized IDs that they produce, and the builder + filters them by those IDs. General per-writer scheme configuration is not implemented. The + planned configuration would select compatible behavior before estimation, sampling, and full + compression, so the output needs no recompression solely to meet the edition. These cells are + conditional on the writer constructing a permitted representation. See + [Compression](design.md#compression) for the current behavior and planned work. + +[^decimal]: The newer writer can construct a decimal array with one integer child and serialize it + in the original format without recompression. Additional lower-part children require the extended + format. See the [decimal example](design.md#example-decimal-children). diff --git a/docs/specs/versioning/design.md b/docs/specs/versioning/design.md index bb725327daa..53e12e80a72 100644 --- a/docs/specs/versioning/design.md +++ b/docs/specs/versioning/design.md @@ -1,4 +1,4 @@ -# How Vortex evolves its formats +# Versioning design A file can outlive the application that wrote it. Its readers can also belong to different services, with different upgrade schedules. Meanwhile, the library writing those files needs to improve its @@ -6,16 +6,14 @@ compression algorithms and in-memory data structures. Tying every such change to would force readers to upgrade even when the stored data could remain the same. Vortex separates the implementation used by an application from the serialized formats it reads and -writes. An application selects an _edition_ to limit its writer's output to a known set of formats. -The [versioning overview](../versioning.md) describes the deployment guarantee. This page explains -how those pieces fit together and what their implementations must preserve. +writes. The [versioning overview](../versioning.md) describes the compatibility guarantee. -## What changes independently +## Versions and formats A library release supplies code: array implementations, compression algorithms, readers, and writers. A serialized format specifies how to interpret stored metadata and buffers. Its _wire ID_ identifies that contract, including the supported data types and any child arrays. An edition groups these IDs -into a set of permitted formats, giving a writer a named target for compatible output. +into a set of permitted formats. For example, a newer library can improve how it compresses a dictionary's values while keeping the same dictionary format. It can also change its internal array fields while retaining code to read @@ -25,7 +23,7 @@ The file container has a separate [version tag](../file-format.md#file-specifica the enclosing format. Component wire IDs describe the arrays and other structures within that container, so those components can evolve independently. -## From values to a file and back +## Serialization In Vortex, compression produces an encoded array in memory. The default compressor chooses among _compression schemes_, each of which changes the representation while preserving values, data types, @@ -41,14 +39,9 @@ registered plugins that interpret those formats. ```{figure} ../../_static/versioning-flow.svg :alt: Edition checks constrain writing. Stored wire IDs select the reader's plugins. -The array path through the default writer and a reader. Edition selection constrains writing. -The reader uses its own library implementations to decode the formats stored in the file. +Serialization and reading can use different versions of the library. ``` -This separation lets one current plugin read several historical formats. It also lets a writer use -an older format when the current array's structure fits that format, without constructing an old -version of the Rust array type. - ## Example: decimal children Consider the decimal values `[1.25, 2.50, 3.75]`. They can be represented as the integers @@ -83,7 +76,7 @@ children to determine whether they could fit in one integer child. An array with the extended format even if its values happen to be small. There is no need for a format-version field on the in-memory array to distinguish these cases. -### Choosing a writable format +### Format selection A serializer must choose the oldest supported writable format that preserves the array's representation without recompression. A plugin can adapt metadata, buffers, or children to fit an @@ -103,7 +96,7 @@ exceptional values, called patches, inside the ALP array. The current plugin rea `Patched` parent around an ALP child without patches. The values stay the same even though the reader's array tree differs from the stored tree. -## Compatibility includes the children +## Children and other components Suppose the decimal serializer selects the original wire ID, but its integer child uses an encoding that the target edition forbids. Checking only the decimal ID would accept a file that the intended @@ -127,17 +120,15 @@ rules for each kind. The kind and ID together identify a contract. For example, the array and layout named `vortex.chunked` are separate components. Supporting one does not imply support for the other. -## Editions as deployment targets +## Editions Applications need a way to select compatible output without maintaining their own inventory of every component. A frozen edition gives that inventory a stable name and records a library version that supports all its members. New formats require a later edition, leaving the earlier target -available to writers serving older deployments. +available to writers targeting older versions. An _edition family_ groups editions for related components. Membership is cumulative within a -family: each later edition includes all earlier members. An application can opt into additional -formats by changing its target, while the writer remains free to choose an earlier format. -Permitting a newer format does not require every output to use it. +family: each later edition includes all earlier members. The `core` family covers the default writer's formats. Optional features have independent families. A writer can select one `core` edition and one `tensor` edition, for example, and use the union of @@ -151,53 +142,21 @@ possible combination. Versions must meet the minimum for each origin, and the im be registered in the reader. An edition declaration supplies permissions, not implementations. Registering a later declaration -with an older library does not teach that library to read or write new formats. Conversely, a -reader with the required implementations does not need the writer's edition selection to interpret -the IDs in a file. - -## Compatibility tables - -Writing and reading answer different questions. Writing checks the format selected by the plugin -against the target's permissions. Reading checks the format actually in the file against the -reader's implementations. - -The following tables use the two decimal formats above. The current core editions permit the -original format. An illustrative later edition permits both. No declared edition currently permits -`vortex.decimal_byte_parts.v2`, so the later edition here is an example, not a selectable release. -Assume the other components of the file are permitted and supported. - -| Serializer output | Target permits original only | Target permits both | -|---|---|---| -| Original format, one signed child | Allowed | Allowed | -| Extended format, additional lower parts | Rejected | Allowed | - -These outcomes depend on the serializer's actual output. The table does not assume that compression -can construct either shape for every input. +with an older library does not teach that library to read or write new formats. -| Format in the file | Reader supporting original only | Reader supporting both | -|---|---|---| -| Original format | Reads | Reads | -| Extended format | Unknown ID | Reads | - -An old writer that supports only the original format cannot produce the extended format, even with -a later edition declaration. A new writer can still produce the original format. This is why the -writer's library version alone cannot determine whether an older reader can read its output. - -The recorded minimum version promises support for an edition's entire permitted set. A file that -uses only a subset can sometimes be read by an earlier version. That possibility does not establish -that the earlier version can read every file produced under the same edition selection. +The [compatibility matrix](compatibility.md) shows the combinations of writer version, edition, +serialized format, and reader version. ## Compatibility invariants -The examples rely on six rules: - 1. **A frozen wire contract is immutable.** Its valid data types, metadata, buffers, children, options, and meanings stay fixed. A reader-visible extension requires a new ID. 2. **Compression, serialization, and reading preserve meaning.** Each serializer produces a valid instance of its chosen contract. Each reader enforces that exact contract. All three operations preserve values, data types, nulls, and the meaning of other components. -3. **Later implementations retain historical read support.** Frozen formats remain readable even - after writers stop choosing them. Edition selection does not restrict what a reader can read. +3. **Readers preserve backward compatibility.** Later implementations retain read support for frozen + formats, including those writers no longer choose. Edition selection does not restrict what a + reader can read. 4. **Frozen edition records are immutable.** Membership, origin, and recorded minimum stay fixed. Membership is cumulative within each family. Selecting multiple families takes their union. 5. **Edition enforcement covers the whole output.** Every serialized component must be permitted, @@ -212,42 +171,35 @@ IDs used by the file ⊆ IDs permitted by the editions ⊆ IDs supported by the ``` The IDs here include their component kinds. With valid serialized data, correct implementations, and -support for the enclosing file format, the reader can interpret the output. Retaining those -contracts and implementations preserves that ability across later releases. +support for the enclosing file format, the reader can interpret the output. This read guarantee is separate from a writer's ability to produce suitable output. A writer must retain the behavior needed for the target editions it supports, but it does not need to retain every -historical writing implementation. Its oldest-format selection policy and compression choices -determine how it produces that output. +historical writing implementation. -## Introducing a format +## New formats -A new format needs testing before its implementation takes on the obligation to read it -indefinitely. A format intended for `core` starts in a dedicated edition family, then can enter -`preview` for broader opt-in use, and finally a later `core` edition for default use. Promotion -changes which editions permit the format. Its wire ID and interpretation remain the same. +A new format starts in a draft edition so it can be tested before its origin commits to reading it +indefinitely. Drafts have no recorded minimum version or frozen guarantee. A format intended for +`core` can progress from its own family to `preview` for broader testing, then to `core` for default +use. Promotion preserves its wire ID and interpretation. The +[registry instructions](editions.md#format-testing-and-promotion) cover promotion, freezing, and +recording the minimum version. -A draft edition has no recorded minimum version and no frozen guarantee. A frozen edition records -the first release of its origin that supports all its members. The -[registry maintenance instructions](editions.md#maintaining-edition-records) describe when to record -that release and how to introduce revisions. Deprecating a format can stop writers from choosing -it, but cannot remove the obligation to read existing files. - -## Current compression behavior and its limit +## Compression The default BtrBlocks compressor filters schemes by their declared output wire IDs. A scheme is excluded if any declared ID is forbidden. Schemes used for child compression go through the same -filtering. Final serialization checks still reject forbidden output, including output from custom -compressors. +filtering. The current decimal scheme produces only single-child arrays and declares the original wire ID. Values too wide for it remain in the standard uncompressed decimal representation. The multi-child -serializer exists, but the default scheme does not construct those arrays and no edition permits -their newer ID. +serializer exists, but the default scheme does not construct those arrays and no declared edition +permits their newer ID. + +### Planned scheme configuration The planned improvement is to configure a scheme's behavior for the selected editions, allowing it to retain an older mode when its newer mode requires a forbidden format. That configuration needs to apply consistently to estimation, sampling, full compression, children, and fallbacks. -General per-writer scheme configuration is not implemented, and its API is unsettled. It improves -which compatible representations the compressor can produce. The final output checks already -establish the edition restriction. +General per-writer scheme configuration is not implemented, and its API is unsettled. diff --git a/docs/specs/versioning/editions.md b/docs/specs/versioning/editions.md index cd81f2a6371..08071a3edfd 100644 --- a/docs/specs/versioning/editions.md +++ b/docs/specs/versioning/editions.md @@ -1,11 +1,8 @@ # Edition registry -This page lists edition membership and reader versions. For configuration, see -[Using editions](using-editions.md). For the reasoning behind frozen formats and cumulative -membership, see [How Vortex evolves its formats](design.md). - Each entry lists the components added by that edition. It also permits every component from -earlier editions in the same family, so an entry does not repeat the complete permitted set. +earlier editions in the same family. See [Using editions](using-editions.md) for configuration and +[Versioning design](design.md) for the compatibility rules. ## Frozen `core` editions @@ -63,7 +60,7 @@ Minimum Vortex Rust crate version: `0.85.0`. - `array`: `vortex.parquet.variant`, `vortex.variant` - `dtype`: `vortex.uuid` -## Editions without a frozen guarantee +## Draft editions These editions have no recorded minimum version of their origin project's code. New formats and revisions get new draft editions. Vortex-maintained draft formats are expected to remain compatible @@ -126,15 +123,10 @@ After initial testing, the format can enter a new `preview` edition for broader the editions that permit it change. The current `preview` edition is empty, so its first component must go into a new edition. -Optional plugins can also keep their own families, such as `tensor`, `zstd`, `spatial`, and `json`, -which add editions independently of `core`. - ## Freezing an edition -A family names its origin, the project that supplies its implementations. A stable edition can -freeze when that project publishes the code that first supports all its members. For `core`, the -origin is `vortex` and the release is a Vortex Rust crate version. Independent plugins follow the -same process using their own versions. +A stable edition can freeze when its origin publishes the code that first supports all its members. +For `core`, this is a Vortex Rust crate release. Independent plugins use their own versions. Until the release version is known, the declaration uses `min_library_version: None`. Once it is known, the field records that original release, usually while the next release is in development. @@ -148,8 +140,7 @@ writers from choosing it, but readers must retain support because existing files Default declarations live in `vortex-edition/src/declarations/`. Optional modules keep declarations with their implementation code. The exported TOML records are under `vortex/editions/`, grouped by -family. Each family names its origin, and each frozen edition records a minimum version of that -origin's code. +family. 1. For a new component, declare its own family and draft edition. For a revision, add a later edition to the family that owns the earlier ID. diff --git a/docs/specs/versioning/using-editions.md b/docs/specs/versioning/using-editions.md index fe33619f49d..bb848d0d5ea 100644 --- a/docs/specs/versioning/using-editions.md +++ b/docs/specs/versioning/using-editions.md @@ -1,10 +1,9 @@ # Using editions -To write files for an older deployment, select editions whose formats that deployment supports. -The [versioning overview](../versioning.md) explains the guarantee. This page shows the Rust -configuration API and the requirements to check on each side of a deployment. +To write files for an older Vortex version, select editions whose formats that version supports. +See [Versioning](../versioning.md) for the compatibility guarantee. -## Configure a writer +## Writer configuration A _session_ holds the registered implementations, edition declarations, and enabled editions. The following function creates write options targeting `core2026.08.0`, whose recorded minimum reader @@ -29,8 +28,7 @@ fn writer_for_older_readers() -> VortexResult { Use the returned options' `write` method to write an array stream to an output. The [Rust quickstart](../../getting-started/rust.rst) covers the input and I/O setup. The example uses -file support from the `vortex` crate. It does not require the consuming application to select the -same edition in its reader session. +file support from the `vortex` crate. The default session registers the standard implementations and edition declarations. It currently enables `core2026.08.3`. Calling `enable_edition` replaces the enabled edition from the same family. @@ -42,7 +40,7 @@ implementations and declarations, then enable the target editions. Enabling an u returns an error. A selection that permits no components cannot serialize any edition-governed component. -## Select optional features +## Edition families An _edition family_ groups editions for related formats. The `core` family covers the default writer's formats. Optional features have their own families, such as `tensor` and `zstd`, so they can @@ -60,7 +58,7 @@ implementations. An edition name such as `core2026.08.3` contains its family, year, month, and a number distinguishing editions in that family and month. These are Vortex editions, separate from Rust language editions. -## Choose reader versions +## Reader versions For each selected frozen edition, find its recorded minimum version and its _origin_: the project that supplies the component implementations. The `core` family's origin is `vortex`, so its @@ -71,21 +69,14 @@ For editions with the same origin, use at least the highest recorded minimum. Fo origins, check each project separately. In both cases, register the implementations in the reader. A sufficiently recent library without a required plugin is not enough. -For example, `core2026.08.0` records `0.84.0`, while `core2026.08.3` records `0.85.0`. A Vortex reader -using `0.85.0` with the required implementations meets either edition's requirements. The recorded -minimum covers every permitted format, including formats that an individual file does not use. -An older reader can sometimes read that file, but that is insufficient evidence that it supports the -writer's entire target edition. +## Write errors -## When writing fails +The writer rejects forbidden formats in arrays, children, layouts, nested extension dtypes, and +stored aggregate functions. -Edition checks apply to the actual serialized output, including child arrays, layouts, nested -extension dtypes, and stored aggregate functions. An array encoding can be permitted while one of -its children uses a forbidden encoding. The writer rejects that output too. - -The default writer filters compression schemes by the formats they declare. A custom strategy or -compressor is responsible for constructing permitted representations. Selecting an edition does -not automatically reconfigure a custom strategy, and final checks still apply. +A custom strategy or compressor is responsible for constructing permitted representations. Selecting +an edition does not automatically reconfigure it. The default compressor's filtering is described in +[Compression](design.md#compression). When a write fails because a format is forbidden, choose a permitted representation or strategy. Alternatively, select a later edition after confirming that the readers meet its requirements. @@ -97,7 +88,7 @@ layout, extension-dtype, and aggregate checks. It does not register missing impl written this way have no edition compatibility guarantee, so producers and consumers must agree on the required implementations themselves. -## When a reader reports an unknown ID +## Unknown IDs An unknown-ID error means that the reader has no registered implementation for that component. Look up its kind and ID in the [registry](editions.md#edition-registry). The kind matters because an @@ -116,5 +107,4 @@ computation. With `allow_unknown`, an unknown aggregate disables pruning for the affected zone-map layout. Its data remains readable if the reader supports the other required formats. Without `allow_unknown`, -the unknown aggregate causes an error. Retaining unknown components or disabling pruning does not -establish full support for the file's formats. +the unknown aggregate causes an error. From 713e3a7a136a80aee79714ef2fabb66bb7be35eb Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Sun, 20 Sep 2026 11:38:42 -0400 Subject: [PATCH 05/12] docs: guide readers through versioning explanations Signed-off-by: "Connor Tsui" --- docs/specs/versioning.md | 49 ++++++++++++++----------- docs/specs/versioning/compatibility.md | 9 +++-- docs/specs/versioning/design.md | 11 ++++-- docs/specs/versioning/using-editions.md | 15 ++++---- 4 files changed, 48 insertions(+), 36 deletions(-) diff --git a/docs/specs/versioning.md b/docs/specs/versioning.md index 2ad9763f09e..f8c41fbe635 100644 --- a/docs/specs/versioning.md +++ b/docs/specs/versioning.md @@ -1,13 +1,24 @@ # Versioning and compatibility +This page is a high-level overview of Vortex's file compatibility guarantees. Read it alongside the +companion pages for the configuration details and the reasoning behind the design: + +- [Using editions](versioning/using-editions.md) explains how to configure writers for older readers + and diagnose compatibility errors. +- [Versioning design](versioning/design.md) explains how formats evolve, with a worked example and + the invariants that preserve compatibility. +- [Compatibility matrix](versioning/compatibility.md) summarizes the combinations of writer version, + edition, serialized format, and reader version. +- [Edition registry](versioning/editions.md) lists the formats and minimum versions for each edition. + Vortex guarantees **backward compatibility** for its frozen serialized formats: newer library versions retain read support for files in those formats. Applications can upgrade their readers without rewriting those files. Newer writers can also select formats that older readers support, so the applications producing and consuming files do not need to upgrade together. An _edition_ names a set of formats that a writer is allowed to put in a file. Once an edition is -_frozen_, that set and its reader requirements stay fixed. The first frozen edition is -`core2025.05.0`, supported from version `0.36.0` of the Vortex Rust library. +[_frozen_](versioning/editions.md#freezing-an-edition), that set and its reader requirements stay fixed. +The first frozen edition is `core2025.05.0`, supported from version `0.36.0` of the Vortex Rust library. This guarantee concerns file compatibility. The library's programming interfaces follow [Rust's semantic versioning rules](https://doc.rust-lang.org/cargo/reference/semver.html), so an API @@ -18,13 +29,16 @@ change can require application changes even when existing files remain readable. Consider two applications. A service writes files using Vortex `0.85.0`, while a query engine reads them using Vortex `0.84.0`. These numbers identify the Rust library versions used by each application. -The service selects `core2026.08.0` for writing. That edition's recorded minimum reader version is -`0.84.0`, so the query engine meets the version requirement. With the required implementations +The service [selects `core2026.08.0` for writing](versioning/using-editions.md#writer-configuration). +That edition's recorded minimum reader version is `0.84.0`, so the query engine meets the version +requirement. With the required implementations registered, it can read valid files successfully written within that edition's restrictions. The service still uses its own library's array implementations and compression algorithms. Its edition selection limits the formats it serializes. Improvements that preserve those formats do -not require an update to the query engine. +not require an update to the query engine. The +[decimal example](versioning/design.md#example-decimal-children) shows how a newer implementation +can still write an older format. If the service instead selects `core2026.08.3`, the edition permits additional formats and records a minimum of `0.85.0`. The older query engine is no longer guaranteed to read every file the service @@ -39,17 +53,18 @@ does not use. supports all those formats can read the output. The reader must also understand the enclosing [file format](file-format.md). -Meeting a minimum library version is part of that requirement. The application must also register -the implementations that read the formats, including any optional plugins. An independent plugin -can have its own versions and compatibility policy. Upgrading Vortex alone does not install it. +Meeting a [minimum library version](versioning/using-editions.md#reader-versions) is part of that +requirement. The application must also register the implementations that read the formats, including +any optional plugins. An independent plugin can have its own versions and compatibility policy. +Upgrading Vortex alone does not install it. Edition selection does not guarantee that every input or custom writing strategy can produce a permitted file. An array can need a format that the edition forbids, or a custom strategy can choose -an unsupported layout. The write fails when its serialized output violates the selection. +an unsupported layout. The [write fails](versioning/using-editions.md#write-errors) when its +serialized output violates the selection. -Draft editions have no frozen compatibility guarantee. Custom formats written with edition checks -disabled also fall outside the edition guarantee. The [registry](versioning/editions.md) distinguishes -frozen editions from drafts and lists their formats and recorded minimum versions. +[Draft editions](versioning/editions.md#draft-editions) have no frozen compatibility guarantee. +Custom formats written with edition checks disabled also fall outside the edition guarantee. ## Upgrades @@ -61,16 +76,6 @@ formats. Selecting an older edition affects writing. It does not prevent the same application from reading newer formats that its registered implementations support. -## Further reading - -- [Using editions](versioning/using-editions.md) shows how to configure a writer, check reader - requirements, and diagnose incompatible output or missing implementations. -- [Versioning design](versioning/design.md) explains wire IDs, serialization, and compatibility rules. -- [Compatibility matrix](versioning/compatibility.md) shows the combinations of writer version, - edition, serialized format, and reader version. -- [Edition registry](versioning/editions.md) lists the formats and minimum versions, with instructions - for introducing formats and maintaining edition records. - ```{toctree} --- maxdepth: 1 diff --git a/docs/specs/versioning/compatibility.md b/docs/specs/versioning/compatibility.md index d7eee4db825..12e73345002 100644 --- a/docs/specs/versioning/compatibility.md +++ b/docs/specs/versioning/compatibility.md @@ -20,11 +20,12 @@ a suitable representation. Reader results apply only after a successful write. | Newer | Original | Original | Allowed[^compression][^decimal] | Reads | Reads[^current-array] | | Newer | Original | Extended | Forbidden | N/A | N/A | | Newer | Later | Original | Allowed[^compression][^decimal] | Reads | Reads[^current-array] | -| Newer | Later | Extended | Allowed[^compression] | Unknown ID | Reads | +| Newer | Later | Extended | Allowed[^compression] | [Unknown ID](using-editions.md#unknown-ids) | Reads | -The format column is not a separate writer setting. The serializer selects a format from the array's -structure, and the writer checks its edition permissions. A writer targeting the later edition can -still produce the original format, which both readers can read. +The format column is not a separate writer setting. The serializer +[selects a format](design.md#format-selection) from the array's structure, and the writer checks its +edition permissions. A writer targeting the later edition can still produce the original format, +which both readers can read. For the compatibility guarantee and minimum reader versions, see [Versioning](../versioning.md). The [design](design.md#compatibility-invariants) explains the invariants behind these outcomes. diff --git a/docs/specs/versioning/design.md b/docs/specs/versioning/design.md index 53e12e80a72..8da5bf0051f 100644 --- a/docs/specs/versioning/design.md +++ b/docs/specs/versioning/design.md @@ -34,7 +34,8 @@ can use its own encoding. An _array plugin_ supplies serialization and deserialization for an in-memory array representation. Its serializer returns a wire ID, metadata, buffers, and children. The writer checks that ID and the serialized children against the selected editions. A reader uses the IDs in the file to find the -registered plugins that interpret those formats. +registered plugins that interpret those formats. A missing implementation causes an +[unknown-ID error](using-editions.md#unknown-ids). ```{figure} ../../_static/versioning-flow.svg :alt: Edition checks constrain writing. Stored wire IDs select the reader's plugins. @@ -51,7 +52,7 @@ these integers in one signed integer child array. The current implementation also supports wider values split across several children: a signed most-significant part followed by unsigned lower parts. One Rust array type handles both shapes. The original format's contract permits only the single-child shape, so the additional children -require a new wire ID. +require a new wire ID.[^decimal-availability] | Array structure | Wire ID selected by the serializer | |---|---| @@ -142,7 +143,8 @@ possible combination. Versions must meet the minimum for each origin, and the im be registered in the reader. An edition declaration supplies permissions, not implementations. Registering a later declaration -with an older library does not teach that library to read or write new formats. +with an older library does not teach that library to read or write new formats. See +[Writer configuration](using-editions.md#writer-configuration) for how to register and select editions. The [compatibility matrix](compatibility.md) shows the combinations of writer version, edition, serialized format, and reader version. @@ -203,3 +205,6 @@ The planned improvement is to configure a scheme's behavior for the selected edi to retain an older mode when its newer mode requires a forbidden format. That configuration needs to apply consistently to estimation, sampling, full compression, children, and fallbacks. General per-writer scheme configuration is not implemented, and its API is unsettled. + +[^decimal-availability]: No declared edition currently permits the multi-child format. The + [compression section](#compression) describes what the default compressor produces today. diff --git a/docs/specs/versioning/using-editions.md b/docs/specs/versioning/using-editions.md index bb848d0d5ea..cd15d9e911f 100644 --- a/docs/specs/versioning/using-editions.md +++ b/docs/specs/versioning/using-editions.md @@ -36,9 +36,10 @@ Set the selection before starting the write, which captures the permitted format An edition declaration describes permitted formats. Registering it does not install the code to read or write those formats. When constructing a session without the defaults, register the required -implementations and declarations, then enable the target editions. Enabling an unregistered edition -returns an error. A selection that permits no components cannot serialize any edition-governed -component. +implementations and declarations, then enable the target editions. See +[Registering plugins](../../developer-guide/internals/session.md#registering-plugins) for the +registration API. Enabling an unregistered edition returns an error. A selection that permits no +components cannot serialize any edition-governed component. ## Edition families @@ -60,10 +61,10 @@ editions in that family and month. These are Vortex editions, separate from Rust ## Reader versions -For each selected frozen edition, find its recorded minimum version and its _origin_: the project -that supplies the component implementations. The `core` family's origin is `vortex`, so its -`min_library_version` refers to the shared Vortex Rust crate version. An independent plugin can name -a different origin with its own release numbers. +For each selected frozen edition, find its recorded minimum version and its _origin_ in the +[registry](editions.md). The origin is the project that supplies the component implementations. +The `core` family's origin is `vortex`, so its `min_library_version` refers to the shared Vortex Rust +crate version. An independent plugin can name a different origin with its own release numbers. For editions with the same origin, use at least the highest recorded minimum. For different origins, check each project separately. In both cases, register the implementations in the reader. From 574925c4d259f287af6fa77118f713e19609ca90 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Mon, 21 Sep 2026 10:08:34 -0400 Subject: [PATCH 06/12] format and reword a few things Signed-off-by: Connor Tsui --- docs/specs/versioning.md | 10 +-- docs/specs/versioning/compatibility.md | 73 +++++++++++--------- docs/specs/versioning/design.md | 92 +++++++++++++------------ docs/specs/versioning/editions.md | 36 +++++----- docs/specs/versioning/using-editions.md | 47 ++++++------- 5 files changed, 133 insertions(+), 125 deletions(-) diff --git a/docs/specs/versioning.md b/docs/specs/versioning.md index f8c41fbe635..0fd9306835e 100644 --- a/docs/specs/versioning.md +++ b/docs/specs/versioning.md @@ -34,9 +34,9 @@ That edition's recorded minimum reader version is `0.84.0`, so the query engine requirement. With the required implementations registered, it can read valid files successfully written within that edition's restrictions. -The service still uses its own library's array implementations and compression algorithms. Its -edition selection limits the formats it serializes. Improvements that preserve those formats do -not require an update to the query engine. The +Edition selection limits the serialized formats, but the service still uses its own library's array +implementations and compression algorithms. Improvements that preserve those formats do not require +an update to the query engine. The [decimal example](versioning/design.md#example-decimal-children) shows how a newer implementation can still write an older format. @@ -73,8 +73,8 @@ the default output permissions. To keep serving older readers, explicitly select requirements those readers meet. Change that selection when the readers can support the additional formats. -Selecting an older edition affects writing. It does not prevent the same application from reading -newer formats that its registered implementations support. +Selecting an older edition restricts what the application writes, but it can still read newer formats +that its registered implementations support. ```{toctree} --- diff --git a/docs/specs/versioning/compatibility.md b/docs/specs/versioning/compatibility.md index 12e73345002..54c56a53b3b 100644 --- a/docs/specs/versioning/compatibility.md +++ b/docs/specs/versioning/compatibility.md @@ -11,44 +11,49 @@ Both readers are assumed to support the rest of the file with the required plugi means the writer implements the format and the edition permits it, provided the writer can construct a suitable representation. Reader results apply only after a successful write. -| Writer version | Target edition | Format | Write result[^representation] | Older reader | Newer reader | -|---|---|---|---|---|---| -| Older | Original | Original | Allowed | Reads | Reads[^current-array] | -| Older | Original | Extended | Unsupported and forbidden | N/A | N/A | -| Older | Later[^declaration] | Original | Allowed | Reads | Reads[^current-array] | -| Older | Later[^declaration] | Extended | Unsupported | N/A | N/A | -| Newer | Original | Original | Allowed[^compression][^decimal] | Reads | Reads[^current-array] | -| Newer | Original | Extended | Forbidden | N/A | N/A | -| Newer | Later | Original | Allowed[^compression][^decimal] | Reads | Reads[^current-array] | -| Newer | Later | Extended | Allowed[^compression] | [Unknown ID](using-editions.md#unknown-ids) | Reads | +| Writer version | Target edition | Format | Write result[^representation] | Older reader | Newer reader | +| -------------- | ------------------- | -------- | ------------------------------- | ------------------------------------------- | --------------------- | +| Older | Original | Original | Allowed | Reads | Reads[^current-array] | +| Older | Original | Extended | Unsupported and forbidden | N/A | N/A | +| Older | Later[^declaration] | Original | Allowed | Reads | Reads[^current-array] | +| Older | Later[^declaration] | Extended | Unsupported | N/A | N/A | +| Newer | Original | Original | Allowed[^compression][^decimal] | Reads | Reads[^current-array] | +| Newer | Original | Extended | Forbidden | N/A | N/A | +| Newer | Later | Original | Allowed[^compression][^decimal] | Reads | Reads[^current-array] | +| Newer | Later | Extended | Allowed[^compression] | [Unknown ID](using-editions.md#unknown-ids) | Reads | The format column is not a separate writer setting. The serializer [selects a format](design.md#format-selection) from the array's structure, and the writer checks its edition permissions. A writer targeting the later edition can still produce the original format, which both readers can read. -For the compatibility guarantee and minimum reader versions, see [Versioning](../versioning.md). -The [design](design.md#compatibility-invariants) explains the invariants behind these outcomes. - -[^representation]: An input array can require a format that the target forbids. The plugin can - provide a lossless structural downgrade. If the array requires recompression, the write path must - arrange it explicitly or fail. - -[^current-array]: The newer reader reads the original format into its current implementation. The - plugin adapts the structure only if necessary. Using a newer version of the Vortex crates does - not itself require an array upgrade or conversion. - -[^declaration]: The older writer needs the later edition's declaration to select it. Registering the - declaration does not add support for the extended format, so the older writer still writes only - the original format. - -[^compression]: Current schemes declare the serialized IDs that they produce, and the builder - filters them by those IDs. General per-writer scheme configuration is not implemented. The - planned configuration would select compatible behavior before estimation, sampling, and full - compression, so the output needs no recompression solely to meet the edition. These cells are - conditional on the writer constructing a permitted representation. See - [Compression](design.md#compression) for the current behavior and planned work. - -[^decimal]: The newer writer can construct a decimal array with one integer child and serialize it - in the original format without recompression. Additional lower-part children require the extended +For the compatibility guarantee and minimum reader versions, see [Versioning](../versioning.md). The +[design](design.md#compatibility-invariants) explains the invariants behind these outcomes. + +[^representation]: + An input array can require a format that the target forbids. The plugin can provide a lossless + structural downgrade. If the array requires recompression, the write path must arrange it + explicitly or fail. + +[^current-array]: + The newer reader reads the original format into its current implementation. The plugin adapts + the structure only if necessary. Using a newer version of the Vortex crates does not itself + require an array upgrade or conversion. + +[^declaration]: + The older writer needs the later edition's declaration to select it. Registering the declaration + does not add support for the extended format, so the older writer still writes only the original + format. + +[^compression]: + Current schemes declare the serialized IDs that they produce, and the builder filters them by + those IDs. General per-writer scheme configuration is not implemented. The planned configuration + would select compatible behavior before estimation, sampling, and full compression, so the + output needs no recompression solely to meet the edition. These cells are conditional on the + writer constructing a permitted representation. See [Compression](design.md#compression) for the + current behavior and planned work. + +[^decimal]: + The newer writer can construct a decimal array with one integer child and serialize it in the + original format without recompression. Additional lower-part children require the extended format. See the [decimal example](design.md#example-decimal-children). diff --git a/docs/specs/versioning/design.md b/docs/specs/versioning/design.md index 8da5bf0051f..1a94eea24c7 100644 --- a/docs/specs/versioning/design.md +++ b/docs/specs/versioning/design.md @@ -10,10 +10,10 @@ writes. The [versioning overview](../versioning.md) describes the compatibility ## Versions and formats -A library release supplies code: array implementations, compression algorithms, readers, and writers. -A serialized format specifies how to interpret stored metadata and buffers. Its _wire ID_ identifies -that contract, including the supported data types and any child arrays. An edition groups these IDs -into a set of permitted formats. +A library release supplies code: array implementations, compression algorithms, readers, and +writers. A serialized format specifies how to interpret stored metadata and buffers. Its _wire ID_ +identifies that contract, including the supported data types and any child arrays. An edition groups +these IDs into a set of permitted formats. For example, a newer library can improve how it compresses a dictionary's values while keeping the same dictionary format. It can also change its internal array fields while retaining code to read @@ -50,17 +50,17 @@ Consider the decimal values `[1.25, 2.50, 3.75]`. They can be represented as the these integers in one signed integer child array. The current implementation also supports wider values split across several children: a signed -most-significant part followed by unsigned lower parts. One Rust array type handles both shapes. -The original format's contract permits only the single-child shape, so the additional children -require a new wire ID.[^decimal-availability] +most-significant part followed by unsigned lower parts. One Rust array type handles both shapes, but +the original format's contract permits only the single-child shape. The additional children +therefore require a new wire ID.[^decimal-availability] -| Array structure | Wire ID selected by the serializer | -|---|---| -| One signed integer child, no lower parts | `vortex.decimal_byte_parts` | -| A signed integer child with additional lower parts | `vortex.decimal_byte_parts.v2` | +| Array structure | Wire ID selected by the serializer | +| -------------------------------------------------- | ---------------------------------- | +| One signed integer child, no lower parts | `vortex.decimal_byte_parts` | +| A signed integer child with additional lower parts | `vortex.decimal_byte_parts.v2` | -The current array type's in-memory ID matches the newer wire ID. The writer must therefore check the -serializer's returned ID to determine which format it puts in the file. +The current array type's in-memory ID matches the newer wire ID, but its serializer can return the +original ID. The writer must therefore check the serializer's returned ID. For the example values, the serializer reuses the child containing `[125, 250, 375]` and writes the original format's metadata. No recompression is needed. The updated reader can decode that file @@ -69,13 +69,13 @@ directly into its current array type, with no lower-part children. The reader must still enforce the original contract when it sees the original ID. For that format, the `lower_part_count` metadata field must be zero and the array must have one signed integer child. Understanding additional children under the new ID does not make them valid under the old ID. -Otherwise, a new writer could label extended data as the original format and produce a file that -an old reader cannot interpret. +Otherwise, a new writer could label extended data as the original format and produce a file that an +old reader cannot interpret. -The serializer chooses from the array's structure. It does not inspect values across several -children to determine whether they could fit in one integer child. An array with lower parts uses -the extended format even if its values happen to be small. There is no need for a format-version -field on the in-memory array to distinguish these cases. +The serializer chooses from the array's structure, not by inspecting whether values across several +children can fit in one integer child. An array with lower parts uses the extended format even if +its values happen to be small. There is no need for a format-version field on the in-memory array to +distinguish these cases. ### Format selection @@ -88,9 +88,9 @@ forbidden, the write fails. It does not retry a newer format because that format permitted. In particular, a custom edition that permits only the newer decimal ID cannot write the single-child array through this serializer, which selects the original ID. -This policy preserves older-reader compatibility when the existing representation allows it. -Producing a different encoding of the same values is a compression decision. If an array needs that -work to fit the target edition, the write path must arrange it explicitly or fail. +This policy preserves older-reader compatibility when the existing representation allows it. If +compatibility requires a different encoding of the same values, that is a compression decision. The +write path must arrange that work explicitly or fail. Reading can also change the array structure. For example, the old ALP floating-point format stores exceptional values, called patches, inside the ALP array. The current plugin reads it into a @@ -106,27 +106,27 @@ reader cannot decode. The writer must check every child recursively. The same requirement extends beyond arrays. A file also describes its layout, logical types, and stored summaries used for pruning. Editions cover each of these component kinds: -| Kind | What its wire ID identifies | -|---|---| -| `array` | An array's serialized representation | -| `layout` | A node in the file's layout tree | -| `dtype` | An extension dtype, which defines a custom logical type | -| `aggregate` | An aggregate function stored in a zone map | +| Kind | What its wire ID identifies | +| ----------- | ------------------------------------------------------- | +| `array` | An array's serialized representation | +| `layout` | A node in the file's layout tree | +| `dtype` | An extension dtype, which defines a custom logical type | +| `aggregate` | An aggregate function stored in a zone map | -A _zone map_ stores summaries for a group of rows, such as its minimum and maximum. Readers use those -summaries to skip groups that cannot match a filter. Their aggregate definitions need stable meaning -just as array formats do. The [component checks](editions.md#component-checks) describe the writing -rules for each kind. +A _zone map_ stores summaries for a group of rows, such as its minimum and maximum. Readers use +those summaries to skip groups that cannot match a filter. Their aggregate definitions need stable +meaning just as array formats do. The [component checks](editions.md#component-checks) describe the +writing rules for each kind. The kind and ID together identify a contract. For example, the array and layout named `vortex.chunked` are separate components. Supporting one does not imply support for the other. ## Editions -Applications need a way to select compatible output without maintaining their own inventory of -every component. A frozen edition gives that inventory a stable name and records a library version -that supports all its members. New formats require a later edition, leaving the earlier target -available to writers targeting older versions. +Applications need a way to select compatible output without maintaining their own inventory of every +component. A frozen edition gives that inventory a stable name and records a library version that +supports all its members. New formats require a later edition, leaving the earlier target available +to writers targeting older versions. An _edition family_ groups editions for related components. Membership is cumulative within a family: each later edition includes all earlier members. @@ -137,14 +137,15 @@ their permitted components. This avoids tying a change in an optional feature to application's core target. The reader must satisfy both selections' requirements. Each family names an _origin_, the project that supplies its implementations. A frozen edition's -minimum version refers to that origin. For `core`, it is the Vortex Rust library. Independent plugins -can use their own release numbers, so there is no single version comparison that covers every -possible combination. Versions must meet the minimum for each origin, and the implementations must -be registered in the reader. +minimum version refers to that origin. For `core`, it is the Vortex Rust library. Independent +plugins can use their own release numbers, so there is no single version comparison that covers +every possible combination. Versions must meet the minimum for each origin, and the implementations +must be registered in the reader. An edition declaration supplies permissions, not implementations. Registering a later declaration with an older library does not teach that library to read or write new formats. See -[Writer configuration](using-editions.md#writer-configuration) for how to register and select editions. +[Writer configuration](using-editions.md#writer-configuration) for how to register and select +editions. The [compatibility matrix](compatibility.md) shows the combinations of writer version, edition, serialized format, and reader version. @@ -202,9 +203,10 @@ permits their newer ID. ### Planned scheme configuration The planned improvement is to configure a scheme's behavior for the selected editions, allowing it -to retain an older mode when its newer mode requires a forbidden format. That configuration needs -to apply consistently to estimation, sampling, full compression, children, and fallbacks. -General per-writer scheme configuration is not implemented, and its API is unsettled. +to retain an older mode when its newer mode requires a forbidden format. That configuration needs to +apply consistently to estimation, sampling, full compression, children, and fallbacks. General +per-writer scheme configuration is not implemented, and its API is unsettled. -[^decimal-availability]: No declared edition currently permits the multi-child format. The +[^decimal-availability]: + No declared edition currently permits the multi-child format. The [compression section](#compression) describes what the default compressor produces today. diff --git a/docs/specs/versioning/editions.md b/docs/specs/versioning/editions.md index 08071a3edfd..ee3cbedd526 100644 --- a/docs/specs/versioning/editions.md +++ b/docs/specs/versioning/editions.md @@ -1,7 +1,7 @@ # Edition registry -Each entry lists the components added by that edition. It also permits every component from -earlier editions in the same family. See [Using editions](using-editions.md) for configuration and +Each entry lists the components added by that edition. It also permits every component from earlier +editions in the same family. See [Using editions](using-editions.md) for configuration and [Versioning design](design.md) for the compatibility rules. ## Frozen `core` editions @@ -72,8 +72,8 @@ This edition currently adds no components. ### `tensor2026.04.0` -- `array`: `vortex.tensor.cosine_similarity`, `vortex.tensor.inner_product`, `vortex.tensor.l2_norm`, - `vortex.tensor.l2_normalize` +- `array`: `vortex.tensor.cosine_similarity`, `vortex.tensor.inner_product`, + `vortex.tensor.l2_norm`, `vortex.tensor.l2_normalize` - `dtype`: `vortex.tensor.fixed_shape_tensor`, `vortex.tensor.vector` ### `zstd2026.02.0` @@ -93,14 +93,14 @@ This edition currently adds no components. ## Component checks -The writer checks the formats it actually serializes against the selected editions. The checks -cover four kinds of component: +The writer checks the formats it actually serializes against the selected editions. The checks cover +four kinds of component: -| Kind | Writing rule | -|---|---| -| Arrays | Check the serializer's returned wire ID and every serialized child recursively. | -| Layouts | Check every serialized layout ID. The writing strategy must use permitted layouts. | -| Extension dtypes | Check all extension dtypes in the schema, including nested ones, before writing bytes. | +| Kind | Writing rule | +| ------------------- | -------------------------------------------------------------------------------------- | +| Arrays | Check the serializer's returned wire ID and every serialized child recursively. | +| Layouts | Check every serialized layout ID. The writing strategy must use permitted layouts. | +| Extension dtypes | Check all extension dtypes in the schema, including nested ones, before writing bytes. | | Aggregate functions | Check every function stored in a zone map against the edition and its format contract. | A forbidden zone-map aggregate causes the write to fail. Silently omitting it would change which @@ -108,15 +108,15 @@ filters can use the configured zone map to skip rows. An aggregate that does not data type is different: the writer omits it, so there is no serialized component to check. For example, `core2026.08.0` declares `min`, `max`, `bounded_min`, `bounded_max`, `nan_count`, and -`null_count`. It does not declare `sum` because zone maps do not store sums. File-level statistics -store sums in a fixed legacy field governed by the enclosing format's contract. +`null_count`. Zone maps do not store sums, so the edition does not declare `sum`. File-level +statistics do store sums, in a fixed legacy field governed by the enclosing format's contract. ## Format testing and promotion A format intended for `core` starts in a dedicated edition family. Its first edition is a draft, -with no recorded `min_library_version` and no frozen compatibility guarantee. The format is expected -to be complete. If testing reveals a defect whose correction changes what readers must understand, -the correction needs a new wire ID and a later edition. +with no recorded `min_library_version` and no frozen compatibility guarantee. Even at this draft +stage, the format is expected to be complete. If testing reveals a defect whose correction changes +what readers must understand, the correction needs a new wire ID and a later edition. After initial testing, the format can enter a new `preview` edition for broader opt-in use. A later `core` edition can include it for default use. Promotion preserves the format and its wire ID. Only @@ -130,8 +130,8 @@ For `core`, this is a Vortex Rust crate release. Independent plugins use their o Until the release version is known, the declaration uses `min_library_version: None`. Once it is known, the field records that original release, usually while the next release is in development. -Filling in the field documents the freeze. The guarantee applies from the recorded release, even -if the declaration is updated later. +Filling in the field documents the freeze. The guarantee applies from the recorded release, even if +the declaration is updated later. A frozen edition's membership, origin, and minimum version stay fixed. Deprecating a format can stop writers from choosing it, but readers must retain support because existing files can contain it. diff --git a/docs/specs/versioning/using-editions.md b/docs/specs/versioning/using-editions.md index cd15d9e911f..0d4c486fb9a 100644 --- a/docs/specs/versioning/using-editions.md +++ b/docs/specs/versioning/using-editions.md @@ -1,7 +1,7 @@ # Using editions -To write files for an older Vortex version, select editions whose formats that version supports. -See [Versioning](../versioning.md) for the compatibility guarantee. +To write files for an older Vortex version, select editions whose formats that version supports. See +[Versioning](../versioning.md) for the compatibility guarantee. ## Writer configuration @@ -34,7 +34,7 @@ The default session registers the standard implementations and edition declarati enables `core2026.08.3`. Calling `enable_edition` replaces the enabled edition from the same family. Set the selection before starting the write, which captures the permitted formats at that point. -An edition declaration describes permitted formats. Registering it does not install the code to +An edition declaration describes permitted formats, but registering it does not install the code to read or write those formats. When constructing a session without the defaults, register the required implementations and declarations, then enable the target editions. See [Registering plugins](../../developer-guide/internals/session.md#registering-plugins) for the @@ -44,8 +44,8 @@ components cannot serialize any edition-governed component. ## Edition families An _edition family_ groups editions for related formats. The `core` family covers the default -writer's formats. Optional features have their own families, such as `tensor` and `zstd`, so they can -add formats without changing an application's `core` selection. +writer's formats. Optional features have their own families, such as `tensor` and `zstd`, so they +can add formats without changing an application's `core` selection. A writer selects at most one edition per family. Selecting `core2026.08.0` and `tensor2026.04.0` permits every component in either edition. Within one family, a later edition includes all earlier @@ -56,28 +56,29 @@ Check the [registry](editions.md#edition-registry) before enabling an optional f `core`'s frozen guarantee to the tensor formats. Both applications need the appropriate tensor implementations. -An edition name such as `core2026.08.3` contains its family, year, month, and a number distinguishing -editions in that family and month. These are Vortex editions, separate from Rust language editions. +An edition name such as `core2026.08.3` contains its family, year, month, and a number +distinguishing editions in that family and month. These are Vortex editions, separate from Rust +language editions. ## Reader versions For each selected frozen edition, find its recorded minimum version and its _origin_ in the -[registry](editions.md). The origin is the project that supplies the component implementations. -The `core` family's origin is `vortex`, so its `min_library_version` refers to the shared Vortex Rust +[registry](editions.md). The origin is the project that supplies the component implementations. The +`core` family's origin is `vortex`, so its `min_library_version` refers to the shared Vortex Rust crate version. An independent plugin can name a different origin with its own release numbers. -For editions with the same origin, use at least the highest recorded minimum. For different -origins, check each project separately. In both cases, register the implementations in the reader. -A sufficiently recent library without a required plugin is not enough. +For editions with the same origin, use at least the highest recorded minimum. For different origins, +check each project separately. In both cases, register the implementations in the reader. A +sufficiently recent library without a required plugin is not enough. ## Write errors The writer rejects forbidden formats in arrays, children, layouts, nested extension dtypes, and stored aggregate functions. -A custom strategy or compressor is responsible for constructing permitted representations. Selecting -an edition does not automatically reconfigure it. The default compressor's filtering is described in -[Compression](design.md#compression). +Selecting an edition restricts the permitted output, but does not automatically reconfigure a custom +strategy or compressor. Those implementations must construct permitted representations themselves. +The default compressor's filtering is described in [Compression](design.md#compression). When a write fails because a format is forbidden, choose a permitted representation or strategy. Alternatively, select a later edition after confirming that the readers meet its requirements. @@ -91,8 +92,8 @@ the required implementations themselves. ## Unknown IDs -An unknown-ID error means that the reader has no registered implementation for that component. -Look up its kind and ID in the [registry](editions.md#edition-registry). The kind matters because an +An unknown-ID error means that the reader has no registered implementation for that component. Look +up its kind and ID in the [registry](editions.md#edition-registry). The kind matters because an array and a layout can share the same ID string while describing different formats. - For a frozen edition, use at least the recorded minimum version of its origin and register the @@ -102,10 +103,10 @@ array and a layout can share the same ID string while describing different forma - For a component absent from the registry, obtain its implementation from the producer and register it with the session. -Inspection and copying tools can use `allow_unknown` to retain the serialized data of unknown arrays, -layouts, and extension dtypes without interpreting it. Those objects are not available for ordinary -computation. +Inspection and copying tools can use `allow_unknown` to retain the serialized data of unknown +arrays, layouts, and extension dtypes without interpreting it. However, retaining those objects does +not make them available for ordinary computation. -With `allow_unknown`, an unknown aggregate disables pruning for the affected zone-map layout. Its -data remains readable if the reader supports the other required formats. Without `allow_unknown`, -the unknown aggregate causes an error. +With `allow_unknown`, an unknown aggregate disables pruning for the affected zone-map layout. +However, the layout's data remains readable if the reader supports the other required formats. +Without `allow_unknown`, the unknown aggregate causes an error. From 1176d86bdade83e3d35011e4ad594e31bea4a2b7 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Mon, 21 Sep 2026 11:42:33 -0400 Subject: [PATCH 07/12] address comments Signed-off-by: Connor Tsui --- docs/_static/versioning-compatibility.svg | 89 ++++++++++++ docs/_static/versioning-flow.svg | 165 +++++++++++++--------- docs/specs/versioning/compatibility.md | 28 +++- docs/specs/versioning/design.md | 9 +- 4 files changed, 219 insertions(+), 72 deletions(-) create mode 100644 docs/_static/versioning-compatibility.svg diff --git a/docs/_static/versioning-compatibility.svg b/docs/_static/versioning-compatibility.svg new file mode 100644 index 00000000000..1a2cc44b57b --- /dev/null +++ b/docs/_static/versioning-compatibility.svg @@ -0,0 +1,89 @@ + + + + Compatibility checks for a particular Vortex file + + First, can the writer construct and serialize the representation? If not, the representation is + unsupported. If it can, do the selected editions permit every serialized component? If not, the + output is forbidden. Otherwise, writing is permitted. After a successful write, can the reader + decode the enclosing file format and every component used by this file? If not, it lacks format + support or a required implementation. Otherwise, the file is readable. This logical checklist + assumes valid data, correct implementations, edition checks enabled, and full decoding with + allow_unknown disabled. It stops at the first unmet requirement, not necessarily the first check + performed by the implementation. Permission to write does not guarantee successful I/O. + + + + + + + + + Can this file be written and read? + Follow each Yes to the next requirement. + + + 1. Writer support + Can this writer construct and + serialize the representation? + + + + + + + Yes + No + + + Unsupported + representation + + + 2. Edition permissions + Do the selected editions permit + every serialized component? + + + + + + + Yes + No + + + Forbidden + output + + + Writing is permitted + Continue after a successful write. + + + + 3. Reader support + Can this reader decode the file format + and every component + used by this file? + + + + + + + Yes + No + + + Unsupported format + or missing + implementation + + + Readable + + diff --git a/docs/_static/versioning-flow.svg b/docs/_static/versioning-flow.svg index f552351b818..37dea78be54 100644 --- a/docs/_static/versioning-flow.svg +++ b/docs/_static/versioning-flow.svg @@ -1,14 +1,20 @@ - - Edition checks on the write path, format dispatch on the read path + A dictionary array from writer to file to reader - The selected edition supplies permitted formats to the default compressor's scheme filter and - to the final serialization checks. Compression produces an in-memory array. Its plugin selects - a wire representation. The writer checks the actual wire IDs, including serialized children, - and rejects forbidden output. A reader uses the stored IDs to find its registered plugins and - construct its own in-memory array, independently of the writer's library version. + This illustrative dictionary encoding represents three non-null i32 values, [10, 20, 10], with + u8 codes [0, 1, 0] and i32 dictionary values [10, 20]. The selected editions filter the default + compressor's schemes and constrain the serialized array IDs. The serializer records the root ID + vortex.dict and dictionary metadata. Its two children use vortex.primitive and own the codes and + values buffers. The root has no data buffers. Array edition checks cover the root and both + children. After a successful write, the file contains schema and row-count context, format IDs, + array metadata, and child buffers. The reader resolves the stored IDs through its registered + plugins and reconstructs a dictionary array with the same logical values, type, and nullability. + Writer and reader can use different library versions. The file writer also checks layouts, + extension types, and stored aggregates. This example uses one dictionary format throughout and + does not predict the default compressor's choice for this small input. - - - - Selected edition: permitted formats + + Illustrative dictionary encoding + + + Selected editions + Permitted serialized formats - - Writer's library + + Writer - - - + + + + - - - - - - - - Default compressor - In-memory array - Plugin serializer - Check actual wire IDs - - - Filter schemes, then - compress values - Buffers and children - Select wire ID, - metadata, and parts - Include serialized children - Reject forbidden output - - - - - - - - Permitted serialized output + + Input: [10, 20, 10] + i32, 3 rows, no nulls + - - Reader's library - - - - - - - File - Decode by stored wire ID - Reader's in-memory array + + Choose an encoding + The default compressor filters schemes + by their declared output format IDs. + + + + Array in memory + Dictionary: i32, 3 rows, no nulls + + + + + Codes (u8): [0, 1, 0] + Values (i32): [10, 20] - - Wire IDs and data - Use registered plugins - Preserve values, types, and nulls + + + + Serialize the array tree + Root ID: vortex.dict + Metadata: code type u8, 2 values + The root has no data buffers. + + + Codes: vortex.primitive + Buffer: u8 [0, 1, 0] + Values: vortex.primitive + Buffer: i32 [10, 20] - - - + + + + Check the serialized IDs + Root and child IDs must be permitted. + A forbidden ID fails the write. + + Successful write + + + File + Type: non-null i32, row count: 3 + Format IDs and array metadata + Codes and values buffers + + + Reader + + + + Resolve the stored IDs + Find registered reader plugins. + Decode metadata and children. + + + + Dictionary array in memory + + Codes (u8): [0, 1, 0] + Values (i32): [10, 20] + Logical values: [10, 20, 10] + i32, 3 rows, no nulls + + Writer and reader versions can differ. + The file writer also checks layouts, + extension types, and stored aggregates. diff --git a/docs/specs/versioning/compatibility.md b/docs/specs/versioning/compatibility.md index 54c56a53b3b..774bc5a03f9 100644 --- a/docs/specs/versioning/compatibility.md +++ b/docs/specs/versioning/compatibility.md @@ -17,9 +17,9 @@ a suitable representation. Reader results apply only after a successful write. | Older | Original | Extended | Unsupported and forbidden | N/A | N/A | | Older | Later[^declaration] | Original | Allowed | Reads | Reads[^current-array] | | Older | Later[^declaration] | Extended | Unsupported | N/A | N/A | -| Newer | Original | Original | Allowed[^compression][^decimal] | Reads | Reads[^current-array] | +| Newer | Original | Original | Allowed[^compression] | Reads | Reads[^current-array] | | Newer | Original | Extended | Forbidden | N/A | N/A | -| Newer | Later | Original | Allowed[^compression][^decimal] | Reads | Reads[^current-array] | +| Newer | Later | Original | Allowed[^compression] | Reads | Reads[^current-array] | | Newer | Later | Extended | Allowed[^compression] | [Unknown ID](using-editions.md#unknown-ids) | Reads | The format column is not a separate writer setting. The serializer @@ -27,6 +27,25 @@ The format column is not a separate writer setting. The serializer edition permissions. A writer targeting the later edition can still produce the original format, which both readers can read. +## Compatibility checks + +This tree checks the requirements for a particular file. It assumes valid data, correct +implementations, edition checks enabled, and full decoding with `allow_unknown` disabled. + +```{figure} ../../_static/versioning-compatibility.svg +:alt: A decision tree checks writer support, edition permissions, and reader support in turn. + +A logical checklist, not the order of implementation steps. Edition checks occur at several points +during writing. The tree stops at the first unmet requirement, while the matrix can show multiple +restrictions. Permission to write does not guarantee successful I/O. +``` + +The [component checks](editions.md#component-checks) cover arrays and their children, layouts, +extension types, and stored aggregates. Reader support concerns the components the file actually +uses, not every component its edition permits. A writer targeting a later edition can therefore +produce a file that an older reader supports. See [Unknown IDs](using-editions.md#unknown-ids) for +missing implementations and the exceptions available with `allow_unknown`. + For the compatibility guarantee and minimum reader versions, see [Versioning](../versioning.md). The [design](design.md#compatibility-invariants) explains the invariants behind these outcomes. @@ -52,8 +71,3 @@ For the compatibility guarantee and minimum reader versions, see [Versioning](.. output needs no recompression solely to meet the edition. These cells are conditional on the writer constructing a permitted representation. See [Compression](design.md#compression) for the current behavior and planned work. - -[^decimal]: - The newer writer can construct a decimal array with one integer child and serialize it in the - original format without recompression. Additional lower-part children require the extended - format. See the [decimal example](design.md#example-decimal-children). diff --git a/docs/specs/versioning/design.md b/docs/specs/versioning/design.md index 1a94eea24c7..72ee705629f 100644 --- a/docs/specs/versioning/design.md +++ b/docs/specs/versioning/design.md @@ -38,11 +38,16 @@ registered plugins that interpret those formats. A missing implementation causes [unknown-ID error](using-editions.md#unknown-ids). ```{figure} ../../_static/versioning-flow.svg -:alt: Edition checks constrain writing. Stored wire IDs select the reader's plugins. +:alt: A dictionary's codes and values are serialized, checked, and read back as a dictionary. -Serialization and reading can use different versions of the library. +An illustrative dictionary encoding. Writer and reader can use different library versions while +preserving values, types, and nullability. The default compressor is not required to choose this +encoding for these values. The example uses one dictionary format throughout. ``` +The figure follows the array through serialization. The file writer also checks layouts, +extension types, and stored aggregates against the selected editions. + ## Example: decimal children Consider the decimal values `[1.25, 2.50, 3.75]`. They can be represented as the integers From 4935ecfd46c6704fe97e11018c349f1fbf579541 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Mon, 21 Sep 2026 13:06:03 -0400 Subject: [PATCH 08/12] docs: clarify versioning examples and array conversions Signed-off-by: "Connor Tsui" --- docs/_static/versioning-compatibility.svg | 168 +++++++++++++--------- docs/_static/versioning-flow.svg | 164 ++++++++------------- docs/specs/versioning/compatibility.md | 108 +++++++------- docs/specs/versioning/design.md | 18 ++- 4 files changed, 232 insertions(+), 226 deletions(-) diff --git a/docs/_static/versioning-compatibility.svg b/docs/_static/versioning-compatibility.svg index 1a2cc44b57b..cafb4d489f1 100644 --- a/docs/_static/versioning-compatibility.svg +++ b/docs/_static/versioning-compatibility.svg @@ -1,17 +1,23 @@ - - Compatibility checks for a particular Vortex file + How array plugins preserve compatibility across memory and wire formats - First, can the writer construct and serialize the representation? If not, the representation is - unsupported. If it can, do the selected editions permit every serialized component? If not, the - output is forbidden. Otherwise, writing is permitted. After a successful write, can the reader - decode the enclosing file format and every component used by this file? If not, it lacks format - support or a required implementation. Otherwise, the file is readable. This logical checklist - assumes valid data, correct implementations, edition checks enabled, and full decoding with - allow_unknown disabled. It stops at the first unmet requirement, not necessarily the first check - performed by the implementation. Permission to write does not guarantee successful I/O. + Writing starts with an in-memory array. Its ID selects a serializer plugin. + If no plugin can serialize this representation, the write fails. Otherwise, the plugin chooses + the oldest supported writable format that preserves the representation without recompression. + The plugin returns a wire ID, metadata, buffers, and child arrays. It can adapt these components + to the selected wire contract. Each returned child is serialized recursively. The writer checks + all returned IDs against the editions, + along with layouts, extension types, and stored aggregates. Forbidden output fails the write, + without retrying with a different permitted wire ID. After a successful write, the + reader must support the file format and have plugins for the stored wire IDs. Each plugin + validates the stored ID's contract and constructs an in-memory array, adapting the + structure when needed and reusing buffers where possible. Missing support or invalid serialized + data causes an error. Values, data types, and nulls are preserved. The diagram assumes edition + checks are enabled and allow_unknown is disabled. It groups related checks, which occur at + several points during writing and reading. - + - Can this file be written and read? - Follow each Yes to the next requirement. - - - 1. Writer support - Can this writer construct and - serialize the representation? + WRITE + Start with an in-memory array. - - + + + + + + + + + + - - Yes - No + + Yes + Then + No + No + Yes + Yes + Yes + No + No - - Unsupported - representation - - 2. Edition permissions - Do the selected editions permit - every serialized component? + + Find the serializer plugin + Look up the in-memory ID. + Can the plugin serialize this + representation without + recompression? - - - - - - Yes - No - - - Forbidden - output + + Select the wire format + Choose the oldest writable format + that preserves the representation. + Adapt metadata, buffers, or children. + Return the wire ID and these parts. + Serialize each returned child recursively. - - Writing is permitted - Continue after a successful write. - + + Check edition permissions + Is every returned wire ID permitted, + including all serialized children? + Also check layouts, extension types, + and stored aggregates. - - 3. Reader support - Can this reader decode the file format - and every component - used by this file? + + Write fails + No serializer or writable format. - - - - - - Yes - No - - - Unsupported format - or missing - implementation + If another encoding is needed, + recompress the array before + serialization. + + + Write fails: forbidden output + No retry with a different permitted ID. + + + File: wire IDs and data + After a successful write + + READ + Select implementations by the stored IDs. + + + Find the reader plugins + Look up the stored wire IDs. + Does the reader support the file + format and all required wire IDs? + + + Validate the wire contract + Are its metadata, buffers, + types, and children valid? + Each wire ID has a fixed contract. + + + Construct the in-memory array + Adapt the stored structure if needed. + Reuse buffers where possible. + Use the reader's array implementation. + + + Missing reader support + Unsupported file format or unknown ID. + + + Reject invalid serialized data + The stored ID determines the contract. - - Readable + Preserve values, types, and nulls. + The in-memory array tree can differ. diff --git a/docs/_static/versioning-flow.svg b/docs/_static/versioning-flow.svg index 37dea78be54..0f3f753f74d 100644 --- a/docs/_static/versioning-flow.svg +++ b/docs/_static/versioning-flow.svg @@ -1,127 +1,81 @@ - - A dictionary array from writer to file to reader + Library 2 uses one array implementation for Formats A and B - This illustrative dictionary encoding represents three non-null i32 values, [10, 20, 10], with - u8 codes [0, 1, 0] and i32 dictionary values [10, 20]. The selected editions filter the default - compressor's schemes and constrain the serialized array IDs. The serializer records the root ID - vortex.dict and dictionary metadata. Its two children use vortex.primitive and own the codes and - values buffers. The root has no data buffers. Array edition checks cover the root and both - children. After a successful write, the file contains schema and row-count context, format IDs, - array metadata, and child buffers. The reader resolves the stored IDs through its registered - plugins and reconstructs a dictionary array with the same logical values, type, and nullability. - Writer and reader can use different library versions. The file writer also checks layouts, - extension types, and stored aggregates. This example uses one dictionary format throughout and - does not predict the default compressor's choice for this small input. + In this illustrative example, Library 1 supports only Format A. Library 2 supports Formats A and B + through one in-memory array implementation. Format A was introduced first. The serializer selects + Format A's wire ID when that format can represent the array without recompression. + Otherwise, Format B's wire ID is needed. The plugin can adapt metadata, buffers, or children. + Edition checks validate the selected wire ID and serialized children. They do not choose the + format. On reading, the stored wire ID selects a deserializer, which must validate that ID's + contract. Library 2 decodes both formats into its own array implementation, adapting the stored + structure when needed. It does not need a separate type for Format A. Values, data types, and + nulls are preserved. Library 1 can read Format A when it supports all other + components the file uses. - - - - - Illustrative dictionary encoding - - - Selected editions - Permitted serialized formats + + + Library 2 uses one array implementation for both wire formats + Illustrative formats with distinct wire IDs. Format A was introduced before Format B. - - Writer + LIBRARY 2 WRITER + WIRE FORMAT + LIBRARY 2 READER - - - - + + + + + + serialize + serialize + deserialize + deserialize - - Input: [10, 20, 10] - i32, 3 rows, no nulls - + + In-memory array + The array's ID selects the plugin. + Choose the oldest writable format + that needs no recompression. + Adapt metadata, buffers, + or children to that contract. - - Choose an encoding - The default compressor filters schemes - by their declared output format IDs. - + + Format A + Represents the array without + recompression. + Wire ID, metadata, buffers, children - - Array in memory - Dictionary: i32, 3 rows, no nulls - - - - - Codes (u8): [0, 1, 0] - Values (i32): [10, 20] - - - - - Serialize the array tree - Root ID: vortex.dict - Metadata: code type u8, 2 values - The root has no data buffers. - - - Codes: vortex.primitive - Buffer: u8 [0, 1, 0] - Values: vortex.primitive - Buffer: i32 [10, 20] - - + + Format B + Required when Format A cannot + preserve the representation. + Wire ID, metadata, buffers, children - - Check the serialized IDs - Root and child IDs must be permitted. - A forbidden ID fails the write. - - Successful write + + In-memory array + The wire ID selects the plugin + and the contract it validates. + Adapt the structure if needed. + Both formats can use the same + in-memory representation. - - File - Type: non-null i32, row count: 3 - Format IDs and array metadata - Codes and values buffers - - - Reader - - - - Resolve the stored IDs - Find registered reader plugins. - Decode metadata and children. - - - - Dictionary array in memory - - Codes (u8): [0, 1, 0] - Values (i32): [10, 20] - - Logical values: [10, 20, 10] - i32, 3 rows, no nulls + + Edition permissions + Check the selected IDs and children. - Writer and reader versions can differ. - The file writer also checks layouts, - extension types, and stored aggregates. + Library 1 supports Format A only. + Library 2 supports both formats. + Preserve values, types, and nulls. + No separate array type for Format A. diff --git a/docs/specs/versioning/compatibility.md b/docs/specs/versioning/compatibility.md index 774bc5a03f9..9cdbf84e4dd 100644 --- a/docs/specs/versioning/compatibility.md +++ b/docs/specs/versioning/compatibility.md @@ -1,73 +1,81 @@ # Compatibility matrix -This matrix covers backward compatibility and writing for older readers as an encoding gains a new -wire format. The versions and editions are illustrative: - -- The older library reads and writes only the original format. -- The newer library reads and writes both formats through its current array implementation. -- The original edition permits only the original format. The later edition permits both. - -Both readers are assumed to support the rest of the file with the required plugins. **Allowed** -means the writer implements the format and the edition permits it, provided the writer can construct -a suitable representation. Reader results apply only after a successful write. - -| Writer version | Target edition | Format | Write result[^representation] | Older reader | Newer reader | -| -------------- | ------------------- | -------- | ------------------------------- | ------------------------------------------- | --------------------- | -| Older | Original | Original | Allowed | Reads | Reads[^current-array] | -| Older | Original | Extended | Unsupported and forbidden | N/A | N/A | -| Older | Later[^declaration] | Original | Allowed | Reads | Reads[^current-array] | -| Older | Later[^declaration] | Extended | Unsupported | N/A | N/A | -| Newer | Original | Original | Allowed[^compression] | Reads | Reads[^current-array] | -| Newer | Original | Extended | Forbidden | N/A | N/A | -| Newer | Later | Original | Allowed[^compression] | Reads | Reads[^current-array] | -| Newer | Later | Extended | Allowed[^compression] | [Unknown ID](using-editions.md#unknown-ids) | Reads | - -The format column is not a separate writer setting. The serializer -[selects a format](design.md#format-selection) from the array's structure, and the writer checks its -edition permissions. A writer targeting the later edition can still produce the original format, -which both readers can read. +The matrix shows which formats each library can write, which the target edition permits, and which +each reader supports. It uses illustrative names rather than actual Vortex versions or editions: + +- **Format A** and **Format B** have distinct wire IDs and contracts. Format A was introduced first. +- **Library 1** reads and writes only Format A. +- **Library 2** reads and writes both formats through one array implementation. +- **Edition 1** permits only Format A. **Edition 2** permits both formats. + +Both readers are assumed to support all other components in the file. **Unsupported** means the +writer has no implementation for that format. **Forbidden** means the target edition excludes it. +**Allowed** means both requirements are met, but the writer must still construct an array that the +format can represent. Reader results apply only after a successful write. + +| Writer | Target edition | Format | Write result[^representation] | Library 1 reader | Library 2 reader | +| --------- | ----------------------- | -------- | ----------------------------- | ------------------------------------------- | --------------------- | +| Library 1 | Edition 1 | Format A | Allowed | Reads | Reads[^current-array] | +| Library 1 | Edition 1 | Format B | Unsupported and forbidden | N/A | N/A | +| Library 1 | Edition 2[^declaration] | Format A | Allowed | Reads | Reads[^current-array] | +| Library 1 | Edition 2[^declaration] | Format B | Unsupported | N/A | N/A | +| Library 2 | Edition 1 | Format A | Allowed[^compression] | Reads | Reads[^current-array] | +| Library 2 | Edition 1 | Format B | Forbidden | N/A | N/A | +| Library 2 | Edition 2 | Format A | Allowed[^compression] | Reads | Reads[^current-array] | +| Library 2 | Edition 2 | Format B | Allowed[^compression] | [Unknown ID](using-editions.md#unknown-ids) | Reads | + +The serializer [selects a format](design.md#format-selection) from the array's structure. The writer +then checks whether the target edition permits it. The format column shows that selection, not a +separate writer setting. Edition 2 permits both formats, so a writer targeting it can still produce +Format A for Library 1 to read. ## Compatibility checks -This tree checks the requirements for a particular file. It assumes valid data, correct -implementations, edition checks enabled, and full decoding with `allow_unknown` disabled. +The diagram follows an array from memory to storage and back through the serializer and reader +plugins. It assumes correct implementations, edition checks enabled, and full decoding with +`allow_unknown` disabled. ```{figure} ../../_static/versioning-compatibility.svg -:alt: A decision tree checks writer support, edition permissions, and reader support in turn. +:alt: Plugins select and validate wire formats while adapting arrays between memory and storage. +:target: ../../_static/versioning-compatibility.svg -A logical checklist, not the order of implementation steps. Edition checks occur at several points -during writing. The tree stops at the first unmet requirement, while the matrix can show multiple -restrictions. Permission to write does not guarantee successful I/O. +The writer selects a plugin by the array's in-memory ID. The reader selects a plugin by the stored +wire ID. Each plugin can adapt the array structure while preserving values, data types, and nulls. +The diagram groups related checks. In the implementation, component checks occur at several points +during writing and reading. ``` +If the serializer returns a forbidden ID, the write fails. The writer does not retry with a different +permitted ID. When the target requires a different encoding, the array must be recompressed before +serialization. See [Format selection](design.md#format-selection). + +The matrix assumes valid serialized data. The diagram also shows the reader rejecting data that +violates the stored ID's contract. Format compatibility does not prevent I/O errors. + The [component checks](editions.md#component-checks) cover arrays and their children, layouts, -extension types, and stored aggregates. Reader support concerns the components the file actually -uses, not every component its edition permits. A writer targeting a later edition can therefore -produce a file that an older reader supports. See [Unknown IDs](using-editions.md#unknown-ids) for +extension types, and stored aggregates. The reader needs implementations for the components the file +uses, not every component its edition permits. See [Unknown IDs](using-editions.md#unknown-ids) for missing implementations and the exceptions available with `allow_unknown`. For the compatibility guarantee and minimum reader versions, see [Versioning](../versioning.md). The [design](design.md#compatibility-invariants) explains the invariants behind these outcomes. [^representation]: - An input array can require a format that the target forbids. The plugin can provide a lossless - structural downgrade. If the array requires recompression, the write path must arrange it - explicitly or fail. + An input array can require a format that the target edition forbids. A serializer can adapt + metadata, buffers, or children without recompression. If the target requires a different + encoding, the array must be recompressed before serialization or the write fails. [^current-array]: - The newer reader reads the original format into its current implementation. The plugin adapts - the structure only if necessary. Using a newer version of the Vortex crates does not itself - require an array upgrade or conversion. + Library 2 reads Format A into its own array implementation, adapting the structure only if + necessary. A library upgrade alone does not require an array conversion. [^declaration]: - The older writer needs the later edition's declaration to select it. Registering the declaration - does not add support for the extended format, so the older writer still writes only the original - format. + Library 1 needs Edition 2's declaration to select it. Registering the declaration does not add + support for Format B, so Library 1 still writes only Format A. [^compression]: - Current schemes declare the serialized IDs that they produce, and the builder filters them by - those IDs. General per-writer scheme configuration is not implemented. The planned configuration - would select compatible behavior before estimation, sampling, and full compression, so the - output needs no recompression solely to meet the edition. These cells are conditional on the - writer constructing a permitted representation. See [Compression](design.md#compression) for the - current behavior and planned work. + The default compressor filters schemes by their declared output wire IDs. General per-writer + scheme configuration is not implemented. The planned configuration will select compatible + behavior before estimation, sampling, and full compression to avoid recompression solely to meet + the edition. These cells still require the writer to construct a permitted representation. See + [Compression](design.md#compression) for the current behavior and planned work. diff --git a/docs/specs/versioning/design.md b/docs/specs/versioning/design.md index 72ee705629f..59bc0d697de 100644 --- a/docs/specs/versioning/design.md +++ b/docs/specs/versioning/design.md @@ -37,16 +37,22 @@ serialized children against the selected editions. A reader uses the IDs in the registered plugins that interpret those formats. A missing implementation causes an [unknown-ID error](using-editions.md#unknown-ids). +The following example uses two formats with distinct wire IDs and contracts. Library 1 supports only +Format A. Library 2 adds support for Format B and retains support for Format A, using one in-memory +array implementation for both. The [compatibility matrix](compatibility.md) uses the same names. + ```{figure} ../../_static/versioning-flow.svg -:alt: A dictionary's codes and values are serialized, checked, and read back as a dictionary. +:alt: Library 2 serializes and deserializes Formats A and B through one in-memory array implementation. +:target: ../../_static/versioning-flow.svg -An illustrative dictionary encoding. Writer and reader can use different library versions while -preserving values, types, and nullability. The default compressor is not required to choose this -encoding for these values. The example uses one dictionary format throughout. +Library 2 writes Format A when it can represent the array without recompression. Otherwise, it needs +Format B. The plugin can adapt metadata, buffers, or children during serialization and +deserialization. Both formats decode into Library 2's array implementation, so it does not need a +separate type for Format A. ``` -The figure follows the array through serialization. The file writer also checks layouts, -extension types, and stored aggregates against the selected editions. +The writer checks the selected wire ID and every serialized child against the target editions. +These checks also cover layouts, extension types, and stored aggregates. ## Example: decimal children From ca8cfad3ee427557248ad82a3f0e2f9ef8278d72 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Mon, 21 Sep 2026 13:26:27 -0400 Subject: [PATCH 09/12] address more comments Signed-off-by: Connor Tsui --- docs/specs/versioning/compatibility.md | 26 +++++----- docs/specs/versioning/design.md | 68 ++++++++++++-------------- 2 files changed, 43 insertions(+), 51 deletions(-) diff --git a/docs/specs/versioning/compatibility.md b/docs/specs/versioning/compatibility.md index 9cdbf84e4dd..bd5fa9b3002 100644 --- a/docs/specs/versioning/compatibility.md +++ b/docs/specs/versioning/compatibility.md @@ -13,16 +13,16 @@ writer has no implementation for that format. **Forbidden** means the target edi **Allowed** means both requirements are met, but the writer must still construct an array that the format can represent. Reader results apply only after a successful write. -| Writer | Target edition | Format | Write result[^representation] | Library 1 reader | Library 2 reader | -| --------- | ----------------------- | -------- | ----------------------------- | ------------------------------------------- | --------------------- | -| Library 1 | Edition 1 | Format A | Allowed | Reads | Reads[^current-array] | -| Library 1 | Edition 1 | Format B | Unsupported and forbidden | N/A | N/A | -| Library 1 | Edition 2[^declaration] | Format A | Allowed | Reads | Reads[^current-array] | -| Library 1 | Edition 2[^declaration] | Format B | Unsupported | N/A | N/A | -| Library 2 | Edition 1 | Format A | Allowed[^compression] | Reads | Reads[^current-array] | -| Library 2 | Edition 1 | Format B | Forbidden | N/A | N/A | -| Library 2 | Edition 2 | Format A | Allowed[^compression] | Reads | Reads[^current-array] | -| Library 2 | Edition 2 | Format B | Allowed[^compression] | [Unknown ID](using-editions.md#unknown-ids) | Reads | +| Writer | Target edition | Format | Write result[^representation] | Library 1 reader | Library 2 reader | +| -------------- | ---------------------------- | ------------- | ----------------------------- | ------------------------------------------- | --------------------- | +| Library 1 | Edition 1 | Format A | Allowed | Reads | Reads[^current-array] | +| Library 1 | Edition 1 | Format B | Unsupported and forbidden | N/A | N/A | +| Library 1 | Edition 2[^declaration] | Format A | Allowed | Reads | Reads[^current-array] | +| Library 1 | Edition 2[^declaration] | Format B | Unsupported | N/A | N/A | +| Library 2 | Edition 1 | Format A | Allowed[^compression] | Reads | Reads[^current-array] | +| Library 2 | Edition 1 | Format B | Forbidden | N/A | N/A | +| Library 2 | Edition 2 | Format A | Allowed[^compression] | Reads | Reads[^current-array] | +| Library 2 | Edition 2 | Format B | Allowed[^compression] | [Unknown ID](using-editions.md#unknown-ids) | Reads | The serializer [selects a format](design.md#format-selection) from the array's structure. The writer then checks whether the target edition permits it. The format column shows that selection, not a @@ -45,9 +45,9 @@ The diagram groups related checks. In the implementation, component checks occur during writing and reading. ``` -If the serializer returns a forbidden ID, the write fails. The writer does not retry with a different -permitted ID. When the target requires a different encoding, the array must be recompressed before -serialization. See [Format selection](design.md#format-selection). +If the serializer returns a forbidden ID, the write fails. The writer does not retry with a +different permitted ID. When the target requires a different encoding, the array must be +recompressed before serialization. See [Format selection](design.md#format-selection). The matrix assumes valid serialized data. The diagram also shows the reader rejecting data that violates the stored ID's contract. Format compatibility does not prevent I/O errors. diff --git a/docs/specs/versioning/design.md b/docs/specs/versioning/design.md index 59bc0d697de..4ddc5b56760 100644 --- a/docs/specs/versioning/design.md +++ b/docs/specs/versioning/design.md @@ -1,12 +1,12 @@ # Versioning design -A file can outlive the application that wrote it. Its readers can also belong to different services, -with different upgrade schedules. Meanwhile, the library writing those files needs to improve its -compression algorithms and in-memory data structures. Tying every such change to a new file format -would force readers to upgrade even when the stored data could remain the same. +Applications that share files can use different library versions and upgrade at different times. New +library releases need to improve compression and in-memory data structures while continuing to read +existing files. They also need a way to write files for applications that have not upgraded. -Vortex separates the implementation used by an application from the serialized formats it reads and -writes. The [versioning overview](../versioning.md) describes the compatibility guarantee. +Vortex separates library implementations from the serialized formats they read and write. An +implementation can change while retaining a format that existing readers understand. The +[versioning overview](../versioning.md) describes the compatibility guarantee. ## Versions and formats @@ -51,42 +51,34 @@ deserialization. Both formats decode into Library 2's array implementation, so i separate type for Format A. ``` -The writer checks the selected wire ID and every serialized child against the target editions. -These checks also cover layouts, extension types, and stored aggregates. +The writer checks the selected wire ID and every serialized child against the target editions. These +checks also cover layouts, extension types, and stored aggregates. ## Example: decimal children -Consider the decimal values `[1.25, 2.50, 3.75]`. They can be represented as the integers -`[125, 250, 375]` with a scale of two decimal places. The original decimal-byte-parts format stores -these integers in one signed integer child array. +The decimal-byte-parts encoding stores decimal values in integer child arrays. It can store each +value in one child or split it across several children. One in-memory array type handles both +shapes, but the original wire contract permits only one child. Supporting additional children +therefore requires a new wire ID.[^decimal-availability] -The current implementation also supports wider values split across several children: a signed -most-significant part followed by unsigned lower parts. One Rust array type handles both shapes, but -the original format's contract permits only the single-child shape. The additional children -therefore require a new wire ID.[^decimal-availability] +| Array structure | Wire ID selected by the serializer | +| ------------------------ | ---------------------------------- | +| One signed integer child | `vortex.decimal_byte_parts` | +| Several integer children | `vortex.decimal_byte_parts.v2` | -| Array structure | Wire ID selected by the serializer | -| -------------------------------------------------- | ---------------------------------- | -| One signed integer child, no lower parts | `vortex.decimal_byte_parts` | -| A signed integer child with additional lower parts | `vortex.decimal_byte_parts.v2` | +For a single-child array, the serializer reuses the child and writes the original format's metadata +without recompression. An array with several children uses the second format, even if its values are +small enough to fit in one child. Combining those parts into one child requires re-encoding, which +the serializer does not perform. -The current array type's in-memory ID matches the newer wire ID, but its serializer can return the -original ID. The writer must therefore check the serializer's returned ID. - -For the example values, the serializer reuses the child containing `[125, 250, 375]` and writes the -original format's metadata. No recompression is needed. The updated reader can decode that file -directly into its current array type, with no lower-part children. - -The reader must still enforce the original contract when it sees the original ID. For that format, -the `lower_part_count` metadata field must be zero and the array must have one signed integer child. -Understanding additional children under the new ID does not make them valid under the old ID. -Otherwise, a new writer could label extended data as the original format and produce a file that an -old reader cannot interpret. +The array type's in-memory ID is `vortex.decimal_byte_parts.v2` for both shapes. Since the +serializer can return a different wire ID, the writer must check the returned ID against the target +editions. -The serializer chooses from the array's structure, not by inspecting whether values across several -children can fit in one integer child. An array with lower parts uses the extended format even if -its values happen to be small. There is no need for a format-version field on the in-memory array to -distinguish these cases. +Both wire formats deserialize into the same in-memory array type. The reader must still validate the +contract identified by the stored ID: `vortex.decimal_byte_parts` requires exactly one signed +integer child. Support for multiple children under the second ID does not make them valid under the +first ID. ### Format selection @@ -104,9 +96,9 @@ compatibility requires a different encoding of the same values, that is a compre write path must arrange that work explicitly or fail. Reading can also change the array structure. For example, the old ALP floating-point format stores -exceptional values, called patches, inside the ALP array. The current plugin reads it into a -`Patched` parent around an ALP child without patches. The values stay the same even though the -reader's array tree differs from the stored tree. +exceptional values, called patches, inside the ALP array. With the experimental `Patched` encoding +enabled, the reader moves those patches into a `Patched` parent around an ALP child without patches. +The values stay the same even though the reader's array tree differs from the stored tree. ## Children and other components From 3b410bb3e89f46ef27ee7cfd1abf65dc72bcc9ef Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Mon, 21 Sep 2026 14:40:25 -0400 Subject: [PATCH 10/12] clarify wording Signed-off-by: Connor Tsui --- docs/specs/versioning.md | 93 +++++++++++++++----------- docs/specs/versioning/compatibility.md | 4 +- docs/specs/versioning/design.md | 31 ++++----- 3 files changed, 71 insertions(+), 57 deletions(-) diff --git a/docs/specs/versioning.md b/docs/specs/versioning.md index 0fd9306835e..6038673e801 100644 --- a/docs/specs/versioning.md +++ b/docs/specs/versioning.md @@ -5,20 +5,28 @@ companion pages for the configuration details and the reasoning behind the desig - [Using editions](versioning/using-editions.md) explains how to configure writers for older readers and diagnose compatibility errors. -- [Versioning design](versioning/design.md) explains how formats evolve, with a worked example and - the invariants that preserve compatibility. +- [Versioning design](versioning/design.md) explains how encodings and wire formats evolve, with a + worked example and the invariants that preserve compatibility. - [Compatibility matrix](versioning/compatibility.md) summarizes the combinations of writer version, - edition, serialized format, and reader version. -- [Edition registry](versioning/editions.md) lists the formats and minimum versions for each edition. - -Vortex guarantees **backward compatibility** for its frozen serialized formats: newer library -versions retain read support for files in those formats. Applications can upgrade their readers -without rewriting those files. Newer writers can also select formats that older readers support, -so the applications producing and consuming files do not need to upgrade together. - -An _edition_ names a set of formats that a writer is allowed to put in a file. Once an edition is -[_frozen_](versioning/editions.md#freezing-an-edition), that set and its reader requirements stay fixed. -The first frozen edition is `core2025.05.0`, supported from version `0.36.0` of the Vortex Rust library. + edition, wire format, and reader version. +- [Edition registry](versioning/editions.md) lists the permitted wire IDs and minimum versions for + each edition. + +An array's _encoding_ describes its in-memory representation. Its _wire format_ defines how to +interpret its serialized metadata, buffers, and children. A _wire ID_ identifies that contract. An +array plugin maps between the two representations, so a reader can reconstruct the data using a +different in-memory encoding from the writer's. + +Vortex guarantees **backward compatibility** for its frozen wire formats: newer library versions +retain the code needed to decode them. Applications can upgrade their readers without rewriting +existing files. Writers can also target wire formats that older readers understand, so the +applications producing and consuming files do not need to upgrade together. + +An _edition_ names the wire formats that a writer is allowed to use for arrays and +[other serialized components](versioning/editions.md#component-checks). Once an edition is +[_frozen_](versioning/editions.md#freezing-an-edition), that set and its reader requirements stay +fixed. The first frozen edition is `core2025.05.0`, supported from version `0.36.0` of the Vortex +Rust library. This guarantee concerns file compatibility. The library's programming interfaces follow [Rust's semantic versioning rules](https://doc.rust-lang.org/cargo/reference/semver.html), so an API @@ -27,54 +35,59 @@ change can require application changes even when existing files remain readable. ## Writing for older readers Consider two applications. A service writes files using Vortex `0.85.0`, while a query engine reads -them using Vortex `0.84.0`. These numbers identify the Rust library versions used by each application. - -The service [selects `core2026.08.0` for writing](versioning/using-editions.md#writer-configuration). -That edition's recorded minimum reader version is `0.84.0`, so the query engine meets the version -requirement. With the required implementations -registered, it can read valid files successfully written within that edition's restrictions. - -Edition selection limits the serialized formats, but the service still uses its own library's array -implementations and compression algorithms. Improvements that preserve those formats do not require -an update to the query engine. The -[decimal example](versioning/design.md#example-decimal-children) shows how a newer implementation -can still write an older format. - -If the service instead selects `core2026.08.3`, the edition permits additional formats and records +them using Vortex `0.84.0`. These numbers identify the Rust library versions used by each +application. + +The service +[selects `core2026.08.0` for writing](versioning/using-editions.md#writer-configuration). That +edition's recorded minimum reader version is `0.84.0`, so the query engine meets the version +requirement. With the required implementations registered, it can read valid files successfully +written within that edition's restrictions. + +Edition selection constrains the serialized output. The service still uses Vortex `0.85.0`'s +compression code and in-memory array encodings. That code can choose different compression schemes +or use different array data structures, provided each serialized component uses a permitted wire ID +and obeys its contract. The query engine can then decode the output into the encodings supported by +Vortex `0.84.0`. The [decimal example](versioning/design.md#example-decimal-children) shows how one +in-memory encoding can serialize to two wire formats. + +If the service instead selects `core2026.08.3`, the edition permits additional wire IDs and records a minimum of `0.85.0`. The older query engine is no longer guaranteed to read every file the service -can produce. It can still read a particular file if that file uses only formats it supports. +can produce. It can still read a particular file if it has implementations for all the wire IDs that +file uses. -The recorded minimum covers every format in the edition, including formats that a particular file -does not use. +The recorded minimum covers every wire ID in the edition, including IDs that a particular file does +not use. ## Compatibility requirements -**A successful write with edition checks enabled uses only permitted formats.** A reader that -supports all those formats can read the output. The reader must also understand the enclosing +**A successful write with edition checks enabled uses only permitted wire IDs.** A reader with +implementations for all those IDs can read the output. The reader must also understand the enclosing [file format](file-format.md). Meeting a [minimum library version](versioning/using-editions.md#reader-versions) is part of that -requirement. The application must also register the implementations that read the formats, including +requirement. The application must also register the implementations for those wire IDs, including any optional plugins. An independent plugin can have its own versions and compatibility policy. Upgrading Vortex alone does not install it. Edition selection does not guarantee that every input or custom writing strategy can produce a -permitted file. An array can need a format that the edition forbids, or a custom strategy can choose -an unsupported layout. The [write fails](versioning/using-editions.md#write-errors) when its -serialized output violates the selection. +permitted file. An array's encoding can require a wire representation that the edition forbids, or a +custom strategy can choose an unsupported layout. The +[write fails](versioning/using-editions.md#write-errors) when its serialized output violates the +selection. [Draft editions](versioning/editions.md#draft-editions) have no frozen compatibility guarantee. -Custom formats written with edition checks disabled also fall outside the edition guarantee. +Custom wire formats written with edition checks disabled also fall outside the edition guarantee. ## Upgrades The default Vortex session selects the newest frozen `core` edition, so a library upgrade can change the default output permissions. To keep serving older readers, explicitly select an edition whose requirements those readers meet. Change that selection when the readers can support the additional -formats. +wire formats. -Selecting an older edition restricts what the application writes, but it can still read newer formats -that its registered implementations support. +Selecting an older edition restricts what the application writes. It does not restrict which wire +IDs the reader's registered plugins can decode. ```{toctree} --- diff --git a/docs/specs/versioning/compatibility.md b/docs/specs/versioning/compatibility.md index bd5fa9b3002..bf70b0b2160 100644 --- a/docs/specs/versioning/compatibility.md +++ b/docs/specs/versioning/compatibility.md @@ -1,7 +1,7 @@ # Compatibility matrix -The matrix shows which formats each library can write, which the target edition permits, and which -each reader supports. It uses illustrative names rather than actual Vortex versions or editions: +The matrix shows which wire formats each library can write, which the target edition permits, and +which each reader supports. The names are illustrative rather than actual Vortex versions or editions: - **Format A** and **Format B** have distinct wire IDs and contracts. Format A was introduced first. - **Library 1** reads and writes only Format A. diff --git a/docs/specs/versioning/design.md b/docs/specs/versioning/design.md index 4ddc5b56760..60e5c6e57ad 100644 --- a/docs/specs/versioning/design.md +++ b/docs/specs/versioning/design.md @@ -4,20 +4,21 @@ Applications that share files can use different library versions and upgrade at library releases need to improve compression and in-memory data structures while continuing to read existing files. They also need a way to write files for applications that have not upgraded. -Vortex separates library implementations from the serialized formats they read and write. An -implementation can change while retaining a format that existing readers understand. The -[versioning overview](../versioning.md) describes the compatibility guarantee. +Vortex separates in-memory array encodings and compression code from the wire formats stored in +files. These implementations can change while retaining wire formats that existing readers +understand. The [versioning overview](../versioning.md) describes the compatibility guarantee. ## Versions and formats A library release supplies code: array implementations, compression algorithms, readers, and -writers. A serialized format specifies how to interpret stored metadata and buffers. Its _wire ID_ -identifies that contract, including the supported data types and any child arrays. An edition groups -these IDs into a set of permitted formats. +writers. An array's _encoding_ defines its in-memory representation. Its _wire format_ specifies how +to interpret serialized metadata, buffers, and children. A _wire ID_ identifies that contract, +including the supported data types. An edition groups wire IDs into a set of permitted serialized +representations. For example, a newer library can improve how it compresses a dictionary's values while keeping the -same dictionary format. It can also change its internal array fields while retaining code to read -old files. Only a change to what a reader must understand requires a new serialized contract. +same dictionary wire format. It can also change its internal array fields while retaining code to +read old files. Only a change to what a reader must understand requires a new serialized contract. The file container has a separate [version tag](../file-format.md#file-specification). It describes the enclosing format. Component wire IDs describe the arrays and other structures within that @@ -31,13 +32,13 @@ and nulls. An array can contain buffers and other arrays, called _children_. A d example, has a child for its values and another for the codes that refer to those values. Each child can use its own encoding. -An _array plugin_ supplies serialization and deserialization for an in-memory array representation. +An _array plugin_ supplies serialization and deserialization for an in-memory array encoding. Its serializer returns a wire ID, metadata, buffers, and children. The writer checks that ID and the serialized children against the selected editions. A reader uses the IDs in the file to find the -registered plugins that interpret those formats. A missing implementation causes an -[unknown-ID error](using-editions.md#unknown-ids). +registered plugins that decode the stored data into in-memory encodings. A missing implementation +causes an [unknown-ID error](using-editions.md#unknown-ids). -The following example uses two formats with distinct wire IDs and contracts. Library 1 supports only +The following example uses two wire formats with distinct IDs and contracts. Library 1 supports only Format A. Library 2 adds support for Format B and retains support for Format A, using one in-memory array implementation for both. The [compatibility matrix](compatibility.md) uses the same names. @@ -102,9 +103,9 @@ The values stay the same even though the reader's array tree differs from the st ## Children and other components -Suppose the decimal serializer selects the original wire ID, but its integer child uses an encoding -that the target edition forbids. Checking only the decimal ID would accept a file that the intended -reader cannot decode. The writer must check every child recursively. +Suppose the decimal serializer selects the original wire ID, but its integer child's serializer +returns an ID that the target edition forbids. Checking only the decimal ID accepts output that the +intended reader cannot decode. The writer must check every child recursively. The same requirement extends beyond arrays. A file also describes its layout, logical types, and stored summaries used for pruning. Editions cover each of these component kinds: From 0ac688c533750ac15461b81ddc378198138fd75b Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Mon, 21 Sep 2026 16:11:53 -0400 Subject: [PATCH 11/12] better flow Signed-off-by: Connor Tsui --- docs/_static/versioning-checks.svg | 70 ++++++++ docs/_static/versioning-compatibility.svg | 14 +- docs/_static/versioning-flow.svg | 131 +++++++------- docs/_theme/vortex/vortex.css | 4 + docs/specs/versioning.md | 48 +++--- docs/specs/versioning/compatibility.md | 56 +++--- docs/specs/versioning/design.md | 198 +++++++++++++--------- docs/specs/versioning/editions.md | 34 ++-- docs/specs/versioning/using-editions.md | 42 ++--- 9 files changed, 349 insertions(+), 248 deletions(-) create mode 100644 docs/_static/versioning-checks.svg diff --git a/docs/_static/versioning-checks.svg b/docs/_static/versioning-checks.svg new file mode 100644 index 00000000000..48c28c67e5c --- /dev/null +++ b/docs/_static/versioning-checks.svg @@ -0,0 +1,70 @@ + + + + Writers check permissions, while readers validate contracts. + + On writing, in-memory array IDs select serializer plugins. The plugins return wire IDs, + metadata, buffers, and children, which are recursively serialized. Edition checks reject any + forbidden output ID, including children's IDs, without retrying a different wire format. + On reading, stored wire IDs select registered deserializer plugins. An unknown ID causes an + error. The plugins validate metadata, buffers, data types, and children against each stored + contract. Invalid data causes an error. Successful decoding constructs the reader's in-memory + encodings, adapting structure while preserving values, data types, and nulls. These are logical + groups of checks, not an exact execution timeline. The enclosing file format must be supported, + serializers must be available, edition enforcement is enabled, and unknown IDs are rejected. + + + + + + + + + Writers check permissions, while readers validate contracts. + Edition checks are enabled, unknown IDs are rejected, and serializers and file support are available. + WRITE + Memory + to storage + READ + Storage + to memory + + + + + + + Select serializer plugins + Use each array's in-memory ID. + Plugins choose wire formats. + Serialize each array and child + The plugin returns an ID, + metadata, buffers, and children. + Is every wire ID permitted? + Check the selected editions. + Yes: the output is permitted. + Find deserializer plugins + Use each stored wire ID. + Is the plugin registered? + Validate each contract + Do metadata, buffers, types, + and children satisfy that ID? + Construct encoded arrays + Adapt the structure as needed. + Keep values, types, and nulls. + No: the write fails without retrying. + No: the wire ID is unknown. + No: the data is invalid. + + + + + + + + Yes + Yes + + diff --git a/docs/_static/versioning-compatibility.svg b/docs/_static/versioning-compatibility.svg index cafb4d489f1..980ec5080f9 100644 --- a/docs/_static/versioning-compatibility.svg +++ b/docs/_static/versioning-compatibility.svg @@ -2,16 +2,16 @@ role="img" aria-labelledby="title description"> - How array plugins preserve compatibility across memory and wire formats + How array plugins convert between in-memory encodings and wire formats Writing starts with an in-memory array. Its ID selects a serializer plugin. If no plugin can serialize this representation, the write fails. Otherwise, the plugin chooses the oldest supported writable format that preserves the representation without recompression. The plugin returns a wire ID, metadata, buffers, and child arrays. It can adapt these components to the selected wire contract. Each returned child is serialized recursively. The writer checks - all returned IDs against the editions, - along with layouts, extension types, and stored aggregates. Forbidden output fails the write, - without retrying with a different permitted wire ID. After a successful write, the + all returned IDs against the editions, along with layouts, extension types, and stored + aggregates. If any ID is forbidden, the write fails without retrying a different permitted ID. + After a successful write, the reader must support the file format and have plugins for the stored wire IDs. Each plugin validates the stored ID's contract and constructs an in-memory array, adapting the structure when needed and reusing buffers where possible. Missing support or invalid serialized @@ -78,7 +78,7 @@ Write fails - No serializer or writable format. + No serializer or writable format exists. If another encoding is needed, recompress the array before @@ -86,7 +86,7 @@ Write fails: forbidden output - No retry with a different permitted ID. + The writer does not retry another ID. File: wire IDs and data @@ -115,7 +115,7 @@ Missing reader support - Unsupported file format or unknown ID. + The file format or an ID is unsupported. Reject invalid serialized data diff --git a/docs/_static/versioning-flow.svg b/docs/_static/versioning-flow.svg index 0f3f753f74d..d3b26be8ffa 100644 --- a/docs/_static/versioning-flow.svg +++ b/docs/_static/versioning-flow.svg @@ -1,81 +1,72 @@ - - Library 2 uses one array implementation for Formats A and B + One decimal encoding, two wire contracts - In this illustrative example, Library 1 supports only Format A. Library 2 supports Formats A and B - through one in-memory array implementation. Format A was introduced first. The serializer selects - Format A's wire ID when that format can represent the array without recompression. - Otherwise, Format B's wire ID is needed. The plugin can adapt metadata, buffers, or children. - Edition checks validate the selected wire ID and serialized children. They do not choose the - format. On reading, the stored wire ID selects a deserializer, which must validate that ID's - contract. Library 2 decodes both formats into its own array implementation, adapting the stored - structure when needed. It does not need a separate type for Format A. Values, data types, and - nulls are preserved. Library 1 can read Format A when it supports all other - components the file uses. + DecimalBytePartsArray holds either one signed integer child or a signed most-significant child + followed by up to three unsigned lower-part children. The plugin serializes the single-child + shape using vortex.decimal_byte_parts, called v1 here, and the multi-child shape using + vortex.decimal_byte_parts.v2. Both wire contracts deserialize into DecimalBytePartsArray. + v1 metadata records the signed child's type and a lower-part count of zero. v2 metadata records + the signed child's type and the ordered types of zero to three lower parts. Thus v2 also accepts + one child, although the serializer selects v1 for that shape. Both contracts require a decimal + data type and have no buffers of their own. Each child has the same length as the parent, and the + signed child carries the parent's nullability, while lower parts are non-nullable. The arrows show serialization + choices and the corresponding reads, not every valid v2 input. Values, data types, and nulls + are preserved. Frozen wire contracts are immutable. Edition permissions are checked separately, + and no declared edition currently permits v2. - + - - - Library 2 uses one array implementation for both wire formats - Illustrative formats with distinct wire IDs. Format A was introduced before Format B. - - LIBRARY 2 WRITER - WIRE FORMAT - LIBRARY 2 READER - - - - - - - - serialize - serialize - deserialize - deserialize - - - In-memory array - The array's ID selects the plugin. - Choose the oldest writable format - that needs no recompression. - Adapt metadata, buffers, - or children to that contract. - - - Format A - Represents the array without - recompression. - Wire ID, metadata, buffers, children - - - Format B - Required when Format A cannot - preserve the representation. - Wire ID, metadata, buffers, children - - - In-memory array - The wire ID selects the plugin - and the contract it validates. - Adapt the structure if needed. - Both formats can use the same - in-memory representation. - - - Edition permissions - Check the selected IDs and children. - - Library 1 supports Format A only. - Library 2 supports both formats. - Preserve values, types, and nulls. - No separate array type for Format A. + + + One decimal encoding, two wire contracts + The plugin converts between an array in memory and its serialized metadata and children. + IN MEMORY + ON WIRE + + DecimalBytePartsArray + + + + + + + + Decimals + Signed child + Decimals + Signed child + Unsigned child + One child + Multiple children (two shown) + + + + + serialize + deserialize + serialize + deserialize + + v1: exactly one signed child + vortex.decimal_byte_parts + Metadata: child type, lower-part count = 0 + Children: [signed] + + v2: signed child + 0 to 3 unsigned children + vortex.decimal_byte_parts.v2 + Metadata: signed type, ordered lower-part types + Children: [signed, lower parts, most significant first] + Although v2 accepts one child, the serializer chooses v1 when there are no lower parts. + Both contracts require a decimal dtype and have no own buffers. Each child has the same length as the parent. + The signed child carries the parent's nullability, whereas lower parts are non-nullable. + Conversions preserve values, types, and nulls. Frozen wire contracts remain fixed. + Edition permissions are checked separately, and no declared edition currently permits v2. diff --git a/docs/_theme/vortex/vortex.css b/docs/_theme/vortex/vortex.css index a70c4dbc268..0b0d9c92d22 100644 --- a/docs/_theme/vortex/vortex.css +++ b/docs/_theme/vortex/vortex.css @@ -252,6 +252,10 @@ table.docutils th, table.docutils td { } table.docutils th { background: var(--bg-alt); } +table.versioning-terms p { margin: 0; } +table.versioning-terms td:first-child { white-space: nowrap; } +figure.versioning-diagram { margin-inline: 0; } + .page-index .content > section > h1 { position: absolute; width: 1px; height: 1px; diff --git a/docs/specs/versioning.md b/docs/specs/versioning.md index 6038673e801..9e8e9a1f5ab 100644 --- a/docs/specs/versioning.md +++ b/docs/specs/versioning.md @@ -1,7 +1,7 @@ # Versioning and compatibility -This page is a high-level overview of Vortex's file compatibility guarantees. Read it alongside the -companion pages for the configuration details and the reasoning behind the design: +Vortex's file compatibility guarantees let applications upgrade their readers and writers +independently. The companion pages explain the configuration and the reasoning behind the design: - [Using editions](versioning/using-editions.md) explains how to configure writers for older readers and diagnose compatibility errors. @@ -12,15 +12,15 @@ companion pages for the configuration details and the reasoning behind the desig - [Edition registry](versioning/editions.md) lists the permitted wire IDs and minimum versions for each edition. -An array's _encoding_ describes its in-memory representation. Its _wire format_ defines how to -interpret its serialized metadata, buffers, and children. A _wire ID_ identifies that contract. An -array plugin maps between the two representations, so a reader can reconstruct the data using a +An array's _encoding_ describes its in-memory representation, whereas its _wire format_ defines how +to interpret its serialized metadata, buffers, and children. A _wire ID_ identifies that contract. +An array plugin maps between the two representations, so a reader can reconstruct the data using a different in-memory encoding from the writer's. Vortex guarantees **backward compatibility** for its frozen wire formats: newer library versions -retain the code needed to decode them. Applications can upgrade their readers without rewriting -existing files. Writers can also target wire formats that older readers understand, so the -applications producing and consuming files do not need to upgrade together. +retain the code needed to decode them. As a result, applications can upgrade their readers without +rewriting existing files. Writers targeting older readers, however, must use wire formats those +readers understand. An _edition_ names the wire formats that a writer is allowed to use for arrays and [other serialized components](versioning/editions.md#component-checks). Once an edition is @@ -28,7 +28,7 @@ An _edition_ names the wire formats that a writer is allowed to use for arrays a fixed. The first frozen edition is `core2025.05.0`, supported from version `0.36.0` of the Vortex Rust library. -This guarantee concerns file compatibility. The library's programming interfaces follow +This guarantee concerns file compatibility. The library's programming interfaces, however, follow [Rust's semantic versioning rules](https://doc.rust-lang.org/cargo/reference/semver.html), so an API change can require application changes even when existing files remain readable. @@ -44,7 +44,7 @@ edition's recorded minimum reader version is `0.84.0`, so the query engine meets requirement. With the required implementations registered, it can read valid files successfully written within that edition's restrictions. -Edition selection constrains the serialized output. The service still uses Vortex `0.85.0`'s +Edition selection constrains the serialized output, but the service still uses Vortex `0.85.0`'s compression code and in-memory array encodings. That code can choose different compression schemes or use different array data structures, provided each serialized component uses a permitted wire ID and obeys its contract. The query engine can then decode the output into the encodings supported by @@ -52,12 +52,10 @@ Vortex `0.84.0`. The [decimal example](versioning/design.md#example-decimal-chil in-memory encoding can serialize to two wire formats. If the service instead selects `core2026.08.3`, the edition permits additional wire IDs and records -a minimum of `0.85.0`. The older query engine is no longer guaranteed to read every file the service -can produce. It can still read a particular file if it has implementations for all the wire IDs that -file uses. - -The recorded minimum covers every wire ID in the edition, including IDs that a particular file does -not use. +a minimum of `0.85.0`, so the query engine running `0.84.0` is no longer guaranteed to read every +file the service can produce. However, it can still read a particular file if it has implementations +for all the wire IDs that file uses. The recorded minimum covers every wire ID in the edition, +including those that a particular file does not use. ## Compatibility requirements @@ -65,14 +63,14 @@ not use. implementations for all those IDs can read the output. The reader must also understand the enclosing [file format](file-format.md). -Meeting a [minimum library version](versioning/using-editions.md#reader-versions) is part of that -requirement. The application must also register the implementations for those wire IDs, including -any optional plugins. An independent plugin can have its own versions and compatibility policy. -Upgrading Vortex alone does not install it. +To rely on a frozen edition's guarantee, the application must meet its +[minimum library version](versioning/using-editions.md#reader-versions) and register the required +implementations, including any optional plugins. An independent plugin can have its own versions and +compatibility policy, so upgrading Vortex alone does not install it. -Edition selection does not guarantee that every input or custom writing strategy can produce a -permitted file. An array's encoding can require a wire representation that the edition forbids, or a -custom strategy can choose an unsupported layout. The +However, edition selection does not guarantee that every input or custom writing strategy can +produce a permitted file. An array's encoding can require a wire representation that the edition +forbids, or a custom strategy can choose an unsupported layout. The [write fails](versioning/using-editions.md#write-errors) when its serialized output violates the selection. @@ -86,8 +84,8 @@ the default output permissions. To keep serving older readers, explicitly select requirements those readers meet. Change that selection when the readers can support the additional wire formats. -Selecting an older edition restricts what the application writes. It does not restrict which wire -IDs the reader's registered plugins can decode. +Selecting an older edition restricts what the application writes, but it does not restrict which +wire IDs the reader's registered plugins can decode. ```{toctree} --- diff --git a/docs/specs/versioning/compatibility.md b/docs/specs/versioning/compatibility.md index bf70b0b2160..e3a6f67a796 100644 --- a/docs/specs/versioning/compatibility.md +++ b/docs/specs/versioning/compatibility.md @@ -1,17 +1,19 @@ # Compatibility matrix The matrix shows which wire formats each library can write, which the target edition permits, and -which each reader supports. The names are illustrative rather than actual Vortex versions or editions: +which each reader supports. The names are illustrative rather than actual Vortex versions or +editions: -- **Format A** and **Format B** have distinct wire IDs and contracts. Format A was introduced first. +- **Format A** and **Format B** have distinct wire IDs and contracts, with Format A introduced + first. - **Library 1** reads and writes only Format A. - **Library 2** reads and writes both formats through one array implementation. -- **Edition 1** permits only Format A. **Edition 2** permits both formats. +- **Edition 1** permits only Format A, whereas **Edition 2** permits both formats. -Both readers are assumed to support all other components in the file. **Unsupported** means the -writer has no implementation for that format. **Forbidden** means the target edition excludes it. -**Allowed** means both requirements are met, but the writer must still construct an array that the -format can represent. Reader results apply only after a successful write. +Both readers are assumed to support all other components in the file. **Unsupported** means that the +writer has no implementation for that format, whereas **Forbidden** means that the target edition +excludes it. **Allowed** means both requirements are met, but the writer must still construct an +array that the format can represent. Reader results apply only after a successful write. | Writer | Target edition | Format | Write result[^representation] | Library 1 reader | Library 2 reader | | -------------- | ---------------------------- | ------------- | ----------------------------- | ------------------------------------------- | --------------------- | @@ -24,10 +26,10 @@ format can represent. Reader results apply only after a successful write. | Library 2 | Edition 2 | Format A | Allowed[^compression] | Reads | Reads[^current-array] | | Library 2 | Edition 2 | Format B | Allowed[^compression] | [Unknown ID](using-editions.md#unknown-ids) | Reads | -The serializer [selects a format](design.md#format-selection) from the array's structure. The writer -then checks whether the target edition permits it. The format column shows that selection, not a -separate writer setting. Edition 2 permits both formats, so a writer targeting it can still produce -Format A for Library 1 to read. +The serializer [selects a format](design.md#format-selection) from the array's structure, after +which the writer checks whether the target edition permits it. The format column shows that +selection, not a separate writer setting. Edition 2 permits both formats, so a writer targeting it +can still produce Format A for Library 1 to read. ## Compatibility checks @@ -39,18 +41,18 @@ plugins. It assumes correct implementations, edition checks enabled, and full de :alt: Plugins select and validate wire formats while adapting arrays between memory and storage. :target: ../../_static/versioning-compatibility.svg -The writer selects a plugin by the array's in-memory ID. The reader selects a plugin by the stored -wire ID. Each plugin can adapt the array structure while preserving values, data types, and nulls. -The diagram groups related checks. In the implementation, component checks occur at several points -during writing and reading. +The writer selects a plugin by the array's in-memory ID, whereas the reader selects a plugin by the +stored wire ID. In either direction, the plugin can adapt the array structure while preserving +values, data types, and nulls. The diagram groups related checks, although in the implementation +component checks occur at several points during writing and reading. ``` -If the serializer returns a forbidden ID, the write fails. The writer does not retry with a -different permitted ID. When the target requires a different encoding, the array must be -recompressed before serialization. See [Format selection](design.md#format-selection). +If the serializer returns a forbidden ID, the write fails without retrying a different permitted ID. +When the target requires a different encoding, the array must be recompressed before serialization. +See [Format selection](design.md#format-selection). -The matrix assumes valid serialized data. The diagram also shows the reader rejecting data that -violates the stored ID's contract. Format compatibility does not prevent I/O errors. +The matrix assumes valid serialized data, whereas the diagram also shows the reader rejecting data +that violates the stored ID's contract. Format compatibility does not prevent I/O errors. The [component checks](editions.md#component-checks) cover arrays and their children, layouts, extension types, and stored aggregates. The reader needs implementations for the components the file @@ -62,8 +64,8 @@ For the compatibility guarantee and minimum reader versions, see [Versioning](.. [^representation]: An input array can require a format that the target edition forbids. A serializer can adapt - metadata, buffers, or children without recompression. If the target requires a different - encoding, the array must be recompressed before serialization or the write fails. + metadata, buffers, or children without recompression, but compatibility can require a different + encoding. In that case, the array must be recompressed before serialization or the write fails. [^current-array]: Library 2 reads Format A into its own array implementation, adapting the structure only if @@ -74,8 +76,8 @@ For the compatibility guarantee and minimum reader versions, see [Versioning](.. support for Format B, so Library 1 still writes only Format A. [^compression]: - The default compressor filters schemes by their declared output wire IDs. General per-writer - scheme configuration is not implemented. The planned configuration will select compatible - behavior before estimation, sampling, and full compression to avoid recompression solely to meet - the edition. These cells still require the writer to construct a permitted representation. See - [Compression](design.md#compression) for the current behavior and planned work. + The default compressor filters schemes by their declared output wire IDs, but general per-writer + scheme configuration is not implemented. The planned configuration would select compatible + behavior before estimation, sampling, and full compression, avoiding recompression solely to + meet the edition. These cells still require the writer to construct a permitted representation. + See [Compression](design.md#compression) for the current behavior and planned work. diff --git a/docs/specs/versioning/design.md b/docs/specs/versioning/design.md index 60e5c6e57ad..3ee3fee4bed 100644 --- a/docs/specs/versioning/design.md +++ b/docs/specs/versioning/design.md @@ -1,28 +1,51 @@ # Versioning design -Applications that share files can use different library versions and upgrade at different times. New -library releases need to improve compression and in-memory data structures while continuing to read -existing files. They also need a way to write files for applications that have not upgraded. +Applications that share files can use different library versions and upgrade at different times. As +new library releases improve compression and in-memory data structures, they must continue to read +existing files and write files for applications that have not upgraded. Vortex separates in-memory array encodings and compression code from the wire formats stored in -files. These implementations can change while retaining wire formats that existing readers -understand. The [versioning overview](../versioning.md) describes the compatibility guarantee. +files. As a result, these implementations can change while retaining wire formats that existing +readers understand. The [versioning overview](../versioning.md) describes the compatibility +guarantee. ## Versions and formats -A library release supplies code: array implementations, compression algorithms, readers, and -writers. An array's _encoding_ defines its in-memory representation. Its _wire format_ specifies how -to interpret serialized metadata, buffers, and children. A _wire ID_ identifies that contract, -including the supported data types. An edition groups wire IDs into a set of permitted serialized -representations. +An array's in-memory and serialized representations have separate contracts. The terms below +distinguish those representations, the code that converts between them, and the permissions that +control what a writer can produce: + +```{list-table} +:header-rows: 1 +:class: versioning-terms + +* - Term + - Meaning +* - **Encoding** + - An array's in-memory representation, including its buffers and child arrays. +* - **Wire format** + - The contract for interpreting serialized data: valid data types, metadata, buffers, children, + and their meaning. +* - **Wire ID** + - The identifier for a wire contract. Once frozen, that contract cannot change under the same ID. +* - **Array plugin** + - Code that serializes an in-memory encoding and deserializes one or more wire formats into + arrays. +* - **Edition** + - A named set of permitted component wire IDs, against which writers check their output. +* - **Library version** + - A release of the implementations: encodings, plugins, compression algorithms, readers, and + writers. +``` For example, a newer library can improve how it compresses a dictionary's values while keeping the -same dictionary wire format. It can also change its internal array fields while retaining code to -read old files. Only a change to what a reader must understand requires a new serialized contract. +same dictionary wire format. Similarly, it can change its internal array fields while retaining code +to read old files. Only a change to what a reader must understand requires a new serialized +contract. -The file container has a separate [version tag](../file-format.md#file-specification). It describes -the enclosing format. Component wire IDs describe the arrays and other structures within that -container, so those components can evolve independently. +The file container has a separate [version tag](../file-format.md#file-specification) that describes +the enclosing format. Component wire IDs, on the other hand, describe the arrays and other +structures within that container, so those components can evolve independently. ## Serialization @@ -32,80 +55,89 @@ and nulls. An array can contain buffers and other arrays, called _children_. A d example, has a child for its values and another for the codes that refer to those values. Each child can use its own encoding. -An _array plugin_ supplies serialization and deserialization for an in-memory array encoding. -Its serializer returns a wire ID, metadata, buffers, and children. The writer checks that ID and the -serialized children against the selected editions. A reader uses the IDs in the file to find the -registered plugins that decode the stored data into in-memory encodings. A missing implementation -causes an [unknown-ID error](using-editions.md#unknown-ids). - -The following example uses two wire formats with distinct IDs and contracts. Library 1 supports only -Format A. Library 2 adds support for Format B and retains support for Format A, using one in-memory -array implementation for both. The [compatibility matrix](compatibility.md) uses the same names. - -```{figure} ../../_static/versioning-flow.svg -:alt: Library 2 serializes and deserializes Formats A and B through one in-memory array implementation. -:target: ../../_static/versioning-flow.svg - -Library 2 writes Format A when it can represent the array without recompression. Otherwise, it needs -Format B. The plugin can adapt metadata, buffers, or children during serialization and -deserialization. Both formats decode into Library 2's array implementation, so it does not need a -separate type for Format A. -``` - -The writer checks the selected wire ID and every serialized child against the target editions. These -checks also cover layouts, extension types, and stored aggregates. +The array's in-memory ID selects its serializer, which returns a wire ID, metadata, buffers, and +children. Those children are then serialized through their own plugins. Conversely, the reader uses +each stored wire ID to select a deserializer and the contract it must validate. Since a plugin can +adapt metadata, buffers, or children in either direction, the in-memory and serialized structures +need not be identical. However, both conversions must preserve values, data types, and nulls. ## Example: decimal children The decimal-byte-parts encoding stores decimal values in integer child arrays. It can store each value in one child or split it across several children. One in-memory array type handles both -shapes, but the original wire contract permits only one child. Supporting additional children -therefore requires a new wire ID.[^decimal-availability] +shapes, but the v1 wire contract permits only one child. Supporting additional children therefore +requires a new wire ID.[^decimal-availability] + +```{figure} ../../_static/versioning-flow.svg +:alt: One decimal encoding holds either one signed child or a signed child with unsigned lower parts. The serializer chooses v1 for one child and v2 for multiple children. Both wire formats deserialize into the same array type. +:target: ../../_static/versioning-flow.svg +:figclass: versioning-diagram -| Array structure | Wire ID selected by the serializer | -| ------------------------ | ---------------------------------- | -| One signed integer child | `vortex.decimal_byte_parts` | -| Several integer children | `vortex.decimal_byte_parts.v2` | +The arrows show the serializer's choices and the corresponding reads. Although v2 also accepts a +single child, the serializer chooses v1 for that shape. Each child has its own wire ID because it is +itself a serialized array. +``` -For a single-child array, the serializer reuses the child and writes the original format's metadata -without recompression. An array with several children uses the second format, even if its values are -small enough to fit in one child. Combining those parts into one child requires re-encoding, which -the serializer does not perform. +For a single-child array, the serializer reuses the child and writes v1 metadata without +recompression. In contrast, an array with several children uses v2, even if its values are small +enough to fit in one child, because combining the parts requires re-encoding that the serializer +does not perform. The array type's in-memory ID is `vortex.decimal_byte_parts.v2` for both shapes. Since the serializer can return a different wire ID, the writer must check the returned ID against the target editions. -Both wire formats deserialize into the same in-memory array type. The reader must still validate the -contract identified by the stored ID: `vortex.decimal_byte_parts` requires exactly one signed -integer child. Support for multiple children under the second ID does not make them valid under the -first ID. +Both wire formats deserialize into the same in-memory array type. However, the reader must still +validate the contract identified by the stored ID: `vortex.decimal_byte_parts` requires exactly one +signed integer child. Support for multiple children under v2 does not make them valid under v1. ### Format selection A serializer must choose the oldest supported writable format that preserves the array's representation without recompression. A plugin can adapt metadata, buffers, or children to fit an -older format. Formats retained only for reading are not candidates for writing. +older format. Formats retained only for reading, however, are not candidates for writing. The serializer makes that choice before the writer checks edition permissions. If the chosen ID is -forbidden, the write fails. It does not retry a newer format because that format happens to be -permitted. In particular, a custom edition that permits only the newer decimal ID cannot write the -single-child array through this serializer, which selects the original ID. +forbidden, the write fails without retrying another format that happens to be permitted. In +particular, a custom edition that permits only the v2 decimal ID cannot write the single-child array +through this serializer, which selects v1. -This policy preserves older-reader compatibility when the existing representation allows it. If -compatibility requires a different encoding of the same values, that is a compression decision. The -write path must arrange that work explicitly or fail. +This policy preserves compatibility with readers of the earlier wire format when the existing +representation allows it. If compatibility requires a different encoding of the same values, the +write path must arrange recompression before serialization or fail. -Reading can also change the array structure. For example, the old ALP floating-point format stores +Reading can also change the array structure. For example, the ALP floating-point wire format stores exceptional values, called patches, inside the ALP array. With the experimental `Patched` encoding enabled, the reader moves those patches into a `Patched` parent around an ALP child without patches. The values stay the same even though the reader's array tree differs from the stored tree. +## Write and read checks + +Choosing a wire format and permitting it are separate steps. On writing, the selected editions +restrict which IDs the serializers may return, including those of every child. On reading, however, +edition selection does not restrict the input. Instead, the reader needs registered implementations +for the stored IDs, and those implementations must validate the corresponding contracts. + +```{figure} ../../_static/versioning-checks.svg +:alt: Writers select plugins by in-memory IDs, serialize arrays and children, and check all returned IDs against edition permissions. Readers select plugins by stored wire IDs, validate each contract, and construct in-memory arrays. Forbidden IDs, unknown IDs, and invalid data cause errors. +:target: ../../_static/versioning-checks.svg +:figclass: versioning-diagram + +The diagram groups related checks, which can occur at several points during writing and reading. +It assumes that edition enforcement is enabled, unknown IDs are rejected, and the enclosing file +format is supported. The [compatibility decision tree](compatibility.md#compatibility-checks) +includes serializer availability and the other conditions needed to read or write a file. +``` + +A missing reader implementation causes an [unknown-ID error](using-editions.md#unknown-ids), whereas +data that violates a known contract causes a validation error. Neither case becomes valid merely +because the reader supports another wire format for the same encoding. + ## Children and other components -Suppose the decimal serializer selects the original wire ID, but its integer child's serializer -returns an ID that the target edition forbids. Checking only the decimal ID accepts output that the -intended reader cannot decode. The writer must check every child recursively. +Suppose the decimal serializer selects the v1 wire ID, but its integer child's serializer returns an +ID that the target edition forbids. Checking only the decimal ID accepts output that the intended +reader cannot decode, so the writer must check every child recursively. The same requirement extends beyond arrays. A file also describes its layout, logical types, and stored summaries used for pruning. Editions cover each of these component kinds: @@ -123,7 +155,7 @@ meaning just as array formats do. The [component checks](editions.md#component-c writing rules for each kind. The kind and ID together identify a contract. For example, the array and layout named -`vortex.chunked` are separate components. Supporting one does not imply support for the other. +`vortex.chunked` are separate components, so supporting one does not imply support for the other. ## Editions @@ -135,10 +167,10 @@ to writers targeting older versions. An _edition family_ groups editions for related components. Membership is cumulative within a family: each later edition includes all earlier members. -The `core` family covers the default writer's formats. Optional features have independent families. -A writer can select one `core` edition and one `tensor` edition, for example, and use the union of -their permitted components. This avoids tying a change in an optional feature to a change in the -application's core target. The reader must satisfy both selections' requirements. +The `core` family covers the default writer's formats, while optional features have independent +families. A writer can select one `core` edition and one `tensor` edition, for example, and use the +union of their permitted components. This avoids tying a change in an optional feature to a change +in the application's core target. However, the reader must satisfy both selections' requirements. Each family names an _origin_, the project that supplies its implementations. A frozen edition's minimum version refers to that origin. For `core`, it is the Vortex Rust library. Independent @@ -146,30 +178,31 @@ plugins can use their own release numbers, so there is no single version compari every possible combination. Versions must meet the minimum for each origin, and the implementations must be registered in the reader. -An edition declaration supplies permissions, not implementations. Registering a later declaration -with an older library does not teach that library to read or write new formats. See +An edition declaration supplies permissions, so registering it does not add the implementations +needed to read or write its wire formats. See [Writer configuration](using-editions.md#writer-configuration) for how to register and select editions. -The [compatibility matrix](compatibility.md) shows the combinations of writer version, edition, -serialized format, and reader version. +The [compatibility matrix](compatibility.md) shows the combinations of writer version, edition, wire +format, and reader version. ## Compatibility invariants 1. **A frozen wire contract is immutable.** Its valid data types, metadata, buffers, children, - options, and meanings stay fixed. A reader-visible extension requires a new ID. + options, and meanings stay fixed, so a reader-visible extension requires a new ID. 2. **Compression, serialization, and reading preserve meaning.** Each serializer produces a valid - instance of its chosen contract. Each reader enforces that exact contract. All three operations - preserve values, data types, nulls, and the meaning of other components. + instance of its chosen contract, and each reader enforces that exact contract. All three + operations preserve values, data types, nulls, and the meaning of other components. 3. **Readers preserve backward compatibility.** Later implementations retain read support for frozen formats, including those writers no longer choose. Edition selection does not restrict what a reader can read. 4. **Frozen edition records are immutable.** Membership, origin, and recorded minimum stay fixed. - Membership is cumulative within each family. Selecting multiple families takes their union. + Later editions include all earlier members within their family, while selecting editions from + multiple families permits the union of their members. 5. **Edition enforcement covers the whole output.** Every serialized component must be permitted, - including children and nested dependencies. Compressor declarations alone are insufficient. + including children and nested dependencies, so compressor declarations alone are insufficient. 6. **Recorded reader requirements are sound.** Each recorded origin version supplies readers for - every member of the edition. Applications must register those implementations. + every member of the edition, but applications must register those implementations to use them. Together, these rules establish the relationship: @@ -195,14 +228,13 @@ recording the minimum version. ## Compression -The default BtrBlocks compressor filters schemes by their declared output wire IDs. A scheme is -excluded if any declared ID is forbidden. Schemes used for child compression go through the same -filtering. +The default BtrBlocks compressor excludes a scheme if any of its declared output wire IDs is +forbidden. This filtering also applies to schemes used for child compression. -The current decimal scheme produces only single-child arrays and declares the original wire ID. -Values too wide for it remain in the standard uncompressed decimal representation. The multi-child +The current decimal scheme produces only single-child arrays and declares the v1 wire ID, so values +too wide for it remain in the standard uncompressed decimal representation. The multi-child serializer exists, but the default scheme does not construct those arrays and no declared edition -permits their newer ID. +permits their v2 ID. ### Planned scheme configuration diff --git a/docs/specs/versioning/editions.md b/docs/specs/versioning/editions.md index ee3cbedd526..5666639e54d 100644 --- a/docs/specs/versioning/editions.md +++ b/docs/specs/versioning/editions.md @@ -1,7 +1,7 @@ # Edition registry -Each entry lists the components added by that edition. It also permits every component from earlier -editions in the same family. See [Using editions](using-editions.md) for configuration and +Each entry lists the components added by that edition, which also permits every component from +earlier editions in the same family. See [Using editions](using-editions.md) for configuration and [Versioning design](design.md) for the compatibility rules. ## Frozen `core` editions @@ -62,9 +62,10 @@ Minimum Vortex Rust crate version: `0.85.0`. ## Draft editions -These editions have no recorded minimum version of their origin project's code. New formats and -revisions get new draft editions. Vortex-maintained draft formats are expected to remain compatible -unless a defect blocks promotion into `core`. Independent plugin projects state their own policy. +Draft editions have no recorded minimum version of their origin project's code, and each new format +or revision requires a new draft edition. Vortex-maintained draft formats are expected to remain +compatible unless a defect blocks promotion into `core`, while independent plugin projects state +their own policy. ### `preview2026.08.0` @@ -104,12 +105,13 @@ four kinds of component: | Aggregate functions | Check every function stored in a zone map against the edition and its format contract. | A forbidden zone-map aggregate causes the write to fail. Silently omitting it would change which -filters can use the configured zone map to skip rows. An aggregate that does not apply to a column's -data type is different: the writer omits it, so there is no serialized component to check. +filters can use the configured zone map to skip rows. By contrast, the writer omits an aggregate +that does not apply to a column's data type, so there is no serialized component to check. For example, `core2026.08.0` declares `min`, `max`, `bounded_min`, `bounded_max`, `nan_count`, and `null_count`. Zone maps do not store sums, so the edition does not declare `sum`. File-level -statistics do store sums, in a fixed legacy field governed by the enclosing format's contract. +statistics, however, do store sums, in a fixed legacy field governed by the enclosing format's +contract. ## Format testing and promotion @@ -118,9 +120,9 @@ with no recorded `min_library_version` and no frozen compatibility guarantee. Ev stage, the format is expected to be complete. If testing reveals a defect whose correction changes what readers must understand, the correction needs a new wire ID and a later edition. -After initial testing, the format can enter a new `preview` edition for broader opt-in use. A later -`core` edition can include it for default use. Promotion preserves the format and its wire ID. Only -the editions that permit it change. The current `preview` edition is empty, so its first component +After initial testing, the format can enter a new `preview` edition for broader opt-in use, then a +later `core` edition for default use. Promotion changes which editions permit the format while +preserving its contract and wire ID. The current `preview` edition is empty, so its first component must go into a new edition. ## Freezing an edition @@ -129,9 +131,9 @@ A stable edition can freeze when its origin publishes the code that first suppor For `core`, this is a Vortex Rust crate release. Independent plugins use their own versions. Until the release version is known, the declaration uses `min_library_version: None`. Once it is -known, the field records that original release, usually while the next release is in development. -Filling in the field documents the freeze. The guarantee applies from the recorded release, even if -the declaration is updated later. +known, the field records the first release that supports all members, usually while the next release +is in development. Recording the version documents the freeze, but the guarantee applies from that +release even if the declaration is updated later. A frozen edition's membership, origin, and minimum version stay fixed. Deprecating a format can stop writers from choosing it, but readers must retain support because existing files can contain it. @@ -155,5 +157,5 @@ family. ``` CI's `check-editions` command rejects changes to frozen records, including renames, unfreezing, and -deletion. It also rejects a new edition that does not follow its family's chronology. Changes to -permitted formats require a later edition. +deletion, so changes to permitted wire formats require a later edition. The command also rejects a +new edition that does not follow its family's chronology. diff --git a/docs/specs/versioning/using-editions.md b/docs/specs/versioning/using-editions.md index 0d4c486fb9a..db08812895e 100644 --- a/docs/specs/versioning/using-editions.md +++ b/docs/specs/versioning/using-editions.md @@ -1,7 +1,7 @@ # Using editions -To write files for an older Vortex version, select editions whose formats that version supports. See -[Versioning](../versioning.md) for the compatibility guarantee. +To write files for an older Vortex version, select editions whose wire formats that version +supports. See [Versioning](../versioning.md) for the compatibility guarantee. ## Writer configuration @@ -30,16 +30,17 @@ Use the returned options' `write` method to write an array stream to an output. [Rust quickstart](../../getting-started/rust.rst) covers the input and I/O setup. The example uses file support from the `vortex` crate. -The default session registers the standard implementations and edition declarations. It currently -enables `core2026.08.3`. Calling `enable_edition` replaces the enabled edition from the same family. -Set the selection before starting the write, which captures the permitted formats at that point. +The default session registers the standard implementations and edition declarations, and it +currently enables `core2026.08.3`. Calling `enable_edition` replaces the enabled edition from the +same family. Set the selection before starting the write, because that is when the writer captures +the permitted wire IDs. An edition declaration describes permitted formats, but registering it does not install the code to read or write those formats. When constructing a session without the defaults, register the required implementations and declarations, then enable the target editions. See [Registering plugins](../../developer-guide/internals/session.md#registering-plugins) for the -registration API. Enabling an unregistered edition returns an error. A selection that permits no -components cannot serialize any edition-governed component. +registration API. Enabling an unregistered edition returns an error, while a selection that permits +no components prevents the writer from serializing any component governed by editions. ## Edition families @@ -49,11 +50,11 @@ can add formats without changing an application's `core` selection. A writer selects at most one edition per family. Selecting `core2026.08.0` and `tensor2026.04.0` permits every component in either edition. Within one family, a later edition includes all earlier -members. Across families, the selections are independent. +members. Across families, however, the selections are independent. Check the [registry](editions.md#edition-registry) before enabling an optional family. For example, `tensor2026.04.0` is a draft and has no frozen minimum reader version. Adding it does not extend -`core`'s frozen guarantee to the tensor formats. Both applications need the appropriate tensor +`core`'s frozen guarantee to the tensor formats. Both applications still need the appropriate tensor implementations. An edition name such as `core2026.08.3` contains its family, year, month, and a number @@ -67,9 +68,10 @@ For each selected frozen edition, find its recorded minimum version and its _ori `core` family's origin is `vortex`, so its `min_library_version` refers to the shared Vortex Rust crate version. An independent plugin can name a different origin with its own release numbers. -For editions with the same origin, use at least the highest recorded minimum. For different origins, -check each project separately. In both cases, register the implementations in the reader. A -sufficiently recent library without a required plugin is not enough. +For editions with the same origin, use at least the highest recorded minimum, whereas editions from +different origins require a separate version check for each project. In both cases, the reader must +register the required implementations, since meeting the version requirement alone does not make a +plugin available. ## Write errors @@ -77,8 +79,8 @@ The writer rejects forbidden formats in arrays, children, layouts, nested extens stored aggregate functions. Selecting an edition restricts the permitted output, but does not automatically reconfigure a custom -strategy or compressor. Those implementations must construct permitted representations themselves. -The default compressor's filtering is described in [Compression](design.md#compression). +strategy or compressor. Those implementations must therefore construct permitted representations +themselves. The default compressor's filtering is described in [Compression](design.md#compression). When a write fails because a format is forbidden, choose a permitted representation or strategy. Alternatively, select a later edition after confirming that the readers meet its requirements. @@ -86,9 +88,9 @@ Alternatively, select a later edition after confirming that the readers meet its a newer format even when its values appear suitable for an older one. For custom or experimental output, `VortexWriteOptions::disable_editions()` disables the array, -layout, extension-dtype, and aggregate checks. It does not register missing implementations. Files -written this way have no edition compatibility guarantee, so producers and consumers must agree on -the required implementations themselves. +layout, extension-dtype, and aggregate checks. However, it does not register missing +implementations. Files written this way have no edition compatibility guarantee, so producers and +consumers must agree on the required implementations themselves. ## Unknown IDs @@ -107,6 +109,6 @@ Inspection and copying tools can use `allow_unknown` to retain the serialized da arrays, layouts, and extension dtypes without interpreting it. However, retaining those objects does not make them available for ordinary computation. -With `allow_unknown`, an unknown aggregate disables pruning for the affected zone-map layout. -However, the layout's data remains readable if the reader supports the other required formats. -Without `allow_unknown`, the unknown aggregate causes an error. +With `allow_unknown`, an unknown aggregate disables pruning for the affected zone-map layout, but +the layout's data remains readable if the reader supports the other required wire formats. Without +`allow_unknown`, the unknown aggregate causes an error. From 1f81692c7464c60f79886d2e22fa6f801718fd33 Mon Sep 17 00:00:00 2001 From: Connor Tsui Date: Mon, 21 Sep 2026 16:44:16 -0400 Subject: [PATCH 12/12] array plugin semantics Signed-off-by: Connor Tsui --- docs/specs/versioning/design.md | 60 ++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/docs/specs/versioning/design.md b/docs/specs/versioning/design.md index 3ee3fee4bed..d2502458124 100644 --- a/docs/specs/versioning/design.md +++ b/docs/specs/versioning/design.md @@ -55,18 +55,50 @@ and nulls. An array can contain buffers and other arrays, called _children_. A d example, has a child for its values and another for the codes that refer to those values. Each child can use its own encoding. -The array's in-memory ID selects its serializer, which returns a wire ID, metadata, buffers, and -children. Those children are then serialized through their own plugins. Conversely, the reader uses -each stored wire ID to select a deserializer and the contract it must validate. Since a plugin can -adapt metadata, buffers, or children in either direction, the in-memory and serialized structures -need not be identical. However, both conversions must preserve values, data types, and nulls. +### Array plugin conversions + +An array plugin provides the code to serialize an in-memory array and read serialized data back into +an array. A plugin can support several wire formats. Support grows by adding wire IDs, while the +rules for frozen IDs stay fixed and readers retain the code needed to read them. + +When writing an array, Vortex finds its serializer using the array's in-memory encoding ID. The +serializer chooses a wire format and returns that format's wire ID, metadata, buffers, and child +arrays. It can construct different metadata, buffers, or children from those in the input array to +meet the chosen format's requirements. The library can therefore change its in-memory array data +structures while continuing to serialize data according to the same wire format. Each returned child +is then serialized through its own plugin. + +When reading, however, Vortex finds the deserializer using the wire ID stored in the file. As it +constructs an in-memory array, the deserializer must check that the metadata, buffers, data types, +and children satisfy the rules for that ID. It can arrange the buffers and children differently in +memory, or return an array whose encoding ID differs from the plugin's own in-memory ID. **A plugin +can therefore read several wire formats into the same in-memory array type.** + +These operations do not upgrade or downgrade the file. A reader can use an array implementation +added after the file was written, but the file's contents and wire IDs remain unchanged. Similarly, +a writer can use its current array implementation to serialize data in a wire format introduced by +an earlier library release. Neither operation requires a separate in-memory array type for each wire +format. Both must preserve values, data types, and nulls, although the array's structure in memory +can change when it is serialized and read back. + +An application can opt out of a particular conversion during reading by +[registering another plugin for the same wire ID](../../developer-guide/internals/session.md#registering-plugins). +That plugin must read the same serialized data correctly, but it can construct a different encoding +in memory. This gives the application control over how the data is represented without changing the +file or the rules for interpreting it. + +For example, the ALP wire format stores exceptional values, called patches, inside the ALP array. +With experimental `Patched` support enabled, the registered plugin constructs a `Patched` parent +that holds those patches and an ALP child that has none. In contrast, the standard ALP plugin keeps +the patches inside the ALP array in memory. Registering the standard plugin opts out of that change +to the array structure, while still reading the same stored data under the same wire ID. ## Example: decimal children -The decimal-byte-parts encoding stores decimal values in integer child arrays. It can store each -value in one child or split it across several children. One in-memory array type handles both -shapes, but the v1 wire contract permits only one child. Supporting additional children therefore -requires a new wire ID.[^decimal-availability] +The decimal-byte-parts plugin can serialize the same in-memory array type using two wire formats. +Its encoding stores decimal values in integer child arrays, either in one child or split across +several children. The v1 wire contract permits only one child, while v2 adds support for multiple +children under a distinct wire ID.[^decimal-availability] ```{figure} ../../_static/versioning-flow.svg :alt: One decimal encoding holds either one signed child or a signed child with unsigned lower parts. The serializer chooses v1 for one child and v2 for multiple children. Both wire formats deserialize into the same array type. @@ -91,7 +123,7 @@ Both wire formats deserialize into the same in-memory array type. However, the r validate the contract identified by the stored ID: `vortex.decimal_byte_parts` requires exactly one signed integer child. Support for multiple children under v2 does not make them valid under v1. -### Format selection +## Format selection A serializer must choose the oldest supported writable format that preserves the array's representation without recompression. A plugin can adapt metadata, buffers, or children to fit an @@ -104,12 +136,8 @@ through this serializer, which selects v1. This policy preserves compatibility with readers of the earlier wire format when the existing representation allows it. If compatibility requires a different encoding of the same values, the -write path must arrange recompression before serialization or fail. - -Reading can also change the array structure. For example, the ALP floating-point wire format stores -exceptional values, called patches, inside the ALP array. With the experimental `Patched` encoding -enabled, the reader moves those patches into a `Patched` parent around an ALP child without patches. -The values stay the same even though the reader's array tree differs from the stored tree. +write path must arrange recompression before serialization or fail. Selecting an edition does not +perform that conversion automatically. ## Write and read checks