You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@arrow.apache.org by pa...@apache.org on 2023/04/08 00:48:09 UTC

[arrow-adbc] branch main updated: feat(r/adbdpostgresql): Package postgresql driver for R (#511)

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 c5f087f  feat(r/adbdpostgresql): Package postgresql driver for R (#511)
c5f087f is described below

commit c5f087fd2f291c5af8ef1160ae6bd506164d9656
Author: Dewey Dunnington <de...@voltrondata.com>
AuthorDate: Fri Apr 7 20:48:04 2023 -0400

    feat(r/adbdpostgresql): Package postgresql driver for R (#511)
    
    This PR packages c/driver/postgresql for R on MacOS and Linux. I'd like
    to defer the Windows issue to another PR because it's slightly more
    complicated (unfortunately I think it will involve shipping a vendored
    copy of libpq).
    
    I added some minor changes to the driver so that the README
    worked...bulk ingest was only supported for int64 and string, but
    integer and double were needed for the existing README to work. The
    example in the README segfaults after 700 ish rows anyway (I limit the
    example to 100 rows) so I could back those changes out. I also get
    intermittent segfaults in an interactive session that I wasn't able to
    diagnose but I don't think those are a result of the R package setup
    (they are probably related to deleting or inspecting connections in an
    unexpected order).
    
    ---------
    
    Co-authored-by: David Li <li...@gmail.com>
---
 .github/workflows/native-unix.yml                  |  24 +-
 c/driver/postgresql/statement.cc                   |  19 +
 c/driver/postgresql/type.cc                        |   8 +-
 c/driver/postgresql/util.h                         |  25 ++
 dev/release/rat_exclude_files.txt                  |   3 +
 docker-compose.yml                                 |  11 +
 r/adbcdrivermanager/R/adbc.R                       |   7 +-
 r/adbcdrivermanager/R/driver_monkey.R              |   5 +-
 r/adbcdrivermanager/README.Rmd                     |   3 +-
 r/adbcdrivermanager/README.md                      |   8 +-
 r/adbcdrivermanager/man/adbc_driver_monkey.Rd      |   5 +-
 .../man/adbc_statement_set_sql_query.Rd            |   2 +-
 r/adbcdrivermanager/src/driver_monkey.c            |   6 +
 r/adbcdrivermanager/src/radbc.cc                   |  11 +-
 .../tests/testthat/test-driver_monkey.R            |  10 +-
 r/adbcpostgresql/.Rbuildignore                     |   9 +
 .../.gitignore}                                    |  13 +-
 r/{adbcsqlite => adbcpostgresql}/DESCRIPTION       |   9 +-
 r/adbcpostgresql/LICENSE.md                        | 194 ++++++++
 r/adbcpostgresql/NAMESPACE                         |  10 +
 r/adbcpostgresql/R/adbcpostgresql-package.R        |  92 ++++
 r/{adbcsqlite => adbcpostgresql}/README.Rmd        |  29 +-
 r/{adbcsqlite => adbcpostgresql}/README.md         |  33 +-
 r/adbcpostgresql/adbcpostgresql.Rproj              |  22 +
 r/adbcpostgresql/bootstrap.R                       |  60 +++
 .../cleanup}                                       |  13 +-
 .../cleanup.win}                                   |  13 +-
 r/adbcpostgresql/configure                         |  80 ++++
 .../configure.win}                                 |  14 +-
 r/adbcpostgresql/man/adbcpostgresql-package.Rd     |  26 ++
 r/adbcpostgresql/man/adbcpostgresql.Rd             |  48 ++
 .../src/.gitignore}                                |  26 +-
 .../src/Makevars.in}                               |  20 +-
 r/adbcpostgresql/src/database.cc                   | 124 +++++
 r/adbcpostgresql/src/init.c                        |  43 ++
 .../src/nanoarrow/.gitignore}                      |  14 +-
 r/adbcpostgresql/src/postgresql.cc                 | 499 +++++++++++++++++++++
 .../tests/testthat.R}                              |  22 +-
 .../tests/testthat/test-adbcpostgres-package.R}    |  48 +-
 r/adbcpostgresql/tools/test.c                      |  21 +
 r/adbcsqlite/DESCRIPTION                           |   1 +
 r/adbcsqlite/README.Rmd                            |   4 +-
 r/adbcsqlite/README.md                             |   7 +-
 .../tests/testthat/test-adbcsqlite-package.R       |   9 +-
 44 files changed, 1487 insertions(+), 163 deletions(-)

diff --git a/.github/workflows/native-unix.yml b/.github/workflows/native-unix.yml
index 52b2411..f1ae9f3 100644
--- a/.github/workflows/native-unix.yml
+++ b/.github/workflows/native-unix.yml
@@ -457,6 +457,8 @@ jobs:
           - {os: macOS-latest,   r: 'release', pkg: 'adbcsqlite'}
           - {os: windows-latest,   r: 'release', pkg: 'adbcsqlite'}
           - {os: ubuntu-latest,   r: 'release', pkg: 'adbcsqlite'}
+          - {os: macOS-latest,   r: 'release', pkg: 'adbcpostgresql'}
+          - {os: ubuntu-latest,   r: 'release', pkg: 'adbcpostgresql'}
 
     env:
       GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
@@ -475,6 +477,12 @@ jobs:
           http-user-agent: ${{ matrix.config.http-user-agent }}
           use-public-rspm: true
 
+      - name: Set PKG_CONFIG_PATH on MacOS
+        if: matrix.config.pkg == 'adbcpostgresql' && runner.os == 'macOS'
+        run: |
+          PKG_CONFIG_PATH="${PKG_CONFIG_PATH}:$(brew --prefix libpq)/lib/pkgconfig:$(brew --prefix openssl)/lib/pkgconfig"
+          echo "PKG_CONFIG_PATH=${PKG_CONFIG_PATH}" >> $GITHUB_ENV
+
       - name: Prepare sources (driver manager)
         if: matrix.config.pkg == 'adbcdrivermanager'
         run: |
@@ -493,11 +501,25 @@ jobs:
       - uses: r-lib/actions/setup-r-dependencies@v2
         with:
           pak-version: devel
-          extra-packages: any::rcmdcheck
+          extra-packages: any::rcmdcheck, local::../adbcdrivermanager
           needs: check
           working-directory: r/${{ matrix.config.pkg }}
 
+      - name: Start postgres test database
+        if: matrix.config.pkg == 'adbcpostgresql' && runner.os == 'Linux'
+        run: |
+          cd r/adbcpostgresql
+          docker compose up --detach postgres_test
+          ADBC_POSTGRESQL_TEST_URI="postgresql://localhost:5432/postgres?user=postgres&password=password"
+          echo "ADBC_POSTGRESQL_TEST_URI=${ADBC_POSTGRESQL_TEST_URI}" >> $GITHUB_ENV
+
       - uses: r-lib/actions/check-r-package@v2
         with:
           upload-snapshots: true
           working-directory: r/${{ matrix.config.pkg }}
