Skip to content

Commit 528cd3f

Browse files
committed
feat(vortex-edition): session-registered editions validated by unit tests
Editions live on the session like encodings do: EditionSession is a session variable holding the per-session registry of editions and edition inclusions, populated explicitly at initialization time. EditionSession::default() seeds the first-party declarations — core2026.07.0 (a draft until its min_vortex_version is recorded, which is the act of freezing) and the 32 inclusions for the encodings the default writer emits — and any crate can declare further inclusions into a session, so declarations can migrate next to each encoding's registration. Declarations are plain constants (EditionId, Edition, EditionInclusion::new), with correctness enforced by unit tests rather than macros: EditionId::validate and EditionInclusion::validate check identifier and version forms, EditionSession::validate checks a whole registry (undeclared references, drafts-newest chronology, malformed versions, members requiring a release newer than their edition declares), and the validate!(CORE_2026_07_0) macro generates a unit test asserting an edition is well-formed, declared in the default session, and part of a valid registry. Everything else is computed: editions(), encodings_in(edition) (membership is inherited by later editions of the family), and current(family). The intended core2026.07.0 set is pinned by a golden test. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KdR42Svu74NcNzC7XJYwLr
1 parent 39f4abe commit 528cd3f

7 files changed

Lines changed: 876 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ members = [
88
"vortex-mask",
99
"vortex-utils",
1010
"vortex-session",
11+
"vortex-edition",
1112
"vortex-flatbuffers",
1213
"vortex-metrics",
1314
"vortex-io",
@@ -295,6 +296,7 @@ vortex-compute = { version = "0.1.0", path = "./vortex-compute", default-feature
295296
vortex-datafusion = { version = "0.1.0", path = "./vortex-datafusion", default-features = false }
296297
vortex-datetime-parts = { version = "0.1.0", path = "./encodings/datetime-parts", default-features = false }
297298
vortex-decimal-byte-parts = { version = "0.1.0", path = "encodings/decimal-byte-parts", default-features = false }
299+
vortex-edition = { version = "0.1.0", path = "./vortex-edition", default-features = false }
298300
vortex-error = { version = "0.1.0", path = "./vortex-error", default-features = false }
299301
vortex-fastlanes = { version = "0.1.0", path = "./encodings/fastlanes", default-features = false }
300302
vortex-file = { version = "0.1.0", path = "./vortex-file", default-features = false }

vortex-edition/Cargo.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[package]
2+
name = "vortex-edition"
3+
authors.workspace = true
4+
description = "Definitions of Vortex editions: named, frozen sets of encodings with a read-compatibility guarantee"
5+
edition = { workspace = true }
6+
homepage = { workspace = true }
7+
categories = { workspace = true }
8+
include = { workspace = true }
9+
keywords = { workspace = true }
10+
license = { workspace = true }
11+
readme = { workspace = true }
12+
repository = { workspace = true }
13+
rust-version = { workspace = true }
14+
version = { workspace = true }
15+
16+
[package.metadata.docs.rs]
17+
all-features = true
18+
19+
[lints]
20+
workspace = true
21+
22+
[dependencies]
23+
parking_lot = { workspace = true }
24+
vortex-session = { workspace = true }

vortex-edition/src/definitions.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! The first-party edition declarations, seeded into
5+
//! [`EditionSession::default`](crate::EditionSession).
6+
//!
7+
//! Each block declares an edition together with the encodings that join the family at it;
8+
//! members of earlier editions are inherited and never restated. Correctness is enforced by
9+
//! unit tests: [`validate!`](crate::validate) covers each declared edition, and the
10+
//! computed set of a frozen edition is pinned by a golden test, so any change to these
11+
//! declarations that alters a frozen set fails CI. New encodings are staged into the newest
12+
//! draft edition. Declarations live centrally here for now; they are expected to migrate
13+
//! next to each encoding's registration.
14+
15+
use crate::Edition;
16+
use crate::EditionDeclaration;
17+
use crate::EditionId;
18+
19+
/// The first edition of the `core` family: the canonical encodings — the uncompressed
20+
/// representations every logical type decodes to. A draft until the release shipping it is
21+
/// published and its version is recorded.
22+
pub const CORE_2026_01_0: EditionId = EditionId::new("core", 2026, 1, 0);
23+
24+
/// The second `core` edition: inherits everything in [`CORE_2026_01_0`] and adds the
25+
/// container and compressed encodings the default file writer emits. A draft until the
26+
/// release shipping it is published and its version is recorded.
27+
pub const CORE_2026_07_0: EditionId = EditionId::new("core", 2026, 7, 0);
28+
29+
/// The first-party editions, each declared together with the encodings it adds.
30+
pub static DEFAULT_DECLARATIONS: &[EditionDeclaration] = &[
31+
EditionDeclaration {
32+
edition: Edition {
33+
id: CORE_2026_01_0,
34+
// TODO(editions): freeze by setting this to the first release shipping this
35+
// edition, once it is published.
36+
min_vortex_version: None,
37+
},
38+
added: &[
39+
"vortex.null",
40+
"vortex.bool",
41+
"vortex.primitive",
42+
"vortex.decimal",
43+
"vortex.varbin",
44+
"vortex.varbinview",
45+
"vortex.list",
46+
"vortex.listview",
47+
"vortex.fixed_size_list",
48+
"vortex.struct",
49+
"vortex.variant",
50+
"vortex.ext",
51+
],
52+
},
53+
EditionDeclaration {
54+
edition: Edition {
55+
id: CORE_2026_07_0,
56+
min_vortex_version: None,
57+
},
58+
added: &[
59+
"vortex.chunked",
60+
"vortex.constant",
61+
"vortex.dict",
62+
"vortex.masked",
63+
"vortex.sparse",
64+
"vortex.alp",
65+
"vortex.alprd",
66+
"vortex.bytebool",
67+
"vortex.datetimeparts",
68+
"vortex.decimal_byte_parts",
69+
"vortex.fsst",
70+
"vortex.pco",
71+
"vortex.runend",
72+
"vortex.sequence",
73+
"vortex.zigzag",
74+
"vortex.zstd",
75+
"fastlanes.bitpacked",
76+
"fastlanes.delta",
77+
"fastlanes.for",
78+
"fastlanes.rle",
79+
],
80+
},
81+
];

vortex-edition/src/lib.rs

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// SPDX-FileCopyrightText: Copyright the Vortex contributors
3+
4+
//! Definitions of Vortex *editions*: named, frozen sets of encodings that a writer may put in
5+
//! a file, carrying a forever read-compatibility guarantee.
6+
//!
7+
//! Editions live on the session, like encodings do: [`EditionSession`] is the session
8+
//! variable holding the edition registry, populated at initialization time. Declarations
9+
//! are plain constants — an [`EditionId`] plus an [`Edition`] record, and one
10+
//! [`EditionInclusion`] per encoding stating that it is a member of an edition *and every
11+
//! later edition of the same family*. Any crate can register declarations into a session,
12+
//! so inclusions can live next to the encoding they describe.
13+
//!
14+
//! An edition is a **draft** until its [`Edition::min_vortex_version`] is recorded —
15+
//! recording it is the act of freezing. The per-edition encoding sets are computed from the
16+
//! registered declarations by [`EditionSession::encodings_in`], and correctness is enforced
17+
//! by unit tests: [`EditionSession::validate`] checks a whole registry, and
18+
//! [`validate!`](crate::validate) generates a test for a single edition.
19+
//!
20+
//! The first-party declarations live in [`definitions`] and seed
21+
//! [`EditionSession::default`]; see the published spec at
22+
//! <https://docs.vortex.dev/specs/editions.html>.
23+
24+
pub mod definitions;
25+
mod session;
26+
#[cfg(test)]
27+
mod tests;
28+
29+
use std::error::Error;
30+
use std::fmt;
31+
use std::fmt::Display;
32+
use std::fmt::Formatter;
33+
34+
pub use session::EditionSession;
35+
pub use session::EditionSessionExt;
36+
37+
/// The identifier of an edition, e.g. `core2026.07.0`.
38+
///
39+
/// The `family` names an independently versioned, additive group of encodings (`core` is the
40+
/// set the default writer emits). The date components record when the edition was frozen and
41+
/// order editions chronologically *within* a family; there is no ordering across families.
42+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43+
pub struct EditionId {
44+
/// The edition family, e.g. `core`.
45+
pub family: &'static str,
46+
/// Year the edition was cut.
47+
pub year: u16,
48+
/// Month the edition was cut.
49+
pub month: u8,
50+
/// Distinguishes editions cut in the same month; normally `0`.
51+
pub version: u8,
52+
}
53+
54+
impl EditionId {
55+
/// Create an edition identifier. Prefer declaring editions with [`edition!`](edition),
56+
/// which parses and validates the textual form at compile time.
57+
pub const fn new(family: &'static str, year: u16, month: u8, version: u8) -> Self {
58+
Self {
59+
family,
60+
year,
61+
month,
62+
version,
63+
}
64+
}
65+
66+
/// Returns true if `self` is the same edition as `other` or an earlier edition of the
67+
/// same family. Editions of different families are never ordered.
68+
pub fn is_at_or_before(&self, other: &EditionId) -> bool {
69+
self.family == other.family
70+
&& (self.year, self.month, self.version) <= (other.year, other.month, other.version)
71+
}
72+
73+
/// Validate the identifier's form: a non-empty lowercase family, a four-digit year,
74+
/// and a month in 01-12. Checked for every declared edition by
75+
/// [`EditionSession::validate`]; the [`validate!`](crate::validate) macro generates a
76+
/// unit test covering a single edition.
77+
pub fn validate(&self) -> Result<(), EditionError> {
78+
if self.family.is_empty() || !self.family.chars().all(|c| c.is_ascii_lowercase()) {
79+
return Err(EditionError::new(format!(
80+
"edition {self} must have a non-empty lowercase family, e.g. `core`"
81+
)));
82+
}
83+
if !(1000..=9999).contains(&self.year) {
84+
return Err(EditionError::new(format!(
85+
"edition {self} must have a four-digit year"
86+
)));
87+
}
88+
if !(1..=12).contains(&self.month) {
89+
return Err(EditionError::new(format!(
90+
"edition {self} must have a month in 01-12"
91+
)));
92+
}
93+
Ok(())
94+
}
95+
}
96+
97+
impl Display for EditionId {
98+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
99+
write!(
100+
f,
101+
"{}{}.{:02}.{}",
102+
self.family, self.year, self.month, self.version
103+
)
104+
}
105+
}
106+
107+
/// An edition: a named set of encodings with a read-compatibility guarantee, registered with
108+
/// [`EditionSession::declare_edition`]. The set itself is computed from the registered
109+
/// [`EditionInclusion`]s by [`EditionSession::encodings_in`].
110+
#[derive(Clone, Copy, Debug)]
111+
pub struct Edition {
112+
/// The edition identifier. Also carries the freeze date: `core2026.07.0` freezes in
113+
/// 2026-07.
114+
pub id: EditionId,
115+
/// The minimum Vortex version whose reader supports every encoding in this edition.
116+
///
117+
/// Recording this is the act of freezing: an edition with `None` is a **draft** — being
118+
/// assembled, carrying no guarantee, free to change, never the default write target.
119+
/// Validated against the members' [`EditionInclusion::required_vortex_release`] values:
120+
/// no member may require a version newer than the edition declares.
121+
pub min_vortex_version: Option<&'static str>,
122+
}
123+
124+
impl Edition {
125+
/// A draft is an edition whose `min_vortex_version` has not been recorded yet.
126+
pub fn is_draft(&self) -> bool {
127+
self.min_vortex_version.is_none()
128+
}
129+
}
130+
131+
/// Declares that an encoding is a member of an edition — and of every later edition of the
132+
/// same family. Registered with [`EditionSession::declare_inclusion`].
133+
#[derive(Clone, Copy, Debug)]
134+
pub struct EditionInclusion {
135+
/// The encoding ID, e.g. `vortex.alp`. Globally unique across everything an edition can
136+
/// cover: when layout encodings join editions, their IDs must be distinct from array
137+
/// encoding IDs.
138+
pub encoding_id: &'static str,
139+
/// The first edition this encoding is a member of.
140+
pub since: EditionId,
141+
/// The earliest Vortex release able to read and execute this encoding, recorded from
142+
/// evidence (e.g. compat-fixture history). `None` until recorded.
143+
pub required_vortex_release: Option<&'static str>,
144+
}
145+
146+
/// Declares an edition together with the encodings that join the family at it, in one
147+
/// block. Registered with [`EditionSession::declare`], which derives each encoding's
148+
/// membership (`since` = the declared edition) from the block structure.
149+
#[derive(Clone, Copy, Debug)]
150+
pub struct EditionDeclaration {
151+
/// The edition being declared.
152+
pub edition: Edition,
153+
/// The encoding ids that join the family at this edition. Members of earlier editions
154+
/// are inherited and never restated.
155+
pub added: &'static [&'static str],
156+
}
157+
158+
impl EditionInclusion {
159+
/// Declare that an encoding is a member of `since` and every later edition of the same
160+
/// family.
161+
pub const fn new(encoding_id: &'static str, since: EditionId) -> Self {
162+
Self {
163+
encoding_id,
164+
since,
165+
required_vortex_release: None,
166+
}
167+
}
168+
169+
/// Validate the declaration's form: a lowercase `namespace.name` encoding id and, if
170+
/// recorded, a well-formed `major.minor.patch` release. Checked for every declared
171+
/// inclusion by [`EditionSession::validate`].
172+
pub fn validate(&self) -> Result<(), EditionError> {
173+
let id = self.encoding_id;
174+
let well_formed = !id.starts_with('.')
175+
&& !id.ends_with('.')
176+
&& id.contains('.')
177+
&& id
178+
.chars()
179+
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || "._-".contains(c));
180+
if !well_formed {
181+
return Err(EditionError::new(format!(
182+
"invalid encoding id {id:?}: expected lowercase `namespace.name`, e.g. \
183+
`vortex.alp`"
184+
)));
185+
}
186+
if let Some(release) = self.required_vortex_release
187+
&& parse_release(release).is_none()
188+
{
189+
return Err(EditionError::new(format!(
190+
"encoding {id} declares malformed required_vortex_release {release:?}"
191+
)));
192+
}
193+
Ok(())
194+
}
195+
}
196+
197+
/// Generates a unit test asserting that the named edition is correctly declared: its
198+
/// identifier is well-formed, it is registered in the default session, and the default
199+
/// session's declarations as a whole validate.
200+
///
201+
/// ```ignore
202+
/// use vortex_edition::definitions::CORE_2026_07_0;
203+
///
204+
/// vortex_edition::validate!(CORE_2026_07_0);
205+
/// ```
206+
#[macro_export]
207+
macro_rules! validate {
208+
($edition:ident) => {
209+
#[expect(non_snake_case, reason = "test module named after the edition const")]
210+
#[cfg(test)]
211+
mod $edition {
212+
#[test]
213+
fn is_correctly_declared() {
214+
let id = super::$edition;
215+
id.validate().unwrap();
216+
217+
let editions = $crate::EditionSession::default();
218+
assert!(
219+
editions.find(&id).is_some(),
220+
"{id} is not declared in the default session",
221+
);
222+
editions.validate().unwrap();
223+
}
224+
}
225+
};
226+
}
227+
228+
/// Parse a `major.minor.patch` release string into a comparable key.
229+
pub(crate) fn parse_release(release: &str) -> Option<Vec<u64>> {
230+
let parts: Vec<u64> = release
231+
.split('.')
232+
.map(|part| part.parse::<u64>().ok())
233+
.collect::<Option<_>>()?;
234+
(parts.len() == 3).then_some(parts)
235+
}
236+
237+
/// Error raised when edition declarations are inconsistent.
238+
#[derive(Debug)]
239+
pub struct EditionError(String);
240+
241+
impl EditionError {
242+
pub(crate) fn new(msg: impl Into<String>) -> Self {
243+
Self(msg.into())
244+
}
245+
}
246+
247+
impl Display for EditionError {
248+
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
249+
f.write_str(&self.0)
250+
}
251+
}
252+
253+
impl Error for EditionError {}

0 commit comments

Comments
 (0)