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"
+  )
+})

Reply via email to