1use std::{
5 fmt::{Display, Formatter},
6 str::FromStr,
7};
8
9use lance_core::deepsize::{Context, DeepSizeOf};
10use lance_core::{Error, Result};
11
12pub const LEGACY_FORMAT_VERSION: &str = "0.1";
13pub const V2_FORMAT_2_0: &str = "2.0";
14pub const V2_FORMAT_2_1: &str = "2.1";
15pub const V2_FORMAT_2_2: &str = "2.2";
16pub const V2_FORMAT_2_3: &str = "2.3";
17
18pub const fn stable_file_version() -> ConcreteFileVersion {
20 ConcreteFileVersion::V2_1
21}
22
23pub const fn next_file_version() -> ConcreteFileVersion {
25 ConcreteFileVersion::V2_3
26}
27
28#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
33pub enum LanceFileVersion {
34 Legacy,
36 V2_0,
38 #[default]
40 V2_1,
41 Stable,
43 V2_2,
45 Next,
47 V2_3,
49}
50
51impl DeepSizeOf for LanceFileVersion {
52 fn deep_size_of_children(&self, _context: &mut Context) -> usize {
53 0
54 }
55}
56
57impl LanceFileVersion {
58 pub const fn resolve(self) -> ConcreteFileVersion {
60 match self {
61 Self::Legacy => ConcreteFileVersion::V1,
62 Self::V2_0 => ConcreteFileVersion::V2_0,
63 Self::V2_1 => ConcreteFileVersion::V2_1,
64 Self::Stable => stable_file_version(),
65 Self::V2_2 => ConcreteFileVersion::V2_2,
66 Self::Next => next_file_version(),
67 Self::V2_3 => ConcreteFileVersion::V2_3,
68 }
69 }
70
71 pub const fn is_unstable(self) -> bool {
73 self.resolve().is_unstable()
74 }
75}
76
77impl Display for LanceFileVersion {
78 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
79 f.write_str(match self {
80 Self::Legacy => LEGACY_FORMAT_VERSION,
81 Self::V2_0 => V2_FORMAT_2_0,
82 Self::V2_1 => V2_FORMAT_2_1,
83 Self::V2_2 => V2_FORMAT_2_2,
84 Self::V2_3 => V2_FORMAT_2_3,
85 Self::Stable => "stable",
86 Self::Next => "next",
87 })
88 }
89}
90
91impl FromStr for LanceFileVersion {
92 type Err = Error;
93
94 fn from_str(value: &str) -> Result<Self> {
95 match value.to_lowercase().as_str() {
96 LEGACY_FORMAT_VERSION | "legacy" => Ok(Self::Legacy),
97 V2_FORMAT_2_0 | "0.3" => Ok(Self::V2_0),
98 V2_FORMAT_2_1 => Ok(Self::V2_1),
99 V2_FORMAT_2_2 => Ok(Self::V2_2),
100 V2_FORMAT_2_3 => Ok(Self::V2_3),
101 "stable" => Ok(Self::Stable),
102 "next" => Ok(Self::Next),
103 _ => Err(unknown_version(value)),
104 }
105 }
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub enum ConcreteFileVersion {
115 V1,
117 V2_0,
119 V2_1,
121 V2_2,
123 V2_3,
125}
126
127impl DeepSizeOf for ConcreteFileVersion {
128 fn deep_size_of_children(&self, _context: &mut Context) -> usize {
129 0
130 }
131}
132
133impl ConcreteFileVersion {
134 pub const fn to_selector(self) -> LanceFileVersion {
138 match self {
139 Self::V1 => LanceFileVersion::Legacy,
140 Self::V2_0 => LanceFileVersion::V2_0,
141 Self::V2_1 => LanceFileVersion::V2_1,
142 Self::V2_2 => LanceFileVersion::V2_2,
143 Self::V2_3 => LanceFileVersion::V2_3,
144 }
145 }
146
147 pub const fn is_unstable(self) -> bool {
149 matches!(self, Self::V2_3)
150 }
151
152 pub fn from_manifest_string(value: &str) -> Result<Self> {
157 match value {
158 LEGACY_FORMAT_VERSION => Ok(Self::V1),
159 V2_FORMAT_2_0 => Ok(Self::V2_0),
160 V2_FORMAT_2_1 => Ok(Self::V2_1),
161 V2_FORMAT_2_2 => Ok(Self::V2_2),
162 V2_FORMAT_2_3 => Ok(Self::V2_3),
163 _ => Err(unknown_version(value)),
164 }
165 }
166
167 pub const fn to_manifest_string(self) -> &'static str {
169 match self {
170 Self::V1 => LEGACY_FORMAT_VERSION,
171 Self::V2_0 => V2_FORMAT_2_0,
172 Self::V2_1 => V2_FORMAT_2_1,
173 Self::V2_2 => V2_FORMAT_2_2,
174 Self::V2_3 => V2_FORMAT_2_3,
175 }
176 }
177
178 pub fn from_data_file_numbers(major: u32, minor: u32) -> Result<Self> {
185 match (major, minor) {
186 (0, 0..=2) => Ok(Self::V1),
187 (0, 3) | (2, 0) => Ok(Self::V2_0),
188 (2, 1) => Ok(Self::V2_1),
189 (2, 2) => Ok(Self::V2_2),
190 (2, 3) => Ok(Self::V2_3),
191 _ => Err(unknown_version(format_args!("{}.{}", major, minor))),
192 }
193 }
194
195 pub const fn to_data_file_numbers(self) -> (u32, u32) {
197 match self {
198 Self::V1 => (0, 2),
199 Self::V2_0 => (2, 0),
200 Self::V2_1 => (2, 1),
201 Self::V2_2 => (2, 2),
202 Self::V2_3 => (2, 3),
203 }
204 }
205
206 pub fn from_footer_numbers(major: u16, minor: u16) -> Result<Self> {
211 match (major, minor) {
212 (0, 0..=2) => Ok(Self::V1),
213 (0, 3) | (2, 0) => Ok(Self::V2_0),
214 (2, 1) => Ok(Self::V2_1),
215 (2, 2) => Ok(Self::V2_2),
216 (2, 3) => Ok(Self::V2_3),
217 _ => Err(unknown_version(format_args!("{}.{}", major, minor))),
218 }
219 }
220
221 pub const fn to_standard_footer_numbers(self) -> (u16, u16) {
223 match self {
224 Self::V1 => (0, 2),
225 Self::V2_0 => (0, 3),
226 Self::V2_1 => (2, 1),
227 Self::V2_2 => (2, 2),
228 Self::V2_3 => (2, 3),
229 }
230 }
231
232 pub const fn to_embedded_footer_numbers(self) -> (u16, u16) {
234 match self {
235 Self::V1 => (0, 2),
236 Self::V2_0 => (2, 0),
237 Self::V2_1 => (2, 1),
238 Self::V2_2 => (2, 2),
239 Self::V2_3 => (2, 3),
240 }
241 }
242}
243
244impl Display for ConcreteFileVersion {
245 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
246 f.write_str(self.to_manifest_string())
247 }
248}
249
250fn unknown_version(value: impl Display) -> Error {
251 Error::invalid_input_source(format!("Unknown Lance storage version: {}", value).into())
252}
253
254#[cfg(test)]
255mod tests {
256 use std::str::FromStr;
257
258 use lance_io::object_store::ObjectStore;
259 use object_store::path::Path;
260
261 use super::*;
262
263 const EXACT_VERSIONS: [ConcreteFileVersion; 5] = [
264 ConcreteFileVersion::V1,
265 ConcreteFileVersion::V2_0,
266 ConcreteFileVersion::V2_1,
267 ConcreteFileVersion::V2_2,
268 ConcreteFileVersion::V2_3,
269 ];
270
271 #[test]
272 fn selector_resolution_is_exact() {
273 let cases = [
274 (LanceFileVersion::Legacy, ConcreteFileVersion::V1),
275 (LanceFileVersion::V2_0, ConcreteFileVersion::V2_0),
276 (LanceFileVersion::V2_1, ConcreteFileVersion::V2_1),
277 (LanceFileVersion::Stable, ConcreteFileVersion::V2_1),
278 (LanceFileVersion::V2_2, ConcreteFileVersion::V2_2),
279 (LanceFileVersion::Next, ConcreteFileVersion::V2_3),
280 (LanceFileVersion::V2_3, ConcreteFileVersion::V2_3),
281 ];
282
283 for (selector, expected) in cases {
284 assert_eq!(selector.resolve(), expected);
285 }
286 }
287
288 #[test]
289 fn public_selector_aliases_remain_unchanged() {
290 let cases = [
291 ("0.1", LanceFileVersion::Legacy),
292 ("legacy", LanceFileVersion::Legacy),
293 ("2.0", LanceFileVersion::V2_0),
294 ("0.3", LanceFileVersion::V2_0),
295 ("2.1", LanceFileVersion::V2_1),
296 ("stable", LanceFileVersion::Stable),
297 ("2.2", LanceFileVersion::V2_2),
298 ("next", LanceFileVersion::Next),
299 ("2.3", LanceFileVersion::V2_3),
300 ];
301
302 for (value, expected) in cases {
303 assert_eq!(LanceFileVersion::from_str(value).unwrap(), expected);
304 }
305 }
306
307 #[test]
308 fn manifest_codec_only_accepts_canonical_exact_versions() {
309 for version in EXACT_VERSIONS {
310 let encoded = version.to_manifest_string();
311 assert_eq!(
312 ConcreteFileVersion::from_manifest_string(encoded).unwrap(),
313 version
314 );
315 }
316
317 for selector_or_alias in ["legacy", "0.3", "stable", "next"] {
318 assert!(ConcreteFileVersion::from_manifest_string(selector_or_alias).is_err());
319 }
320 }
321
322 #[test]
323 fn data_file_codec_preserves_wire_numbers() {
324 let cases = [
325 (ConcreteFileVersion::V1, (0, 2)),
326 (ConcreteFileVersion::V2_0, (2, 0)),
327 (ConcreteFileVersion::V2_1, (2, 1)),
328 (ConcreteFileVersion::V2_2, (2, 2)),
329 (ConcreteFileVersion::V2_3, (2, 3)),
330 ];
331
332 for (version, encoded) in cases {
333 assert_eq!(version.to_data_file_numbers(), encoded);
334 assert_eq!(
335 ConcreteFileVersion::from_data_file_numbers(encoded.0, encoded.1).unwrap(),
336 version
337 );
338 }
339 for minor in 0..=2 {
340 assert_eq!(
341 ConcreteFileVersion::from_data_file_numbers(0, minor).unwrap(),
342 ConcreteFileVersion::V1
343 );
344 }
345 assert_eq!(
346 ConcreteFileVersion::from_data_file_numbers(0, 3).unwrap(),
347 ConcreteFileVersion::V2_0
348 );
349 }
350
351 #[test]
352 fn footer_codec_preserves_both_v2_0_writer_representations() {
353 let standard_cases = [
354 (ConcreteFileVersion::V1, (0, 2)),
355 (ConcreteFileVersion::V2_0, (0, 3)),
356 (ConcreteFileVersion::V2_1, (2, 1)),
357 (ConcreteFileVersion::V2_2, (2, 2)),
358 (ConcreteFileVersion::V2_3, (2, 3)),
359 ];
360 let embedded_cases = [
361 (ConcreteFileVersion::V1, (0, 2)),
362 (ConcreteFileVersion::V2_0, (2, 0)),
363 (ConcreteFileVersion::V2_1, (2, 1)),
364 (ConcreteFileVersion::V2_2, (2, 2)),
365 (ConcreteFileVersion::V2_3, (2, 3)),
366 ];
367
368 for (version, encoded) in standard_cases {
369 assert_eq!(version.to_standard_footer_numbers(), encoded);
370 assert_eq!(
371 ConcreteFileVersion::from_footer_numbers(encoded.0, encoded.1).unwrap(),
372 version
373 );
374 }
375 for (version, encoded) in embedded_cases {
376 assert_eq!(version.to_embedded_footer_numbers(), encoded);
377 assert_eq!(
378 ConcreteFileVersion::from_footer_numbers(encoded.0, encoded.1).unwrap(),
379 version
380 );
381 }
382 for minor in 0..=2 {
383 assert_eq!(
384 ConcreteFileVersion::from_footer_numbers(0, minor).unwrap(),
385 ConcreteFileVersion::V1
386 );
387 }
388 }
389
390 #[tokio::test]
391 async fn file_version_detection_accepts_all_legacy_footer_aliases() {
392 let object_store = ObjectStore::memory();
393 for minor in 0u16..=2 {
394 let path = Path::from(format!("legacy-{minor}.lance"));
395 let mut footer = Vec::with_capacity(8);
396 footer.extend_from_slice(&0u16.to_le_bytes());
397 footer.extend_from_slice(&minor.to_le_bytes());
398 footer.extend_from_slice(crate::format::MAGIC);
399 object_store.put(&path, &footer).await.unwrap();
400
401 assert_eq!(
402 crate::determine_file_version(&object_store, &path, Some(footer.len()))
403 .await
404 .unwrap(),
405 ConcreteFileVersion::V1
406 );
407 }
408 }
409}