This is an automated email from the ASF dual-hosted git repository.
jiayu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git
The following commit(s) were added to refs/heads/main by this push:
new b7432e4 feat(r/sedonadb): Add support for runtime linking of PROJ
(#166)
b7432e4 is described below
commit b7432e4306c3e4dad8df14096e2b5bf58f33dd12
Author: Dewey Dunnington <[email protected]>
AuthorDate: Tue Sep 30 01:08:31 2025 -0500
feat(r/sedonadb): Add support for runtime linking of PROJ (#166)
Co-authored-by: Copilot <[email protected]>
---
Cargo.lock | 1 +
r/sedonadb/NAMESPACE | 1 +
r/sedonadb/R/000-wrappers.R | 5 ++
r/sedonadb/R/context.R | 114 +++++++++++++++++++++++++++++++
r/sedonadb/man/sd_configure_proj.Rd | 44 ++++++++++++
r/sedonadb/src/init.c | 10 +++
r/sedonadb/src/rust/Cargo.toml | 1 +
r/sedonadb/src/rust/api.h | 3 +
r/sedonadb/src/rust/src/lib.rs | 25 +++++++
r/sedonadb/tests/testthat/test-context.R | 17 +++++
10 files changed, 221 insertions(+)
diff --git a/Cargo.lock b/Cargo.lock
index 4a0603c..56a5058 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5164,6 +5164,7 @@ dependencies = [
"sedona-adbc",
"sedona-expr",
"sedona-geoparquet",
+ "sedona-proj",
"sedona-schema",
"thiserror 2.0.16",
"tokio",
diff --git a/r/sedonadb/NAMESPACE b/r/sedonadb/NAMESPACE
index 0be588e..262b6dd 100644
--- a/r/sedonadb/NAMESPACE
+++ b/r/sedonadb/NAMESPACE
@@ -19,6 +19,7 @@ S3method(print,sedonadb_dataframe)
export(as_sedonadb_dataframe)
export(sd_collect)
export(sd_compute)
+export(sd_configure_proj)
export(sd_count)
export(sd_drop_view)
export(sd_preview)
diff --git a/r/sedonadb/R/000-wrappers.R b/r/sedonadb/R/000-wrappers.R
index a375d29..7f71799 100644
--- a/r/sedonadb/R/000-wrappers.R
+++ b/r/sedonadb/R/000-wrappers.R
@@ -55,6 +55,11 @@ NULL
}
+`configure_proj_shared` <- function(`shared_library_path` = NULL,
`database_path` = NULL, `search_path` = NULL) {
+ invisible(.Call(savvy_configure_proj_shared__impl, `shared_library_path`,
`database_path`, `search_path`))
+}
+
+
`init_r_runtime_interrupts` <- function(`interrupts_call`, `pkg_env`) {
invisible(.Call(savvy_init_r_runtime_interrupts__impl, `interrupts_call`,
`pkg_env`))
}
diff --git a/r/sedonadb/R/context.R b/r/sedonadb/R/context.R
index 2b6342b..86a55f8 100644
--- a/r/sedonadb/R/context.R
+++ b/r/sedonadb/R/context.R
@@ -93,3 +93,117 @@ ctx <- function() {
global_ctx <- new.env(parent = emptyenv())
global_ctx$ctx <- NULL
+
+
+
+#' Configure PROJ
+#'
+#' Performs a runtime configuration of PROJ, which can be used in place of
+#' a build-time linked version of PROJ or to add in support if PROJ was
+#' not linked at build time.
+#'
+#' @param preset One of:
+#' - `"homebrew"`: Look for PROJ installed by Homebrew. This is the easiest
+#' option on MacOS.
+#' - `"system"`: Look for PROJ in the platform library load path (e.g.,
+#' after installing system proj on Linux).
+#' - `"auto"`: Try all presets in the order listed above, issuing a warning
+#' if none can be configured.
+#' @param shared_library An absolute or relative path to a shared library
+#' valid for the platform.
+#' @param database_path A path to proj.db
+#' @param search_path A path to the data files required by PROJ for some
+#' transforms.
+#'
+#' @returns NULL, invisibly
+#' @export
+#'
+#' @examples
+#' sd_configure_proj("auto")
+#'
+sd_configure_proj <- function(preset = NULL,
+ shared_library = NULL,
+ database_path = NULL,
+ search_path = NULL) {
+ if (!is.null(preset)) {
+ switch (preset,
+ homebrew = {
+ configure_proj_prefix(Sys.getenv("HOMEBREW_PREFIX", "/opt/homebrew"))
+ return(invisible(NULL))
+ },
+ system = {
+ configure_proj_system()
+ return(invisible(NULL))
+ },
+ auto = {
+ presets <- c("homebrew", "system")
+ errors <- c()
+ for (preset in presets) {
+ maybe_err <- try(sd_configure_proj(preset), silent = TRUE)
+ if (!inherits(maybe_err, "try-error")) {
+ return(invisible(NULL))
+ } else {
+ errors <- c(errors, sprintf("%s: %s", preset, maybe_err))
+ }
+ }
+
+ packageStartupMessage(
+ sprintf(
+ "Failed to configure PROJ (tried %s):\n%s",
+ paste0("'", presets, "'", collapse = ", "),
+ paste0(errors, collapse = "\n")
+ )
+ )
+
+ return(invisible(NULL))
+ },
+ stop(sprintf("Unknown preset: '%s'", preset))
+ )
+ }
+
+ # We could check a shared library with dyn.load(), but this may error for
+ # valid system PROJ that isn't an absolute filename.
+
+ if (!is.null(database_path)) {
+ if (!file.exists(database_path)) {
+ stop(sprintf("Invalid database path: '%s' does not exist",
database_path))
+ }
+ }
+
+ if (!is.null(search_path)) {
+ if (!dir.exists(search_path)) {
+ stop(sprintf("Invalid search path: '%s' does not exist", search_path))
+ }
+ }
+
+ configure_proj_shared(
+ shared_library_path = shared_library,
+ database_path = database_path,
+ search_path = search_path
+ )
+}
+
+configure_proj_system <- function() {
+ sd_configure_proj(shared_library = proj_dll_name())
+}
+
+configure_proj_prefix <- function(prefix) {
+ if (!dir.exists(prefix)) {
+ stop(sprintf("Can't configure PROJ from prefix '%s': does not exist",
prefix))
+ }
+
+ sd_configure_proj(
+ shared_library = file.path(prefix, "lib", proj_dll_name()),
+ database_path = file.path(prefix, "share", "proj", "proj.db"),
+ search_path = file.path(prefix, "share", "proj")
+ )
+}
+
+proj_dll_name <- function() {
+ switch(tolower(Sys.info()[["sysname"]]),
+ windows = "proj.dll",
+ darwin = "libproj.dylib",
+ linux = "libproj.so",
+ stop(sprintf("Can't determine system PROJ shared library name for OS: %s",
Sys.info()[["sysname"]]))
+ )
+}
diff --git a/r/sedonadb/man/sd_configure_proj.Rd
b/r/sedonadb/man/sd_configure_proj.Rd
new file mode 100644
index 0000000..2ddb2b1
--- /dev/null
+++ b/r/sedonadb/man/sd_configure_proj.Rd
@@ -0,0 +1,44 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/context.R
+\name{sd_configure_proj}
+\alias{sd_configure_proj}
+\title{Configure PROJ}
+\usage{
+sd_configure_proj(
+ preset = NULL,
+ shared_library = NULL,
+ database_path = NULL,
+ search_path = NULL
+)
+}
+\arguments{
+\item{preset}{One of:
+\itemize{
+\item \code{"homebrew"}: Look for PROJ installed by Homebrew. This is the
easiest
+option on MacOS.
+\item \code{"system"}: Look for PROJ in the platform library load path (e.g.,
+after installing system proj on Linux).
+\item \code{"auto"}: Try all presets in the order listed above, issuing a
warning
+if none can be configured.
+}}
+
+\item{shared_library}{An absolute or relative path to a shared library
+valid for the platform.}
+
+\item{database_path}{A path to proj.db}
+
+\item{search_path}{A path to the data files required by PROJ for some
+transforms.}
+}
+\value{
+NULL, invisibly
+}
+\description{
+Performs a runtime configuration of PROJ, which can be used in place of
+a build-time linked version of PROJ or to add in support if PROJ was
+not linked at build time.
+}
+\examples{
+sd_configure_proj("auto")
+
+}
diff --git a/r/sedonadb/src/init.c b/r/sedonadb/src/init.c
index 2434cda..e4f02bd 100644
--- a/r/sedonadb/src/init.c
+++ b/r/sedonadb/src/init.c
@@ -51,6 +51,14 @@ SEXP handle_result(SEXP res_) {
return (SEXP)res;
}
+SEXP savvy_configure_proj_shared__impl(SEXP c_arg__shared_library_path,
+ SEXP c_arg__database_path,
+ SEXP c_arg__search_path) {
+ SEXP res = savvy_configure_proj_shared__ffi(
+ c_arg__shared_library_path, c_arg__database_path, c_arg__search_path);
+ return handle_result(res);
+}
+
SEXP savvy_init_r_runtime__impl(DllInfo *c_arg___dll_info) {
SEXP res = savvy_init_r_runtime__ffi(c_arg___dll_info);
return handle_result(res);
@@ -156,6 +164,8 @@ SEXP savvy_InternalDataFrame_to_view__impl(SEXP self__,
SEXP c_arg__ctx,
}
static const R_CallMethodDef CallEntries[] = {
+ {"savvy_configure_proj_shared__impl",
+ (DL_FUNC)&savvy_configure_proj_shared__impl, 3},
{"savvy_init_r_runtime_interrupts__impl",
(DL_FUNC)&savvy_init_r_runtime_interrupts__impl, 2},
{"savvy_sedonadb_adbc_init_func__impl",
diff --git a/r/sedonadb/src/rust/Cargo.toml b/r/sedonadb/src/rust/Cargo.toml
index 681621e..280c3ca 100644
--- a/r/sedonadb/src/rust/Cargo.toml
+++ b/r/sedonadb/src/rust/Cargo.toml
@@ -34,6 +34,7 @@ sedona = { path = "../../../../rust/sedona" }
sedona-adbc = { path = "../../../../rust/sedona-adbc" }
sedona-expr = { path = "../../../../rust/sedona-expr" }
sedona-geoparquet = { path = "../../../../rust/sedona-geoparquet" }
+sedona-proj = { path = "../../../../c/sedona-proj", default-features = false }
sedona-schema = { path = "../../../../rust/sedona-schema" }
thiserror = { workspace = true }
tokio = { workspace = true }
diff --git a/r/sedonadb/src/rust/api.h b/r/sedonadb/src/rust/api.h
index 4138988..303d30a 100644
--- a/r/sedonadb/src/rust/api.h
+++ b/r/sedonadb/src/rust/api.h
@@ -15,6 +15,9 @@
// specific language governing permissions and limitations
// under the License.
+SEXP savvy_configure_proj_shared__ffi(SEXP c_arg__shared_library_path,
+ SEXP c_arg__database_path,
+ SEXP c_arg__search_path);
SEXP savvy_init_r_runtime__ffi(DllInfo *c_arg___dll_info);
SEXP savvy_init_r_runtime_interrupts__ffi(SEXP c_arg__interrupts_call,
SEXP c_arg__pkg_env);
diff --git a/r/sedonadb/src/rust/src/lib.rs b/r/sedonadb/src/rust/src/lib.rs
index edc2913..727c36b 100644
--- a/r/sedonadb/src/rust/src/lib.rs
+++ b/r/sedonadb/src/rust/src/lib.rs
@@ -22,6 +22,7 @@ use savvy::savvy;
use savvy_ffi::R_NilValue;
use sedona_adbc::AdbcSedonadbDriverInit;
+use sedona_proj::register::{configure_global_proj_engine,
ProjCrsEngineBuilder};
mod context;
mod dataframe;
@@ -40,3 +41,27 @@ fn sedonadb_adbc_init_func() -> savvy::Result<savvy::Sexp> {
)))
}
}
+
+#[savvy]
+fn configure_proj_shared(
+ shared_library_path: Option<&str>,
+ database_path: Option<&str>,
+ search_path: Option<&str>,
+) -> savvy::Result<()> {
+ let mut builder = ProjCrsEngineBuilder::default();
+
+ if let Some(shared_library_path) = shared_library_path {
+ builder = builder.with_shared_library(shared_library_path.into());
+ }
+
+ if let Some(database_path) = database_path {
+ builder = builder.with_database_path(database_path.into());
+ }
+
+ if let Some(search_path) = search_path {
+ builder = builder.with_search_paths(vec![search_path.into()]);
+ }
+
+ configure_global_proj_engine(builder)?;
+ Ok(())
+}
diff --git a/r/sedonadb/tests/testthat/test-context.R
b/r/sedonadb/tests/testthat/test-context.R
index b6ea4e6..d0e1554 100644
--- a/r/sedonadb/tests/testthat/test-context.R
+++ b/r/sedonadb/tests/testthat/test-context.R
@@ -39,3 +39,20 @@ test_that("views can be created and dropped", {
expect_error(sd_sql("SELECT * FROM foofy"), "table '(.*?)' not found")
expect_error(sd_view("foofy"), "No table named 'foofy'")
})
+
+test_that("configure_proj() errors for invalid inputs", {
+ expect_error(
+ sd_configure_proj("not a preset"),
+ "Unknown preset"
+ )
+
+ expect_error(
+ sd_configure_proj(database_path = "file that does not exist"),
+ "Invalid database path"
+ )
+
+ expect_error(
+ sd_configure_proj(search_path = "dir that does not exist"),
+ "Invalid search path"
+ )
+})