Skip to content

Commit 870ee74

Browse files
authored
Merge branch 'new-feats' into feature/cran-issue
2 parents f292e28 + 37077db commit 870ee74

34 files changed

Lines changed: 3691 additions & 78 deletions

.claude/CLAUDE.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,18 @@ Tests use **testthat edition 3**. `tests/testthat/setup.R` loads shared spatial
9191

9292
## Adding a New Function
9393

94-
When implementing a new exported function, complete **all four steps** before considering the task done.
94+
When implementing a new exported function, first write a plan (step 0), get it approved, then complete the remaining steps before considering the task done.
95+
96+
### 0. Plan first (before writing any code)
97+
98+
Present a short, written plan and wait for approval before implementing. The plan should cover:
99+
100+
- **Name**: the proposed `ddbs_*` name, and whether it should follow an existing family convention (e.g. `ddbs_line_*` for linestring ops, `ddbs_get_n*` for counts) rather than a bare mirror of the `ST_*` name.
101+
- **R file**: which `R/` script it goes in (see Key File Map).
102+
- **pkgdown**: which `reference:` section in `_pkgdown.yml` it belongs to.
103+
- **Macro**: whether to also register a DuckDB macro (in `R/utils_not_exported.R`) so it works inside `dplyr::mutate()`/`summarise()`.
104+
105+
Before presenting the plan, verify the DuckDB function empirically (signature, return type, CRS behaviour, edge cases) with a quick query rather than assuming.
95106

96107
### 1. Implementation and documentation
97108

