This is an automated email from the ASF dual-hosted git repository.

jiayu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/sedona-db.git


The following commit(s) were added to refs/heads/main by this push:
     new e2b3627  feat(r/sedonadb): Add R bindings (#23)
e2b3627 is described below

commit e2b3627ac4c21a96be625b456586d44fecd18fd4
Author: Dewey Dunnington <[email protected]>
AuthorDate: Wed Sep 10 21:28:42 2025 +0000

    feat(r/sedonadb): Add R bindings (#23)
    
    * import files
    
    * update static lib name
    
    * pre-commit
    
    * first pass at licensing
    
    * license R files
    
    * license the rust folder
    
    * formatting
    
    * maybe with ci job
    
    * try macos
    
    * fix compile
    
    * fix readme
    
    * pre-commit
    
    * Update r/sedonadb/src/rust/src/lib.rs
    
    Co-authored-by: Copilot <[email protected]>
    
    * remove wraper code
    
    ---------
    
    Co-authored-by: Copilot <[email protected]>
---
 .github/workflows/r.yml                            |  94 ++++++++
 .gitignore                                         |   3 +
 Cargo.lock                                         |  60 +++++
 Cargo.toml                                         |   1 +
 ci/scripts/rat/license_ignore.txt                  |   6 +
 r/sedonadb/.Rbuildignore                           |  12 +
 .gitignore => r/sedonadb/.gitignore                |  35 +--
 r/sedonadb/DESCRIPTION                             |  23 ++
 r/sedonadb/LICENSE.md                              | 194 ++++++++++++++++
 r/sedonadb/NAMESPACE                               |  33 +++
 r/sedonadb/R/000-wrappers.R                        | 219 ++++++++++++++++++
 .gitignore => r/sedonadb/R/adbc.R                  |  48 ++--
 r/sedonadb/R/context.R                             |  95 ++++++++
 r/sedonadb/R/dataframe.R                           | 249 +++++++++++++++++++++
 .gitignore => r/sedonadb/R/pkg-sf.R                |  48 ++--
 .gitignore => r/sedonadb/R/sedonadb-package.R      |  33 +--
 r/sedonadb/R/zzz.R                                 | 115 ++++++++++
 r/sedonadb/README.Rmd                              |  88 ++++++++
 r/sedonadb/README.md                               | 120 ++++++++++
 .gitignore => r/sedonadb/cleanup                   |  29 +--
 .gitignore => r/sedonadb/cleanup.win               |  29 +--
 r/sedonadb/configure                               |  91 ++++++++
 r/sedonadb/configure.win                           |  60 +++++
 .../inst/files/natural-earth_cities_geo.parquet    | Bin 0 -> 17322 bytes
 .../natural-earth_countries-geography_geo.parquet  | Bin 0 -> 181446 bytes
 .../inst/files/natural-earth_countries_geo.parquet | Bin 0 -> 186590 bytes
 r/sedonadb/man/as_sedonadb_dataframe.Rd            |  25 +++
 .../man/figures/README-unnamed-chunk-3-1.png       | Bin 0 -> 344965 bytes
 r/sedonadb/man/sd_compute.Rd                       |  29 +++
 r/sedonadb/man/sd_count.Rd                         |  21 ++
 r/sedonadb/man/sd_drop_view.Rd                     |  27 +++
 r/sedonadb/man/sd_preview.Rd                       |  32 +++
 r/sedonadb/man/sd_read_parquet.Rd                  |  22 ++
 r/sedonadb/man/sd_sql.Rd                           |  21 ++
 r/sedonadb/man/sd_to_view.Rd                       |  27 +++
 r/sedonadb/man/sedonadb-package.Rd                 |  15 ++
 r/sedonadb/man/sedonadb_adbc.Rd                    |  26 +++
 r/sedonadb/sedonadb.Rproj                          |  22 ++
 .gitignore => r/sedonadb/src/.gitignore            |  33 +--
 r/sedonadb/src/Makevars.in                         |  60 +++++
 r/sedonadb/src/Makevars.win.in                     |  41 ++++
 r/sedonadb/src/init.c                              | 201 +++++++++++++++++
 r/sedonadb/src/rust/.cargo/config.toml             |  10 +
 .gitignore => r/sedonadb/src/rust/Cargo.toml       |  50 ++---
 r/sedonadb/src/rust/api.h                          |  46 ++++
 r/sedonadb/src/rust/src/context.rs                 | 124 ++++++++++
 r/sedonadb/src/rust/src/dataframe.rs               | 194 ++++++++++++++++
 r/sedonadb/src/rust/src/error.rs                   |  58 +++++
 r/sedonadb/src/rust/src/lib.rs                     |  42 ++++
 r/sedonadb/src/rust/src/runtime.rs                 | 153 +++++++++++++
 r/sedonadb/src/sedonadb-win.def                    |   2 +
 .gitignore => r/sedonadb/tests/testthat.R          |  37 +--
 .../sedonadb/tests/testthat/test-adbc.R            |  42 ++--
 .../sedonadb/tests/testthat/test-context.R         |  52 ++---
 r/sedonadb/tests/testthat/test-dataframe.R         | 137 ++++++++++++
 55 files changed, 2929 insertions(+), 305 deletions(-)

diff --git a/.github/workflows/r.yml b/.github/workflows/r.yml
new file mode 100644
index 0000000..3fdf161
--- /dev/null
+++ b/.github/workflows/r.yml
@@ -0,0 +1,94 @@
+# 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.
+
+name: r
+
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    branches:
+      - main
+    paths:
+      - 'Cargo.toml'
+      - 'Cargo.lock'
+      - '.github/workflows/r.yml'
+      - 'rust/**'
+      - 'c/**'
+      - 'r/**'
+jobs:
+
+  test:
+    runs-on: ${{ matrix.config.os }}
+    name: ${{ matrix.config.os }} (${{ matrix.config.r }})
+
+    strategy:
+      fail-fast: false
+      matrix:
+        config:
+          - {os: ubuntu-latest, r: 'release'}
+          - {os: macos-latest, r: 'release'}
+
+    env:
+      GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }}
+      R_KEEP_PKG_SOURCE: yes
+
+    steps:
+      - uses: actions/checkout@v4
+      - uses: r-lib/actions/setup-pandoc@v2
+      - uses: r-lib/actions/setup-r@v2
+        with:
+          r-version: ${{ matrix.config.r }}
+          http-user-agent: ${{ matrix.config.http-user-agent }}
+          use-public-rspm: true
+
+      - name: Use stable Rust
+        id: rust
+        run: |
+          rustup toolchain install stable --no-self-update
+          rustup default stable
+
+      - name: Install dependencies (Linux)
+        if: matrix.config.os == 'ubuntu-latest'
+        run: sudo apt-get update && sudo apt-get install -y libgeos-dev
+
+      - name: Install dependencies (MacOS)
+        if: matrix.config.os == 'macos-latest'
+        run: brew install geos
+
+      - uses: Swatinem/rust-cache@v2
+        with:
+          # Update this key to force a new cache
+          prefix-key: "r-v1"
+
+      - name: Install build dependencies
+        run: |
+            R -e 'install.packages(c("nanoarrow", "geoarrow"))'
+
+      - name: Install R Package
+        run: |
+          R CMD INSTALL r/sedonadb --preclean
+
+      - uses: r-lib/actions/setup-r-dependencies@v2
+        with:
+          needs: check
+          working-directory: r/sedonadb
+
+      - name: Test R Package
+        run: |
+            R -e 'library(testthat); test_local("r/sedonadb", 
MultiReporter$new(list(CheckReporter$new(), LocationReporter$new())))'
diff --git a/.gitignore b/.gitignore
index 7441737..3ef7d21 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,6 @@ site/
 
 # Python cache files
 __pycache__
+
+# R-related files
+.Rproj.user
diff --git a/Cargo.lock b/Cargo.lock
index 21d1617..5a975d9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4694,6 +4694,47 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "savvy"
+version = "0.8.13"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "9b0b77a8477c7edc3166bdebc6f8fc449967942aa3d25f22b13a336df29da3e2"
+dependencies = [
+ "cc",
+ "rustversion",
+ "savvy-ffi",
+ "savvy-macro",
+]
+
+[[package]]
+name = "savvy-bindgen"
+version = "0.8.13"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "a29c423a6e85d4d7c4172db33ace022acea97b2c6fded04d28da50a14591c87d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.106",
+]
+
+[[package]]
+name = "savvy-ffi"
+version = "0.8.13"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "e06cec12411cc3a0cc7eaa13601c431f770cbc89825576fea8aebc369b3ed41c"
+
+[[package]]
+name = "savvy-macro"
+version = "0.8.13"
+source = "registry+https://github.com/rust-lang/crates.io-index";
+checksum = "df4c38f496dd2a8f5aac0bdff321e88f59d241fc4cdf1b75f4859a69134c020f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "savvy-bindgen",
+ "syn 2.0.106",
+]
+
 [[package]]
 name = "schannel"
 version = "0.1.28"
@@ -5141,6 +5182,25 @@ dependencies = [
  "tokio",
 ]
 
+[[package]]
+name = "sedonadbr"
+version = "0.1.0"
+dependencies = [
+ "arrow-array",
+ "arrow-schema",
+ "datafusion",
+ "datafusion-common",
+ "savvy",
+ "savvy-ffi",
+ "sedona",
+ "sedona-adbc",
+ "sedona-expr",
+ "sedona-geoparquet",
+ "sedona-schema",
+ "thiserror 2.0.16",
+ "tokio",
+]
+
 [[package]]
 name = "semver"
 version = "1.0.26"
