Skip to content

Commit 935d1bf

Browse files
committed
fix: Local path normalization on Windows
1 parent 3a40a9b commit 935d1bf

5 files changed

Lines changed: 60 additions & 17 deletions

File tree

R/00_classes.R

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
#' @import S7
22
NULL
33

4+
#' Check if a path is a local filesystem path (not a cloud URI)
5+
#'
6+
#' @param path Character. Path to check.
7+
#' @return Logical. TRUE if the path is a local filesystem path.
8+
#' @noRd
9+
is_local_path <- function(path) {
10+
# Cloud storage URIs typically start with a scheme like s3://, gs://, az://, abfs://, etc.
11+
12+
!grepl("^[a-zA-Z][a-zA-Z0-9+.-]*://", path)
13+
}
14+
415
.onLoad <- function(...) {
516
# Register cloud storage handlers (GCS, S3, Azure) for Delta Lake
617

R/delta_table.R

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ delta_table <- function(
6262
stop("'path' must be a single character string")
6363
}
6464

65+
# Normalize path if it's a local path to avoid mixed slashes on Windows
66+
if (is_local_path(path)) {
67+
path <- normalizePath(path, mustWork = FALSE, winslash = "/")
68+
}
69+
6570
if (!is.null(version) && !is.null(datetime)) {
6671
stop("Cannot specify both 'version' and 'datetime'")
6772
}
@@ -359,6 +364,11 @@ method(load_datetime, DeltaTable) <- function(table, ..., datetime) {
359364
#'
360365
#' @export
361366
is_delta_table_path <- function(path, storage_options = NULL) {
367+
# Normalize path if it's a local path to avoid mixed slashes on Windows
368+
if (is.character(path) && length(path) == 1 && is_local_path(path)) {
369+
path <- normalizePath(path, mustWork = FALSE, winslash = "/")
370+
}
371+
362372
result <- is_delta_table(path, storage_options)
363373
if (methods::is(result, "error")) {
364374
rlang::abort(result$value)

R/merge.R

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ delta_merge <- function(
158158
stop("'table' must be a DeltaTable object or a single character path")
159159
}
160160

161+
# Normalize path if it's a local path to avoid mixed slashes on Windows
162+
if (is_local_path(table_path)) {
163+
table_path <- normalizePath(table_path, mustWork = FALSE, winslash = "/")
164+
}
165+
161166
DeltaMergeBuilder(
162167
table_path = table_path,
163168
storage_options = storage_options,

R/write.R

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,6 @@
11
#' @importFrom rlang abort
22
NULL
33

4-
#' Check if a path is a local filesystem path (not a cloud URI)
5-
#'
6-
#' @param path Character. Path to check.
7-
#' @return Logical. TRUE if the path is a local filesystem path.
8-
#' @noRd
9-
is_local_path <- function(path) {
10-
# Cloud storage URIs typically start with a scheme like s3://, gs://, az://, abfs://, etc.
11-
12-
!grepl("^[a-zA-Z][a-zA-Z0-9+.-]*://", path)
13-
}
144

155
#' Ensure directory exists for local paths
166
#'
@@ -120,6 +110,15 @@ write_deltalake <- function(
120110
stop("'table_or_uri' must be a single character string")
121111
}
122112

113+
# Normalize path if it's a local path to avoid mixed slashes on Windows
114+
if (is_local_path(table_or_uri)) {
115+
table_or_uri <- normalizePath(
116+
table_or_uri,
117+
mustWork = FALSE,
118+
winslash = "/"
119+
)
120+
}
121+
123122
# Create directory if it's a local path and doesn't exist
124123
ensure_directory_exists(table_or_uri)
125124

@@ -227,6 +226,11 @@ create_deltalake <- function(
227226
stop("'table_uri' must be a single character string")
228227
}
229228

229+
# Normalize path if it's a local path to avoid mixed slashes on Windows
230+
if (is_local_path(table_uri)) {
231+
table_uri <- normalizePath(table_uri, mustWork = FALSE, winslash = "/")
232+
}
233+
230234
# Create directory if it's a local path and doesn't exist
231235
ensure_directory_exists(table_uri)
232236

src/rust/src/lib.rs

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -127,18 +127,31 @@ pub(crate) fn parse_storage_options(opts: &List) -> HashMap<String, String> {
127127

128128
/// Helper to convert a path string to URL
129129
pub(crate) fn path_to_url(path: &str) -> std::result::Result<url::Url, String> {
130-
// Try parsing as URL first
131-
if let Ok(url) = url::Url::parse(path) {
132-
return Ok(url);
130+
// Try parsing as URL first. On Windows, a path like "C:\..." might be
131+
// parsed as a URL with scheme "C", so we only accept it as a URL if it
132+
// contains "://" and the scheme is longer than 1 character (to avoid drive letters).
133+
if path.contains("://") {
134+
if let Ok(url) = url::Url::parse(path) {
135+
if url.scheme().len() > 1 {
136+
return Ok(url);
137+
}
138+
}
133139
}
134140

135141
// Treat as local path
136142
let path_buf = std::path::Path::new(path);
137-
let canonical = path_buf
138-
.canonicalize()
139-
.unwrap_or_else(|_| path_buf.to_path_buf());
140143

141-
url::Url::from_file_path(&canonical)
144+
// Get absolute path. We avoid canonicalize() on Windows because it adds
145+
// the \\?\ prefix which some object store implementations don't handle well.
146+
let abs_path = if path_buf.is_absolute() {
147+
path_buf.to_path_buf()
148+
} else {
149+
std::env::current_dir()
150+
.map(|curr| curr.join(path_buf))
151+
.unwrap_or_else(|_| path_buf.to_path_buf())
152+
};
153+
154+
url::Url::from_file_path(&abs_path)
142155
.map_err(|_| format!("Failed to create URL from path: {}", path))
143156
}
144157

0 commit comments

Comments
 (0)