Skip to content

Commit b2d8cc6

Browse files
authored
feat(r/DataFrameSource): Use duckdb or SQLite (first available) (#178)
* refactor: DataFrameSource extends DBISource * feat(DataFrameSource): Choose between duckdb and sqlite engines * chore: Make duckdb an optional dependency * feat: Use duckdb or sqlite based on first available * chore(pkg-r): Avoid CRAN note about whisker * ci(pkg-r): Don't check-depends-only * chore: Add NEWS item
1 parent bed4182 commit b2d8cc6

File tree

12 files changed

+334
-204
lines changed

12 files changed

+334
-204
lines changed

.github/workflows/R-CMD-check.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,4 @@ jobs:
3131
uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1
3232
with:
3333
working-directory: ./pkg-r
34+
check-depends-only: false

pkg-r/DESCRIPTION

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ Imports:
2424
bslib,
2525
cli,
2626
DBI,
27-
duckdb,
2827
ellmer (>= 0.3.0),
2928
htmltools,
3029
lifecycle,
@@ -39,14 +38,15 @@ Imports:
3938
Suggests:
4039
bsicons,
4140
DT,
41+
duckdb,
4242
knitr,
4343
palmerpenguins,
4444
rmarkdown,
4545
RSQLite,
4646
shinytest2,
4747
testthat (>= 3.0.0),
4848
withr
49-
VignetteBuilder:
49+
VignetteBuilder:
5050
knitr
5151
Remotes:
5252
posit-dev/shinychat/pkg-r

pkg-r/NEWS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# querychat (development version)
22

3+
* `querychat()` and `QueryChat$new()` now use either `{duckdb}` or `{SQLite}` for the in-memory database backend for data frames, depending on which package is installed. If both are installed, `{duckdb}` will be preferred. You can explicitly choose the `engine` in `DataFrameSource$new()` or set `querychat.DataFrameSource.engine` option to choose a global default. (#178)
4+
35
* `QueryChat$sidebar()`, `QueryChat$ui()`, and `QueryChat$server()` now support an optional `id` parameter to enable use within Shiny modules. When used in a module UI function, pass `id = ns("your_id")` where `ns` is the namespacing function from `shiny::NS()`. In the corresponding module server function, pass the unwrapped ID to `QueryChat$server(id = "your_id")`. This enables multiple independent QueryChat instances from the same QueryChat object. (#172)
46

57
* `QueryChat$client()` can now create standalone querychat-enabled chat clients with configurable tools and callbacks, enabling use outside of Shiny applications. (#168)

pkg-r/R/DataSource.R

Lines changed: 61 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -89,31 +89,43 @@ DataSource <- R6::R6Class(
8989
#' Data Frame Source
9090
#'
9191
#' @description
92-
#' A DataSource implementation that wraps a data frame using DuckDB for SQL
93-
#' query execution.
92+
#' A DataSource implementation that wraps a data frame using DuckDB or SQLite
93+
#' for SQL query execution.
9494
#'
9595
#' @details
96-
#' This class creates an in-memory DuckDB connection and registers the provided
97-
#' data frame as a table. All SQL queries are executed against this DuckDB table.
96+
#' This class creates an in-memory database connection and registers the
97+
#' provided data frame as a table. All SQL queries are executed against this
98+
#' database table. See [DBISource] for the full description of available
99+
#' methods.
100+
#'
101+
#' By default, DataFrameSource uses the first available engine from duckdb
102+
#' (checked first) or RSQLite. You can explicitly set the `engine` parameter to
103+
#' choose between "duckdb" or "sqlite", or set the global option
104+
#' `querychat.DataFrameSource.engine` to choose the default engine for all
105+
#' DataFrameSource instances. At least one of these packages must be installed.
98106
#'
99107
#' @export
100108
#' @examples
101109
#' \dontrun{
102-
#' # Create a data frame source
110+
#' # Create a data frame source (uses first available: duckdb or sqlite)
103111
#' df_source <- DataFrameSource$new(mtcars, "mtcars")
104112
#'
105113
#' # Get database type
106-
#' df_source$get_db_type() # Returns "DuckDB"
114+
#' df_source$get_db_type() # Returns "DuckDB" or "SQLite"
107115
#'
108116
#' # Execute a query
109117
#' result <- df_source$execute_query("SELECT * FROM mtcars WHERE mpg > 25")
110118
#'
119+
#' # Explicitly choose an engine
120+
#' df_sqlite <- DataFrameSource$new(mtcars, "mtcars", engine = "sqlite")
121+
#'
111122
#' # Clean up when done
112123
#' df_source$cleanup()
124+
#' df_sqlite$cleanup()
113125
#' }
114126
DataFrameSource <- R6::R6Class(
115127
"DataFrameSource",
116-
inherit = DataSource,
128+
inherit = DBISource,
117129
private = list(
118130
conn = NULL
119131
),
@@ -125,94 +137,44 @@ DataFrameSource <- R6::R6Class(
125137
#' @param table_name Name to use for the table in SQL queries. Must be a
126138
#' valid table name (start with letter, contain only letters, numbers,
127139
#' and underscores)
140+
#' @param engine Database engine to use: "duckdb" or "sqlite". Set the
141+
#' global option `querychat.DataFrameSource.engine` to specify the default
142+
#' engine for all instances. If NULL (default), uses the first available
143+
#' engine from duckdb or RSQLite (in that order).
128144
#' @return A new DataFrameSource object
129145
#' @examples
130146
#' \dontrun{
131147
#' source <- DataFrameSource$new(iris, "iris")
132148
#' }
133-
initialize = function(df, table_name) {
149+
initialize = function(
150+
df,
151+
table_name,
152+
engine = getOption("querychat.DataFrameSource.engine", NULL)
153+
) {
134154
check_data_frame(df)
135155
check_sql_table_name(table_name)
136156

137-
self$table_name <- table_name
138-
139-
# Create DuckDB connection and register the data frame
140-
private$conn <- DBI::dbConnect(duckdb::duckdb(), dbdir = ":memory:")
141-
duckdb::duckdb_register(
142-
private$conn,
143-
table_name,
144-
df,
145-
experimental = FALSE
146-
)
147-
},
157+
engine <- engine %||% get_default_dataframe_engine()
158+
engine <- tolower(engine)
159+
arg_match(engine, c("duckdb", "sqlite"))
148160

149-
#' @description Get the database type
150-
#' @return The string "DuckDB"
151-
get_db_type = function() {
152-
"DuckDB"
153-
},
154-
155-
#' @description
156-
#' Get schema information for the data frame
157-
#'
158-
#' @param categorical_threshold Maximum number of unique values for a text
159-
#' column to be considered categorical (default: 20)
160-
#' @return A string describing the schema
161-
get_schema = function(categorical_threshold = 20) {
162-
check_number_whole(categorical_threshold, min = 1)
163-
get_schema_impl(private$conn, self$table_name, categorical_threshold)
164-
},
161+
self$table_name <- table_name
165162

166-
#' @description
167-
#' Execute a SQL query
168-
#'
169-
#' @param query SQL query string. If NULL or empty, returns all data
170-
#' @return A data frame with query results
171-
execute_query = function(query) {
172-
check_string(query, allow_null = TRUE, allow_empty = TRUE)
173-
if (is.null(query) || !nzchar(query)) {
174-
query <- paste0(
175-
"SELECT * FROM ",
176-
DBI::dbQuoteIdentifier(private$conn, self$table_name)
163+
# Create in-memory connection and register the data frame
164+
if (engine == "duckdb") {
165+
check_installed("duckdb")
166+
private$conn <- DBI::dbConnect(duckdb::duckdb(), dbdir = ":memory:")
167+
duckdb::duckdb_register(
168+
private$conn,
169+
table_name,
170+
df,
171+
experimental = FALSE
177172
)
173+
} else if (engine == "sqlite") {
174+
check_installed("RSQLite")
175+
private$conn <- DBI::dbConnect(RSQLite::SQLite(), ":memory:")
176+
DBI::dbWriteTable(private$conn, table_name, df)
178177
}
179-
DBI::dbGetQuery(private$conn, query)
180-
},
181-
182-
#' @description
183-
#' Test a SQL query by fetching only one row
184-
#'
185-
#' @param query SQL query string
186-
#' @return A data frame with one row of results
187-
test_query = function(query) {
188-
check_string(query, allow_null = TRUE, allow_empty = TRUE)
189-
if (is.null(query) || !nzchar(query)) {
190-
return(invisible(NULL))
191-
}
192-
193-
rs <- DBI::dbSendQuery(private$conn, query)
194-
df <- DBI::dbFetch(rs, n = 1)
195-
DBI::dbClearResult(rs)
196-
df
197-
},
198-
199-
#' @description
200-
#' Get all data from the table
201-
#'
202-
#' @return A data frame containing all data
203-
get_data = function() {
204-
self$execute_query(NULL)
205-
},
206-
207-
#' @description
208-
#' Close the DuckDB connection
209-
#'
210-
#' @return NULL (invisibly)
211-
cleanup = function() {
212-
if (!is.null(private$conn) && DBI::dbIsValid(private$conn)) {
213-
DBI::dbDisconnect(private$conn)
214-
}
215-
invisible(NULL)
216178
}
217179
)
218180
)
@@ -390,6 +352,22 @@ is_data_source <- function(x) {
390352
}
391353

392354

355+
get_default_dataframe_engine <- function() {
356+
if (is_installed("duckdb")) {
357+
return("duckdb")
358+
}
359+
if (is_installed("RSQLite")) {
360+
return("sqlite")
361+
}
362+
cli::cli_abort(c(
363+
"No compatible database engine installed for DataFrameSource",
364+
"i" = "Install either {.pkg duckdb} or {.pkg RSQLite}:",
365+
" " = "{.run install.packages(\"duckdb\")}",
366+
" " = "{.run install.packages(\"RSQLite\")}"
367+
))
368+
}
369+
370+
393371
get_schema_impl <- function(conn, table_name, categorical_threshold = 20) {
394372
# Get column information
395373
columns <- DBI::dbListFields(conn, table_name)

pkg-r/R/querychat-package.R

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,6 @@ release_bullets <- function() {
8484
}
8585

8686
suppress_rcmdcheck <- function() {
87-
duckdb::duckdb
8887
S7::S7_class
88+
whisker::whisker.render
8989
}

0 commit comments

Comments
 (0)