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