A DataSource implementation that reads data from a
pins board. When the "duckdb" engine is used
and the pin type is one DuckDB can read natively (parquet, CSV, JSON), the
data is loaded directly from the cached pin files into DuckDB without
deserializing into R. For other pin types (e.g. RDS), or when the "sqlite"
engine is used, the data is deserialized via pin_read() and must produce
a data frame (or tibble), which is then registered with the chosen engine
just like DataFrameSource.
When loaded into DuckDB, the connection's external file access is locked down so that LLM-generated SQL cannot reach the filesystem.
Multiple pins (and pins mixed with data frames) can be combined in one
chat: every table is materialized into a shared DuckDB connection, so the
LLM can join and filter across them. Pins using engine = "sqlite" can't
join multi-table chats.
If the pin has a title, description, or tags, QueryChat uses them as
the default data_description, which you can override.
Lazy queries with pins
PinSource materializes the full dataset into DuckDB. For large parquet
pins where you want lazy query execution, read the pin files yourself and
pass a tbl_sql to querychat() instead:
paths <- pins::pin_download(board, "my_pin")
con <- DBI::dbConnect(duckdb::duckdb())
DBI::dbExecute(
con,
sprintf("CREATE VIEW my_pin AS SELECT * FROM read_parquet('%s')", paths[1])
)
qc <- querychat(dplyr::tbl(con, "my_pin"))The pin files are still downloaded to a local cache — pin_download()
always fetches them. But rather than loading everything into memory, DuckDB
reads the parquet file lazily through dbplyr.
This approach skips the security lockdown that PinSource applies, so
LLM-generated SQL can access files on the local system.
Super classes
DataSource -> DBISource -> PinSource
Methods
PinSource$new()
Create a new PinSource
Arguments
boardA pins board object (e.g. from
pins::board_folder()orpins::board_connect()).nameName of the pin to read.
...Not used; included for extensibility.
table_nameName to use for the table in SQL queries. Defaults to the pin name.
versionPin version to read. If
NULL(default), reads the latest version.engineDatabase engine to use:
"duckdb"or"sqlite". Set the global optionquerychat.DataFrameSource.engineto specify the default engine. IfNULL(default), uses the first available engine from duckdb or RSQLite (in that order). Parquet, CSV, and JSON pins are read most efficiently with the"duckdb"engine; with"sqlite"they are deserialized viapin_read()instead.
PinSource$register_into()
Materialize this pin into a shared DuckDB connection.
Internal hook for joining a shared DuckDBExecutor. The caller owns
con and locks it down once all tables are materialized.
PinSource$get_data_description()
Get a human-readable description of the pin for use in the system prompt.
PinSource$cleanup()
Disconnect the DuckDB or SQLite connection this PinSource opened, and shut down the DuckDB instance if used.
Unlike DBISource's cleanup(), this isn't a no-op: PinSource always
opens its own connection (never a caller-supplied one), so it owns it.
Examples
if (rlang::is_installed(c("pins", "duckdb"))) {
# Create a temporary board and pin some data
board <- pins::board_temp()
pins::pin_write(board, mtcars, "mtcars", type = "parquet")
# Create a PinSource
ps <- PinSource$new(board, "mtcars")
# Query the pinned data
ps$execute_query("SELECT * FROM mtcars WHERE mpg > 25")
ps$cleanup()
}
#> Creating new version '20260914T214756Z-c0340'
#> Writing to pin 'mtcars'
#> duckdb keeps downloaded extensions and secrets in a temporary directory:
#> ℹ /tmp/RtmpZdCyHx/duckdb
#> This is removed when the R session ends.
#> • Extensions are re-downloaded each session.
#> • Secrets are lost.
#> ℹ Run duckdb(shared_home = TRUE) (or create ~/.duckdb) to keep them (suitable for most users).
#> ℹ Run duckdb(shared_home = FALSE) to accept the temporary directory (and silence this message).
#> ℹ See ?duckdb_storage for details and alternatives.
