diff --git a/.github/workflows/R-CMD-check.yml b/.github/workflows/R-CMD-check.yml
index 6a6dd4d72..bf44cf23f 100644
--- a/.github/workflows/R-CMD-check.yml
+++ b/.github/workflows/R-CMD-check.yml
@@ -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
diff --git a/pkg-r/DESCRIPTION b/pkg-r/DESCRIPTION
index 77b7bc7cb..d9637ea5a 100644
--- a/pkg-r/DESCRIPTION
+++ b/pkg-r/DESCRIPTION
@@ -24,7 +24,6 @@ Imports:
bslib,
cli,
DBI,
- duckdb,
ellmer (>= 0.3.0),
htmltools,
lifecycle,
@@ -39,6 +38,7 @@ Imports:
Suggests:
bsicons,
DT,
+ duckdb,
knitr,
palmerpenguins,
rmarkdown,
@@ -46,7 +46,7 @@ Suggests:
shinytest2,
testthat (>= 3.0.0),
withr
-VignetteBuilder:
+VignetteBuilder:
knitr
Remotes:
posit-dev/shinychat/pkg-r
diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md
index 144a3f292..9ea2860db 100644
--- a/pkg-r/NEWS.md
+++ b/pkg-r/NEWS.md
@@ -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)
diff --git a/pkg-r/R/DataSource.R b/pkg-r/R/DataSource.R
index 92a217c1a..621c32e4e 100644
--- a/pkg-r/R/DataSource.R
+++ b/pkg-r/R/DataSource.R
@@ -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
),
@@ -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)
}
)
)
@@ -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)
diff --git a/pkg-r/R/querychat-package.R b/pkg-r/R/querychat-package.R
index bd28cf139..cc9656341 100644
--- a/pkg-r/R/querychat-package.R
+++ b/pkg-r/R/querychat-package.R
@@ -84,6 +84,6 @@ release_bullets <- function() {
}
suppress_rcmdcheck <- function() {
- duckdb::duckdb
S7::S7_class
+ whisker::whisker.render
}
diff --git a/pkg-r/man/DataFrameSource.Rd b/pkg-r/man/DataFrameSource.Rd
index ab2632c12..b33d1e66c 100644
--- a/pkg-r/man/DataFrameSource.Rd
+++ b/pkg-r/man/DataFrameSource.Rd
@@ -4,26 +4,38 @@
\alias{DataFrameSource}
\title{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 \link{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 \code{engine} parameter to
+choose between "duckdb" or "sqlite", or set the global option
+\code{querychat.DataFrameSource.engine} to choose the default engine for all
+DataFrameSource instances. At least one of these packages must be installed.
}
\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()
}
## ------------------------------------------------
@@ -34,29 +46,39 @@ df_source$cleanup()
source <- DataFrameSource$new(iris, "iris")
}
}
-\section{Super class}{
-\code{\link[querychat:DataSource]{querychat::DataSource}} -> \code{DataFrameSource}
+\section{Super classes}{
+\code{\link[querychat:DataSource]{querychat::DataSource}} -> \code{\link[querychat:DBISource]{querychat::DBISource}} -> \code{DataFrameSource}
}
\section{Methods}{
\subsection{Public methods}{
\itemize{
\item \href{#method-DataFrameSource-new}{\code{DataFrameSource$new()}}
-\item \href{#method-DataFrameSource-get_db_type}{\code{DataFrameSource$get_db_type()}}
-\item \href{#method-DataFrameSource-get_schema}{\code{DataFrameSource$get_schema()}}
-\item \href{#method-DataFrameSource-execute_query}{\code{DataFrameSource$execute_query()}}
-\item \href{#method-DataFrameSource-test_query}{\code{DataFrameSource$test_query()}}
-\item \href{#method-DataFrameSource-get_data}{\code{DataFrameSource$get_data()}}
-\item \href{#method-DataFrameSource-cleanup}{\code{DataFrameSource$cleanup()}}
\item \href{#method-DataFrameSource-clone}{\code{DataFrameSource$clone()}}
}
}
+\if{html}{\out{
+Inherited methods
+
+
+}}
\if{html}{\out{
}}
\if{html}{\out{}}
\if{latex}{\out{\hypertarget{method-DataFrameSource-new}{}}}
\subsection{Method \code{new()}}{
Create a new DataFrameSource
\subsection{Usage}{
-\if{html}{\out{}}\preformatted{DataFrameSource$new(df, table_name)}\if{html}{\out{
}}
+\if{html}{\out{}}\preformatted{DataFrameSource$new(
+ df,
+ table_name,
+ engine = getOption("querychat.DataFrameSource.engine", NULL)
+)}\if{html}{\out{
}}
}
\subsection{Arguments}{
@@ -67,6 +89,11 @@ Create a new DataFrameSource
\item{\code{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)}
+
+\item{\code{engine}}{Database engine to use: "duckdb" or "sqlite". Set the
+global option \code{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).}
}
\if{html}{\out{}}
}
@@ -83,106 +110,6 @@ source <- DataFrameSource$new(iris, "iris")
}
-}
-\if{html}{\out{
}}
-\if{html}{\out{}}
-\if{latex}{\out{\hypertarget{method-DataFrameSource-get_db_type}{}}}
-\subsection{Method \code{get_db_type()}}{
-Get the database type
-\subsection{Usage}{
-\if{html}{\out{}}\preformatted{DataFrameSource$get_db_type()}\if{html}{\out{
}}
-}
-
-\subsection{Returns}{
-The string "DuckDB"
-}
-}
-\if{html}{\out{
}}
-\if{html}{\out{}}
-\if{latex}{\out{\hypertarget{method-DataFrameSource-get_schema}{}}}
-\subsection{Method \code{get_schema()}}{
-Get schema information for the data frame
-\subsection{Usage}{
-\if{html}{\out{}}\preformatted{DataFrameSource$get_schema(categorical_threshold = 20)}\if{html}{\out{
}}
-}
-
-\subsection{Arguments}{
-\if{html}{\out{}}
-\describe{
-\item{\code{categorical_threshold}}{Maximum number of unique values for a text
-column to be considered categorical (default: 20)}
-}
-\if{html}{\out{
}}
-}
-\subsection{Returns}{
-A string describing the schema
-}
-}
-\if{html}{\out{
}}
-\if{html}{\out{}}
-\if{latex}{\out{\hypertarget{method-DataFrameSource-execute_query}{}}}
-\subsection{Method \code{execute_query()}}{
-Execute a SQL query
-\subsection{Usage}{
-\if{html}{\out{}}\preformatted{DataFrameSource$execute_query(query)}\if{html}{\out{
}}
-}
-
-\subsection{Arguments}{
-\if{html}{\out{}}
-\describe{
-\item{\code{query}}{SQL query string. If NULL or empty, returns all data}
-}
-\if{html}{\out{
}}
-}
-\subsection{Returns}{
-A data frame with query results
-}
-}
-\if{html}{\out{
}}
-\if{html}{\out{}}
-\if{latex}{\out{\hypertarget{method-DataFrameSource-test_query}{}}}
-\subsection{Method \code{test_query()}}{
-Test a SQL query by fetching only one row
-\subsection{Usage}{
-\if{html}{\out{}}\preformatted{DataFrameSource$test_query(query)}\if{html}{\out{
}}
-}
-
-\subsection{Arguments}{
-\if{html}{\out{}}
-\describe{
-\item{\code{query}}{SQL query string}
-}
-\if{html}{\out{
}}
-}
-\subsection{Returns}{
-A data frame with one row of results
-}
-}
-\if{html}{\out{
}}
-\if{html}{\out{}}
-\if{latex}{\out{\hypertarget{method-DataFrameSource-get_data}{}}}
-\subsection{Method \code{get_data()}}{
-Get all data from the table
-\subsection{Usage}{
-\if{html}{\out{}}\preformatted{DataFrameSource$get_data()}\if{html}{\out{
}}
-}
-
-\subsection{Returns}{
-A data frame containing all data
-}
-}
-\if{html}{\out{
}}
-\if{html}{\out{}}
-\if{latex}{\out{\hypertarget{method-DataFrameSource-cleanup}{}}}
-\subsection{Method \code{cleanup()}}{
-Close the DuckDB connection
-\subsection{Usage}{
-\if{html}{\out{}}\preformatted{DataFrameSource$cleanup()}\if{html}{\out{
}}
-}
-
-\subsection{Returns}{
-NULL (invisibly)
-}
}
\if{html}{\out{
}}
\if{html}{\out{}}
diff --git a/pkg-r/tests/testthat/_snaps/DataSource.md b/pkg-r/tests/testthat/_snaps/DataSource.md
index 70d79b18d..11eff61da 100644
--- a/pkg-r/tests/testthat/_snaps/DataSource.md
+++ b/pkg-r/tests/testthat/_snaps/DataSource.md
@@ -106,6 +106,30 @@
Error in `initialize()`:
! `table_name` must be a single string, not `NULL`.
+# DataFrameSource engine parameter / engine parameter validation / errors on invalid engine name
+
+ Code
+ DataFrameSource$new(new_test_df(), "test_table", engine = "postgres")
+ Condition
+ Error in `initialize()`:
+ ! `engine` must be one of "duckdb" or "sqlite", not "postgres".
+
+---
+
+ Code
+ DataFrameSource$new(new_test_df(), "test_table", engine = "invalid")
+ Condition
+ Error in `initialize()`:
+ ! `engine` must be one of "duckdb" or "sqlite", not "invalid".
+
+---
+
+ Code
+ DataFrameSource$new(new_test_df(), "test_table", engine = "")
+ Condition
+ Error in `initialize()`:
+ ! `engine` must be one of "duckdb" or "sqlite", not "".
+
# DBISource$new() / errors with non-DBI connection
Code
diff --git a/pkg-r/tests/testthat/helper-fixtures.R b/pkg-r/tests/testthat/helper-fixtures.R
index d9478e653..c98e06437 100644
--- a/pkg-r/tests/testthat/helper-fixtures.R
+++ b/pkg-r/tests/testthat/helper-fixtures.R
@@ -71,13 +71,21 @@ local_sqlite_connection <- function(
list(conn = conn, path = temp_db)
}
+# Skip test if no DataFrameSource engine is available
+skip_if_no_dataframe_engine <- function() {
+ if (!rlang::is_installed("duckdb") && !rlang::is_installed("RSQLite")) {
+ skip("Neither duckdb nor RSQLite is installed")
+ }
+}
+
# Create a DataFrameSource with automatic cleanup
local_data_frame_source <- function(
data,
table_name = "test_table",
+ engine = "duckdb",
env = parent.frame()
) {
- df_source <- DataFrameSource$new(data, table_name)
+ df_source <- DataFrameSource$new(data, table_name, engine = engine)
withr::defer(df_source$cleanup(), envir = env)
df_source
}
diff --git a/pkg-r/tests/testthat/test-DataSource.R b/pkg-r/tests/testthat/test-DataSource.R
index e386b2fa2..38c63dbb6 100644
--- a/pkg-r/tests/testthat/test-DataSource.R
+++ b/pkg-r/tests/testthat/test-DataSource.R
@@ -30,6 +30,8 @@ describe("DataSource base class", {
})
describe("DataFrameSource$new()", {
+ skip_if_no_dataframe_engine()
+
it("creates proper R6 object for DataFrameSource", {
test_df <- new_test_df()
@@ -63,6 +65,160 @@ describe("DataFrameSource$new()", {
})
})
+describe("DataFrameSource engine parameter", {
+ describe("with duckdb engine", {
+ skip_if_not_installed("duckdb")
+
+ it("creates connection with duckdb backend", {
+ df_source <- local_data_frame_source(new_test_df(), engine = "duckdb")
+
+ expect_s3_class(df_source, "DataFrameSource")
+ expect_s3_class(df_source, "DBISource")
+ expect_equal(df_source$table_name, "test_table")
+ expect_equal(df_source$get_db_type(), "DuckDB")
+ })
+
+ it("executes queries correctly", {
+ test_df <- new_test_df()
+ df_source <- local_data_frame_source(test_df, engine = "duckdb")
+
+ # Test filtering
+ result <- df_source$execute_query(
+ "SELECT * FROM test_table WHERE value > 25"
+ )
+ expect_equal(nrow(result), 3)
+ expect_equal(result$value, c(30, 40, 50))
+
+ # Test get_data
+ all_data <- df_source$get_data()
+ expect_equal(all_data, test_df)
+
+ # Test test_query
+ one_row <- df_source$test_query("SELECT * FROM test_table")
+ expect_equal(nrow(one_row), 1)
+ })
+
+ it("returns correct schema", {
+ df_source <- local_data_frame_source(
+ new_mixed_types_df(),
+ engine = "duckdb"
+ )
+ schema <- df_source$get_schema()
+
+ expect_type(schema, "character")
+ expect_match(schema, "Table: test_table")
+ expect_match(schema, "id \\(INTEGER\\)")
+ expect_match(schema, "name \\(TEXT\\)")
+ expect_match(schema, "active \\(BOOLEAN\\)")
+ })
+ })
+
+ describe("with sqlite engine", {
+ skip_if_not_installed("RSQLite")
+
+ it("creates connection with sqlite backend", {
+ df_source <- local_data_frame_source(new_test_df(), engine = "sqlite")
+
+ expect_s3_class(df_source, "DataFrameSource")
+ expect_s3_class(df_source, "DBISource")
+ expect_equal(df_source$table_name, "test_table")
+ expect_equal(df_source$get_db_type(), "SQLite")
+ })
+
+ it("executes queries correctly", {
+ test_df <- new_test_df()
+ df_source <- local_data_frame_source(test_df, engine = "sqlite")
+
+ # Test filtering
+ result <- df_source$execute_query(
+ "SELECT * FROM test_table WHERE value > 25"
+ )
+ expect_equal(nrow(result), 3)
+ expect_equal(result$value, c(30, 40, 50))
+
+ # Test get_data
+ all_data <- df_source$get_data()
+ expect_equal(all_data, test_df)
+
+ # Test test_query
+ one_row <- df_source$test_query("SELECT * FROM test_table")
+ expect_equal(nrow(one_row), 1)
+ })
+
+ it("returns correct schema", {
+ df_source <- local_data_frame_source(
+ new_mixed_types_df(),
+ engine = "sqlite"
+ )
+ schema <- df_source$get_schema()
+
+ expect_type(schema, "character")
+ expect_match(schema, "Table:")
+ expect_match(schema, "test_table")
+ expect_match(schema, "id \\(INTEGER\\)")
+ expect_match(schema, "name \\(TEXT\\)")
+ # SQLite stores booleans as INTEGER (0/1)
+ expect_match(schema, "active \\(INTEGER\\)")
+ })
+ })
+
+ describe("engine parameter validation", {
+ it("is case-insensitive", {
+ skip_if_not_installed("duckdb")
+ skip_if_not_installed("RSQLite")
+
+ # Test various case combinations
+ df1 <- local_data_frame_source(new_test_df(), engine = "DUCKDB")
+ expect_equal(df1$get_db_type(), "DuckDB")
+
+ df2 <- local_data_frame_source(new_test_df(), engine = "DuckDb")
+ expect_equal(df2$get_db_type(), "DuckDB")
+
+ df3 <- local_data_frame_source(new_test_df(), engine = "SQLite")
+ expect_equal(df3$get_db_type(), "SQLite")
+
+ df4 <- local_data_frame_source(new_test_df(), engine = "SQLITE")
+ expect_equal(df4$get_db_type(), "SQLite")
+ })
+
+ it("errors on invalid engine name", {
+ expect_snapshot(error = TRUE, {
+ DataFrameSource$new(new_test_df(), "test_table", engine = "postgres")
+ })
+
+ expect_snapshot(error = TRUE, {
+ DataFrameSource$new(new_test_df(), "test_table", engine = "invalid")
+ })
+
+ expect_snapshot(error = TRUE, {
+ DataFrameSource$new(new_test_df(), "test_table", engine = "")
+ })
+ })
+
+ it("respects getOption('querychat.DataFrameSource.engine')", {
+ skip_if_not_installed("duckdb")
+ skip_if_not_installed("RSQLite")
+
+ # Test default (duckdb)
+ withr::local_options(querychat.DataFrameSource.engine = NULL)
+ df1 <- DataFrameSource$new(new_test_df(), "test_table")
+ withr::defer(df1$cleanup())
+ expect_equal(df1$get_db_type(), "DuckDB")
+
+ # Test option set to sqlite
+ withr::local_options(querychat.DataFrameSource.engine = "sqlite")
+ df2 <- DataFrameSource$new(new_test_df(), "test_table")
+ withr::defer(df2$cleanup())
+ expect_equal(df2$get_db_type(), "SQLite")
+
+ # Test explicit parameter overrides option
+ withr::local_options(querychat.DataFrameSource.engine = "sqlite")
+ df3 <- local_data_frame_source(new_test_df(), engine = "duckdb")
+ expect_equal(df3$get_db_type(), "DuckDB")
+ })
+ })
+})
+
describe("DBISource$new()", {
it("creates proper R6 object for DBISource", {
db <- local_sqlite_connection(new_users_df(), "users")
@@ -114,6 +270,8 @@ describe("DBISource$new()", {
describe("DataSource$get_schema()", {
it("returns proper schema for DataFrameSource", {
+ skip_if_no_dataframe_engine()
+
df_source <- local_data_frame_source(new_mixed_types_df())
schema <- df_source$get_schema()
@@ -141,6 +299,8 @@ describe("DataSource$get_schema()", {
})
it("correctly reports min/max values for numeric columns", {
+ skip_if_no_dataframe_engine()
+
df_source <- local_data_frame_source(new_metrics_df())
schema <- df_source$get_schema()
@@ -152,9 +312,12 @@ describe("DataSource$get_schema()", {
})
describe("DataSource$get_db_type()", {
- it("returns DuckDB for DataFrameSource", {
+ it("returns correct database type for DataFrameSource", {
+ skip_if_no_dataframe_engine()
+
df_source <- local_data_frame_source(new_test_df())
- expect_equal(df_source$get_db_type(), "DuckDB")
+ db_type <- df_source$get_db_type()
+ expect_true(db_type %in% c("DuckDB", "SQLite"))
})
it("returns correct type for SQLite connections", {
@@ -171,6 +334,9 @@ describe("DataSource$get_data()", {
test_df <- new_test_df()
it("returns all data for both DataFrameSource and DBISource", {
+ skip_if_no_dataframe_engine()
+ skip_if_not_installed("RSQLite")
+
df_source <- local_data_frame_source(test_df)
result <- df_source$get_data()
@@ -189,6 +355,9 @@ describe("DataSource$get_data()", {
})
describe("DataSource$execute_query()", {
+ skip_if_no_dataframe_engine()
+ skip_if_not_installed("RSQLite")
+
test_df <- new_test_df(rows = 4)
df_source <- local_data_frame_source(test_df)
db <- local_sqlite_connection(test_df)
@@ -435,6 +604,8 @@ describe("DBISource$test_query()", {
})
describe("DataFrameSource$test_query()", {
+ skip_if_no_dataframe_engine()
+
test_df <- new_users_df()
df_source <- local_data_frame_source(test_df, "test_table")
diff --git a/pkg-r/tests/testthat/test-QueryChat.R b/pkg-r/tests/testthat/test-QueryChat.R
index 5c0f39c5d..b01a66c5e 100644
--- a/pkg-r/tests/testthat/test-QueryChat.R
+++ b/pkg-r/tests/testthat/test-QueryChat.R
@@ -1,4 +1,6 @@
describe("QueryChat$new()", {
+ skip_if_no_dataframe_engine()
+
it("automatically converts data.frame to DataFrameSource", {
qc <- QueryChat$new(
data_source = new_test_df(),
@@ -231,6 +233,8 @@ describe("QueryChat$system_prompt", {
})
describe("QueryChat$data_source", {
+ skip_if_no_dataframe_engine()
+
it("returns the data source object", {
test_df <- new_test_df()
qc <- QueryChat$new(test_df, greeting = "Test")
@@ -470,6 +474,7 @@ test_that("QueryChat$server() errors when called outside Shiny context", {
})
describe("querychat()", {
+ skip_if_no_dataframe_engine()
withr::local_envvar(OPENAI_API_KEY = "boop")
it("creates a QueryChat object", {
@@ -597,6 +602,8 @@ describe("QueryChat$console()", {
})
describe("normalize_data_source()", {
+ skip_if_no_dataframe_engine()
+
it("returns DataSource objects unchanged", {
test_df <- new_test_df()
df_source <- DataFrameSource$new(test_df, "test_df")
diff --git a/pkg-r/tests/testthat/test-QueryChatSystemPrompt.R b/pkg-r/tests/testthat/test-QueryChatSystemPrompt.R
index f781a3f99..9eb964b1f 100644
--- a/pkg-r/tests/testthat/test-QueryChatSystemPrompt.R
+++ b/pkg-r/tests/testthat/test-QueryChatSystemPrompt.R
@@ -1,4 +1,6 @@
describe("QueryChatSystemPrompt$new()", {
+ skip_if_no_dataframe_engine()
+
it("initializes with string template", {
df <- new_test_df()
ds <- DataFrameSource$new(df, "test_table")
diff --git a/pkg-r/tests/testthat/test-querychat_tools.R b/pkg-r/tests/testthat/test-querychat_tools.R
index 8c43b8380..991705149 100644
--- a/pkg-r/tests/testthat/test-querychat_tools.R
+++ b/pkg-r/tests/testthat/test-querychat_tools.R
@@ -1,4 +1,6 @@
test_that("tool_update_dashboard() checks inputs", {
+ skip_if_no_dataframe_engine()
+
expect_snapshot(error = TRUE, tool_update_dashboard("foo"))
df_source <- local_data_frame_source(new_test_df())
@@ -88,6 +90,8 @@ describe("querychat_tool_details_option()", {
})
describe("querychat_tool_result()", {
+ skip_if_no_dataframe_engine()
+
it("returns successful result for valid query action", {
df_source <- local_data_frame_source(new_test_df())
@@ -283,6 +287,8 @@ describe("querychat_tool_result()", {
})
describe("tool_query()", {
+ skip_if_no_dataframe_engine()
+
it("returns an ellmer tool object", {
df_source <- local_data_frame_source(new_test_df())
tool <- tool_query(df_source)
@@ -312,6 +318,8 @@ describe("tool_query()", {
})
describe("tool_update_dashboard()", {
+ skip_if_no_dataframe_engine()
+
it("returns an ellmer tool object", {
df_source <- local_data_frame_source(new_test_df())
@@ -370,6 +378,8 @@ describe("tool_reset_dashboard()", {
})
describe("tool_update_dashboard_impl()", {
+ skip_if_no_dataframe_engine()
+
it("returns a function", {
df_source <- local_data_frame_source(new_test_df())
current_query <- shiny::reactiveVal("SELECT * FROM test_table")