diff --git a/Cargo.toml b/Cargo.toml
index 4797430..0a8e803 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -21,6 +21,7 @@ members = [
     "c/sedona-proj",
     "c/sedona-s2geography",
     "c/sedona-tg",
+    "r/sedonadb/src/rust",
     "rust/sedona-adbc",
     "rust/sedona-expr",
     "rust/sedona-functions",
diff --git a/ci/scripts/rat/license_ignore.txt 
b/ci/scripts/rat/license_ignore.txt
index a3b26d0..daf8efe 100644
--- a/ci/scripts/rat/license_ignore.txt
+++ b/ci/scripts/rat/license_ignore.txt
@@ -1,5 +1,8 @@
+.Rbuildignore
 *.ipynb
 *.json
+*.Rd
+*.Rproj
 *.svg
 c/sedona-geoarrow-c/src/geoarrow/*
 c/sedona-geoarrow-c/src/geoarrow/ryu/*
@@ -9,5 +12,8 @@ c/sedona-s2geography/s2geometry/*
 c/sedona-tg/src/tg/*
 Cargo.lock
 ci/scripts/rat/license_*.txt
+DESCRIPTION
 LICENSE
+NAMESPACE
+sedonadb-win.def
 submodules/geoarrow-data/*
diff --git a/r/sedonadb/.Rbuildignore b/r/sedonadb/.Rbuildignore
new file mode 100644
index 0000000..7e7df63
--- /dev/null
+++ b/r/sedonadb/.Rbuildignore
@@ -0,0 +1,12 @@
+^sedonadb\.Rproj$
+^\.Rproj\.user$
+^LICENSE\.md$
+
+^src/rust/\.cargo$
+^src/rust/target$
+^src/Makevars$
+^src/Makevars\.win$
+^\.cache$
+^compile_commands\.json$
+^\.vscode$
+^README\.Rmd$
diff --git a/.gitignore b/r/sedonadb/.gitignore
similarity index 70%
copy from .gitignore
copy to r/sedonadb/.gitignore
index 7441737..a440e54 100644
--- a/.gitignore
+++ b/r/sedonadb/.gitignore
@@ -15,31 +15,12 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# MacOS
+.Rproj.user
+.Rhistory
+.Rdata
+.httr-oauth
 .DS_Store
-
-# sedona-cli
-.history
-
-# Generated by cargo
-debug/
-target/
-
-# Generated by rustfmt
-**/*.rs.bk
-
-# Generated by cargo llvm-cov
-coverage/
-
-# MSVC Windows builds of rustc generate these, which store debugging 
information
-*.pdb
-
-# IDE-specific settings
-.idea/
-.vscode/
-
-# documentation
-site/
-
-# Python cache files
-__pycache__
+.quarto
+.cache
+compile_commands.json
+.vscode
diff --git a/r/sedonadb/DESCRIPTION b/r/sedonadb/DESCRIPTION
new file mode 100644
index 0000000..5167391
--- /dev/null
+++ b/r/sedonadb/DESCRIPTION
@@ -0,0 +1,23 @@
+Package: sedonadb
+Title: Bindings for Apache SedonaDB
+Version: 0.0.0.9000
+Authors@R:
+    person("Dewey", "Dunnington", , "[email protected]", role = c("aut", 
"cre"))
+Description: Provides bindings for Apache SedonaDB, a lightweight
+    query engine optimized for spatial workflows.
+License: Apache License (>= 2)
+Encoding: UTF-8
+Roxygen: list(markdown = TRUE)
+RoxygenNote: 7.3.2
+SystemRequirements: Cargo (Rust's package manager), rustc
+Depends: R (>= 4.1.0)
+Suggests:
+    adbcdrivermanager,
+    rlang,
+    testthat (>= 3.0.0),
+    withr,
+    wk
+Config/testthat/edition: 3
+Imports:
+    geoarrow,
+    nanoarrow
diff --git a/r/sedonadb/LICENSE.md b/r/sedonadb/LICENSE.md
new file mode 100644
index 0000000..b62a9b5
--- /dev/null
+++ b/r/sedonadb/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/sedonadb/NAMESPACE b/r/sedonadb/NAMESPACE
new file mode 100644
index 0000000..0be588e
--- /dev/null
+++ b/r/sedonadb/NAMESPACE
@@ -0,0 +1,33 @@
+# Generated by roxygen2: do not edit by hand
+
+S3method("$<-",savvy_sedonadb__sealed)
+S3method("[[<-",savvy_sedonadb__sealed)
+S3method(as.data.frame,sedonadb_dataframe)
+S3method(as_nanoarrow_array_stream,sedonadb_dataframe)
+S3method(as_sedonadb_dataframe,data.frame)
+S3method(as_sedonadb_dataframe,nanoarrow_array)
+S3method(as_sedonadb_dataframe,nanoarrow_array_stream)
+S3method(as_sedonadb_dataframe,sedonadb_dataframe)
+S3method(as_sedonadb_dataframe,sf)
+S3method(dim,sedonadb_dataframe)
+S3method(dimnames,sedonadb_dataframe)
+S3method(head,sedonadb_dataframe)
+S3method(infer_nanoarrow_schema,sedonadb_dataframe)
+S3method(print,InternalContext__bundle)
+S3method(print,InternalDataFrame__bundle)
+S3method(print,sedonadb_dataframe)
+export(as_sedonadb_dataframe)
+export(sd_collect)
+export(sd_compute)
+export(sd_count)
+export(sd_drop_view)
+export(sd_preview)
+export(sd_read_parquet)
+export(sd_sql)
+export(sd_to_view)
+export(sd_view)
+export(sedonadb_adbc)
+importFrom(nanoarrow,as_nanoarrow_array_stream)
+importFrom(nanoarrow,infer_nanoarrow_schema)
+importFrom(utils,head)
+useDynLib(sedonadb, .registration = TRUE)
diff --git a/r/sedonadb/R/000-wrappers.R b/r/sedonadb/R/000-wrappers.R
new file mode 100644
index 0000000..a375d29
--- /dev/null
+++ b/r/sedonadb/R/000-wrappers.R
@@ -0,0 +1,219 @@
+# 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.
+
+# Generated by savvy: do not edit by hand
+#
+# Note:
+#   This wrapper file is named as `000-wrappers.R` so that this file is loaded
+#   first, which allows users to override the functions defined here (e.g., a
+#   print() method for an enum).
+
+#' @useDynLib sedonadb, .registration = TRUE
+#' @keywords internal
+NULL
+
+# Check class and extract the external pointer embedded in the environment
+.savvy_extract_ptr <- function(e, class) {
+  if(is.null(e)) {
+    return(NULL)
+  }
+
+  if(inherits(e, class)) {
+    e$.ptr
+  } else {
+    msg <- paste0("Expected ", class, ", got ", class(e)[1])
+    stop(msg, call. = FALSE)
+  }
+}
+
+# Prohibit modifying environments
+
+#' @export
+`$<-.savvy_sedonadb__sealed` <- function(x, name, value) {
+  class <- gsub("__bundle$", "", class(x)[1])
+  stop(class, " cannot be modified", call. = FALSE)
+}
+
+#' @export
+`[[<-.savvy_sedonadb__sealed` <- function(x, i, value) {
+  class <- gsub("__bundle$", "", class(x)[1])
+  stop(class, " cannot be modified", call. = FALSE)
+}
+
+
+`init_r_runtime_interrupts` <- function(`interrupts_call`, `pkg_env`) {
+  invisible(.Call(savvy_init_r_runtime_interrupts__impl, `interrupts_call`, 
`pkg_env`))
+}
+
+
+`sedonadb_adbc_init_func` <- function() {
+  .Call(savvy_sedonadb_adbc_init_func__impl)
+}
+
+### wrapper functions for InternalContext
+
+`InternalContext_data_frame_from_array_stream` <- function(self) {
+  function(`stream_xptr`, `collect_now`) {
+    
.savvy_wrap_InternalDataFrame(.Call(savvy_InternalContext_data_frame_from_array_stream__impl,
 `self`, `stream_xptr`, `collect_now`))
+  }
+}
+
+`InternalContext_deregister_table` <- function(self) {
+  function(`table_ref`) {
+    invisible(.Call(savvy_InternalContext_deregister_table__impl, `self`, 
`table_ref`))
+  }
+}
+
+`InternalContext_read_parquet` <- function(self) {
+  function(`paths`) {
+    
.savvy_wrap_InternalDataFrame(.Call(savvy_InternalContext_read_parquet__impl, 
`self`, `paths`))
+  }
+}
+
+`InternalContext_sql` <- function(self) {
+  function(`query`) {
+    .savvy_wrap_InternalDataFrame(.Call(savvy_InternalContext_sql__impl, 
`self`, `query`))
+  }
+}
+
+`InternalContext_view` <- function(self) {
+  function(`table_ref`) {
+    .savvy_wrap_InternalDataFrame(.Call(savvy_InternalContext_view__impl, 
`self`, `table_ref`))
+  }
+}
+
+`.savvy_wrap_InternalContext` <- function(ptr) {
+  e <- new.env(parent = emptyenv())
+  e$.ptr <- ptr
+  e$`data_frame_from_array_stream` <- 
`InternalContext_data_frame_from_array_stream`(ptr)
+  e$`deregister_table` <- `InternalContext_deregister_table`(ptr)
+  e$`read_parquet` <- `InternalContext_read_parquet`(ptr)
+  e$`sql` <- `InternalContext_sql`(ptr)
+  e$`view` <- `InternalContext_view`(ptr)
+
+  class(e) <- c("InternalContext", "savvy_sedonadb__sealed")
+  e
+}
+
+
+
+`InternalContext` <- new.env(parent = emptyenv())
+
+### associated functions for InternalContext
+
+`InternalContext`$`new` <- function() {
+  .savvy_wrap_InternalContext(.Call(savvy_InternalContext_new__impl))
+}
+
+
+class(`InternalContext`) <- c("InternalContext__bundle", 
"savvy_sedonadb__sealed")
+
+#' @export
+`print.InternalContext__bundle` <- function(x, ...) {
+  cat('InternalContext\n')
+}
+
+### wrapper functions for InternalDataFrame
+
+`InternalDataFrame_collect` <- function(self) {
+  function(`out`) {
+    .Call(savvy_InternalDataFrame_collect__impl, `self`, `out`)
+  }
+}
+
+`InternalDataFrame_compute` <- function(self) {
+  function(`ctx`) {
+    `ctx` <- .savvy_extract_ptr(`ctx`, "InternalContext")
+    .savvy_wrap_InternalDataFrame(.Call(savvy_InternalDataFrame_compute__impl, 
`self`, `ctx`))
+  }
+}
+
+`InternalDataFrame_count` <- function(self) {
+  function() {
+    .Call(savvy_InternalDataFrame_count__impl, `self`)
+  }
+}
+
+`InternalDataFrame_limit` <- function(self) {
+  function(`n`) {
+    .savvy_wrap_InternalDataFrame(.Call(savvy_InternalDataFrame_limit__impl, 
`self`, `n`))
+  }
+}
+
+`InternalDataFrame_primary_geometry_column_index` <- function(self) {
+  function() {
+    .Call(savvy_InternalDataFrame_primary_geometry_column_index__impl, `self`)
+  }
+}
+
+`InternalDataFrame_show` <- function(self) {
+  function(`ctx`, `width_chars`, `ascii`, `limit` = NULL) {
+    `ctx` <- .savvy_extract_ptr(`ctx`, "InternalContext")
+    .Call(savvy_InternalDataFrame_show__impl, `self`, `ctx`, `width_chars`, 
`ascii`, `limit`)
+  }
+}
+
+`InternalDataFrame_to_arrow_schema` <- function(self) {
+  function(`out`) {
+    invisible(.Call(savvy_InternalDataFrame_to_arrow_schema__impl, `self`, 
`out`))
+  }
+}
+
+`InternalDataFrame_to_arrow_stream` <- function(self) {
+  function(`out`) {
+    invisible(.Call(savvy_InternalDataFrame_to_arrow_stream__impl, `self`, 
`out`))
+  }
+}
+
+`InternalDataFrame_to_view` <- function(self) {
+  function(`ctx`, `table_ref`, `overwrite`) {
+    `ctx` <- .savvy_extract_ptr(`ctx`, "InternalContext")
+    invisible(.Call(savvy_InternalDataFrame_to_view__impl, `self`, `ctx`, 
`table_ref`, `overwrite`))
+  }
+}
+
+`.savvy_wrap_InternalDataFrame` <- function(ptr) {
+  e <- new.env(parent = emptyenv())
+  e$.ptr <- ptr
+  e$`collect` <- `InternalDataFrame_collect`(ptr)
+  e$`compute` <- `InternalDataFrame_compute`(ptr)
+  e$`count` <- `InternalDataFrame_count`(ptr)
+  e$`limit` <- `InternalDataFrame_limit`(ptr)
+  e$`primary_geometry_column_index` <- 
`InternalDataFrame_primary_geometry_column_index`(ptr)
+  e$`show` <- `InternalDataFrame_show`(ptr)
+  e$`to_arrow_schema` <- `InternalDataFrame_to_arrow_schema`(ptr)
+  e$`to_arrow_stream` <- `InternalDataFrame_to_arrow_stream`(ptr)
+  e$`to_view` <- `InternalDataFrame_to_view`(ptr)
+
+  class(e) <- c("InternalDataFrame", "savvy_sedonadb__sealed")
+  e
+}
+
+
+
+`InternalDataFrame` <- new.env(parent = emptyenv())
+
+### associated functions for InternalDataFrame
+
+
+
+class(`InternalDataFrame`) <- c("InternalDataFrame__bundle", 
"savvy_sedonadb__sealed")
+
+#' @export
+`print.InternalDataFrame__bundle` <- function(x, ...) {
+  cat('InternalDataFrame\n')
+}
diff --git a/.gitignore b/r/sedonadb/R/adbc.R
similarity index 58%
copy from .gitignore
copy to r/sedonadb/R/adbc.R
index 7441737..6f2157b 100644
--- a/.gitignore
+++ b/r/sedonadb/R/adbc.R
@@ -15,31 +15,23 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# MacOS
-.DS_Store
-
-# sedona-cli
-.history
-
-# Generated by cargo
-debug/
-target/
-
-# Generated by rustfmt
-**/*.rs.bk
-
-# Generated by cargo llvm-cov
-coverage/
-
-# MSVC Windows builds of rustc generate these, which store debugging 
information
-*.pdb
-
-# IDE-specific settings
-.idea/
-.vscode/
-
-# documentation
-site/
-
-# Python cache files
-__pycache__
+#' SedonaDB ADBC Driver
+#'
+#' @returns An [adbcdrivermanager::adbc_driver()] of class
+#'   'sedonadb_driver_sedonadb'
+#' @export
+#'
+#' @examples
+#' library(adbcdrivermanager)
+#'
+#' con <- sedonadb_adbc() |>
+#'   adbc_database_init() |>
+#'   adbc_connection_init()
+#' con |>
+#'   read_adbc("SELECT ST_Point(0, 1) as geometry") |>
+#'   as.data.frame()
+#'
+sedonadb_adbc <- function() {
+    init_func <- structure(sedonadb_adbc_init_func(), class = 
"adbc_driver_init_func")
+    adbcdrivermanager::adbc_driver(init_func, subclass = 
"sedonadb_driver_sedonadb")
+}
diff --git a/r/sedonadb/R/context.R b/r/sedonadb/R/context.R
new file mode 100644
index 0000000..2b6342b
--- /dev/null
+++ b/r/sedonadb/R/context.R
@@ -0,0 +1,95 @@
+# 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.
+
+#' Create a DataFrame from one or more Parquet files
+#'
+#' The query will only be executed when requested.
+#'
+#' @param path One or more paths or URIs to Parquet files
+#'
+#' @returns A sedonadb_dataframe
+#' @export
+#'
+#' @examples
+#' path <- system.file("files/natural-earth_cities_geo.parquet", package = 
"sedonadb")
+#' sd_read_parquet(path) |> head(5) |> sd_preview()
+#'
+sd_read_parquet <- function(path) {
+  ctx <- ctx()
+  df <- ctx$read_parquet(path)
+  new_sedonadb_dataframe(ctx, df)
+}
+
+#' Create a DataFrame from SQL
+#'
+#' The query will only be executed when requested.
+#'
+#' @param sql A SQL string to execute
+#'
+#' @returns A sedonadb_dataframe
+#' @export
+#'
+#' @examples
+#' sd_sql("SELECT ST_Point(0, 1) as geom") |> sd_preview()
+#'
+sd_sql <- function(sql) {
+  ctx <- ctx()
+  df <- ctx$sql(sql)
+  new_sedonadb_dataframe(ctx, df)
+}
+
+#' Create or Drop a named view
+#'
+#' Remove a view created with [sd_to_view()] from the context.
+#'
+#' @param table_ref The name of the view reference
+#' @returns The context, invisibly
+#' @export
+#'
+#' @examples
+#' sd_sql("SELECT 1 as one") |> sd_to_view("foofy")
+#' sd_view("foofy")
+#' sd_drop_view("foofy")
+#' try(sd_view("foofy"))
+#'
+sd_drop_view <- function(table_ref) {
+  ctx <- ctx()
+  ctx$deregister_table(table_ref)
+  invisible(ctx)
+}
+
+#' @rdname sd_drop_view
+#' @export
+sd_view <- function(table_ref) {
+  ctx <- ctx()
+  df <- ctx$view(table_ref)
+  new_sedonadb_dataframe(ctx, df)
+}
+
+# We use just one context for now. In theory we could support multiple
+# contexts with a shared runtime, which would scope the registration
+# of various components more cleanly from the runtime.
+ctx <- function() {
+  if (is.null(global_ctx$ctx)) {
+    global_ctx$ctx <- InternalContext$new()
+  }
+
+  global_ctx$ctx
+}
+
+global_ctx <- new.env(parent = emptyenv())
+global_ctx$ctx <- NULL
diff --git a/r/sedonadb/R/dataframe.R b/r/sedonadb/R/dataframe.R
new file mode 100644
index 0000000..321961c
--- /dev/null
+++ b/r/sedonadb/R/dataframe.R
@@ -0,0 +1,249 @@
+# 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.
+
+#' Convert an object to a DataFrame
+#'
+#' @param x An object to convert
+#' @param ... Extra arguments passed to/from methods
+#' @param schema The requested schema
+#'
+#' @returns A sedonadb_dataframe
+#' @export
+#'
+#' @examples
+#' as_sedonadb_dataframe(data.frame(x = 1:3))
+#'
+as_sedonadb_dataframe <- function(x, ..., schema = NULL) {
+  UseMethod("as_sedonadb_dataframe")
+}
+
+#' @export
+as_sedonadb_dataframe.sedonadb_dataframe <- function(x, ..., schema = NULL) {
+  # In the future, schema can be handled with a cast
+  x
+}
+
+#' @export
+as_sedonadb_dataframe.data.frame <- function(x, ..., schema = NULL) {
+  array <- nanoarrow::as_nanoarrow_array(x, schema = schema)
+  stream <- nanoarrow::basic_array_stream(list(array))
+  ctx <- ctx()
+  df <- ctx$data_frame_from_array_stream(stream, collect_now = TRUE)
+  new_sedonadb_dataframe(ctx, df)
+}
+
+#' @export
+as_sedonadb_dataframe.nanoarrow_array <- function(x, ..., schema = NULL) {
+  stream <- nanoarrow::as_nanoarrow_array_stream(x, schema = schema)
+  ctx <- ctx()
+  df <- ctx$data_frame_from_array_stream(stream, collect_now = TRUE)
+
+  # Verify schema is handled
+  as_sedonadb_dataframe(new_sedonadb_dataframe(ctx, df), schema = schema)
+}
+
+#' @export
+as_sedonadb_dataframe.nanoarrow_array_stream <- function(x, ..., schema = NULL,
+                                                         lazy = TRUE) {
+  stream <- nanoarrow::as_nanoarrow_array_stream(x, schema = schema)
+  ctx <- ctx()
+  df <- ctx$data_frame_from_array_stream(stream, collect_now = !lazy)
+
+  # Verify schema is handled
+  as_sedonadb_dataframe(new_sedonadb_dataframe(ctx, df), schema = schema)
+}
+
+#' Count rows in a DataFrame
+#'
+#' @param .data A sedonadb_dataframe
+#'
+#' @returns The number of rows after executing the query
+#' @export
+#'
+#' @examples
+#' sd_sql("SELECT 1 as one") |> sd_count()
+#'
+sd_count <- function(.data) {
+  .data$df$count()
+}
+
+#' Register a DataFrame as a named view
+#'
+#' This is useful for creating a view that can be referenced in a SQL
+#' statement. Use [sd_drop_view()] to remove it.
+#'
+#' @inheritParams sd_count
+#' @inheritParams sd_drop_view
+#' @param overwrite Use TRUE to overwrite a view with the same name (if it 
exists)
+#'
+#' @returns .data, invisibly
+#' @export
+#'
+#' @examples
+#' sd_sql("SELECT 1 as one") |> sd_to_view("foofy")
+#' sd_sql("SELECT * FROM foofy")
+#'
+sd_to_view <- function(.data, table_ref, overwrite = FALSE) {
+  .data <- as_sedonadb_dataframe(.data)
+  .data$df$to_view(.data$ctx, table_ref, overwrite)
+  invisible(.data)
+}
+
+#' Collect a DataFrame into memory
+#'
+#' Use `sd_compute()` to collect and return the result as a DataFrame;
+#' use `sd_collect()` to collect and return the result as an R data.frame.
+#'
+#' @inheritParams sd_count
+#' @param ptype The target R object. See [nanoarrow::convert_array_stream].
+#'
+#' @returns `sd_compute()` returns a sedonadb_dataframe; `sd_collect()` returns
+#'   a data.frame (or subclass according to `ptype`).
+#' @export
+#'
+#' @examples
+#' sd_sql("SELECT 1 as one") |> sd_compute()
+#' sd_sql("SELECT 1 as one") |> sd_collect()
+#'
+sd_compute <- function(.data) {
+  .data <- as_sedonadb_dataframe(.data)
+  df <- .data$df$compute(.data$ctx)
+  new_sedonadb_dataframe(.data$ctx, df)
+}
+
+#' @export
+#' @rdname sd_compute
+sd_collect <- function(.data, ptype = NULL) {
+  .data <- as_sedonadb_dataframe(.data)
+  stream <- nanoarrow::nanoarrow_allocate_array_stream()
+  size <- .data$df$collect(stream)
+  nanoarrow::convert_array_stream(stream, size = size, to = ptype)
+}
+
+#' Preview and print the results of running a query
+#'
+#' This is used to implement `print()` for the sedonadb_dataframe or can
+#' be used to explicitly preview if `options(sedonadb.interactive = FALSE)`.
+#'
+#' @inheritParams sd_count
+#' @param n The number of rows to preview. Use `Inf` to preview all rows.
+#'   Defaults to `getOption("pillar.print_max")`.
+#' @param ascii Use `TRUE` to force ASCII table formatting or `FALSE` to force
+#'   unicode formatting. By default, use a heuristic to determine if the output
+#'   is unicode-friendly or the value of `getOption("cli.unicode")`.
+#' @param width The character width of the output. Defaults to
+#'   `getOption("width")`.
+#'
+#' @returns .data, invisibly
+#' @export
+#'
+#' @examples
+#' sd_sql("SELECT 1 as one") |> sd_preview()
+#'
+sd_preview <- function(.data, n = NULL, ascii = NULL, width = NULL) {
+  .data <- as_sedonadb_dataframe(.data)
+
+  if (is.null(width)) {
+    width <- getOption("width")
+  }
+
+  if (is.null(n)) {
+    n <- getOption("pillar.print_max", 6)
+  }
+
+  if (is.null(ascii)) {
+    ascii <- !is_utf8_output()
+  }
+
+  content <- .data$df$show(
+    .data$ctx,
+    width_chars = as.integer(width),
+    limit = as.double(n),
+    ascii = ascii
+  )
+
+  cat(content)
+  cat(paste0("Preview of up to ", n, " row(s)\n"))
+
+  invisible(.data)
+}
+
+
+new_sedonadb_dataframe <- function(ctx, internal_df) {
+  structure(list(ctx = ctx, df = internal_df), class = "sedonadb_dataframe")
+}
+
+#' @importFrom utils head
+#' @export
+head.sedonadb_dataframe <- function(x, n = 6L, ...) {
+  new_sedonadb_dataframe(x$ctx, x$df$limit(as.double(n)))
+}
+
+#' @export
+dimnames.sedonadb_dataframe <- function(x, ...) {
+  list(NULL, names(infer_nanoarrow_schema(x)$children))
+}
+
+#' @export
+dim.sedonadb_dataframe <- function(x, ...) {
+  c(NA_integer_, length(infer_nanoarrow_schema(x)$children))
+}
+
+#' @export
+as.data.frame.sedonadb_dataframe <- function(x, ...) {
+  stream <- nanoarrow::nanoarrow_allocate_array_stream()
+  size <- x$df$collect(stream)
+  nanoarrow::convert_array_stream(stream, size = size)
+}
+
+#' @importFrom nanoarrow infer_nanoarrow_schema
+#' @export
+infer_nanoarrow_schema.sedonadb_dataframe <- function(x, ...) {
+  schema <- nanoarrow::nanoarrow_allocate_schema()
+  x$df$to_arrow_schema(schema)
+  schema
+}
+
+#' @importFrom nanoarrow as_nanoarrow_array_stream
+#' @export
+as_nanoarrow_array_stream.sedonadb_dataframe <- function(x, ...) {
+  stream <- nanoarrow::nanoarrow_allocate_array_stream()
+  x$df$to_arrow_stream(stream)
+  stream
+}
+
+#' @export
+print.sedonadb_dataframe <- function(x, ..., width = NULL, n = NULL) {
+  if (isTRUE(getOption("sedonadb.interactive", TRUE))) {
+    sd_preview(x, n = n, width = width)
+  } else {
+    sd_preview(x, n = 0)
+    cat("Use options(sedonadb.interactive = TRUE) or use sd_preview() to 
print\n")
+  }
+
+  invisible(x)
+}
+
+# Borrowed from cli but without detecting LaTeX output.
+is_utf8_output <- function() {
+  opt <- getOption("cli.unicode", NULL)
+  if (!is.null(opt)) {
+    isTRUE(opt)
+  } else {
+    l10n_info()$`UTF-8`
+  }
+}
diff --git a/.gitignore b/r/sedonadb/R/pkg-sf.R
similarity index 57%
copy from .gitignore
copy to r/sedonadb/R/pkg-sf.R
index 7441737..d402457 100644
--- a/.gitignore
+++ b/r/sedonadb/R/pkg-sf.R
@@ -15,31 +15,23 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# MacOS
-.DS_Store
-
-# sedona-cli
-.history
-
-# Generated by cargo
-debug/
-target/
-
-# Generated by rustfmt
-**/*.rs.bk
-
-# Generated by cargo llvm-cov
-coverage/
-
-# MSVC Windows builds of rustc generate these, which store debugging 
information
-*.pdb
-
-# IDE-specific settings
-.idea/
-.vscode/
-
-# documentation
-site/
-
-# Python cache files
-__pycache__
+#' @export
+as_sedonadb_dataframe.sf <- function(x, ..., schema = NULL) {
+  stream <- nanoarrow::as_nanoarrow_array_stream(
+    x,
+    schema = schema,
+    geometry_schema = geoarrow::geoarrow_wkb()
+  )
+  ctx <- ctx()
+  df <- ctx$data_frame_from_array_stream(stream, collect_now = TRUE)
+
+  # Verify schema is handled
+  as_sedonadb_dataframe(new_sedonadb_dataframe(ctx, df), schema = schema)
+}
+
+# dynamically registered in zzz.R
+st_as_sf.sedonadb_dataframe <- function(x, ...) {
+  stream <- nanoarrow::nanoarrow_allocate_array_stream()
+  size <- x$df$collect(stream)
+  sf::st_as_sf(stream)
+}
diff --git a/.gitignore b/r/sedonadb/R/sedonadb-package.R
similarity index 69%
copy from .gitignore
copy to r/sedonadb/R/sedonadb-package.R
index 7441737..d8b6f3b 100644
--- a/.gitignore
+++ b/r/sedonadb/R/sedonadb-package.R
@@ -14,32 +14,9 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+#' @keywords internal
+"_PACKAGE"
 
-# MacOS
-.DS_Store
-
-# sedona-cli
-.history
-
-# Generated by cargo
-debug/
-target/
-
-# Generated by rustfmt
-**/*.rs.bk
-
-# Generated by cargo llvm-cov
-coverage/
-
-# MSVC Windows builds of rustc generate these, which store debugging 
information
-*.pdb
-
-# IDE-specific settings
-.idea/
-.vscode/
-
-# documentation
-site/
-
-# Python cache files
-__pycache__
+## usethis namespace: start
+## usethis namespace: end
+NULL
diff --git a/r/sedonadb/R/zzz.R b/r/sedonadb/R/zzz.R
new file mode 100644
index 0000000..f9e6d18
--- /dev/null
+++ b/r/sedonadb/R/zzz.R
@@ -0,0 +1,115 @@
+# 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.
+
+.onLoad <- function(...) {
+  # Load geoarrow to manage conversion of arrow results to/from spatial objects
+  requireNamespace("geoarrow", quietly = TRUE)
+
+  s3_register("sf::st_as_sf", "sedonadb_dataframe")
+
+  # Inject what we need to reduce the Rust code to a simple Rf_eval()
+  ns <- asNamespace("sedonadb")
+  call <- call("check_interrupts")
+  init_r_runtime_interrupts(call, ns)
+}
+
+# The function we call from Rust to check for interrupts. R checks for
+# interrupts automatically when evaluating regular R code and signals
+# an interrupt condition,
+check_interrupts <- function() {
+  tryCatch({
+    FALSE
+  }, interrupt = function(...) TRUE, error = function(...) TRUE)
+}
+
+# Permissively licensed (unlicense) from
+# 
https://github.com/r-lib/vctrs/blob/b63a233bd8b8d6511cf3f7d9182c359a3b8c3cab/R/register-s3.R
+s3_register <- function(generic, class, method = NULL) {
+  stopifnot(is.character(generic), length(generic) == 1)
+  stopifnot(is.character(class), length(class) == 1)
+
+  pieces <- strsplit(generic, "::")[[1]]
+  stopifnot(length(pieces) == 2)
+  package <- pieces[[1]]
+  generic <- pieces[[2]]
+
+  caller <- parent.frame()
+
+  get_method_env <- function() {
+    top <- topenv(caller)
+    if (isNamespace(top)) {
+      asNamespace(environmentName(top))
+    } else {
+      caller
+    }
+  }
+  get_method <- function(method) {
+    if (is.null(method)) {
+      get(paste0(generic, ".", class), envir = get_method_env())
+    } else {
+      method
+    }
+  }
+
+  register <- function(...) {
+    envir <- asNamespace(package)
+
+    # Refresh the method each time, it might have been updated by
+    # `devtools::load_all()`
+    method_fn <- get_method(method)
+    stopifnot(is.function(method_fn))
+
+
+    # Only register if generic can be accessed
+    if (exists(generic, envir)) {
+      registerS3method(generic, class, method_fn, envir = envir)
+    } else if (identical(Sys.getenv("NOT_CRAN"), "true")) {
+      warn <- .rlang_s3_register_compat("warn")
+
+      warn(c(
+        sprintf(
+          "Can't find generic `%s` in package %s to register S3 method.",
+          generic,
+          package
+        ),
+        "i" = "This message is only shown to developers using devtools.",
+        "i" = sprintf("Do you need to update %s to the latest version?", 
package)
+      ))
+    }
+  }
+
+  # Always register hook in case package is later unloaded & reloaded
+  setHook(packageEvent(package, "onLoad"), function(...) {
+    register()
+  })
+
+  # For compatibility with R < 4.0 where base isn't locked
+  is_sealed <- function(pkg) {
+    identical(pkg, "base") || environmentIsLocked(asNamespace(pkg))
+  }
+
+  # Avoid registration failures during loading (pkgload or regular).
+  # Check that environment is locked because the registering package
+  # might be a dependency of the package that exports the generic. In
+  # that case, the exports (and the generic) might not be populated
+  # yet (#1225).
+  if (isNamespaceLoaded(package) && is_sealed(package)) {
+    register()
+  }
+
+  invisible()
+}
diff --git a/r/sedonadb/README.Rmd b/r/sedonadb/README.Rmd
new file mode 100644
index 0000000..5b9a784
--- /dev/null
+++ b/r/sedonadb/README.Rmd
@@ -0,0 +1,88 @@
+---
+output: github_document
+---
+
+<!---
+  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.
+-->
+
+```{r, include = FALSE}
+knitr::opts_chunk$set(
+  collapse = TRUE,
+  comment = "#>",
+  fig.path = "man/figures/README-",
+  out.width = "100%",
+  dpi = 300
+)
+```
+
+# sedonadb
+
+<!-- badges: start -->
+<!-- badges: end -->
+
+The goal of sedonadb is to provide an R interface to [Apache 
SedonaDB](https://sedona.apache.org/sedonadb). SedonaDB provides a 
[DataFusion](https://datafusion.apache.org)-powered single-node engine with a 
wide range of spatial capabilities built in, including spatial SQL with high 
function coverage and GeoParquet IO.
+
+## Installation
+
+You can install the development version of sedonadb from 
[GitHub](https://github.com/) with:
+
+``` shell
+git clone https://github.com/apache/sedona-db.git
+cd sedona-db/r/sedonadb
+R CMD INSTALL .
+```
+
+Installing a development version of sedonadb requires a [Rust 
compiler](https://rustup.rs) and a GEOS system dependency (e.g., `brew install 
geos` or `apt-get install libgeos-dev`). Install instructions for these 
dependencies on other platforms can be found on the [sf package 
homepage](https://r-spatial.github.io/sf).
+
+## Example
+
+You can use SedonaDB to read (Geo)Parquet files from anywhere. Filters are used
+to reduce the amount of data downloaded based on GeoParquet and/or Parquet
+metadata:
+
+```{r example}
+library(sedonadb)
+
+url <- 
"https://github.com/geoarrow/geoarrow-data/releases/download/v0.2.0/microsoft-buildings_point_geo.parquet";
+sd_read_parquet(url) |> sd_to_view("buildings", overwrite = TRUE)
+
+filter <- "POLYGON ((-73.4341 44.0087, -73.4341 43.7981, -73.2531 43.7981, 
-73.2531 43.8889, -73.1531 43.8889, -73.1531 44.0087, -73.4341 44.0087))"
+
+sd_sql(glue::glue("
+  SELECT * FROM buildings
+  WHERE ST_Intersects(ST_SetSRID(ST_GeomFromText('{filter}'), 4326), geometry)
+")) |> sd_preview()
+```
+
+Conversion to and from sf are supported:
+
+```{r}
+library(sf)
+
+nc <- sf::read_sf(system.file("shape/nc.shp", package = "sf"))
+nc |> sd_to_view("nc", overwrite = TRUE)
+
+sd_sql("SELECT * FROM nc")
+```
+
+```{r}
+sd_sql("SELECT ST_Buffer(geometry, 0.1) as geometry FROM nc") |>
+  st_as_sf() |>
+  plot()
+```
diff --git a/r/sedonadb/README.md b/r/sedonadb/README.md
new file mode 100644
index 0000000..20bc812
--- /dev/null
+++ b/r/sedonadb/README.md
@@ -0,0 +1,120 @@
+
+<!---
+  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
+&#10;    http://www.apache.org/licenses/LICENSE-2.0
+&#10;  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.
+-->
+
+# sedonadb
+
+<!-- badges: start -->
+
+<!-- badges: end -->
+
+The goal of sedonadb is to provide an R interface to [Apache
+SedonaDB](https://sedona.apache.org/sedonadb). SedonaDB provides a
+[DataFusion](https://datafusion.apache.org)-powered single-node engine
+with a wide range of spatial capabilities built in, including spatial
+SQL with high function coverage and GeoParquet IO.
+
+## Installation
+
+You can install the development version of sedonadb from
+[GitHub](https://github.com/) with:
+
+``` shell
+git clone https://github.com/apache/sedona-db.git
+cd sedona-db/r/sedonadb
+R CMD INSTALL .
+```
+
+Installing a development version of sedonadb requires a [Rust
+compiler](https://rustup.rs) and a GEOS system dependency (e.g.,
+`brew install geos` or `apt-get install libgeos-dev`). Install
+instructions for these dependencies on other platforms can be found on
+the [sf package homepage](https://r-spatial.github.io/sf).
+
+## Example
+
+You can use SedonaDB to read (Geo)Parquet files from anywhere. Filters
+are used to reduce the amount of data downloaded based on GeoParquet
+and/or Parquet metadata:
+
+``` r
+library(sedonadb)
+
+url <- 
"https://github.com/geoarrow/geoarrow-data/releases/download/v0.2.0/microsoft-buildings_point_geo.parquet";
+sd_read_parquet(url) |> sd_to_view("buildings", overwrite = TRUE)
+
+filter <- "POLYGON ((-73.4341 44.0087, -73.4341 43.7981, -73.2531 43.7981, 
-73.2531 43.8889, -73.1531 43.8889, -73.1531 44.0087, -73.4341 44.0087))"
+
+sd_sql(glue::glue("
+  SELECT * FROM buildings
+  WHERE ST_Intersects(ST_SetSRID(ST_GeomFromText('{filter}'), 4326), geometry)
+")) |> sd_preview()
+#> ┌─────────────────────────────────┐
+#> │             geometry            │
+#> │             geometry            │
+#> ╞═════════════════════════════════╡
+#> │ POINT(-73.29533522 44.00847556) │
+#> ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+#> │ POINT(-73.29092778 44.00421331) │
+#> ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+#> │ POINT(-73.277808 43.998823)     │
+#> ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+#> │ POINT(-73.277524 44.004619)     │
+#> ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+#> │ POINT(-73.2774573 44.0044719)   │
+#> ├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+#> │ POINT(-73.27775838 44.0046742)  │
+#> └─────────────────────────────────┘
+#> Preview of up to 6 row(s)
+```
+
+Conversion to and from sf are supported:
+
+``` r
+library(sf)
+#> Linking to GEOS 3.13.0, GDAL 3.8.5, PROJ 9.5.1; sf_use_s2() is TRUE
+
+nc <- sf::read_sf(system.file("shape/nc.shp", package = "sf"))
+nc |> sd_to_view("nc", overwrite = TRUE)
+
+sd_sql("SELECT * FROM nc")
+#> 
┌─────────┬───┬─────────┬──────────────────────────────────────────────────────┐
+#> │   AREA  ┆ … ┆ NWBIR79 ┆                       geometry                    
   │
+#> │ float64 ┆   ┆ float64 ┆                       geometry                    
   │
+#> 
╞═════════╪═══╪═════════╪══════════════════════════════════════════════════════╡
+#> │   0.114 ┆ … ┆    19.0 ┆ MULTIPOLYGON(((-81.4727554321289 
36.23435592651367,… │
+#> 
├╌╌╌╌╌╌╌╌╌┼╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+#> │   0.061 ┆ … ┆    12.0 ┆ MULTIPOLYGON(((-81.2398910522461 
36.36536407470703,… │
+#> 
├╌╌╌╌╌╌╌╌╌┼╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+#> │   0.143 ┆ … ┆   260.0 ┆ MULTIPOLYGON(((-80.45634460449219 
36.24255752563476… │
+#> 
├╌╌╌╌╌╌╌╌╌┼╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+#> │    0.07 ┆ … ┆   145.0 ┆ MULTIPOLYGON(((-76.00897216796875 
36.31959533691406… │
+#> 
├╌╌╌╌╌╌╌╌╌┼╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+#> │   0.153 ┆ … ┆  1197.0 ┆ MULTIPOLYGON(((-77.21766662597656 
36.24098205566406… │
+#> 
├╌╌╌╌╌╌╌╌╌┼╌╌╌┼╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
+#> │   0.097 ┆ … ┆  1237.0 ┆ MULTIPOLYGON(((-76.74506378173828 
36.23391723632812… │
+#> 
└─────────┴───┴─────────┴──────────────────────────────────────────────────────┘
+#> Preview of up to 6 row(s)
+```
+
+``` r
+sd_sql("SELECT ST_Buffer(geometry, 0.1) as geometry FROM nc") |>
+  st_as_sf() |>
+  plot()
+```
+
+<img src="man/figures/README-unnamed-chunk-3-1.png" width="100%" />
diff --git a/.gitignore b/r/sedonadb/cleanup
old mode 100644
new mode 100755
similarity index 69%
copy from .gitignore
copy to r/sedonadb/cleanup
index 7441737..00c8865
--- a/.gitignore
+++ b/r/sedonadb/cleanup
@@ -15,31 +15,4 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# MacOS
-.DS_Store
-
-# sedona-cli
-.history
-
-# Generated by cargo
-debug/
-target/
-
-# Generated by rustfmt
-**/*.rs.bk
-
-# Generated by cargo llvm-cov
-coverage/
-
-# MSVC Windows builds of rustc generate these, which store debugging 
information
-*.pdb
-
-# IDE-specific settings
-.idea/
-.vscode/
-
-# documentation
-site/
-
-# Python cache files
-__pycache__
+rm -f src/Makevars
diff --git a/.gitignore b/r/sedonadb/cleanup.win
old mode 100644
new mode 100755
similarity index 69%
copy from .gitignore
copy to r/sedonadb/cleanup.win
index 7441737..e81e481
--- a/.gitignore
+++ b/r/sedonadb/cleanup.win
@@ -15,31 +15,4 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# MacOS
-.DS_Store
-
-# sedona-cli
-.history
-
-# Generated by cargo
-debug/
-target/
-
-# Generated by rustfmt
-**/*.rs.bk
-
-# Generated by cargo llvm-cov
-coverage/
-
-# MSVC Windows builds of rustc generate these, which store debugging 
information
-*.pdb
-
-# IDE-specific settings
-.idea/
-.vscode/
-
-# documentation
-site/
-
-# Python cache files
-__pycache__
+rm -f src/Makevars.win
diff --git a/r/sedonadb/configure b/r/sedonadb/configure
new file mode 100755
index 0000000..a4f74a6
--- /dev/null
+++ b/r/sedonadb/configure
@@ -0,0 +1,91 @@
+# 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.
+
+# We need to link GEOS libs. The cargo build will use pkg-config
+# and then geos-config, in that order, and requires pkg-config to be
+# available (even if it isn't used). Here we just try pkg-config (LIB_DIR
+# can be used to specify the location of -lgeos_c)
+
+pkg-config geos  2>/dev/null
+if [ $? -eq 0 ]; then
+  PKGCONFIG_LIBS=`pkg-config --libs geos`
+fi
+
+if [ "$LIB_DIR" ]; then
+  echo "Found LIB_DIR!"
+  PKG_LIBS="-L$LIB_DIR -lgeos_c $PKG_LIBS"
+elif [ "$PKGCONFIG_CFLAGS" ] || [ "$PKGCONFIG_LIBS" ]; then
+  echo "Found GEOS pkg-config libs!"
+  PKG_LIBS=${PKGCONFIG_LIBS}
+fi
+
+
+# Even when `cargo` is on `PATH`, `rustc` might not in some cases. This adds
+# ~/.cargo/bin to PATH to address such cases. Note that is not always available
+# (e.g. or on Ubuntu with Rust installed via APT).
+if [ -d "${HOME}/.cargo/bin" ]; then
+  export PATH="${PATH}:${HOME}/.cargo/bin"
+fi
+
+CARGO_VERSION="$(cargo --version)"
+
+if [ $? -ne 0 ]; then
+  echo "-------------- ERROR: CONFIGURATION FAILED --------------------"
+  echo ""
+  echo "The cargo command is not available. To install Rust, please refer"
+  echo "to the official instruction:"
+  echo ""
+  echo "https://www.rust-lang.org/tools/install";
+  echo ""
+  echo "---------------------------------------------------------------"
+
+  exit 1
+fi
+
+# There's a little chance that rustc is not available on PATH while cargo is.
+# So, just ignore the error case.
+RUSTC_VERSION="$(rustc --version || true)"
+
+# Report the version of Rustc to comply with the CRAN policy
+echo "using Rust package manager: '${CARGO_VERSION}'"
+echo "using Rust compiler: '${RUSTC_VERSION}'"
+
+if [ "$(uname)" = "Emscripten" ]; then
+  TARGET="wasm32-unknown-emscripten"
+fi
+
+# allow overriding profile externally (e.g. on CI)
+if [ -n "${SAVVY_PROFILE}" ]; then
+  PROFILE="${SAVVY_PROFILE}"
+# catch DEBUG envvar, which is passed from pkgbuild::compile_dll()
+elif [ "${DEBUG}" = "true" ]; then
+  PROFILE=dev
+else
+  PROFILE=release
+fi
+
+# e.g. SAVVY_FEATURES="a b"  -->  "--features 'a b'"
+if [ -n "${SAVVY_FEATURES}" ]; then
+  FEATURE_FLAGS="--features '${SAVVY_FEATURES}'"
+fi
+
+sed \
+  -e "s/@TARGET@/${TARGET}/" \
+  -e "s/@PROFILE@/${PROFILE}/" \
+  -e "s/@FEATURE_FLAGS@/${FEATURE_FLAGS}/" \
+  -e "s|@PKG_LIBS@|${PKG_LIBS}|" \
+  src/Makevars.in > src/Makevars
diff --git a/r/sedonadb/configure.win b/r/sedonadb/configure.win
new file mode 100755
index 0000000..6db580b
--- /dev/null
+++ b/r/sedonadb/configure.win
@@ -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.
+
+CARGO_VERSION="$(cargo --version)"
+
+if [ $? -ne 0 ]; then
+  echo "-------------- ERROR: CONFIGURATION FAILED --------------------"
+  echo ""
+  echo "The cargo command is not available. To install Rust, please refer"
+  echo "to the official instruction:"
+  echo ""
+  echo "https://www.rust-lang.org/tools/install";
+  echo ""
+  echo "---------------------------------------------------------------"
+
+  exit 1
+fi
+
+# There's a little chance that rustc is not available on PATH while cargo is.
+# So, just ignore the error case.
+RUSTC_VERSION="$(rustc --version || true)"
+
+# Report the version of Rustc to comply with the CRAN policy
+echo "using Rust package manager: '${CARGO_VERSION}'"
+echo "using Rust compiler: '${RUSTC_VERSION}'"
+
+# allow overriding profile externally (e.g. on CI)
+if [ -n "${SAVVY_PROFILE}" ]; then
+  PROFILE="${SAVVY_PROFILE}"
+# catch DEBUG envvar, which is passed from pkgbuild::compile_dll()
+elif [ "${DEBUG}" = "true" ]; then
+  PROFILE=dev
+else
+  PROFILE=release
+fi
+
+# e.g. SAVVY_FEATURES="a b"  -->  "--features 'a b'"
+if [ -n "${SAVVY_FEATURES}" ]; then
+  FEATURE_FLAGS="--features '${SAVVY_FEATURES}'"
+fi
+
+sed \
+  -e "s/@TARGET@/x86_64-pc-windows-gnu/" \
+  -e "s/@PROFILE@/${PROFILE}/" \
+  -e "s/@FEATURE_FLAGS@/${FEATURE_FLAGS}/" \
+  src/Makevars.win.in > src/Makevars.win
diff --git a/r/sedonadb/inst/files/natural-earth_cities_geo.parquet 
b/r/sedonadb/inst/files/natural-earth_cities_geo.parquet
new file mode 100644
index 0000000..bc419b4
Binary files /dev/null and 
b/r/sedonadb/inst/files/natural-earth_cities_geo.parquet differ
diff --git 
a/r/sedonadb/inst/files/natural-earth_countries-geography_geo.parquet 
b/r/sedonadb/inst/files/natural-earth_countries-geography_geo.parquet
new file mode 100644
index 0000000..078dc12
Binary files /dev/null and 
b/r/sedonadb/inst/files/natural-earth_countries-geography_geo.parquet differ
diff --git a/r/sedonadb/inst/files/natural-earth_countries_geo.parquet 
b/r/sedonadb/inst/files/natural-earth_countries_geo.parquet
new file mode 100644
index 0000000..a9f3bd4
Binary files /dev/null and 
b/r/sedonadb/inst/files/natural-earth_countries_geo.parquet differ
diff --git a/r/sedonadb/man/as_sedonadb_dataframe.Rd 
b/r/sedonadb/man/as_sedonadb_dataframe.Rd
new file mode 100644
index 0000000..b5c3060
--- /dev/null
+++ b/r/sedonadb/man/as_sedonadb_dataframe.Rd
@@ -0,0 +1,25 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/dataframe.R
+\name{as_sedonadb_dataframe}
+\alias{as_sedonadb_dataframe}
+\title{Convert an object to a DataFrame}
+\usage{
+as_sedonadb_dataframe(x, ..., schema = NULL)
+}
+\arguments{
+\item{x}{An object to convert}
+
+\item{...}{Extra arguments passed to/from methods}
+
+\item{schema}{The requested schema}
+}
+\value{
+A sedonadb_dataframe
+}
+\description{
+Convert an object to a DataFrame
+}
+\examples{
+as_sedonadb_dataframe(data.frame(x = 1:3))
+
+}
diff --git a/r/sedonadb/man/figures/README-unnamed-chunk-3-1.png 
b/r/sedonadb/man/figures/README-unnamed-chunk-3-1.png
new file mode 100644
index 0000000..3eb52b6
Binary files /dev/null and 
b/r/sedonadb/man/figures/README-unnamed-chunk-3-1.png differ
diff --git a/r/sedonadb/man/sd_compute.Rd b/r/sedonadb/man/sd_compute.Rd
new file mode 100644
index 0000000..97590fd
--- /dev/null
+++ b/r/sedonadb/man/sd_compute.Rd
@@ -0,0 +1,29 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/dataframe.R
+\name{sd_compute}
+\alias{sd_compute}
+\alias{sd_collect}
+\title{Collect a DataFrame into memory}
+\usage{
+sd_compute(.data)
+
+sd_collect(.data, ptype = NULL)
+}
+\arguments{
+\item{.data}{A sedonadb_dataframe}
+
+\item{ptype}{The target R object. See 
\link[nanoarrow:convert_array_stream]{nanoarrow::convert_array_stream}.}
+}
+\value{
+\code{sd_compute()} returns a sedonadb_dataframe; \code{sd_collect()} returns
+a data.frame (or subclass according to \code{ptype}).
+}
+\description{
+Use \code{sd_compute()} to collect and return the result as a DataFrame;
+use \code{sd_collect()} to collect and return the result as an R data.frame.
+}
+\examples{
+sd_sql("SELECT 1 as one") |> sd_compute()
+sd_sql("SELECT 1 as one") |> sd_collect()
+
+}
diff --git a/r/sedonadb/man/sd_count.Rd b/r/sedonadb/man/sd_count.Rd
new file mode 100644
index 0000000..c93b9d5
--- /dev/null
+++ b/r/sedonadb/man/sd_count.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/dataframe.R
+\name{sd_count}
+\alias{sd_count}
+\title{Count rows in a DataFrame}
+\usage{
+sd_count(.data)
+}
+\arguments{
+\item{.data}{A sedonadb_dataframe}
+}
+\value{
+The number of rows after executing the query
+}
+\description{
+Count rows in a DataFrame
+}
+\examples{
+sd_sql("SELECT 1 as one") |> sd_count()
+
+}
diff --git a/r/sedonadb/man/sd_drop_view.Rd b/r/sedonadb/man/sd_drop_view.Rd
new file mode 100644
index 0000000..046b260
--- /dev/null
+++ b/r/sedonadb/man/sd_drop_view.Rd
@@ -0,0 +1,27 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/context.R
+\name{sd_drop_view}
+\alias{sd_drop_view}
+\alias{sd_view}
+\title{Create or Drop a named view}
+\usage{
+sd_drop_view(table_ref)
+
+sd_view(table_ref)
+}
+\arguments{
+\item{table_ref}{The name of the view reference}
+}
+\value{
+The context, invisibly
+}
+\description{
+Remove a view created with \code{\link[=sd_to_view]{sd_to_view()}} from the 
context.
+}
+\examples{
+sd_sql("SELECT 1 as one") |> sd_to_view("foofy")
+sd_view("foofy")
+sd_drop_view("foofy")
+try(sd_view("foofy"))
+
+}
diff --git a/r/sedonadb/man/sd_preview.Rd b/r/sedonadb/man/sd_preview.Rd
new file mode 100644
index 0000000..351dd5a
--- /dev/null
+++ b/r/sedonadb/man/sd_preview.Rd
@@ -0,0 +1,32 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/dataframe.R
+\name{sd_preview}
+\alias{sd_preview}
+\title{Preview and print the results of running a query}
+\usage{
+sd_preview(.data, n = NULL, ascii = NULL, width = NULL)
+}
+\arguments{
+\item{.data}{A sedonadb_dataframe}
+
+\item{n}{The number of rows to preview. Use \code{Inf} to preview all rows.
+Defaults to \code{getOption("pillar.print_max")}.}
+
+\item{ascii}{Use \code{TRUE} to force ASCII table formatting or \code{FALSE} 
to force
+unicode formatting. By default, use a heuristic to determine if the output
+is unicode-friendly or the value of \code{getOption("cli.unicode")}.}
+
+\item{width}{The character width of the output. Defaults to
+\code{getOption("width")}.}
+}
+\value{
+.data, invisibly
+}
+\description{
+This is used to implement \code{print()} for the sedonadb_dataframe or can
+be used to explicitly preview if \code{options(sedonadb.interactive = FALSE)}.
+}
+\examples{
+sd_sql("SELECT 1 as one") |> sd_preview()
+
+}
diff --git a/r/sedonadb/man/sd_read_parquet.Rd 
b/r/sedonadb/man/sd_read_parquet.Rd
new file mode 100644
index 0000000..c370c77
--- /dev/null
+++ b/r/sedonadb/man/sd_read_parquet.Rd
@@ -0,0 +1,22 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/context.R
+\name{sd_read_parquet}
+\alias{sd_read_parquet}
+\title{Create a DataFrame from one or more Parquet files}
+\usage{
+sd_read_parquet(path)
+}
+\arguments{
+\item{path}{One or more paths or URIs to Parquet files}
+}
+\value{
+A sedonadb_dataframe
+}
+\description{
+The query will only be executed when requested.
+}
+\examples{
+path <- system.file("files/natural-earth_cities_geo.parquet", package = 
"sedonadb")
+sd_read_parquet(path) |> head(5) |> sd_preview()
+
+}
diff --git a/r/sedonadb/man/sd_sql.Rd b/r/sedonadb/man/sd_sql.Rd
new file mode 100644
index 0000000..3480720
--- /dev/null
+++ b/r/sedonadb/man/sd_sql.Rd
@@ -0,0 +1,21 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/context.R
+\name{sd_sql}
+\alias{sd_sql}
+\title{Create a DataFrame from SQL}
+\usage{
+sd_sql(sql)
+}
+\arguments{
+\item{sql}{A SQL string to execute}
+}
+\value{
+A sedonadb_dataframe
+}
+\description{
+The query will only be executed when requested.
+}
+\examples{
+sd_sql("SELECT ST_Point(0, 1) as geom") |> sd_preview()
+
+}
diff --git a/r/sedonadb/man/sd_to_view.Rd b/r/sedonadb/man/sd_to_view.Rd
new file mode 100644
index 0000000..5c3ab02
--- /dev/null
+++ b/r/sedonadb/man/sd_to_view.Rd
@@ -0,0 +1,27 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/dataframe.R
+\name{sd_to_view}
+\alias{sd_to_view}
+\title{Register a DataFrame as a named view}
+\usage{
+sd_to_view(.data, table_ref, overwrite = FALSE)
+}
+\arguments{
+\item{.data}{A sedonadb_dataframe}
+
+\item{table_ref}{The name of the view reference}
+
+\item{overwrite}{Use TRUE to overwrite a view with the same name (if it 
exists)}
+}
+\value{
+.data, invisibly
+}
+\description{
+This is useful for creating a view that can be referenced in a SQL
+statement. Use \code{\link[=sd_drop_view]{sd_drop_view()}} to remove it.
+}
+\examples{
+sd_sql("SELECT 1 as one") |> sd_to_view("foofy")
+sd_sql("SELECT * FROM foofy")
+
+}
diff --git a/r/sedonadb/man/sedonadb-package.Rd 
b/r/sedonadb/man/sedonadb-package.Rd
new file mode 100644
index 0000000..6503612
--- /dev/null
+++ b/r/sedonadb/man/sedonadb-package.Rd
@@ -0,0 +1,15 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/sedonadb-package.R
+\docType{package}
+\name{sedonadb-package}
+\alias{sedonadb}
+\alias{sedonadb-package}
+\title{sedonadb: Bindings for Apache SedonaDB}
+\description{
+Provides bindings for Apache SedonaDB, a lightweight query engine optimized 
for spatial workflows.
+}
+\author{
+\strong{Maintainer}: Dewey Dunnington \email{[email protected]}
+
+}
+\keyword{internal}
diff --git a/r/sedonadb/man/sedonadb_adbc.Rd b/r/sedonadb/man/sedonadb_adbc.Rd
new file mode 100644
index 0000000..e75e956
--- /dev/null
+++ b/r/sedonadb/man/sedonadb_adbc.Rd
@@ -0,0 +1,26 @@
+% Generated by roxygen2: do not edit by hand
+% Please edit documentation in R/adbc.R
+\name{sedonadb_adbc}
+\alias{sedonadb_adbc}
+\title{SedonaDB ADBC Driver}
+\usage{
+sedonadb_adbc()
+}
+\value{
+An 
\code{\link[adbcdrivermanager:adbc_driver_void]{adbcdrivermanager::adbc_driver()}}
 of class
+'sedonadb_driver_sedonadb'
+}
+\description{
+SedonaDB ADBC Driver
+}
+\examples{
+library(adbcdrivermanager)
+
+con <- sedonadb_adbc() |>
+  adbc_database_init() |>
+  adbc_connection_init()
+con |>
+  read_adbc("SELECT ST_Point(0, 1) as geometry") |>
+  as.data.frame()
+
+}
diff --git a/r/sedonadb/sedonadb.Rproj b/r/sedonadb/sedonadb.Rproj
new file mode 100644
index 0000000..69fafd4
--- /dev/null
+++ b/r/sedonadb/sedonadb.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/.gitignore b/r/sedonadb/src/.gitignore
similarity index 69%
copy from .gitignore
copy to r/sedonadb/src/.gitignore
index 7441737..e103a0a 100644
--- a/.gitignore
+++ b/r/sedonadb/src/.gitignore
@@ -15,31 +15,10 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# MacOS
-.DS_Store
+*.o
+*.so
+*.dll
+target
 
-# sedona-cli
-.history
-
-# Generated by cargo
-debug/
-target/
-
-# Generated by rustfmt
-**/*.rs.bk
-
-# Generated by cargo llvm-cov
-coverage/
-
-# MSVC Windows builds of rustc generate these, which store debugging 
information
-*.pdb
-
-# IDE-specific settings
-.idea/
-.vscode/
-
-# documentation
-site/
-
-# Python cache files
-__pycache__
+Makevars
+Makevars.win
diff --git a/r/sedonadb/src/Makevars.in b/r/sedonadb/src/Makevars.in
new file mode 100644
index 0000000..7828f4c
--- /dev/null
+++ b/r/sedonadb/src/Makevars.in
@@ -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.
+
+TARGET = @TARGET@
+
+PROFILE = @PROFILE@
+FEATURE_FLAGS = @FEATURE_FLAGS@
+
+# Add flags if necessary
+RUSTFLAGS =
+
+TARGET_DIR = $(CURDIR)/rust/target
+LIBDIR = $(TARGET_DIR)/$(TARGET)/$(subst dev,debug,$(PROFILE))
+STATLIB = $(LIBDIR)/libsedonadbr.a
+PKG_LIBS = -L$(LIBDIR) -lsedonadbr @PKG_LIBS@
+
+CARGO_BUILD_ARGS = --lib --profile $(PROFILE) $(FEATURE_FLAGS) 
--manifest-path=./rust/Cargo.toml --target-dir $(TARGET_DIR)
+
+all: $(SHLIB) clean_intermediate
+
+$(SHLIB): $(STATLIB)
+
+$(STATLIB):
+       # In some environments, ~/.cargo/bin might not be included in PATH, so 
we need
+       # to set it here to ensure cargo can be invoked. It is appended to PATH 
and
+       # therefore is only used if cargo is absent from the user's PATH.
+       export PATH="$(PATH):$(HOME)/.cargo/bin" && \
+         export CC="$(CC)" && \
+         export CFLAGS="$(CFLAGS)" && \
+         export RUSTFLAGS="$(RUSTFLAGS)" && \
+         if [ "$(TARGET)" != "wasm32-unknown-emscripten" ]; then \
+           cargo build $(CARGO_BUILD_ARGS); \
+         else \
+           export CARGO_PROFILE_DEV_PANIC="abort" && \
+           export CARGO_PROFILE_RELEASE_PANIC="abort" && \
+           export RUSTFLAGS="$(RUSTFLAGS) -Zdefault-visibility=hidden" && \
+           cargo +nightly build $(CARGO_BUILD_ARGS) --target $(TARGET) 
-Zbuild-std=panic_abort,std; \
+         fi
+
+clean_intermediate: $(SHLIB)
+       rm -f $(STATLIB)
+
+clean:
+       rm -Rf $(SHLIB) $(OBJECTS) $(STATLIB) ./rust/target
+
+.PHONY: all clean_intermediate clean
diff --git a/r/sedonadb/src/Makevars.win.in b/r/sedonadb/src/Makevars.win.in
new file mode 100644
index 0000000..a9f15a4
--- /dev/null
+++ b/r/sedonadb/src/Makevars.win.in
@@ -0,0 +1,41 @@
+TARGET = @TARGET@
+
+PROFILE = @PROFILE@
+FEATURE_FLAGS = @FEATURE_FLAGS@
+
+# Add flags if necessary
+RUSTFLAGS =
+
+TARGET_DIR = $(CURDIR)/rust/target
+LIBDIR = $(TARGET_DIR)/$(TARGET)/$(subst dev,debug,$(PROFILE))
+STATLIB = $(LIBDIR)/libsedonadb.a
+PKG_LIBS = -L$(LIBDIR) -lsedonadb -lws2_32 -ladvapi32 -luserenv -lbcrypt 
-lntdll
+
+# Rtools doesn't have the linker in the location that cargo expects, so we need
+# to overwrite it via configuration.
+CARGO_LINKER = x86_64-w64-mingw32.static.posix-gcc.exe
+
+all: $(SHLIB) clean_intermediate
+
+$(SHLIB): $(STATLIB)
+
+$(STATLIB):
+       # When the GNU toolchain is used (i.e. on CRAN), -lgcc_eh is specified 
for
+       # building proc-macro2, but Rtools doesn't contain libgcc_eh. This 
isn't used
+       # in actual, but we need this tweak to please the compiler.
+       mkdir -p $(LIBDIR)/libgcc_mock && touch 
$(LIBDIR)/libgcc_mock/libgcc_eh.a
+
+       export CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER="$(CARGO_LINKER)" && \
+         export LIBRARY_PATH="$${LIBRARY_PATH};$(LIBDIR)/libgcc_mock" && \
+         export CC="$(CC)" && \
+         export CFLAGS="$(CFLAGS)" && \
+         export RUSTFLAGS="$(RUSTFLAGS)" && \
+         cargo build --target $(TARGET) --lib --profile $(PROFILE) 
$(FEATURE_FLAGS) --manifest-path ./rust/Cargo.toml --target-dir $(TARGET_DIR)
+
+clean_intermediate: $(SHLIB)
+       rm -f $(STATLIB)
+
+clean:
+       rm -Rf $(SHLIB) $(OBJECTS) $(STATLIB) ./rust/target
+
+.PHONY: all clean_intermediate clean
diff --git a/r/sedonadb/src/init.c b/r/sedonadb/src/init.c
new file mode 100644
index 0000000..2434cda
--- /dev/null
+++ b/r/sedonadb/src/init.c
@@ -0,0 +1,201 @@
+// 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 <Rinternals.h>
+
+#include <R_ext/Parse.h>
+#include <stdint.h>
+
+#include "rust/api.h"
+
+static uintptr_t TAGGED_POINTER_MASK = (uintptr_t)1;
+
+SEXP handle_result(SEXP res_) {
+  uintptr_t res = (uintptr_t)res_;
+
+  // An error is indicated by tag.
+  if ((res & TAGGED_POINTER_MASK) == 1) {
+    // Remove tag
+    SEXP res_aligned = (SEXP)(res & ~TAGGED_POINTER_MASK);
+
+    // Currently, there are two types of error cases:
+    //
+    //   1. Error from Rust code
+    //   2. Error from R's C API, which is caught by R_UnwindProtect()
+    //
+    if (TYPEOF(res_aligned) == CHARSXP) {
+      // In case 1, the result is an error message that can be passed to
+      // Rf_errorcall() directly.
+      Rf_errorcall(R_NilValue, "%s", CHAR(res_aligned));
+    } else {
+      // In case 2, the result is the token to restart the
+      // cleanup process on R's side.
+      R_ContinueUnwind(res_aligned);
+    }
+  }
+
+  return (SEXP)res;
+}
+
+SEXP savvy_init_r_runtime__impl(DllInfo *c_arg___dll_info) {
+  SEXP res = savvy_init_r_runtime__ffi(c_arg___dll_info);
+  return handle_result(res);
+}
+
+SEXP savvy_init_r_runtime_interrupts__impl(SEXP c_arg__interrupts_call,
+                                           SEXP c_arg__pkg_env) {
+  SEXP res = savvy_init_r_runtime_interrupts__ffi(c_arg__interrupts_call,
+                                                  c_arg__pkg_env);
+  return handle_result(res);
+}
+
+SEXP savvy_sedonadb_adbc_init_func__impl(void) {
+  SEXP res = savvy_sedonadb_adbc_init_func__ffi();
+  return handle_result(res);
+}
+
+SEXP savvy_InternalContext_data_frame_from_array_stream__impl(
+    SEXP self__, SEXP c_arg__stream_xptr, SEXP c_arg__collect_now) {
+  SEXP res = savvy_InternalContext_data_frame_from_array_stream__ffi(
+      self__, c_arg__stream_xptr, c_arg__collect_now);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalContext_deregister_table__impl(SEXP self__,
+                                                  SEXP c_arg__table_ref) {
+  SEXP res =
+      savvy_InternalContext_deregister_table__ffi(self__, c_arg__table_ref);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalContext_new__impl(void) {
+  SEXP res = savvy_InternalContext_new__ffi();
+  return handle_result(res);
+}
+
+SEXP savvy_InternalContext_read_parquet__impl(SEXP self__, SEXP c_arg__paths) {
+  SEXP res = savvy_InternalContext_read_parquet__ffi(self__, c_arg__paths);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalContext_sql__impl(SEXP self__, SEXP c_arg__query) {
+  SEXP res = savvy_InternalContext_sql__ffi(self__, c_arg__query);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalContext_view__impl(SEXP self__, SEXP c_arg__table_ref) {
+  SEXP res = savvy_InternalContext_view__ffi(self__, c_arg__table_ref);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalDataFrame_collect__impl(SEXP self__, SEXP c_arg__out) {
+  SEXP res = savvy_InternalDataFrame_collect__ffi(self__, c_arg__out);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalDataFrame_compute__impl(SEXP self__, SEXP c_arg__ctx) {
+  SEXP res = savvy_InternalDataFrame_compute__ffi(self__, c_arg__ctx);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalDataFrame_count__impl(SEXP self__) {
+  SEXP res = savvy_InternalDataFrame_count__ffi(self__);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalDataFrame_limit__impl(SEXP self__, SEXP c_arg__n) {
+  SEXP res = savvy_InternalDataFrame_limit__ffi(self__, c_arg__n);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalDataFrame_primary_geometry_column_index__impl(SEXP self__) {
+  SEXP res = 
savvy_InternalDataFrame_primary_geometry_column_index__ffi(self__);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalDataFrame_show__impl(SEXP self__, SEXP c_arg__ctx,
+                                        SEXP c_arg__width_chars,
+                                        SEXP c_arg__ascii, SEXP c_arg__limit) {
+  SEXP res = savvy_InternalDataFrame_show__ffi(
+      self__, c_arg__ctx, c_arg__width_chars, c_arg__ascii, c_arg__limit);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalDataFrame_to_arrow_schema__impl(SEXP self__,
+                                                   SEXP c_arg__out) {
+  SEXP res = savvy_InternalDataFrame_to_arrow_schema__ffi(self__, c_arg__out);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalDataFrame_to_arrow_stream__impl(SEXP self__,
+                                                   SEXP c_arg__out) {
+  SEXP res = savvy_InternalDataFrame_to_arrow_stream__ffi(self__, c_arg__out);
+  return handle_result(res);
+}
+
+SEXP savvy_InternalDataFrame_to_view__impl(SEXP self__, SEXP c_arg__ctx,
+                                           SEXP c_arg__table_ref,
+                                           SEXP c_arg__overwrite) {
+  SEXP res = savvy_InternalDataFrame_to_view__ffi(
+      self__, c_arg__ctx, c_arg__table_ref, c_arg__overwrite);
+  return handle_result(res);
+}
+
+static const R_CallMethodDef CallEntries[] = {
+    {"savvy_init_r_runtime_interrupts__impl",
+     (DL_FUNC)&savvy_init_r_runtime_interrupts__impl, 2},
+    {"savvy_sedonadb_adbc_init_func__impl",
+     (DL_FUNC)&savvy_sedonadb_adbc_init_func__impl, 0},
+    {"savvy_InternalContext_data_frame_from_array_stream__impl",
+     (DL_FUNC)&savvy_InternalContext_data_frame_from_array_stream__impl, 3},
+    {"savvy_InternalContext_deregister_table__impl",
+     (DL_FUNC)&savvy_InternalContext_deregister_table__impl, 2},
+    {"savvy_InternalContext_new__impl",
+     (DL_FUNC)&savvy_InternalContext_new__impl, 0},
+    {"savvy_InternalContext_read_parquet__impl",
+     (DL_FUNC)&savvy_InternalContext_read_parquet__impl, 2},
+    {"savvy_InternalContext_sql__impl",
+     (DL_FUNC)&savvy_InternalContext_sql__impl, 2},
+    {"savvy_InternalContext_view__impl",
+     (DL_FUNC)&savvy_InternalContext_view__impl, 2},
+    {"savvy_InternalDataFrame_collect__impl",
+     (DL_FUNC)&savvy_InternalDataFrame_collect__impl, 2},
+    {"savvy_InternalDataFrame_compute__impl",
+     (DL_FUNC)&savvy_InternalDataFrame_compute__impl, 2},
+    {"savvy_InternalDataFrame_count__impl",
+     (DL_FUNC)&savvy_InternalDataFrame_count__impl, 1},
+    {"savvy_InternalDataFrame_limit__impl",
+     (DL_FUNC)&savvy_InternalDataFrame_limit__impl, 2},
+    {"savvy_InternalDataFrame_primary_geometry_column_index__impl",
+     (DL_FUNC)&savvy_InternalDataFrame_primary_geometry_column_index__impl, 1},
+    {"savvy_InternalDataFrame_show__impl",
+     (DL_FUNC)&savvy_InternalDataFrame_show__impl, 5},
+    {"savvy_InternalDataFrame_to_arrow_schema__impl",
+     (DL_FUNC)&savvy_InternalDataFrame_to_arrow_schema__impl, 2},
+    {"savvy_InternalDataFrame_to_arrow_stream__impl",
+     (DL_FUNC)&savvy_InternalDataFrame_to_arrow_stream__impl, 2},
+    {"savvy_InternalDataFrame_to_view__impl",
+     (DL_FUNC)&savvy_InternalDataFrame_to_view__impl, 4},
+    {NULL, NULL, 0}};
+
+void R_init_sedonadb(DllInfo *dll) {
+  R_registerRoutines(dll, NULL, CallEntries, NULL, NULL);
+  R_useDynamicSymbols(dll, FALSE);
+
+  // Functions for initialization, if any.
+  savvy_init_r_runtime__impl(dll);
+}
diff --git a/r/sedonadb/src/rust/.cargo/config.toml 
b/r/sedonadb/src/rust/.cargo/config.toml
new file mode 100644
index 0000000..3d8bdda
--- /dev/null
+++ b/r/sedonadb/src/rust/.cargo/config.toml
@@ -0,0 +1,10 @@
+# On Windows, link.exe fails when the artifact contains unresolved symbols
+# (i.e., R's API, which cannot be used without a real R session). This option
+# makes the linker ignore these problems.
+#
+# This setting is needed only when you run `cargo test`, not when `R CMD check`
+# etc. The `.cargo` directory need to be excluded on building the package (i.e.
+# add `^src/rust/\.cargo$` to `.Rbuildignore`) because otherwise you'll get the
+# "hidden files and directories" NOTE.
+[target.x86_64-pc-windows-msvc]
+rustflags = ["-C", "link-arg=/FORCE:UNRESOLVED"]
diff --git a/.gitignore b/r/sedonadb/src/rust/Cargo.toml
similarity index 55%
copy from .gitignore
copy to r/sedonadb/src/rust/Cargo.toml
index 7441737..645fa23 100644
--- a/.gitignore
+++ b/r/sedonadb/src/rust/Cargo.toml
@@ -15,31 +15,25 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# MacOS
-.DS_Store
-
-# sedona-cli
-.history
-
-# Generated by cargo
-debug/
-target/
-
-# Generated by rustfmt
-**/*.rs.bk
-
-# Generated by cargo llvm-cov
-coverage/
-
-# MSVC Windows builds of rustc generate these, which store debugging 
information
-*.pdb
-
-# IDE-specific settings
-.idea/
-.vscode/
-
-# documentation
-site/
-
-# Python cache files
-__pycache__
+[package]
+name = "sedonadbr"
+version = "0.1.0"
+edition = "2021"
+
+[lib]
+crate-type = ["staticlib", "lib"]
+
+[dependencies]
+arrow-schema = { workspace = true }
+arrow-array = { workspace = true }
+datafusion = { workspace = true }
+datafusion-common = { workspace = true }
+savvy = "*"
+savvy-ffi = "*"
+sedona = { path = "../../../../rust/sedona" }
+sedona-adbc = { path = "../../../../rust/sedona-adbc" }
+sedona-expr = { path = "../../../../rust/sedona-expr" }
+sedona-geoparquet = { path = "../../../../rust/sedona-geoparquet" }
+sedona-schema = { path = "../../../../rust/sedona-schema" }
+thiserror = { workspace = true }
+tokio = { workspace = true }
diff --git a/r/sedonadb/src/rust/api.h b/r/sedonadb/src/rust/api.h
new file mode 100644
index 0000000..4138988
--- /dev/null
+++ b/r/sedonadb/src/rust/api.h
@@ -0,0 +1,46 @@
+// 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.
+
+SEXP savvy_init_r_runtime__ffi(DllInfo *c_arg___dll_info);
+SEXP savvy_init_r_runtime_interrupts__ffi(SEXP c_arg__interrupts_call,
+                                          SEXP c_arg__pkg_env);
+SEXP savvy_sedonadb_adbc_init_func__ffi(void);
+
+// methods and associated functions for InternalContext
+SEXP savvy_InternalContext_data_frame_from_array_stream__ffi(
+    SEXP self__, SEXP c_arg__stream_xptr, SEXP c_arg__collect_now);
+SEXP savvy_InternalContext_deregister_table__ffi(SEXP self__,
+                                                 SEXP c_arg__table_ref);
+SEXP savvy_InternalContext_new__ffi(void);
+SEXP savvy_InternalContext_read_parquet__ffi(SEXP self__, SEXP c_arg__paths);
+SEXP savvy_InternalContext_sql__ffi(SEXP self__, SEXP c_arg__query);
+SEXP savvy_InternalContext_view__ffi(SEXP self__, SEXP c_arg__table_ref);
+
+// methods and associated functions for InternalDataFrame
+SEXP savvy_InternalDataFrame_collect__ffi(SEXP self__, SEXP c_arg__out);
+SEXP savvy_InternalDataFrame_compute__ffi(SEXP self__, SEXP c_arg__ctx);
+SEXP savvy_InternalDataFrame_count__ffi(SEXP self__);
+SEXP savvy_InternalDataFrame_limit__ffi(SEXP self__, SEXP c_arg__n);
+SEXP savvy_InternalDataFrame_primary_geometry_column_index__ffi(SEXP self__);
+SEXP savvy_InternalDataFrame_show__ffi(SEXP self__, SEXP c_arg__ctx,
+                                       SEXP c_arg__width_chars,
+                                       SEXP c_arg__ascii, SEXP c_arg__limit);
+SEXP savvy_InternalDataFrame_to_arrow_schema__ffi(SEXP self__, SEXP 
c_arg__out);
+SEXP savvy_InternalDataFrame_to_arrow_stream__ffi(SEXP self__, SEXP 
c_arg__out);
+SEXP savvy_InternalDataFrame_to_view__ffi(SEXP self__, SEXP c_arg__ctx,
+                                          SEXP c_arg__table_ref,
+                                          SEXP c_arg__overwrite);
diff --git a/r/sedonadb/src/rust/src/context.rs 
b/r/sedonadb/src/rust/src/context.rs
new file mode 100644
index 0000000..67f52a3
--- /dev/null
+++ b/r/sedonadb/src/rust/src/context.rs
@@ -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.
+use std::sync::Arc;
+
+use arrow_array::{
+    ffi_stream::{ArrowArrayStreamReader, FFI_ArrowArrayStream},
+    RecordBatchReader,
+};
+use arrow_schema::ArrowError;
+use datafusion::catalog::{MemTable, TableProvider};
+use savvy::{savvy, savvy_err, Result};
+
+use sedona::{context::SedonaContext, 
record_batch_reader_provider::RecordBatchReaderProvider};
+use sedona_geoparquet::provider::GeoParquetReadOptions;
+use tokio::runtime::Runtime;
+
+use crate::{
+    dataframe::{new_data_frame, InternalDataFrame},
+    runtime::wait_for_future_captured_r,
+};
+
+#[savvy]
+pub struct InternalContext {
+    pub inner: Arc<SedonaContext>,
+    pub runtime: Arc<Runtime>,
+}
+
+#[savvy]
+impl InternalContext {
+    pub fn new() -> Result<Self> {
+        let runtime = tokio::runtime::Builder::new_multi_thread()
+            .enable_all()
+            .build()?;
+
+        let inner = wait_for_future_captured_r(&runtime, 
SedonaContext::new_local_interactive())??;
+
+        Ok(Self {
+            inner: Arc::new(inner),
+            runtime: Arc::new(runtime),
+        })
+    }
+
+    pub fn read_parquet(&self, paths: savvy::Sexp) -> 
Result<InternalDataFrame> {
+        let paths_strsxp = savvy::StringSexp::try_from(paths)?;
+        let table_paths = paths_strsxp
+            .iter()
+            .map(|s| s.to_string())
+            .collect::<Vec<_>>();
+
+        let inner_context = self.inner.clone();
+        let inner = wait_for_future_captured_r(&self.runtime, async move {
+            inner_context
+                .read_parquet(table_paths, GeoParquetReadOptions::default())
+                .await
+        })??;
+
+        Ok(new_data_frame(inner, self.runtime.clone()))
+    }
+
+    pub fn sql(&self, query: &str) -> Result<InternalDataFrame> {
+        let query_string = query.to_string();
+        let inner_context = self.inner.clone();
+        let inner = wait_for_future_captured_r(&self.runtime, async move {
+            inner_context.sql(&query_string).await
+        })??;
+        Ok(new_data_frame(inner, self.runtime.clone()))
+    }
+
+    pub fn view(&self, table_ref: &str) -> Result<InternalDataFrame> {
+        let inner_context = self.inner.clone();
+        let table_ref_string = table_ref.to_string();
+        let inner = wait_for_future_captured_r(&self.runtime, async move {
+            inner_context.ctx.table(table_ref_string).await
+        })??;
+        Ok(new_data_frame(inner, self.runtime.clone()))
+    }
+
+    pub fn data_frame_from_array_stream(
+        &self,
+        stream_xptr: savvy::Sexp,
+        collect_now: bool,
+    ) -> savvy::Result<InternalDataFrame> {
+        let ffi_stream =
+            unsafe { savvy_ffi::R_ExternalPtrAddr(stream_xptr.0) as *mut 
FFI_ArrowArrayStream };
+        if ffi_stream.is_null() {
+            return Err(savvy_err!("external pointer to null in 
to_arrow_schema()"));
+        }
+
+        let stream = unsafe { FFI_ArrowArrayStream::from_raw(ffi_stream as _) 
};
+        let stream_reader = ArrowArrayStreamReader::try_new(stream)?;
+
+        // Some readers are sensitive to being collected on the R thread or 
not, so
+        // provide the option to collect everything immediately.
+        let provider: Arc<dyn TableProvider> = if collect_now {
+            let schema = stream_reader.schema();
+            let batches = stream_reader.collect::<std::result::Result<Vec<_>, 
ArrowError>>()?;
+            Arc::new(MemTable::try_new(schema, vec![batches])?)
+        } else {
+            Arc::new(RecordBatchReaderProvider::new(Box::new(stream_reader)))
+        };
+
+        let inner = self.inner.ctx.read_table(provider)?;
+        Ok(new_data_frame(inner, self.runtime.clone()))
+    }
+
+    pub fn deregister_table(&self, table_ref: &str) -> savvy::Result<()> {
+        self.inner.ctx.deregister_table(table_ref)?;
+        Ok(())
+    }
+}
diff --git a/r/sedonadb/src/rust/src/dataframe.rs 
b/r/sedonadb/src/rust/src/dataframe.rs
new file mode 100644
index 0000000..274aa49
--- /dev/null
+++ b/r/sedonadb/src/rust/src/dataframe.rs
@@ -0,0 +1,194 @@
+// 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.
+use std::ptr::swap_nonoverlapping;
+use std::sync::Arc;
+
+use arrow_array::ffi::FFI_ArrowSchema;
+use arrow_array::ffi_stream::FFI_ArrowArrayStream;
+use arrow_array::{RecordBatchIterator, RecordBatchReader};
+use datafusion::catalog::MemTable;
+use datafusion::prelude::DataFrame;
+use savvy::{savvy, savvy_err, Result};
+use sedona::context::SedonaDataFrame;
+use sedona::reader::SedonaStreamReader;
+use sedona::show::{DisplayMode, DisplayTableOptions};
+use sedona_schema::schema::SedonaSchema;
+use tokio::runtime::Runtime;
+
+use crate::context::InternalContext;
+use crate::runtime::wait_for_future_captured_r;
+
+#[savvy]
+pub struct InternalDataFrame {
+    pub inner: DataFrame,
+    pub runtime: Arc<Runtime>,
+}
+
+pub fn new_data_frame(inner: DataFrame, runtime: Arc<Runtime>) -> 
InternalDataFrame {
+    InternalDataFrame { inner, runtime }
+}
+
+#[savvy]
+impl InternalDataFrame {
+    fn limit(&self, n: f64) -> Result<InternalDataFrame> {
+        let inner = self.inner.clone().limit(0, Some(n.floor() as usize))?;
+        Ok(InternalDataFrame {
+            inner,
+            runtime: self.runtime.clone(),
+        })
+    }
+
+    fn count(&self) -> Result<savvy::Sexp> {
+        let inner = self.inner.clone();
+        let counted =
+            wait_for_future_captured_r(&self.runtime, async move { 
inner.count().await })??;
+
+        let counted_double = counted as f64;
+        savvy::Sexp::try_from(counted_double)
+    }
+
+    fn primary_geometry_column_index(&self) -> Result<savvy::Sexp> {
+        if let Some(col) = 
self.inner.schema().primary_geometry_column_index()? {
+            Ok(unsafe { 
savvy::Sexp(savvy_ffi::Rf_ScalarInteger(col.try_into()?)) })
+        } else {
+            Ok(savvy::NullSexp.into())
+        }
+    }
+
+    fn to_arrow_schema(&self, out: savvy::Sexp) -> Result<()> {
+        let out_void = unsafe { savvy_ffi::R_ExternalPtrAddr(out.0) };
+        if out_void.is_null() {
+            return Err(savvy_err!("external pointer to null in 
to_arrow_schema()"));
+        }
+
+        let schema_no_qualifiers = 
self.inner.schema().clone().strip_qualifiers();
+        let schema = schema_no_qualifiers.as_arrow();
+        let mut ffi_schema = FFI_ArrowSchema::try_from(schema)?;
+        let ffi_out = out_void as *mut FFI_ArrowSchema;
+        unsafe { swap_nonoverlapping(&mut ffi_schema, ffi_out, 1) };
+        Ok(())
+    }
+
+    fn to_arrow_stream(&self, out: savvy::Sexp) -> Result<()> {
+        let out_void = unsafe { savvy_ffi::R_ExternalPtrAddr(out.0) };
+        if out_void.is_null() {
+            return Err(savvy_err!("external pointer to null in 
to_arrow_stream()"));
+        }
+
+        let inner = self.inner.clone();
+        let stream =
+            wait_for_future_captured_r(
+                &self.runtime,
+                async move { inner.execute_stream().await },
+            )??;
+
+        let reader = SedonaStreamReader::new(self.runtime.clone(), stream);
+        let reader: Box<dyn RecordBatchReader + Send> = Box::new(reader);
+
+        let mut ffi_stream = FFI_ArrowArrayStream::new(reader);
+        let ffi_out = out_void as *mut FFI_ArrowArrayStream;
+        unsafe { swap_nonoverlapping(&mut ffi_stream, ffi_out, 1) };
+
+        Ok(())
+    }
+
+    fn compute(&self, ctx: &InternalContext) -> Result<InternalDataFrame> {
+        let schema = self.inner.schema();
+        let batches =
+            wait_for_future_captured_r(&self.runtime, 
self.inner.clone().collect_partitioned())??;
+        let provider = Arc::new(MemTable::try_new(schema.clone().into(), 
batches)?);
+        let inner = ctx.inner.ctx.read_table(provider)?;
+        Ok(new_data_frame(inner, self.runtime.clone()))
+    }
+
+    fn collect(&self, out: savvy::Sexp) -> Result<savvy::Sexp> {
+        let out_void = unsafe { savvy_ffi::R_ExternalPtrAddr(out.0) };
+        if out_void.is_null() {
+            return Err(savvy_err!("external pointer to null in collect()"));
+        }
+
+        let inner = self.inner.clone();
+        let batches =
+            wait_for_future_captured_r(&self.runtime, async move { 
inner.collect().await })??;
+
+        let size: usize = batches.iter().map(|batch| batch.num_rows()).sum();
+
+        let reader: Box<dyn RecordBatchReader + Send> = if batches.is_empty() {
+            let schema_no_qualifiers = 
self.inner.schema().clone().strip_qualifiers();
+            let schema = schema_no_qualifiers.as_arrow();
+            Box::new(RecordBatchIterator::new(
+                vec![].into_iter(),
+                Arc::new(schema.clone()),
+            ))
+        } else {
+            let schema = batches[0].schema();
+            Box::new(RecordBatchIterator::new(
+                batches.into_iter().map(Ok),
+                schema,
+            ))
+        };
+
+        let mut ffi_stream = FFI_ArrowArrayStream::new(reader);
+        let ffi_out = out_void as *mut FFI_ArrowArrayStream;
+        unsafe { swap_nonoverlapping(&mut ffi_stream, ffi_out, 1) };
+
+        savvy::Sexp::try_from(size as f64)
+    }
+
+    fn to_view(&self, ctx: &InternalContext, table_ref: &str, overwrite: bool) 
-> Result<()> {
+        let provider = self.inner.clone().into_view();
+        if overwrite && ctx.inner.ctx.table_exist(table_ref)? {
+            ctx.deregister_table(table_ref)?;
+        }
+
+        ctx.inner.ctx.register_table(table_ref, provider)?;
+        Ok(())
+    }
+
+    fn show(
+        &self,
+        ctx: &InternalContext,
+        width_chars: i32,
+        ascii: bool,
+        limit: Option<f64>,
+    ) -> Result<savvy::Sexp> {
+        let mut options = DisplayTableOptions::new();
+        options.table_width = width_chars.try_into().unwrap_or(u16::MAX);
+        options.arrow_options = options.arrow_options.with_types_info(true);
+        if !ascii {
+            options.display_mode = DisplayMode::Utf8;
+        }
+
+        let inner = self.inner.clone();
+        let inner_context = ctx.inner.clone();
+        let limit_usize = limit.map(|value| {
+            if value > i32::MAX as f64 {
+                i32::MAX as usize
+            } else {
+                value as usize
+            }
+        });
+
+        let out_string = wait_for_future_captured_r(&self.runtime, async move {
+            inner
+                .show_sedona(&inner_context, limit_usize, options)
+                .await
+        })??;
+
+        savvy::Sexp::try_from(out_string)
+    }
+}
diff --git a/r/sedonadb/src/rust/src/error.rs b/r/sedonadb/src/rust/src/error.rs
new file mode 100644
index 0000000..645d336
--- /dev/null
+++ b/r/sedonadb/src/rust/src/error.rs
@@ -0,0 +1,58 @@
+// 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.
+use arrow_schema::ArrowError;
+use datafusion_common::DataFusionError;
+
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum RSedonaError {
+    #[error("{0}")]
+    DF(Box<DataFusionError>),
+    #[error("{0}")]
+    TokioIO(tokio::io::Error),
+    #[error("{0}")]
+    TokioJoin(tokio::task::JoinError),
+    #[error("{0}")]
+    Internal(String),
+    #[error("Interrupted")]
+    Interrupted,
+}
+
+impl From<DataFusionError> for RSedonaError {
+    fn from(other: DataFusionError) -> Self {
+        RSedonaError::DF(Box::new(other))
+    }
+}
+
+impl From<tokio::io::Error> for RSedonaError {
+    fn from(other: tokio::io::Error) -> Self {
+        RSedonaError::TokioIO(other)
+    }
+}
+
+impl From<tokio::task::JoinError> for RSedonaError {
+    fn from(other: tokio::task::JoinError) -> Self {
+        RSedonaError::TokioJoin(other)
+    }
+}
+
+impl From<ArrowError> for RSedonaError {
+    fn from(other: ArrowError) -> Self {
+        RSedonaError::DF(Box::new(DataFusionError::ArrowError(Box::new(other), 
None)))
+    }
+}
diff --git a/r/sedonadb/src/rust/src/lib.rs b/r/sedonadb/src/rust/src/lib.rs
new file mode 100644
index 0000000..edc2913
--- /dev/null
+++ b/r/sedonadb/src/rust/src/lib.rs
@@ -0,0 +1,42 @@
+// 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.
+// Example functions
+
+use std::ffi::c_void;
+
+use savvy::savvy;
+
+use savvy_ffi::R_NilValue;
+use sedona_adbc::AdbcSedonadbDriverInit;
+
+mod context;
+mod dataframe;
+mod error;
+mod runtime;
+
+#[savvy]
+fn sedonadb_adbc_init_func() -> savvy::Result<savvy::Sexp> {
+    let driver_init_void = AdbcSedonadbDriverInit as *mut c_void;
+
+    unsafe {
+        Ok(savvy::Sexp(savvy_ffi::R_MakeExternalPtr(
+            driver_init_void,
+            R_NilValue,
+            R_NilValue,
+        )))
+    }
+}
diff --git a/r/sedonadb/src/rust/src/runtime.rs 
b/r/sedonadb/src/rust/src/runtime.rs
new file mode 100644
index 0000000..fb1c535
--- /dev/null
+++ b/r/sedonadb/src/rust/src/runtime.rs
@@ -0,0 +1,153 @@
+// 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.
+use std::{
+    future::Future,
+    sync::{OnceLock, RwLock},
+    time::Duration,
+};
+
+use savvy::{savvy, savvy_err, savvy_init};
+use savvy_ffi::R_NilValue;
+use tokio::{runtime::Runtime, time::sleep};
+
+use crate::error::RSedonaError;
+
+pub fn wait_for_future_captured_r<F>(runtime: &Runtime, fut: F) -> 
Result<F::Output, RSedonaError>
+where
+    F: Future + Send + 'static,
+    F::Output: Send,
+{
+    const INTERVAL_CHECK_SIGNALS: Duration = Duration::from_millis(1_000);
+    let handle = runtime.spawn(async move {
+        tokio::pin!(fut);
+        loop {
+            tokio::select! {
+                res = &mut fut => break Ok(res),
+                _ = sleep(INTERVAL_CHECK_SIGNALS) => {
+                    let handle = R.get().unwrap().rt().spawn(async move {
+                        R.get().unwrap().check_signals()
+                    });
+                    handle.await??;
+                }
+            }
+        }
+    });
+
+    R.get().unwrap().rt().block_on(handle)?
+}
+
+struct RRuntime {
+    rt: Runtime,
+    check_interrupts_call: RwLock<Option<savvy::Sexp>>,
+    pkg_env: RwLock<Option<savvy::Sexp>>,
+}
+
+unsafe impl Send for RRuntime {}
+
+unsafe impl Sync for RRuntime {}
+
+impl RRuntime {
+    pub fn try_new() -> Result<Self, RSedonaError> {
+        let rt = tokio::runtime::Builder::new_current_thread()
+            .enable_all()
+            .build()?;
+        Ok(Self {
+            rt,
+            check_interrupts_call: RwLock::new(None),
+            pkg_env: RwLock::new(None),
+        })
+    }
+
+    pub fn rt(&self) -> &Runtime {
+        &self.rt
+    }
+
+    pub fn check_signals(&self) -> Result<(), RSedonaError> {
+        let (maybe_call, maybe_env) = match (
+            self.check_interrupts_call.try_read(),
+            self.pkg_env.try_read(),
+        ) {
+            (Ok(call), Ok(env)) => (call, env),
+            _ => {
+                return Err(RSedonaError::Internal(
+                    "Check interrupts call could not be read".to_string(),
+                ));
+            }
+        };
+
+        let (call, env) = match (maybe_call.as_ref(), maybe_env.as_ref()) {
+            (Some(call), Some(env)) => (call, env),
+            _ => {
+                return Err(RSedonaError::Internal(
+                    "Check interrupts not set".to_string(),
+                ));
+            }
+        };
+
+        unsafe {
+            // We could save this error for future things that can actually 
fail (like evaluating
+            // R UDFs)
+            let is_interrupted_sexp = savvy::unwind_protect(|| 
savvy_ffi::Rf_eval(call.0, env.0))
+                .expect(
+                "Check interrupts function must not error (i.e., must be 
wrapped in tryCatch())",
+            );
+
+            let is_interrupted: bool = savvy::Sexp(is_interrupted_sexp)
+                .try_into()
+                .expect("Check interrupts function must return a bool");
+
+            if is_interrupted {
+                Err(RSedonaError::Interrupted)
+            } else {
+                Ok(())
+            }
+        }
+    }
+}
+
+static R: OnceLock<RRuntime> = OnceLock::new();
+
+#[savvy_init]
+fn init_r_runtime(_dll_info: *mut savvy_ffi::DllInfo) -> savvy::Result<()> {
+    R.get_or_init(|| RRuntime::try_new().unwrap());
+    Ok(())
+}
+
+#[savvy]
+fn init_r_runtime_interrupts(
+    interrupts_call: savvy::Sexp,
+    pkg_env: savvy::Sexp,
+) -> savvy::Result<()> {
+    if let Some(r) = R.get() {
+        unsafe {
+            savvy::unwind_protect(|| {
+                savvy_ffi::R_PreserveObject(interrupts_call.0);
+                savvy_ffi::R_PreserveObject(pkg_env.0);
+                R_NilValue
+            })?;
+        }
+
+        r.check_interrupts_call
+            .try_write()?
+            .replace(interrupts_call);
+        r.pkg_env.try_write()?.replace(pkg_env);
+
+        Ok(())
+    } else {
+        Err(savvy_err!("R runtime not initialized"))
+    }
+}
diff --git a/r/sedonadb/src/sedonadb-win.def b/r/sedonadb/src/sedonadb-win.def
new file mode 100644
index 0000000..0a809fa
--- /dev/null
+++ b/r/sedonadb/src/sedonadb-win.def
@@ -0,0 +1,2 @@
+EXPORTS
+R_init_sedonadb
diff --git a/.gitignore b/r/sedonadb/tests/testthat.R
similarity index 66%
copy from .gitignore
copy to r/sedonadb/tests/testthat.R
index 7441737..fb41f2b 100644
--- a/.gitignore
+++ b/r/sedonadb/tests/testthat.R
@@ -14,32 +14,15 @@
 # KIND, either express or implied.  See the License for the
 # specific language governing permissions and limitations
 # under the License.
+# 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/testing-design.html#sec-tests-files-overview
+# * https://testthat.r-lib.org/articles/special-files.html
 
-# MacOS
-.DS_Store
-
-# sedona-cli
-.history
-
-# Generated by cargo
-debug/
-target/
-
-# Generated by rustfmt
-**/*.rs.bk
-
-# Generated by cargo llvm-cov
-coverage/
-
-# MSVC Windows builds of rustc generate these, which store debugging 
information
-*.pdb
-
-# IDE-specific settings
-.idea/
-.vscode/
-
-# documentation
-site/
+library(testthat)
+library(sedonadb)
 
-# Python cache files
-__pycache__
+test_check("sedonadb")
diff --git a/.gitignore b/r/sedonadb/tests/testthat/test-adbc.R
similarity index 69%
copy from .gitignore
copy to r/sedonadb/tests/testthat/test-adbc.R
index 7441737..d4e6b83 100644
--- a/.gitignore
+++ b/r/sedonadb/tests/testthat/test-adbc.R
@@ -15,31 +15,17 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# MacOS
-.DS_Store
-
-# sedona-cli
-.history
-
-# Generated by cargo
-debug/
-target/
-
-# Generated by rustfmt
-**/*.rs.bk
-
-# Generated by cargo llvm-cov
-coverage/
-
-# MSVC Windows builds of rustc generate these, which store debugging 
information
-*.pdb
-
-# IDE-specific settings
-.idea/
-.vscode/
-
-# documentation
-site/
-
-# Python cache files
-__pycache__
+test_that("adbc driver works", {
+  con <- sedonadb_adbc() |>
+    adbcdrivermanager::adbc_database_init() |>
+    adbcdrivermanager::adbc_connection_init()
+
+  df <-  con |>
+    adbcdrivermanager::read_adbc("SELECT ST_Point(0, 1) as geometry") |>
+    as.data.frame()
+
+  expect_identical(
+    wk::as_wkt(df$geometry),
+    wk::wkt("POINT (0 1)")
+  )
+})
diff --git a/.gitignore b/r/sedonadb/tests/testthat/test-context.R
similarity index 51%
copy from .gitignore
copy to r/sedonadb/tests/testthat/test-context.R
index 7441737..b6ea4e6 100644
--- a/.gitignore
+++ b/r/sedonadb/tests/testthat/test-context.R
@@ -15,31 +15,27 @@
 # specific language governing permissions and limitations
 # under the License.
 
-# MacOS
-.DS_Store
-
-# sedona-cli
-.history
-
-# Generated by cargo
-debug/
-target/
-
-# Generated by rustfmt
-**/*.rs.bk
-
-# Generated by cargo llvm-cov
-coverage/
-
-# MSVC Windows builds of rustc generate these, which store debugging 
information
-*.pdb
-
-# IDE-specific settings
-.idea/
-.vscode/
-
-# documentation
-site/
-
-# Python cache files
-__pycache__
+test_that("sd_read_parquet() works", {
+  path <- system.file("files/natural-earth_cities_geo.parquet", package = 
"sedonadb")
+  expect_identical(sd_count(sd_read_parquet(path)), 243)
+
+  expect_identical(sd_count(sd_read_parquet(c(path, path))), 243 * 2)
+})
+
+test_that("views can be created and dropped", {
+  df <- sd_sql("SELECT 1 as one")
+  expect_true(rlang::is_reference(sd_to_view(df, "foofy"), df))
+  expect_identical(
+    sd_sql("SELECT * FROM foofy") |> sd_collect(),
+    data.frame(one = 1)
+  )
+
+  expect_identical(
+    sd_view("foofy") |> sd_collect(),
+    data.frame(one = 1)
+  )
+
+  sd_drop_view("foofy")
+  expect_error(sd_sql("SELECT * FROM foofy"), "table '(.*?)' not found")
+  expect_error(sd_view("foofy"), "No table named 'foofy'")
+})
diff --git a/r/sedonadb/tests/testthat/test-dataframe.R 
b/r/sedonadb/tests/testthat/test-dataframe.R
new file mode 100644
index 0000000..f530f65
--- /dev/null
+++ b/r/sedonadb/tests/testthat/test-dataframe.R
@@ -0,0 +1,137 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+test_that("dataframe can be created from data.frame", {
+  df <- as_sedonadb_dataframe(data.frame(one = 1, two = "two"))
+  expect_s3_class(df, "sedonadb_dataframe")
+  expect_identical(sd_collect(df), data.frame(one = 1, two = "two"))
+
+  # Ensure that geo columns with crs are handled
+  df <- as_sedonadb_dataframe(
+    data.frame(
+      geom = wk::as_wkb(wk::wkt("POINT (0 1)", crs = "EPSG:32620"))
+    )
+  )
+
+  re_df <- sd_collect(df)
+  expect_identical(
+    wk::as_wkt(re_df$geom),
+    wk::wkt("POINT (0 1)", crs = wk::wk_crs_projjson("EPSG:32620"))
+  )
+})
+
+test_that("dataframe can be created from nanoarrow objects", {
+  r_df <- data.frame(geom = wk::as_wkb("POINT (0 1)"))
+
+  array <- nanoarrow::as_nanoarrow_array(r_df)
+  df <- as_sedonadb_dataframe(array)
+  expect_s3_class(df, "sedonadb_dataframe")
+  expect_identical(sd_collect(df, ptype = r_df), r_df)
+
+  stream <- nanoarrow::as_nanoarrow_array_stream(r_df)
+  df <- as_sedonadb_dataframe(stream, lazy = TRUE)
+  expect_s3_class(df, "sedonadb_dataframe")
+  expect_identical(sd_collect(df, ptype = r_df), r_df)
+
+  stream <- nanoarrow::as_nanoarrow_array_stream(r_df)
+  df <- as_sedonadb_dataframe(stream, lazy = FALSE)
+  expect_s3_class(df, "sedonadb_dataframe")
+  expect_identical(sd_collect(df, ptype = r_df), r_df)
+})
+
+test_that("dataframe property accessors work", {
+  df <- sd_sql("SELECT ST_Point(0, 1) as pt")
+  expect_identical(ncol(df), 1L)
+  expect_identical(nrow(df), NA_integer_)
+  expect_identical(colnames(df), "pt")
+})
+
+test_that("dataframe head() works", {
+  df <- sd_sql("SELECT 1 as one, 'two' as two")
+  expect_identical(
+    as.data.frame(head(df, 0)),
+    data.frame(one = double(), two = character())
+  )
+})
+
+test_that("dataframe rows can be counted", {
+  df <- sd_sql("SELECT 1 as one, 'two' as two")
+  expect_identical(sd_count(df), 1)
+})
+
+test_that("dataframe can be computed", {
+  df <- sd_sql("SELECT 1 as one, 'two' as two")
+  df_computed <- sd_compute(df)
+  expect_identical(sd_collect(df), sd_collect(df_computed))
+})
+
+test_that("dataframe can be collected", {
+  df <- sd_sql("SELECT 1 as one, 'two' as two")
+  expect_identical(
+    sd_collect(df),
+    data.frame(one = 1, two = "two")
+  )
+
+  expect_identical(
+    sd_collect(df, ptype = data.frame(one = integer(), two = character())),
+    data.frame(one = 1L, two = "two")
+  )
+})
+
+test_that("dataframe can be converted to an R data.frame", {
+  df <- sd_sql("SELECT 1 as one, 'two' as two")
+  expect_identical(
+    as.data.frame(df),
+    data.frame(one = 1, two = "two")
+  )
+})
+
+test_that("dataframe can be converted to an array stream", {
+  df <- sd_sql("SELECT 1 as one, 'two' as two")
+  stream <- nanoarrow::as_nanoarrow_array_stream(df)
+  expect_s3_class(stream, "nanoarrow_array_stream")
+  expect_identical(
+    as.data.frame(stream),
+    data.frame(one = 1, two = "two")
+  )
+})
+
+test_that("dataframe can be printed", {
+  df <- sd_sql("SELECT ST_Point(0, 1) as pt")
+  expect_output(expect_identical(print(df), df), "POINT")
+})
+
+test_that("dataframe print uses ASCII when requested", {
+  df <- sd_sql("SELECT ST_Point(0, 1) as pt")
+  withr::with_options(list(cli.unicode = FALSE), {
+    expect_output(print(df), "--+")
+  })
+})
+
+test_that("dataframe print limits max output based on options", {
+  df <- sd_sql("SELECT ST_Point(0, 1) as pt")
+  withr::with_options(list(pillar.print_max = 0), {
+    expect_output(print(df), "Preview of up to 0 row\\(s\\)")
+  })
+})
+
+test_that("dataframe print limits max output based on options", {
+  df <- sd_sql("SELECT 'a really really really really long string' as str")
+  withr::with_options(list(width = 10, cli.unicode = FALSE), {
+    expect_output(print(df), "| a r... |")
+  })
+})

Reply via email to