| Title: | DBI Package for the DuckDB Database Management System |
| Version: | 1.5.6 |
| Description: | The DuckDB project is an embedded analytical data management system with support for the Structured Query Language (SQL). This package includes all of DuckDB and an R Database Interface (DBI) connector. |
| License: | MIT + file LICENSE |
| URL: | https://r.duckdb.org/, https://github.com/duckdb/duckdb-r |
| BugReports: | https://github.com/duckdb/duckdb-r/issues |
| Depends: | DBI, R (≥ 4.2.0) |
| Imports: | methods, utils |
| Suggests: | adbcdrivermanager, arrow (≥ 13.0.0), bit64, callr, clock, DBItest, dbplyr (≥ 2.6.0), dplyr, nanoarrow, rlang, sf, testthat (≥ 3.0.0), tibble, vctrs, wk, withr |
| Biarch: | true |
| Config/build/compilation-database: | false |
| Config/build/never-clean: | true |
| Config/comment/compilation-database: | Generate manually with pkgload:::generate_db() for faster pkgload::load_all() |
| Config/gha/extra-packages: | adbcdrivermanager=?ignore-unavailable&ignore-build-errors, arrow=?ignore-build-errors |
| Config/comment/gha/extra-packages: | arrow does not compile against Rtools45, and the aarch64 Windows runner has no binary to install instead. adbcdrivermanager needs ignore-unavailable as well, and needs it on every runner: CRAN archived it, and pak is called with dependencies = "all", which resolves Enhances too, so the package is in the plan and resolves to nothing. ignore-build-errors does not cover that -- it demotes a failed build, not a failed lookup. ignore-build-errors stays for the day it returns to CRAN and Rtools45 has to build it again. Explained in handbook/operations/ci/matrix/README.md. |
| Config/testthat/edition: | 3 |
| Encoding: | UTF-8 |
| SystemRequirements: | xz (for building from source) |
| Config/roxygen2/version: | 8.1.0.9000 |
| NeedsCompilation: | yes |
| Packaged: | 2026-09-28 19:19:26 UTC; runner |
| Author: | Hannes Mühleisen |
| Maintainer: | Kirill Müller <kirill@cynkra.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-09-29 08:50:02 UTC |
duckdb: DBI Package for the DuckDB Database Management System
Description
The DuckDB project is an embedded analytical data management system with support for the Structured Query Language (SQL). This package includes all of DuckDB and an R Database Interface (DBI) connector.
Author(s)
Maintainer: Kirill Müller kirill@cynkra.com (ORCID)
Authors:
Hannes Mühleisen hannes@cwi.nl (ORCID)
Mark Raasveldt mark.raasveldt@cwi.nl (ORCID)
Other contributors:
Stichting DuckDB Foundation [copyright holder]
Apache Software Foundation [copyright holder]
PostgreSQL Global Development Group [copyright holder]
The Regents of the University of California [copyright holder]
Cameron Desrochers [copyright holder]
Victor Zverovich [copyright holder]
RAD Game Tools [copyright holder]
Valve Software [copyright holder]
Rich Geldreich [copyright holder]
Tenacious Software LLC [copyright holder]
The RE2 Authors [copyright holder]
Google Inc. [copyright holder]
Facebook Inc. [copyright holder]
Steven G. Johnson [copyright holder]
Jiahao Chen [copyright holder]
Tony Kelman [copyright holder]
Jonas Fonseca [copyright holder]
Lukas Fittl [copyright holder]
Salvatore Sanfilippo [copyright holder]
Art.sy, Inc. [copyright holder]
Oran Agra [copyright holder]
Redis Labs, Inc. [copyright holder]
Melissa O'Neill [copyright holder]
PCG Project contributors [copyright holder]
See Also
Useful links:
Report bugs at https://github.com/duckdb/duckdb-r/issues
DuckDB SQL backend for dbplyr
Description
This is a SQL backend for dbplyr tailored to take into account DuckDB's possibilities. This mainly follows the backend for PostgreSQL, but contains more mapped functions.
tbl_file() is an experimental variant of dplyr::tbl() to directly access files on disk.
It is safer than dplyr::tbl() because there is no risk of misinterpreting the request,
and paths with special characters are supported.
tbl_function() is an experimental variant of dplyr::tbl() to create a lazy table from a table-generating function,
useful for reading nonstandard CSV files or other data sources.
It is safer than dplyr::tbl() because there is no risk of misinterpreting the query.
See https://duckdb.org/docs/data/overview for details on data importing functions.
As an alternative, use dplyr::tbl(src, dplyr::sql("SELECT ... FROM ...")) for custom SQL queries.
tbl_query() is deprecated in favor of tbl_function().
Use simulate_duckdb() with lazy_frame() to see simulated SQL without opening a DuckDB connection.
Usage
tbl_file(src = NULL, path, ..., cache = FALSE)
tbl_function(src, query, ..., cache = FALSE)
tbl_query(src, query, ...)
simulate_duckdb(...)
Arguments
src |
A duckdb connection object, |
path |
Path to existing Parquet, CSV or JSON file |
... |
Any parameters to be forwarded |
cache |
Enable object cache for Parquet files |
query |
SQL code, omitting the |
Examples
library(dplyr, warn.conflicts = FALSE)
con <- DBI::dbConnect(duckdb(), path = ":memory:")
db <- copy_to(con, data.frame(a = 1:3, b = letters[2:4]))
db %>%
filter(a > 1) %>%
select(b)
path <- tempfile(fileext = ".csv")
write.csv(data.frame(a = 1:3, b = letters[2:4]))
db_csv <- tbl_file(con, path)
db_csv %>%
summarize(sum_a = sum(a))
db_csv_fun <- tbl_function(con, paste0("read_csv_auto('", path, "')"))
db_csv %>%
count()
DBI::dbDisconnect(con, shutdown = TRUE)
Get the default connection
Description
default_conn() returns a default, built-in connection.
Usage
default_conn()
Details
Currently, the connection is established with duckdb(environment_scan = TRUE) and dbConnect(timezone_out = "", array = "matrix")
so that data frames are automatically available as tables,
timestamps are returned in the local timezone,
and DuckDB's array type is returned as an R matrix.
The details of how the connection is established are subject to change.
In particular, returning the output as a tibble or other object may be supported in the future.
This connection is intended for interactive use. There is no way for this or other packages to comprehensively track the state of this connection, so scripts and packages should manage their own connections.
Value
A DuckDB connection object
Examples
conn <- default_conn()
sql_query("SELECT 42", conn = conn)
Connect to a DuckDB database instance
Description
duckdb() creates or reuses a database instance.
duckdb_shutdown() shuts down a database instance.
Return an adbcdrivermanager::adbc_driver() for use with Arrow Database Connectivity via the adbcdrivermanager package.
dbConnect() connects to a database instance.
dbDisconnect() closes a DuckDB database connection.
The associated DuckDB database instance is shut down automatically,
it is no longer necessary to set shutdown = TRUE or to call duckdb_shutdown().
Usage
duckdb(
dbdir = DBDIR_MEMORY,
read_only = FALSE,
bigint = "numeric",
config = list(),
...,
home = NULL,
shared_home = NULL,
allow_extensions = NULL,
environment_scan = FALSE
)
duckdb_shutdown(drv)
duckdb_adbc()
## S4 method for signature 'duckdb_driver'
dbConnect(
drv,
dbdir = DBDIR_MEMORY,
...,
debug = getOption("duckdb.debug", FALSE),
read_only = FALSE,
timezone_out = "UTC",
tz_out_convert = c("with", "force"),
config = list(),
bigint = "numeric",
array = "none",
geometry = "blob",
map = "data.frame"
)
## S4 method for signature 'duckdb_connection'
dbDisconnect(conn, ..., shutdown = TRUE)
Arguments
dbdir |
Location for database files.
Should be a path to an existing directory in the file system.
With the default (or |
read_only |
Set to |
bigint |
How 64-bit integers should be returned.
There are two options: |
config |
Named list with DuckDB configuration flags, see https://duckdb.org/docs/configuration/overview#configuration-reference for the possible options. These flags are only applied when the database object is instantiated. Subsequent connections cannot apply them, and fail rather than ignoring them. |
... |
These dots are for future extensions and must be empty. |
home |
Root directory for DuckDB's downloaded extensions and stored secrets.
|
shared_home |
Opt in or out of the shared
Cannot be combined with |
allow_extensions |
The argument takes precedence over the |
environment_scan |
Set to |
drv |
Object returned by |
debug |
Print additional debug information, such as queries. |
timezone_out |
The time zone in which plain |
tz_out_convert |
How to convert timestamp columns to the timezone specified in |
array |
How arrays should be returned.
There are two options: |
geometry |
How geometry columns should be returned.
There are two options: |
map |
How |
conn |
A |
shutdown |
Unused. The database instance is shut down automatically. |
Details
The behavior of with = "force" at DST transitions depends on
how R handles translation from the underlying time representation to a human-readable format.
If the timestamp is invalid in the target timezone, the resulting value may be NA or an adjusted time.
Value
duckdb() returns an object of class duckdb_driver.
dbDisconnect() and duckdb_shutdown() are called for their side effect.
An object of class "adbc_driver"
dbConnect() returns an object of class duckdb_connection.
Database instances and driver reuse
duckdb() returns a driver object that owns a DuckDB database instance.
dbConnect() opens connections to that instance,
and many connections can share one instance.
For a file-based dbdir, the instance is cached, keyed by the (normalized) path:
calling duckdb() again with the same dbdir returns the same driver and instance while it is still alive.
This is deliberate.
DuckDB allows only a single read-write handle to a database file at a time,
so opening a second instance of the same file fails with a lock error in another process,
and, on Linux and macOS, is not prevented at all within the same one.
Reusing one instance instead lets any number of dbConnect(duckdb(dbdir = "my.db")) calls share it.
An in-memory database (:memory:, the default) has no file to lock and is never cached:
every duckdb() call creates a fresh, isolated instance.
The key is the path as the engine resolves it, not as normalizePath() does.
DuckDB canonicalizes the longest part of the path that exists and appends the rest,
so a database that does not exist yet gets the key it will keep once created,
and two spellings of one database (a relative path, a symlink, a different separator)
share an instance instead of each opening their own.
A path that resolves no further is used as it stands rather than refused.
Symbolic links are not supported on Windows,
where creating one takes administrator rights or Developer Mode:
there, a symlink to a database that does not exist yet fails to open.
Because the instance is created once per database file,
config, read_only, home, and shared_home take effect only at creation.
A call that reuses an existing instance cannot apply them, and fails rather than dropping them.
Passing dbdir to dbConnect() fails too when the driver owns a database file of its own,
because the connection would go to dbdir while the driver kept its own database open.
To apply different values to a file-based database –
for example to reopen it read-only, or to send extensions and secrets elsewhere –
first release the instance with duckdb_shutdown(), which also drops it from the cache,
then create it again.
dbDisconnect() closes one connection, and its shutdown argument is unused.
Connections keep the instance alive,
so it is released once the last connection to it closes,
unless a result not yet cleared with dbClearResult() or an Arrow stream not yet released still uses it.
The cache does not find an instance that only such a result or stream keeps open,
so duckdb() with the same dbdir then opens a second instance of the file in the same session.
A driver that was never connected to releases its instance
when the driver is garbage-collected or the session ends.
dbIsValid() reports whether a driver still holds an instance.
DuckDB extensions on Linux
DuckDB's prebuilt extensions for Linux are compiled with the GNU C++ standard library (libstdc++).
Loading one into a duckdb package that was itself built with a different C++ standard library –
most commonly libc++ (clang's -stdlib=libc++) –
is an ABI mismatch that crashes R (https://github.com/duckdb/duckdb-r/issues/1107).
Almost all Linux builds (CRAN binaries and most source installs) use libstdc++ and are unaffected;
macOS and Windows are unaffected.
Each duckdb() call decides whether the driver it returns may load extensions,
via the allow_extensions argument, the duckdb.allow_extensions option,
the DUCKDB_R_ALLOW_EXTENSIONS environment variable, or automatic detection.
On the automatic path a build that was not compiled with libstdc++ on Linux disables extensions:
INSTALL / LOAD raise a clear error instead of crashing,
automatic extension install/load is turned off,
and a throttled advisory message is shown when duckdb() is called.
Pass allow_extensions = FALSE to disable extensions and silence that message,
or allow_extensions = TRUE to attempt loading anyway (which may still crash R).
The decision is carried on the returned driver as the experimental allow_extensions slot (see duckdb_driver).
Examples
library(adbcdrivermanager)
with_adbc(db <- adbc_database_init(duckdb_adbc()), {
as.data.frame(read_adbc(db, "SELECT 1 as one;"))
})
drv <- duckdb()
con <- dbConnect(drv)
dbGetQuery(con, "SELECT 'Hello, world!'")
dbDisconnect(con)
duckdb_shutdown(drv)
# Shorter:
con <- dbConnect(duckdb())
dbGetQuery(con, "SELECT 'Hello, world!'")
dbDisconnect(con, shutdown = TRUE)
DuckDB connection class
Description
Implements DBIConnection.
Usage
## S4 method for signature 'duckdb_connection'
dbAppendTable(conn, name, value, ..., row.names = NULL)
## S4 method for signature 'duckdb_connection'
dbBegin(conn, ...)
## S4 method for signature 'duckdb_connection'
dbCommit(conn, ...)
## S4 method for signature 'duckdb_connection'
dbDataType(dbObj, obj, ...)
## S4 method for signature 'duckdb_connection,ANY'
dbExistsTable(conn, name, ...)
## S4 method for signature 'duckdb_connection'
dbGetInfo(dbObj, ...)
## S4 method for signature 'duckdb_connection'
dbIsValid(dbObj, ...)
## S4 method for signature 'duckdb_connection,Id'
dbListFields(conn, name, ...)
## S4 method for signature 'duckdb_connection,character'
dbListFields(conn, name, ...)
## S4 method for signature 'duckdb_connection'
dbListTables(conn, ...)
## S4 method for signature 'duckdb_connection,ANY'
dbQuoteIdentifier(conn, x, ...)
## S4 method for signature 'duckdb_connection'
dbQuoteLiteral(conn, x, ...)
## S4 method for signature 'duckdb_connection,character'
dbRemoveTable(conn, name, ..., fail_if_missing = TRUE)
## S4 method for signature 'duckdb_connection'
dbRollback(conn, ...)
## S4 method for signature 'duckdb_connection,character'
dbSendQueryArrow(conn, statement, params = NULL, ...)
## S4 method for signature 'duckdb_connection,character'
dbSendQuery(conn, statement, params = NULL, ..., arrow = FALSE)
## S4 method for signature 'duckdb_connection,character,data.frame'
dbWriteTable(
conn,
name,
value,
...,
row.names = FALSE,
overwrite = FALSE,
append = FALSE,
field.types = NULL,
temporary = FALSE
)
## S4 method for signature 'duckdb_connection'
show(object)
Arguments
conn |
A duckdb_connection object as returned by |
name |
The table name, passed on to
|
value |
A data.frame (or coercible to data.frame). |
... |
Other parameters passed on to methods. |
row.names |
Whether the row.names of the data.frame should be preserved |
dbObj |
An object inheriting from class duckdb_connection. |
obj |
An R object whose SQL type we want to determine. |
statement |
a character string containing SQL. |
params |
For |
arrow |
Whether the query should be returned as an Arrow Table |
overwrite |
If a table with the given name already exists, should it be overwritten? |
append |
If a table with the given name already exists, just try to append the passed data to it |
field.types |
Override the auto-generated SQL types |
temporary |
Should the created table be temporary? |
object |
Any R object |
Slots
conn_refexternal pointer to the underlying DuckDB connection.
driverthe duckdb_driver this connection was opened from.
debugwhether debug information (such as queries) is printed.
convert_optsinternal options controlling how result values are converted to R.
reserved_wordscharacter vector of the engine's reserved SQL keywords, used to quote identifiers.
timezone_outtime zone results are returned in; superseded by
convert_opts, from which it is copied at construction, and no longer read internally.tz_out_converthow timestamps are converted to
timezone_out("with"or"force"); superseded byconvert_opts.biginthow 64-bit integers are returned; superseded by
convert_opts.
Multiple statements
A statement can hold several SQL statements separated by semicolons,
in dbSendQuery(), dbSendQueryArrow(),
and the helpers built on them, such as dbExecute() and dbGetQuery().
They run in order, and each is prepared only after those before it have run,
so it sees their effects:
a PRAGMA that generates SQL, such as create_fts_index,
finds a table created earlier in the same string.
Every statement but the last runs when the query is sent,
params bind to the last statement only,
and only the last statement's result is returned.
The whole string is parsed before anything runs,
so a syntax error in any statement means that none of them run.
Any other error stops at the statement that raised it,
and the statements before it keep their effect,
because the string does not run in a transaction of its own.
For all or nothing, run the call inside dbWithTransaction(),
or call dbBegin() before it and dbRollback() if it fails.
To know which statements have run when one fails,
send one statement per call.
DuckDB driver class
Description
Implements DBIDriver.
Usage
## S4 method for signature 'duckdb_driver'
dbDataType(dbObj, obj, ...)
## S4 method for signature 'duckdb_driver'
dbGetInfo(dbObj, ...)
## S4 method for signature 'duckdb_driver'
dbIsValid(dbObj, ...)
## S4 method for signature 'duckdb_driver'
show(object)
Arguments
dbObj |
An object inheriting from class duckdb_driver. |
... |
Other arguments to methods. |
object |
Any R object |
Slots
database_refexternal pointer to the underlying DuckDB database instance.
confignamed list of DuckDB configuration flags applied when the instance was created.
dbdirpath to the database file, or
":memory:"for an in-memory database.read_onlywhether the database was opened read-only.
convert_optsinternal options controlling how result values are converted to R (bigint handling, time zone, ...).
biginthow 64-bit integers are returned (
"numeric"or"integer64").allow_extensionswhether this driver permits loading DuckDB extensions (
INSTALL/LOAD), resolved once when the driver is created. See theallow_extensionsargument ofduckdb().
DuckDB error conditions
Description
Every error the database engine raises reaches R as a condition of class duckdb_error,
carrying DuckDB's own classification alongside the message.
Catch it with tryCatch() or rlang::try_fetch() and branch on the fields rather than on the message text,
which is formatted for display and is not a stable interface.
Details
The condition is raised against the call that caused it – DBI::dbGetQuery(),
DBI::dbExecute(), DBI::dbBind(), DBI::dbFetch(), and the relational API alike – and the fields survive that rethrow.
Fields
error_typeDuckDB's exception type as a string, such as
"BINDER","PARSER","CONSTRAINT","CONVERSION","IO"or"OUT_OF_MEMORY". This is the field to classify on. The set is the engine's and grows with it, so treat an unrecognized value as "some other error" rather than failing on it.extra_infoA named character vector of whatever else the engine attached to the error, for instance the position within the query. Which names appear depends on the error, and empty is a normal answer.
contextThe internal operation that failed, such as
"rapi_prepare"or"rapi_execute". Useful to tell a failure at prepare time from one at execution time; the individual names are an implementation detail and may change.raw_messageThe message as the engine phrased it, without the exception type prefix and without the display formatting.
A field the engine did not supply is absent from the condition, so reading it gives NULL.
Classification code should therefore treat NULL as "unknown" and keep a fallback branch:
an error raised before the engine is reached, by an R-level check or by a failing callback,
is an ordinary error with none of these fields.
Errors are formatted with bullets when rlang is installed; without it the message is a single line, and the class and the fields are the same.
Examples
con <- dbConnect(duckdb())
err <- tryCatch(dbGetQuery(con, "SELECT missing_column"), error = identity)
class(err)
err$error_type
err$context
dbDisconnect(con, shutdown = TRUE)
DuckDB EXPLAIN query tree
Description
DuckDB EXPLAIN query tree
Memory-efficient reading and writing
Description
A result that fits comfortably in the database can still be too large for R, and a data frame that fits in R can still be copied on its way into the database. This page shows memory-efficient ways for reading and writing:
Memory limits
Streamed reading via Arrow
Registering a data frame for writing
The routes below keep R's share to one batch, or one data frame, at a time. Follow these recipes to process datasets larger than memory.
Set the limit where it takes effect
DuckDB and R allocate from two different budgets.
The engine's memory is bounded by memory_limit and can spill to disk;
R's vectors are unbounded and only spilled to the system swap space, if at all.
The engine's limit is set on the driver or on a live connection.
library(DBI) con <- dbConnect(duckdb(config = list(memory_limit = "2GB"))) # or, on a live connection: dbExecute(con, "SET memory_limit = '2GB'")
Reading: stream, and release each batch
Reading via Arrow stream never holds the result whole.
Execution starts at DBI::dbSendQueryArrow() and pauses at a bounded buffer.
Each DBI::dbFetchArrowChunk() hands over one batch that can be converted using as.data.frame().
For best results, release a batch immediately via nanoarrow::nanoarrow_pointer_release();
duckdb_result_arrow says what the release does and what happens without it.
rs <- dbSendQueryArrow(con, "SELECT * FROM huge")
repeat {
batch <- dbFetchArrowChunk(rs, chunk_size = 1e6)
if (batch$length == 0) break
df <- as.data.frame(batch)
# ... consume df ...
nanoarrow::nanoarrow_pointer_release(batch)
}
dbClearResult(rs)
Writing: hand the data frame over, and let the engine scan it
DBI::dbWriteTable() copies nothing on the R side:
it registers the data frame as a view that the engine scans in place, creates the table from that scan, and unregisters.
DBI::dbAppendTable() does the same into an existing table,
so data that arrives in pieces is appended piece by piece with the same cost.
dbWriteTable(con, "t", df) # one frame dbAppendTable(con, "t", next_df) # or piece by piece
Where no table is needed at all, duckdb_register() alone lets queries scan the frame with negligible overhead.
duckdb_register(con, "df", df) dbGetQuery(con, "SELECT count(*) FROM df WHERE a > 0.5")
Files: let the engine read them
DuckDB supports readers for various formats, bypassing R memory entirely.
dbExecute(con, "CREATE TABLE t AS SELECT * FROM read_parquet('data.parquet')")
Arrow streams: append a batch at a time
Data that arrives as an Arrow stream, from a pipe, a socket or another library's reader,
goes in through DBI::dbWriteTableArrow(), or DBI::dbAppendTableArrow() into an existing table.
Both pull one batch on R's thread and append it as a data frame.
stream <- nanoarrow::read_nanoarrow(file("data.arrows", "rb"))
dbWriteTableArrow(con, "t", stream)
Measurements
The method, every other route, and the same measurements through the Python, Node, Go and Rust clients
are recorded in the repository:
experiments/2026-09-14-memory-clients/ for reading,
experiments/2026-09-19-memory-ingest/ for writing.
See Also
duckdb_result_arrow for the Arrow result and what releases a batch,
duckdb_register() and duckdb_register_arrow() for scanning R data in place,
duckdb_storage for where the engine spills.
Reads a CSV file into DuckDB
Description
Directly reads a CSV file into DuckDB, tries to detect and create the correct schema for it. This usually is much faster than reading the data into R and writing it to DuckDB.
Usage
duckdb_read_csv(
conn,
name,
files,
...,
header = TRUE,
na.strings = "",
nrow.check = 500,
delim = ",",
quote = "\"",
col.names = NULL,
col.types = NULL,
lower.case.names = FALSE,
sep = delim,
transaction = TRUE,
temporary = FALSE
)
Arguments
conn |
A DuckDB connection, created by |
name |
The name for the virtual table that is registered or unregistered |
files |
One or more CSV file names, should all have the same structure though |
... |
These dots are for future extensions and must be empty. |
header |
Whether or not the CSV files have a separate header in the first line |
na.strings |
Which strings in the CSV files should be considered to be NULL |
nrow.check |
How many rows should be read from the CSV file to figure out data types |
delim |
Which field separator should be used |
quote |
Which quote character is used for columns in the CSV file |
col.names |
Override the detected or generated column names |
col.types |
Character vector of column types in the same order as col.names, or a named character vector where names are column names and types pairs. Valid types are DuckDB data types, e.g. VARCHAR, DOUBLE, DATE, BIGINT, BOOLEAN, etc. |
lower.case.names |
Transform column names to lower case |
sep |
Alias for delim for compatibility |
transaction |
Should a transaction be used for the entire operation |
temporary |
Set to |
Details
If the table already exists in the database, the csv is appended to it. Otherwise the table is created.
Value
The number of rows in the resulted table, invisibly.
Examples
con <- dbConnect(duckdb())
data <- data.frame(a = 1:3, b = letters[1:3])
path <- tempfile(fileext = ".csv")
write.csv(data, path, row.names = FALSE)
duckdb_read_csv(con, "data", path)
dbReadTable(con, "data")
dbDisconnect(con)
# Providing data types for columns
path <- tempfile(fileext = ".csv")
write.csv(iris, path, row.names = FALSE)
con <- dbConnect(duckdb())
duckdb_read_csv(con, "iris", path,
col.types = c(
Sepal.Length = "DOUBLE",
Sepal.Width = "DOUBLE",
Petal.Length = "DOUBLE",
Petal.Width = "DOUBLE",
Species = "VARCHAR"
)
)
dbReadTable(con, "iris")
dbDisconnect(con)
Register a data frame as a virtual table
Description
duckdb_register() registers a data frame as a virtual table (view) in a DuckDB connection.
No data is copied.
Usage
duckdb_register(conn, name, df, overwrite = FALSE, experimental = FALSE)
duckdb_unregister(conn, name)
Arguments
conn |
A DuckDB connection, created by |
name |
The name for the virtual table that is registered or unregistered |
df |
A |
overwrite |
Should an existing registration be overwritten? |
experimental |
Enable experimental optimizations |
Details
duckdb_unregister() unregisters a previously registered data frame.
Value
These functions are called for their side effect.
Examples
con <- dbConnect(duckdb())
data <- data.frame(a = 1:3, b = letters[1:3])
duckdb_register(con, "data", data)
dbReadTable(con, "data")
duckdb_unregister(con, "data")
dbDisconnect(con)
Register an Arrow data source as a virtual table
Description
duckdb_register_arrow() registers an Arrow data source as a virtual table (view) in a DuckDB connection.
No data is copied.
Usage
duckdb_register_arrow(conn, name, arrow_scannable, use_async = NULL)
duckdb_unregister_arrow(conn, name)
duckdb_list_arrow(conn)
Arguments
conn |
A DuckDB connection, created by |
name |
The name for the virtual table that is registered or unregistered |
arrow_scannable |
A scannable Arrow-object |
use_async |
Switched to the asynchronous scanner. (deprecated) |
Details
duckdb_unregister_arrow() unregisters a previously registered data frame.
Value
These functions are called for their side effect.
DuckDB Result Set
Description
Methods for accessing result sets for queries on DuckDB connections. Implements DBIResult.
Usage
duckdb_fetch_arrow(res, chunk_size = 1e+06)
duckdb_fetch_record_batch(res, chunk_size = 1e+06)
## S4 method for signature 'duckdb_result'
dbBind(res, params, ...)
## S4 method for signature 'duckdb_result'
dbClearResult(res, ...)
## S4 method for signature 'duckdb_result'
dbColumnInfo(res, ...)
## S4 method for signature 'duckdb_result'
dbFetch(res, n = -1, ...)
## S4 method for signature 'duckdb_result'
dbGetInfo(dbObj, ...)
## S4 method for signature 'duckdb_result'
dbGetRowCount(res, ...)
## S4 method for signature 'duckdb_result'
dbGetRowsAffected(res, ...)
## S4 method for signature 'duckdb_result'
dbGetStatement(res, ...)
## S4 method for signature 'duckdb_result'
dbHasCompleted(res, ...)
## S4 method for signature 'duckdb_result'
dbIsValid(dbObj, ...)
## S4 method for signature 'duckdb_result'
show(object)
Arguments
res |
Query result to be converted to a Record Batch Reader |
chunk_size |
The chunk size |
params |
For |
... |
Other arguments passed on to methods. |
n |
maximum number of records to retrieve per fetch. Use |
dbObj |
An object inheriting from class duckdb_result. |
object |
Any R object |
Slots
connectionthe duckdb_connection the query was executed on.
stmt_lstinternal list describing the prepared statement (names, types, ...).
envenvironment holding the result's mutable fetch state.
arrowwhether the result is fetched via Arrow.
query_resultexternal pointer to the underlying materialized query result.
DuckDB Arrow Result Set
Description
Streaming Arrow result for queries on DuckDB connections. Implements DBIResultArrow-class.
Usage
## S4 method for signature 'duckdb_result_arrow'
dbBind(res, params, ...)
## S4 method for signature 'duckdb_result_arrow'
dbBindArrow(res, params, ...)
## S4 method for signature 'duckdb_result_arrow'
dbClearResult(res, ...)
## S4 method for signature 'duckdb_result_arrow'
dbColumnInfo(res, ...)
## S4 method for signature 'duckdb_result_arrow'
dbFetchArrow(res, ..., chunk_size = 1e+06)
## S4 method for signature 'duckdb_result_arrow'
dbFetchArrowChunk(res, ..., chunk_size = 1e+06)
## S4 method for signature 'duckdb_result_arrow'
dbGetRowCount(res, ...)
## S4 method for signature 'duckdb_result_arrow'
dbGetRowsAffected(res, ...)
## S4 method for signature 'duckdb_result_arrow'
dbGetStatement(res, ...)
## S4 method for signature 'duckdb_result_arrow'
dbHasCompleted(res, ...)
## S4 method for signature 'duckdb_result_arrow'
dbIsValid(dbObj, ...)
## S4 method for signature 'duckdb_result_arrow'
show(object)
Arguments
res |
An object inheriting from DBI::DBIResult. |
params |
For |
... |
Other arguments passed on to methods. |
chunk_size |
The chunk size in rows used when pulling Arrow batches from DuckDB. |
dbObj |
An object inheriting from DBIObject, i.e. DBIDriver, DBIConnection, or a DBIResult |
object |
Any R object |
Slots
connectionthe duckdb_connection the query was executed on.
stmt_lstinternal list describing the prepared statement.
envenvironment holding the result's mutable fetch state.
Releasing a batch
Each batch that dbFetchArrowChunk() returns is a nanoarrow_array
whose buffers live outside R's heap, allocated by the engine.
They are freed by the batch's release callback, which runs in one of two ways.
nanoarrow::nanoarrow_pointer_release() runs it at once,
whatever else still refers to the batch, and gives the most control:
a loop that converts each batch and releases it holds one batch at a time,
however large the result.
Dropping the batch instead leaves the callback to R's garbage collector,
which runs on R's own allocations and never sees these buffers,
so batches accumulate until a collection happens;
gc() is the fallback that forces one,
and it frees a batch only if nothing refers to it any more.
Converting a batch with as.data.frame() copies numeric columns,
but character columns are converted lazily
and keep their part of the batch alive until they are materialized or dropped,
whichever way the batch itself was released.
Releasing a batch never affects the result it came from;
the next dbFetchArrowChunk() proceeds as before.
DuckDB file-system usage: storage locations and how they are resolved
Description
DuckDB writes several distinct kinds of data to the file system.
This page catalogs every such location and documents the policy the duckdb R package uses to choose them.
By default the package never creates anything in your home directory on its own:
downloaded extensions and stored secrets go under the R session's temporary directory
unless a ~/.duckdb directory already exists (or you point the package somewhere explicitly).
duckdb_storage_status() reports where each location currently resolves.
Usage
duckdb_storage_status()
Details
duckdb_storage_status() reports the directory the package would currently use for downloaded extensions and for persisted secrets,
and which tier of the resolution above chose it.
It has no side effects:
it never prompts and never creates a directory, so an as-yet-uncreated ~/.duckdb is reported as the per-session temporary default.
Value
duckdb_storage_status() returns a data frame (class "duckdb_storage_status") with one row per kind of state
and columns kind, source, and directory; its print method renders a readable summary when the result is auto-printed.
Kinds of on-disk state
- Home directory
The base DuckDB uses to expand a leading
~and to derive default sub-locations. DuckDB setting:home_directory. The package does not set this: doing so would also redirect~in user SQL (e.g.COPY ... TO '~/out.csv'). The extension and secret locations below are pointed at the resolved home root directly instead.- Extension binaries
Downloaded
*.duckdb_extensionfiles (e.g.spatial,httpfs,h3). DuckDB setting:extension_directory. A re-usable cache placed at<home>/extensions, where<home>is resolved as described below.- Stored secrets
Persisted credentials under
stored_secrets. DuckDB setting:secret_directory. Placed at<home>/stored_secrets, the same<home>.- Temporary / spill files
Out-of-core intermediates for sorts, hash joins, and similar operations. DuckDB settings:
temp_directory,max_temp_directory_size. Temporary storage is on by default, with the DuckDB CLI's semantics: the directory is created only when a query actually spills, and removed again when the database instance shuts down. An on-disk database keeps the engine's own default,<dbdir>.tmpnext to the database file. For an in-memory (:memory:) database the engine's own default would spill to.tmpin the current working directory, so the package points it at a fresh per-instance sub-directory belowtempdir()instead. Every in-memory instance gets its own spill directory: instances must not share one, because the engine's spill file names are deterministic and an instance cleans up its directory when it shuts down. This is a separate setting from the extension/secret home (see below).- Logs and profiling output
Written only when a path is explicitly configured (DuckDB settings
log_query_path,http_logging_output, profiling output). They default to off, so nothing is written without the user asking, and the user chooses where it goes.- Database file, WAL, and checkpoints
Chosen by the user through the
dbdirargument ofduckdb(). The package does not manage these.
Resolving the home directory
Extensions and secrets share one home root, resolved fresh on every call to duckdb() that creates a new database driver object.
The first source that yields a value wins:
the
homeargument toduckdb();the
duckdb.homeR option, e.g.options(duckdb.home = "/path/to/duckdb");the
DUCKDB_R_HOMEenvironment variable;-
~/.duckdb, if that directory already exists – the location shared with the DuckDB CLI and other clients; In interactive sessions only, the package offers to create
~/.duckdbonce: answer "yes" to create and use it, "no" to fall through to the temporary directory below, or cancel the prompt to abort with an error.Otherwise a per-session sub-directory of
tempdir().
The extension cache is then <home>/extensions and the secret store is <home>/stored_secrets.
Because the decision is remade on every new driver object,
creating ~/.duckdb (or setting the option/variable) takes effect immediately for drivers created afterwards.
Existing drivers are unaffected.
The shared_home argument of duckdb() overrides this resolution:
shared_home = TRUE uses (and creates) ~/.duckdb,
and shared_home = FALSE forces a per-session tempdir() even if ~/.duckdb already exists.
Per-location reference
| Kind | DuckDB setting | How to set it | Default |
| Home | home_directory | -- | left untouched (not set) |
| Extensions | extension_directory | home arg / duckdb.home / DUCKDB_R_HOME (as <home>/extensions) | tempdir() sub-directory (set) |
| Stored secrets | secret_directory | like extensions (<home>/stored_secrets) | tempdir() sub-directory (set) |
| Temp/spill | temp_directory | duckdb.temp_directory / DUCKDB_R_TEMP_DIRECTORY | memory: tempdir() sub-directory (set); disk: <dbdir>.tmp |
| Logs | log_query_path | DuckDB setting | disabled (off) |
"set" means duckdb() sets the value explicitly in the database config.
The home directory is left untouched so that ~ in user SQL keeps its usual meaning.
The temp/spill setting is left unset for an on-disk database:
the engine's own <dbdir>.tmp default already matches the DuckDB CLI,
no matter whether the database is opened through duckdb() or through the dbdir argument of DBI::dbConnect().
An extension_directory / secret_directory / temp_directory passed directly in the config list is always honored
and takes precedence over the resolution above.
Messages
- Storage-location message
When the package picked the location itself (a per-session
tempdir(), or an existing~/.duckdb),duckdb()emits an informational message describing where extensions and secrets are going and how to change it. It is throttled by session type: in an interactive session at most once every eight hours (a human can act on it); in a non-interactive session up to 60 times, after which it goes silent for good, so a long-running or automated process is not reminded forever. The message is suppressed entirely when you chose the location yourself – thehomeorshared_homeargument, theduckdb.homeoption, or theDUCKDB_R_HOMEenvironment variable. Non-interactively it covers both the temporary directory and an existing~/.duckdb; interactively it is issued only when the user opts out of creating~/.duckdb. It is also suppressed once you have made any explicithomeorshared_homechoice earlier in the session: having set the location explicitly once, you have seen how, so later auto-resolved calls stay quiet.
Silencing the message
Make the choice explicit and it is no longer announced.
Pass shared_home to duckdb() – TRUE to keep extensions and secrets under ~/.duckdb,
FALSE to accept a per-session temporary directory.
Alternatively, point home (or the duckdb.home option / DUCKDB_R_HOME variable) at a location of your choice.
As a last resort, use suppressMessages():
# Explicit arguments: con <- dbConnect(duckdb(shared_home = FALSE)) con <- dbConnect(duckdb(home = "/path/to/duckdb")) # As a fallback: con <- suppressMessages(dbConnect(duckdb())) # With configuration: Sys.setenv(DUCKDB_R_HOME = "/path/to/duckdb") con <- dbConnect(duckdb()) options(duckdb.home = "/path/to/duckdb") con <- dbConnect(duckdb())
Use by other packages
Packages that use duckdb inherit this policy:
duckdb never writes outside
tempdir()on its own during checks. In a non-interactive session (which allR CMD checkruns are) it usestempdir()by default unless a~/.duckdbalready exists, and it never creates~/.duckdbunless requested. So a package that merely opens a database needs no special handling.Downloading and installing an extension is the caller's responsibility. Ensure that all tests involving extensions are skipped if the download fails. For robust testing on CRAN and other platforms, ensure that the extensions your package uses can be downloaded and installed. Run the check in a subprocess to avoid crashing the main R process if the extension is incompatible with the platform. To force a throwaway cache in your own tests, connect with an explicit home:
tempdir_for_tests <- withr::local_tempdir() con <- DBI::dbConnect(duckdb(home = tempdir_for_tests))
See Also
duckdb() for the home and shared_home arguments.
Examples
duckdb_storage_status()
DuckDB data types in R
Description
This page documents every DuckDB type as it crosses to R and back: what a value of the type becomes when read, which R value writes it again, and what to do where neither works. See duckdb_types_arrow for a description of the conversion via Arrow.
The routes
Reading.
dbGetQuery() converts each column by its type, and four dbConnect() arguments change the shape,
with defaults bigint = "numeric", array = "none", map = "data.frame" and geometry = "blob".
dbGetQueryArrow() hands out the engine's own Arrow export instead,
and what each type becomes there, and in the R readers that convert the stream, is documented in duckdb_types_arrow.
A cast to VARCHAR in the query reads any type as text.
Writing.
dbWriteTable() and duckdb_register() take a column's type from its R class,
and field.types casts that column to the type it names, from any value that casts.
dbAppendTable() casts to the type of the existing column, and a parameter (params =) binds by its R class, cast by the query.
A character column holding a value's text form writes every scalar type through field.types, because DuckDB parses the text it prints.
Arrow, registered with duckdb_register_arrow(), writes the types no R class does.
Numbers
The numeric and boolean types:
-
BOOLEAN(BOOL,LOGICAL) reads aslogical, andlogicalwrites it. -
TINYINT,SMALLINT,UTINYINT,USMALLINTread asinteger, exactly.integerwritesINTEGER, andfield.typesnames the narrower type. -
INTEGER(INT4,INT,SIGNED) reads asinteger, exactly but for the minimum, andintegerwrites it. -
UINTEGERreads asnumeric, exactly;numericwritesDOUBLE, andfield.typesnamesUINTEGER. -
BIGINT(INT8,LONG) reads asnumeric, exact up to 2^53, and its rounding past that is a limitation. Withbigint = "integer64"it reads asbit64::integer64, exact but for the minimum. Aninteger64column or parameter writesBIGINTwhateverbigintsays. -
UBIGINTreads asnumeric, exact up to 2^53, and its rounding past that is a limitation. Withbigint = "integer64"it reads asinteger64, which holds the values below 2^63. Below 2^63, theinteger64it reads as writes it back throughfield.types, and its text writes any value. -
HUGEINT,UHUGEINTread asnumeric, andbigintdoes not change that; their rounding is a limitation. Their text is exact both ways. -
BIGNUM(VARINT) reads and writes through its text, and Arrow writes it (see duckdb_types_arrow). -
DECIMAL(width, scale)(NUMERIC) reads asnumericat every width; its rounding is a limitation. Its text is exact both ways, and Arrow writes it exactly. -
FLOAT(REAL) andDOUBLEread asnumeric, andnumericwritesDOUBLE.NaNreads and writes asNaN, never asNA, andNAisNULLin both directions.
Text and binary
The text, blob
and bitstring types, and UUID:
-
VARCHAR(CHAR,BPCHAR,TEXT,STRING) reads ascharacter, andcharacterwrites it; non-UTF-8 text is a limitation. -
BLOB(BYTEA,BINARY,VARBINARY) reads as a list of raw vectors. Ablob::blobor a list of raw vectors writes it. -
BIT(BITSTRING) reads and writes through its text. -
UUIDreads ascharacter, lowercase and hyphenated.characterwritesVARCHAR, andfield.typesmakes it aUUID.
Dates and times
The date, time, timestamp and interval types:
-
DATEreads asDate, and aDatewrites it, stored as double or as integer. -
TIMEreads asdifftimein seconds. Its text writes it throughfield.types, and so does Arrow (see duckdb_types_arrow). -
TIME_NSreads through Arrow, and to the microsecond through a cast toTIMEin the query. Its text writes it, and so does Arrow. -
TIMETZ(TIME WITH TIME ZONE) reads as thedifftimeof its local time. The offset it drops is a limitation. Its text writes it. -
TIMESTAMP_S,TIMESTAMP_MS,TIMESTAMP(DATETIME) read asPOSIXct.POSIXctwritesTIMESTAMP, the instant in UTC, andfield.typesnames the other precisions. -
TIMESTAMP_NSreads asPOSIXct.POSIXctwrites it to the microsecond throughfield.types, and Arrow writes it directly. -
TIMESTAMPTZ(TIMESTAMP WITH TIME ZONE) reads asPOSIXct.POSIXctwrites the plainTIMESTAMPof the same instant;field.typesmakes itTIMESTAMPTZ, and Arrow writes it directly. -
INTERVALreads asdifftimein seconds, counting a month as 30 days and a day as 24 hours. Adifftimein any unit, or anhms, writesINTERVAL.
Enums and nested types
The enum type, and the nested ones:
-
ENUMreads asfactor, with every value of the type as a level. Afactorororderedcolumn writesENUMof its levels; afactorparameter binds asVARCHAR. -
ARRAY(INTEGER[3]) reads witharray = "matrix", as a matrix with a row per value. A matrix column writes it. -
LIST(INTEGER[]) reads as a list of vectors,NULLfor aNULLrow, and a list column whose elements share a type writes it. -
MAPreads as a list ofdata.frame(key, value), which writes a list of structs unlessfield.typesnames the map. Withmap = "list_of", thevctrs::list_of()it reads as writes back asMAPwithoutfield.types(#200), and a list column of named lists writes a list of structs, an entry per name, valued by the first element of the name's value, or byNULLwhere that value isNULLor empty. Its text casts toMAPin the query and as a parameter. -
STRUCT(ROW) reads as a data frame column. A data frame column writes it, and a data frame parameter binds a struct per row. -
UNIONreads in the query throughunion_tag(),union_extract()or a cast toVARCHAR, and an Arrow result carries it. A column of a member's type writes it throughfield.types, which picks that member; text picks theVARCHARmember. -
VARIANTreads as a list, each value converted by its own type. A column of the value's type writes it throughfield.types.
Geometry
The GEOMETRY type, its coordinate reference system (CRS),
and the spatial extension's own types:
-
GEOMETRYreads as WKB. Withgeometry = "blob", the default, it reads as a list of raw vectors; withgeometry = "wk", aswk_wkb, carrying the column's CRS as an attribute, whichsf::st_as_sfc()converts onward, CRS included. The type is core since DuckDB 1.5, so reading one needs no extension; the geometry functions are thespatialextension's. Arrow carries the column as GeoArrow WKB with its CRS, in both directions (see duckdb_types_arrow). -
WKT writes
GEOMETRY. Acharactercolumn of WKT, assf::st_as_text()makes it, writes aGEOMETRYcolumn withfield.types = c(geom = "GEOMETRY")and appends to one withdbAppendTable(), because the cast fromVARCHARparses WKT. Naming the CRS in the type, as"GEOMETRY('EPSG:4267')", gives the column its CRS. Writing WKB as raw vectors, ansfobject or ansfccolumn is a limitation. -
The
spatialextension's own types are aliases, and read as what they alias.POINT_2D,POINT_3D,POINT_4D,BOX_2DandBOX_2DFare structs, and read as data frame columns;LINESTRING_2DandLINESTRING_3Dare lists of point structs, and read as lists of data frames;POLYGON_2DandPOLYGON_3Dare lists of those rings, and read as lists of lists;WKB_BLOBis aBLOB, and reads as raw vectors. The same shapes write the plain struct or list, andfield.typesnaming the alias casts back to it. They cast toGEOMETRYin the query, and the point, linestring, polygon and WKB types cast from it, as'POINT (1 2)'::GEOMETRY::POINT_2D.
Everything else
-
An untyped
NULLcomes back asNA_integer_, matching the engine's ownSELECT NULL; mapping it to logicalNAinstead was declined (#155). A typedNULL, as a scanned logical column or a boundNAparameter, round-trips as logicalNA. -
JSON, thejsonextension's alias ofVARCHAR, reads ascharacter. Its text writes it throughfield.types. -
INET, theinetextension's address type, reads as a data frame column whoseaddressis aHUGEINTread as a double, exact for IPv4; an IPv6 address is a limitation. Its text reads and writes it exactly.
Limitations and reference
The limitations are listed in the handbook, in usage/types/.
The mapping is implemented in src/types.cpp (R vector to LogicalType) and src/transform.cpp (the way back).
The list of types is DuckDB's own documentation for the release vendored here,
and every entry on this page was measured on DuckDB 1.5.5, in experiments/2026-09-26-type-catalog/,
experiments/2026-09-27-review-limits/,
experiments/2026-09-28-type-rereview/
or, for geometry route by route, experiments/2026-08-09-spatial-interop/.
Which zone labels a timestamp is documented in the handbook's timestamps/.
What expr_constant(NA) builds in the relational API is documented in the handbook's relational/.
DuckDB data types through Arrow
Description
This page documents every DuckDB type as it crosses to R through Arrow and back: the Arrow type the engine exports it as, what the two R readers make of that, which Arrow type writes it again, and which R functions keep Arrow's types on the way in. See duckdb_types for a description of the direct conversion to R vectors.
The routes
Reading.
Every function that returns Arrow hands out the engine's own export, under the connection's settings:
dbGetQueryArrow(), dbFetchArrow() and dbFetchArrowChunk() after dbSendQueryArrow(), dbReadTableArrow(),
duckdb_fetch_arrow() and duckdb_fetch_record_batch() after dbSendQuery(arrow = TRUE), and arrow::to_arrow().
No R vector exists until a reader converts the stream,
and the two readers differ, so each entry below names both:
nanoarrow's as.data.frame(), and arrow's as.data.frame() of arrow::as_arrow_table().
The export settings are DuckDB's, set with SET, and each changes the Arrow type of some columns:
-
arrow_lossless_conversion = trueexports each type that has no exact Arrow counterpart as an extension type that names it, so that DuckDB, or another Arrow consumer that knows the name, gets the type back. Where nanoarrow falls back to the storage of one, the entries below say so. -
arrow_large_buffer_size = truegives strings, binary data and lists 64-bit offsets, aslarge_string,large_binaryandlarge_list, which both readers convert as they convert the others. -
arrow_output_version,'1.0'by default, gates the newer layouts. From'1.4', binary data exports asbinary_view, andproduce_arrow_string_view = trueandarrow_output_list_view = truetake effect, asstring_viewandlist_view; with an older version those two change nothing. From'1.5', aDECIMALup to width 9 exports asdecimal32and up to width 18 asdecimal64. nanoarrow convertsstring_viewandbinary_view.
Writing.
Only the routes that let DuckDB scan the Arrow data keep its types:
duckdb_register_arrow(), and arrow::to_duckdb(), which calls it.
duckdb_register_arrow() takes whatever arrow::Scanner$create() scans.
Nothing is copied: the result is a view, and CREATE TABLE ... AS SELECT * FROM it writes a table.
A registered arrow Table is scanned by every query, and a registered RecordBatchReader is a limitation.
Every other route converts through an R data frame, so a column lands as the type its R vector writes (see duckdb_types).
dbWriteTableArrow(), dbCreateTableArrow() and dbAppendTableArrow() are DBI's defaults, which do that batch by batch.
dbBindArrow() converts the same way, then binds by position.
Reading
Numbers
-
BOOLEANexports asbooland reads aslogical; witharrow_lossless_conversion, asarrow.bool8, which nanoarrow reads as theintegerof its storage. -
TINYINT,SMALLINT,UTINYINT,USMALLINTexport asint8,int16,uint8anduint16, and read asinteger, exactly. -
INTEGERexports asint32and reads asinteger, exactly but for the minimum. -
UINTEGERexports asuint32. nanoarrow reads it asnumeric; arrow reads it asintegerwhen every value fits, and asnumericotherwise. -
BIGINTexports asint64. nanoarrow reads it asnumeric, exact up to 2^53. arrow reads it asintegerwhen every value fits and asbit64::integer64otherwise, exactly but for the minimum, or asinteger64always underoptions(arrow.int64_downcast = FALSE). -
UBIGINTexports asuint64, and both read it asnumeric, exact up to 2^53; its rounding past that is a limitation. -
HUGEINT,UHUGEINTexport asdecimal128(38, 0), and both read them asnumeric; their rounding is a limitation, and so is aUHUGEINTof 2^127 or more reading as negative. Witharrow_lossless_conversionthey export asarrow.opaque, which carries every value. Their text reads them exactly (see duckdb_types). -
BIGNUMexports asarrow.opaqueunder either setting; nanoarrow reads its storage bytes as ablob. -
DECIMAL(width, scale)exports asdecimal128(width, scale), or narrower from output version 1.5, and both read it asnumeric; its rounding is a limitation. -
FLOAT,DOUBLEexport asfloatanddouble, and read asnumeric.
Text and binary
-
VARCHARexports asstringand reads ascharacter. -
BLOBexports asbinary; nanoarrow reads it as ablob::blob, and arrow as anarrow_binarylist of raw vectors. -
BITexports asbinary, the bytes DuckDB stores for the bit string, and reads as abloborarrow_binaryof those bytes. Witharrow_lossless_conversionit exports asarrow.opaque, which nanoarrow reads as the same bytes. Its text reads it as0and1(see duckdb_types). -
UUIDexports asstring, lowercase and hyphenated, and reads ascharacter. Witharrow_lossless_conversionit exports asarrow.uuid.
Dates and times
-
DATEexports asdate32and reads asDate. -
TIMEexports astime64('us')and reads ashms. -
TIME_NSexports astime64('ns')and reads ashms. -
TIMETZexports as thetime64('us')of its local time and reads ashms; the offset it drops is a limitation. Witharrow_lossless_conversionit exports asarrow.opaque, which keeps the offset. -
TIMESTAMP_S,TIMESTAMP_MS,TIMESTAMP,TIMESTAMP_NSexport astimestampin their own unit, without a zone, and read asPOSIXct, whose double cannot hold aTIMESTAMP_NS's nanoseconds, a limitation. The two readers label the same instant differently: nanoarrow gives it the zoneUTC, so it prints the stored clock, and arrow gives it none, so it prints in R's session zone, a different clock outside UTC. -
TIMESTAMPTZexports astimestamp('us', zone), where the zone is DuckDB'sTimeZonesetting, and both readers read it as aPOSIXctlabelled with that zone. -
INTERVALexports asinterval_month_day_nano.
Enums and nested types
-
ENUMexports as a dictionary of its values; nanoarrow reads it ascharacter, and arrow asfactor. -
ARRAYexports as afixed_size_listand reads as a list of vectors (vctrs::list_of()orarrow_fixed_size_list), without thearray = "matrix"thatdbGetQuery()needs. -
LISTexports aslistand reads as a list of vectors (vctrs::list_of()orarrow_list). -
MAPexports asmapand reads as a list of key and value data frames. -
STRUCTexports asstructand reads as a data frame column, a tibble in arrow's case. -
UNIONexports as asparse_union. nanoarrow reads it as a data frame with a column per member,NAwhere the value is another member's.
Everything else
-
NULL, untyped, exports asint32and reads asNA_integer_, as it does throughdbGetQuery(). -
JSONexports asstringand reads ascharacter; witharrow_lossless_conversionit exports asarrow.json, which nanoarrow reads ascharacter. -
INETexports as a struct whoseaddressis theHUGEINTthe engine stores, as adecimal128(38, 0), which both read as a double, the same asdbGetQuery()does, and its text reads the address (see duckdb_types). Witharrow_lossless_conversionthat field becomesarrow.opaque.
Writing
Arrow types
Each Arrow type lands as one DuckDB type when DuckDB scans it:
-
bool,int8toint64,uint8touint64,float,doubleland asBOOLEAN, the integer type of the same width and sign,FLOATandDOUBLE. -
decimal32,decimal64,decimal128land asDECIMALof the same width and scale. -
string,large_string,string_viewland asVARCHAR, andbinary,large_binary,binary_view,fixed_size_binaryasBLOB. -
date32,date64land asDATE. -
time32andtime64('us')land asTIME, andtime64('ns')asTIME_NS. -
timestampwithout a zone lands asTIMESTAMP_S,TIMESTAMP_MS,TIMESTAMPorTIMESTAMP_NSby its unit, and with a zone asTIMESTAMPTZ, the same instant. -
durationlands asINTERVALin any unit. -
interval_monthsandinterval_month_day_nanoland asINTERVAL, each part kept. -
list,large_list,list_viewland asLIST, andfixed_size_listasARRAY. -
structlands asSTRUCT,mapasMAP, andsparse_unionasUNION. -
A dictionary lands as
VARCHAR. -
nalands as a column of typeNULL. -
The extension types land as the DuckDB type they name:
arrow.uuidasUUID,arrow.jsonasJSON,arrow.bool8asBOOLEAN, andarrow.opaqueas the DuckDB type in its metadata. What GeoArrow WKB lands as is under Geometry, and the other GeoArrow encodings are a limitation.
R classes, through Arrow
An R vector reaches DuckDB through Arrow as the Arrow type its package infers,
which differs from what dbWriteTable() gives for some classes.
Where nothing is said below, nanoarrow and arrow infer the type dbWriteTable() writes.
-
factorinfers a dictionary and lands asVARCHAR, wheredbWriteTable()writesENUM. arrow lands anorderedone asVARCHARtoo. -
POSIXctinfers atimestampwith its zone, or R's session zone where it has none, and lands asTIMESTAMPTZ, wheredbWriteTable()writes a plainTIMESTAMP. -
difftimeinfers adurationand lands asINTERVALin hours and below, so 2 days land as48:00:00, wheredbWriteTable()keeps the days. -
hmsinferstime32and lands asTIME, the one R route to that type; its truncation is a limitation. -
A plain list of vectors lands as
LISTthrough arrow. -
A matrix column lands as
ARRAYthrough nanoarrow.
Geometry
GEOMETRY and the spatial extension's own types through Arrow, and where the geoarrow and sf packages meet them:
-
GEOMETRYexports asgeoarrow.wkb, with the column's CRS in the field's metadata, as PROJJSON where the core orspatialknows the CRS, and as its identifier otherwise:OGC:CRS84exports as PROJJSON withoutspatial, andEPSG:4267only with it. It staysgeoarrow.wkbunder every export setting,arrow_lossless_conversionincluded;arrow_large_buffer_sizemakes its storagelarge_binary, and anarrow_output_versionfrom'1.4'makes itbinary_view. With the geoarrow package loaded, both readers convert it to ageoarrow_vctrin each of those layouts, arrow the view one included. -
sf reads a result through GeoArrow in one call. With geoarrow loaded,
sf::st_as_sf(dbGetQueryArrow(con, sql))gives ansfwhose geometries and CRS equal the source's, in the large and view layouts too, and so dosf::st_as_sf()of the result'sarrow::as_arrow_table(), andsf::st_as_sfc()of thegeoarrow_vctrcolumn ofas.data.frame(). -
The
spatialextension's own types cross as their storage.POINT_2Dand the other point and box types export as astruct,LINESTRING_2DandLINESTRING_3Das alistof point structs,POLYGON_2DandPOLYGON_3Das a list of those lists, andWKB_BLOBasbinary, witharrow_lossless_conversiontoo, and each reader converts them as it converts those Arrow types. -
GeoArrow WKB writes
GEOMETRYwith its CRS. Encode the geometry column as WKB, asgeoarrow::as_geoarrow_vctr(sf::st_geometry(x), schema = geoarrow::geoarrow_wkb(crs = sf::st_crs(x))), or as awk::as_wkb()column, which nanoarrow infers asgeoarrow.wkbonce geoarrow is loaded. Registerarrow::as_arrow_table(nanoarrow::as_nanoarrow_array_stream(df))withduckdb_register_arrow(), and aCREATE TABLE ... AS SELECTfrom the view writes aGEOMETRYcolumn with the CRS, which reads back into ansfequal to the one written. Withspatialloaded, the type names the CRS by its identifier, asGEOMETRY('EPSG:4267'); without it, it keeps the PROJJSON it came as, andsf::st_crs()reads the same CRS from both. WKB without a CRS lands as plainGEOMETRY, and so do the large and view layouts DuckDB's own export makes. -
A
geoarrow_vctrcolumn holds indices. Thegeoarrow_vctrthatas.data.frame()gives for a geometry in an Arrow result is an integer vector of indices into the Arrow data it holds, and writing it back is a limitation.
Limitations and reference
The limitations are listed in the handbook, in usage/arrow-types/.
The routes through R vectors are documented in duckdb_types,
and how a stream behaves, when it drains and what invalidates it, is documented in the handbook's integrations/.
Every entry on this page was measured on DuckDB 1.5.5, nanoarrow 0.9.0 and arrow 25.0.1
in experiments/2026-09-27-arrow-types/
and experiments/2026-09-28-type-rereview/,
and geometry also with geoarrow 0.4.4 and sf 1.1-3 in experiments/2026-09-27-geoarrow/.
Deprecated functions
Description
read_csv_duckdb() has been superseded by duckdb_read_csv().
The order of the arguments has changed.
Usage
read_csv_duckdb(conn, files, tablename, ...)
Run an SQL query or statement
Description
sql_query() runs an arbitrary SQL query using DBI::dbGetQuery() and returns a data.frame with the query results.
sql_exec() runs an arbitrary SQL statement using DBI::dbExecute() and returns the number of affected rows.
These functions are intended as an easy way to interactively run DuckDB without having to manage connections. By default, data frame objects are available as views.
Scripts and packages should manage their own connections and prefer the DBI methods for more control.
Usage
sql_query(sql, conn = default_conn())
sql_exec(sql, conn = default_conn())
Arguments
sql |
A SQL string |
conn |
An optional connection, defaults to |
Value
A data frame with the query result
Examples
# Queries
sql_query("SELECT 42")
# Statements with side effects
sql_exec("CREATE TABLE test (a INTEGER, b VARCHAR)")
sql_exec("INSERT INTO test VALUES (1, 'one'), (2, 'two')")
sql_query("FROM test")
# Data frames available as views
sql_query("FROM mtcars")
Stream a dbplyr table on DuckDB into Arrow
Description
to_arrow_stream() is a streaming counterpart of arrow::to_arrow()
for dbplyr tables on a DuckDB connection.
It sends the query with DBI::dbGetQueryArrow()
and hands the stream to arrow::as_record_batch_reader(),
so the rows arrive batch by batch and the result is not held twice.
arrow::to_arrow() materializes the whole result first,
through dbSendQuery(arrow = TRUE).
The streaming comes with hard limits, listed in the handbook, in
usage/integrations/.
Use to_arrow_stream() where a large result goes straight into Arrow
and nothing else runs on its connection until the reader has been read
to the end.
Where that cannot be arranged, use arrow::to_arrow(),
or run everything else on a second connection.
Usage
to_arrow_stream(.data)
Arguments
.data |
A dbplyr table on a DuckDB connection, or an Arrow object, which is returned unchanged. |
Value
An Arrow RecordBatchReader,
or an arrow_dplyr_query over it if .data is grouped.
Examples
con <- dbConnect(duckdb())
dbWriteTable(con, "mtcars", mtcars)
# Read the reader to the end before anything else runs on `con`.
reader <- to_arrow_stream(dplyr::filter(dplyr::tbl(con, "mtcars"), cyl == 4))
as.data.frame(reader$read_table())
# Another statement on `con` breaks a reader that has not been read yet.
reader <- to_arrow_stream(dplyr::tbl(con, "mtcars"))
dbGetQuery(con, "SELECT 1")
try(reader$read_table())
# A second connection to the same database leaves the reader alone.
other <- dbConnect(con@driver)
reader <- to_arrow_stream(dplyr::tbl(con, "mtcars"))
dbGetQuery(other, "SELECT count(*) FROM mtcars")
reader$read_table()$num_rows
dbDisconnect(other)
dbDisconnect(con)