Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/R-CMD-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ jobs:
uses: rstudio/shiny-workflows/.github/workflows/R-CMD-check.yaml@v1
with:
working-directory: ./pkg-r
check-depends-only: false
4 changes: 2 additions & 2 deletions pkg-r/DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ Imports:
bslib,
cli,
DBI,
duckdb,
ellmer (>= 0.3.0),
htmltools,
lifecycle,
Expand All @@ -39,14 +38,15 @@ Imports:
Suggests:
bsicons,
DT,
duckdb,
knitr,
palmerpenguins,
rmarkdown,
RSQLite,
shinytest2,
testthat (>= 3.0.0),
withr
VignetteBuilder:
VignetteBuilder:
knitr
Remotes:
posit-dev/shinychat/pkg-r
Expand Down
2 changes: 2 additions & 0 deletions pkg-r/NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# querychat (development version)

* `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)

* `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)

* `QueryChat$client()` can now create standalone querychat-enabled chat clients with configurable tools and callbacks, enabling use outside of Shiny applications. (#168)
Expand Down
144 changes: 61 additions & 83 deletions pkg-r/R/DataSource.R
Original file line number Diff line number Diff line change
Expand Up @@ -89,31 +89,43 @@ DataSource <- R6::R6Class(
#' Data Frame Source
#'
#' @description
#' A DataSource implementation that wraps a data frame using DuckDB for SQL
#' query execution.
#' A DataSource implementation that wraps a data frame using DuckDB or SQLite
#' for SQL query execution.
#'
#' @details
#' This class creates an in-memory DuckDB connection and registers the provided
#' data frame as a table. All SQL queries are executed against this DuckDB table.
#' This class creates an in-memory database connection and registers the
#' provided data frame as a table. All SQL queries are executed against this
#' database table. See [DBISource] for the full description of available
#' methods.
#'
#' By default, DataFrameSource uses the first available engine from duckdb
#' (checked first) or RSQLite. You can explicitly set the `engine` parameter to
#' choose between "duckdb" or "sqlite", or set the global option
#' `querychat.DataFrameSource.engine` to choose the default engine for all
#' DataFrameSource instances. At least one of these packages must be installed.
#'
#' @export
#' @examples
#' \dontrun{
#' # Create a data frame source
#' # Create a data frame source (uses first available: duckdb or sqlite)
#' df_source <- DataFrameSource$new(mtcars, "mtcars")
#'
#' # Get database type
#' df_source$get_db_type() # Returns "DuckDB"
#' df_source$get_db_type() # Returns "DuckDB" or "SQLite"
#'
#' # Execute a query
#' result <- df_source$execute_query("SELECT * FROM mtcars WHERE mpg > 25")
#'
#' # Explicitly choose an engine
#' df_sqlite <- DataFrameSource$new(mtcars, "mtcars", engine = "sqlite")
#'
#' # Clean up when done
#' df_source$cleanup()
#' df_sqlite$cleanup()
#' }
DataFrameSource <- R6::R6Class(
"DataFrameSource",
inherit = DataSource,
inherit = DBISource,
private = list(
conn = NULL
),
Expand All @@ -125,94 +137,44 @@ DataFrameSource <- R6::R6Class(
#' @param table_name Name to use for the table in SQL queries. Must be a
#' valid table name (start with letter, contain only letters, numbers,
#' and underscores)
#' @param engine Database engine to use: "duckdb" or "sqlite". Set the
#' global option `querychat.DataFrameSource.engine` to specify the default
#' engine for all instances. If NULL (default), uses the first available
#' engine from duckdb or RSQLite (in that order).
#' @return A new DataFrameSource object
#' @examples
#' \dontrun{
#' source <- DataFrameSource$new(iris, "iris")
#' }
initialize = function(df, table_name) {
initialize = function(
df,
table_name,
engine = getOption("querychat.DataFrameSource.engine", NULL)
) {
check_data_frame(df)
check_sql_table_name(table_name)

self$table_name <- table_name

# Create DuckDB connection and register the data frame
private$conn <- DBI::dbConnect(duckdb::duckdb(), dbdir = ":memory:")
duckdb::duckdb_register(
private$conn,
table_name,
df,
experimental = FALSE
)
},
engine <- engine %||% get_default_dataframe_engine()
engine <- tolower(engine)
arg_match(engine, c("duckdb", "sqlite"))

#' @description Get the database type
#' @return The string "DuckDB"
get_db_type = function() {
"DuckDB"
},

#' @description
#' Get schema information for the data frame
#'
#' @param categorical_threshold Maximum number of unique values for a text
#' column to be considered categorical (default: 20)
#' @return A string describing the schema
get_schema = function(categorical_threshold = 20) {
check_number_whole(categorical_threshold, min = 1)
get_schema_impl(private$conn, self$table_name, categorical_threshold)
},
self$table_name <- table_name

#' @description
#' Execute a SQL query
#'
#' @param query SQL query string. If NULL or empty, returns all data
#' @return A data frame with query results
execute_query = function(query) {
check_string(query, allow_null = TRUE, allow_empty = TRUE)
if (is.null(query) || !nzchar(query)) {
query <- paste0(
"SELECT * FROM ",
DBI::dbQuoteIdentifier(private$conn, self$table_name)
# Create in-memory connection and register the data frame
if (engine == "duckdb") {
check_installed("duckdb")
private$conn <- DBI::dbConnect(duckdb::duckdb(), dbdir = ":memory:")
duckdb::duckdb_register(
private$conn,
table_name,
df,
experimental = FALSE
)
} else if (engine == "sqlite") {
check_installed("RSQLite")
private$conn <- DBI::dbConnect(RSQLite::SQLite(), ":memory:")
DBI::dbWriteTable(private$conn, table_name, df)
}
DBI::dbGetQuery(private$conn, query)
},

#' @description
#' Test a SQL query by fetching only one row
#'
#' @param query SQL query string
#' @return A data frame with one row of results
test_query = function(query) {
check_string(query, allow_null = TRUE, allow_empty = TRUE)
if (is.null(query) || !nzchar(query)) {
return(invisible(NULL))
}

rs <- DBI::dbSendQuery(private$conn, query)
df <- DBI::dbFetch(rs, n = 1)
DBI::dbClearResult(rs)
df
},

#' @description
#' Get all data from the table
#'
#' @return A data frame containing all data
get_data = function() {
self$execute_query(NULL)
},

#' @description
#' Close the DuckDB connection
#'
#' @return NULL (invisibly)
cleanup = function() {
if (!is.null(private$conn) && DBI::dbIsValid(private$conn)) {
DBI::dbDisconnect(private$conn)
}
invisible(NULL)
}
)
)
Expand Down Expand Up @@ -390,6 +352,22 @@ is_data_source <- function(x) {
}


get_default_dataframe_engine <- function() {
if (is_installed("duckdb")) {
return("duckdb")
}
if (is_installed("RSQLite")) {
return("sqlite")
}
cli::cli_abort(c(
"No compatible database engine installed for DataFrameSource",
"i" = "Install either {.pkg duckdb} or {.pkg RSQLite}:",
" " = "{.run install.packages(\"duckdb\")}",
" " = "{.run install.packages(\"RSQLite\")}"
))
}


get_schema_impl <- function(conn, table_name, categorical_threshold = 20) {
# Get column information
columns <- DBI::dbListFields(conn, table_name)
Expand Down
2 changes: 1 addition & 1 deletion pkg-r/R/querychat-package.R
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,6 @@ release_bullets <- function() {
}

suppress_rcmdcheck <- function() {
duckdb::duckdb
S7::S7_class
whisker::whisker.render
}
Loading
Loading