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?