@@ -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