use std::borrow::Cow;
use std::ops::Deref;
use std::path::PathBuf;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use delta_kernel_derive::internal_api;
use url::Url;
use crate::{DeltaResult, Error};
macro_rules! require {
( $cond:expr, $err:expr ) => {
if !($cond) {
return Err($err);
}
};
}
pub(crate) use require;
pub trait CollectInto<T>: IntoIterator + Sized {
fn collect_into(self) -> T;
}
impl<I: IntoIterator, T: FromIterator<I::Item>> CollectInto<T> for I {
fn collect_into(self) -> T {
T::from_iter(self)
}
}
#[allow(unused)]
#[internal_api]
pub(crate) fn try_parse_uri(uri: impl AsRef<str>) -> DeltaResult<Url> {
let uri = uri.as_ref();
let uri_type = resolve_uri_type(uri)?;
let url = match uri_type {
UriType::LocalPath(path) => {
if !path.exists() {
return Err(Error::InvalidTableLocation(format!(
"Path does not exist: {path:?}"
)));
}
if !path.is_dir() {
return Err(Error::InvalidTableLocation(format!(
"{path:?} is not a directory"
)));
}
let path = std::fs::canonicalize(path).map_err(|err| {
let msg = format!("Invalid table location: {uri} Error: {err:?}");
Error::InvalidTableLocation(msg)
})?;
Url::from_directory_path(path.clone()).map_err(|_| {
let msg = format!(
"Could not construct a URL from canonicalized path: {path:?}.\n\
Something must be very wrong with the table path."
);
Error::InvalidTableLocation(msg)
})?
}
UriType::Url(url) => url,
};
Ok(url)
}
#[allow(unused)]
#[derive(Debug)]
enum UriType {
LocalPath(PathBuf),
Url(Url),
}
#[allow(unused)]
fn resolve_uri_type(table_uri: impl AsRef<str>) -> DeltaResult<UriType> {
let table_uri = table_uri.as_ref();
let table_uri = if table_uri.ends_with('/') {
Cow::Borrowed(table_uri)
} else {
Cow::Owned(format!("{table_uri}/"))
};
if let Ok(url) = Url::parse(&table_uri) {
let scheme = url.scheme().to_string();
if url.scheme() == "file" {
Ok(UriType::LocalPath(
url.to_file_path()
.map_err(|_| Error::invalid_table_location(table_uri))?,
))
} else if scheme.len() == 1 {
Ok(UriType::LocalPath(PathBuf::from(table_uri.as_ref())))
} else {
Ok(UriType::Url(url))
}
} else {
Ok(UriType::LocalPath(table_uri.deref().into()))
}
}
pub(crate) fn current_time_duration() -> DeltaResult<Duration> {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| Error::generic(format!("System time before Unix epoch: {e}")))
}
pub(crate) fn current_time_ms() -> DeltaResult<i64> {
let duration = current_time_duration()?;
i64::try_from(duration.as_millis())
.map_err(|_| Error::generic("Current timestamp exceeds i64 millisecond range"))
}
#[internal_api]
pub(crate) trait FoldWithOption: Sized {
fn fold_with<U>(self, opt: Option<U>, f: impl FnOnce(Self, U) -> Self) -> Self {
match opt {
Some(value) => f(self, value),
None => self,
}
}
fn try_fold_with<U, E>(
self,
opt: Option<U>,
f: impl FnOnce(Self, U) -> Result<Self, E>,
) -> Result<Self, E> {
match opt {
Some(value) => f(self, value),
None => Ok(self),
}
}
}
impl<T: Sized> FoldWithOption for T {}
pub(crate) trait IteratorExt: Iterator + Sized {
fn on_complete<F: FnOnce()>(self, f: F) -> OnComplete<Self, F> {
OnComplete {
inner: self,
on_complete: Some(f),
}
}
}
impl<I: Iterator> IteratorExt for I {}
pub(crate) struct OnComplete<I, F: FnOnce()> {
inner: I,
on_complete: Option<F>,
}
impl<I, F: FnOnce()> Drop for OnComplete<I, F> {
fn drop(&mut self) {
if self.on_complete.is_some() {
tracing::debug!(
"OnComplete iterator dropped before exhaustion; completion callback not called"
);
}
}
}
impl<I, F> Iterator for OnComplete<I, F>
where
I: Iterator,
F: FnOnce(),
{
type Item = I::Item;
fn size_hint(&self) -> (usize, Option<usize>) {
self.inner.size_hint()
}
fn next(&mut self) -> Option<Self::Item> {
match self.inner.next() {
Some(item) => Some(item),
None => {
if let Some(f) = self.on_complete.take() {
f();
}
None
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_path_parsing() {
for x in [
#[cfg(not(windows))]
"file:///foo/bar",
#[cfg(not(windows))]
"file:///foo/bar/",
"/foo/bar",
"/foo/bar/",
"../foo/bar",
"../foo/bar/",
"c:/foo/bar",
"c:/",
"file:///C:/",
] {
match resolve_uri_type(x) {
Ok(UriType::LocalPath(_)) => {}
x => panic!("Should have parsed as a local path {x:?}"),
}
}
for x in [
"s3://foo/bar",
"s3a://foo/bar",
"memory://foo/bar",
"gs://foo/bar",
"https://foo/bar/",
"unknown://foo/bar",
"s2://foo/bar",
] {
match resolve_uri_type(x) {
Ok(UriType::Url(_)) => {}
x => panic!("Should have parsed as a url {x:?}"),
}
}
#[cfg(not(windows))]
resolve_uri_type("file://foo/bar").expect_err("file://foo/bar should not have parsed");
}
#[test]
fn try_from_uri_without_trailing_slash() {
let location = "s3://foo/__unitystorage/catalogs/cid/tables/tid";
let url = try_parse_uri(location).unwrap();
assert_eq!(
url.to_string(),
"s3://foo/__unitystorage/catalogs/cid/tables/tid/"
);
}
mod on_complete_tests {
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::sync::Arc;
use super::*;
#[test]
fn test_calls_on_exhaustion() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = called.clone();
let mut iter = vec![1, 2].into_iter().on_complete(move || {
called_clone.store(true, Ordering::SeqCst);
});
assert_eq!(iter.next(), Some(1));
assert!(!called.load(Ordering::SeqCst));
assert_eq!(iter.next(), Some(2));
assert_eq!(iter.next(), None);
assert!(called.load(Ordering::SeqCst));
}
#[test]
fn test_does_not_call_on_early_drop() {
let called = Arc::new(AtomicBool::new(false));
let called_clone = called.clone();
{
let mut iter = vec![1, 2].into_iter().on_complete(move || {
called_clone.store(true, Ordering::SeqCst);
});
assert_eq!(iter.next(), Some(1));
}
assert!(!called.load(Ordering::SeqCst));
}
#[test]
fn test_calls_only_once() {
let count = Arc::new(AtomicU32::new(0));
let count_clone = count.clone();
{
let mut iter = vec![1].into_iter().on_complete(move || {
count_clone.fetch_add(1, Ordering::SeqCst);
});
assert_eq!(iter.next(), Some(1));
assert_eq!(iter.next(), None); assert_eq!(iter.next(), None); } assert_eq!(count.load(Ordering::SeqCst), 1);
}
}
}