1use std::fmt::{Display, Formatter};
5
6use lance_core::deepsize::{Context, DeepSizeOf};
7use lance_core::{Error, Result};
8
9pub use lance_encoding::version::{
10 LEGACY_FORMAT_VERSION, LanceFileVersion, V2_FORMAT_2_0, V2_FORMAT_2_1, V2_FORMAT_2_2,
11 V2_FORMAT_2_3,
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum ConcreteFileVersion {
21 V1,
23 V2_0,
25 V2_1,
27 V2_2,
29 V2_3,
31}
32
33impl DeepSizeOf for ConcreteFileVersion {
34 fn deep_size_of_children(&self, _context: &mut Context) -> usize {
35 0
36 }
37}
38
39impl ConcreteFileVersion {
40 pub fn from_manifest_string(value: &str) -> Result<Self> {
45 match value {
46 LEGACY_FORMAT_VERSION => Ok(Self::V1),
47 V2_FORMAT_2_0 => Ok(Self::V2_0),
48 V2_FORMAT_2_1 => Ok(Self::V2_1),
49 V2_FORMAT_2_2 => Ok(Self::V2_2),
50 V2_FORMAT_2_3 => Ok(Self::V2_3),
51 _ => Err(unknown_version(value)),
52 }
53 }
54
55 pub const fn to_manifest_string(self) -> &'static str {
57 match self {
58 Self::V1 => LEGACY_FORMAT_VERSION,
59 Self::V2_0 => V2_FORMAT_2_0,
60 Self::V2_1 => V2_FORMAT_2_1,
61 Self::V2_2 => V2_FORMAT_2_2,
62 Self::V2_3 => V2_FORMAT_2_3,
63 }
64 }
65
66 pub fn from_data_file_numbers(major: u32, minor: u32) -> Result<Self> {
73 match (major, minor) {
74 (0, 0..=2) => Ok(Self::V1),
75 (0, 3) | (2, 0) => Ok(Self::V2_0),
76 (2, 1) => Ok(Self::V2_1),
77 (2, 2) => Ok(Self::V2_2),
78 (2, 3) => Ok(Self::V2_3),
79 _ => Err(unknown_version(format_args!("{}.{}", major, minor))),
80 }
81 }
82
83 pub const fn to_data_file_numbers(self) -> (u32, u32) {
85 match self {
86 Self::V1 => (0, 2),
87 Self::V2_0 => (2, 0),
88 Self::V2_1 => (2, 1),
89 Self::V2_2 => (2, 2),
90 Self::V2_3 => (2, 3),
91 }
92 }
93
94 pub fn from_footer_numbers(major: u16, minor: u16) -> Result<Self> {
99 match (major, minor) {
100 (0, 0..=2) => Ok(Self::V1),
101 (0, 3) | (2, 0) => Ok(Self::V2_0),
102 (2, 1) => Ok(Self::V2_1),
103 (2, 2) => Ok(Self::V2_2),
104 (2, 3) => Ok(Self::V2_3),
105 _ => Err(unknown_version(format_args!("{}.{}", major, minor))),
106 }
107 }
108
109 pub const fn to_standard_footer_numbers(self) -> (u16, u16) {
111 match self {
112 Self::V1 => (0, 2),
113 Self::V2_0 => (0, 3),
114 Self::V2_1 => (2, 1),
115 Self::V2_2 => (2, 2),
116 Self::V2_3 => (2, 3),
117 }
118 }
119
120 pub const fn to_embedded_footer_numbers(self) -> (u16, u16) {
122 match self {
123 Self::V1 => (0, 2),
124 Self::V2_0 => (2, 0),
125 Self::V2_1 => (2, 1),
126 Self::V2_2 => (2, 2),
127 Self::V2_3 => (2, 3),
128 }
129 }
130}
131
132impl Display for ConcreteFileVersion {
133 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
134 f.write_str(self.to_manifest_string())
135 }
136}
137
138impl From<ConcreteFileVersion> for LanceFileVersion {
139 fn from(value: ConcreteFileVersion) -> Self {
140 match value {
141 ConcreteFileVersion::V1 => Self::Legacy,
142 ConcreteFileVersion::V2_0 => Self::V2_0,
143 ConcreteFileVersion::V2_1 => Self::V2_1,
144 ConcreteFileVersion::V2_2 => Self::V2_2,
145 ConcreteFileVersion::V2_3 => Self::V2_3,
146 }
147 }
148}
149
150impl From<LanceFileVersion> for ConcreteFileVersion {
151 fn from(value: LanceFileVersion) -> Self {
152 match value.resolve() {
153 LanceFileVersion::Legacy => Self::V1,
154 LanceFileVersion::V2_0 => Self::V2_0,
155 LanceFileVersion::V2_1 => Self::V2_1,
156 LanceFileVersion::V2_2 => Self::V2_2,
157 LanceFileVersion::V2_3 => Self::V2_3,
158 LanceFileVersion::Stable | LanceFileVersion::Next => {
159 unreachable!("resolved file-version selector must be exact")
160 }
161 }
162 }
163}
164
165fn unknown_version(value: impl Display) -> Error {
166 Error::invalid_input_source(format!("Unknown Lance storage version: {}", value).into())
167}
168
169#[cfg(test)]
170mod tests {
171 use std::str::FromStr;
172
173 use lance_io::object_store::ObjectStore;
174 use object_store::path::Path;
175
176 use super::*;
177
178 const EXACT_VERSIONS: [ConcreteFileVersion; 5] = [
179 ConcreteFileVersion::V1,
180 ConcreteFileVersion::V2_0,
181 ConcreteFileVersion::V2_1,
182 ConcreteFileVersion::V2_2,
183 ConcreteFileVersion::V2_3,
184 ];
185
186 #[test]
187 fn selector_resolution_is_exact() {
188 let cases = [
189 (LanceFileVersion::Legacy, ConcreteFileVersion::V1),
190 (LanceFileVersion::V2_0, ConcreteFileVersion::V2_0),
191 (LanceFileVersion::V2_1, ConcreteFileVersion::V2_1),
192 (LanceFileVersion::Stable, ConcreteFileVersion::V2_1),
193 (LanceFileVersion::V2_2, ConcreteFileVersion::V2_2),
194 (LanceFileVersion::Next, ConcreteFileVersion::V2_3),
195 (LanceFileVersion::V2_3, ConcreteFileVersion::V2_3),
196 ];
197
198 for (selector, expected) in cases {
199 assert_eq!(ConcreteFileVersion::from(selector), expected);
200 assert_eq!(LanceFileVersion::from(expected), selector.resolve());
201 }
202 }
203
204 #[test]
205 fn public_selector_aliases_remain_unchanged() {
206 let cases = [
207 ("0.1", LanceFileVersion::Legacy),
208 ("legacy", LanceFileVersion::Legacy),
209 ("2.0", LanceFileVersion::V2_0),
210 ("0.3", LanceFileVersion::V2_0),
211 ("2.1", LanceFileVersion::V2_1),
212 ("stable", LanceFileVersion::Stable),
213 ("2.2", LanceFileVersion::V2_2),
214 ("next", LanceFileVersion::Next),
215 ("2.3", LanceFileVersion::V2_3),
216 ];
217
218 for (value, expected) in cases {
219 assert_eq!(LanceFileVersion::from_str(value).unwrap(), expected);
220 }
221 }
222
223 #[test]
224 fn manifest_codec_only_accepts_canonical_exact_versions() {
225 for version in EXACT_VERSIONS {
226 let encoded = version.to_manifest_string();
227 assert_eq!(
228 ConcreteFileVersion::from_manifest_string(encoded).unwrap(),
229 version
230 );
231 }
232
233 for selector_or_alias in ["legacy", "0.3", "stable", "next"] {
234 assert!(ConcreteFileVersion::from_manifest_string(selector_or_alias).is_err());
235 }
236 }
237
238 #[test]
239 fn data_file_codec_preserves_wire_numbers() {
240 let cases = [
241 (ConcreteFileVersion::V1, (0, 2)),
242 (ConcreteFileVersion::V2_0, (2, 0)),
243 (ConcreteFileVersion::V2_1, (2, 1)),
244 (ConcreteFileVersion::V2_2, (2, 2)),
245 (ConcreteFileVersion::V2_3, (2, 3)),
246 ];
247
248 for (version, encoded) in cases {
249 assert_eq!(version.to_data_file_numbers(), encoded);
250 assert_eq!(
251 ConcreteFileVersion::from_data_file_numbers(encoded.0, encoded.1).unwrap(),
252 version
253 );
254 }
255 for minor in 0..=2 {
256 assert_eq!(
257 ConcreteFileVersion::from_data_file_numbers(0, minor).unwrap(),
258 ConcreteFileVersion::V1
259 );
260 }
261 assert_eq!(
262 ConcreteFileVersion::from_data_file_numbers(0, 3).unwrap(),
263 ConcreteFileVersion::V2_0
264 );
265 }
266
267 #[test]
268 fn footer_codec_preserves_both_v2_0_writer_representations() {
269 let standard_cases = [
270 (ConcreteFileVersion::V1, (0, 2)),
271 (ConcreteFileVersion::V2_0, (0, 3)),
272 (ConcreteFileVersion::V2_1, (2, 1)),
273 (ConcreteFileVersion::V2_2, (2, 2)),
274 (ConcreteFileVersion::V2_3, (2, 3)),
275 ];
276 let embedded_cases = [
277 (ConcreteFileVersion::V1, (0, 2)),
278 (ConcreteFileVersion::V2_0, (2, 0)),
279 (ConcreteFileVersion::V2_1, (2, 1)),
280 (ConcreteFileVersion::V2_2, (2, 2)),
281 (ConcreteFileVersion::V2_3, (2, 3)),
282 ];
283
284 for (version, encoded) in standard_cases {
285 assert_eq!(version.to_standard_footer_numbers(), encoded);
286 assert_eq!(
287 ConcreteFileVersion::from_footer_numbers(encoded.0, encoded.1).unwrap(),
288 version
289 );
290 }
291 for (version, encoded) in embedded_cases {
292 assert_eq!(version.to_embedded_footer_numbers(), encoded);
293 assert_eq!(
294 ConcreteFileVersion::from_footer_numbers(encoded.0, encoded.1).unwrap(),
295 version
296 );
297 }
298 for minor in 0..=2 {
299 assert_eq!(
300 ConcreteFileVersion::from_footer_numbers(0, minor).unwrap(),
301 ConcreteFileVersion::V1
302 );
303 }
304 }
305
306 #[tokio::test]
307 async fn file_version_detection_accepts_all_legacy_footer_aliases() {
308 let object_store = ObjectStore::memory();
309 for minor in 0u16..=2 {
310 let path = Path::from(format!("legacy-{minor}.lance"));
311 let mut footer = Vec::with_capacity(8);
312 footer.extend_from_slice(&0u16.to_le_bytes());
313 footer.extend_from_slice(&minor.to_le_bytes());
314 footer.extend_from_slice(crate::format::MAGIC);
315 object_store.put(&path, &footer).await.unwrap();
316
317 assert_eq!(
318 crate::determine_file_version(&object_store, &path, Some(footer.len()))
319 .await
320 .unwrap(),
321 LanceFileVersion::Legacy
322 );
323 }
324 }
325}