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/arrow-adbc.git


The following commit(s) were added to refs/heads/main by this push:
     new 8f357e5  feat(r): Add scoping + lifecycle helpers (#693)
8f357e5 is described below

commit 8f357e530be7d1db5673e7d406178e46152dc16c
Author: Dewey Dunnington <[email protected]>
AuthorDate: Wed May 24 09:12:38 2023 -0400

    feat(r): Add scoping + lifecycle helpers (#693)
    
    See https://github.com/r-dbi/adbc/discussions/4
    
    This PR adds some quality-of-life improvements for managing the
    lifecycle of databases, connections, statements, and streams that may
    have been generated by any of them. This required quite a bit of
    iteration but hopefully saves others working with ADBC in R some
    head-scratching trying to get the lifecycles correct.
    
    While poking through the lifecycles, I also fixed some errors (memory
    protection, consistency, strict prototypes, order of New -> SetOptions
    -> Init) and used the new helpers where appropriate (e.g., to avoid a
    dangling garbage-collectible reference if SetOptions errors).
    
     @krlmlr @nbenn do you mind having a quick look though the R bits?
    
    ``` r
    library(adbcdrivermanager)
    
    stmt <- local({
      # Registers exit handlers in the correct order to ensure prompt cleanup
      db <- local_adbc(adbc_database_init(adbc_driver_log()))
    
      con <- local_adbc(adbc_connection_init(db))
      adbc_connection_join(con, db)
    
      stmt <- local_adbc(adbc_statement_init(con))
      adbc_statement_join(stmt, con)
    
      adbc_xptr_move(stmt)
    })
    #> LogDatabaseNew()
    #> LogDatabaseInit()
    #> LogConnectionNew()
    #> LogConnectionInit()
    #> LogStatementNew()
    
    # Everything is released immediately
    adbc_statement_release(stmt)
    #> LogStatementRelease()
    #> LogConnectionRelease()
    #> LogDatabaseRelease()
    #> LogDriverRelease()
    
    # If an error occurs when creating one of the objects, everything
    # is still released immediately
    local({
      db <- local_adbc(adbc_database_init(adbc_driver_log()))
    
      con <- local_adbc(adbc_connection_init(db))
      adbc_connection_join(con, db)
    
      stop("Something happened!")
    
      stmt <- local_adbc(adbc_statement_init(con))
      adbc_statement_join(stmt, con)
    
      adbc_xptr_move(stmt)
    })
    #> LogDatabaseNew()
    #> LogDatabaseInit()
    #> LogConnectionNew()
    #> LogConnectionInit()
    #> Error in eval(quote({: Something happened!
    #> LogConnectionRelease()
    #> LogDatabaseRelease()
    #> LogDriverRelease()
    ```
    
    <sup>Created on 2023-05-19 with [reprex
    v2.0.2](https://reprex.tidyverse.org)</sup>
---
 .pre-commit-config.yaml                            |   4 +-
 r/adbcdrivermanager/DESCRIPTION                    |   3 +-
 r/adbcdrivermanager/NAMESPACE                      |   7 +
 r/adbcdrivermanager/R/adbc.R                       |  52 +++++--
 r/adbcdrivermanager/R/helpers.R                    | 170 +++++++++++++++++++++
 r/adbcdrivermanager/R/utils.R                      |  99 ++++++++++++
 r/adbcdrivermanager/man/adbc_connection_join.Rd    |  56 +++++++
 r/adbcdrivermanager/man/adbc_xptr_move.Rd          |  41 +++++
 r/adbcdrivermanager/man/with_adbc.Rd               |  65 ++++++++
 r/adbcdrivermanager/src/driver_log.c               |   3 +-
 r/adbcdrivermanager/src/driver_monkey.c            |   2 +-
 r/adbcdrivermanager/src/driver_void.c              |   2 +-
 r/adbcdrivermanager/src/init.c                     |  20 ++-
 r/adbcdrivermanager/src/radbc.cc                   |  65 +++++++-
 r/adbcdrivermanager/src/radbc.h                    |  20 +++
 .../tests/testthat/_snaps/driver_log.md            |   4 +-
 .../tests/testthat/_snaps/helpers.md               |  27 ++++
 r/adbcdrivermanager/tests/testthat/test-helpers.R  | 131 ++++++++++++++++
 r/adbcdrivermanager/tests/testthat/test-utils.R    |  33 ++++
 r/adbcdrivermanager/tools/make-callentries.R       |   4 +-
 20 files changed, 772 insertions(+), 36 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 8353583..37b5e9c 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -28,9 +28,9 @@ repos:
     - id: check-yaml
       exclude: ci/conda/meta.yaml
     - id: end-of-file-fixer
-      exclude: r/adbcdrivermanager/tests/testthat/_snaps/driver_log.md
+      exclude: "^r/.*?/_snaps/.*?.md$"
     - id: trailing-whitespace
-      exclude: r/adbcdrivermanager/tests/testthat/_snaps/driver_log.md
+      exclude: "^r/.*?/_snaps/.*?.md$"
   - repo: https://github.com/pocc/pre-commit-hooks
     rev: v1.3.5
     hooks:
diff --git a/r/adbcdrivermanager/DESCRIPTION b/r/adbcdrivermanager/DESCRIPTION
index 555fa4c..0fb1799 100644
--- a/r/adbcdrivermanager/DESCRIPTION
+++ b/r/adbcdrivermanager/DESCRIPTION
@@ -17,7 +17,8 @@ Roxygen: list(markdown = TRUE)
 RoxygenNote: 7.2.3
 SystemRequirements: C++11
 Suggests:
-    testthat (>= 3.0.0)
+    testthat (>= 3.0.0),
+    withr
 Config/testthat/edition: 3
 Config/build/bootstrap: TRUE
 URL: https://github.com/apache/arrow-adbc
diff --git a/r/adbcdrivermanager/NAMESPACE b/r/adbcdrivermanager/NAMESPACE
index c13dd38..24c0528 100644
--- a/r/adbcdrivermanager/NAMESPACE
+++ b/r/adbcdrivermanager/NAMESPACE
@@ -32,6 +32,7 @@ export(adbc_connection_get_table_schema)
 export(adbc_connection_get_table_types)
 export(adbc_connection_init)
 export(adbc_connection_init_default)
+export(adbc_connection_join)
 export(adbc_connection_read_partition)
 export(adbc_connection_release)
 export(adbc_connection_rollback)
@@ -50,10 +51,16 @@ export(adbc_statement_execute_query)
 export(adbc_statement_get_parameter_schema)
 export(adbc_statement_init)
 export(adbc_statement_init_default)
+export(adbc_statement_join)
 export(adbc_statement_prepare)
 export(adbc_statement_release)
 export(adbc_statement_set_options)
 export(adbc_statement_set_sql_query)
 export(adbc_statement_set_substrait_plan)
+export(adbc_stream_join)
+export(adbc_xptr_is_valid)
+export(adbc_xptr_move)
+export(local_adbc)
+export(with_adbc)
 importFrom(utils,str)
 useDynLib(adbcdrivermanager, .registration = TRUE)
diff --git a/r/adbcdrivermanager/R/adbc.R b/r/adbcdrivermanager/R/adbc.R
index f9a05c8..773e9d6 100644
--- a/r/adbcdrivermanager/R/adbc.R
+++ b/r/adbcdrivermanager/R/adbc.R
@@ -53,13 +53,17 @@ adbc_database_init_default <- function(driver, options = 
NULL, subclass = charac
   }
 
   database$driver <- driver
-  adbc_database_set_options(database, options)
 
-  error <- adbc_allocate_error()
-  status <- .Call(RAdbcDatabaseInit, database, error)
-  stop_for_error(status, error)
-  class(database) <- c(subclass, class(database))
-  database
+  with_adbc(database, {
+    adbc_database_set_options(database, options)
+
+    error <- adbc_allocate_error()
+    status <- .Call(RAdbcDatabaseInit, database, error)
+    stop_for_error(status, error)
+    class(database) <- c(subclass, class(database))
+
+    adbc_xptr_move(database)
+  })
 }
 
 #' @rdname adbc_database_init
@@ -118,13 +122,18 @@ adbc_connection_init.default <- function(database, ...) {
 adbc_connection_init_default <- function(database, options = NULL, subclass = 
character()) {
   connection <- .Call(RAdbcConnectionNew)
   connection$database <- database
-  error <- adbc_allocate_error()
-  status <- .Call(RAdbcConnectionInit, connection, database, error)
-  stop_for_error(status, error)
 
-  adbc_connection_set_options(connection, options)
-  class(connection) <- c(subclass, class(connection))
-  connection
+  with_adbc(connection, {
+    adbc_connection_set_options(connection, options)
+
+    error <- adbc_allocate_error()
+    status <- .Call(RAdbcConnectionInit, connection, database, error)
+    stop_for_error(status, error)
+
+    class(connection) <- c(subclass, class(connection))
+
+    adbc_xptr_move(connection)
+  })
 }
 
 #' @rdname adbc_connection_init
@@ -151,6 +160,11 @@ adbc_connection_set_options <- function(connection, 
options) {
 #' @rdname adbc_connection_init
 #' @export
 adbc_connection_release <- function(connection) {
+  if (isTRUE(connection$.release_database)) {
+    database <- connection$database
+    on.exit(adbc_database_release(database))
+  }
+
   error <- adbc_allocate_error()
   status <- .Call(RAdbcConnectionRelease, connection, error)
   stop_for_error(status, error)
@@ -308,9 +322,12 @@ adbc_statement_init.default <- function(connection, ...) {
 adbc_statement_init_default <- function(connection, options = NULL, subclass = 
character()) {
   statement <- .Call(RAdbcStatementNew, connection)
   statement$connection <- connection
-  adbc_statement_set_options(statement, options)
-  class(statement) <- c(subclass, class(statement))
-  statement
+
+  with_adbc(statement, {
+    adbc_statement_set_options(statement, options)
+    class(statement) <- c(subclass, class(statement))
+    adbc_xptr_move(statement)
+  })
 }
 
 #' @rdname adbc_statement_init
@@ -337,6 +354,11 @@ adbc_statement_set_options <- function(statement, options) 
{
 #' @rdname adbc_statement_init
 #' @export
 adbc_statement_release <- function(statement) {
+  if (isTRUE(statement$.release_connection)) {
+    connection <- statement$connection
+    on.exit(adbc_connection_release(connection))
+  }
+
   error <- adbc_allocate_error()
   status <- .Call(RAdbcStatementRelease, statement, error)
   stop_for_error(status, error)
diff --git a/r/adbcdrivermanager/R/helpers.R b/r/adbcdrivermanager/R/helpers.R
new file mode 100644
index 0000000..809faa5
--- /dev/null
+++ b/r/adbcdrivermanager/R/helpers.R
@@ -0,0 +1,170 @@
+# 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.
+
+#' Cleanup helpers
+#'
+#' Managing the lifecycle of databases, connections, and statements can
+#' be complex and error-prone. The R objects that wrap the underlying ADBC
+#' pointers will perform cleanup in the correct order if you rely on garbage
+#' collection (i.e., do nothing and let the objects go out of scope); however
+#' it is good practice to explicitly clean up these objects. These helpers
+#' are designed to make explicit and predictable cleanup easy to accomplish.
+#'
+#' Note that you can use [adbc_connection_join()],
+#' [adbc_statement_join()], and [adbc_stream_join()]
+#' to tie the lifecycle of the parent object to that of the child object.
+#' These functions mark any previous references to the parent object as
+#' released so you can still use local and with helpers to manage the parent
+#' object before it is joined.
+#'
+#' @param x An ADBC database, ADBC connection, ADBC statement, or
+#'   nanoarrow_array_stream returned from calls to an ADBC function.
+#' @param code Code to execute before cleaning up the input.
+#' @param .local_envir The execution environment whose scope should be tied
+#'   to the input.
+#'
+#' @return
+#'   - `with_adbc()` returns the result of `code`
+#'   - `local_adbc()` returns the input, invisibly.
+#' @export
+#'
+#' @examples
+#' # Using with_adbc():
+#' with_adbc(db <- adbc_database_init(adbc_driver_void()), {
+#'   with_adbc(con <- adbc_connection_init(db), {
+#'     with_adbc(stmt <- adbc_statement_init(con), {
+#'       # adbc_statement_set_sql_query(stmt, "SELECT * FROM foofy")
+#'       # adbc_statement_execute_query(stmt)
+#'       "some result"
+#'     })
+#'   })
+#' })
+#'
+#' # Using local_adbc_*() (works best within a function, test, or local())
+#' local({
+#'   db <- local_adbc(adbc_database_init(adbc_driver_void()))
+#'   con <- local_adbc(adbc_connection_init(db))
+#'   stmt <- local_adbc(adbc_statement_init(con))
+#'   # adbc_statement_set_sql_query(stmt, "SELECT * FROM foofy")
+#'   # adbc_statement_execute_query(stmt)
+#'   "some result"
+#' })
+#'
+with_adbc <- function(x, code) {
+  assert_adbc(x)
+
+  on.exit(adbc_release_non_null(x))
+  force(code)
+}
+
+#' @rdname with_adbc
+#' @export
+local_adbc <- function(x, .local_envir = parent.frame()) {
+  assert_adbc(x)
+
+  withr::defer(adbc_release_non_null(x), envir = .local_envir)
+  invisible(x)
+}
+
+#' Join the lifecycle of a unique parent to its child
+#'
+#' It is occasionally useful to return a connection, statement, or stream
+#' from a function that was created from a unique parent. These helpers
+#' tie the lifecycle of a unique parent object to its child such that the
+#' parent object is released predictably and immediately after the child.
+#' These functions will invalidate all references to the previous R object.
+#'
+#' @param database A database created with [adbc_database_init()]
+#' @param connection A connection created with [adbc_connection_init()]
+#' @param statement A statement created with [adbc_statement_init()]
+#' @param stream A 
[nanoarrow_array_stream][nanoarrow::as_nanoarrow_array_stream]
+#' @inheritParams with_adbc
+#'
+#' @return The input, invisibly.
+#' @export
+#'
+#' @examples
+#' # Use local_adbc to ensure prompt cleanup on error;
+#' # use join functions to return a single object that manages
+#' # the lifecycle of all three.
+#' stmt <- local({
+#'   db <- local_adbc(adbc_database_init(adbc_driver_log()))
+#'
+#'   con <- local_adbc(adbc_connection_init(db))
+#'   adbc_connection_join(con, db)
+#'
+#'   stmt <- local_adbc(adbc_statement_init(con))
+#'   adbc_statement_join(stmt, con)
+#'
+#'   adbc_xptr_move(stmt)
+#' })
+#'
+#' # Everything is released immediately when the last object is released
+#' adbc_statement_release(stmt)
+#'
+adbc_connection_join <- function(connection, database) {
+  assert_adbc(connection, "adbc_connection")
+  assert_adbc(database, "adbc_database")
+
+  connection$.release_database <- TRUE
+  connection$database <- adbc_xptr_move(database)
+  invisible(connection)
+}
+
+#' @rdname adbc_connection_join
+#' @export
+adbc_statement_join <- function(statement, connection) {
+  assert_adbc(statement, "adbc_statement")
+  assert_adbc(connection, "adbc_connection")
+
+  statement$.release_connection <- TRUE
+  statement$connection <- adbc_xptr_move(connection)
+  invisible(statement)
+}
+
+#' @rdname adbc_connection_join
+#' @export
+adbc_stream_join <- function(stream, x) {
+  if (utils::packageVersion("nanoarrow") >= "0.1.0.9000") {
+    assert_adbc(stream, "nanoarrow_array_stream")
+    assert_adbc(x)
+
+    self_contained_finalizer <- function() {
+      try(adbc_release_non_null(x))
+    }
+
+    # Make sure we don't keep any variables around that aren't needed
+    # for the finalizer and make sure we invalidate the original statement
+    self_contained_finalizer_env <- as.environment(
+      list(x = adbc_xptr_move(x))
+    )
+    parent.env(self_contained_finalizer_env) <- 
asNamespace("adbcdrivermanager")
+    environment(self_contained_finalizer) <- self_contained_finalizer_env
+
+    # This finalizer will run immediately on release (if released explicitly
+    # on the main R thread) or on garbage collection otherwise.
+
+    # Until the release version of nanoarrow contains this we will get a check
+    # warning for nanoarrow::array_stream_set_finalizer()
+    set_finalizer <- asNamespace("nanoarrow")[["array_stream_set_finalizer"]]
+    set_finalizer(stream, self_contained_finalizer)
+
+    invisible(stream)
+  } else {
+    stop("adbc_stream_join_statement() requires nanoarrow >= 0.2.0")
+  }
+}
diff --git a/r/adbcdrivermanager/R/utils.R b/r/adbcdrivermanager/R/utils.R
index af9ecaf..e1e7577 100644
--- a/r/adbcdrivermanager/R/utils.R
+++ b/r/adbcdrivermanager/R/utils.R
@@ -94,3 +94,102 @@ str.adbc_xptr <- function(object, ...) {
   str(env_proxy, ...)
   invisible(object)
 }
+
+
+#' Low-level pointer details
+#'
+#' - `adbc_xptr_move()` allocates a fresh R object and moves all values pointed
+#'   to by `x` into it. The original R object is invalidated by zeroing its
+#'   content. This is useful when returning from a function where
+#'   [lifecycle helpers][with_adbc] were used to manage the original
+#'   object.
+#' - `adbc_xptr_is_valid()` provides a means by which to test for an 
invalidated
+#'   pointer.
+#'
+#' @param x An 'adbc_database', 'adbc_connection', 'adbc_statement', or
+#'   'nanoarrow_array_stream'
+#'
+#' @return
+#' - `adbc_xptr_move()`: A freshly-allocated R object identical to `x`
+#' - `adbc_xptr_is_valid()`: Returns FALSE if the ADBC object pointed to by `x`
+#'   has been invalidated.
+#' @export
+#'
+#' @examples
+#' db <- adbc_database_init(adbc_driver_void())
+#' adbc_xptr_is_valid(db)
+#' db_new <- adbc_xptr_move(db)
+#' adbc_xptr_is_valid(db)
+#' adbc_xptr_is_valid(db_new)
+#'
+adbc_xptr_move <- function(x) {
+  if (inherits(x, "adbc_database")) {
+    .Call(RAdbcMoveDatabase, x)
+  } else if (inherits(x, "adbc_connection")) {
+    .Call(RAdbcMoveConnection, x)
+  } else if (inherits(x, "adbc_statement")) {
+    .Call(RAdbcMoveStatement, x)
+  } else if (inherits(x, "nanoarrow_array_stream")) {
+    stream <- nanoarrow::nanoarrow_allocate_array_stream()
+    nanoarrow::nanoarrow_pointer_move(x, stream)
+    stream
+  } else {
+    assert_adbc(x)
+  }
+}
+
+#' @rdname adbc_xptr_move
+#' @export
+adbc_xptr_is_valid <- function(x) {
+  if (inherits(x, "adbc_database")) {
+    .Call(RAdbcDatabaseValid, x)
+  } else if (inherits(x, "adbc_connection")) {
+    .Call(RAdbcConnectionValid, x)
+  } else if (inherits(x, "adbc_statement")) {
+    .Call(RAdbcStatementValid, x)
+  } else if (inherits(x, "nanoarrow_array_stream")) {
+    nanoarrow::nanoarrow_pointer_is_valid(x)
+  } else {
+    assert_adbc(x)
+  }
+}
+
+# Usually we want errors for an attempt at double release; however,
+# the helpers we want to be compatible with adbc_xptr_move() which sets the
+# managed pointer to NULL.
+adbc_release_non_null <- function(x) {
+  if (!adbc_xptr_is_valid(x)) {
+    return()
+  }
+
+  if (inherits(x, "adbc_database")) {
+    adbc_database_release(x)
+  } else if (inherits(x, "adbc_connection")) {
+    adbc_connection_release(x)
+  } else if (inherits(x, "adbc_statement")) {
+    adbc_statement_release(x)
+  } else if (inherits(x, "nanoarrow_array_stream")) {
+    nanoarrow::nanoarrow_pointer_release(x)
+  } else {
+    assert_adbc(x)
+  }
+}
+
+adbc_classes <- c(
+  "adbc_database", "adbc_connection", "adbc_statement",
+  "nanoarrow_array_stream"
+)
+
+assert_adbc <- function(x, what = adbc_classes) {
+  if (inherits(x, what)) {
+    return(invisible(x))
+  }
+
+  stop(
+    sprintf(
+      "`x` must inherit from one of: %s",
+      paste0("'", what, "'", collapse = ", ")
+    ),
+    call. = sys.call(-1)
+  )
+}
diff --git a/r/adbcdrivermanager/man/adbc_connection_join.Rd 
b/r/adbcdrivermanager/man/adbc_connection_join.Rd
new file mode 100644
index 0000000..823a33b
--- /dev/null
+++ b/r/adbcdrivermanager/man/adbc_connection_join.Rd
@@ -0,0 +1,56 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/helpers.R
+\name{adbc_connection_join}
+\alias{adbc_connection_join}
+\alias{adbc_statement_join}
+\alias{adbc_stream_join}
+\title{Join the lifecycle of a unique parent to its child}
+\usage{
+adbc_connection_join(connection, database)
+
+adbc_statement_join(statement, connection)
+
+adbc_stream_join(stream, x)
+}
+\arguments{
+\item{connection}{A connection created with 
\code{\link[=adbc_connection_init]{adbc_connection_init()}}}
+
+\item{database}{A database created with 
\code{\link[=adbc_database_init]{adbc_database_init()}}}
+
+\item{statement}{A statement created with 
\code{\link[=adbc_statement_init]{adbc_statement_init()}}}
+
+\item{stream}{A 
\link[nanoarrow:as_nanoarrow_array_stream]{nanoarrow_array_stream}}
+
+\item{x}{An ADBC database, ADBC connection, ADBC statement, or
+nanoarrow_array_stream returned from calls to an ADBC function.}
+}
+\value{
+The input, invisibly.
+}
+\description{
+It is occasionally useful to return a connection, statement, or stream
+from a function that was created from a unique parent. These helpers
+tie the lifecycle of a unique parent object to its child such that the
+parent object is released predictably and immediately after the child.
+These functions will invalidate all references to the previous R object.
+}
+\examples{
+# Use local_adbc to ensure prompt cleanup on error;
+# use join functions to return a single object that manages
+# the lifecycle of all three.
+stmt <- local({
+  db <- local_adbc(adbc_database_init(adbc_driver_log()))
+
+  con <- local_adbc(adbc_connection_init(db))
+  adbc_connection_join(con, db)
+
+  stmt <- local_adbc(adbc_statement_init(con))
+  adbc_statement_join(stmt, con)
+
+  adbc_xptr_move(stmt)
+})
+
+# Everything is released immediately when the last object is released
+adbc_statement_release(stmt)
+
+}
diff --git a/r/adbcdrivermanager/man/adbc_xptr_move.Rd 
b/r/adbcdrivermanager/man/adbc_xptr_move.Rd
new file mode 100644
index 0000000..2dc2cea
--- /dev/null
+++ b/r/adbcdrivermanager/man/adbc_xptr_move.Rd
@@ -0,0 +1,41 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/utils.R
+\name{adbc_xptr_move}
+\alias{adbc_xptr_move}
+\alias{adbc_xptr_is_valid}
+\title{Low-level pointer details}
+\usage{
+adbc_xptr_move(x)
+
+adbc_xptr_is_valid(x)
+}
+\arguments{
+\item{x}{An 'adbc_database', 'adbc_connection', 'adbc_statement', or
+'nanoarrow_array_stream'}
+}
+\value{
+\itemize{
+\item \code{adbc_xptr_move()}: A freshly-allocated R object identical to 
\code{x}
+\item \code{adbc_xptr_is_valid()}: Returns FALSE if the ADBC object pointed to 
by \code{x}
+has been invalidated.
+}
+}
+\description{
+\itemize{
+\item \code{adbc_xptr_move()} allocates a fresh R object and moves all values 
pointed
+to by \code{x} into it. The original R object is invalidated by zeroing its
+content. This is useful when returning from a function where
+\link[=with_adbc]{lifecycle helpers} were used to manage the original
+object.
+\item \code{adbc_xptr_is_valid()} provides a means by which to test for an 
invalidated
+pointer.
+}
+}
+\examples{
+db <- adbc_database_init(adbc_driver_void())
+adbc_xptr_is_valid(db)
+db_new <- adbc_xptr_move(db)
+adbc_xptr_is_valid(db)
+adbc_xptr_is_valid(db_new)
+
+}
diff --git a/r/adbcdrivermanager/man/with_adbc.Rd 
b/r/adbcdrivermanager/man/with_adbc.Rd
new file mode 100644
index 0000000..5e16a9c
--- /dev/null
+++ b/r/adbcdrivermanager/man/with_adbc.Rd
@@ -0,0 +1,65 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/helpers.R
+\name{with_adbc}
+\alias{with_adbc}
+\alias{local_adbc}
+\title{Cleanup helpers}
+\usage{
+with_adbc(x, code)
+
+local_adbc(x, .local_envir = parent.frame())
+}
+\arguments{
+\item{x}{An ADBC database, ADBC connection, ADBC statement, or
+nanoarrow_array_stream returned from calls to an ADBC function.}
+
+\item{code}{Code to execute before cleaning up the input.}
+
+\item{.local_envir}{The execution environment whose scope should be tied
+to the input.}
+}
+\value{
+\itemize{
+\item \code{with_adbc()} returns the result of \code{code}
+\item \code{local_adbc()} returns the input, invisibly.
+}
+}
+\description{
+Managing the lifecycle of databases, connections, and statements can
+be complex and error-prone. The R objects that wrap the underlying ADBC
+pointers will perform cleanup in the correct order if you rely on garbage
+collection (i.e., do nothing and let the objects go out of scope); however
+it is good practice to explicitly clean up these objects. These helpers
+are designed to make explicit and predictable cleanup easy to accomplish.
+}
+\details{
+Note that you can use 
\code{\link[=adbc_connection_join]{adbc_connection_join()}},
+\code{\link[=adbc_statement_join]{adbc_statement_join()}}, and 
\code{\link[=adbc_stream_join]{adbc_stream_join()}}
+to tie the lifecycle of the parent object to that of the child object.
+These functions mark any previous references to the parent object as
+released so you can still use local and with helpers to manage the parent
+object before it is joined.
+}
+\examples{
+# Using with_adbc():
+with_adbc(db <- adbc_database_init(adbc_driver_void()), {
+  with_adbc(con <- adbc_connection_init(db), {
+    with_adbc(stmt <- adbc_statement_init(con), {
+      # adbc_statement_set_sql_query(stmt, "SELECT * FROM foofy")
+      # adbc_statement_execute_query(stmt)
+      "some result"
+    })
+  })
+})
+
+# Using local_adbc_*() (works best within a function, test, or local())
+local({
+  db <- local_adbc(adbc_database_init(adbc_driver_void()))
+  con <- local_adbc(adbc_connection_init(db))
+  stmt <- local_adbc(adbc_statement_init(con))
+  # adbc_statement_set_sql_query(stmt, "SELECT * FROM foofy")
+  # adbc_statement_execute_query(stmt)
+  "some result"
+})
+
+}
diff --git a/r/adbcdrivermanager/src/driver_log.c 
b/r/adbcdrivermanager/src/driver_log.c
index 5a0520d..543ae9a 100644
--- a/r/adbcdrivermanager/src/driver_log.c
+++ b/r/adbcdrivermanager/src/driver_log.c
@@ -286,7 +286,6 @@ static AdbcStatusCode LogStatementSetSqlQuery(struct 
AdbcStatement* statement,
 
 static AdbcStatusCode LogDriverInitFunc(int version, void* raw_driver,
                                         struct AdbcError* error) {
-  Rprintf("LogDriverInitFunc()\n");
   if (version != ADBC_VERSION_1_0_0) return ADBC_STATUS_NOT_IMPLEMENTED;
   struct AdbcDriver* driver = (struct AdbcDriver*)raw_driver;
   memset(driver, 0, sizeof(struct AdbcDriver));
@@ -334,7 +333,7 @@ static AdbcStatusCode LogDriverInitFunc(int version, void* 
raw_driver,
   return ADBC_STATUS_OK;
 }
 
-SEXP RAdbcLogDriverInitFunc() {
+SEXP RAdbcLogDriverInitFunc(void) {
   SEXP xptr =
       PROTECT(R_MakeExternalPtrFn((DL_FUNC)LogDriverInitFunc, R_NilValue, 
R_NilValue));
   Rf_setAttrib(xptr, R_ClassSymbol, Rf_mkString("adbc_driver_init_func"));
diff --git a/r/adbcdrivermanager/src/driver_monkey.c 
b/r/adbcdrivermanager/src/driver_monkey.c
index 6dc8738..9076975 100644
--- a/r/adbcdrivermanager/src/driver_monkey.c
+++ b/r/adbcdrivermanager/src/driver_monkey.c
@@ -333,7 +333,7 @@ static AdbcStatusCode MonkeyDriverInitFunc(int version, 
void* raw_driver,
   return ADBC_STATUS_OK;
 }
 
-SEXP RAdbcMonkeyDriverInitFunc() {
+SEXP RAdbcMonkeyDriverInitFunc(void) {
   SEXP xptr =
       PROTECT(R_MakeExternalPtrFn((DL_FUNC)MonkeyDriverInitFunc, R_NilValue, 
R_NilValue));
   Rf_setAttrib(xptr, R_ClassSymbol, Rf_mkString("adbc_driver_init_func"));
diff --git a/r/adbcdrivermanager/src/driver_void.c 
b/r/adbcdrivermanager/src/driver_void.c
index b349387..59cfe03 100644
--- a/r/adbcdrivermanager/src/driver_void.c
+++ b/r/adbcdrivermanager/src/driver_void.c
@@ -307,7 +307,7 @@ static AdbcStatusCode VoidDriverInitFunc(int version, void* 
raw_driver,
   return ADBC_STATUS_OK;
 }
 
-SEXP RAdbcVoidDriverInitFunc() {
+SEXP RAdbcVoidDriverInitFunc(void) {
   SEXP xptr =
       PROTECT(R_MakeExternalPtrFn((DL_FUNC)VoidDriverInitFunc, R_NilValue, 
R_NilValue));
   Rf_setAttrib(xptr, R_ClassSymbol, Rf_mkString("adbc_driver_init_func"));
diff --git a/r/adbcdrivermanager/src/init.c b/r/adbcdrivermanager/src/init.c
index de2f95f..0d9f65e 100644
--- a/r/adbcdrivermanager/src/init.c
+++ b/r/adbcdrivermanager/src/init.c
@@ -20,20 +20,24 @@
 #include <Rinternals.h>
 
 /* generated by tools/make-callentries.R */
-SEXP RAdbcLogDriverInitFunc();
-SEXP RAdbcMonkeyDriverInitFunc();
-SEXP RAdbcVoidDriverInitFunc();
+SEXP RAdbcLogDriverInitFunc(void);
+SEXP RAdbcMonkeyDriverInitFunc(void);
+SEXP RAdbcVoidDriverInitFunc(void);
 SEXP RAdbcAllocateError(SEXP shelter_sexp);
 SEXP RAdbcErrorProxy(SEXP error_xptr);
 SEXP RAdbcStatusCodeMessage(SEXP status_sexp);
 SEXP RAdbcLoadDriver(SEXP driver_name_sexp, SEXP entrypoint_sexp);
 SEXP RAdbcLoadDriverFromInitFunc(SEXP driver_init_func_xptr);
 SEXP RAdbcDatabaseNew(SEXP driver_init_func_xptr);
+SEXP RAdbcMoveDatabase(SEXP database_xptr);
+SEXP RAdbcDatabaseValid(SEXP database_xptr);
 SEXP RAdbcDatabaseSetOption(SEXP database_xptr, SEXP key_sexp, SEXP value_sexp,
                             SEXP error_xptr);
 SEXP RAdbcDatabaseInit(SEXP database_xptr, SEXP error_xptr);
 SEXP RAdbcDatabaseRelease(SEXP database_xptr, SEXP error_xptr);
-SEXP RAdbcConnectionNew();
+SEXP RAdbcConnectionNew(void);
+SEXP RAdbcMoveConnection(SEXP connection_xptr);
+SEXP RAdbcConnectionValid(SEXP connection_xptr);
 SEXP RAdbcConnectionSetOption(SEXP connection_xptr, SEXP key_sexp, SEXP 
value_sexp,
                               SEXP error_xptr);
 SEXP RAdbcConnectionInit(SEXP connection_xptr, SEXP database_xptr, SEXP 
error_xptr);
@@ -54,6 +58,8 @@ SEXP RAdbcConnectionReadPartition(SEXP connection_xptr, SEXP 
serialized_partitio
 SEXP RAdbcConnectionCommit(SEXP connection_xptr, SEXP error_xptr);
 SEXP RAdbcConnectionRollback(SEXP connection_xptr, SEXP error_xptr);
 SEXP RAdbcStatementNew(SEXP connection_xptr);
+SEXP RAdbcMoveStatement(SEXP statement_xptr);
+SEXP RAdbcStatementValid(SEXP statement_xptr);
 SEXP RAdbcStatementSetOption(SEXP statement_xptr, SEXP key_sexp, SEXP 
value_sexp,
                              SEXP error_xptr);
 SEXP RAdbcStatementRelease(SEXP statement_xptr, SEXP error_xptr);
@@ -81,10 +87,14 @@ static const R_CallMethodDef CallEntries[] = {
     {"RAdbcLoadDriver", (DL_FUNC)&RAdbcLoadDriver, 2},
     {"RAdbcLoadDriverFromInitFunc", (DL_FUNC)&RAdbcLoadDriverFromInitFunc, 1},
     {"RAdbcDatabaseNew", (DL_FUNC)&RAdbcDatabaseNew, 1},
+    {"RAdbcMoveDatabase", (DL_FUNC)&RAdbcMoveDatabase, 1},
+    {"RAdbcDatabaseValid", (DL_FUNC)&RAdbcDatabaseValid, 1},
     {"RAdbcDatabaseSetOption", (DL_FUNC)&RAdbcDatabaseSetOption, 4},
     {"RAdbcDatabaseInit", (DL_FUNC)&RAdbcDatabaseInit, 2},
     {"RAdbcDatabaseRelease", (DL_FUNC)&RAdbcDatabaseRelease, 2},
     {"RAdbcConnectionNew", (DL_FUNC)&RAdbcConnectionNew, 0},
+    {"RAdbcMoveConnection", (DL_FUNC)&RAdbcMoveConnection, 1},
+    {"RAdbcConnectionValid", (DL_FUNC)&RAdbcConnectionValid, 1},
     {"RAdbcConnectionSetOption", (DL_FUNC)&RAdbcConnectionSetOption, 4},
     {"RAdbcConnectionInit", (DL_FUNC)&RAdbcConnectionInit, 3},
     {"RAdbcConnectionRelease", (DL_FUNC)&RAdbcConnectionRelease, 2},
@@ -96,6 +106,8 @@ static const R_CallMethodDef CallEntries[] = {
     {"RAdbcConnectionCommit", (DL_FUNC)&RAdbcConnectionCommit, 2},
     {"RAdbcConnectionRollback", (DL_FUNC)&RAdbcConnectionRollback, 2},
     {"RAdbcStatementNew", (DL_FUNC)&RAdbcStatementNew, 1},
+    {"RAdbcMoveStatement", (DL_FUNC)&RAdbcMoveStatement, 1},
+    {"RAdbcStatementValid", (DL_FUNC)&RAdbcStatementValid, 1},
     {"RAdbcStatementSetOption", (DL_FUNC)&RAdbcStatementSetOption, 4},
     {"RAdbcStatementRelease", (DL_FUNC)&RAdbcStatementRelease, 2},
     {"RAdbcStatementSetSqlQuery", (DL_FUNC)&RAdbcStatementSetSqlQuery, 3},
diff --git a/r/adbcdrivermanager/src/radbc.cc b/r/adbcdrivermanager/src/radbc.cc
index 1b977cf..a04fea8 100644
--- a/r/adbcdrivermanager/src/radbc.cc
+++ b/r/adbcdrivermanager/src/radbc.cc
@@ -112,7 +112,7 @@ extern "C" SEXP RAdbcLoadDriverFromInitFunc(SEXP 
driver_init_func_xptr) {
 }
 
 extern "C" SEXP RAdbcDatabaseNew(SEXP driver_init_func_xptr) {
-  SEXP database_xptr = adbc_allocate_xptr<AdbcDatabase>();
+  SEXP database_xptr = PROTECT(adbc_allocate_xptr<AdbcDatabase>());
   R_RegisterCFinalizer(database_xptr, &finalize_database_xptr);
 
   AdbcDatabase* database = adbc_from_xptr<AdbcDatabase>(database_xptr);
@@ -132,9 +132,28 @@ extern "C" SEXP RAdbcDatabaseNew(SEXP 
driver_init_func_xptr) {
     adbc_error_stop(status, &error, "RAdbcDatabaseNew()");
   }
 
+  UNPROTECT(1);
   return database_xptr;
 }
 
+extern "C" SEXP RAdbcMoveDatabase(SEXP database_xptr) {
+  AdbcDatabase* database = adbc_from_xptr<AdbcDatabase>(database_xptr);
+  SEXP database_xptr_new = PROTECT(adbc_allocate_xptr<AdbcDatabase>());
+  AdbcDatabase* database_new = adbc_from_xptr<AdbcDatabase>(database_xptr_new);
+
+  memcpy(database_new, database, sizeof(AdbcDatabase));
+  adbc_xptr_move_attrs(database_xptr, database_xptr_new);
+  memset(database, 0, sizeof(AdbcDatabase));
+
+  UNPROTECT(1);
+  return database_xptr_new;
+}
+
+extern "C" SEXP RAdbcDatabaseValid(SEXP database_xptr) {
+  AdbcDatabase* database = adbc_from_xptr<AdbcDatabase>(database_xptr);
+  return Rf_ScalarLogical(database != nullptr && database->private_data != 
nullptr);
+}
+
 extern "C" SEXP RAdbcDatabaseSetOption(SEXP database_xptr, SEXP key_sexp, SEXP 
value_sexp,
                                        SEXP error_xptr) {
   auto database = adbc_from_xptr<AdbcDatabase>(database_xptr);
@@ -153,8 +172,8 @@ extern "C" SEXP RAdbcDatabaseInit(SEXP database_xptr, SEXP 
error_xptr) {
 extern "C" SEXP RAdbcDatabaseRelease(SEXP database_xptr, SEXP error_xptr) {
   auto database = adbc_from_xptr<AdbcDatabase>(database_xptr);
   auto error = adbc_from_xptr<AdbcError>(error_xptr);
-  R_SetExternalPtrTag(database_xptr, R_NilValue);
-  return adbc_wrap_status(AdbcDatabaseRelease(database, error));
+  int status = AdbcDatabaseRelease(database, error);
+  return adbc_wrap_status(status);
 }
 
 static void finalize_connection_xptr(SEXP connection_xptr) {
@@ -172,7 +191,7 @@ static void finalize_connection_xptr(SEXP connection_xptr) {
   adbc_xptr_default_finalize<AdbcConnection>(connection_xptr);
 }
 
-extern "C" SEXP RAdbcConnectionNew() {
+extern "C" SEXP RAdbcConnectionNew(void) {
   SEXP connection_xptr = PROTECT(adbc_allocate_xptr<AdbcConnection>());
   R_RegisterCFinalizer(connection_xptr, &finalize_connection_xptr);
 
@@ -186,6 +205,24 @@ extern "C" SEXP RAdbcConnectionNew() {
   return connection_xptr;
 }
 
+extern "C" SEXP RAdbcMoveConnection(SEXP connection_xptr) {
+  AdbcConnection* connection = adbc_from_xptr<AdbcConnection>(connection_xptr);
+  SEXP connection_xptr_new = PROTECT(adbc_allocate_xptr<AdbcConnection>());
+  AdbcConnection* connection_new = 
adbc_from_xptr<AdbcConnection>(connection_xptr_new);
+
+  memcpy(connection_new, connection, sizeof(AdbcConnection));
+  adbc_xptr_move_attrs(connection_xptr, connection_xptr_new);
+  memset(connection, 0, sizeof(AdbcConnection));
+
+  UNPROTECT(1);
+  return connection_xptr_new;
+}
+
+extern "C" SEXP RAdbcConnectionValid(SEXP connection_xptr) {
+  AdbcConnection* connection = adbc_from_xptr<AdbcConnection>(connection_xptr);
+  return Rf_ScalarLogical(connection != nullptr && connection->private_data != 
nullptr);
+}
+
 extern "C" SEXP RAdbcConnectionSetOption(SEXP connection_xptr, SEXP key_sexp,
                                          SEXP value_sexp, SEXP error_xptr) {
   auto connection = adbc_from_xptr<AdbcConnection>(connection_xptr);
@@ -215,7 +252,6 @@ extern "C" SEXP RAdbcConnectionRelease(SEXP 
connection_xptr, SEXP error_xptr) {
   auto connection = adbc_from_xptr<AdbcConnection>(connection_xptr);
   auto error = adbc_from_xptr<AdbcError>(error_xptr);
   int status = AdbcConnectionRelease(connection, error);
-  R_SetExternalPtrProtected(connection_xptr, R_NilValue);
   return adbc_wrap_status(status);
 }
 
@@ -346,6 +382,24 @@ extern "C" SEXP RAdbcStatementNew(SEXP connection_xptr) {
   return statement_xptr;
 }
 
+extern "C" SEXP RAdbcMoveStatement(SEXP statement_xptr) {
+  AdbcStatement* statement = adbc_from_xptr<AdbcStatement>(statement_xptr);
+  SEXP statement_xptr_new = PROTECT(adbc_allocate_xptr<AdbcStatement>());
+  AdbcStatement* statement_new = 
adbc_from_xptr<AdbcStatement>(statement_xptr_new);
+
+  memcpy(statement_new, statement, sizeof(AdbcStatement));
+  adbc_xptr_move_attrs(statement_xptr, statement_xptr_new);
+  memset(statement, 0, sizeof(AdbcStatement));
+
+  UNPROTECT(1);
+  return statement_xptr_new;
+}
+
+extern "C" SEXP RAdbcStatementValid(SEXP statement_xptr) {
+  AdbcStatement* statement = adbc_from_xptr<AdbcStatement>(statement_xptr);
+  return Rf_ScalarLogical(statement != nullptr && statement->private_data != 
nullptr);
+}
+
 extern "C" SEXP RAdbcStatementSetOption(SEXP statement_xptr, SEXP key_sexp,
                                         SEXP value_sexp, SEXP error_xptr) {
   auto statement = adbc_from_xptr<AdbcStatement>(statement_xptr);
@@ -359,7 +413,6 @@ extern "C" SEXP RAdbcStatementRelease(SEXP statement_xptr, 
SEXP error_xptr) {
   auto statement = adbc_from_xptr<AdbcStatement>(statement_xptr);
   auto error = adbc_from_xptr<AdbcError>(error_xptr);
   int status = AdbcStatementRelease(statement, error);
-  R_SetExternalPtrProtected(statement_xptr, R_NilValue);
   return adbc_wrap_status(status);
 }
 
diff --git a/r/adbcdrivermanager/src/radbc.h b/r/adbcdrivermanager/src/radbc.h
index 76cc7df..73b37a3 100644
--- a/r/adbcdrivermanager/src/radbc.h
+++ b/r/adbcdrivermanager/src/radbc.h
@@ -111,6 +111,26 @@ static inline void adbc_xptr_default_finalize(SEXP xptr) {
   }
 }
 
+static inline void adbc_xptr_move_attrs(SEXP xptr_old, SEXP xptr_new) {
+  SEXP cls_old = PROTECT(Rf_getAttrib(xptr_old, R_ClassSymbol));
+  SEXP tag_old = PROTECT(R_ExternalPtrTag(xptr_old));
+  SEXP prot_old = PROTECT(R_ExternalPtrProtected(xptr_old));
+
+  SEXP tag_new = PROTECT(R_ExternalPtrTag(xptr_new));
+  SEXP prot_new = PROTECT(R_ExternalPtrProtected(xptr_new));
+
+  Rf_setAttrib(xptr_new, R_ClassSymbol, cls_old);
+  R_SetExternalPtrTag(xptr_new, tag_old);
+  R_SetExternalPtrProtected(xptr_new, prot_old);
+
+  // Don't change the class of the original object...not necessary for
+  // lifecycle management and potentially very confusing
+  R_SetExternalPtrTag(xptr_old, tag_new);
+  R_SetExternalPtrProtected(xptr_old, prot_new);
+
+  UNPROTECT(5);
+}
+
 static inline const char* adbc_as_const_char(SEXP sexp) {
   if (TYPEOF(sexp) != STRSXP || Rf_length(sexp) != 1) {
     Rf_error("Expected character(1) for conversion to const char*");
diff --git a/r/adbcdrivermanager/tests/testthat/_snaps/driver_log.md 
b/r/adbcdrivermanager/tests/testthat/_snaps/driver_log.md
index 4658a3f..9ddaa7b 100644
--- a/r/adbcdrivermanager/tests/testthat/_snaps/driver_log.md
+++ b/r/adbcdrivermanager/tests/testthat/_snaps/driver_log.md
@@ -3,8 +3,6 @@
     Code
       db <- adbc_database_init(adbc_driver_log(), key = "value")
     Output
-      LogDriverInitFunc()
-      LogDriverInitFunc()
       LogDatabaseNew()
       LogDatabaseSetOption()
       LogDatabaseInit()
@@ -12,8 +10,8 @@
       con <- adbc_connection_init(db, key = "value")
     Output
       LogConnectionNew()
-      LogConnectionInit()
       LogConnectionSetOption()
+      LogConnectionInit()
     Code
       stmt <- adbc_statement_init(con, key = "value")
     Output
diff --git a/r/adbcdrivermanager/tests/testthat/_snaps/helpers.md 
b/r/adbcdrivermanager/tests/testthat/_snaps/helpers.md
new file mode 100644
index 0000000..aef27d0
--- /dev/null
+++ b/r/adbcdrivermanager/tests/testthat/_snaps/helpers.md
@@ -0,0 +1,27 @@
+# joiners work for databases, connections, and statements
+
+    Code
+      stmt <- local({
+        db <- local_adbc(adbc_database_init(adbc_driver_log()))
+        con <- local_adbc(adbc_connection_init(db))
+        adbc_connection_join(con, db)
+        expect_false(adbc_xptr_is_valid(db))
+        stmt <- local_adbc(adbc_statement_init(con))
+        adbc_statement_join(stmt, con)
+        expect_false(adbc_xptr_is_valid(con))
+        adbc_xptr_move(stmt)
+      })
+    Output
+      LogDatabaseNew()
+      LogDatabaseInit()
+      LogConnectionNew()
+      LogConnectionInit()
+      LogStatementNew()
+    Code
+      adbc_statement_release(stmt)
+    Output
+      LogStatementRelease()
+      LogConnectionRelease()
+      LogDatabaseRelease()
+      LogDriverRelease()
+
diff --git a/r/adbcdrivermanager/tests/testthat/test-helpers.R 
b/r/adbcdrivermanager/tests/testthat/test-helpers.R
new file mode 100644
index 0000000..c1fb679
--- /dev/null
+++ b/r/adbcdrivermanager/tests/testthat/test-helpers.R
@@ -0,0 +1,131 @@
+# 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("with_adbc() and local_adbc() release databases", {
+  db <- adbc_database_init(adbc_driver_void())
+  expect_identical(with_adbc(db, "value"), "value")
+  expect_error(
+    adbc_database_release(db),
+    "ADBC_STATUS_INVALID_STATE"
+  )
+
+  db <- adbc_database_init(adbc_driver_void())
+  local({
+    expect_identical(local_adbc(db), db)
+  })
+  expect_error(
+    adbc_database_release(db),
+    "ADBC_STATUS_INVALID_STATE"
+  )
+})
+
+test_that("with_adbc() and local_adbc() release connections", {
+  db <- adbc_database_init(adbc_driver_void())
+  con <- adbc_connection_init(db)
+  expect_identical(with_adbc(con, "value"), "value")
+  expect_error(
+    adbc_connection_release(con),
+    "ADBC_STATUS_INVALID_STATE"
+  )
+
+  con <- adbc_connection_init(db)
+  local({
+    expect_identical(local_adbc(con), con)
+  })
+  expect_error(
+    adbc_connection_release(con),
+    "ADBC_STATUS_INVALID_STATE"
+  )
+})
+
+test_that("with_adbc() and local_adbc() release statements", {
+  db <- adbc_database_init(adbc_driver_void())
+  con <- adbc_connection_init(db)
+  stmt <- adbc_statement_init(con)
+  expect_identical(with_adbc(stmt, "value"), "value")
+  expect_error(
+    adbc_statement_release(stmt),
+    "ADBC_STATUS_INVALID_STATE"
+  )
+
+  stmt <- adbc_statement_init(con)
+  local({
+    expect_identical(local_adbc(stmt), stmt)
+  })
+  expect_error(
+    adbc_statement_release(stmt),
+    "ADBC_STATUS_INVALID_STATE"
+  )
+})
+
+test_that("with_adbc() and local_adbc() release streams", {
+  stream <- nanoarrow::basic_array_stream(list(1:5))
+  expect_identical(with_adbc(stream, "value"), "value")
+  expect_false(nanoarrow::nanoarrow_pointer_is_valid(stream))
+
+  stream <- nanoarrow::basic_array_stream(list(1:5))
+  local({
+    expect_identical(local_adbc(stream), stream)
+  })
+  expect_false(nanoarrow::nanoarrow_pointer_is_valid(stream))
+})
+
+test_that("joiners work for databases, connections, and statements", {
+  expect_snapshot({
+    stmt <- local({
+      db <- local_adbc(adbc_database_init(adbc_driver_log()))
+
+      con <- local_adbc(adbc_connection_init(db))
+      adbc_connection_join(con, db)
+      expect_false(adbc_xptr_is_valid(db))
+
+      stmt <- local_adbc(adbc_statement_init(con))
+      adbc_statement_join(stmt, con)
+      expect_false(adbc_xptr_is_valid(con))
+
+      adbc_xptr_move(stmt)
+    })
+
+    adbc_statement_release(stmt)
+  })
+})
+
+test_that("joiners work with streams", {
+  skip_if_not(packageVersion("nanoarrow") >= "0.1.0.9000")
+
+  stream <- local({
+    db <- local_adbc(adbc_database_init(adbc_driver_monkey()))
+
+    con <- local_adbc(adbc_connection_init(db))
+    adbc_connection_join(con, db)
+    expect_false(adbc_xptr_is_valid(db))
+
+    stmt <- local_adbc(adbc_statement_init(con, data.frame(x = 1:5)))
+    adbc_statement_join(stmt, con)
+    expect_false(adbc_xptr_is_valid(con))
+
+    stream <- local_adbc(nanoarrow::nanoarrow_allocate_array_stream())
+    adbc_statement_execute_query(stmt, stream)
+    adbc_stream_join(stream, stmt)
+    expect_false(adbc_xptr_is_valid(stmt))
+
+    adbc_xptr_move(stream)
+  })
+
+  expect_identical(as.data.frame(stream), data.frame(x = 1:5))
+  expect_silent(stream$release())
+})
diff --git a/r/adbcdrivermanager/tests/testthat/test-utils.R 
b/r/adbcdrivermanager/tests/testthat/test-utils.R
index 956cc13..938ce95 100644
--- a/r/adbcdrivermanager/tests/testthat/test-utils.R
+++ b/r/adbcdrivermanager/tests/testthat/test-utils.R
@@ -53,3 +53,36 @@ test_that("external pointer embedded environment works", {
   db[["key"]] <- "value2"
   expect_identical(db[["key"]], "value2")
 })
+
+test_that("pointer mover leaves behind an invalid external pointer", {
+  db <- adbc_database_init(adbc_driver_void())
+  con <- adbc_connection_init(db)
+  stmt <- adbc_statement_init(con)
+
+  expect_true(adbc_xptr_is_valid(db))
+  expect_true(adbc_xptr_is_valid(adbc_xptr_move(db)))
+  expect_false(adbc_xptr_is_valid(db))
+
+  expect_true(adbc_xptr_is_valid(con))
+  expect_true(adbc_xptr_is_valid(adbc_xptr_move(con)))
+  expect_false(adbc_xptr_is_valid(con))
+
+  expect_true(adbc_xptr_is_valid(stmt))
+  expect_true(adbc_xptr_is_valid(adbc_xptr_move(stmt)))
+  expect_false(adbc_xptr_is_valid(stmt))
+
+  stream <- nanoarrow::basic_array_stream(list(1:5))
+  expect_true(adbc_xptr_is_valid(stream))
+  expect_true(adbc_xptr_is_valid(adbc_xptr_move(stream)))
+  expect_false(adbc_xptr_is_valid(stream))
+
+  expect_error(
+    adbc_xptr_is_valid(NULL),
+    "must inherit from one of"
+  )
+
+  expect_error(
+    adbc_xptr_move(NULL),
+    "must inherit from one of"
+  )
+})
diff --git a/r/adbcdrivermanager/tools/make-callentries.R 
b/r/adbcdrivermanager/tools/make-callentries.R
index 88a4528..cf4d8bb 100644
--- a/r/adbcdrivermanager/tools/make-callentries.R
+++ b/r/adbcdrivermanager/tools/make-callentries.R
@@ -38,7 +38,7 @@ defs <- tibble(
     str_remove("SEXP RAdbc[^\\(]+\\(") %>%
     str_remove("\\)$") %>%
     str_split("\\s*,\\s*") %>%
-    map(~{if(identical(.x, "")) character(0) else .x}),
+    map(~{if(identical(.x, "") || identical(.x, "void")) character(0) else 
.x}),
   n_args = map(args, length)
 )
 
@@ -74,3 +74,5 @@ stopifnot(str_detect(init, pattern))
 init %>%
   str_replace(pattern, header) %>%
   write_file("src/init.c")
+
+system("clang-format -i src/init.c")


Reply via email to