You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by np...@apache.org on 2022/10/13 23:59:41 UTC
[arrow] branch master updated: ARROW-13766: [R] Add slice_*() methods (#14361)
This is an automated email from the ASF dual-hosted git repository.
npr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/arrow.git
The following commit(s) were added to refs/heads/master by this push:
new 80e398623d ARROW-13766: [R] Add slice_*() methods (#14361)
80e398623d is described below
commit 80e398623d956304acaeb3922e367d45ed96ddec
Author: Neal Richardson <ne...@gmail.com>
AuthorDate: Thu Oct 13 19:59:32 2022 -0400
ARROW-13766: [R] Add slice_*() methods (#14361)
This PR implements `slice_head,()` `slice_tail()`, `slice_min()`, `slice_max()` and `slice_sample()`. `slice_sample()` requires a clever hack using a UDF because the `random()` C++ function apparently does not work; see ARROW-17974.
Authored-by: Neal Richardson <ne...@gmail.com>
Signed-off-by: Neal Richardson <ne...@gmail.com>
---
r/.lintr | 1 +
r/DESCRIPTION | 1 +
r/NAMESPACE | 3 +
r/R/array.R | 4 +-
r/R/arrow-datum.R | 6 ++
r/R/arrow-package.R | 27 ++++-
r/R/dataset-scan.R | 16 ++-
r/R/dplyr-funcs-doc.R | 17 ++--
r/R/dplyr-funcs-type.R | 4 +-
r/R/dplyr-funcs.R | 14 ++-
r/R/dplyr-slice.R | 158 +++++++++++++++++++++++++++++
r/R/dplyr.R | 10 ++
r/R/expression.R | 7 +-
r/R/record-batch-reader.R | 5 +
r/R/util.R | 3 +-
r/data-raw/docgen.R | 3 +
r/man/acero.Rd | 17 ++--
r/tests/testthat/test-dplyr-slice.R | 192 ++++++++++++++++++++++++++++++++++++
18 files changed, 464 insertions(+), 24 deletions(-)
diff --git a/r/.lintr b/r/.lintr
index 619339afca..1bd80aff4c 100644
--- a/r/.lintr
+++ b/r/.lintr
@@ -27,5 +27,6 @@ linters: linters_with_defaults(
)
exclusions: list(
"R/arrowExports.R",
+ "R/dplyr-funcs-doc.R",
"data-raw/codegen.R"
)
diff --git a/r/DESCRIPTION b/r/DESCRIPTION
index 4b526e8b8a..5a69d46896 100644
--- a/r/DESCRIPTION
+++ b/r/DESCRIPTION
@@ -116,6 +116,7 @@ Collate:
'dplyr-join.R'
'dplyr-mutate.R'
'dplyr-select.R'
+ 'dplyr-slice.R'
'dplyr-summarize.R'
'dplyr-union.R'
'record-batch.R'
diff --git a/r/NAMESPACE b/r/NAMESPACE
index e20e61c0e3..59055ff2b7 100644
--- a/r/NAMESPACE
+++ b/r/NAMESPACE
@@ -421,6 +421,8 @@ importFrom(rlang,as_quosure)
importFrom(rlang,call2)
importFrom(rlang,call_args)
importFrom(rlang,caller_env)
+importFrom(rlang,check_dots_empty)
+importFrom(rlang,dots_list)
importFrom(rlang,dots_n)
importFrom(rlang,enexpr)
importFrom(rlang,enexprs)
@@ -472,6 +474,7 @@ importFrom(stats,na.fail)
importFrom(stats,na.omit)
importFrom(stats,na.pass)
importFrom(stats,quantile)
+importFrom(stats,runif)
importFrom(tidyselect,all_of)
importFrom(tidyselect,contains)
importFrom(tidyselect,ends_with)
diff --git a/r/R/array.R b/r/R/array.R
index 7c2fb5c783..c730bd742b 100644
--- a/r/R/array.R
+++ b/r/R/array.R
@@ -349,7 +349,7 @@ stop_cant_convert_array <- function(x, type) {
"Can't create Array from object of type %s",
paste(class(x), collapse = " / ")
),
- call = rlang::caller_env()
+ call = caller_env()
)
} else {
abort(
@@ -358,7 +358,7 @@ stop_cant_convert_array <- function(x, type) {
format(type$code()),
paste(class(x), collapse = " / ")
),
- call = rlang::caller_env()
+ call = caller_env()
)
}
}
diff --git a/r/R/arrow-datum.R b/r/R/arrow-datum.R
index 33c67a5285..cb3bfa57f6 100644
--- a/r/R/arrow-datum.R
+++ b/r/R/arrow-datum.R
@@ -299,6 +299,9 @@ head.ArrowDatum <- function(x, n = 6L, ...) {
} else {
n <- min(len, n)
}
+ if (!is.integer(n)) {
+ n <- floor(n)
+ }
if (n == len) {
return(x)
}
@@ -310,6 +313,9 @@ head.ArrowDatum <- function(x, n = 6L, ...) {
tail.ArrowDatum <- function(x, n = 6L, ...) {
assert_is(n, c("numeric", "integer"))
assert_that(length(n) == 1)
+ if (!is.integer(n)) {
+ n <- floor(n)
+ }
len <- NROW(x)
if (n < 0) {
# tail(x, negative) means all but the first n rows
diff --git a/r/R/arrow-package.R b/r/R/arrow-package.R
index 143f4c191b..477fa67e7c 100644
--- a/r/R/arrow-package.R
+++ b/r/R/arrow-package.R
@@ -26,7 +26,7 @@
#' @importFrom rlang expr caller_env is_character quo_name is_quosure enexpr enexprs as_quosure
#' @importFrom rlang is_list call2 is_empty as_function as_label arg_match is_symbol is_call call_args
#' @importFrom rlang quo_set_env quo_get_env is_formula quo_is_call f_rhs parse_expr f_env new_quosure
-#' @importFrom rlang new_quosures expr_text
+#' @importFrom rlang new_quosures expr_text caller_env check_dots_empty dots_list
#' @importFrom tidyselect vars_pull vars_rename vars_select eval_select
#' @importFrom glue glue
#' @useDynLib arrow, .registration = TRUE
@@ -64,6 +64,31 @@ supported_dplyr_methods <- list(
rename_with = NULL,
union = NULL,
union_all = NULL,
+ slice_head = c(
+ "slicing within groups not supported;",
+ "Arrow datasets do not have row order, so head is non-deterministic;",
+ "`prop` only supported on queries where `nrow()` is knowable without evaluating"
+ ),
+ slice_tail = c(
+ "slicing within groups not supported;",
+ "Arrow datasets do not have row order, so tail is non-deterministic;",
+ "`prop` only supported on queries where `nrow()` is knowable without evaluating"
+ ),
+ slice_min = c(
+ "slicing within groups not supported;",
+ "`with_ties = TRUE` (dplyr default) is not supported;",
+ "`prop` only supported on queries where `nrow()` is knowable without evaluating"
+ ),
+ slice_max = c(
+ "slicing within groups not supported;",
+ "`with_ties = TRUE` (dplyr default) is not supported;",
+ "`prop` only supported on queries where `nrow()` is knowable without evaluating"
+ ),
+ slice_sample = c(
+ "slicing within groups not supported;",
+ "`replace = TRUE` and the `weight_by` argument not supported;",
+ "`n` only supported on queries where `nrow()` is knowable without evaluating"
+ ),
glimpse = NULL,
show_query = NULL,
explain = NULL
diff --git a/r/R/dataset-scan.R b/r/R/dataset-scan.R
index 883bf14113..17ebb56c05 100644
--- a/r/R/dataset-scan.R
+++ b/r/R/dataset-scan.R
@@ -157,9 +157,14 @@ names.Scanner <- function(x) names(x$schema)
#' @export
head.Scanner <- function(x, n = 6L, ...) {
+ assert_is(n, c("numeric", "integer"))
+ assert_that(length(n) == 1)
# Negative n requires knowing nrow(x), which requires a scan itself
assert_that(n >= 0)
- dataset___Scanner__head(x, n)
+ if (!is.integer(n)) {
+ n <- floor(n)
+ }
+ dataset___Scanner__head(x, floor(n))
}
#' @export
@@ -168,8 +173,13 @@ tail.Scanner <- function(x, n = 6L, ...) {
}
tail_from_batches <- function(batches, n) {
+ assert_is(n, c("numeric", "integer"))
+ assert_that(length(n) == 1)
# Negative n requires knowing nrow(x), which requires a scan itself
- assert_that(n >= 0) # For now
+ assert_that(n >= 0)
+ if (!is.integer(n)) {
+ n <- floor(n)
+ }
result <- list()
batch_num <- 0
# Given a list of batches, iterate from the back
@@ -224,7 +234,7 @@ map_batches <- function(X, FUN, ..., .schema = NULL, .lazy = FALSE, .data.frame
}
FUN <- as_mapper(FUN)
reader <- as_record_batch_reader(X)
- dots <- rlang::list2(...)
+ dots <- list2(...)
# If no schema is supplied, we have to evaluate the first batch here
if (is.null(.schema)) {
diff --git a/r/R/dplyr-funcs-doc.R b/r/R/dplyr-funcs-doc.R
index 5360c7fad6..d399e37e10 100644
--- a/r/R/dplyr-funcs-doc.R
+++ b/r/R/dplyr-funcs-doc.R
@@ -19,9 +19,9 @@
#' Functions available in Arrow dplyr queries
#'
-#' The `arrow` package contains methods for 32 `dplyr` table functions, many of
+#' The `arrow` package contains methods for 37 `dplyr` table functions, many of
#' which are "verbs" that do transformations to one or more tables.
-#' The package also has mappings of 205 R functions to the corresponding
+#' The package also has mappings of 207 R functions to the corresponding
#' functions in the Arrow compute library. These allow you to write code inside
#' of `dplyr` methods that call R functions, including many in packages like
#' `stringr` and `lubridate`, and they will get translated to Arrow and run
@@ -62,6 +62,11 @@
#' * [`select()`][dplyr::select()]
#' * [`semi_join()`][dplyr::semi_join()]
#' * [`show_query()`][dplyr::show_query()]
+#' * [`slice_head()`][dplyr::slice_head()]: slicing within groups not supported; Arrow datasets do not have row order, so head is non-deterministic; `prop` only supported on queries where `nrow()` is knowable without evaluating
+#' * [`slice_max()`][dplyr::slice_max()]: slicing within groups not supported; `with_ties = TRUE` (dplyr default) is not supported; `prop` only supported on queries where `nrow()` is knowable without evaluating
+#' * [`slice_min()`][dplyr::slice_min()]: slicing within groups not supported; `with_ties = TRUE` (dplyr default) is not supported; `prop` only supported on queries where `nrow()` is knowable without evaluating
+#' * [`slice_sample()`][dplyr::slice_sample()]: slicing within groups not supported; `replace = TRUE` and the `weight_by` argument not supported; `n` only supported on queries where `nrow()` is knowable without evaluating
+#' * [`slice_tail()`][dplyr::slice_tail()]: slicing within groups not supported; Arrow datasets do not have row order, so tail is non-deterministic; `prop` only supported on queries where `nrow()` is knowable without evaluating
#' * [`summarise()`][dplyr::summarise()]
#' * [`tally()`][dplyr::tally()]
#' * [`transmute()`][dplyr::transmute()]
@@ -78,7 +83,7 @@
#' Functions can be called either as `pkg::fun()` or just `fun()`, i.e. both
#' `str_sub()` and `stringr::str_sub()` work.
#'
-#' In addition to these functions, you can call any of Arrow's 243 compute
+#' In addition to these functions, you can call any of Arrow's 244 compute
#' functions directly. Arrow has many functions that don't map to an existing R
#' function. In other cases where there is an R function mapping, you can still
#' call the Arrow function directly if you don't want the adaptations that the R
@@ -185,13 +190,13 @@
#'
#' ## dplyr
#'
-#' * [`across()`][dplyr::across()]: not yet supported inside `filter()`;
-#' purrr-style lambda functions
-#' and use of `where()` selection helper not yet supported
+#' * [`across()`][dplyr::across()]: Use of `where()` selection helper not yet supported
#' * [`between()`][dplyr::between()]
#' * [`case_when()`][dplyr::case_when()]
#' * [`coalesce()`][dplyr::coalesce()]
#' * [`desc()`][dplyr::desc()]
+#' * [`if_all()`][dplyr::if_all()]
+#' * [`if_any()`][dplyr::if_any()]
#' * [`if_else()`][dplyr::if_else()]
#' * [`n()`][dplyr::n()]
#' * [`n_distinct()`][dplyr::n_distinct()]
diff --git a/r/R/dplyr-funcs-type.R b/r/R/dplyr-funcs-type.R
index aa50cdebc5..429ea51e02 100644
--- a/r/R/dplyr-funcs-type.R
+++ b/r/R/dplyr-funcs-type.R
@@ -126,7 +126,7 @@ register_bindings_type_cast <- function() {
# it is difficult to replicate the .name_repair semantics and expanding of
# unnamed data frame arguments in the same way that the tibble() constructor
# does.
- args <- rlang::dots_list(..., .named = TRUE, .homonyms = "error")
+ args <- dots_list(..., .named = TRUE, .homonyms = "error")
build_expr(
"make_struct",
@@ -151,7 +151,7 @@ register_bindings_type_cast <- function() {
if (!is.null(row.names)) arrow_not_supported("row.names")
if (!is.null(check.rows)) arrow_not_supported("check.rows")
- args <- rlang::dots_list(..., .named = fix.empty.names)
+ args <- dots_list(..., .named = fix.empty.names)
if (is.null(names(args))) {
names(args) <- rep("", length(args))
}
diff --git a/r/R/dplyr-funcs.R b/r/R/dplyr-funcs.R
index a66db112d9..33054b6f40 100644
--- a/r/R/dplyr-funcs.R
+++ b/r/R/dplyr-funcs.R
@@ -136,8 +136,9 @@ call_binding_agg <- function(fun_name, ...) {
agg_funcs[[fun_name]](...)
}
-# Called in .onLoad()
+#' @importFrom stats runif
create_binding_cache <- function() {
+ # Called in .onLoad()
.cache$docs <- list()
# Register all available Arrow Compute functions, namespaced as arrow_fun.
@@ -160,6 +161,17 @@ create_binding_cache <- function() {
register_bindings_type()
register_bindings_augmented()
+ # HACK because random() doesn't work (ARROW-17974)
+ register_scalar_function(
+ "_random_along",
+ function(context, x) {
+ Array$create(runif(length(x)))
+ },
+ in_type = schema(x = boolean()),
+ out_type = float64(),
+ auto_convert = FALSE
+ )
+
# We only create the cache for nse_funcs and not agg_funcs
.cache$functions <- c(as.list(nse_funcs), arrow_funcs)
}
diff --git a/r/R/dplyr-slice.R b/r/R/dplyr-slice.R
new file mode 100644
index 0000000000..102986cdbf
--- /dev/null
+++ b/r/R/dplyr-slice.R
@@ -0,0 +1,158 @@
+# 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.
+
+
+# The following S3 methods are registered on load if dplyr is present
+
+slice_head.arrow_dplyr_query <- function(.data, ..., n, prop) {
+ if (length(dplyr::group_vars(.data)) > 0) {
+ arrow_not_supported("Slicing grouped data")
+ }
+ check_dots_empty()
+
+ if (missing(n)) {
+ n <- prop_to_n(.data, prop)
+ }
+
+ head(.data, n)
+}
+slice_head.Dataset <- slice_head.ArrowTabular <- slice_head.RecordBatchReader <- slice_head.arrow_dplyr_query
+
+slice_tail.arrow_dplyr_query <- function(.data, ..., n, prop) {
+ if (length(dplyr::group_vars(.data)) > 0) {
+ arrow_not_supported("Slicing grouped data")
+ }
+ check_dots_empty()
+
+ if (missing(n)) {
+ n <- prop_to_n(.data, prop)
+ }
+
+ tail(.data, n)
+}
+slice_tail.Dataset <- slice_tail.ArrowTabular <- slice_tail.RecordBatchReader <- slice_tail.arrow_dplyr_query
+
+slice_min.arrow_dplyr_query <- function(.data, order_by, ..., n, prop, with_ties = TRUE) {
+ if (length(dplyr::group_vars(.data)) > 0) {
+ arrow_not_supported("Slicing grouped data")
+ }
+ if (with_ties) {
+ arrow_not_supported("with_ties = TRUE")
+ }
+ check_dots_empty()
+
+ if (missing(n)) {
+ n <- prop_to_n(.data, prop)
+ }
+
+ head(dplyr::arrange(.data, {{ order_by }}), n)
+}
+slice_min.Dataset <- slice_min.ArrowTabular <- slice_min.RecordBatchReader <- slice_min.arrow_dplyr_query
+
+slice_max.arrow_dplyr_query <- function(.data, order_by, ..., n, prop, with_ties = TRUE) {
+ if (length(dplyr::group_vars(.data)) > 0) {
+ arrow_not_supported("Slicing grouped data")
+ }
+ if (with_ties) {
+ arrow_not_supported("with_ties = TRUE")
+ }
+ check_dots_empty()
+
+ if (missing(n)) {
+ n <- prop_to_n(.data, prop)
+ }
+
+ sorted <- dplyr::arrange(.data, {{ order_by }})
+ # Invert the sort order of the things in ... so they're descending
+ # TODO: handle possibility that .data was already sorted and we don't want
+ # to invert those sorts? Does that matter? Or no because there's no promise
+ # of order of which TopK elements you get if there are ties?
+ sorted$arrange_desc <- !sorted$arrange_desc
+ head(sorted, n)
+}
+slice_max.Dataset <- slice_max.ArrowTabular <- slice_max.RecordBatchReader <- slice_max.arrow_dplyr_query
+
+slice_sample.arrow_dplyr_query <- function(.data,
+ ...,
+ n,
+ prop,
+ weight_by = NULL,
+ replace = FALSE) {
+ if (length(dplyr::group_vars(.data)) > 0) {
+ arrow_not_supported("Slicing grouped data")
+ }
+ if (replace) {
+ arrow_not_supported("Sampling with replacement")
+ }
+ if (!missing(weight_by)) {
+ # You could do this by multiplying the random() column * weight_by
+ # but you'd need to calculate sum(weight_by) in order to normalize
+ arrow_not_supported("weight_by")
+ }
+ check_dots_empty()
+
+ # If we want n rows sampled, we have to convert n to prop, oversample some
+ # just to make sure we get enough, then head(n)
+ sampling_n <- missing(prop)
+ if (sampling_n) {
+ prop <- min(n_to_prop(.data, n) + .05, 1)
+ }
+ validate_prop(prop)
+
+ if (prop < 1) {
+ .data <- as_adq(.data)
+ # TODO(ARROW-17974): use Expression$create("random") instead of UDF hack
+ # HACK: use our UDF to generate random. It needs an input column because
+ # nullary functions don't work, and that column has to be typed. We've
+ # chosen boolean() type because it's compact and can always be created:
+ # pick any column and do is.na, that will be boolean.
+ # TODO: get an actual FieldRef because the first col could be derived
+ ref <- Expression$create("is_null", .data$selected_columns[[1]])
+ expr <- Expression$create("_random_along", ref) < prop
+ .data <- set_filters(.data, expr)
+ }
+ if (sampling_n) {
+ .data <- head(.data, n)
+ }
+
+ .data
+}
+slice_sample.Dataset <- slice_sample.ArrowTabular <- slice_sample.RecordBatchReader <- slice_sample.arrow_dplyr_query
+
+
+prop_to_n <- function(.data, prop) {
+ nrows <- nrow(.data)
+ if (is.na(nrows)) {
+ arrow_not_supported("Slicing with `prop` when the query has joins or aggregations")
+ }
+ validate_prop(prop)
+ nrows * prop
+}
+
+validate_prop <- function(prop) {
+ if (!is.numeric(prop) || length(prop) != 1 || is.na(prop) || prop < 0 || prop > 1) {
+ stop("`prop` must be a single numeric value between 0 and 1", call. = FALSE)
+ }
+}
+
+n_to_prop <- function(.data, n) {
+ nrows <- nrow(.data)
+ if (is.na(nrows)) {
+ arrow_not_supported("slice_sample() with `n` when the query has joins or aggregations")
+ }
+ n / nrows
+}
diff --git a/r/R/dplyr.R b/r/R/dplyr.R
index a96678f975..1f529066dd 100644
--- a/r/R/dplyr.R
+++ b/r/R/dplyr.R
@@ -215,12 +215,22 @@ as.data.frame.arrow_dplyr_query <- function(x, row.names = NULL, optional = FALS
#' @export
head.arrow_dplyr_query <- function(x, n = 6L, ...) {
+ assert_is(n, c("numeric", "integer"))
+ assert_that(length(n) == 1)
+ if (!is.integer(n)) {
+ n <- floor(n)
+ }
x$head <- n
collapse.arrow_dplyr_query(x)
}
#' @export
tail.arrow_dplyr_query <- function(x, n = 6L, ...) {
+ assert_is(n, c("numeric", "integer"))
+ assert_that(length(n) == 1)
+ if (!is.integer(n)) {
+ n <- floor(n)
+ }
x$tail <- n
collapse.arrow_dplyr_query(x)
}
diff --git a/r/R/expression.R b/r/R/expression.R
index 7a5a600d95..99296aab39 100644
--- a/r/R/expression.R
+++ b/r/R/expression.R
@@ -173,7 +173,12 @@ Expression$create <- function(function_name,
assert_that(is.string(function_name))
assert_that(is_list_of(args, "Expression"), msg = "Expression arguments must be Expression objects")
expr <- compute___expr__call(function_name, args, options)
- expr$schema <- unify_schemas(schemas = lapply(args, function(x) x$schema))
+ if (length(args)) {
+ expr$schema <- unify_schemas(schemas = lapply(args, function(x) x$schema))
+ } else {
+ # TODO: this shouldn't be necessary
+ expr$schema <- schema()
+ }
expr
}
diff --git a/r/R/record-batch-reader.R b/r/R/record-batch-reader.R
index e1dd52ed71..40dd2f516d 100644
--- a/r/R/record-batch-reader.R
+++ b/r/R/record-batch-reader.R
@@ -130,8 +130,13 @@ as.data.frame.RecordBatchReader <- function(x, row.names = NULL, optional = FALS
#' @export
head.RecordBatchReader <- function(x, n = 6L, ...) {
+ assert_is(n, c("numeric", "integer"))
+ assert_that(length(n) == 1)
# Negative n requires knowing nrow(x), which requires consuming the whole RBR
assert_that(n >= 0)
+ if (!is.integer(n)) {
+ n <- floor(n)
+ }
RecordBatchReader__Head(x, n)
}
diff --git a/r/R/util.R b/r/R/util.R
index d8ecb2732f..8d319fab82 100644
--- a/r/R/util.R
+++ b/r/R/util.R
@@ -153,7 +153,7 @@ as_writable_table <- function(x) {
abort(
"Object must be coercible to an Arrow Table using `as_arrow_table()`",
parent = e,
- call = rlang::caller_env(2)
+ call = caller_env(2)
)
}
)
@@ -238,7 +238,6 @@ is_compressed <- function(compression) {
# handler function which checks for a number of different read errors
augment_io_error_msg <- function(e, call, schema = NULL, format = NULL) {
-
msg <- conditionMessage(e)
if (!is.null(schema)) {
diff --git a/r/data-raw/docgen.R b/r/data-raw/docgen.R
index e2c7f94eaf..8db3bb7e80 100644
--- a/r/data-raw/docgen.R
+++ b/r/data-raw/docgen.R
@@ -143,6 +143,9 @@ docs[["dplyr::desc"]] <- character(0)
# add tidyselect helpers by parsing the reexports file
tidyselect <- grep("^tidyselect::", readLines("R/reexports-tidyselect.R"), value = TRUE)
+# HACK: remove the _random_along UDF we're using (fix in ARROW-17974)
+docs[["_random_along"]] <- NULL
+
docs <- c(docs, setNames(rep(list(NULL), length(tidyselect)), tidyselect))
fun_df <- tibble::tibble(
diff --git a/r/man/acero.Rd b/r/man/acero.Rd
index 76f1b13fe3..5cbe211d00 100644
--- a/r/man/acero.Rd
+++ b/r/man/acero.Rd
@@ -4,9 +4,9 @@
\alias{acero}
\title{Functions available in Arrow dplyr queries}
\description{
-The \code{arrow} package contains methods for 32 \code{dplyr} table functions, many of
+The \code{arrow} package contains methods for 37 \code{dplyr} table functions, many of
which are "verbs" that do transformations to one or more tables.
-The package also has mappings of 205 R functions to the corresponding
+The package also has mappings of 207 R functions to the corresponding
functions in the Arrow compute library. These allow you to write code inside
of \code{dplyr} methods that call R functions, including many in packages like
\code{stringr} and \code{lubridate}, and they will get translated to Arrow and run
@@ -46,6 +46,11 @@ Table into an R \code{data.frame}.
\item \code{\link[dplyr:select]{select()}}
\item \code{\link[dplyr:filter-joins]{semi_join()}}
\item \code{\link[dplyr:explain]{show_query()}}
+\item \code{\link[dplyr:slice]{slice_head()}}: slicing within groups not supported; Arrow datasets do not have row order, so head is non-deterministic; \code{prop} only supported on queries where \code{nrow()} is knowable without evaluating
+\item \code{\link[dplyr:slice]{slice_max()}}: slicing within groups not supported; \code{with_ties = TRUE} (dplyr default) is not supported; \code{prop} only supported on queries where \code{nrow()} is knowable without evaluating
+\item \code{\link[dplyr:slice]{slice_min()}}: slicing within groups not supported; \code{with_ties = TRUE} (dplyr default) is not supported; \code{prop} only supported on queries where \code{nrow()} is knowable without evaluating
+\item \code{\link[dplyr:slice]{slice_sample()}}: slicing within groups not supported; \code{replace = TRUE} and the \code{weight_by} argument not supported; \code{n} only supported on queries where \code{nrow()} is knowable without evaluating
+\item \code{\link[dplyr:slice]{slice_tail()}}: slicing within groups not supported; Arrow datasets do not have row order, so tail is non-deterministic; \code{prop} only supported on queries where \code{nrow()} is knowable without evaluating
\item \code{\link[dplyr:summarise]{summarise()}}
\item \code{\link[dplyr:count]{tally()}}
\item \code{\link[dplyr:mutate]{transmute()}}
@@ -63,7 +68,7 @@ can assume that the function works in Acero just as it does in R.
Functions can be called either as \code{pkg::fun()} or just \code{fun()}, i.e. both
\code{str_sub()} and \code{stringr::str_sub()} work.
-In addition to these functions, you can call any of Arrow's 243 compute
+In addition to these functions, you can call any of Arrow's 244 compute
functions directly. Arrow has many functions that don't map to an existing R
function. In other cases where there is an R function mapping, you can still
call the Arrow function directly if you don't want the adaptations that the R
@@ -175,13 +180,13 @@ as \code{arrow_ascii_is_decimal}.
\subsection{dplyr}{
\itemize{
-\item \code{\link[dplyr:across]{across()}}: not yet supported inside \code{filter()};
-purrr-style lambda functions
-and use of \code{where()} selection helper not yet supported
+\item \code{\link[dplyr:across]{across()}}: Use of \code{where()} selection helper not yet supported
\item \code{\link[dplyr:between]{between()}}
\item \code{\link[dplyr:case_when]{case_when()}}
\item \code{\link[dplyr:coalesce]{coalesce()}}
\item \code{\link[dplyr:desc]{desc()}}
+\item \code{\link[dplyr:across]{if_all()}}
+\item \code{\link[dplyr:across]{if_any()}}
\item \code{\link[dplyr:if_else]{if_else()}}
\item \code{\link[dplyr:context]{n()}}
\item \code{\link[dplyr:n_distinct]{n_distinct()}}
diff --git a/r/tests/testthat/test-dplyr-slice.R b/r/tests/testthat/test-dplyr-slice.R
new file mode 100644
index 0000000000..c12dd97aa4
--- /dev/null
+++ b/r/tests/testthat/test-dplyr-slice.R
@@ -0,0 +1,192 @@
+# 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.
+
+library(dplyr, warn.conflicts = FALSE)
+
+tbl <- example_data
+
+test_that("slice_head/tail, ungrouped", {
+ # head/tail are not deterministic in Arrow because data is unordered
+ # so we can't assert identical to dplyr, just assert right number of rows
+ tab <- arrow_table(tbl)
+ expect_equal(
+ tab %>%
+ slice_head(n = 5) %>%
+ nrow(),
+ 5
+ )
+ expect_equal(
+ tab %>%
+ slice_tail(n = 5) %>%
+ nrow(),
+ 5
+ )
+
+ expect_equal(
+ tab %>%
+ slice_head(prop = .25) %>%
+ nrow(),
+ 2
+ )
+ expect_equal(
+ tab %>%
+ slice_tail(prop = .25) %>%
+ nrow(),
+ 2
+ )
+})
+
+test_that("slice_min/max, ungrouped", {
+ # with_ties must be FALSE
+ tab <- arrow_table(tbl)
+ expect_error(
+ tab %>% slice_max(int, n = 5),
+ "with_ties = TRUE"
+ )
+ expect_error(
+ tab %>% slice_min(int, n = 5),
+ "with_ties = TRUE"
+ )
+ compare_dplyr_binding(
+ .input %>%
+ slice_max(int, n = 4, with_ties = FALSE) %>%
+ collect(),
+ tbl
+ )
+ compare_dplyr_binding(
+ .input %>%
+ slice_min(int, n = 4, with_ties = FALSE) %>%
+ collect(),
+ tbl
+ )
+
+ compare_dplyr_binding(
+ .input %>%
+ slice_max(int, prop = .25, with_ties = FALSE) %>%
+ collect(),
+ tbl
+ )
+ compare_dplyr_binding(
+ .input %>%
+ slice_min(int, prop = .25, with_ties = FALSE) %>%
+ collect(),
+ tbl
+ )
+})
+
+test_that("slice_sample, ungrouped", {
+ tab <- arrow_table(tbl)
+ expect_error(
+ tab %>% slice_sample(replace = TRUE),
+ "Sampling with replacement"
+ )
+ expect_error(
+ tab %>% slice_sample(weight_by = dbl),
+ "weight_by"
+ )
+
+ # Because this is random (and we only have 10 rows), try several times
+ for (i in 1:10) {
+ sampled_prop <- tab %>%
+ slice_sample(prop = .2) %>%
+ collect() %>%
+ nrow()
+ if (sampled_prop == 2) break
+ }
+ expect_equal(sampled_prop, 2)
+
+ # Test that slice_sample(n) returns n rows
+ # With a larger dataset, we would be more confident to get exactly n
+ # but with this dataset, we should at least not get >n rows
+ sampled_n <- tab %>%
+ slice_sample(n = 2) %>%
+ collect() %>%
+ nrow()
+ expect_lte(sampled_n, 2)
+
+ # Test with dataset, which matters for the UDF HACK
+ sampled_n <- tab %>%
+ InMemoryDataset$create() %>%
+ slice_sample(n = 2) %>%
+ collect() %>%
+ nrow()
+ expect_lte(sampled_n, 2)
+})
+
+test_that("slice_* not supported with groups", {
+ grouped <- tbl %>%
+ arrow_table() %>%
+ group_by(lgl)
+ expect_error(
+ slice_head(grouped, n = 5),
+ "Slicing grouped data not supported in Arrow"
+ )
+ expect_error(
+ slice_tail(grouped, n = 5),
+ "Slicing grouped data not supported in Arrow"
+ )
+ expect_error(
+ slice_min(grouped, n = 5),
+ "Slicing grouped data not supported in Arrow"
+ )
+ expect_error(
+ slice_max(grouped, n = 5),
+ "Slicing grouped data not supported in Arrow"
+ )
+ expect_error(
+ slice_sample(grouped, n = 5),
+ "Slicing grouped data not supported in Arrow"
+ )
+})
+
+test_that("input validation", {
+ tab <- arrow_table(tbl)
+ for (p in list("a", -1, 2, c(.01, .02), NA_real_)) {
+ expect_error(
+ slice_head(tab, prop = !!p),
+ "`prop` must be a single numeric value between 0 and 1",
+ fixed = TRUE
+ )
+ }
+
+ expect_error(
+ tab %>% slice_tail(n = 3, with_ties = FALSE),
+ "`...` must be empty"
+ )
+})
+
+test_that("n <-> prop conversion when nrow is not known", {
+ joined <- tbl %>%
+ arrow_table() %>%
+ full_join(tbl, by = "int")
+ expect_true(is.na(nrow(joined)))
+
+ expect_error(
+ joined %>%
+ slice_min(int, prop = .25, with_ties = FALSE),
+ "Slicing with `prop` when"
+ )
+
+ expect_error(
+ joined %>%
+ slice_sample(n = 5),
+ "slice_sample() with `n` when",
+ fixed = TRUE
+ )
+})
+
+# TODO: handle edge case where prop = 1, do nothing?