Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions encodings/zstd/src/editions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@ pub static DECLARATION: EditionDeclaration = EditionDeclaration {

#[cfg(test)]
mod tests {
use vortex_edition::EditionError;
use vortex_edition::EditionSessionExt;
use vortex_edition::test_harness::validate_edition;
use vortex_error::VortexResult;

use super::*;

#[test]
fn zstd_edition_is_valid() -> Result<(), EditionError> {
fn zstd_edition_is_valid() -> VortexResult<()> {
let session = vortex_array::array_session();
crate::initialize(&session);
validate_edition(&session.editions(), &ZSTD_2026_02)
Expand Down
18 changes: 4 additions & 14 deletions vortex-btrblocks/tests/golden.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ use vortex_edition::EditionSession;
use vortex_edition::EditionSessionExt;
use vortex_edition::declarations::core::CORE_2026_08_3;
use vortex_error::VortexResult;
use vortex_error::vortex_err;
use vortex_session::VortexSession;

static SESSION: LazyLock<VortexSession> = LazyLock::new(vortex_array::array_session);
Expand Down Expand Up @@ -404,20 +403,13 @@ fn without_onpair(builder: BtrBlocksCompressorBuilder) -> BtrBlocksCompressorBui
fn edition_session(editions: &[EditionId]) -> VortexResult<VortexSession> {
let session = vortex_array::array_session().with::<EditionSession>();
for family in EDITION_FAMILIES {
session
.editions()
.declare_family(family)
.map_err(|error| vortex_err!("{error}"))?;
session.editions().declare_family(family)?;
}
for declaration in EDITION_DECLARATIONS {
session
.register_edition(declaration)
.map_err(|error| vortex_err!("{error}"))?;
session.register_edition(declaration)?;
}
for edition in editions {
session
.enable_edition(*edition)
.map_err(|error| vortex_err!("{error}"))?;
session.enable_edition(*edition)?;
}
Ok(session)
}
Expand Down Expand Up @@ -471,9 +463,7 @@ fn golden_onpair() -> VortexResult<()> {
fn golden_compact() -> VortexResult<()> {
let session = edition_session(&[CORE_2026_08_3])?;
vortex_zstd::initialize(&session);
session
.enable_edition(vortex_zstd::editions::ZSTD_2026_02)
.map_err(|error| vortex_err!("{error}"))?;
session.enable_edition(vortex_zstd::editions::ZSTD_2026_02)?;
let compressor = compressor_for_session(
&session,
BtrBlocksCompressorBuilder::default().with_compact(),
Expand Down
1 change: 1 addition & 0 deletions vortex-edition/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,5 @@ workspace = true

[dependencies]
parking_lot = { workspace = true }
vortex-error = { workspace = true }
vortex-session = { workspace = true }
61 changes: 17 additions & 44 deletions vortex-edition/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ pub mod test_harness;
#[cfg(test)]
mod tests;

use std::error::Error;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;
Expand All @@ -48,6 +47,8 @@ pub use declarations::EDITION_FAMILIES;
pub use session::EditionSession;
pub use session::EditionSessionExt;
pub use session::EnabledEditions;
use vortex_error::VortexResult;
use vortex_error::vortex_bail;
use vortex_session::registry::Id;

/// The identifier of an edition, e.g. `core2026.07.0`.
Expand Down Expand Up @@ -92,21 +93,15 @@ impl EditionId {
/// and a month in 01-12. Checked for every declared edition by
/// [`EditionSession::validate`] and per edition by
/// [`test_harness::validate_edition`].
pub fn validate(&self) -> Result<(), EditionError> {
pub fn validate(&self) -> VortexResult<()> {
if self.family.is_empty() || !self.family.chars().all(|c| c.is_ascii_lowercase()) {
return Err(EditionError::new(format!(
"edition {self} must have a non-empty lowercase family, e.g. `core`"
)));
vortex_bail!("edition {self} must have a non-empty lowercase family, e.g. `core`");
}
if !(1000..=9999).contains(&self.year) {
return Err(EditionError::new(format!(
"edition {self} must have a four-digit year"
)));
vortex_bail!("edition {self} must have a four-digit year");
}
if !(1..=12).contains(&self.month) {
return Err(EditionError::new(format!(
"edition {self} must have a month in 01-12"
)));
vortex_bail!("edition {self} must have a month in 01-12");
}
Ok(())
}
Expand Down Expand Up @@ -143,24 +138,21 @@ pub struct EditionFamily {
impl EditionFamily {
/// Validate the family's form: a non-empty lowercase name, origin, and doc. Checked for every
/// declared family by [`EditionSession::validate`].
pub fn validate(&self) -> Result<(), EditionError> {
pub fn validate(&self) -> VortexResult<()> {
if self.name.is_empty() || !self.name.chars().all(|c| c.is_ascii_lowercase()) {
return Err(EditionError::new(format!(
vortex_bail!(
"edition family {:?} must have a non-empty lowercase name, e.g. `core`",
self.name
)));
);
}
if self.origin.trim().is_empty() {
return Err(EditionError::new(format!(
vortex_bail!(
"edition family {} must name its origin library or project",
self.name
)));
);
}
if self.doc.trim().is_empty() {
return Err(EditionError::new(format!(
"edition family {} must document what it is for",
self.name
)));
vortex_bail!("edition family {} must document what it is for", self.name);
}
Ok(())
}
Expand Down Expand Up @@ -367,7 +359,7 @@ impl EditionInclusion {
/// Validate the declaration's form: a lowercase `namespace.name` component id and, if
/// recorded, a well-formed `major.minor.patch` release. Checked for every declared
/// inclusion by [`EditionSession::validate`].
pub fn validate(&self) -> Result<(), EditionError> {
pub fn validate(&self) -> VortexResult<()> {
let id = self.component_id.as_str();
let well_formed = !id.starts_with('.')
&& !id.ends_with('.')
Expand All @@ -376,18 +368,18 @@ impl EditionInclusion {
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || "._-".contains(c));
if !well_formed {
return Err(EditionError::new(format!(
vortex_bail!(
"invalid {} id {id:?}: expected lowercase `namespace.name`, e.g. `vortex.alp`",
self.kind
)));
);
}
if let Some(release) = self.required_vortex_release
&& parse_release(release).is_none()
{
return Err(EditionError::new(format!(
vortex_bail!(
"{} {id} declares malformed required_vortex_release {release:?}",
self.kind
)));
);
}
Ok(())
}
Expand All @@ -401,22 +393,3 @@ pub(crate) fn parse_release(release: &str) -> Option<Vec<u64>> {
.collect::<Option<_>>()?;
(parts.len() == 3).then_some(parts)
}

/// Error raised when edition declarations are inconsistent.
#[derive(Debug)]
pub struct EditionError(String);

impl EditionError {
/// Create an error with the given message.
pub fn new(msg: impl Into<String>) -> Self {
Self(msg.into())
}
}

impl Display for EditionError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}

impl Error for EditionError {}
67 changes: 35 additions & 32 deletions vortex-edition/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ use std::collections::BTreeMap;
use std::sync::Arc;

use parking_lot::RwLock;
use vortex_error::VortexResult;
use vortex_error::vortex_bail;
use vortex_session::ArcSwapMap;
use vortex_session::SessionExt;
use vortex_session::SessionGuard;
Expand All @@ -17,7 +19,6 @@ use vortex_session::registry::Id;
use crate::ComponentKind;
use crate::Edition;
use crate::EditionDeclaration;
use crate::EditionError;
use crate::EditionFamily;
use crate::EditionId;
use crate::EditionInclusion;
Expand Down Expand Up @@ -87,7 +88,7 @@ impl EditionSession {

/// Declare an edition together with the members added at it. Each entry's membership
/// (`since`) is the declared edition; earlier entries are inherited and must not be restated.
pub fn declare(&self, declaration: &EditionDeclaration) -> Result<(), EditionError> {
pub fn declare(&self, declaration: &EditionDeclaration) -> VortexResult<()> {
self.declare_edition(declaration.edition)?;
for member in declaration.added {
self.declare_inclusion(EditionInclusion::new(
Expand All @@ -102,13 +103,10 @@ impl EditionSession {
/// Declare an edition family. Errors if a family with the same name is already
/// declared. Every family must be declared before [`EditionSession::validate`] will
/// accept editions belonging to it.
pub fn declare_family(&self, family: &EditionFamily) -> Result<(), EditionError> {
pub fn declare_family(&self, family: &EditionFamily) -> VortexResult<()> {
let mut inner = self.inner.write();
if inner.families.contains_key(family.name) {
return Err(EditionError::new(format!(
"duplicate edition family {}",
family.name
)));
vortex_bail!("duplicate edition family {}", family.name);
}
inner.families.insert(family.name.to_string(), *family);
Ok(())
Expand All @@ -125,11 +123,11 @@ impl EditionSession {
}

/// Declare an edition. Errors if an edition with the same id is already declared.
pub fn declare_edition(&self, edition: Edition) -> Result<(), EditionError> {
pub fn declare_edition(&self, edition: Edition) -> VortexResult<()> {
let mut inner = self.inner.write();
let key = edition.id.to_string();
if inner.editions.contains_key(&key) {
return Err(EditionError::new(format!("duplicate edition {key}")));
vortex_bail!("duplicate edition {key}");
}
inner.editions.insert(key, edition);
Ok(())
Expand All @@ -138,7 +136,7 @@ impl EditionSession {
/// Declare an edition inclusion. A component may belong to multiple families but joins each
/// family only once. A newer wire representation uses a new component ID. Kind is part of the
/// key, so an array encoding and a layout may share an id.
pub fn declare_inclusion(&self, inclusion: EditionInclusion) -> Result<(), EditionError> {
pub fn declare_inclusion(&self, inclusion: EditionInclusion) -> VortexResult<()> {
let mut inner = self.inner.write();
let by_id = inner.inclusions.entry(inclusion.kind).or_default();
let history = by_id.entry(inclusion.component_id).or_default();
Expand All @@ -154,10 +152,13 @@ impl EditionSession {
});

if let Some(previous) = previous {
return Err(EditionError::new(format!(
vortex_bail!(
"{} {} already joined family {} in edition {}",
inclusion.kind, inclusion.component_id, inclusion.since.family, previous.since,
)));
inclusion.kind,
inclusion.component_id,
inclusion.since.family,
previous.since,
);
}
history.push(inclusion);
history.sort_by_key(|entry| {
Expand Down Expand Up @@ -222,7 +223,7 @@ impl EditionSession {
/// inclusions referencing undeclared editions, editions out of chronological order within
/// a family (unversioned drafts must be newest), malformed version strings, and members
/// requiring a release newer than their edition declares.
pub fn validate(&self) -> Result<(), EditionError> {
pub fn validate(&self) -> VortexResult<()> {
let editions = self.editions();

for family in self.families() {
Expand All @@ -232,19 +233,20 @@ impl EditionSession {
for edition in &editions {
edition.id.validate()?;
if self.find_family(edition.id.family).is_none() {
return Err(EditionError::new(format!(
vortex_bail!(
"edition {} belongs to undeclared family {}; declare the family before \
its editions",
edition.id, edition.id.family,
)));
edition.id,
edition.id.family,
);
}
if let Some(version) = edition.min_library_version
&& parse_release(version).is_none()
{
return Err(EditionError::new(format!(
vortex_bail!(
"edition {} declares malformed min_library_version {version:?}",
edition.id
)));
);
}
}

Expand All @@ -253,10 +255,11 @@ impl EditionSession {
for pair in editions.windows(2) {
let (prev, next) = (&pair[0], &pair[1]);
if prev.id.family == next.id.family && prev.is_draft() && !next.is_draft() {
return Err(EditionError::new(format!(
vortex_bail!(
"frozen edition {} follows draft {}; drafts must be newest in a family",
next.id, prev.id,
)));
next.id,
prev.id,
);
}
}

Expand All @@ -270,24 +273,26 @@ impl EditionSession {
inclusion.validate()?;

let Some(edition) = inner.editions.get(&inclusion.since.to_string()) else {
return Err(EditionError::new(format!(
vortex_bail!(
"{} {} is included in undeclared edition {}",
inclusion.kind, inclusion.component_id, inclusion.since
)));
inclusion.kind,
inclusion.component_id,
inclusion.since
);
};

if let Some(required) = inclusion.required_vortex_release.and_then(parse_release)
&& let Some(declared) = edition.min_library_version.and_then(parse_release)
&& required > declared
{
return Err(EditionError::new(format!(
vortex_bail!(
"{} {} requires release {}, newer than edition {}'s declared \
min_library_version",
inclusion.kind,
inclusion.component_id,
inclusion.required_vortex_release.unwrap_or_default(),
edition.id,
)));
);
}
}

Expand Down Expand Up @@ -331,7 +336,7 @@ pub trait EditionSessionExt: SessionExt {
}

/// Register an edition declaration with this session.
fn register_edition(&self, declaration: &EditionDeclaration) -> Result<(), EditionError> {
fn register_edition(&self, declaration: &EditionDeclaration) -> VortexResult<()> {
self.editions().declare(declaration)
}

Expand All @@ -340,11 +345,9 @@ pub trait EditionSessionExt: SessionExt {
/// Enabling an edition replaces the enabled edition from the same family. An edition
/// must be registered first so a typo or unavailable third-party declaration cannot
/// silently produce an empty writable set.
fn enable_edition(&self, edition: EditionId) -> Result<(), EditionError> {
fn enable_edition(&self, edition: EditionId) -> VortexResult<()> {
if self.editions().find(&edition).is_none() {
return Err(EditionError::new(format!(
"cannot enable unregistered edition {edition}"
)));
vortex_bail!("cannot enable unregistered edition {edition}");
}
self.enabled_editions().enable(edition);
Ok(())
Expand Down
Loading
Loading