+
+      - name: Stop postgres test database
+        if: matrix.config.pkg == 'adbcpostgresql' && runner.os == 'Linux'
+        run: |
+          cd r/adbcpostgresql
+          docker compose down
diff --git a/c/driver/postgresql/statement.cc b/c/driver/postgresql/statement.cc
index 968badd..8bd1782 100644
--- a/c/driver/postgresql/statement.cc
+++ b/c/driver/postgresql/statement.cc
@@ -228,6 +228,10 @@ struct BindStream {
           pg_type = PgType::kInt8;
           param_lengths[i] = 8;
           break;
+        case ArrowType::NANOARROW_TYPE_DOUBLE:
+          pg_type = PgType::kFloat8;
+          param_lengths[i] = 8;
+          break;
         case ArrowType::NANOARROW_TYPE_STRING:
           pg_type = PgType::kText;
           param_lengths[i] = 0;
@@ -304,12 +308,24 @@ struct BindStream {
             param_values[col] = param_values_buffer.data() + param_values_offsets[col];
           }
           switch (bind_schema_fields[col].type) {
+            case ArrowType::NANOARROW_TYPE_INT32: {
+              const int64_t value = ToNetworkInt32(
+                  array_view->children[col]->buffer_views[1].data.as_int32[row]);
+              std::memcpy(param_values[col], &value, sizeof(int32_t));
+              break;
+            }
             case ArrowType::NANOARROW_TYPE_INT64: {
               const int64_t value = ToNetworkInt64(
                   array_view->children[col]->buffer_views[1].data.as_int64[row]);
               std::memcpy(param_values[col], &value, sizeof(int64_t));
               break;
             }
+            case ArrowType::NANOARROW_TYPE_DOUBLE: {
+              const uint64_t value = ToNetworkFloat8(
+                  array_view->children[col]->buffer_views[1].data.as_double[row]);
+              std::memcpy(param_values[col], &value, sizeof(uint64_t));
+              break;
+            }
             case ArrowType::NANOARROW_TYPE_STRING: {
               const ArrowBufferView view =
                   ArrowArrayViewGetBytesUnsafe(array_view->children[col], row);
@@ -722,6 +738,9 @@ AdbcStatusCode PostgresStatement::CreateBulkTable(
       case ArrowType::NANOARROW_TYPE_INT64:
         create += " BIGINT";
         break;
+      case ArrowType::NANOARROW_TYPE_DOUBLE:
+        create += " DOUBLE PRECISION";
+        break;
       case ArrowType::NANOARROW_TYPE_STRING:
         create += " TEXT";
         break;
diff --git a/c/driver/postgresql/type.cc b/c/driver/postgresql/type.cc
index 472c3a5..f2f894a 100644
--- a/c/driver/postgresql/type.cc
+++ b/c/driver/postgresql/type.cc
@@ -27,9 +27,15 @@ void TypeMapping::Insert(uint32_t oid, const char* typname, const char* typrecei
   }
 
   // Record 'canonical' types
-  if (std::strcmp(typname, "int8") == 0) {
+  if (std::strcmp(typname, "int4") == 0) {
+    // DCHECK_EQ(type, PgType::kInt4);
+    canonical_types[PgType::kInt4] = oid;
+  } else if (std::strcmp(typname, "int8") == 0) {
     // DCHECK_EQ(type, PgType::kInt8);
     canonical_types[PgType::kInt8] = oid;
+  } else if (std::strcmp(typname, "float8") == 0) {
+    // DCHECK_EQ(type, PgType::kFloat8);
+    canonical_types[PgType::kFloat8] = oid;
   } else if (std::strcmp(typname, "text") == 0) {
     canonical_types[PgType::kText] = oid;
   }
diff --git a/c/driver/postgresql/util.h b/c/driver/postgresql/util.h
index 6da9e19..264dda7 100644
--- a/c/driver/postgresql/util.h
+++ b/c/driver/postgresql/util.h
@@ -41,12 +41,18 @@ namespace adbcpq {
 #define MAKE_NAME(x, y) CONCAT(x, y)
 
 #if defined(_WIN32)
+static inline uint32_t SwapNetworkToHost(uint32_t x) { return ntohl(x); }
+static inline uint32_t SwapHostToNetwork(uint32_t x) { return htonl(x); }
 static inline uint64_t SwapNetworkToHost(uint64_t x) { return ntohll(x); }
 static inline uint64_t SwapHostToNetwork(uint64_t x) { return htonll(x); }
 #elif defined(__APPLE__)
+static inline uint32_t SwapNetworkToHost(uint32_t x) { return OSSwapBigToHostInt32(x); }
+static inline uint32_t SwapHostToNetwork(uint32_t x) { return OSSwapHostToBigInt32(x); }
 static inline uint64_t SwapNetworkToHost(uint64_t x) { return OSSwapBigToHostInt64(x); }
 static inline uint64_t SwapHostToNetwork(uint64_t x) { return OSSwapHostToBigInt64(x); }
 #else
+static inline uint32_t SwapNetworkToHost(uint32_t x) { return be32toh(x); }
+static inline uint32_t SwapHostToNetwork(uint32_t x) { return htobe32(x); }
 static inline uint64_t SwapNetworkToHost(uint64_t x) { return be64toh(x); }
 static inline uint64_t SwapHostToNetwork(uint64_t x) { return htobe64(x); }
 #endif
@@ -156,8 +162,27 @@ static inline int64_t LoadNetworkInt64(const char* buf) {
   return static_cast<int64_t>(LoadNetworkUInt64(buf));
 }
 
+static inline double LoadNetworkFloat8(const char* buf) {
+  uint64_t vint;
+  memcpy(&vint, buf, sizeof(uint64_t));
+  vint = SwapHostToNetwork(vint);
+  double out;
+  memcpy(&out, &vint, sizeof(double));
+  return out;
+}
+
+static inline uint32_t ToNetworkInt32(int32_t v) {
+  return SwapHostToNetwork(static_cast<uint32_t>(v));
+}
+
 static inline uint64_t ToNetworkInt64(int64_t v) {
   return SwapHostToNetwork(static_cast<uint64_t>(v));
 }
 
+static inline uint64_t ToNetworkFloat8(double v) {
+  uint64_t vint;
+  memcpy(&vint, &v, sizeof(uint64_t));
+  return SwapHostToNetwork(vint);
+}
+
 }  // namespace adbcpq
diff --git a/dev/release/rat_exclude_files.txt b/dev/release/rat_exclude_files.txt
index a291e5c..5c7be27 100644
--- a/dev/release/rat_exclude_files.txt
+++ b/dev/release/rat_exclude_files.txt
@@ -18,6 +18,9 @@ r/adbcdrivermanager/.Rbuildignore
 r/adbcsqlite/DESCRIPTION
 r/adbcsqlite/NAMESPACE
 r/adbcsqlite/.Rbuildignore
+r/adbcpostgresql/DESCRIPTION
+r/adbcpostgresql/NAMESPACE
+r/adbcpostgresql/.Rbuildignore
 c/vendor/sqlite3/sqlite3.c
 c/vendor/sqlite3/sqlite3.h
 *.Rproj
diff --git a/docker-compose.yml b/docker-compose.yml
index acd47d1..55cc5c5 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -117,3 +117,14 @@ services:
     volumes:
       - .:/adbc:delegated
     command: /adbc/ci/scripts/python_wheel_unix_test.sh /adbc
+
+  ###################### Test database environments ############################
+
+  postgres_test:
+      container_name: adbc_postgres_test
+      image: postgres:latest
+      environment:
+        POSTGRES_USER: postgres
+        POSTGRES_PASSWORD: password
+      ports:
+        - "5432:5432"
diff --git a/r/adbcdrivermanager/R/adbc.R b/r/adbcdrivermanager/R/adbc.R
index 7f9798c..f9a05c8 100644
--- a/r/adbcdrivermanager/R/adbc.R
+++ b/r/adbcdrivermanager/R/adbc.R
@@ -431,10 +431,9 @@ adbc_statement_bind_stream <- function(statement, stream, schema = NULL) {
 
 #' @rdname adbc_statement_set_sql_query
 #' @export
-adbc_statement_execute_query <- function(statement) {
+adbc_statement_execute_query <- function(statement, stream = NULL) {
   error <- adbc_allocate_error()
-  out_stream <- nanoarrow::nanoarrow_allocate_array_stream()
-  result <- .Call(RAdbcStatementExecuteQuery, statement, out_stream, error)
+  result <- .Call(RAdbcStatementExecuteQuery, statement, stream, error)
   stop_for_error(result$status, error)
-  out_stream
+  result$rows_affected
 }
diff --git a/r/adbcdrivermanager/R/driver_monkey.R b/r/adbcdrivermanager/R/driver_monkey.R
index c944c72..460263d 100644
--- a/r/adbcdrivermanager/R/driver_monkey.R
+++ b/r/adbcdrivermanager/R/driver_monkey.R
@@ -26,8 +26,9 @@
 #' db <- adbc_database_init(adbc_driver_monkey())
 #' con <- adbc_connection_init(db)
 #' stmt <- adbc_statement_init(con, mtcars)
-#' result <- adbc_statement_execute_query(stmt)
-#' as.data.frame(result$get_next())
+#' stream <- nanoarrow::nanoarrow_allocate_array_stream()
+#' adbc_statement_execute_query(stmt, stream)
+#' as.data.frame(stream$get_next())
 #'
 adbc_driver_monkey <- function() {
   if (is.null(internal_driver_env$monkey)) {
diff --git a/r/adbcdrivermanager/README.Rmd b/r/adbcdrivermanager/README.Rmd
index d117a9e..3907fce 100644
--- a/r/adbcdrivermanager/README.Rmd
+++ b/r/adbcdrivermanager/README.Rmd
@@ -76,7 +76,8 @@ adbc_statement_set_sql_query(stmt, "SELECT * FROM flights")
 # which can be materialized using as.data.frame(), as_tibble(),
 # or converted to an arrow::RecordBatchReader using
 # arrow::as_record_batch_reader()
-(stream <- adbc_statement_execute_query(stmt))
+stream <- nanoarrow::nanoarrow_allocate_array_stream()
+adbc_statement_execute_query(stmt, stream)
 
 # Materialize the whole query as a tibble
 tibble::as_tibble(stream)
diff --git a/r/adbcdrivermanager/README.md b/r/adbcdrivermanager/README.md
index 92661bd..18b23a3 100644
--- a/r/adbcdrivermanager/README.md
+++ b/r/adbcdrivermanager/README.md
@@ -65,11 +65,9 @@ adbc_statement_set_sql_query(stmt, "SELECT * FROM flights")
 # which can be materialized using as.data.frame(), as_tibble(),
 # or converted to an arrow::RecordBatchReader using
 # arrow::as_record_batch_reader()
-(stream <- adbc_statement_execute_query(stmt))
-#> <nanoarrow_array_stream struct<year: int32, month: int32, day: int32, dep_time: int32, sched_dep_time: int32, dep_delay: double, arr_time: int32, sched_arr_time: int32, arr_delay: double, carrier: string, flight: int32, tailnum: string, origin: string, dest: string, air_time: double, distance: double, hour: double, minute: double, time_hour: timestamp('us', 'America/New_York')>>
-#>  $ get_schema:function ()
-#>  $ get_next  :function (schema = x$get_schema(), validate = TRUE)
-#>  $ release   :function ()
+stream <- nanoarrow::nanoarrow_allocate_array_stream()
+adbc_statement_execute_query(stmt, stream)
+#> [1] -1
 
 # Materialize the whole query as a tibble
 tibble::as_tibble(stream)
diff --git a/r/adbcdrivermanager/man/adbc_driver_monkey.Rd b/r/adbcdrivermanager/man/adbc_driver_monkey.Rd
index d4eaed0..6d0fd27 100644
--- a/r/adbcdrivermanager/man/adbc_driver_monkey.Rd
+++ b/r/adbcdrivermanager/man/adbc_driver_monkey.Rd
@@ -16,7 +16,8 @@ A driver whose query results are set in advance.
 db <- adbc_database_init(adbc_driver_monkey())
 con <- adbc_connection_init(db)
 stmt <- adbc_statement_init(con, mtcars)
-result <- adbc_statement_execute_query(stmt)
-as.data.frame(result$get_next())
+stream <- nanoarrow::nanoarrow_allocate_array_stream()
+adbc_statement_execute_query(stmt, stream)
+as.data.frame(stream$get_next())
 
 }
diff --git a/r/adbcdrivermanager/man/adbc_statement_set_sql_query.Rd b/r/adbcdrivermanager/man/adbc_statement_set_sql_query.Rd
index 76fcffc..5102d81 100644
--- a/r/adbcdrivermanager/man/adbc_statement_set_sql_query.Rd
+++ b/r/adbcdrivermanager/man/adbc_statement_set_sql_query.Rd
@@ -22,7 +22,7 @@ adbc_statement_bind(statement, values, schema = NULL)
 
 adbc_statement_bind_stream(statement, stream, schema = NULL)
 
-adbc_statement_execute_query(statement)
+adbc_statement_execute_query(statement, stream = NULL)
 }
 \arguments{
 \item{statement}{An \link[=adbc_statement_init]{adbc_statement}}
diff --git a/r/adbcdrivermanager/src/driver_monkey.c b/r/adbcdrivermanager/src/driver_monkey.c
index 22ecb31..6dc8738 100644
--- a/r/adbcdrivermanager/src/driver_monkey.c
+++ b/r/adbcdrivermanager/src/driver_monkey.c
@@ -215,8 +215,14 @@ static AdbcStatusCode MonkeyStatementExecuteQuery(struct AdbcStatement* statemen
                                                   struct ArrowArrayStream* out,
                                                   int64_t* rows_affected,
                                                   struct AdbcError* error) {
+  if (out == NULL) {
+    *rows_affected = 0;
+    return ADBC_STATUS_OK;
+  }
+
   struct MonkeyStatementPrivate* statement_private =
       (struct MonkeyStatementPrivate*)statement->private_data;
+
   memcpy(out, &statement_private->stream, sizeof(struct ArrowArrayStream));
   statement_private->stream.release = NULL;
   *rows_affected = -1;
diff --git a/r/adbcdrivermanager/src/radbc.cc b/r/adbcdrivermanager/src/radbc.cc
index 5872c65..70c3f60 100644
--- a/r/adbcdrivermanager/src/radbc.cc
+++ b/r/adbcdrivermanager/src/radbc.cc
@@ -408,9 +408,16 @@ extern "C" SEXP RAdbcStatementBindStream(SEXP statement_xptr, SEXP stream_xptr,
 extern "C" SEXP RAdbcStatementExecuteQuery(SEXP statement_xptr, SEXP out_stream_xptr,
                                            SEXP error_xptr) {
   auto statement = adbc_from_xptr<AdbcStatement>(statement_xptr);
-  auto out_stream = adbc_from_xptr<ArrowArrayStream>(out_stream_xptr);
+
+  ArrowArrayStream* out_stream;
+  if (out_stream_xptr == R_NilValue) {
+    out_stream = nullptr;
+  } else {
+    out_stream = adbc_from_xptr<ArrowArrayStream>(out_stream_xptr);
+  }
+
   auto error = adbc_from_xptr<AdbcError>(error_xptr);
-  int64_t rows_affected = 0;
+  int64_t rows_affected = -1;
   int status = AdbcStatementExecuteQuery(statement, out_stream, &rows_affected, error);
 
   const char* names[] = {"status", "rows_affected", ""};
diff --git a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R b/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
index 5c6843d..ba587d1 100644
--- a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
+++ b/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
@@ -24,6 +24,12 @@ test_that("the monkey driver sees, and the monkey driver does", {
   input <- data.frame(x = 1:10)
   stmt <- adbc_statement_init(con, input)
   expect_s3_class(stmt, "adbc_statement_monkey")
-  result <- adbc_statement_execute_query(stmt)
-  expect_identical(as.data.frame(result$get_next()), input)
+  stream <- nanoarrow::nanoarrow_allocate_array_stream()
+  expect_identical(adbc_statement_execute_query(stmt, stream), -1)
+  expect_identical(as.data.frame(stream$get_next()), input)
+  adbc_statement_release(stmt)
+
+  stmt <- adbc_statement_init(con, input)
+  expect_identical(adbc_statement_execute_query(stmt, NULL), 0)
+  adbc_statement_release(stmt)
 })
diff --git a/r/adbcpostgresql/.Rbuildignore b/r/adbcpostgresql/.Rbuildignore
new file mode 100644
index 0000000..dfb192a
--- /dev/null
+++ b/r/adbcpostgresql/.Rbuildignore
@@ -0,0 +1,9 @@
+^adbcpostgresql\.Rproj$
+^\.Rproj\.user$
+^LICENSE\.md$
+^bootstrap\.R$
+^README\.Rmd$
+^src/Makevars$
+^src/sqlite3\.c$
+^src/sqlite3\.h$
+^docker-compose\.yml$
diff --git a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R b/r/adbcpostgresql/.gitignore
similarity index 62%
copy from r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
copy to r/adbcpostgresql/.gitignore
index 5c6843d..439ee9b 100644
--- a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
+++ b/r/adbcpostgresql/.gitignore
@@ -15,15 +15,4 @@
 # specific language governing permissions and limitations
 # under the License.
 
-test_that("the monkey driver sees, and the monkey driver does", {
-  db <- adbc_database_init(adbc_driver_monkey())
-  expect_s3_class(db, "adbc_database_monkey")
-  con <- adbc_connection_init(db)
-  expect_s3_class(con, "adbc_connection_monkey")
-
-  input <- data.frame(x = 1:10)
-  stmt <- adbc_statement_init(con, input)
-  expect_s3_class(stmt, "adbc_statement_monkey")
-  result <- adbc_statement_execute_query(stmt)
-  expect_identical(as.data.frame(result$get_next()), input)
-})
+.Rproj.user
diff --git a/r/adbcsqlite/DESCRIPTION b/r/adbcpostgresql/DESCRIPTION
similarity index 81%
copy from r/adbcsqlite/DESCRIPTION
copy to r/adbcpostgresql/DESCRIPTION
index ca4400d..2a793f3 100644
--- a/r/adbcsqlite/DESCRIPTION
+++ b/r/adbcpostgresql/DESCRIPTION
@@ -1,5 +1,5 @@
-Package: adbcsqlite
-Title: Arrow Database Connectivity (ADBC) SQLite Driver
+Package: adbcpostgresql
+Title: Arrow Database Connectivity (ADBC) PostgreSQL Driver
 Version: 0.0.0.9000
 Authors@R: c(
       person("Dewey", "Dunnington", , "dewey@dunnington.ca", role = c("aut", "cre"),
@@ -7,13 +7,14 @@ Authors@R: c(
       person("Apache Arrow", email = "dev@arrow.apache.org", role = c("aut", "cph"))
     )
 Description: Provides a developer-facing interface to the Arrow Database
-  Connectivity (ADBC) SQLite driver.
+  Connectivity (ADBC) PostgreSQL driver.
 License: Apache License (>= 2)
 Encoding: UTF-8
 Roxygen: list(markdown = TRUE)
 RoxygenNote: 7.2.3
-SystemRequirements: SQLite3
+SystemRequirements: libpq
 Suggests:
+    nanoarrow,
     testthat (>= 3.0.0)
 Config/testthat/edition: 3
 Config/build/bootstrap: TRUE
diff --git a/r/adbcpostgresql/LICENSE.md b/r/adbcpostgresql/LICENSE.md
new file mode 100644
index 0000000..b62a9b5
--- /dev/null
+++ b/r/adbcpostgresql/LICENSE.md
@@ -0,0 +1,194 @@
+Apache License
+==============
+
+_Version 2.0, January 2004_
+_&lt;<http://www.apache.org/licenses/>&gt;_
+
+### Terms and Conditions for use, reproduction, and distribution
+
+#### 1. Definitions
+
+“License” shall mean the terms and conditions for use, reproduction, and
+distribution as defined by Sections 1 through 9 of this document.
+
+“Licensor” shall mean the copyright owner or entity authorized by the copyright
+owner that is granting the License.
+
+“Legal Entity” shall mean the union of the acting entity and all other entities
+that control, are controlled by, or are under common control with that entity.
+For the purposes of this definition, “control” means **(i)** the power, direct or
+indirect, to cause the direction or management of such entity, whether by
+contract or otherwise, or **(ii)** ownership of fifty percent (50%) or more of the
+outstanding shares, or **(iii)** beneficial ownership of such entity.
+
+“You” (or “Your”) shall mean an individual or Legal Entity exercising
+permissions granted by this License.
+
+“Source” form shall mean the preferred form for making modifications, including
+but not limited to software source code, documentation source, and configuration
+files.
+
+“Object” form shall mean any form resulting from mechanical transformation or
+translation of a Source form, including but not limited to compiled object code,
+generated documentation, and conversions to other media types.
+
+“Work” shall mean the work of authorship, whether in Source or Object form, made
+available under the License, as indicated by a copyright notice that is included
+in or attached to the work (an example is provided in the Appendix below).
+
+“Derivative Works” shall mean any work, whether in Source or Object form, that
+is based on (or derived from) the Work and for which the editorial revisions,
+annotations, elaborations, or other modifications represent, as a whole, an
+original work of authorship. For the purposes of this License, Derivative Works
+shall not include works that remain separable from, or merely link (or bind by
+name) to the interfaces of, the Work and Derivative Works thereof.
+
+“Contribution” shall mean any work of authorship, including the original version
+of the Work and any modifications or additions to that Work or Derivative Works
+thereof, that is intentionally submitted to Licensor for inclusion in the Work
+by the copyright owner or by an individual or Legal Entity authorized to submit
+on behalf of the copyright owner. For the purposes of this definition,
+“submitted” means any form of electronic, verbal, or written communication sent
+to the Licensor or its representatives, including but not limited to
+communication on electronic mailing lists, source code control systems, and
+issue tracking systems that are managed by, or on behalf of, the Licensor for
+the purpose of discussing and improving the Work, but excluding communication
+that is conspicuously marked or otherwise designated in writing by the copyright
+owner as “Not a Contribution.”
+
+“Contributor” shall mean Licensor and any individual or Legal Entity on behalf
+of whom a Contribution has been received by Licensor and subsequently
+incorporated within the Work.
+
+#### 2. Grant of Copyright License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable copyright license to reproduce, prepare Derivative Works of,
+publicly display, publicly perform, sublicense, and distribute the Work and such
+Derivative Works in Source or Object form.
+
+#### 3. Grant of Patent License
+
+Subject to the terms and conditions of this License, each Contributor hereby
+grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
+irrevocable (except as stated in this section) patent license to make, have
+made, use, offer to sell, sell, import, and otherwise transfer the Work, where
+such license applies only to those patent claims licensable by such Contributor
+that are necessarily infringed by their Contribution(s) alone or by combination
+of their Contribution(s) with the Work to which such Contribution(s) was
+submitted. If You institute patent litigation against any entity (including a
+cross-claim or counterclaim in a lawsuit) alleging that the Work or a
+Contribution incorporated within the Work constitutes direct or contributory
+patent infringement, then any patent licenses granted to You under this License
+for that Work shall terminate as of the date such litigation is filed.
+
+#### 4. Redistribution
+
+You may reproduce and distribute copies of the Work or Derivative Works thereof
+in any medium, with or without modifications, and in Source or Object form,
+provided that You meet the following conditions:
+
+* **(a)** You must give any other recipients of the Work or Derivative Works a copy of
+this License; and
+* **(b)** You must cause any modified files to carry prominent notices stating that You
+changed the files; and
+* **(c)** You must retain, in the Source form of any Derivative Works that You distribute,
+all copyright, patent, trademark, and attribution notices from the Source form
+of the Work, excluding those notices that do not pertain to any part of the
+Derivative Works; and
+* **(d)** If the Work includes a “NOTICE” text file as part of its distribution, then any
+Derivative Works that You distribute must include a readable copy of the
+attribution notices contained within such NOTICE file, excluding those notices
+that do not pertain to any part of the Derivative Works, in at least one of the
+following places: within a NOTICE text file distributed as part of the
+Derivative Works; within the Source form or documentation, if provided along
+with the Derivative Works; or, within a display generated by the Derivative
+Works, if and wherever such third-party notices normally appear. The contents of
+the NOTICE file are for informational purposes only and do not modify the
+License. You may add Your own attribution notices within Derivative Works that
+You distribute, alongside or as an addendum to the NOTICE text from the Work,
+provided that such additional attribution notices cannot be construed as
+modifying the License.
+
+You may add Your own copyright statement to Your modifications and may provide
+additional or different license terms and conditions for use, reproduction, or
+distribution of Your modifications, or for any such Derivative Works as a whole,
+provided Your use, reproduction, and distribution of the Work otherwise complies
+with the conditions stated in this License.
+
+#### 5. Submission of Contributions
+
+Unless You explicitly state otherwise, any Contribution intentionally submitted
+for inclusion in the Work by You to the Licensor shall be under the terms and
+conditions of this License, without any additional terms or conditions.
+Notwithstanding the above, nothing herein shall supersede or modify the terms of
+any separate license agreement you may have executed with Licensor regarding
+such Contributions.
+
+#### 6. Trademarks
+
+This License does not grant permission to use the trade names, trademarks,
+service marks, or product names of the Licensor, except as required for
+reasonable and customary use in describing the origin of the Work and
+reproducing the content of the NOTICE file.
+
+#### 7. Disclaimer of Warranty
+
+Unless required by applicable law or agreed to in writing, Licensor provides the
+Work (and each Contributor provides its Contributions) on an “AS IS” BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
+including, without limitation, any warranties or conditions of TITLE,
+NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
+solely responsible for determining the appropriateness of using or
+redistributing the Work and assume any risks associated with Your exercise of
+permissions under this License.
+
+#### 8. Limitation of Liability
+
+In no event and under no legal theory, whether in tort (including negligence),
+contract, or otherwise, unless required by applicable law (such as deliberate
+and grossly negligent acts) or agreed to in writing, shall any Contributor be
+liable to You for damages, including any direct, indirect, special, incidental,
+or consequential damages of any character arising as a result of this License or
+out of the use or inability to use the Work (including but not limited to
+damages for loss of goodwill, work stoppage, computer failure or malfunction, or
+any and all other commercial damages or losses), even if such Contributor has
+been advised of the possibility of such damages.
+
+#### 9. Accepting Warranty or Additional Liability
+
+While redistributing the Work or Derivative Works thereof, You may choose to
+offer, and charge a fee for, acceptance of support, warranty, indemnity, or
+other liability obligations and/or rights consistent with this License. However,
+in accepting such obligations, You may act only on Your own behalf and on Your
+sole responsibility, not on behalf of any other Contributor, and only if You
+agree to indemnify, defend, and hold each Contributor harmless for any liability
+incurred by, or claims asserted against, such Contributor by reason of your
+accepting any such warranty or additional liability.
+
+_END OF TERMS AND CONDITIONS_
+
+### APPENDIX: How to apply the Apache License to your work
+
+To apply the Apache License to your work, attach the following boilerplate
+notice, with the fields enclosed by brackets `[]` replaced with your own
+identifying information. (Don't include the brackets!) The text should be
+enclosed in the appropriate comment syntax for the file format. We also
+recommend that a file or class name and description of purpose be included on
+the same “printed page” as the copyright notice for easier identification within
+third-party archives.
+
+    Copyright [yyyy] [name of copyright owner]
+
+    Licensed 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.
diff --git a/r/adbcpostgresql/NAMESPACE b/r/adbcpostgresql/NAMESPACE
new file mode 100644
index 0000000..78a5d79
--- /dev/null
+++ b/r/adbcpostgresql/NAMESPACE
@@ -0,0 +1,10 @@
+# Generated by roxygen2: do not edit by hand
+
+S3method(adbc_connection_init,adbcpostgresql_database)
+S3method(adbc_database_init,adbcpostgresql_driver_postgresql)
+S3method(adbc_statement_init,adbcpostgresql_connection)
+export(adbcpostgresql)
+importFrom(adbcdrivermanager,adbc_connection_init)
+importFrom(adbcdrivermanager,adbc_database_init)
+importFrom(adbcdrivermanager,adbc_statement_init)
+useDynLib(adbcpostgresql, .registration = TRUE)
diff --git a/r/adbcpostgresql/R/adbcpostgresql-package.R b/r/adbcpostgresql/R/adbcpostgresql-package.R
new file mode 100644
index 0000000..a7730be
--- /dev/null
+++ b/r/adbcpostgresql/R/adbcpostgresql-package.R
@@ -0,0 +1,92 @@
+# 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.
+
+#' @keywords internal
+#' @aliases NULL
+"_PACKAGE"
+
+## usethis namespace: start
+#' @useDynLib adbcpostgresql, .registration = TRUE
+## usethis namespace: end
+NULL
+
+#' ADBC PostgreSQL Driver
+#'
+#' @inheritParams adbcdrivermanager::adbc_database_init
+#' @inheritParams adbcdrivermanager::adbc_connection_init
+#' @inheritParams adbcdrivermanager::adbc_statement_init
+#' @param uri A URI to a database path (e.g.,
+#'   `postgresql://localhost:1234/postgres?user=user&password=password`)
+#' @param adbc.connection.autocommit Use FALSE to disable the default
+#'   autocommit behaviour.
+#' @param adbc.ingest.target_table The name of the target table for a bulk insert.
+#' @param adbc.ingest.mode Whether to create (the default) or append.
+#'
+#' @return An [adbcdrivermanager::adbc_driver()]
+#' @export
+#'
+#' @examples
+#' adbcpostgresql()
+#'
+adbcpostgresql <- function() {
+  adbcdrivermanager::adbc_driver(
+    .Call(adbcpostgresql_c_postgresql),
+    subclass = "adbcpostgresql_driver_postgresql"
+  )
+}
+
+#' @rdname adbcpostgresql
+#' @importFrom adbcdrivermanager adbc_database_init
+#' @export
+adbc_database_init.adbcpostgresql_driver_postgresql <- function(driver, uri) {
+  adbcdrivermanager::adbc_database_init_default(
+    driver,
+    list(uri = uri),
+    subclass = "adbcpostgresql_database"
+  )
+}
+
+#' @rdname adbcpostgresql
+#' @importFrom adbcdrivermanager adbc_connection_init
+#' @export
+adbc_connection_init.adbcpostgresql_database <- function(database,
+                                                     adbc.connection.autocommit = NULL) {
+  options <- list(adbc.connection.autocommit = adbc.connection.autocommit)
+  adbcdrivermanager::adbc_connection_init_default(
+    database,
+    options[!vapply(options, is.null, logical(1))],
+    subclass = "adbcpostgresql_connection"
+  )
+}
+
+#' @rdname adbcpostgresql
+#' @importFrom adbcdrivermanager adbc_statement_init
+#' @export
+adbc_statement_init.adbcpostgresql_connection <- function(connection,
+                                                      adbc.ingest.target_table = NULL,
+                                                      adbc.ingest.mode = NULL) {
+  options <- list(
+    adbc.ingest.target_table = adbc.ingest.target_table,
+    adbc.ingest.mode = adbc.ingest.mode
+  )
+
+  adbcdrivermanager::adbc_statement_init_default(
+    connection,
+    options[!vapply(options, is.null, logical(1))],
+    subclass = "adbcpostgresql_statement"
+  )
+}
diff --git a/r/adbcsqlite/README.Rmd b/r/adbcpostgresql/README.Rmd
similarity index 68%
copy from r/adbcsqlite/README.Rmd
copy to r/adbcpostgresql/README.Rmd
index 2d9488a..eaf12cc 100644
--- a/r/adbcsqlite/README.Rmd
+++ b/r/adbcpostgresql/README.Rmd
@@ -30,21 +30,21 @@ knitr::opts_chunk$set(
 )
 ```
 
-# adbcsqlite
+# adbcpostgresql
 
 <!-- badges: start -->
 <!-- badges: end -->
 
-The goal of adbcdrivermanager is to provide a low-level developer-facing interface
-to the Arrow Database Connectivity (ADBC) SQLite driver.
+The goal of adbcpostgresql is to provide a low-level developer-facing interface
+to the Arrow Database Connectivity (ADBC) PostgreSQL driver.
 
 ## Installation
 
-You can install the development version of adbcsqlite from [GitHub](https://github.com/) with:
+You can install the development version of adbcpostgresql from [GitHub](https://github.com/) with:
 
 ``` r
 # install.packages("remotes")
-remotes::install_github("apache/arrow-adbc/r/adbcsqlite")
+remotes::install_github("apache/arrow-adbc/r/adbcpostgresql")
 ```
 
 ## Example
@@ -55,11 +55,12 @@ This is a basic example which shows you how to solve a common problem:
 library(adbcdrivermanager)
 
 # Use the driver manager to connect to a database
-db <- adbc_database_init(adbcsqlite::adbcsqlite(), uri = ":memory:")
+uri <- Sys.getenv("ADBC_POSTGRESQL_TEST_URI")
+db <- adbc_database_init(adbcpostgresql::adbcpostgresql(), uri = uri)
 con <- adbc_connection_init(db)
 
 # Write a table
-flights <- nycflights13::flights
+flights <- head(nycflights13::flights, 100)
 # (timestamp not supported yet)
 flights$time_hour <- NULL
 
@@ -70,12 +71,24 @@ adbc_statement_release(stmt)
 
 # Query it
 stmt <- adbc_statement_init(con)
+stream <- nanoarrow::nanoarrow_allocate_array_stream()
+
 adbc_statement_set_sql_query(stmt, "SELECT * from flights")
-result <- tibble::as_tibble(adbc_statement_execute_query(stmt))
+adbc_statement_execute_query(stmt, stream)
+result <- tibble::as_tibble(stream)
 adbc_statement_release(stmt)
 
 result
+```
+
+```{r example-clean-up, include=FALSE}
+stmt <- adbc_statement_init(con)
+adbc_statement_set_sql_query(stmt, "DROP TABLE flights")
+adbc_statement_execute_query(stmt)
+adbc_statement_release(stmt)
+```
 
+```{r example-clean-up2, include=FALSE}
 # Clean up
 adbc_connection_release(con)
 adbc_database_release(db)
diff --git a/r/adbcsqlite/README.md b/r/adbcpostgresql/README.md
similarity index 77%
copy from r/adbcsqlite/README.md
copy to r/adbcpostgresql/README.md
index 68b7151..9270f84 100644
--- a/r/adbcsqlite/README.md
+++ b/r/adbcpostgresql/README.md
@@ -17,22 +17,22 @@
 -->
 <!-- README.md is generated from README.Rmd. Please edit that file -->
 
-# adbcsqlite
+# adbcpostgresql
 
 <!-- badges: start -->
 <!-- badges: end -->
 
-The goal of adbcdrivermanager is to provide a low-level developer-facing
-interface to the Arrow Database Connectivity (ADBC) SQLite driver.
+The goal of adbcpostgresql is to provide a low-level developer-facing
+interface to the Arrow Database Connectivity (ADBC) PostgreSQL driver.
 
 ## Installation
 
-You can install the development version of adbcsqlite from
+You can install the development version of adbcpostgresql from
 [GitHub](https://github.com/) with:
 
 ``` r
 # install.packages("remotes")
-remotes::install_github("apache/arrow-adbc/r/adbcsqlite")
+remotes::install_github("apache/arrow-adbc/r/adbcpostgresql")
 ```
 
 ## Example
@@ -43,30 +43,35 @@ This is a basic example which shows you how to solve a common problem:
 library(adbcdrivermanager)
 
 # Use the driver manager to connect to a database
-db <- adbc_database_init(adbcsqlite::adbcsqlite(), uri = ":memory:")
+uri <- Sys.getenv("ADBC_POSTGRESQL_TEST_URI")
+db <- adbc_database_init(adbcpostgresql::adbcpostgresql(), uri = uri)
 con <- adbc_connection_init(db)
 
 # Write a table
-flights <- nycflights13::flights
+flights <- head(nycflights13::flights, 100)
 # (timestamp not supported yet)
 flights$time_hour <- NULL
 
 stmt <- adbc_statement_init(con, adbc.ingest.target_table = "flights")
 adbc_statement_bind(stmt, flights)
 adbc_statement_execute_query(stmt)
-#> <nanoarrow_array_stream[invalid pointer]>
+#> [1] 100
 adbc_statement_release(stmt)
 
 # Query it
 stmt <- adbc_statement_init(con)
+stream <- nanoarrow::nanoarrow_allocate_array_stream()
+
 adbc_statement_set_sql_query(stmt, "SELECT * from flights")
-result <- tibble::as_tibble(adbc_statement_execute_query(stmt))
+adbc_statement_execute_query(stmt, stream)
+#> [1] -1
+result <- tibble::as_tibble(stream)
 adbc_statement_release(stmt)
 
 result
-#> # A tibble: 336,776 × 18
+#> # A tibble: 100 × 18
 #>     year month   day dep_time sched_de…¹ dep_d…² arr_t…³ sched…⁴ arr_d…⁵ carrier
-#>    <dbl> <dbl> <dbl>    <dbl>      <dbl>   <dbl>   <dbl>   <dbl>   <dbl> <chr>
+#>    <int> <int> <int>    <int>      <int>   <dbl>   <int>   <int>   <dbl> <chr>
 #>  1  2013     1     1      517        515       2     830     819      11 UA
 #>  2  2013     1     1      533        529       4     850     830      20 UA
 #>  3  2013     1     1      542        540       2     923     850      33 AA
@@ -77,12 +82,8 @@ result
 #>  8  2013     1     1      557        600      -3     709     723     -14 EV
 #>  9  2013     1     1      557        600      -3     838     846      -8 B6
 #> 10  2013     1     1      558        600      -2     753     745       8 AA
-#> # … with 336,766 more rows, 8 more variables: flight <dbl>, tailnum <chr>,
+#> # … with 90 more rows, 8 more variables: flight <int>, tailnum <chr>,
 #> #   origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>, hour <dbl>,
 #> #   minute <dbl>, and abbreviated variable names ¹​sched_dep_time, ²​dep_delay,
 #> #   ³​arr_time, ⁴​sched_arr_time, ⁵​arr_delay
-
-# Clean up
-adbc_connection_release(con)
-adbc_database_release(db)
 ```
diff --git a/r/adbcpostgresql/adbcpostgresql.Rproj b/r/adbcpostgresql/adbcpostgresql.Rproj
new file mode 100644
index 0000000..69fafd4
--- /dev/null
+++ b/r/adbcpostgresql/adbcpostgresql.Rproj
@@ -0,0 +1,22 @@
+Version: 1.0
+
+RestoreWorkspace: No
+SaveWorkspace: No
+AlwaysSaveHistory: Default
+
+EnableCodeIndexing: Yes
+UseSpacesForTab: Yes
+NumSpacesForTab: 2
+Encoding: UTF-8
+
+RnwWeave: Sweave
+LaTeX: pdfLaTeX
+
+AutoAppendNewline: Yes
+StripTrailingWhitespace: Yes
+LineEndingConversion: Posix
+
+BuildType: Package
+PackageUseDevtools: Yes
+PackageInstallArgs: --no-multiarch --with-keep.source
+PackageRoxygenize: rd,collate,namespace
diff --git a/r/adbcpostgresql/bootstrap.R b/r/adbcpostgresql/bootstrap.R
new file mode 100644
index 0000000..030157e
--- /dev/null
+++ b/r/adbcpostgresql/bootstrap.R
@@ -0,0 +1,60 @@
+# 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.
+
+# If we are building within the repo, copy the latest adbc.h and driver source
+# into src/
+files_to_vendor <- c(
+  "../../adbc.h",
+  "../../c/driver/postgresql/util.h",
+  "../../c/driver/postgresql/type.h",
+  "../../c/driver/postgresql/type.cc",
+  "../../c/driver/postgresql/statement.h",
+  "../../c/driver/postgresql/statement.cc",
+  "../../c/driver/postgresql/connection.h",
+  "../../c/driver/postgresql/connection.cc",
+  "../../c/driver/postgresql/database.h",
+  "../../c/driver/postgresql/database.cc",
+  "../../c/driver/postgresql/postgresql.cc",
+  "../../c/vendor/nanoarrow/nanoarrow.h",
+  "../../c/vendor/nanoarrow/nanoarrow.c"
+)
+
+if (all(file.exists(files_to_vendor))) {
+  files_dst <- file.path("src", basename(files_to_vendor))
+
+  n_removed <- suppressWarnings(sum(file.remove(files_dst)))
+  if (n_removed > 0) {
+    cat(sprintf("Removed %d previously vendored files from src/\n", n_removed))
+  }
+
+  cat(
+    sprintf(
+      "Vendoring files from arrow-adbc to src/:\n%s\n",
+      paste("-", files_to_vendor, collapse = "\n")
+    )
+  )
+
+  if (all(file.copy(files_to_vendor, "src"))) {
+    file.rename(
+      c("src/nanoarrow.c", "src/nanoarrow.h"),
+      c("src/nanoarrow/nanoarrow.c", "src/nanoarrow/nanoarrow.h")
+    )
+    cat("All files successfully copied to src/\n")
+  } else {
+    stop("Failed to vendor all files")
+  }
+}
diff --git a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R b/r/adbcpostgresql/cleanup
old mode 100644
new mode 100755
similarity index 62%
copy from r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
copy to r/adbcpostgresql/cleanup
index 5c6843d..a940f2d
--- a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
+++ b/r/adbcpostgresql/cleanup
@@ -15,15 +15,4 @@
 # specific language governing permissions and limitations
 # under the License.
 
-test_that("the monkey driver sees, and the monkey driver does", {
-  db <- adbc_database_init(adbc_driver_monkey())
-  expect_s3_class(db, "adbc_database_monkey")
-  con <- adbc_connection_init(db)
-  expect_s3_class(con, "adbc_connection_monkey")
-
-  input <- data.frame(x = 1:10)
-  stmt <- adbc_statement_init(con, input)
-  expect_s3_class(stmt, "adbc_statement_monkey")
-  result <- adbc_statement_execute_query(stmt)
-  expect_identical(as.data.frame(result$get_next()), input)
-})
+rm src/*.o src/nanoarrow/*.o || true
diff --git a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R b/r/adbcpostgresql/cleanup.win
old mode 100644
new mode 100755
similarity index 62%
copy from r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
copy to r/adbcpostgresql/cleanup.win
index 5c6843d..7bee0e8
--- a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
+++ b/r/adbcpostgresql/cleanup.win
@@ -15,15 +15,4 @@
 # specific language governing permissions and limitations
 # under the License.
 
-test_that("the monkey driver sees, and the monkey driver does", {
-  db <- adbc_database_init(adbc_driver_monkey())
-  expect_s3_class(db, "adbc_database_monkey")
-  con <- adbc_connection_init(db)
-  expect_s3_class(con, "adbc_connection_monkey")
-
-  input <- data.frame(x = 1:10)
-  stmt <- adbc_statement_init(con, input)
-  expect_s3_class(stmt, "adbc_statement_monkey")
-  result <- adbc_statement_execute_query(stmt)
-  expect_identical(as.data.frame(result$get_next()), input)
-})
+./cleanup
diff --git a/r/adbcpostgresql/configure b/r/adbcpostgresql/configure
new file mode 100755
index 0000000..2369855
--- /dev/null
+++ b/r/adbcpostgresql/configure
@@ -0,0 +1,80 @@
+# 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.
+
+# Run bootstrap.R. This will have already run if we are installing a source
+# package built with pkgbuild::build() with pkgbuild >1.4.0; however, we
+# run it again in case this is R CMD INSTALL on a directory or
+# devtools::load_all(). This will vendor files from elsewhere in the
+# ADBC repo into this package. If the file doesn't exist, we're installing
+# from a pre-built tarball.
+if [ -f bootstrap.R ]; then
+  $R_HOME/bin/Rscript bootstrap.R
+fi
+
+# Include and library flags
+PKG_LIBS="$PKG_LIBS"
+
+# Check for pkg-config
+HAS_PKG_CONFIG=""
+pkg-config libpq --exists
+if [ $? -eq 0 ]; then
+  HAS_PKG_CONFIG=true
+fi
+
+echo "Checking for --configure-vars INCLUDE_DIR or LIB_DIR"
+if [ "$INCLUDE_DIR" ] || [ "$LIB_DIR" ]; then
+  echo "Found --configure-vars INCLUDE_DIR and/or LIB_DIR"
+  PKG_CPPFLAGS="-I$INCLUDE_DIR $PKG_CPPFLAGS"
+  PKG_LIBS="-L$LIB_DIR $PKG_LIBS"
+elif [ ! -z "$HAS_PKG_CONFIG" ]; then
+  echo "Using pkg-config libpq"
+  PKG_CPPFLAGS="`pkg-config libpq --cflags` $PKG_CPPFLAGS"
+  PKG_LIBS="`pkg-config libpq --libs` -lpq $PKG_LIBS"
+else
+  echo "INCLUDE_DIR/LIB_DIR and pkg-config not found; trying PKG_LIBS=-lpq"
+  PKG_LIBS="-lpq"
+fi
+
+echo "Testing R CMD SHLIB with $PKG_CPPFLAGS $PKG_LIBS"
+PKG_CPPFLAGS="$PKG_CPPFLAGS" PKG_LIBS="$PKG_LIBS" \
+  $R_HOME/bin/R CMD SHLIB tools/test.c -o compile_test >compile_test.log 2>&1
+
+if [ $? -ne 0 ]; then
+  echo "Test compile failed"
+  cat compile_test.log
+  exit 1
+else
+  echo "Success!"
+fi
+
+rm -f tools/test.o compile_test compile_test.log || true
+
+sed \
+  -e "s|@cppflags@|$PKG_CPPFLAGS|" \
+  -e "s|@libs@|$PKG_LIBS|" \
+  src/Makevars.in > src/Makevars
+
+
+if [ -f "src/adbc.h" ]; then
+  echo "Found vendored ADBC"
+  exit 0
+fi
+
+
+echo "Vendored ADBC PostgreSQL driver was not found."
+echo "This source package was probably built incorrectly and it's probably not your fault"
+exit 1
diff --git a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R b/r/adbcpostgresql/configure.win
old mode 100644
new mode 100755
similarity index 62%
copy from r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
copy to r/adbcpostgresql/configure.win
index 5c6843d..105bb7f
--- a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
+++ b/r/adbcpostgresql/configure.win
@@ -15,15 +15,5 @@
 # specific language governing permissions and limitations
 # under the License.
 
-test_that("the monkey driver sees, and the monkey driver does", {
-  db <- adbc_database_init(adbc_driver_monkey())
-  expect_s3_class(db, "adbc_database_monkey")
-  con <- adbc_connection_init(db)
-  expect_s3_class(con, "adbc_connection_monkey")
-
-  input <- data.frame(x = 1:10)
-  stmt <- adbc_statement_init(con, input)
-  expect_s3_class(stmt, "adbc_statement_monkey")
-  result <- adbc_statement_execute_query(stmt)
-  expect_identical(as.data.frame(result$get_next()), input)
-})
+# Just call the original configure script
+./configure
diff --git a/r/adbcpostgresql/man/adbcpostgresql-package.Rd b/r/adbcpostgresql/man/adbcpostgresql-package.Rd
new file mode 100644
index 0000000..f879957
--- /dev/null
+++ b/r/adbcpostgresql/man/adbcpostgresql-package.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/adbcpostgresql-package.R
+\docType{package}
+\name{adbcpostgresql-package}
+\title{adbcpostgresql: Arrow Database Connectivity (ADBC) PostgreSQL Driver}
+\description{
+Provides a developer-facing interface to the Arrow Database Connectivity (ADBC) PostgreSQL driver.
+}
+\seealso{
+Useful links:
+\itemize{
+  \item \url{https://github.com/apache/arrow-adbc}
+  \item Report bugs at \url{https://github.com/apache/arrow-adbc/issues}
+}
+
+}
+\author{
+\strong{Maintainer}: Dewey Dunnington \email{dewey@dunnington.ca} (\href{https://orcid.org/0000-0002-9415-4582}{ORCID})
+
+Authors:
+\itemize{
+  \item Apache Arrow \email{dev@arrow.apache.org} [copyright holder]
+}
+
+}
+\keyword{internal}
diff --git a/r/adbcpostgresql/man/adbcpostgresql.Rd b/r/adbcpostgresql/man/adbcpostgresql.Rd
new file mode 100644
index 0000000..fc37088
--- /dev/null
+++ b/r/adbcpostgresql/man/adbcpostgresql.Rd
@@ -0,0 +1,48 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/adbcpostgresql-package.R
+\name{adbcpostgresql}
+\alias{adbcpostgresql}
+\alias{adbc_database_init.adbcpostgresql_driver_postgresql}
+\alias{adbc_connection_init.adbcpostgresql_database}
+\alias{adbc_statement_init.adbcpostgresql_connection}
+\title{ADBC PostgreSQL Driver}
+\usage{
+adbcpostgresql()
+
+\method{adbc_database_init}{adbcpostgresql_driver_postgresql}(driver, uri)
+
+\method{adbc_connection_init}{adbcpostgresql_database}(database, adbc.connection.autocommit = NULL)
+
+\method{adbc_statement_init}{adbcpostgresql_connection}(
+  connection,
+  adbc.ingest.target_table = NULL,
+  adbc.ingest.mode = NULL
+)
+}
+\arguments{
+\item{driver}{An \code{\link[adbcdrivermanager:adbc_driver]{adbc_driver()}}.}
+
+\item{uri}{A URI to a database path (e.g.,
+\verb{postgresql://localhost:1234/postgres?user=user&password=password})}
+
+\item{database}{An \link[adbcdrivermanager:adbc_database_init]{adbc_database}.}
+
+\item{adbc.connection.autocommit}{Use FALSE to disable the default
+autocommit behaviour.}
+
+\item{connection}{An \link[adbcdrivermanager:adbc_connection_init]{adbc_connection}}
+
+\item{adbc.ingest.target_table}{The name of the target table for a bulk insert.}
+
+\item{adbc.ingest.mode}{Whether to create (the default) or append.}
+}
+\value{
+An \code{\link[adbcdrivermanager:adbc_driver_void]{adbcdrivermanager::adbc_driver()}}
+}
+\description{
+ADBC PostgreSQL Driver
+}
+\examples{
+adbcpostgresql()
+
+}
diff --git a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R b/r/adbcpostgresql/src/.gitignore
similarity index 62%
copy from r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
copy to r/adbcpostgresql/src/.gitignore
index 5c6843d..45c6e7e 100644
--- a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
+++ b/r/adbcpostgresql/src/.gitignore
@@ -15,15 +15,17 @@
 # specific language governing permissions and limitations
 # under the License.
 
-test_that("the monkey driver sees, and the monkey driver does", {
-  db <- adbc_database_init(adbc_driver_monkey())
-  expect_s3_class(db, "adbc_database_monkey")
-  con <- adbc_connection_init(db)
-  expect_s3_class(con, "adbc_connection_monkey")
-
-  input <- data.frame(x = 1:10)
-  stmt <- adbc_statement_init(con, input)
-  expect_s3_class(stmt, "adbc_statement_monkey")
-  result <- adbc_statement_execute_query(stmt)
-  expect_identical(as.data.frame(result$get_next()), input)
-})
+*.o
+*.so
+*.dll
+adbc.h
+connection.cc
+connection.h
+database.h
+postgresql.cc
+statement.h
+statement.cc
+type.cc
+type.h
+util.h
+Makevars
diff --git a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R b/r/adbcpostgresql/src/Makevars.in
similarity index 62%
copy from r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
copy to r/adbcpostgresql/src/Makevars.in
index 5c6843d..fe9d8e5 100644
--- a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
+++ b/r/adbcpostgresql/src/Makevars.in
@@ -15,15 +15,13 @@
 # specific language governing permissions and limitations
 # under the License.
 
-test_that("the monkey driver sees, and the monkey driver does", {
-  db <- adbc_database_init(adbc_driver_monkey())
-  expect_s3_class(db, "adbc_database_monkey")
-  con <- adbc_connection_init(db)
-  expect_s3_class(con, "adbc_connection_monkey")
+PKG_CPPFLAGS=-I../src @cppflags@ -DADBC_EXPORT=""
+PKG_LIBS=@libs@
 
-  input <- data.frame(x = 1:10)
-  stmt <- adbc_statement_init(con, input)
-  expect_s3_class(stmt, "adbc_statement_monkey")
-  result <- adbc_statement_execute_query(stmt)
-  expect_identical(as.data.frame(result$get_next()), input)
-})
+OBJECTS = init.o \
+    connection.o \
+    database.o \
+    statement.o \
+    type.o \
+    postgresql.o \
+    nanoarrow/nanoarrow.o
diff --git a/r/adbcpostgresql/src/database.cc b/r/adbcpostgresql/src/database.cc
new file mode 100644
index 0000000..bc5e0ec
--- /dev/null
+++ b/r/adbcpostgresql/src/database.cc
@@ -0,0 +1,124 @@
+// 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.
+
+#include "database.h"
+
+#include <cstring>
+#include <memory>
+
+#include <adbc.h>
+#include <libpq-fe.h>
+#include <nanoarrow/nanoarrow.h>
+
+#include "util.h"
+
+namespace adbcpq {
+
+PostgresDatabase::PostgresDatabase() : open_connections_(0) {
+  type_mapping_ = std::make_shared<TypeMapping>();
+}
+PostgresDatabase::~PostgresDatabase() = default;
+
+AdbcStatusCode PostgresDatabase::Init(struct AdbcError* error) {
+  // Connect to validate the parameters.
+  PGconn* conn = nullptr;
+  AdbcStatusCode final_status = Connect(&conn, error);
+  if (final_status != ADBC_STATUS_OK) {
+    return final_status;
+  }
+
+  // Build the type mapping table.
+  const std::string kTypeQuery = R"(
+SELECT
+    oid,
+    typname,
+    typreceive
+FROM
+    pg_catalog.pg_type
+)";
+
+  pg_result* result = PQexec(conn, kTypeQuery.c_str());
+  ExecStatusType pq_status = PQresultStatus(result);
+  if (pq_status == PGRES_TUPLES_OK) {
+    int num_rows = PQntuples(result);
+    for (int row = 0; row < num_rows; row++) {
+      const uint32_t oid = static_cast<uint32_t>(
+          std::strtol(PQgetvalue(result, row, 0), /*str_end=*/nullptr, /*base=*/10));
+      const char* typname = PQgetvalue(result, row, 1);
+      const char* typreceive = PQgetvalue(result, row, 2);
+
+      type_mapping_->Insert(oid, typname, typreceive);
+    }
+  } else {
+    SetError(error, "Failed to build type mapping table: ", PQerrorMessage(conn));
+    final_status = ADBC_STATUS_IO;
+  }
+  PQclear(result);
+
+  // Disconnect since PostgreSQL connections can be heavy.
+  {
+    AdbcStatusCode status = Disconnect(&conn, error);
+    if (status != ADBC_STATUS_OK) final_status = status;
+  }
+  return final_status;
+}
+
+AdbcStatusCode PostgresDatabase::Release(struct AdbcError* error) {
+  if (open_connections_ != 0) {
+    SetError(error, "Database released with ", open_connections_, " open connections");
+    return ADBC_STATUS_INVALID_STATE;
+  }
+  return ADBC_STATUS_OK;
+}
+
+AdbcStatusCode PostgresDatabase::SetOption(const char* key, const char* value,
+                                           struct AdbcError* error) {
+  if (strcmp(key, "uri") == 0) {
+    uri_ = value;
+  } else {
+    SetError(error, "Unknown database option ", key);
+    return ADBC_STATUS_NOT_IMPLEMENTED;
+  }
+  return ADBC_STATUS_OK;
+}
+
+AdbcStatusCode PostgresDatabase::Connect(PGconn** conn, struct AdbcError* error) {
+  if (uri_.empty()) {
+    SetError(error, "Must set database option 'uri' before creating a connection");
+    return ADBC_STATUS_INVALID_STATE;
+  }
+  *conn = PQconnectdb(uri_.c_str());
+  if (PQstatus(*conn) != CONNECTION_OK) {
+    SetError(error, "Failed to connect: ", PQerrorMessage(*conn));
+    PQfinish(*conn);
+    *conn = nullptr;
+    return ADBC_STATUS_IO;
+  }
+  open_connections_++;
+  return ADBC_STATUS_OK;
+}
+
+AdbcStatusCode PostgresDatabase::Disconnect(PGconn** conn, struct AdbcError* error) {
+  PQfinish(*conn);
+  *conn = nullptr;
+  if (--open_connections_ < 0) {
+    SetError(error, "Open connection count underflowed");
+    return ADBC_STATUS_INTERNAL;
+  }
+  return ADBC_STATUS_OK;
+}
+}  // namespace adbcpq
diff --git a/r/adbcpostgresql/src/init.c b/r/adbcpostgresql/src/init.c
new file mode 100644
index 0000000..91ef4ad
--- /dev/null
+++ b/r/adbcpostgresql/src/init.c
@@ -0,0 +1,43 @@
+// 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.
+
+#define R_NO_REMAP
+#include <R.h>
+#include <Rinternals.h>
+
+#include <adbc.h>
+
+AdbcStatusCode AdbcDriverInit(int version, void* raw_driver, struct AdbcError* error);
+
+static SEXP init_func_xptr = 0;
+
+SEXP adbcpostgresql_c_postgresql(void) { return init_func_xptr; }
+
+static const R_CallMethodDef CallEntries[] = {
+    {"adbcpostgresql_c_postgresql", (DL_FUNC)&adbcpostgresql_c_postgresql, 0},
+    {NULL, NULL, 0}};
+
+void R_init_adbcpostgresql(DllInfo* dll) {
+  R_registerRoutines(dll, NULL, CallEntries, NULL, NULL);
+  R_useDynamicSymbols(dll, FALSE);
+
+  init_func_xptr =
+      PROTECT(R_MakeExternalPtrFn((DL_FUNC)AdbcDriverInit, R_NilValue, R_NilValue));
+  Rf_setAttrib(init_func_xptr, R_ClassSymbol, Rf_mkString("adbc_driver_init_func"));
+  R_PreserveObject(init_func_xptr);
+  UNPROTECT(1);
+}
diff --git a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R b/r/adbcpostgresql/src/nanoarrow/.gitignore
similarity index 62%
copy from r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
copy to r/adbcpostgresql/src/nanoarrow/.gitignore
index 5c6843d..87e59e2 100644
--- a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
+++ b/r/adbcpostgresql/src/nanoarrow/.gitignore
@@ -15,15 +15,5 @@
 # specific language governing permissions and limitations
 # under the License.
 
-test_that("the monkey driver sees, and the monkey driver does", {
-  db <- adbc_database_init(adbc_driver_monkey())
-  expect_s3_class(db, "adbc_database_monkey")
-  con <- adbc_connection_init(db)
-  expect_s3_class(con, "adbc_connection_monkey")
-
-  input <- data.frame(x = 1:10)
-  stmt <- adbc_statement_init(con, input)
-  expect_s3_class(stmt, "adbc_statement_monkey")
-  result <- adbc_statement_execute_query(stmt)
-  expect_identical(as.data.frame(result$get_next()), input)
-})
+nanoarrow.c
+nanoarrow.h
diff --git a/r/adbcpostgresql/src/postgresql.cc b/r/adbcpostgresql/src/postgresql.cc
new file mode 100644
index 0000000..d4be5ce
--- /dev/null
+++ b/r/adbcpostgresql/src/postgresql.cc
@@ -0,0 +1,499 @@
+// 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.
+
+// A libpq-based PostgreSQL driver for ADBC.
+
+#include <cstring>
+#include <memory>
+
+#include <adbc.h>
+
+#include "connection.h"
+#include "database.h"
+#include "statement.h"
+#include "util.h"
+
+using adbcpq::PostgresConnection;
+using adbcpq::PostgresDatabase;
+using adbcpq::PostgresStatement;
+
+// ---------------------------------------------------------------------
+// ADBC interface implementation - as private functions so that these
+// don't get replaced by the dynamic linker. If we implemented these
+// under the Adbc* names, then DriverInit, the linker may resolve
+// functions to the address of the functions provided by the driver
+// manager instead of our functions.
+//
+// We could also:
+// - Play games with RTLD_DEEPBIND - but this doesn't work with ASan
+// - Use __attribute__((visibility("protected"))) - but this is
+//   apparently poorly supported by some linkers
+// - Play with -Bsymbolic(-functions) - but this has other
+//   consequences and complicates the build setup
+//
+// So in the end some manual effort here was chosen.
+
+// ---------------------------------------------------------------------
+// AdbcDatabase
+
+namespace {
+using adbcpq::SetError;
+AdbcStatusCode PostgresDatabaseInit(struct AdbcDatabase* database,
+                                    struct AdbcError* error) {
+  if (!database || !database->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto ptr = reinterpret_cast<std::shared_ptr<PostgresDatabase>*>(database->private_data);
+  return (*ptr)->Init(error);
+}
+
+AdbcStatusCode PostgresDatabaseNew(struct AdbcDatabase* database,
+                                   struct AdbcError* error) {
+  if (!database) {
+    SetError(error, "database must not be null");
+    return ADBC_STATUS_INVALID_STATE;
+  }
+  if (database->private_data) {
+    SetError(error, "database is already initialized");
+    return ADBC_STATUS_INVALID_STATE;
+  }
+  auto impl = std::make_shared<PostgresDatabase>();
+  database->private_data = new std::shared_ptr<PostgresDatabase>(impl);
+  return ADBC_STATUS_OK;
+}
+
+AdbcStatusCode PostgresDatabaseRelease(struct AdbcDatabase* database,
+                                       struct AdbcError* error) {
+  if (!database->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto ptr = reinterpret_cast<std::shared_ptr<PostgresDatabase>*>(database->private_data);
+  AdbcStatusCode status = (*ptr)->Release(error);
+  delete ptr;
+  database->private_data = nullptr;
+  return status;
+}
+
+AdbcStatusCode PostgresDatabaseSetOption(struct AdbcDatabase* database, const char* key,
+                                         const char* value, struct AdbcError* error) {
+  if (!database || !database->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto ptr = reinterpret_cast<std::shared_ptr<PostgresDatabase>*>(database->private_data);
+  return (*ptr)->SetOption(key, value, error);
+}
+}  // namespace
+
+AdbcStatusCode AdbcDatabaseInit(struct AdbcDatabase* database, struct AdbcError* error) {
+  return PostgresDatabaseInit(database, error);
+}
+
+AdbcStatusCode AdbcDatabaseNew(struct AdbcDatabase* database, struct AdbcError* error) {
+  return PostgresDatabaseNew(database, error);
+}
+
+AdbcStatusCode AdbcDatabaseRelease(struct AdbcDatabase* database,
+                                   struct AdbcError* error) {
+  return PostgresDatabaseRelease(database, error);
+}
+
+AdbcStatusCode AdbcDatabaseSetOption(struct AdbcDatabase* database, const char* key,
+                                     const char* value, struct AdbcError* error) {
+  return PostgresDatabaseSetOption(database, key, value, error);
+}
+
+// ---------------------------------------------------------------------
+// AdbcConnection
+
+namespace {
+AdbcStatusCode PostgresConnectionCommit(struct AdbcConnection* connection,
+                                        struct AdbcError* error) {
+  if (!connection->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto ptr =
+      reinterpret_cast<std::shared_ptr<PostgresConnection>*>(connection->private_data);
+  return (*ptr)->Commit(error);
+}
+
+AdbcStatusCode PostgresConnectionGetInfo(struct AdbcConnection* connection,
+                                         uint32_t* info_codes, size_t info_codes_length,
+                                         struct ArrowArrayStream* stream,
+                                         struct AdbcError* error) {
+  return ADBC_STATUS_NOT_IMPLEMENTED;
+}
+
+AdbcStatusCode PostgresConnectionGetObjects(
+    struct AdbcConnection* connection, int depth, const char* catalog,
+    const char* db_schema, const char* table_name, const char** table_types,
+    const char* column_name, struct ArrowArrayStream* stream, struct AdbcError* error) {
+  return ADBC_STATUS_NOT_IMPLEMENTED;
+}
+
+AdbcStatusCode PostgresConnectionGetTableSchema(
+    struct AdbcConnection* connection, const char* catalog, const char* db_schema,
+    const char* table_name, struct ArrowSchema* schema, struct AdbcError* error) {
+  if (!connection->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto ptr =
+      reinterpret_cast<std::shared_ptr<PostgresConnection>*>(connection->private_data);
+  return (*ptr)->GetTableSchema(catalog, db_schema, table_name, schema, error);
+}
+
+AdbcStatusCode PostgresConnectionGetTableTypes(struct AdbcConnection* connection,
+                                               struct ArrowArrayStream* stream,
+                                               struct AdbcError* error) {
+  return ADBC_STATUS_NOT_IMPLEMENTED;
+}
+
+AdbcStatusCode PostgresConnectionInit(struct AdbcConnection* connection,
+                                      struct AdbcDatabase* database,
+                                      struct AdbcError* error) {
+  if (!connection->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto ptr =
+      reinterpret_cast<std::shared_ptr<PostgresConnection>*>(connection->private_data);
+  return (*ptr)->Init(database, error);
+}
+
+AdbcStatusCode PostgresConnectionNew(struct AdbcConnection* connection,
+                                     struct AdbcError* error) {
+  auto impl = std::make_shared<PostgresConnection>();
+  connection->private_data = new std::shared_ptr<PostgresConnection>(impl);
+  return ADBC_STATUS_OK;
+}
+
+AdbcStatusCode PostgresConnectionReadPartition(struct AdbcConnection* connection,
+                                               const uint8_t* serialized_partition,
+                                               size_t serialized_length,
+                                               struct ArrowArrayStream* out,
+                                               struct AdbcError* error) {
+  if (!connection->private_data) return ADBC_STATUS_INVALID_STATE;
+  return ADBC_STATUS_NOT_IMPLEMENTED;
+}
+
+AdbcStatusCode PostgresConnectionRelease(struct AdbcConnection* connection,
+                                         struct AdbcError* error) {
+  if (!connection->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto ptr =
+      reinterpret_cast<std::shared_ptr<PostgresConnection>*>(connection->private_data);
+  AdbcStatusCode status = (*ptr)->Release(error);
+  delete ptr;
+  connection->private_data = nullptr;
+  return status;
+}
+
+AdbcStatusCode PostgresConnectionRollback(struct AdbcConnection* connection,
+                                          struct AdbcError* error) {
+  if (!connection->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto ptr =
+      reinterpret_cast<std::shared_ptr<PostgresConnection>*>(connection->private_data);
+  return (*ptr)->Rollback(error);
+}
+
+AdbcStatusCode PostgresConnectionSetOption(struct AdbcConnection* connection,
+                                           const char* key, const char* value,
+                                           struct AdbcError* error) {
+  if (!connection->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto ptr =
+      reinterpret_cast<std::shared_ptr<PostgresConnection>*>(connection->private_data);
+  return (*ptr)->SetOption(key, value, error);
+}
+
+}  // namespace
+AdbcStatusCode AdbcConnectionCommit(struct AdbcConnection* connection,
+                                    struct AdbcError* error) {
+  return PostgresConnectionCommit(connection, error);
+}
+
+AdbcStatusCode AdbcConnectionGetInfo(struct AdbcConnection* connection,
+                                     uint32_t* info_codes, size_t info_codes_length,
+                                     struct ArrowArrayStream* stream,
+                                     struct AdbcError* error) {
+  return PostgresConnectionGetInfo(connection, info_codes, info_codes_length, stream,
+                                   error);
+}
+
+AdbcStatusCode AdbcConnectionGetObjects(struct AdbcConnection* connection, int depth,
+                                        const char* catalog, const char* db_schema,
+                                        const char* table_name, const char** table_types,
+                                        const char* column_name,
+                                        struct ArrowArrayStream* stream,
+                                        struct AdbcError* error) {
+  return PostgresConnectionGetObjects(connection, depth, catalog, db_schema, table_name,
+                                      table_types, column_name, stream, error);
+}
+
+AdbcStatusCode AdbcConnectionGetTableSchema(struct AdbcConnection* connection,
+                                            const char* catalog, const char* db_schema,
+                                            const char* table_name,
+                                            struct ArrowSchema* schema,
+                                            struct AdbcError* error) {
+  return PostgresConnectionGetTableSchema(connection, catalog, db_schema, table_name,
+                                          schema, error);
+}
+
+AdbcStatusCode AdbcConnectionGetTableTypes(struct AdbcConnection* connection,
+                                           struct ArrowArrayStream* stream,
+                                           struct AdbcError* error) {
+  return PostgresConnectionGetTableTypes(connection, stream, error);
+}
+
+AdbcStatusCode AdbcConnectionInit(struct AdbcConnection* connection,
+                                  struct AdbcDatabase* database,
+                                  struct AdbcError* error) {
+  return PostgresConnectionInit(connection, database, error);
+}
+
+AdbcStatusCode AdbcConnectionNew(struct AdbcConnection* connection,
+                                 struct AdbcError* error) {
+  return PostgresConnectionNew(connection, error);
+}
+
+AdbcStatusCode AdbcConnectionReadPartition(struct AdbcConnection* connection,
+                                           const uint8_t* serialized_partition,
+                                           size_t serialized_length,
+                                           struct ArrowArrayStream* out,
+                                           struct AdbcError* error) {
+  return PostgresConnectionReadPartition(connection, serialized_partition,
+                                         serialized_length, out, error);
+}
+
+AdbcStatusCode AdbcConnectionRelease(struct AdbcConnection* connection,
+                                     struct AdbcError* error) {
+  return PostgresConnectionRelease(connection, error);
+}
+
+AdbcStatusCode AdbcConnectionRollback(struct AdbcConnection* connection,
+                                      struct AdbcError* error) {
+  return PostgresConnectionRollback(connection, error);
+}
+
+AdbcStatusCode AdbcConnectionSetOption(struct AdbcConnection* connection, const char* key,
+                                       const char* value, struct AdbcError* error) {
+  return PostgresConnectionSetOption(connection, key, value, error);
+}
+
+// ---------------------------------------------------------------------
+// AdbcStatement
+
+namespace {
+AdbcStatusCode PostgresStatementBind(struct AdbcStatement* statement,
+                                     struct ArrowArray* values,
+                                     struct ArrowSchema* schema,
+                                     struct AdbcError* error) {
+  if (!statement->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto* ptr =
+      reinterpret_cast<std::shared_ptr<PostgresStatement>*>(statement->private_data);
+  return (*ptr)->Bind(values, schema, error);
+}
+
+AdbcStatusCode PostgresStatementBindStream(struct AdbcStatement* statement,
+                                           struct ArrowArrayStream* stream,
+                                           struct AdbcError* error) {
+  if (!statement->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto* ptr =
+      reinterpret_cast<std::shared_ptr<PostgresStatement>*>(statement->private_data);
+  return (*ptr)->Bind(stream, error);
+}
+
+AdbcStatusCode PostgresStatementExecutePartitions(struct AdbcStatement* statement,
+                                                  struct ArrowSchema* schema,
+                                                  struct AdbcPartitions* partitions,
+                                                  int64_t* rows_affected,
+                                                  struct AdbcError* error) {
+  if (!statement->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto* ptr =
+      reinterpret_cast<std::shared_ptr<PostgresStatement>*>(statement->private_data);
+  return ADBC_STATUS_NOT_IMPLEMENTED;
+}
+
+AdbcStatusCode PostgresStatementExecuteQuery(struct AdbcStatement* statement,
+                                             struct ArrowArrayStream* output,
+                                             int64_t* rows_affected,
+                                             struct AdbcError* error) {
+  if (!statement->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto* ptr =
+      reinterpret_cast<std::shared_ptr<PostgresStatement>*>(statement->private_data);
+  return (*ptr)->ExecuteQuery(output, rows_affected, error);
+}
+
+AdbcStatusCode PostgresStatementGetPartitionDesc(struct AdbcStatement* statement,
+                                                 uint8_t* partition_desc,
+                                                 struct AdbcError* error) {
+  return ADBC_STATUS_NOT_IMPLEMENTED;
+}
+
+AdbcStatusCode PostgresStatementGetPartitionDescSize(struct AdbcStatement* statement,
+                                                     size_t* length,
+                                                     struct AdbcError* error) {
+  return ADBC_STATUS_NOT_IMPLEMENTED;
+}
+
+AdbcStatusCode PostgresStatementGetParameterSchema(struct AdbcStatement* statement,
+                                                   struct ArrowSchema* schema,
+                                                   struct AdbcError* error) {
+  if (!statement->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto* ptr =
+      reinterpret_cast<std::shared_ptr<PostgresStatement>*>(statement->private_data);
+  return (*ptr)->GetParameterSchema(schema, error);
+}
+
+AdbcStatusCode PostgresStatementNew(struct AdbcConnection* connection,
+                                    struct AdbcStatement* statement,
+                                    struct AdbcError* error) {
+  auto impl = std::make_shared<PostgresStatement>();
+  statement->private_data = new std::shared_ptr<PostgresStatement>(impl);
+  return impl->New(connection, error);
+}
+
+AdbcStatusCode PostgresStatementPrepare(struct AdbcStatement* statement,
+                                        struct AdbcError* error) {
+  if (!statement->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto* ptr =
+      reinterpret_cast<std::shared_ptr<PostgresStatement>*>(statement->private_data);
+  return (*ptr)->Prepare(error);
+}
+
+AdbcStatusCode PostgresStatementRelease(struct AdbcStatement* statement,
+                                        struct AdbcError* error) {
+  if (!statement->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto* ptr =
+      reinterpret_cast<std::shared_ptr<PostgresStatement>*>(statement->private_data);
+  auto status = (*ptr)->Release(error);
+  delete ptr;
+  statement->private_data = nullptr;
+  return status;
+}
+
+AdbcStatusCode PostgresStatementSetOption(struct AdbcStatement* statement,
+                                          const char* key, const char* value,
+                                          struct AdbcError* error) {
+  if (!statement->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto* ptr =
+      reinterpret_cast<std::shared_ptr<PostgresStatement>*>(statement->private_data);
+  return (*ptr)->SetOption(key, value, error);
+}
+
+AdbcStatusCode PostgresStatementSetSqlQuery(struct AdbcStatement* statement,
+                                            const char* query, struct AdbcError* error) {
+  if (!statement->private_data) return ADBC_STATUS_INVALID_STATE;
+  auto* ptr =
+      reinterpret_cast<std::shared_ptr<PostgresStatement>*>(statement->private_data);
+  return (*ptr)->SetSqlQuery(query, error);
+}
+}  // namespace
+
+AdbcStatusCode AdbcStatementBind(struct AdbcStatement* statement,
+                                 struct ArrowArray* values, struct ArrowSchema* schema,
+                                 struct AdbcError* error) {
+  return PostgresStatementBind(statement, values, schema, error);
+}
+
+AdbcStatusCode AdbcStatementBindStream(struct AdbcStatement* statement,
+                                       struct ArrowArrayStream* stream,
+                                       struct AdbcError* error) {
+  return PostgresStatementBindStream(statement, stream, error);
+}
+
+AdbcStatusCode AdbcStatementExecutePartitions(struct AdbcStatement* statement,
+                                              ArrowSchema* schema,
+                                              struct AdbcPartitions* partitions,
+                                              int64_t* rows_affected,
+                                              struct AdbcError* error) {
+  return PostgresStatementExecutePartitions(statement, schema, partitions, rows_affected,
+                                            error);
+}
+
+AdbcStatusCode AdbcStatementExecuteQuery(struct AdbcStatement* statement,
+                                         struct ArrowArrayStream* output,
+                                         int64_t* rows_affected,
+                                         struct AdbcError* error) {
+  return PostgresStatementExecuteQuery(statement, output, rows_affected, error);
+}
+
+AdbcStatusCode AdbcStatementGetPartitionDesc(struct AdbcStatement* statement,
+                                             uint8_t* partition_desc,
+                                             struct AdbcError* error) {
+  return PostgresStatementGetPartitionDesc(statement, partition_desc, error);
+}
+
+AdbcStatusCode AdbcStatementGetPartitionDescSize(struct AdbcStatement* statement,
+                                                 size_t* length,
+                                                 struct AdbcError* error) {
+  return PostgresStatementGetPartitionDescSize(statement, length, error);
+}
+
+AdbcStatusCode AdbcStatementGetParameterSchema(struct AdbcStatement* statement,
+                                               struct ArrowSchema* schema,
+                                               struct AdbcError* error) {
+  return PostgresStatementGetParameterSchema(statement, schema, error);
+}
+
+AdbcStatusCode AdbcStatementNew(struct AdbcConnection* connection,
+                                struct AdbcStatement* statement,
+                                struct AdbcError* error) {
+  return PostgresStatementNew(connection, statement, error);
+}
+
+AdbcStatusCode AdbcStatementPrepare(struct AdbcStatement* statement,
+                                    struct AdbcError* error) {
+  return PostgresStatementPrepare(statement, error);
+}
+
+AdbcStatusCode AdbcStatementRelease(struct AdbcStatement* statement,
+                                    struct AdbcError* error) {
+  return PostgresStatementRelease(statement, error);
+}
+
+AdbcStatusCode AdbcStatementSetOption(struct AdbcStatement* statement, const char* key,
+                                      const char* value, struct AdbcError* error) {
+  return PostgresStatementSetOption(statement, key, value, error);
+}
+
+AdbcStatusCode AdbcStatementSetSqlQuery(struct AdbcStatement* statement,
+                                        const char* query, struct AdbcError* error) {
+  return PostgresStatementSetSqlQuery(statement, query, error);
+}
+
+extern "C" {
+ADBC_EXPORT
+AdbcStatusCode AdbcDriverInit(int version, void* raw_driver, struct AdbcError* error) {
+  if (version != ADBC_VERSION_1_0_0) return ADBC_STATUS_NOT_IMPLEMENTED;
+
+  auto* driver = reinterpret_cast<struct AdbcDriver*>(raw_driver);
+  std::memset(driver, 0, sizeof(*driver));
+  driver->DatabaseInit = PostgresDatabaseInit;
+  driver->DatabaseNew = PostgresDatabaseNew;
+  driver->DatabaseRelease = PostgresDatabaseRelease;
+  driver->DatabaseSetOption = PostgresDatabaseSetOption;
+
+  driver->ConnectionCommit = PostgresConnectionCommit;
+  driver->ConnectionGetInfo = PostgresConnectionGetInfo;
+  driver->ConnectionGetObjects = PostgresConnectionGetObjects;
+  driver->ConnectionGetTableSchema = PostgresConnectionGetTableSchema;
+  driver->ConnectionGetTableTypes = PostgresConnectionGetTableTypes;
+  driver->ConnectionInit = PostgresConnectionInit;
+  driver->ConnectionNew = PostgresConnectionNew;
+  driver->ConnectionReadPartition = PostgresConnectionReadPartition;
+  driver->ConnectionRelease = PostgresConnectionRelease;
+  driver->ConnectionRollback = PostgresConnectionRollback;
+  driver->ConnectionSetOption = PostgresConnectionSetOption;
+
+  driver->StatementBind = PostgresStatementBind;
+  driver->StatementBindStream = PostgresStatementBindStream;
+  driver->StatementExecutePartitions = PostgresStatementExecutePartitions;
+  driver->StatementExecuteQuery = PostgresStatementExecuteQuery;
+  driver->StatementGetParameterSchema = PostgresStatementGetParameterSchema;
+  driver->StatementNew = PostgresStatementNew;
+  driver->StatementPrepare = PostgresStatementPrepare;
+  driver->StatementRelease = PostgresStatementRelease;
+  driver->StatementSetOption = PostgresStatementSetOption;
+  driver->StatementSetSqlQuery = PostgresStatementSetSqlQuery;
+  return ADBC_STATUS_OK;
+}
+}
diff --git a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R b/r/adbcpostgresql/tests/testthat.R
similarity index 62%
copy from r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
copy to r/adbcpostgresql/tests/testthat.R
index 5c6843d..2acb42f 100644
--- a/r/adbcdrivermanager/tests/testthat/test-driver_monkey.R
+++ b/r/adbcpostgresql/tests/testthat.R
@@ -15,15 +15,15 @@
 # specific language governing permissions and limitations
 # under the License.
 
-test_that("the monkey driver sees, and the monkey driver does", {
-  db <- adbc_database_init(adbc_driver_monkey())
-  expect_s3_class(db, "adbc_database_monkey")
-  con <- adbc_connection_init(db)
-  expect_s3_class(con, "adbc_connection_monkey")
+# This file is part of the standard setup for testthat.
+# It is recommended that you do not modify it.
+#
+# Where should you do additional test configuration?
+# Learn more about the roles of various files in:
+# * https://r-pkgs.org/tests.html
+# * https://testthat.r-lib.org/reference/test_package.html#special-files
+
+library(testthat)
+library(adbcpostgresql)
 
-  input <- data.frame(x = 1:10)
-  stmt <- adbc_statement_init(con, input)
-  expect_s3_class(stmt, "adbc_statement_monkey")
-  result <- adbc_statement_execute_query(stmt)
-  expect_identical(as.data.frame(result$get_next()), input)
-})
+test_check("adbcpostgresql")
diff --git a/r/adbcsqlite/tests/testthat/test-adbcsqlite-package.R b/r/adbcpostgresql/tests/testthat/test-adbcpostgres-package.R
similarity index 58%
copy from r/adbcsqlite/tests/testthat/test-adbcsqlite-package.R
copy to r/adbcpostgresql/tests/testthat/test-adbcpostgres-package.R
index 3a211ec..0769976 100644
--- a/r/adbcsqlite/tests/testthat/test-adbcsqlite-package.R
+++ b/r/adbcpostgresql/tests/testthat/test-adbcpostgres-package.R
@@ -15,27 +15,47 @@
 # specific language governing permissions and limitations
 # under the License.
 
-test_that("adbcsqlite() works", {
-  expect_s3_class(adbcsqlite(), "adbc_driver")
+test_that("adbcpostgresql() works", {
+  expect_s3_class(adbcpostgresql(), "adbc_driver")
 })
 
 test_that("default options can open a database and execute a query", {
-  db <- adbcdrivermanager::adbc_database_init(adbcsqlite())
-  expect_s3_class(db, "adbcsqlite_database")
+  test_db_uri <- Sys.getenv("ADBC_POSTGRESQL_TEST_URI", "")
+  skip_if(identical(test_db_uri, ""))
+
+  db <- adbcdrivermanager::adbc_database_init(
+    adbcpostgresql(),
+    uri = test_db_uri
+  )
+  expect_s3_class(db, "adbcpostgresql_database")
 
   con <- adbcdrivermanager::adbc_connection_init(db)
-  expect_s3_class(con, "adbcsqlite_connection")
+  expect_s3_class(con, "adbcpostgresql_connection")
 
   stmt <- adbcdrivermanager::adbc_statement_init(con)
-  expect_s3_class(stmt, "adbcsqlite_statement")
+  expect_s3_class(stmt, "adbcpostgresql_statement")
 
   adbcdrivermanager::adbc_statement_set_sql_query(
     stmt,
-    "CREATE TABLE crossfit (exercise TEXT, difficulty_level INTEGER)"
+    "CREATE TABLE crossfit (exercise TEXT, difficulty_level INTEGER);"
   )
-  adbcdrivermanager::adbc_statement_execute_query(stmt)$release()
+  adbcdrivermanager::adbc_statement_execute_query(stmt)
   adbcdrivermanager::adbc_statement_release(stmt)
 
+  # If we get this far, remove the table and disconnect when the test is done
+  on.exit({
+    stmt <- adbcdrivermanager::adbc_statement_init(con)
+    adbcdrivermanager::adbc_statement_set_sql_query(
+      stmt,
+      "DROP TABLE IF EXISTS crossfit;"
+    )
+    adbcdrivermanager::adbc_statement_execute_query(stmt)
+    adbcdrivermanager::adbc_statement_release(stmt)
+
+    adbcdrivermanager::adbc_connection_release(con)
+    adbcdrivermanager::adbc_database_release(db)
+  })
+
   stmt <- adbcdrivermanager::adbc_statement_init(con)
   adbcdrivermanager::adbc_statement_set_sql_query(
     stmt,
@@ -45,25 +65,25 @@ test_that("default options can open a database and execute a query", {
       ('Push Jerk', 7),
       ('Bar Muscle Up', 10);"
   )
-  adbcdrivermanager::adbc_statement_execute_query(stmt)$release()
+  adbcdrivermanager::adbc_statement_execute_query(stmt)
   adbcdrivermanager::adbc_statement_release(stmt)
 
   stmt <- adbcdrivermanager::adbc_statement_init(con)
   adbcdrivermanager::adbc_statement_set_sql_query(
     stmt,
-    "SELECT * from crossfit"
+    "SELECT * from crossfit ORDER BY difficulty_level"
   )
 
+  stream <- nanoarrow::nanoarrow_allocate_array_stream()
+  adbcdrivermanager::adbc_statement_execute_query(stmt, stream)
   expect_identical(
-    as.data.frame(adbcdrivermanager::adbc_statement_execute_query(stmt)),
+    as.data.frame(stream),
     data.frame(
       exercise = c("Push Ups", "Pull Ups", "Push Jerk", "Bar Muscle Up"),
-      difficulty_level = c(3, 5, 7, 10),
+      difficulty_level = c(3L, 5L, 7L, 10L),
       stringsAsFactors = FALSE
     )
   )
 
   adbcdrivermanager::adbc_statement_release(stmt)
-  adbcdrivermanager::adbc_connection_release(con)
-  adbcdrivermanager::adbc_database_release(db)
 })
diff --git a/r/adbcpostgresql/tools/test.c b/r/adbcpostgresql/tools/test.c
new file mode 100644
index 0000000..aacd816
--- /dev/null
+++ b/r/adbcpostgresql/tools/test.c
@@ -0,0 +1,21 @@
+// 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.
+
+#include <libpq-fe.h>
+#include <stdio.h>
+
+void print_libpq_runtime_version(void) { printf("%d", PQlibVersion()); }
diff --git a/r/adbcsqlite/DESCRIPTION b/r/adbcsqlite/DESCRIPTION
index ca4400d..b555045 100644
--- a/r/adbcsqlite/DESCRIPTION
+++ b/r/adbcsqlite/DESCRIPTION
@@ -14,6 +14,7 @@ Roxygen: list(markdown = TRUE)
 RoxygenNote: 7.2.3
 SystemRequirements: SQLite3
 Suggests:
+    nanoarrow,
     testthat (>= 3.0.0)
 Config/testthat/edition: 3
 Config/build/bootstrap: TRUE
diff --git a/r/adbcsqlite/README.Rmd b/r/adbcsqlite/README.Rmd
index 2d9488a..6443efb 100644
--- a/r/adbcsqlite/README.Rmd
+++ b/r/adbcsqlite/README.Rmd
@@ -71,7 +71,9 @@ adbc_statement_release(stmt)
 # Query it
 stmt <- adbc_statement_init(con)
 adbc_statement_set_sql_query(stmt, "SELECT * from flights")
-result <- tibble::as_tibble(adbc_statement_execute_query(stmt))
+stream <- nanoarrow::nanoarrow_allocate_array_stream()
+adbc_statement_execute_query(stmt, stream)
+result <- tibble::as_tibble(stream)
 adbc_statement_release(stmt)
 
 result
diff --git a/r/adbcsqlite/README.md b/r/adbcsqlite/README.md
index 68b7151..59a974d 100644
--- a/r/adbcsqlite/README.md
+++ b/r/adbcsqlite/README.md
@@ -54,13 +54,16 @@ flights$time_hour <- NULL
 stmt <- adbc_statement_init(con, adbc.ingest.target_table = "flights")
 adbc_statement_bind(stmt, flights)
 adbc_statement_execute_query(stmt)
-#> <nanoarrow_array_stream[invalid pointer]>
+#> [1] 336776
 adbc_statement_release(stmt)
 
 # Query it
 stmt <- adbc_statement_init(con)
 adbc_statement_set_sql_query(stmt, "SELECT * from flights")
-result <- tibble::as_tibble(adbc_statement_execute_query(stmt))
+stream <- nanoarrow::nanoarrow_allocate_array_stream()
+adbc_statement_execute_query(stmt, stream)
+#> [1] -1
+result <- tibble::as_tibble(stream)
 adbc_statement_release(stmt)
 
 result
diff --git a/r/adbcsqlite/tests/testthat/test-adbcsqlite-package.R b/r/adbcsqlite/tests/testthat/test-adbcsqlite-package.R
index 3a211ec..0c8a541 100644
--- a/r/adbcsqlite/tests/testthat/test-adbcsqlite-package.R
+++ b/r/adbcsqlite/tests/testthat/test-adbcsqlite-package.R
@@ -33,7 +33,7 @@ test_that("default options can open a database and execute a query", {
     stmt,
     "CREATE TABLE crossfit (exercise TEXT, difficulty_level INTEGER)"
   )
-  adbcdrivermanager::adbc_statement_execute_query(stmt)$release()
+  adbcdrivermanager::adbc_statement_execute_query(stmt)
   adbcdrivermanager::adbc_statement_release(stmt)
 
   stmt <- adbcdrivermanager::adbc_statement_init(con)
@@ -45,7 +45,7 @@ test_that("default options can open a database and execute a query", {
       ('Push Jerk', 7),
       ('Bar Muscle Up', 10);"
   )
-  adbcdrivermanager::adbc_statement_execute_query(stmt)$release()
+  adbcdrivermanager::adbc_statement_execute_query(stmt)
   adbcdrivermanager::adbc_statement_release(stmt)
 
   stmt <- adbcdrivermanager::adbc_statement_init(con)
@@ -54,8 +54,11 @@ test_that("default options can open a database and execute a query", {
     "SELECT * from crossfit"
   )
 
+  stream <- nanoarrow::nanoarrow_allocate_array_stream()
+  adbcdrivermanager::adbc_statement_execute_query(stmt, stream)
+
   expect_identical(
-    as.data.frame(adbcdrivermanager::adbc_statement_execute_query(stmt)),
+    as.data.frame(stream),
     data.frame(
       exercise = c("Push Ups", "Pull Ups", "Push Jerk", "Bar Muscle Up"),
       difficulty_level = c(3, 5, 7, 10),