This is an automated email from the ASF dual-hosted git repository.

paleolimbot 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 de33ed69 feat(r/sedonadb): Add R bindings for parameterized queries 
(#662)
de33ed69 is described below

commit de33ed69aec4321323dba05b569113bad06252cc
Author: Dewey Dunnington <[email protected]>
AuthorDate: Wed Feb 25 09:38:32 2026 -0600

    feat(r/sedonadb): Add R bindings for parameterized queries (#662)
    
    Co-authored-by: Copilot <[email protected]>
---
 r/sedonadb/NAMESPACE                          | 11 +++++++
 r/sedonadb/R/000-wrappers.R                   | 11 +++++++
 r/sedonadb/R/context.R                        | 30 +++++++++--------
 r/sedonadb/R/dataframe.R                      | 25 ++++++++++++++
 r/sedonadb/R/literal.R                        | 43 ++++++++++++++++++++++++
 r/sedonadb/R/pkg-sf.R                         | 25 ++++++++++++++
 r/sedonadb/R/utils.R                          | 29 +++++++++++++++++
 r/sedonadb/man/sd_sql.Rd                      | 12 +++++--
 r/sedonadb/man/sd_to_view.Rd                  |  6 ++--
 r/sedonadb/man/sd_with_params.Rd              | 28 ++++++++++++++++
 r/sedonadb/src/init.c                         |  9 +++++
 r/sedonadb/src/rust/api.h                     |  2 ++
 r/sedonadb/src/rust/src/dataframe.rs          |  6 ++++
 r/sedonadb/src/rust/src/expression.rs         | 39 +++++++++++++++++++++-
 r/sedonadb/tests/testthat/_snaps/context.md   |  2 +-
 r/sedonadb/tests/testthat/_snaps/dataframe.md | 12 +++++++
 r/sedonadb/tests/testthat/_snaps/literal.md   |  8 +++++
 r/sedonadb/tests/testthat/test-context.R      | 12 +++++++
 r/sedonadb/tests/testthat/test-dataframe.R    | 35 ++++++++++++++++++++
 r/sedonadb/tests/testthat/test-literal.R      | 31 ++++++++++++++++++
 r/sedonadb/tests/testthat/test-pkg-sf.R       | 47 +++++++++++++++++++++++++++
 21 files changed, 401 insertions(+), 22 deletions(-)

diff --git a/r/sedonadb/NAMESPACE b/r/sedonadb/NAMESPACE
index 35553123..7138d90d 100644
--- a/r/sedonadb/NAMESPACE
+++ b/r/sedonadb/NAMESPACE
@@ -12,12 +12,22 @@ S3method(as_sedonadb_dataframe,nanoarrow_array_stream)
 S3method(as_sedonadb_dataframe,sedonadb_dataframe)
 S3method(as_sedonadb_dataframe,sf)
 S3method(as_sedonadb_literal,"NULL")
+S3method(as_sedonadb_literal,SedonaDBExpr)
+S3method(as_sedonadb_literal,bbox)
 S3method(as_sedonadb_literal,character)
+S3method(as_sedonadb_literal,crs)
+S3method(as_sedonadb_literal,data.frame)
 S3method(as_sedonadb_literal,double)
 S3method(as_sedonadb_literal,integer)
 S3method(as_sedonadb_literal,nanoarrow_array)
 S3method(as_sedonadb_literal,raw)
+S3method(as_sedonadb_literal,sfc)
+S3method(as_sedonadb_literal,sfg)
+S3method(as_sedonadb_literal,wk_crc)
+S3method(as_sedonadb_literal,wk_rct)
 S3method(as_sedonadb_literal,wk_wkb)
+S3method(as_sedonadb_literal,wk_wkt)
+S3method(as_sedonadb_literal,wk_xy)
 S3method(dim,sedonadb_dataframe)
 S3method(dimnames,sedonadb_dataframe)
 S3method(head,sedonadb_dataframe)
@@ -69,6 +79,7 @@ export(sd_to_view)
 export(sd_transmute)
 export(sd_ungroup)
 export(sd_view)
+export(sd_with_params)
 export(sd_write_parquet)
 export(sedonadb_adbc)
 importFrom(nanoarrow,as_nanoarrow_array_stream)
diff --git a/r/sedonadb/R/000-wrappers.R b/r/sedonadb/R/000-wrappers.R
index f8884991..7ef69f70 100644
--- a/r/sedonadb/R/000-wrappers.R
+++ b/r/sedonadb/R/000-wrappers.R
@@ -363,6 +363,16 @@ class(`InternalContext`) <- c(
   }
 }
 
+`InternalDataFrame_with_params` <- function(self) {
+  function(`params_sexp`) {
+    .savvy_wrap_InternalDataFrame(.Call(
+      savvy_InternalDataFrame_with_params__impl,
+      `self`,
+      `params_sexp`
+    ))
+  }
+}
+
 `.savvy_wrap_InternalDataFrame` <- function(ptr) {
   e <- new.env(parent = emptyenv())
   e$.ptr <- ptr
@@ -384,6 +394,7 @@ class(`InternalContext`) <- c(
   e$`to_parquet` <- `InternalDataFrame_to_parquet`(ptr)
   e$`to_provider` <- `InternalDataFrame_to_provider`(ptr)
   e$`to_view` <- `InternalDataFrame_to_view`(ptr)
+  e$`with_params` <- `InternalDataFrame_with_params`(ptr)
 
   class(e) <- c(
     "sedonadb::InternalDataFrame",
diff --git a/r/sedonadb/R/context.R b/r/sedonadb/R/context.R
index 2384f5bb..b26ee9f8 100644
--- a/r/sedonadb/R/context.R
+++ b/r/sedonadb/R/context.R
@@ -51,16 +51,7 @@ sd_connect <- function(
   memory_pool_type = NULL,
   unspillable_reserve_ratio = NULL
 ) {
-  unsupported_options <- list(...)
-  if (length(unsupported_options) != 0) {
-    warning(
-      sprintf(
-        "Unrecognized options for sd_connect(): %s",
-        paste(names(unsupported_options), collapse = ", ")
-      )
-    )
-  }
-
+  check_dots_empty(..., label = "sd_connect()")
   options <- list()
 
   if (!is.null(memory_limit)) {
@@ -146,22 +137,33 @@ sd_ctx_read_parquet <- function(ctx, path) {
 #'
 #' @param ctx A SedonaDB context.
 #' @param sql A SQL string to execute
+#' @param params A list of parameters to fill placeholders in the query.
+#' @param ... These dots are for future extensions and currently must be empty.
 #'
 #' @returns A sedonadb_dataframe
 #' @export
 #'
 #' @examples
-#' sd_sql("SELECT ST_Point(0, 1) as geom") |> sd_preview()
+#' sd_sql("SELECT ST_Point(0, 1) as geom")
+#' sd_sql("SELECT ST_Point($1, $2) as geom", params = list(1, 2))
+#' sd_sql("SELECT ST_Point($x, $y) as geom", params = list(x = 1, y = 2))
 #'
-sd_sql <- function(sql) {
-  sd_ctx_sql(ctx(), sql)
+sd_sql <- function(sql, ..., params = NULL) {
+  sd_ctx_sql(ctx(), sql, ..., params = params)
 }
 
 #' @rdname sd_sql
 #' @export
-sd_ctx_sql <- function(ctx, sql) {
+sd_ctx_sql <- function(ctx, sql, ..., params = NULL) {
   check_ctx(ctx)
   df <- ctx$sql(sql)
+  check_dots_empty(..., label = "sd_sql()")
+
+  if (!is.null(params)) {
+    params <- lapply(params, as_sedonadb_literal)
+    df <- df$with_params(params)
+  }
+
   new_sedonadb_dataframe(ctx, df)
 }
 
diff --git a/r/sedonadb/R/dataframe.R b/r/sedonadb/R/dataframe.R
index 6eb3f56a..2824a777 100644
--- a/r/sedonadb/R/dataframe.R
+++ b/r/sedonadb/R/dataframe.R
@@ -116,6 +116,31 @@ sd_count <- function(.data) {
   .data$df$count()
 }
 
+#' Fill in placeholders
+#'
+#' This is a slightly more verbose form of [sd_sql()] with `params` that is
+#' useful if a data frame is to be repeatedly queried.
+#'
+#' @inheritParams sd_count
+#' @param ... Named or unnamed parameters that will be coerced to literals
+#'   with [as_sedonadb_literal()].
+#'
+#' @returns A sedonadb_dataframe with the provided parameters filled into the 
query
+#' @export
+#'
+#' @examples
+#' sd_sql("SELECT ST_Point($1, $2) as pt") |>
+#'   sd_with_params(11, 12)
+#' sd_sql("SELECT ST_Point($x, $y) as pt") |>
+#'   sd_with_params(x = 11, y = 12)
+#'
+sd_with_params <- function(.data, ...) {
+  .data <- as_sedonadb_dataframe(.data)
+  params <- lapply(list(...), as_sedonadb_literal)
+  df <- .data$df$with_params(params)
+  new_sedonadb_dataframe(.data$ctx, df)
+}
+
 #' Register a DataFrame as a named view
 #'
 #' This is useful for creating a view that can be referenced in a SQL
diff --git a/r/sedonadb/R/literal.R b/r/sedonadb/R/literal.R
index 679239ca..53c135fe 100644
--- a/r/sedonadb/R/literal.R
+++ b/r/sedonadb/R/literal.R
@@ -37,6 +37,14 @@ as_sedonadb_literal <- function(x, ..., type = NULL, factory 
= NULL) {
   UseMethod("as_sedonadb_literal")
 }
 
+#' @export
+as_sedonadb_literal.SedonaDBExpr <- function(x, ..., type = NULL) {
+  # Technically this could be a different type of expression but for
+  # now we just need as_sedonadb_literal(as_sedonadb_literal(...)) not
+  # to error.
+  x
+}
+
 #' @export
 as_sedonadb_literal.NULL <- function(x, ..., type = NULL) {
   na <- nanoarrow::nanoarrow_array_init(nanoarrow::na_na()) |>
@@ -64,11 +72,46 @@ as_sedonadb_literal.raw <- function(x, ..., type = NULL) {
   as_sedonadb_literal_from_nanoarrow(list(x), ..., type = type)
 }
 
+#' @export
+as_sedonadb_literal.data.frame <- function(x, ..., type = NULL) {
+  if (nrow(x) != 1 || ncol(x) != 1) {
+    stop(
+      sprintf(
+        "Can't convert data.frame with dimensions %d x %d to SedonaDB literal",
+        nrow(x),
+        ncol(x)
+      )
+    )
+  }
+
+  as_sedonadb_literal(x[[1]], type = type)
+}
+
 #' @export
 as_sedonadb_literal.wk_wkb <- function(x, ..., type = NULL) {
   as_sedonadb_literal_from_nanoarrow(x, ..., type = type)
 }
 
+#' @export
+as_sedonadb_literal.wk_wkt <- function(x, ..., type = NULL) {
+  as_sedonadb_literal(wk::as_wkb(x), type = type)
+}
+
+#' @export
+as_sedonadb_literal.wk_xy <- function(x, ..., type = NULL) {
+  as_sedonadb_literal(wk::as_wkb(x), type = type)
+}
+
+#' @export
+as_sedonadb_literal.wk_rct <- function(x, ..., type = NULL) {
+  as_sedonadb_literal(wk::as_wkb(x), type = type)
+}
+
+#' @export
+as_sedonadb_literal.wk_crc <- function(x, ..., type = NULL) {
+  as_sedonadb_literal(wk::as_wkb(x), type = type)
+}
+
 as_sedonadb_literal_from_nanoarrow <- function(x, ..., type = NULL) {
   array <- nanoarrow::as_nanoarrow_array(x)
   if (array$length != 1L) {
diff --git a/r/sedonadb/R/pkg-sf.R b/r/sedonadb/R/pkg-sf.R
index 7140f6a0..5b201b09 100644
--- a/r/sedonadb/R/pkg-sf.R
+++ b/r/sedonadb/R/pkg-sf.R
@@ -15,6 +15,31 @@
 # specific language governing permissions and limitations
 # under the License.
 
+#' @export
+as_sedonadb_literal.sfc <- function(x, ..., type = NULL) {
+  as_sedonadb_literal(wk::as_wkb(x), type = type)
+}
+
+#' @export
+as_sedonadb_literal.sfg <- function(x, ..., type = NULL) {
+  as_sedonadb_literal(wk::as_wkb(x), type = type)
+}
+
+#' @export
+as_sedonadb_literal.bbox <- function(x, ..., type = NULL) {
+  as_sedonadb_literal(wk::as_wkb(x), type = type)
+}
+
+#' @export
+as_sedonadb_literal.crs <- function(x, ..., type = NULL) {
+  projjson <- wk::wk_crs_projjson(x)
+  if (identical(projjson, NA_character_)) {
+    as_sedonadb_literal(NULL)
+  } else {
+    as_sedonadb_literal(projjson)
+  }
+}
+
 #' @export
 as_sedonadb_dataframe.sf <- function(x, ..., schema = NULL) {
   stream <- nanoarrow::as_nanoarrow_array_stream(
diff --git a/r/sedonadb/R/utils.R b/r/sedonadb/R/utils.R
new file mode 100644
index 00000000..a424d4be
--- /dev/null
+++ b/r/sedonadb/R/utils.R
@@ -0,0 +1,29 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+check_dots_empty <- function(..., label) {
+  unsupported_options <- list(...)
+  if (length(unsupported_options) != 0) {
+    warning(
+      sprintf(
+        "Unsupported arguments in %s: %s",
+        label,
+        paste(names(unsupported_options), collapse = ", ")
+      )
+    )
+  }
+}
diff --git a/r/sedonadb/man/sd_sql.Rd b/r/sedonadb/man/sd_sql.Rd
index dc670062..2647c586 100644
--- a/r/sedonadb/man/sd_sql.Rd
+++ b/r/sedonadb/man/sd_sql.Rd
@@ -5,13 +5,17 @@
 \alias{sd_ctx_sql}
 \title{Create a DataFrame from SQL}
 \usage{
-sd_sql(sql)
+sd_sql(sql, ..., params = NULL)
 
-sd_ctx_sql(ctx, sql)
+sd_ctx_sql(ctx, sql, ..., params = NULL)
 }
 \arguments{
 \item{sql}{A SQL string to execute}
 
+\item{...}{These dots are for future extensions and currently must be empty.}
+
+\item{params}{A list of parameters to fill placeholders in the query.}
+
 \item{ctx}{A SedonaDB context.}
 }
 \value{
@@ -21,6 +25,8 @@ A sedonadb_dataframe
 The query will only be executed when requested.
 }
 \examples{
-sd_sql("SELECT ST_Point(0, 1) as geom") |> sd_preview()
+sd_sql("SELECT ST_Point(0, 1) as geom")
+sd_sql("SELECT ST_Point($1, $2) as geom", params = list(1, 2))
+sd_sql("SELECT ST_Point($x, $y) as geom", params = list(x = 1, y = 2))
 
 }
diff --git a/r/sedonadb/man/sd_to_view.Rd b/r/sedonadb/man/sd_to_view.Rd
index 3acb58bf..a0d498e3 100644
--- a/r/sedonadb/man/sd_to_view.Rd
+++ b/r/sedonadb/man/sd_to_view.Rd
@@ -4,16 +4,16 @@
 \alias{sd_to_view}
 \title{Register a DataFrame as a named view}
 \usage{
-sd_to_view(.data, table_ref, ctx = NULL, overwrite = FALSE)
+sd_to_view(.data, table_ref, overwrite = FALSE, ctx = NULL)
 }
 \arguments{
 \item{.data}{A sedonadb_dataframe or an object that can be coerced to one.}
 
 \item{table_ref}{The name of the view reference}
 
-\item{ctx}{A SedonaDB context.}
-
 \item{overwrite}{Use TRUE to overwrite a view with the same name (if it 
exists)}
+
+\item{ctx}{A SedonaDB context.}
 }
 \value{
 .data, invisibly
diff --git a/r/sedonadb/man/sd_with_params.Rd b/r/sedonadb/man/sd_with_params.Rd
new file mode 100644
index 00000000..de311ac7
--- /dev/null
+++ b/r/sedonadb/man/sd_with_params.Rd
@@ -0,0 +1,28 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/dataframe.R
+\name{sd_with_params}
+\alias{sd_with_params}
+\title{Fill in placeholders}
+\usage{
+sd_with_params(.data, ...)
+}
+\arguments{
+\item{.data}{A sedonadb_dataframe or an object that can be coerced to one.}
+
+\item{...}{Named or unnamed parameters that will be coerced to literals
+with \code{\link[=as_sedonadb_literal]{as_sedonadb_literal()}}.}
+}
+\value{
+A sedonadb_dataframe with the provided parameters filled into the query
+}
+\description{
+This is a slightly more verbose form of \code{\link[=sd_sql]{sd_sql()}} with 
\code{params} that is
+useful if a data frame is to be repeatedly queried.
+}
+\examples{
+sd_sql("SELECT ST_Point($1, $2) as pt") |>
+  sd_with_params(11, 12)
+sd_sql("SELECT ST_Point($x, $y) as pt") |>
+  sd_with_params(x = 11, y = 12)
+
+}
diff --git a/r/sedonadb/src/init.c b/r/sedonadb/src/init.c
index c2af14c0..d2b6c572 100644
--- a/r/sedonadb/src/init.c
+++ b/r/sedonadb/src/init.c
@@ -243,6 +243,13 @@ SEXP savvy_InternalDataFrame_to_view__impl(SEXP self__, 
SEXP c_arg__ctx,
   return handle_result(res);
 }
 
+SEXP savvy_InternalDataFrame_with_params__impl(SEXP self__,
+                                               SEXP c_arg__params_sexp) {
+  SEXP res =
+      savvy_InternalDataFrame_with_params__ffi(self__, c_arg__params_sexp);
+  return handle_result(res);
+}
+
 SEXP savvy_SedonaDBExpr_alias__impl(SEXP self__, SEXP c_arg__name) {
   SEXP res = savvy_SedonaDBExpr_alias__ffi(self__, c_arg__name);
   return handle_result(res);
@@ -379,6 +386,8 @@ static const R_CallMethodDef CallEntries[] = {
      (DL_FUNC)&savvy_InternalDataFrame_to_provider__impl, 1},
     {"savvy_InternalDataFrame_to_view__impl",
      (DL_FUNC)&savvy_InternalDataFrame_to_view__impl, 4},
+    {"savvy_InternalDataFrame_with_params__impl",
+     (DL_FUNC)&savvy_InternalDataFrame_with_params__impl, 2},
     {"savvy_SedonaDBExpr_alias__impl", 
(DL_FUNC)&savvy_SedonaDBExpr_alias__impl,
      2},
     {"savvy_SedonaDBExpr_cast__impl", (DL_FUNC)&savvy_SedonaDBExpr_cast__impl,
diff --git a/r/sedonadb/src/rust/api.h b/r/sedonadb/src/rust/api.h
index 152f17bf..6820d68e 100644
--- a/r/sedonadb/src/rust/api.h
+++ b/r/sedonadb/src/rust/api.h
@@ -69,6 +69,8 @@ SEXP savvy_InternalDataFrame_to_provider__ffi(SEXP self__);
 SEXP savvy_InternalDataFrame_to_view__ffi(SEXP self__, SEXP c_arg__ctx,
                                           SEXP c_arg__table_ref,
                                           SEXP c_arg__overwrite);
+SEXP savvy_InternalDataFrame_with_params__ffi(SEXP self__,
+                                              SEXP c_arg__params_sexp);
 
 // methods and associated functions for SedonaDBExpr
 SEXP savvy_SedonaDBExpr_alias__ffi(SEXP self__, SEXP c_arg__name);
diff --git a/r/sedonadb/src/rust/src/dataframe.rs 
b/r/sedonadb/src/rust/src/dataframe.rs
index 371785fa..49bcd555 100644
--- a/r/sedonadb/src/rust/src/dataframe.rs
+++ b/r/sedonadb/src/rust/src/dataframe.rs
@@ -366,4 +366,10 @@ impl InternalDataFrame {
         let inner = self.inner.clone().aggregate(group_by_exprs, exprs)?;
         Ok(new_data_frame(inner, self.runtime.clone()))
     }
+
+    fn with_params(&self, params_sexp: savvy::Sexp) -> 
savvy::Result<InternalDataFrame> {
+        let param_values = SedonaDBExprFactory::param_values(params_sexp)?;
+        let inner = self.inner.clone().with_param_values(param_values)?;
+        Ok(new_data_frame(inner, self.runtime.clone()))
+    }
 }
diff --git a/r/sedonadb/src/rust/src/expression.rs 
b/r/sedonadb/src/rust/src/expression.rs
index be51a085..fadb393d 100644
--- a/r/sedonadb/src/rust/src/expression.rs
+++ b/r/sedonadb/src/rust/src/expression.rs
@@ -17,7 +17,7 @@
 
 use std::sync::Arc;
 
-use datafusion_common::{Column, Result, ScalarValue};
+use datafusion_common::{metadata::ScalarAndMetadata, Column, ParamValues, 
Result, ScalarValue};
 use datafusion_expr::{
     expr::{AggregateFunction, FieldMetadata, NullTreatment, ScalarFunction},
     BinaryExpr, Cast, Expr, Operator,
@@ -206,6 +206,43 @@ impl SedonaDBExprFactory {
             })
             .collect()
     }
+
+    pub fn param_values(exprs_sexp: savvy::Sexp) -> savvy::Result<ParamValues> 
{
+        let literals = savvy::ListSexp::try_from(exprs_sexp)?
+            .iter()
+            .map(
+                |(name, item)| -> savvy::Result<(String, ScalarAndMetadata)> {
+                    // item here is the Environment wrapper around the 
external pointer
+                    let expr_wrapper: &SedonaDBExpr =
+                        EnvironmentSexp::try_from(item)?.try_into()?;
+                    if let Expr::Literal(scalar, metadata) = 
&expr_wrapper.inner {
+                        Ok((
+                            name.to_string(),
+                            ScalarAndMetadata::new(scalar.clone(), 
metadata.clone()),
+                        ))
+                    } else {
+                        Err(savvy_err!(
+                            "Expected literal expression but got {:?}",
+                            expr_wrapper.inner
+                        ))
+                    }
+                },
+            )
+            .collect::<savvy::Result<Vec<_>>>()?;
+
+        let has_names = literals.iter().any(|(name, _)| !name.is_empty());
+        if literals.is_empty() || has_names {
+            if !literals.iter().all(|(name, _)| !name.is_empty()) {
+                return Err(savvy_err!("params must be all named or all 
unnamed"));
+            }
+
+            Ok(ParamValues::Map(literals.into_iter().rev().collect()))
+        } else {
+            Ok(ParamValues::List(
+                literals.into_iter().map(|(_, param)| param).collect(),
+            ))
+        }
+    }
 }
 
 impl TryFrom<EnvironmentSexp> for &SedonaDBExpr {
diff --git a/r/sedonadb/tests/testthat/_snaps/context.md 
b/r/sedonadb/tests/testthat/_snaps/context.md
index 76c5ef83..80406f31 100644
--- a/r/sedonadb/tests/testthat/_snaps/context.md
+++ b/r/sedonadb/tests/testthat/_snaps/context.md
@@ -4,5 +4,5 @@
 
 # unrecognized options result in a warning
 
-    Unrecognized options for sd_connect(): not_an_option
+    Unsupported arguments in sd_connect(): not_an_option
 
diff --git a/r/sedonadb/tests/testthat/_snaps/dataframe.md 
b/r/sedonadb/tests/testthat/_snaps/dataframe.md
index e9f66332..5d8d82d7 100644
--- a/r/sedonadb/tests/testthat/_snaps/dataframe.md
+++ b/r/sedonadb/tests/testthat/_snaps/dataframe.md
@@ -1,3 +1,15 @@
+# sd_with_params() fills in placeholder values
+
+    Error during planning: No value found for placeholder with name $x
+
+---
+
+    Error during planning: No value found for placeholder with name $1
+
+---
+
+    params must be all named or all unnamed
+
 # dataframe can be printed
 
     Code
diff --git a/r/sedonadb/tests/testthat/_snaps/literal.md 
b/r/sedonadb/tests/testthat/_snaps/literal.md
index c115e766..b6a8faea 100644
--- a/r/sedonadb/tests/testthat/_snaps/literal.md
+++ b/r/sedonadb/tests/testthat/_snaps/literal.md
@@ -6,3 +6,11 @@
       <SedonaDBExpr>
       Binary("1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,240,63") FieldMetadata { 
inner: {"ARROW:extension:metadata": "{}", "ARROW:extension:name": 
"geoarrow.wkb"} }
 
+# data.frame can be converted to SedonaDB literal
+
+    Can't convert data.frame with dimensions 5 x 1 to SedonaDB literal
+
+---
+
+    Can't convert data.frame with dimensions 1 x 2 to SedonaDB literal
+
diff --git a/r/sedonadb/tests/testthat/test-context.R 
b/r/sedonadb/tests/testthat/test-context.R
index 9ee4b672..6146ff6c 100644
--- a/r/sedonadb/tests/testthat/test-context.R
+++ b/r/sedonadb/tests/testthat/test-context.R
@@ -120,3 +120,15 @@ test_that(".fns can have its contents listed", {
   expect_contains(names(.fns), "st_intersects")
   expect_contains(.DollarNames(.fns, "st_int"), "st_intersects")
 })
+
+test_that("sd_sql() applies params to query", {
+  df <- sd_sql(
+    "SELECT ST_AsText(ST_Translate($1, $2, $3)) AS geom",
+    params = list(wk::as_wkb("POINT (0 0)"), 2, 3)
+  )
+
+  expect_identical(
+    df |> sd_collect(),
+    data.frame(geom = "POINT(2 3)")
+  )
+})
diff --git a/r/sedonadb/tests/testthat/test-dataframe.R 
b/r/sedonadb/tests/testthat/test-dataframe.R
index 7ef09e2d..86393e4d 100644
--- a/r/sedonadb/tests/testthat/test-dataframe.R
+++ b/r/sedonadb/tests/testthat/test-dataframe.R
@@ -83,6 +83,41 @@ test_that("dataframe rows can be counted", {
   expect_identical(sd_count(df), 1)
 })
 
+test_that("sd_with_params() fills in placeholder values", {
+  df <- sd_sql("SELECT $1 + 1 AS two") |> sd_with_params(1)
+  expect_identical(sd_collect(df), data.frame(two = 2))
+
+  df <- sd_sql("SELECT $x + 1 AS two") |> sd_with_params(x = 1)
+  expect_identical(sd_collect(df), data.frame(two = 2))
+
+  # Check multiple parameters
+  df <- sd_sql("SELECT 'one' || $1 || $2 AS onetwothree") |>
+    sd_with_params("two", "three")
+  expect_identical(sd_collect(df), data.frame(onetwothree = "onetwothree"))
+
+  df <- sd_sql("SELECT 'one' || $x || $y AS onetwothree") |>
+    sd_with_params(x = "two", y = "three")
+  expect_identical(sd_collect(df), data.frame(onetwothree = "onetwothree"))
+
+  # Check order (first name wins, like an R list)
+  df <- sd_sql("SELECT 'one' || $x || $y AS onetwothree") |>
+    sd_with_params(x = "two", y = "three", x = "gazornenplat")
+  expect_identical(sd_collect(df), data.frame(onetwothree = "onetwothree"))
+
+  # Check that an error occurs for missing parameters
+  expect_snapshot_error(
+    sd_sql("SELECT $x + 1 AS two") |> sd_with_params()
+  )
+  expect_snapshot_error(
+    sd_sql("SELECT $1 + 1 AS two") |> sd_with_params()
+  )
+
+  # Check error for mixed named/unnamed
+  expect_snapshot_error(
+    sd_sql("SELECT $x + 1 AS two") |> sd_with_params(x = 1, 2)
+  )
+})
+
 test_that("dataframe can be computed", {
   df <- sd_sql("SELECT 1 as one, 'two' as two")
   df_computed <- sd_compute(df)
diff --git a/r/sedonadb/tests/testthat/test-literal.R 
b/r/sedonadb/tests/testthat/test-literal.R
index fe66016d..b20816e2 100644
--- a/r/sedonadb/tests/testthat/test-literal.R
+++ b/r/sedonadb/tests/testthat/test-literal.R
@@ -59,3 +59,34 @@ test_that("non-scalars can't be automatically converted to 
literals", {
     "Can't convert non-scalar to sedonadb_expr"
   )
 })
+
+test_that("data.frame can be converted to SedonaDB literal", {
+  expect_identical(
+    as_sedonadb_literal(data.frame(x = 1.0))$debug_string(),
+    "Literal(Float64(1), None)"
+  )
+
+  expect_snapshot_error(
+    as_sedonadb_literal(data.frame(x = 1:5))
+  )
+
+  expect_snapshot_error(
+    as_sedonadb_literal(data.frame(x = 1, y = 2))
+  )
+})
+
+test_that("geometry objects can be converted to SedonaDB literals", {
+  objects <- list(
+    wk::as_wkb("POINT (0 1)"),
+    wk::as_wkt("POINT (0 1)"),
+    wk::xy(0, 1),
+    wk::rct(0, 1, 2, 4),
+    wk::crc(0, 1, 2)
+  )
+
+  for (x in objects) {
+    df <- sd_sql("SELECT ST_Translate($1, 0, 0) as geom", params = list(x))
+    collected <- sd_collect(df)
+    expect_identical(wk::as_wkb(x), wk::as_wkb(collected$geom))
+  }
+})
diff --git a/r/sedonadb/tests/testthat/test-pkg-sf.R 
b/r/sedonadb/tests/testthat/test-pkg-sf.R
new file mode 100644
index 00000000..38fd98d7
--- /dev/null
+++ b/r/sedonadb/tests/testthat/test-pkg-sf.R
@@ -0,0 +1,47 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+test_that("sf geometry objects can be converted to SedonaDB literals", {
+  skip_if_not_installed("sf")
+
+  objects <- list(
+    sf::st_as_sf(sf::st_as_sfc("POINT (0 1)")),
+    sf::st_as_sfc("POINT (0 1)"),
+    sf::st_point(c(0, 1)),
+    sf::st_bbox(sf::st_sfc(sf::st_point(c(0, 1)), sf::st_point(c(2, 3))))
+  )
+
+  for (x in objects) {
+    df <- sd_sql("SELECT ST_Translate($1, 0, 0) as geom", params = list(x))
+    collected <- sd_collect(df)
+    expect_identical(sf::st_as_sfc(collected$geom), 
sf::st_as_sfc(wk::as_wkb(x)))
+  }
+})
+
+test_that("sf objects can be converted to and from SedonaDB data frames", {
+  skip_if_not_installed("sf")
+
+  nc <- sf::read_sf(system.file("shape/nc.shp", package = "sf"))
+  df <- as_sedonadb_dataframe(nc)
+
+  # Compare attributes separately
+  expect_true(sf::st_as_sf(df) |> sf::st_crs() == nc |> sf::st_crs())
+  expect_equal(
+    sf::st_as_sf(df) |> sf::st_set_crs(NA) |> as.data.frame(),
+    nc |> sf::st_set_crs(NA) |> as.data.frame()
+  )
+})

Reply via email to