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")