Skip to content
Draft
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
2 changes: 2 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions vortex-edition/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ workspace = true
[dependencies]
parking_lot = { workspace = true }
vortex-session = { workspace = true }
vortex-utils = { workspace = true }
34 changes: 34 additions & 0 deletions vortex-edition/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use vortex_session::SessionExt;
use vortex_session::SessionGuard;
use vortex_session::SessionVar;
use vortex_session::registry::Id;
use vortex_utils::aliases::hash_set::HashSet;

use crate::Edition;
use crate::EditionDeclaration;
Expand Down Expand Up @@ -150,6 +151,17 @@ impl EditionSession {
.collect()
}

/// Every encoding id declared by a registered edition, in encoding id order, regardless of
/// which editions are enabled.
///
/// This is the set of encodings the edition system speaks for; anything outside it is
/// unknown to the registered declarations and therefore ungated by
/// [`EditionSessionExt::retain_writable_encodings`].
pub fn declared_encodings(&self) -> Vec<Id> {
// The map is keyed by encoding id, so the keys are already sorted by it.
self.inner.read().inclusions.keys().copied().collect()
}

/// Validate all registered declarations. Errors on 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
Expand Down Expand Up @@ -284,6 +296,28 @@ pub trait EditionSessionExt: SessionExt {
ids.dedup();
ids
}

/// Retain only the encodings this session may write, sorted by encoding id and deduplicated.
///
/// An encoding survives when an enabled edition includes it, or when no registered edition
/// declares it at all. The second case matters because the edition system only speaks for
/// the encodings its declarations describe: a session that registers no editions — or one
/// that registers an encoding of its own without declaring an inclusion for it — is not
/// gated. An encoding that *is* declared, in an edition the session has not enabled, is
/// removed: writing it would put an encoding in the file that the enabled editions'
/// read-compatibility guarantee does not cover.
fn retain_writable_encodings(&self, encodings: impl IntoIterator<Item = Id>) -> Vec<Id> {
let declared: HashSet<Id> = self.editions().declared_encodings().into_iter().collect();
let enabled: HashSet<Id> = self.enabled_encoding_ids().into_iter().collect();

let mut ids: Vec<Id> = encodings
.into_iter()
.filter(|id| enabled.contains(id) || !declared.contains(id))
.collect();
ids.sort_unstable();
ids.dedup();
ids
}
}

impl<S: SessionExt> EditionSessionExt for S {}
63 changes: 63 additions & 0 deletions vortex-edition/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-FileCopyrightText: Copyright the Vortex contributors

use vortex_session::VortexSession;
use vortex_session::registry::Id;

use crate::Edition;
use crate::EditionDeclaration;
Expand Down Expand Up @@ -274,3 +275,65 @@ fn edition_ids_order_within_family_only() {
fn edition_id_display() {
assert_eq!(FIRST.to_string(), "test2026.01.0");
}

#[test]
fn declared_encodings_covers_every_registered_edition() {
let editions = session();
let declared = editions.declared_encodings();
let declared: Vec<&str> = declared.iter().map(|id| id.as_str()).collect();
assert_eq!(declared, ["test.alpha", "test.beta", "test.gamma"]);
assert!(EditionSession::empty().declared_encodings().is_empty());
}

#[test]
fn retain_writable_encodings_gates_declared_but_disabled_encodings()
-> Result<(), crate::EditionError> {
#[expect(clippy::disallowed_methods, reason = "interning test encoding ids")]
fn ids(names: &[&str]) -> Vec<Id> {
names.iter().map(|name| Id::new(name)).collect()
}

let session = VortexSession::empty().with::<EditionSession>();
for declaration in DECLARATIONS {
session.register_edition(declaration)?;
}

let candidates = ids(&["test.alpha", "test.gamma", "test.undeclared"]);

// With no edition enabled, every declared encoding is gated out and undeclared ones survive.
let writable = session.retain_writable_encodings(candidates.clone());
assert_eq!(
writable.iter().map(|id| id.as_str()).collect::<Vec<_>>(),
["test.undeclared"]
);

// Enabling the first edition admits its members, but not those added by a later one.
session.enable_edition(FIRST)?;
let writable = session.retain_writable_encodings(candidates.clone());
assert_eq!(
writable.iter().map(|id| id.as_str()).collect::<Vec<_>>(),
["test.alpha", "test.undeclared"]
);

session.enable_edition(SECOND)?;
let writable = session.retain_writable_encodings(candidates);
assert_eq!(
writable.iter().map(|id| id.as_str()).collect::<Vec<_>>(),
["test.alpha", "test.gamma", "test.undeclared"]
);
Ok(())
}

#[test]
fn retain_writable_encodings_is_ungated_without_declarations() {
#[expect(clippy::disallowed_methods, reason = "interning test encoding ids")]
let candidates: Vec<Id> = ["test.alpha", "test.gamma"].map(Id::new).into();

// A session that registers no editions has nothing to gate against.
let session = VortexSession::empty().with::<EditionSession>();
assert_eq!(
session.retain_writable_encodings(candidates).len(),
2,
"a session with no declarations must not gate anything"
);
}
1 change: 1 addition & 0 deletions vortex-file/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ vortex-bytebool = { workspace = true }

vortex-datetime-parts = { workspace = true }
vortex-decimal-byte-parts = { workspace = true }
vortex-edition = { workspace = true }
vortex-error = { workspace = true }
vortex-fastlanes = { workspace = true }
vortex-flatbuffers = { workspace = true, features = ["file"] }
Expand Down
Loading
Loading