DESCRIPTION

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Type: Package
22
Package: duckspatial
33
Title: R Interface to 'DuckDB' Database with Spatial Extension
4-
Version: 1.1.3
4+
Version: 1.1.2.9000
55
Authors@R: c(
66
person(
77
"Adrián", "Cidre González",
@@ -56,6 +56,7 @@ Suggests:
5656
areal,
5757
bench,
5858
duckdbfs,
59+
geojsonsf,
5960
ggplot2 (>= 3.3.1),
6061
knitr,
6162
lwgeom,

NAMESPACE

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export(ddbs_affine)
4949
export(ddbs_area)
5050
export(ddbs_as_geojson)
5151
export(ddbs_as_hexwkb)
52+
export(ddbs_as_mvt_geom)
5253
export(ddbs_as_points)
5354
export(ddbs_as_text)
5455
export(ddbs_as_wkb)
@@ -80,6 +81,7 @@ export(ddbs_dump)
8081
export(ddbs_endpoint)
8182
export(ddbs_envelope)
8283
export(ddbs_equals)
84+
export(ddbs_extension_info)
8385
export(ddbs_exterior_ring)
8486
export(ddbs_filter)
8587
export(ddbs_flip)
@@ -89,8 +91,14 @@ export(ddbs_force_3d)
8991
export(ddbs_force_4d)
9092
export(ddbs_generate_points)
9193
export(ddbs_geom_col)
94+
export(ddbs_geom_from_geojson)
95+
export(ddbs_geom_from_hexewkb)
96+
export(ddbs_geom_from_hexwkb)
97+
export(ddbs_geom_from_text)
98+
export(ddbs_geom_from_wkb)
9299
export(ddbs_geometry_type)
93100
export(ddbs_get_ngeometries)
101+
export(ddbs_get_ninterior_rings)
94102
export(ddbs_get_npoints)
95103
export(ddbs_get_resources)
96104
export(ddbs_glimpse)
@@ -99,6 +107,7 @@ export(ddbs_has_z)
99107
export(ddbs_install)
100108
export(ddbs_interpolate_aw)
101109
export(ddbs_intersection)
110+
export(ddbs_intersection_agg)
102111
export(ddbs_intersects)
103112
export(ddbs_intersects_extent)
104113
export(ddbs_is_closed)
@@ -113,6 +122,7 @@ export(ddbs_line_endpoint)
113122
export(ddbs_line_interpolate)
114123
export(ddbs_line_locate_point)
115124
export(ddbs_line_merge)
125+
export(ddbs_line_node)
116126
export(ddbs_line_startpoint)
117127
export(ddbs_line_substring)
118128
export(ddbs_list_tables)
@@ -129,6 +139,7 @@ export(ddbs_minimum_rotated_rectangle)
129139
export(ddbs_mmax)
130140
export(ddbs_mmin)
131141
export(ddbs_multi)
142+
export(ddbs_normalize)
132143
export(ddbs_open_dataset)
133144
export(ddbs_options)
134145
export(ddbs_overlaps)
@@ -140,9 +151,11 @@ export(ddbs_quadkey)
140151
export(ddbs_read_meta)
141152
export(ddbs_read_table)
142153
export(ddbs_read_vector)
154+
export(ddbs_reduce_precision)
143155
export(ddbs_register_table)
144156
export(ddbs_register_vector)
145157
export(ddbs_remove_repeated_points)
158+
export(ddbs_reverse)
146159
export(ddbs_rotate)
147160
export(ddbs_rotate_3d)
148161
export(ddbs_scale)
@@ -165,6 +178,7 @@ export(ddbs_voronoi)
165178
export(ddbs_within)
166179
export(ddbs_within_properly)
167180
export(ddbs_write_dataset)
181+
export(ddbs_write_mbtiles)
168182
export(ddbs_write_table)
169183
export(ddbs_write_vector)
170184
export(ddbs_x)

NEWS.md

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,35 @@
11

2-
# duckspatial 1.1.3
2+
3+
4+
# duckspatial (development version)
5+
6+
## NEW FEATURES
7+
8+
* `ddbs_extension_info()`: prints a `glimpse()` of a DuckDB extension's row from `duckdb_extensions()` (the spatial extension by default), showing its installed/loaded status, version, and install path.
9+
10+
* `ddbs_reduce_precision()`: snaps geometry coordinates to a regular grid, reducing their precision.
11+
12+
* `ddbs_line_node()`: nodes a set of line geometries, splitting them at every crossing and returning a fully noded `MULTILINESTRING`.
13+
14+
* `ddbs_intersection_agg()`: computes the geometric intersection (common area) of a set of geometries, optionally grouped by one or more columns. The intersection counterpart to `ddbs_union_agg()`.
15+
16+
* `ddbs_reverse()`: returns each geometry with the order of its vertices reversed.
17+
18+
* `ddbs_normalize()`: returns each geometry in its normalized (canonical) form.
19+
20+
* `ddbs_write_mbtiles()`: generates a Mapbox Vector Tile pyramid from a spatial dataset and writes it to an MBTiles file, ready to serve or convert to PMTiles.
21+
22+
* `ddbs_as_mvt_geom()`: transforms geometries into Mapbox Vector Tile (MVT) coordinate space, clipping them to a tile's bounding box and mapping the coordinates into the tile's integer pixel space.
23+
24+
* `ddbs_geom_from_text()`, `ddbs_geom_from_wkb()`, `ddbs_geom_from_hexwkb()`, `ddbs_geom_from_hexewkb()`, `ddbs_geom_from_geojson()`: parse serialized geometries (WKT, WKB, HEXWKB, HEXEWKB, GeoJSON) into a spatial object. These are the inverses of the `ddbs_as_*()` serializers.
25+
26+
* `ddbs_get_ninterior_rings()`: returns the number of interior rings (holes) in a POLYGON geometry.
27+
28+
## ENHANCEMENTS
29+
30+
* `ddbs_install()`: gains a `repos` argument to install an extension from a specific DuckDB repository (e.g. `"core"`, `"core_nightly"`, `"community"`). When `NULL` (default), the previous behaviour is kept (core, then community) (#144).
31+
32+
* `ddbs_as_geojson()`: now includes all non-geometry columns as feature `properties` instead of serializing only the geometry. By default it returns a single GeoJSON `FeatureCollection` (matching `geojsonsf::sf_geojson()`); pass `feature_collection = FALSE` for a vector with one `Feature` per row (#141).
333

434
## BUG FIXES
535

R/db_extension.R

Lines changed: 113 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@
77
#' @param upgrade if TRUE, it upgrades the DuckDB extension to the latest version
88
#' @template quiet
99
#' @param extension name of the extension to install, default is "spatial"
10+
#' @param repos optional character string naming the repository to install the
11+
#' extension from (e.g. \code{"core"}, \code{"core_nightly"}, or
12+
#' \code{"community"}); a URL or path can also be supplied. If \code{NULL}
13+
#' (default), the \code{core} repository is tried first, then \code{community}.
14+
#' Switching an already-installed extension to a different repository requires
15+
#' \code{upgrade = TRUE}. See
16+
#' \url{https://duckdb.org/docs/stable/extensions/installing_extensions}.
1017
#'
1118
#' @returns TRUE (invisibly) for successful installation
1219
#' @export
@@ -32,10 +39,11 @@
3239
#' duckdb::dbDisconnect(conn)
3340
#' }
3441
ddbs_install <- function(
35-
conn,
36-
upgrade = FALSE,
37-
quiet = FALSE,
38-
extension = "spatial"
42+
conn,
43+
upgrade = FALSE,
44+
quiet = FALSE,
45+
extension = "spatial",
46+
repos = NULL
3947
) {
4048

4149
# 1. Get extensions list
@@ -44,6 +52,7 @@ ddbs_install <- function(
4452
# 2. Checks
4553
## 2.1. Check connection
4654
dbConnCheck(conn)
55+
if (!is.null(repos)) assert_character_scalar(repos, "repos")
4756
## 2.2. Check if it's installed / needs upgrade
4857
target_ext <- ext[ext$extension_name == extension, ]
4958
if (nrow(target_ext) == 1 && target_ext$installed) {
@@ -66,7 +75,9 @@ ddbs_install <- function(
6675
isTRUE(result$install_mode == "repository" && !result$requires_version_upgrade)
6776
}, error = function(e) FALSE)
6877

69-
if (isTRUE(latest)) {
78+
# Skip the short-circuit when an explicit repository is requested, so the
79+
# forced install from `repos` still runs (e.g. switching core -> core_nightly)
80+
if (isTRUE(latest) && is.null(repos)) {
7081
if (isFALSE(quiet)) {
7182
cli::cli_alert_info(
7283
"{extension} extension version {.val {target_ext$extension_version}} is already the latest version."
@@ -81,51 +92,56 @@ ddbs_install <- function(
8192
cli::cli_abort("{extension} is already loaded in the connection. Upgrading the version is only allowed in non-loaded connections.")
8293
}
8394

84-
# 3. For non-upgrade installs, try LOAD first — core extensions (like spatial
85-
# in DuckDB >= 1.5) are bundled and loadable without writing to disk.
86-
# This avoids filesystem writes that fail in read-only environments (e.g. CRAN).
87-
if (!upgrade) {
88-
load_ok <- tryCatch({
89-
suppressMessages(DBI::dbExecute(conn, glue::glue("LOAD {extension};")))
90-
TRUE
91-
}, error = function(e) FALSE)
95+
# 3. Install/upgrade extension
96+
install_kw <- if (upgrade) "FORCE INSTALL" else "INSTALL"
9297

93-
if (load_ok) {
94-
if (isFALSE(quiet)) {
95-
cli::cli_alert_success("{extension} extension installed")
96-
}
97-
return(invisible(TRUE))
98-
}
99-
}
98+
if (!is.null(repos)) {
99+
100+
## 3a. Explicit repository chosen by the user (no fallback). Named repos
101+
## (core, core_nightly, community, ...) are bare identifiers; URLs/paths
102+
## must be single-quoted.
103+
repos_sql <- if (grepl("^[A-Za-z0-9_]+$", repos)) repos else ddbs_quote_sql_string(conn, repos)
100104

101-
# 4. Install/upgrade extension - try core, then community, then error
102-
install_sql <- if (upgrade) "FORCE INSTALL {extension};" else "INSTALL {extension};"
103-
community_sql <- if (upgrade) "FORCE INSTALL {extension} FROM community;" else "INSTALL {extension} FROM community;"
104-
105-
installed <- tryCatch({
106-
suppressMessages(DBI::dbExecute(conn, glue::glue(install_sql)))
107-
"core"
108-
}, error = function(e) {
109-
tryCatch({
110-
suppressMessages(DBI::dbExecute(conn, glue::glue(community_sql)))
111-
"community"
112-
}, error = function(e2) {
113-
NULL
105+
installed <- tryCatch({
106+
suppressMessages(DBI::dbExecute(conn, glue::glue("{install_kw} {extension} FROM {repos_sql};")))
107+
repos
108+
}, error = function(e) {
109+
cli::cli_abort(c(
110+
"Failed to {if (upgrade) 'upgrade' else 'install'} the {extension} extension from the {.val {repos}} repository.",
111+
"x" = conditionMessage(e),
112+
"i" = "If {extension} is already installed from a different repository, set {.code upgrade = TRUE} to switch.",
113+
"i" = "Check the repository name: {.url https://duckdb.org/docs/stable/extensions/installing_extensions}"
114+
))
114115
})
115-
})
116-
117-
if (is.null(installed)) {
118-
cli::cli_abort(c(
119-
"Failed to {if (upgrade) 'upgrade' else 'install'} the {extension} extension.",
120-
"i" = "It could not be found in the core or community repositories.",
121-
"i" = "It might not be available for this version of DuckDB",
122-
"i" = "Check that the extension name is correct: {.url https://duckdb.org/docs/extensions/overview}"
123-
))
116+
117+
} else {
118+
119+
## 3b. Default: try core, then community, then error
120+
installed <- tryCatch({
121+
suppressMessages(DBI::dbExecute(conn, glue::glue("{install_kw} {extension};")))
122+
"core"
123+
}, error = function(e) {
124+
tryCatch({
125+
suppressMessages(DBI::dbExecute(conn, glue::glue("{install_kw} {extension} FROM community;")))
126+
"community"
127+
}, error = function(e2) {
128+
NULL
129+
})
130+
})
131+
132+
if (is.null(installed)) {
133+
cli::cli_abort(c(
134+
"Failed to {if (upgrade) 'upgrade' else 'install'} the {extension} extension.",
135+
"i" = "It could not be found in the core or community repositories.",
136+
"i" = "It might not be available for this version of DuckDB",
137+
"i" = "Check that the extension name is correct: {.url https://duckdb.org/docs/extensions/overview}"
138+
))
139+
}
124140
}
125141

126142
if (isFALSE(quiet)) {
127143
action <- if (upgrade) "upgraded" else "installed"
128-
repo_note <- if (installed == "community") " (from community repository)" else ""
144+
repo_note <- if (!identical(installed, "core")) glue::glue(" (from {installed} repository)") else ""
129145
cli::cli_alert_success("{extension} extension {action}{repo_note}")
130146
}
131147

@@ -198,3 +214,57 @@ ddbs_load <- function(
198214
}
199215

200216
}
217+
218+
219+
220+
#' Glimpse the status of a DuckDB extension
221+
#'
222+
#' Retrieves the row from DuckDB's \code{duckdb_extensions()} catalog for a given
223+
#' extension (the spatial extension by default) and prints a transposed
224+
#' \code{\link[dplyr]{glimpse}} of it: whether it is installed and loaded, its
225+
#' version, install path, description, and so on.
226+
#'
227+
#' @template conn_null
228+
#' @param extension name of the extension to inspect, default is "spatial"
229+
#'
230+
#' @returns A one-row \code{tibble} with the extension's metadata (invisibly).
231+
#' Called mainly for the glimpse printed as a side effect.
232+
#' @export
233+
#'
234+
#' @examples
235+
#' \dontrun{
236+
#' ## load package
237+
#' library(duckspatial)
238+
#'
239+
#' # inspect the spatial extension on the default connection
240+
#' ddbs_extension_info()
241+
#'
242+
#' # or pass an explicit connection
243+
#' conn <- ddbs_create_conn()
244+
#' ddbs_extension_info(conn)
245+
#' ddbs_stop_conn(conn)
246+
#' }
247+
ddbs_extension_info <- function(conn = NULL, extension = "spatial") {
248+
249+
# 1. Resolve and validate inputs
250+
conn <- conn %||% ddbs_default_conn()
251+
dbConnCheck(conn)
252+
assert_character_scalar(extension, "extension")
253+
254+
# 2. Query the extension catalog
255+
info <- DBI::dbGetQuery(
256+
conn,
257+
"SELECT * FROM duckdb_extensions() WHERE extension_name = ?",
258+
params = list(extension)
259+
)
260+
261+
if (nrow(info) == 0) {
262+
cli::cli_abort(
263+
"Extension {.val {extension}} was not found in {.fn duckdb_extensions}."
264+
)
265+
}
266+
267+
# 3. Glimpse and return the row invisibly
268+
dplyr::glimpse(tibble::as_tibble(info))
269+
270+
}

0 commit comments

Comments
 (0)