From ad11dc4516913d3f9c1bc2c8cb002ece2071ac0f Mon Sep 17 00:00:00 2001 From: Carl Date: Tue, 5 Dec 2023 22:33:05 +0000 Subject: [PATCH 1/3] :artificial_satellite: --- DESCRIPTION | 3 +- NAMESPACE | 1 + NEWS.md | 9 ++- R/open_dataset.R | 72 +++++++++++++------ R/spatial_join.R | 123 +++++++++++++++++++++++++++++++++ R/write_dataset.R | 2 - inst/examples/spatial_module.R | 36 ++++++++++ man/as_view.Rd | 28 ++++++++ man/open_dataset.Rd | 7 +- man/spatial_join.Rd | 65 +++++++++++++++++ tests/testthat/test-spatial.R | 34 +++++++++ 11 files changed, 351 insertions(+), 29 deletions(-) create mode 100644 R/spatial_join.R create mode 100644 inst/examples/spatial_module.R create mode 100644 man/as_view.Rd create mode 100644 man/spatial_join.Rd diff --git a/DESCRIPTION b/DESCRIPTION index e308d2f..9ca47f4 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -18,7 +18,8 @@ Imports: DBI, dbplyr, dplyr, - duckdb (>= 0.8.1) + duckdb (>= 0.8.1), + fs Suggests: curl, sf, diff --git a/NAMESPACE b/NAMESPACE index 403c0ac..aa8b5de 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +export(as_view) export(cached_connection) export(close_connection) export(duckdb_s3_config) diff --git a/NEWS.md b/NEWS.md index 294ab10..b6a4dd7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,9 +1,14 @@ # duckdbfs 0.0.4 -* `open_dataset()` gains option `sf` to format, allowing users to parse - spatial vector data in simple features standard (objects read by `sf`) +* `open_dataset()` gains the ability to read spatial vector data formats + (objects read by `sf`) using `format="sf"` * default geometry column in `to_sf()` is now termed `geom`, to match the default used in `duckdb`'s `st_read()` function. +* `open_dataset()` now tries to guess the data format instead of defaulting to + parquet when no format is explicitly provided. + +* new function, `spatial_join()`, allows a variety of spatial joins. +* new helper function, `as_view()`, creates a temporary view of a query. # duckdbfs 0.0.3 diff --git a/R/open_dataset.R b/R/open_dataset.R index a8aa2a4..1d9b69b 100644 --- a/R/open_dataset.R +++ b/R/open_dataset.R @@ -14,8 +14,9 @@ #' column name across all files (NOTE: this can add considerably to #' the initial execution time) #' @param format The format of the dataset files. One of `"parquet"`, `"csv"`, -#' `"tsv"`, `"text"` or `"sf"` (for any Simple Features spatial dataset supported -#' by the sf package). +#' `"tsv"`, or `"sf"` (spatial vector files supported by the sf package / GDAL). +#' if no argument is provided, the function will try to guess the type based +#' on minimal heuristics. #' @param conn A connection to a database. #' @param tblname The name of the table to create in the database. #' @param mode The mode to create the table in. One of `"VIEW"` or `"TABLE"`. @@ -60,7 +61,7 @@ open_dataset <- function(sources, schema = NULL, hive_style = TRUE, unify_schemas = FALSE, - format = c("parquet", "csv", "tsv", "text", "sf"), + format = c("parquet", "csv", "tsv", "sf"), conn = cached_connection(), tblname = tmp_tbl_name(), mode = "VIEW", @@ -68,6 +69,8 @@ open_dataset <- function(sources, recursive = TRUE, ...) { + format <- select_format(sources, format) + sources <- parse_uri(sources, conn = conn, recursive = recursive) if(length(list(...)) > 0) { # can also be specified in URI query notation @@ -77,8 +80,6 @@ open_dataset <- function(sources, # ensure active connection version <- DBI::dbExecute(conn, "PRAGMA version;") - - format <- match.arg(format) if(format == "sf") { load_spatial(conn = conn) } @@ -95,6 +96,41 @@ open_dataset <- function(sources, dplyr::tbl(conn, tblname) } +select_format <- function(sources, format) { + ## does not guess file types in s3 buckets. + + if(length(format) == 1) { + return(format) + } + + # format for vector sources always based on first element + sources <- sources[[1]] + + + if( fs::is_dir(sources) ) { + sources <- fs::dir_ls(sources, recurse = TRUE, type="file") + sources <- sources[[1]] + } + format <- tools::file_ext(sources) + + # detect spatial types + if(grepl("^/vsi", sources)) { + return("sf") + } + if(format %in% c("fgb", "shp", "json", "geojson", "gdb", "gpkg", + "kml", "gmt")) { + return("sf") + } + + # default + if (format == "") { + return("parquet") + } + + format +} + + use_recursive <- function(sources) { !all(identical(tools::file_ext(sources), "")) } @@ -112,7 +148,14 @@ query_string <- function(tblname, union_by_name = FALSE, filename = FALSE) { - format <- match.arg(format) + # format <- match.arg(format) + scanner <- switch(format, + "parquet" = "parquet_scan(", + "csv" = "read_csv_auto(", + "sf" = "st_read(", + "read_csv_auto(" + ) + source_uris <- vec_as_str(sources) ## Allow overwrites on VIEW @@ -120,14 +163,6 @@ query_string <- function(tblname, "VIEW" = "OR REPLACE TEMPORARY VIEW", "TABLE" = "TABLE") - scanner <- switch(format, - "parquet" = "parquet_scan(", - "csv" = "read_csv_auto(", - "tsv" = "read_csv_auto(", - "text" = "read_csv_auto(", - "sf" = "st_read(" - ) - tabular_options <- paste0( ", HIVE_PARTITIONING=",hive_partitioning, ", UNION_BY_NAME=",union_by_name, @@ -136,12 +171,9 @@ query_string <- function(tblname, options <- switch(format, "parquet" = tabular_options, "csv" = tabular_options, - "tsv" = tabular_options, - "text" = tabular_options, - "sf" = "" + "sf" = "", + tabular_options ) - - paste0( paste("CREATE", mode, tblname, "AS SELECT * FROM "), paste0(scanner, source_uris, options, @@ -152,8 +184,6 @@ query_string <- function(tblname, tmp_tbl_name <- function(n = 15) { paste0(sample(letters, n, replace = TRUE), collapse = "") } - - remote_src <- function(conn) { dbplyr::remote_src(conn) } diff --git a/R/spatial_join.R b/R/spatial_join.R new file mode 100644 index 0000000..cffab80 --- /dev/null +++ b/R/spatial_join.R @@ -0,0 +1,123 @@ + +#' as_view +#' +#' Create a View of the current query. This can be an effective way to allow +#' a query chain to remain lazy +#' @param x a duckdb spatial dataset +#' @inheritParams open_dataset +#' @examplesIf interactive() +#' path <- system.file("extdata/spatial-test.csv", package="duckdbfs") +#' df <- open_dataset(path) +#' library(dplyr) +#' +#' df |> filter(latitude > 5) |> as_view() +#' +#' @export +as_view <- function(x, tblname = tmp_tbl_name(), conn = cached_connection()) { + + # assert x is a tbl_lazy, a tbl_sql, and a tbl_duckdb_connection + + ## lazy_base_query objects are good to go. + if(inherits(x$lazy_query, "lazy_base_query")) { + return(x) + } + ## lazy_select_query objects are unnamed, + ## convert to named views so we can re-use them in queries + q <- dbplyr::sql_render(x) + query_to_view(q, tblname, conn) +} + +query_to_view <- function(query, + tblname = tmp_tbl_name(), + conn = cached_connection()) { + q <- paste("CREATE OR REPLACE TEMPORARY VIEW", tblname, "AS", query) + DBI::dbSendQuery(conn, q) + dplyr::tbl(conn, tblname) +} + +#' spatial_join +#' +#' @param x a duckdb table with a spatial geometry column called "geom" +#' @param y a duckdb table with a spatial geometry column called "geom" +#' @param by A spatial join function. +#' @param join JOIN type (left, right, inner, full) +#' @param args additional arguments to join function (e.g. distance for st_dwithin) +#' @param tblname name for the temporary view +#' @param conn the duckdb connection (imputed by duckdbfs by default, +#' must be shared across both tables) +#' @return a (lazy) view of the resulting table. Users can continue to operate +#' on using dplyr operations and call to_st() to collect this as an sf object. +#' @examplesIf interactive() +#' +#' # note we can read in remote data in a variety of vector formats: +#' countries <- +#' paste0("/vsicurl/", +#' "https://github.com/cboettig/duckdbfs/", +#' "raw/spatial-read/inst/extdata/world.gpkg") |> +#' open_dataset(format = "sf") +#' +#' cities <- +#' paste0("/vsicurl/https://github.com/cboettig/duckdbfs/raw/", +#' "spatial-read/inst/extdata/metro.fgb") |> +#' open_dataset(format = "sf") +#' +#' countries |> +#' dplyr::filter(iso_a3 == "AUS") |> +#' spatial_join(cities) +#' +#' # cities within 10 degrees of Australia: +#' countries |> +#' dplyr::filter(iso_a3 == "AUS") |> +#' spatial_join(cities, by = "st_dwithin", args = 10) +#' +spatial_join <- function(x, + y, + by=c("st_intersects", "st_within", + "st_dwithin", "st_touches", + "st_contains", "st_containsproperly", + "st_covers", "st_overlaps"), + args = "", + join="left", + tblname = tmp_tbl_name(), + conn = cached_connection()) { + + by <- match.arg(by) + ## x,y may be promised queries + x <- as_view(x) + y <- as_view(y) + + # buil spatial join query + x.name <- remote_name(x, conn) + y.name <- remote_name(y, conn) + x.geom <- paste0(x.name, ".geom") + y.geom <- paste0(y.name, ".geom") + + if(args != ""){ + args <- paste(",", args) + } + + # be more careful than SELECT * + + # x.geom becomes the "geom" column, y.geom becomes geom:1 + query <- paste( + "SELECT *", + "FROM", x.name, + join, "JOIN", y.name, + "ON", paste0(by, "(", x.geom, ", ", y.geom, args, ")") + ) + query_to_view(query, tblname, conn) + +} + +# st_contains, +# ST_ContainsProperly +# st_intersects, +# st_within, +# st_dwithin, +# st_covers +# st_overlaps +# st_touches + +# st_union is not a join operation + + diff --git a/R/write_dataset.R b/R/write_dataset.R index f727995..f4d55c1 100644 --- a/R/write_dataset.R +++ b/R/write_dataset.R @@ -32,9 +32,7 @@ write_dataset <- function(dataset, DBI::dbWriteTable(conn, name = tblname, value = dataset) } else { - tblname <- as.character(remote_name(dataset, conn)) - } path <- parse_uri(path, conn = conn, recursive = FALSE) diff --git a/inst/examples/spatial_module.R b/inst/examples/spatial_module.R new file mode 100644 index 0000000..4b59e2a --- /dev/null +++ b/inst/examples/spatial_module.R @@ -0,0 +1,36 @@ +st_read <- function() { + +} + +st_write <- function() { + +} + +st_perimeter <- function() { + # no sf equivalent +} + +# functions that operate on geometries already work within `mutate` calls +st_area <- function() { + +} + + +st_intersection <- function(x, y, ...) { + sf::st_intersection(x, y, ...) +} + +st_intersects <- function(x, y, ...) { + if(inherits(x, "sf")) { + sf::st_intersection(x, y, ...) + } + +} + + +st_union <- function(x, y, ..., + by_feature = by_feature, is_coverage = is_coverage) { + if(inherits(x, "sf")) { + sf::st_union(x, y, ..., by_feature = by_feature, is_coverage = is_coverage) + } +} diff --git a/man/as_view.Rd b/man/as_view.Rd new file mode 100644 index 0000000..f256bdb --- /dev/null +++ b/man/as_view.Rd @@ -0,0 +1,28 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/spatial_join.R +\name{as_view} +\alias{as_view} +\title{as_view} +\usage{ +as_view(x, tblname = tmp_tbl_name(), conn = cached_connection()) +} +\arguments{ +\item{x}{a duckdb spatial dataset} + +\item{tblname}{The name of the table to create in the database.} + +\item{conn}{A connection to a database.} +} +\description{ +Create a View of the current query. This can be an effective way to allow +a query chain to remain lazy +} +\examples{ +\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} +path <- system.file("extdata/spatial-test.csv", package="duckdbfs") +df <- open_dataset(path) +library(dplyr) + +df |> filter(latitude > 5) |> as_view() +\dontshow{\}) # examplesIf} +} diff --git a/man/open_dataset.Rd b/man/open_dataset.Rd index bff4270..50115e8 100644 --- a/man/open_dataset.Rd +++ b/man/open_dataset.Rd @@ -9,7 +9,7 @@ open_dataset( schema = NULL, hive_style = TRUE, unify_schemas = FALSE, - format = c("parquet", "csv", "tsv", "text", "sf"), + format = c("parquet", "csv", "tsv", "sf"), conn = cached_connection(), tblname = tmp_tbl_name(), mode = "VIEW", @@ -33,8 +33,9 @@ column name across all files (NOTE: this can add considerably to the initial execution time)} \item{format}{The format of the dataset files. One of \code{"parquet"}, \code{"csv"}, -\code{"tsv"}, \code{"text"} or \code{"sf"} (for any Simple Features spatial dataset supported -by the sf package).} +\code{"tsv"}, or \code{"sf"} (spatial vector files supported by the sf package / GDAL). +if no argument is provided, the function will try to guess the type based +on minimal heuristics.} \item{conn}{A connection to a database.} diff --git a/man/spatial_join.Rd b/man/spatial_join.Rd new file mode 100644 index 0000000..fbadc3e --- /dev/null +++ b/man/spatial_join.Rd @@ -0,0 +1,65 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/spatial_join.R +\name{spatial_join} +\alias{spatial_join} +\title{spatial_join} +\usage{ +spatial_join( + x, + y, + by = c("st_intersects", "st_within", "st_dwithin", "st_touches", "st_contains", + "st_containsproperly", "st_covers", "st_overlaps"), + args = "", + join = "left", + tblname = tmp_tbl_name(), + conn = cached_connection() +) +} +\arguments{ +\item{x}{a duckdb table with a spatial geometry column called "geom"} + +\item{y}{a duckdb table with a spatial geometry column called "geom"} + +\item{by}{A spatial join function.} + +\item{args}{additional arguments to join function (e.g. distance for st_dwithin)} + +\item{join}{JOIN type (left, right, inner, full)} + +\item{tblname}{name for the temporary view} + +\item{conn}{the duckdb connection (imputed by duckdbfs by default, +must be shared across both tables)} +} +\value{ +a (lazy) view of the resulting table. Users can continue to operate +on using dplyr operations and call to_st() to collect this as an sf object. +} +\description{ +spatial_join +} +\examples{ +\dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} + +# note we can read in remote data in a variety of vector formats: +countries <- +paste0("/vsicurl/", + "https://github.com/cboettig/duckdbfs/", + "raw/spatial-read/inst/extdata/world.gpkg") |> +open_dataset(format = "sf") + +cities <- + paste0("/vsicurl/https://github.com/cboettig/duckdbfs/raw/", + "spatial-read/inst/extdata/metro.fgb") |> + open_dataset(format = "sf") + +countries |> + dplyr::filter(iso_a3 == "AUS") |> + spatial_join(cities) + +# cities within 10 degrees of Australia: +countries |> + dplyr::filter(iso_a3 == "AUS") |> + spatial_join(cities, by = "st_dwithin", args = 10) +\dontshow{\}) # examplesIf} +} diff --git a/tests/testthat/test-spatial.R b/tests/testthat/test-spatial.R index 8f66eb9..d83566d 100644 --- a/tests/testthat/test-spatial.R +++ b/tests/testthat/test-spatial.R @@ -37,3 +37,37 @@ test_that("spatial vector read", { }) + +test_that("spatial_join", { + + + skip_if_not_installed("sf") + skip_on_os("windows") # come on duckdb, support extensions on windows + skip_if_offline() # needs to be able to load the spatial module + skip_on_cran() + + countries <- + paste0("/vsicurl/", + "https://github.com/cboettig/duckdbfs/", + "raw/spatial-read/inst/extdata/world.gpkg") |> + open_dataset() + + cities <- + paste0("/vsicurl/https://github.com/cboettig/duckdbfs/raw/", + "spatial-read/inst/extdata/metro.fgb") |> + open_dataset() + + out <- + countries |> + dplyr::filter(iso_a3 == "AUS") |> + spatial_join(cities) + + expect_s3_class(out, "tbl_lazy") + + local <- to_sf(out) + expect_s3_class(local, "sf") + expect_true(all(local$iso_a3 == "AUS")) + + ## add examples of other types of spatial joins +}) + From ea4f1c60f0fc819ebce3140547cb9c636a0838c8 Mon Sep 17 00:00:00 2001 From: Carl Date: Wed, 6 Dec 2023 00:00:39 +0000 Subject: [PATCH 2/3] WIP --- R/spatial_join.R | 31 ++++++++++++++++++++++++------- README.Rmd | 35 +++++++++++++++++++++++++++++++++++ man/spatial_join.Rd | 29 ++++++++++++++++++++++------- 3 files changed, 81 insertions(+), 14 deletions(-) diff --git a/R/spatial_join.R b/R/spatial_join.R index cffab80..eea1505 100644 --- a/R/spatial_join.R +++ b/R/spatial_join.R @@ -39,7 +39,7 @@ query_to_view <- function(query, #' #' @param x a duckdb table with a spatial geometry column called "geom" #' @param y a duckdb table with a spatial geometry column called "geom" -#' @param by A spatial join function. +#' @param by A spatial join function, see details. #' @param join JOIN type (left, right, inner, full) #' @param args additional arguments to join function (e.g. distance for st_dwithin) #' @param tblname name for the temporary view @@ -47,6 +47,26 @@ query_to_view <- function(query, #' must be shared across both tables) #' @return a (lazy) view of the resulting table. Users can continue to operate #' on using dplyr operations and call to_st() to collect this as an sf object. +#' @details +#' +#' Possible [spatial joins](https://postgis.net/workshops/postgis-intro/spatial_relationships.html) include: +#' +#' ---------------|---------------------------------------------- +#' st_intersects | Geometry A intersects with geometry B +#' st_disjoint | The complement of intersects +#' st_within | Geometry A is within geometry B (complement of contains) +#' st_dwithin | Geometries are within a specified distance, expressed in the same units as the coordinate reference system of the geometries. +#' st_touches | Two polygons touch if the that have at least one point in common, even if their interiors do not touch. +#' st_contains | Geometry A entirely contains to geometry B. (complement of within) +#' st_containsproperly | stricter version of `st_contains` (boundary counts as external) +#' st_covers | Geometry B. is inside or on boundary of A. (A polygon covers a point on its boundary but does not contain it.) +#' st_overlaps | Geometry A intersects but does not completely contain geometry B +#' st_equals | Geometry A is equal to geometry B +#' st_crosses | Lines or points in geometry A cross geometry B. +#' +#' All though SQL is not case sensitive, this function expects only +#' lower case names for "by" functions. +#' #' @examplesIf interactive() #' #' # note we can read in remote data in a variety of vector formats: @@ -65,17 +85,14 @@ query_to_view <- function(query, #' dplyr::filter(iso_a3 == "AUS") |> #' spatial_join(cities) #' -#' # cities within 10 degrees of Australia: -#' countries |> -#' dplyr::filter(iso_a3 == "AUS") |> -#' spatial_join(cities, by = "st_dwithin", args = 10) -#' spatial_join <- function(x, y, by=c("st_intersects", "st_within", "st_dwithin", "st_touches", "st_contains", "st_containsproperly", - "st_covers", "st_overlaps"), + "st_covers", "st_overlaps", + "st_crosses", "st_equals", + "st_disjoint"), args = "", join="left", tblname = tmp_tbl_name(), diff --git a/README.Rmd b/README.Rmd index 42081b8..283ea78 100644 --- a/README.Rmd +++ b/README.Rmd @@ -162,6 +162,41 @@ sf_obj <- countries |> filter(continent == "Africa") |> to_sf() plot(sf_obj["name"]) ``` +## Spatial joins + +One very common operation are spatial joins, which can be a very powerful way to subset large data. +For instance, we can return all points (cities) within a set of polygons + +```{r} +cities <- + paste0("/vsicurl/https://github.com/cboettig/duckdbfs/raw/", + "spatial-read/inst/extdata/metro.fgb") |> + open_dataset(format = "sf") + +countries |> + dplyr::filter(continent == "Oceania") |> + spatial_join(cities, by = "st_intersects", join="inner") |> + select(name_long, sovereignt, pop2020) + +``` + + +Possible [spatial joins](https://postgis.net/workshops/postgis-intro/spatial_relationships.html) include: + +---------------|---------------------------------------------- +st_intersects | Geometry A intersects with geometry B +st_disjoint | The complement of intersects +st_within | Geometry A is within geometry B (complement of contains) +st_dwithin | Geometries are within a specified distance, expressed in the same units as the coordinate reference system of the geometries. +st_touches | Two polygons touch if the that have at least one point in common, even if their interiors do not touch. +st_contains | Geometry A entirely contains to geometry B. (complement of within) +st_containsproperly | stricter version of `st_contains` (boundary counts as external) +st_covers | geometry B is inside or on boundary of A. (A polygon covers a point on its boundary but does not contain it.) +st_overlaps | geometry A intersects but does not completely contain geometry B +st_equals | geometry A is equal to geometry B +st_crosses | Lines or points in geometry A cross geometry B. + +Note that while SQL functions are not case-sensitive, `spatial_join` expects lower-case names. ## Writing datasets diff --git a/man/spatial_join.Rd b/man/spatial_join.Rd index fbadc3e..a293c52 100644 --- a/man/spatial_join.Rd +++ b/man/spatial_join.Rd @@ -8,7 +8,8 @@ spatial_join( x, y, by = c("st_intersects", "st_within", "st_dwithin", "st_touches", "st_contains", - "st_containsproperly", "st_covers", "st_overlaps"), + "st_containsproperly", "st_covers", "st_overlaps", "st_crosses", "st_equals", + "st_disjoint"), args = "", join = "left", tblname = tmp_tbl_name(), @@ -20,7 +21,7 @@ spatial_join( \item{y}{a duckdb table with a spatial geometry column called "geom"} -\item{by}{A spatial join function.} +\item{by}{A spatial join function, see details.} \item{args}{additional arguments to join function (e.g. distance for st_dwithin)} @@ -38,6 +39,25 @@ on using dplyr operations and call to_st() to collect this as an sf object. \description{ spatial_join } +\details{ +Possible \href{https://postgis.net/workshops/postgis-intro/spatial_relationships.html}{spatial joins} include: + +---------------|---------------------------------------------- +st_intersects | Geometry A intersects with geometry B +st_disjoint | The complement of intersects +st_within | Geometry A is within geometry B (complement of contains) +st_dwithin | Geometries are within a specified distance, expressed in the same units as the coordinate reference system of the geometries. +st_touches | Two polygons touch if the that have at least one point in common, even if their interiors do not touch. +st_contains | Geometry A entirely contains to geometry B. (complement of within) +st_containsproperly | stricter version of \code{st_contains} (boundary counts as external) +st_covers | Geometry B. is inside or on boundary of A. (A polygon covers a point on its boundary but does not contain it.) +st_overlaps | Geometry A intersects but does not completely contain geometry B +st_equals | Geometry A is equal to geometry B +st_crosses | Lines or points in geometry A cross geometry B. + +All though SQL is not case sensitive, this function expects only +lower case names for "by" functions. +} \examples{ \dontshow{if (interactive()) (if (getRversion() >= "3.4") withAutoprint else force)(\{ # examplesIf} @@ -56,10 +76,5 @@ cities <- countries |> dplyr::filter(iso_a3 == "AUS") |> spatial_join(cities) - -# cities within 10 degrees of Australia: -countries |> - dplyr::filter(iso_a3 == "AUS") |> - spatial_join(cities, by = "st_dwithin", args = 10) \dontshow{\}) # examplesIf} } From 6bf01db6f0beb8e7c7b9ee6500d05772b349c957 Mon Sep 17 00:00:00 2001 From: Carl Date: Wed, 6 Dec 2023 01:52:08 +0000 Subject: [PATCH 3/3] :duck: --- .github/workflows/R-CMD-check.yaml | 2 +- NAMESPACE | 1 + R/open_dataset.R | 5 + R/spatial_join.R | 103 ++++++------- README.Rmd | 25 +-- README.md | 185 ++++++++++++++++------- man/figures/README-unnamed-chunk-8-1.png | Bin 0 -> 52780 bytes man/spatial_join.Rd | 27 ++-- 8 files changed, 213 insertions(+), 135 deletions(-) create mode 100644 man/figures/README-unnamed-chunk-8-1.png diff --git a/.github/workflows/R-CMD-check.yaml b/.github/workflows/R-CMD-check.yaml index cf615c6..621e7e8 100644 --- a/.github/workflows/R-CMD-check.yaml +++ b/.github/workflows/R-CMD-check.yaml @@ -22,7 +22,7 @@ jobs: - {os: windows-latest, r: 'release'} - {os: ubuntu-latest, r: 'devel', http-user-agent: 'release'} - {os: ubuntu-latest, r: 'release'} - - {os: ubuntu-latest, r: 'oldrel-1'} +# - {os: ubuntu-latest, r: 'oldrel-1'} env: GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} diff --git a/NAMESPACE b/NAMESPACE index aa8b5de..b3b2166 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -6,5 +6,6 @@ export(close_connection) export(duckdb_s3_config) export(load_spatial) export(open_dataset) +export(spatial_join) export(to_sf) export(write_dataset) diff --git a/R/open_dataset.R b/R/open_dataset.R index 1d9b69b..4d87249 100644 --- a/R/open_dataset.R +++ b/R/open_dataset.R @@ -106,6 +106,10 @@ select_format <- function(sources, format) { # format for vector sources always based on first element sources <- sources[[1]] + # default to parquet for S3 addresses + if(grepl("^s3://", sources)) { + return("parquet") + } if( fs::is_dir(sources) ) { sources <- fs::dir_ls(sources, recurse = TRUE, type="file") @@ -122,6 +126,7 @@ select_format <- function(sources, format) { return("sf") } + # default if (format == "") { return("parquet") diff --git a/R/spatial_join.R b/R/spatial_join.R index eea1505..5aa865d 100644 --- a/R/spatial_join.R +++ b/R/spatial_join.R @@ -1,40 +1,3 @@ - -#' as_view -#' -#' Create a View of the current query. This can be an effective way to allow -#' a query chain to remain lazy -#' @param x a duckdb spatial dataset -#' @inheritParams open_dataset -#' @examplesIf interactive() -#' path <- system.file("extdata/spatial-test.csv", package="duckdbfs") -#' df <- open_dataset(path) -#' library(dplyr) -#' -#' df |> filter(latitude > 5) |> as_view() -#' -#' @export -as_view <- function(x, tblname = tmp_tbl_name(), conn = cached_connection()) { - - # assert x is a tbl_lazy, a tbl_sql, and a tbl_duckdb_connection - - ## lazy_base_query objects are good to go. - if(inherits(x$lazy_query, "lazy_base_query")) { - return(x) - } - ## lazy_select_query objects are unnamed, - ## convert to named views so we can re-use them in queries - q <- dbplyr::sql_render(x) - query_to_view(q, tblname, conn) -} - -query_to_view <- function(query, - tblname = tmp_tbl_name(), - conn = cached_connection()) { - q <- paste("CREATE OR REPLACE TEMPORARY VIEW", tblname, "AS", query) - DBI::dbSendQuery(conn, q) - dplyr::tbl(conn, tblname) -} - #' spatial_join #' #' @param x a duckdb table with a spatial geometry column called "geom" @@ -51,18 +14,19 @@ query_to_view <- function(query, #' #' Possible [spatial joins](https://postgis.net/workshops/postgis-intro/spatial_relationships.html) include: #' -#' ---------------|---------------------------------------------- -#' st_intersects | Geometry A intersects with geometry B -#' st_disjoint | The complement of intersects -#' st_within | Geometry A is within geometry B (complement of contains) -#' st_dwithin | Geometries are within a specified distance, expressed in the same units as the coordinate reference system of the geometries. -#' st_touches | Two polygons touch if the that have at least one point in common, even if their interiors do not touch. -#' st_contains | Geometry A entirely contains to geometry B. (complement of within) +#' Function | Description +#' -------------------- | -------------------------------------------------------------------------------------------- +#' st_intersects | Geometry A intersects with geometry B +#' st_disjoint | The complement of intersects +#' st_within | Geometry A is within geometry B (complement of contains) +#' st_dwithin | Geometries are within a specified distance, expressed in the same units as the coordinate reference system. +#' st_touches | Two polygons touch if the that have at least one point in common, even if their interiors do not touch. +#' st_contains | Geometry A entirely contains to geometry B. (complement of within) #' st_containsproperly | stricter version of `st_contains` (boundary counts as external) -#' st_covers | Geometry B. is inside or on boundary of A. (A polygon covers a point on its boundary but does not contain it.) -#' st_overlaps | Geometry A intersects but does not completely contain geometry B -#' st_equals | Geometry A is equal to geometry B -#' st_crosses | Lines or points in geometry A cross geometry B. +#' st_covers | geometry B is inside or on boundary of A. (A polygon covers a point on its boundary but does not contain it.) +#' st_overlaps | geometry A intersects but does not completely contain geometry B +#' st_equals | geometry A is equal to geometry B +#' st_crosses | Lines or points in geometry A cross geometry B. #' #' All though SQL is not case sensitive, this function expects only #' lower case names for "by" functions. @@ -85,6 +49,7 @@ query_to_view <- function(query, #' dplyr::filter(iso_a3 == "AUS") |> #' spatial_join(cities) #' +#' @export spatial_join <- function(x, y, by=c("st_intersects", "st_within", @@ -126,15 +91,39 @@ spatial_join <- function(x, } -# st_contains, -# ST_ContainsProperly -# st_intersects, -# st_within, -# st_dwithin, -# st_covers -# st_overlaps -# st_touches -# st_union is not a join operation +#' as_view +#' +#' Create a View of the current query. This can be an effective way to allow +#' a query chain to remain lazy +#' @param x a duckdb spatial dataset +#' @inheritParams open_dataset +#' @examplesIf interactive() +#' path <- system.file("extdata/spatial-test.csv", package="duckdbfs") +#' df <- open_dataset(path) +#' library(dplyr) +#' +#' df |> filter(latitude > 5) |> as_view() +#' +#' @export +as_view <- function(x, tblname = tmp_tbl_name(), conn = cached_connection()) { + # assert x is a tbl_lazy, a tbl_sql, and a tbl_duckdb_connection + ## lazy_base_query objects are good to go. + if(inherits(x$lazy_query, "lazy_base_query")) { + return(x) + } + ## lazy_select_query objects are unnamed, + ## convert to named views so we can re-use them in queries + q <- dbplyr::sql_render(x) + query_to_view(q, tblname, conn) +} + +query_to_view <- function(query, + tblname = tmp_tbl_name(), + conn = cached_connection()) { + q <- paste("CREATE OR REPLACE TEMPORARY VIEW", tblname, "AS", query) + DBI::dbSendQuery(conn, q) + dplyr::tbl(conn, tblname) +} diff --git a/README.Rmd b/README.Rmd index 283ea78..46c462b 100644 --- a/README.Rmd +++ b/README.Rmd @@ -183,18 +183,19 @@ countries |> Possible [spatial joins](https://postgis.net/workshops/postgis-intro/spatial_relationships.html) include: ----------------|---------------------------------------------- -st_intersects | Geometry A intersects with geometry B -st_disjoint | The complement of intersects -st_within | Geometry A is within geometry B (complement of contains) -st_dwithin | Geometries are within a specified distance, expressed in the same units as the coordinate reference system of the geometries. -st_touches | Two polygons touch if the that have at least one point in common, even if their interiors do not touch. -st_contains | Geometry A entirely contains to geometry B. (complement of within) -st_containsproperly | stricter version of `st_contains` (boundary counts as external) -st_covers | geometry B is inside or on boundary of A. (A polygon covers a point on its boundary but does not contain it.) -st_overlaps | geometry A intersects but does not completely contain geometry B -st_equals | geometry A is equal to geometry B -st_crosses | Lines or points in geometry A cross geometry B. + Function | Description +-------------------- | -------------------------------------------------------------------------------------------- + st_intersects | Geometry A intersects with geometry B + st_disjoint | The complement of intersects + st_within | Geometry A is within geometry B (complement of contains) + st_dwithin | Geometries are within a specified distance, expressed in the same units as the coordinate reference system. + st_touches | Two polygons touch if the that have at least one point in common, even if their interiors do not touch. + st_contains | Geometry A entirely contains to geometry B. (complement of within) + st_containsproperly | stricter version of `st_contains` (boundary counts as external) + st_covers | geometry B is inside or on boundary of A. (A polygon covers a point on its boundary but does not contain it.) + st_overlaps | geometry A intersects but does not completely contain geometry B + st_equals | geometry A is equal to geometry B + st_crosses | Lines or points in geometry A cross geometry B. Note that while SQL functions are not case-sensitive, `spatial_join` expects lower-case names. diff --git a/README.md b/README.md index c375ded..65d6184 100644 --- a/README.md +++ b/README.md @@ -61,13 +61,13 @@ explicitly request `duckdb` join the two schemas. Leave this as default, ``` r ds <- open_dataset(urls, unify_schemas = TRUE) ds -#> # Source: table [3 x 4] -#> # Database: DuckDB 0.8.1 [unknown@Linux 6.4.6-76060406-generic:R 4.3.1/:memory:] -#> i j x k -#> -#> 1 42 84 1 NA -#> 2 42 84 1 NA -#> 3 NA 128 2 33 +#> # Source: table [3 x 4] +#> # Database: DuckDB v0.9.2 [unknown@Linux 6.5.6-76060506-generic:R 4.3.2/:memory:] +#> i j x k +#> +#> 1 42 84 1 NA +#> 2 42 84 1 NA +#> 3 NA 128 2 33 ``` Use `filter()`, `select()`, etc from dplyr to subset and process data – @@ -107,12 +107,17 @@ efi <- open_dataset("s3://anonymous@neon4cast-scores/parquet/aquatics?endpoint_o `duckdb` can also understand a wide array of spatial data queries for spatial vector data, similar to operations found in the popular `sf` -package. Most spatial query operations require an geometry column that -expresses the simple feature geometry in `duckdb`’s internal geometry -format (nearly but not exactly WKB). A common pattern will first -generate the geometry column from raw columns, such as `latitude` and -`lognitude` columns, using the `duckdb` implementation of the a method -familiar to postgis, `ST_Point`: +package. See [the list of supported +functions](https://github.com/duckdb/duckdb_spatial#supported-functions) +for details. Most spatial query operations require an geometry column +that expresses the simple feature geometry in `duckdb`’s internal +geometry format (nearly but not exactly WKB). + +### Generating spatial data from tabular + +A common pattern will first generate the geometry column from raw +columns, such as `latitude` and `lognitude` columns, using the `duckdb` +implementation of the a method familiar to postgis, `st_point`: ``` r spatial_ex <- paste0("https://raw.githubusercontent.com/cboettig/duckdbfs/", @@ -120,51 +125,15 @@ spatial_ex <- paste0("https://raw.githubusercontent.com/cboettig/duckdbfs/", open_dataset(format = "csv") spatial_ex |> - mutate(geometry = ST_Point(longitude, latitude)) |> - to_sf() -#> Simple feature collection with 10 features and 3 fields -#> Geometry type: POINT -#> Dimension: XY -#> Bounding box: xmin: 1 ymin: 1 xmax: 10 ymax: 10 -#> CRS: NA -#> site latitude longitude geometry -#> 1 a 1 1 POINT (1 1) -#> 2 b 2 2 POINT (2 2) -#> 3 c 3 3 POINT (3 3) -#> 4 d 4 4 POINT (4 4) -#> 5 e 5 5 POINT (5 5) -#> 6 f 6 6 POINT (6 6) -#> 7 g 7 7 POINT (7 7) -#> 8 h 8 8 POINT (8 8) -#> 9 i 9 9 POINT (9 9) -#> 10 j 10 10 POINT (10 10) -``` - -Recall that when used against any sort of external database like -`duckdb`, most `dplyr` functions like `dplyr::mutate()` are being -transcribed into SQL by `dbplyr`, and not actually ever run in R. This -allows us to seamlessly pass along spatial functions like `ST_Point`, -despite this not being an available R function. The `to_sf()` coercion -will parse its input into a SQL query that gets passed to `duckdb`, and -the return object will be collected through `sf::st_read`, returning an -(in-memory) `sf` object. - -Note that we can add arbitrary spatial functions that operate on this -geometry, provided we do so prior to our call to `to_sf`. For instance, -here we first create our geometry column from lat/lon columns, and then -compute the distance from each element to a spatial point: - -``` r -spatial_ex |> - mutate(geometry = ST_Point(longitude, latitude)) |> - mutate(dist = ST_Distance(geometry, ST_Point(0,0))) |> + mutate(geometry = st_point(longitude, latitude)) |> + mutate(dist = st_distance(geometry, st_point(0,0))) |> to_sf() #> Simple feature collection with 10 features and 4 fields #> Geometry type: POINT #> Dimension: XY #> Bounding box: xmin: 1 ymin: 1 xmax: 10 ymax: 10 #> CRS: NA -#> site latitude longitude dist geometry +#> site latitude longitude dist geom #> 1 a 1 1 1.414214 POINT (1 1) #> 2 b 2 2 2.828427 POINT (2 2) #> 3 c 3 3 4.242641 POINT (3 3) @@ -177,11 +146,123 @@ spatial_ex |> #> 10 j 10 10 14.142136 POINT (10 10) ``` +Recall that when used against any sort of external database like +`duckdb`, most `dplyr` functions like `dplyr::mutate()` are being +transcribed into SQL by `dbplyr`, and not actually ever run in R. This +allows us to seamlessly pass along spatial functions like `st_point`, +despite this not being an available R function. (Also note that SQL is +not case-sensitive, so this function is also written as `ST_Point`). +Optionally, we can do additional operations on this geometry column, +such as computing distances (`st_distance` shown here), spatial filters, +and so forth. The `to_sf()` coercion will parse its input into a SQL +query that gets passed to `duckdb`, and the return object will be +collected through `sf::st_read`, returning an (in-memory) `sf` object. + For more details including a complete list of the dozens of spatial operations currently supported and notes on performance and current limitations, see the [duckdb spatial docs](https://github.com/duckdb/duckdb_spatial) +### Reading spatial vector files + +The `duckdb` spatial package can also use GDAL to read large spatial +vector files. This includes support for the GDAL virtual filesystem. +This means that we can easily subset columns from a wide array of +potentially remote file types and filter on rows and columns, and +perform many spatial operations without ever reading the entire objects +into memory in R. + +To read spatial vector (simple feature) files, indicate `format="sf"`. +Use virtual filesystem prefixes to access range requests over http, S3, +and other such systems. + +``` r +url <- "https://github.com/cboettig/duckdbfs/raw/25744032021cc2b9bbc560f95b77b3eb088c9abb/inst/extdata/world.gpkg" + +countries <- + paste0("/vsicurl/", url) |> + open_dataset(format="sf") +``` + +Which country polygon contains Melbourne? Note the result is still a +lazy read, we haven’t downloaded or read in the full spatial data +object. + +``` r +library(sf) +#> Linking to GEOS 3.10.2, GDAL 3.4.1, PROJ 8.2.1; sf_use_s2() is TRUE +melbourne <- st_point(c(144.9633, -37.814)) |> st_as_text() + +countries |> + filter(st_contains(geom, ST_GeomFromText({melbourne}))) +#> # Source: SQL [1 x 16] +#> # Database: DuckDB v0.9.2 [unknown@Linux 6.5.6-76060506-generic:R 4.3.2/:memory:] +#> iso_a3 name sovereignt continent area pop_est pop_est_dens economy +#> +#> 1 AUS Australia Australia Oceania 7682300 21262641 2.77 2. Develo… +#> # ℹ 8 more variables: income_grp , gdp_cap_est , life_exp , +#> # well_being , footprint , inequality , HPI , geom +``` + +As before, we use `to_sf()` to read in the query results as a native +(in-memory) `sf` object: + +``` r +sf_obj <- countries |> filter(continent == "Africa") |> to_sf() +plot(sf_obj["name"]) +``` + + + +## Spatial joins + +One very common operation are spatial joins, which can be a very +powerful way to subset large data. For instance, we can return all +points (cities) within a set of polygons + +``` r +cities <- + paste0("/vsicurl/https://github.com/cboettig/duckdbfs/raw/", + "spatial-read/inst/extdata/metro.fgb") |> + open_dataset(format = "sf") + +countries |> + dplyr::filter(continent == "Oceania") |> + spatial_join(cities, by = "st_intersects", join="inner") |> + select(name_long, sovereignt, pop2020) +#> # Source: SQL [6 x 3] +#> # Database: DuckDB v0.9.2 [unknown@Linux 6.5.6-76060506-generic:R 4.3.2/:memory:] +#> name_long sovereignt pop2020 +#> +#> 1 Brisbane Australia 2388517 +#> 2 Perth Australia 2036118 +#> 3 Sydney Australia 4729406 +#> 4 Adelaide Australia 1320783 +#> 5 Auckland New Zealand 1426070 +#> 6 Melbourne Australia 4500501 +``` + +Possible [spatial +joins](https://postgis.net/workshops/postgis-intro/spatial_relationships.html) +include: + +| Function | Description | +|---------------------|---------------------------------------------------------------------------------------------------------------| +| st_intersects | Geometry A intersects with geometry B | +| st_disjoint | The complement of intersects | +| st_within | Geometry A is within geometry B (complement of contains) | +| st_dwithin | Geometries are within a specified distance, expressed in the same units as the coordinate reference system. | +| st_touches | Two polygons touch if the that have at least one point in common, even if their interiors do not touch. | +| st_contains | Geometry A entirely contains to geometry B. (complement of within) | +| st_containsproperly | stricter version of `st_contains` (boundary counts as external) | +| st_covers | geometry B is inside or on boundary of A. (A polygon covers a point on its boundary but does not contain it.) | +| st_overlaps | geometry A intersects but does not completely contain geometry B | +| st_equals | geometry A is equal to geometry B | +| st_crosses | Lines or points in geometry A cross geometry B. | + +Note that while SQL functions are not case-sensitive, `spatial_join` +expects lower-case names. + ## Writing datasets Like `arrow::write_dataset()`, `duckdbfs::write_dataset()` can write diff --git a/man/figures/README-unnamed-chunk-8-1.png b/man/figures/README-unnamed-chunk-8-1.png new file mode 100644 index 0000000000000000000000000000000000000000..ba26f8011dce88ea6ec0fed1482294ce48045116 GIT binary patch literal 52780 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

p{9PA`p{AwfAIRgXB_4V~NG&ErSjv){i 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[akq}gz#1%| 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