1use std::borrow::Cow;
3use std::ops::Deref;
4use std::path::PathBuf;
5use std::time::{Duration, SystemTime, UNIX_EPOCH};
6
7use delta_kernel_derive::internal_api;
8use url::Url;
9
10use crate::{DeltaResult, Error};
11
12macro_rules! require {
14 ( $cond:expr, $err:expr ) => {
15 if !($cond) {
16 return Err($err);
17 }
18 };
19}
20
21pub(crate) use require;
22
23pub trait CollectInto<T>: IntoIterator + Sized {
47 fn collect_into(self) -> T;
49}
50
51impl<I: IntoIterator, T: FromIterator<I::Item>> CollectInto<T> for I {
53 fn collect_into(self) -> T {
54 T::from_iter(self)
55 }
56}
57
58#[allow(unused)]
61#[internal_api]
62pub(crate) fn try_parse_uri(uri: impl AsRef<str>) -> DeltaResult<Url> {
63 let uri = uri.as_ref();
64 let uri_type = resolve_uri_type(uri)?;
65 let url = match uri_type {
66 UriType::LocalPath(path) => {
67 if !path.exists() {
68 return Err(Error::InvalidTableLocation(format!(
70 "Path does not exist: {path:?}"
71 )));
72 }
73 if !path.is_dir() {
74 return Err(Error::InvalidTableLocation(format!(
75 "{path:?} is not a directory"
76 )));
77 }
78 let path = std::fs::canonicalize(path).map_err(|err| {
79 let msg = format!("Invalid table location: {uri} Error: {err:?}");
80 Error::InvalidTableLocation(msg)
81 })?;
82 Url::from_directory_path(path.clone()).map_err(|_| {
83 let msg = format!(
84 "Could not construct a URL from canonicalized path: {path:?}.\n\
85 Something must be very wrong with the table path."
86 );
87 Error::InvalidTableLocation(msg)
88 })?
89 }
90 UriType::Url(url) => url,
91 };
92 Ok(url)
93}
94
95#[allow(unused)]
96#[derive(Debug)]
97enum UriType {
98 LocalPath(PathBuf),
99 Url(Url),
100}
101
102#[allow(unused)]
107fn resolve_uri_type(table_uri: impl AsRef<str>) -> DeltaResult<UriType> {
108 let table_uri = table_uri.as_ref();
109 let table_uri = if table_uri.ends_with('/') {
110 Cow::Borrowed(table_uri)
111 } else {
112 Cow::Owned(format!("{table_uri}/"))
113 };
114 if let Ok(url) = Url::parse(&table_uri) {
115 let scheme = url.scheme().to_string();
116 if url.scheme() == "file" {
117 Ok(UriType::LocalPath(
118 url.to_file_path()
119 .map_err(|_| Error::invalid_table_location(table_uri))?,
120 ))
121 } else if scheme.len() == 1 {
122 Ok(UriType::LocalPath(PathBuf::from(table_uri.as_ref())))
125 } else {
126 Ok(UriType::Url(url))
127 }
128 } else {
129 Ok(UriType::LocalPath(table_uri.deref().into()))
130 }
131}
132
133pub(crate) fn current_time_duration() -> DeltaResult<Duration> {
135 SystemTime::now()
136 .duration_since(UNIX_EPOCH)
137 .map_err(|e| Error::generic(format!("System time before Unix epoch: {e}")))
138}
139
140pub(crate) fn current_time_ms() -> DeltaResult<i64> {
142 let duration = current_time_duration()?;
143 i64::try_from(duration.as_millis())
144 .map_err(|_| Error::generic("Current timestamp exceeds i64 millisecond range"))
145}
146
147#[internal_api]
149pub(crate) trait FoldWithOption: Sized {
150 fn fold_with<U>(self, opt: Option<U>, f: impl FnOnce(Self, U) -> Self) -> Self {
156 match opt {
157 Some(value) => f(self, value),
158 None => self,
159 }
160 }
161
162 #[cfg_attr(not(feature = "internal-api"), allow(dead_code))]
165 fn try_fold_with<U, E>(
166 self,
167 opt: Option<U>,
168 f: impl FnOnce(Self, U) -> Result<Self, E>,
169 ) -> Result<Self, E> {
170 match opt {
171 Some(value) => f(self, value),
172 None => Ok(self),
173 }
174 }
175}
176
177impl<T: Sized> FoldWithOption for T {}
179
180pub(crate) trait IteratorExt: Iterator + Sized {
182 fn on_complete<F: FnOnce()>(self, f: F) -> OnComplete<Self, F> {
187 OnComplete {
188 inner: self,
189 on_complete: Some(f),
190 }
191 }
192}
193
194impl<I: Iterator> IteratorExt for I {}
195
196pub(crate) struct OnComplete<I, F: FnOnce()> {
198 inner: I,
199 on_complete: Option<F>,
200}
201
202impl<I, F: FnOnce()> Drop for OnComplete<I, F> {
203 fn drop(&mut self) {
204 if self.on_complete.is_some() {
205 tracing::debug!(
206 "OnComplete iterator dropped before exhaustion; completion callback not called"
207 );
208 }
209 }
210}
211
212impl<I, F> Iterator for OnComplete<I, F>
213where
214 I: Iterator,
215 F: FnOnce(),
216{
217 type Item = I::Item;
218
219 fn size_hint(&self) -> (usize, Option<usize>) {
220 self.inner.size_hint()
221 }
222
223 fn next(&mut self) -> Option<Self::Item> {
224 match self.inner.next() {
225 Some(item) => Some(item),
226 None => {
227 if let Some(f) = self.on_complete.take() {
228 f();
229 }
230 None
231 }
232 }
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239
240 #[test]
241 fn test_path_parsing() {
242 for x in [
243 #[cfg(not(windows))]
245 "file:///foo/bar",
246 #[cfg(not(windows))]
247 "file:///foo/bar/",
248 "/foo/bar",
249 "/foo/bar/",
250 "../foo/bar",
251 "../foo/bar/",
252 "c:/foo/bar",
253 "c:/",
254 "file:///C:/",
255 ] {
256 match resolve_uri_type(x) {
257 Ok(UriType::LocalPath(_)) => {}
258 x => panic!("Should have parsed as a local path {x:?}"),
259 }
260 }
261
262 for x in [
263 "s3://foo/bar",
264 "s3a://foo/bar",
265 "memory://foo/bar",
266 "gs://foo/bar",
267 "https://foo/bar/",
268 "unknown://foo/bar",
269 "s2://foo/bar",
270 ] {
271 match resolve_uri_type(x) {
272 Ok(UriType::Url(_)) => {}
273 x => panic!("Should have parsed as a url {x:?}"),
274 }
275 }
276
277 #[cfg(not(windows))]
278 resolve_uri_type("file://foo/bar").expect_err("file://foo/bar should not have parsed");
279 }
280
281 #[test]
282 fn try_from_uri_without_trailing_slash() {
283 let location = "s3://foo/__unitystorage/catalogs/cid/tables/tid";
284 let url = try_parse_uri(location).unwrap();
285
286 assert_eq!(
287 url.to_string(),
288 "s3://foo/__unitystorage/catalogs/cid/tables/tid/"
289 );
290 }
291
292 mod on_complete_tests {
293 use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
294 use std::sync::Arc;
295
296 use super::*;
297
298 #[test]
299 fn test_calls_on_exhaustion() {
300 let called = Arc::new(AtomicBool::new(false));
301 let called_clone = called.clone();
302 let mut iter = vec![1, 2].into_iter().on_complete(move || {
303 called_clone.store(true, Ordering::SeqCst);
304 });
305 assert_eq!(iter.next(), Some(1));
306 assert!(!called.load(Ordering::SeqCst));
307 assert_eq!(iter.next(), Some(2));
308 assert_eq!(iter.next(), None);
309 assert!(called.load(Ordering::SeqCst));
310 }
311
312 #[test]
313 fn test_does_not_call_on_early_drop() {
314 let called = Arc::new(AtomicBool::new(false));
315 let called_clone = called.clone();
316 {
317 let mut iter = vec![1, 2].into_iter().on_complete(move || {
318 called_clone.store(true, Ordering::SeqCst);
319 });
320 assert_eq!(iter.next(), Some(1));
321 }
323 assert!(!called.load(Ordering::SeqCst));
324 }
325
326 #[test]
327 fn test_calls_only_once() {
328 let count = Arc::new(AtomicU32::new(0));
329 let count_clone = count.clone();
330 {
331 let mut iter = vec![1].into_iter().on_complete(move || {
332 count_clone.fetch_add(1, Ordering::SeqCst);
333 });
334 assert_eq!(iter.next(), Some(1));
335 assert_eq!(iter.next(), None); assert_eq!(iter.next(), None); } assert_eq!(count.load(Ordering::SeqCst), 1);
339 }
340 }
341}