Skip to content

Commit 36043cd

Browse files
committed
Merge branch 'dev'
2 parents 479e234 + 03c1896 commit 36043cd

14 files changed

Lines changed: 754 additions & 128 deletions

DESCRIPTION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ Imports:
4040
duckdb (>= 1.5.1),
4141
geoarrow,
4242
glue,
43+
jsonlite,
4344
lifecycle,
4445
nanoarrow,
4546
rlang,

NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646

4747
* `ddbs_as_points()` allows to create a `duckspatial_df` from raw coordinate or WKT columns. It also gains two new arguments: `remove` and `na.fail` (#125).
4848

49+
* `ddbs_open_dataset()`: can open geoparquet files when the geometry is encoded as WKB geoparquet. It also fails with a better error message when the geometry is encoded as a native arrow/geoarrow encoding (#129).
50+
4951
## BUG FIXES
5052

5153
* Large datasets couldn't be processed because an `arrow` code limitation in `ddbs_register_table()` (#124).

R/io_open_dataset.R

Lines changed: 150 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ ddbs_open_dataset <- function(path,
7373

7474
# Capture the call for error reporting
7575
fn_call <- rlang::current_call()
76+
crs_override <- crs
7677

7778
# Get or create connection
7879
if (is.null(conn)) {
@@ -104,30 +105,6 @@ ddbs_open_dataset <- function(path,
104105
")
105106
}
106107

107-
# -- CRS DETECTION --
108-
# Suppress CRS warnings for all formats since:
109-
# 1. File-not-found errors will be caught later when executing the view
110-
# 2. Invalid format errors will be caught later
111-
# 3. CRS warnings before the main error just confuse users
112-
113-
if (is.null(crs)) {
114-
if (fmt == "parquet") {
115-
crs <- suppressWarnings(tryCatch(get_parquet_crs(path, conn), error = function(e) NULL))
116-
} else {
117-
# Spatial formats: selectively suppress file/format warnings, preserve CRS warnings
118-
crs <- withCallingHandlers(
119-
tryCatch(get_file_crs(path, conn), error = function(e) NULL),
120-
warning = function(w) {
121-
if (grepl("Cannot open|No such file|not recognized|missing value where", w$message, ignore.case = TRUE)) {
122-
invokeRestart("muffleWarning")
123-
}
124-
}
125-
)
126-
}
127-
} else if (!inherits(crs, "crs")) {
128-
crs <- sf::st_crs(crs)
129-
}
130-
131108
view_name <- ddbs_temp_view_name()
132109

133110
# -- QUERY CONSTRUCTION --
@@ -166,8 +143,6 @@ ddbs_open_dataset <- function(path,
166143

167144
if (is_parquet_check) {
168145
is_likely_parquet <- TRUE
169-
# If we successfully probed likely Parquet, we might want to try to get CRS now if still NULL
170-
if (is.null(crs)) crs <- get_parquet_crs(path, conn)
171146
}
172147
}
173148

@@ -195,19 +170,33 @@ ddbs_open_dataset <- function(path,
195170
scan_query <- glue::glue("read_parquet('{path}'{p_args_str})")
196171

197172
# Resolve geometry column
173+
try_cols <- tryCatch({
174+
DBI::dbGetQuery(conn, glue::glue("DESCRIBE SELECT * FROM {scan_query}"))
175+
}, error = function(e) NULL)
176+
198177
if (is.null(geom_col)) {
199-
try_cols <- tryCatch({
200-
DBI::dbGetQuery(conn, glue::glue("DESCRIBE SELECT * FROM {scan_query}"))
201-
}, error = function(e) NULL)
202-
203178
if (!is.null(try_cols)) {
204-
possibles <- c("geometry", "geom", "wkb_geometry")
205-
found <- try_cols$column_name[try_cols$column_name %in% possibles]
206-
if (length(found) > 0) geom_col <- found[1]
179+
geom_col <- ddbs_describe_geometry_col(try_cols)
207180
} else {
208181
geom_col <- NULL
209182
}
210183
}
184+
185+
# Intercept GeoArrow structs (native Arrow encoding) which DuckDB cannot parse
186+
if (!is.null(geom_col) && !is.null(try_cols)) {
187+
col_type <- try_cols$column_type[try_cols$column_name == geom_col]
188+
if (length(col_type) > 0 && grepl("STRUCT", toupper(col_type[1]))) {
189+
cli::cli_abort(c(
190+
"The geometry column {.val {geom_col}} uses a native Arrow/GeoArrow struct encoding that DuckDB's spatial extension cannot parse here.",
191+
"x" = "This file uses a GeoParquet/GeoArrow geometry encoding that {.pkg duckspatial} cannot open through DuckDB yet.",
192+
"i" = "To work around this, rewrite the geometry column to WKB GeoParquet, for example using:",
193+
" " = " {.code duckspatial::ddbs_write_dataset(data, path)}",
194+
" " = " # OR if using geoarrow:",
195+
" " = " {.code data${geom_col} <- geoarrow::as_geoarrow_vctr(data${geom_col}, schema = geoarrow::geoarrow_wkb())}",
196+
" " = " {.code arrow::write_parquet(data, path)}"
197+
))
198+
}
199+
}
211200

212201
view_query <- create_temp_table(
213202
name = view_name,
@@ -289,7 +278,6 @@ ddbs_open_dataset <- function(path,
289278
query_str <- glue::glue("ST_Read('{path}'{args_str})")
290279

291280
if (is.null(geom_col)) {
292-
geom_col <- "geom"
293281
try_cols <- tryCatch({
294282
DBI::dbGetQuery(conn, glue::glue("DESCRIBE SELECT * FROM {query_str}"))
295283
}, error = function(e) {
@@ -323,9 +311,7 @@ ddbs_open_dataset <- function(path,
323311
})
324312

325313
if (!is.null(try_cols)) {
326-
ctype <- if("column_type" %in% names(try_cols)) try_cols$column_type else try_cols$data_type
327-
geom_cols <- try_cols$column_name[grepl("GEOMETRY|WKB_BLOB", ctype, ignore.case = TRUE)]
328-
if (length(geom_cols) > 0) geom_col <- geom_cols[1]
314+
geom_col <- ddbs_describe_geometry_col(try_cols)
329315
}
330316
}
331317

@@ -376,9 +362,27 @@ ddbs_open_dataset <- function(path,
376362

377363
# Get lazy table reference
378364
duck_tbl <- dplyr::tbl(conn, view_name)
365+
366+
view_cols <- tryCatch(
367+
DBI::dbGetQuery(conn, glue::glue("DESCRIBE {view_name}")),
368+
error = function(e) NULL
369+
)
370+
371+
# Resolve geometry column: uses user-supplied name if valid,
372+
# otherwise falls back to auto-detection heuristic.
373+
geom_col <- ddbs_describe_geometry_col(view_cols, geom_col)
379374

380375
# Return already if there's no geometry
381376
if (is.null(geom_col)) return(duck_tbl)
377+
378+
crs <- ddbs_open_dataset_crs(
379+
crs = crs_override,
380+
conn = conn,
381+
view_name = view_name,
382+
geom_col = geom_col,
383+
path = path,
384+
fmt = if (is_likely_parquet) "parquet" else fmt
385+
)
382386

383387
# Return duckspatial if there's geometry col
384388
result <- new_duckspatial_df(
@@ -391,3 +395,112 @@ ddbs_open_dataset <- function(path,
391395

392396
return(result)
393397
}
398+
399+
#' Detect a geometry column from DuckDB DESCRIBE output
400+
#'
401+
#' @keywords internal
402+
#' @noRd
403+
ddbs_describe_geometry_col <- function(desc, geom_col = NULL) {
404+
if (is.null(desc) || nrow(desc) == 0) {
405+
return(NULL)
406+
}
407+
408+
col_type <- if ("column_type" %in% names(desc)) desc$column_type else desc$data_type
409+
410+
is_geometry_type <- grepl("^GEOMETRY(\\(|$)", col_type, ignore.case = TRUE)
411+
is_wkb_type <- grepl("WKB_BLOB", col_type, ignore.case = TRUE)
412+
is_blob_type <- grepl("^BLOB$", col_type, ignore.case = TRUE)
413+
is_struct_type <- grepl("^STRUCT", col_type, ignore.case = TRUE)
414+
is_spatial_type <- is_geometry_type | is_wkb_type | is_blob_type | is_struct_type
415+
416+
# 1. If user supplied a geom_col, validate it
417+
if (!is.null(geom_col) && !is.na(geom_col)) {
418+
match_idx <- which(desc$column_name == geom_col)
419+
if (length(match_idx) > 0 && is_spatial_type[match_idx[1]]) {
420+
return(geom_col)
421+
}
422+
}
423+
424+
# 2. Auto-detection heuristics
425+
426+
# 2.1. First priority: Native GEOMETRY types
427+
geom_cols <- desc$column_name[is_geometry_type]
428+
if (length(geom_cols) > 0) {
429+
return(geom_cols[1])
430+
}
431+
432+
# 2.2. Second priority: Spatial column names with compatible types (WKB/BLOB/STRUCT)
433+
known <- c("geom", "geometry", "wkb_geometry")
434+
found <- desc$column_name[desc$column_name %in% known & is_spatial_type]
435+
if (length(found) > 0) {
436+
return(found[1])
437+
}
438+
439+
# 2.3. Third priority: Known spatial formats fallback (WKB_BLOB)
440+
# Do not treat arbitrary BLOB columns as geometry unless they were
441+
# explicitly selected by name above or exposed as native GEOMETRY.
442+
wkb_cols <- desc$column_name[is_wkb_type]
443+
if (length(wkb_cols) > 0) {
444+
return(wkb_cols[1])
445+
}
446+
447+
NULL
448+
}
449+
450+
#' Resolve CRS for an opened DuckDB-backed dataset
451+
#'
452+
#' @keywords internal
453+
#' @noRd
454+
ddbs_open_dataset_crs <- function(crs, conn, view_name, geom_col, path, fmt) {
455+
if (!is.null(crs)) {
456+
return(if (inherits(crs, "crs")) crs else sf::st_crs(crs))
457+
}
458+
459+
crs_from_duckdb <- tryCatch({
460+
q_geom <- DBI::dbQuoteIdentifier(conn, geom_col)
461+
res <- DBI::dbGetQuery(conn, glue::glue(
462+
"SELECT ST_CRS({q_geom}) AS crs ",
463+
"FROM {view_name} ",
464+
"WHERE {q_geom} IS NOT NULL ",
465+
"LIMIT 1"
466+
))
467+
468+
if (nrow(res) == 0 || is.na(res$crs[1]) || identical(res$crs[1], "")) {
469+
NULL
470+
} else {
471+
sf::st_crs(res$crs[1])
472+
}
473+
}, error = function(e) NULL)
474+
475+
if (!is.null(crs_from_duckdb) && !is.na(crs_from_duckdb)) {
476+
return(crs_from_duckdb)
477+
}
478+
479+
if (!identical(fmt, "parquet")) {
480+
crs_from_gdal_meta <- withCallingHandlers(
481+
tryCatch(get_file_crs(path, conn), error = function(e) NULL),
482+
warning = function(w) {
483+
if (grepl("Cannot open|No such file|not recognized|missing value where", w$message, ignore.case = TRUE)) {
484+
invokeRestart("muffleWarning")
485+
}
486+
}
487+
)
488+
489+
if (!is.null(crs_from_gdal_meta) && !is.na(crs_from_gdal_meta)) {
490+
return(crs_from_gdal_meta)
491+
}
492+
}
493+
494+
if (identical(fmt, "parquet")) {
495+
crs_from_geoparquet <- suppressWarnings(
496+
tryCatch(get_parquet_crs(path, conn), error = function(e) NULL)
497+
)
498+
499+
if (!is.null(crs_from_geoparquet) && !is.na(crs_from_geoparquet)) {
500+
return(crs_from_geoparquet)
501+
}
502+
}
503+
504+
warning("CRS could not be auto-detected; returning NA CRS.", call. = FALSE)
505+
sf::st_crs(NA)
506+
}

0 commit comments

Comments
 (0)