iceberg/catalog/
metadata_location.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::fmt::Display;
19use std::str::FromStr;
20
21use uuid::Uuid;
22
23use crate::{Error, ErrorKind, Result};
24
25/// Helper for parsing a location of the format: `<location>/metadata/<version>-<uuid>.metadata.json`
26#[derive(Clone, Debug, PartialEq)]
27pub struct MetadataLocation {
28    table_location: String,
29    version: i32,
30    id: Uuid,
31}
32
33impl MetadataLocation {
34    /// Creates a completely new metadata location starting at version 0.
35    /// Only used for creating a new table. For updates, see `with_next_version`.
36    pub fn new_with_table_location(table_location: impl ToString) -> Self {
37        Self {
38            table_location: table_location.to_string(),
39            version: 0,
40            id: Uuid::new_v4(),
41        }
42    }
43
44    /// Creates a new metadata location for an updated metadata file.
45    pub fn with_next_version(&self) -> Self {
46        Self {
47            table_location: self.table_location.clone(),
48            version: self.version + 1,
49            id: Uuid::new_v4(),
50        }
51    }
52
53    fn parse_metadata_path_prefix(path: &str) -> Result<String> {
54        let prefix = path.strip_suffix("/metadata").ok_or(Error::new(
55            ErrorKind::Unexpected,
56            format!(
57                "Metadata location not under \"/metadata\" subdirectory: {}",
58                path
59            ),
60        ))?;
61
62        Ok(prefix.to_string())
63    }
64
65    /// Parses a file name of the format `<version>-<uuid>.metadata.json`.
66    fn parse_file_name(file_name: &str) -> Result<(i32, Uuid)> {
67        let (version, id) = file_name
68            .strip_suffix(".metadata.json")
69            .ok_or(Error::new(
70                ErrorKind::Unexpected,
71                format!("Invalid metadata file ending: {}", file_name),
72            ))?
73            .split_once('-')
74            .ok_or(Error::new(
75                ErrorKind::Unexpected,
76                format!("Invalid metadata file name format: {}", file_name),
77            ))?;
78
79        Ok((version.parse::<i32>()?, Uuid::parse_str(id)?))
80    }
81}
82
83impl Display for MetadataLocation {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        write!(
86            f,
87            "{}/metadata/{:0>5}-{}.metadata.json",
88            self.table_location, self.version, self.id
89        )
90    }
91}
92
93impl FromStr for MetadataLocation {
94    type Err = Error;
95
96    fn from_str(s: &str) -> Result<Self> {
97        let (path, file_name) = s.rsplit_once('/').ok_or(Error::new(
98            ErrorKind::Unexpected,
99            format!("Invalid metadata location: {}", s),
100        ))?;
101
102        let prefix = Self::parse_metadata_path_prefix(path)?;
103        let (version, id) = Self::parse_file_name(file_name)?;
104
105        Ok(MetadataLocation {
106            table_location: prefix,
107            version,
108            id,
109        })
110    }
111}
112
113#[cfg(test)]
114mod test {
115    use std::str::FromStr;
116
117    use uuid::Uuid;
118
119    use crate::MetadataLocation;
120
121    #[test]
122    fn test_metadata_location_from_string() {
123        let test_cases = vec![
124            // No prefix
125            (
126                "/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
127                Ok(MetadataLocation {
128                    table_location: "".to_string(),
129                    version: 1234567,
130                    id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
131                }),
132            ),
133            // Some prefix
134            (
135                "/abc/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
136                Ok(MetadataLocation {
137                    table_location: "/abc".to_string(),
138                    version: 1234567,
139                    id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
140                }),
141            ),
142            // Longer prefix
143            (
144                "/abc/def/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
145                Ok(MetadataLocation {
146                    table_location: "/abc/def".to_string(),
147                    version: 1234567,
148                    id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
149                }),
150            ),
151            // Prefix with special characters
152            (
153                "https://127.0.0.1/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
154                Ok(MetadataLocation {
155                    table_location: "https://127.0.0.1".to_string(),
156                    version: 1234567,
157                    id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
158                }),
159            ),
160            // Another id
161            (
162                "/abc/metadata/1234567-81056704-ce5b-41c4-bb83-eb6408081af6.metadata.json",
163                Ok(MetadataLocation {
164                    table_location: "/abc".to_string(),
165                    version: 1234567,
166                    id: Uuid::from_str("81056704-ce5b-41c4-bb83-eb6408081af6").unwrap(),
167                }),
168            ),
169            // Version 0
170            (
171                "/abc/metadata/00000-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
172                Ok(MetadataLocation {
173                    table_location: "/abc".to_string(),
174                    version: 0,
175                    id: Uuid::from_str("2cd22b57-5127-4198-92ba-e4e67c79821b").unwrap(),
176                }),
177            ),
178            // Negative version
179            (
180                "/metadata/-123-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
181                Err("".to_string()),
182            ),
183            // Invalid uuid
184            (
185                "/metadata/1234567-no-valid-id.metadata.json",
186                Err("".to_string()),
187            ),
188            // Non-numeric version
189            (
190                "/metadata/noversion-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
191                Err("".to_string()),
192            ),
193            // No /metadata subdirectory
194            (
195                "/wrongsubdir/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
196                Err("".to_string()),
197            ),
198            // No .metadata.json suffix
199            (
200                "/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata",
201                Err("".to_string()),
202            ),
203            (
204                "/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.wrong.file",
205                Err("".to_string()),
206            ),
207        ];
208
209        for (input, expected) in test_cases {
210            match MetadataLocation::from_str(input) {
211                Ok(metadata_location) => {
212                    assert!(expected.is_ok());
213                    assert_eq!(metadata_location, expected.unwrap());
214                }
215                Err(_) => assert!(expected.is_err()),
216            }
217        }
218    }
219
220    #[test]
221    fn test_metadata_location_with_next_version() {
222        let test_cases = vec![
223            MetadataLocation::new_with_table_location("/abc"),
224            MetadataLocation::from_str(
225                "/abc/def/metadata/1234567-2cd22b57-5127-4198-92ba-e4e67c79821b.metadata.json",
226            )
227            .unwrap(),
228        ];
229
230        for input in test_cases {
231            let next = MetadataLocation::from_str(&input.to_string())
232                .unwrap()
233                .with_next_version();
234            assert_eq!(next.table_location, input.table_location);
235            assert_eq!(next.version, input.version + 1);
236            assert_ne!(next.id, input.id);
237        }
238    }
239}