Skip to content

Commit fb5bf69

Browse files
authored
Merge branch 'main' into fix/r-system-prompt-no-query
2 parents 5268f4e + 091c0e2 commit fb5bf69

File tree

15 files changed

+375
-203
lines changed

15 files changed

+375
-203
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-py/docs/_examples/multiple-datasets.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,19 @@
1515
qc_penguins.ui()
1616

1717
with ui.nav_panel("Titanic"):
18+
1819
@render.data_frame
1920
def titanic_table():
2021
return qc_titanic.df()
2122

23+
2224
with ui.nav_panel("Penguins"):
25+
2326
@render.data_frame
2427
def penguins_table():
2528
return qc_penguins.df()
2629

30+
2731
ui.page_opts(
2832
id="navbar",
2933
title="Multiple Datasets with querychat",

pkg-py/docs/_examples/titanic-dashboard.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ def fare():
3232
avg = qc.df()["fare"].mean()
3333
return f"${avg:.2f}"
3434

35+
3536
with ui.layout_columns():
3637
with ui.card():
3738
with ui.card_header():
@@ -59,6 +60,7 @@ def survival_by_class():
5960
labels={"pclass": "Class", "survived": "Survival Rate"},
6061
)
6162

63+
6264
with ui.layout_columns():
6365
with ui.card():
6466
ui.card_header("Age Distribution")
@@ -76,6 +78,7 @@ def fare_by_class():
7678
df = qc.df()
7779
return px.box(df, x="pclass", y="fare", color="survived")
7880

81+
7982
ui.page_opts(
8083
title="Titanic Survival Analysis",
8184
fillable=True,

pkg-py/src/querychat/_datasource.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,11 +115,26 @@ def __init__(self, df: IntoFrame, table_name: str):
115115
Name of the table in SQL queries
116116
117117
"""
118-
self._conn = duckdb.connect(database=":memory:")
119118
self._df = nw.from_native(df)
120119
self.table_name = table_name
121-
# TODO(@gadenbuie): If the data frame is already SQL-backed, maybe we shouldn't be making a new copy here.
120+
121+
self._conn = duckdb.connect(database=":memory:")
122+
# TODO(@gadenbuie): What if the data frame is already SQL-backed?
122123
self._conn.register(table_name, self._df.lazy().collect().to_pandas())
124+
self._conn.execute("""
125+
-- extensions: lock down supply chain + auto behaviors
126+
SET allow_community_extensions = false;
127+
SET allow_unsigned_extensions = false;
128+
SET autoinstall_known_extensions = false;
129+
SET autoload_known_extensions = false;
130+
131+
-- external I/O: block file/database/network access from SQL
132+
SET enable_external_access = false;
133+
SET disabled_filesystems = 'LocalFileSystem';
134+
135+
-- freeze configuration so user SQL can't relax anything
136+
SET lock_configuration = true;
137+
""")
123138

124139
def get_db_type(self) -> str:
125140
"""

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: 78 additions & 80 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,64 @@ 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
157+
engine <- engine %||% get_default_dataframe_engine()
158+
engine <- tolower(engine)
159+
arg_match(engine, c("duckdb", "sqlite"))
138160

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-
},
161+
self$table_name <- table_name
148162

149-
#' @description Get the database type
150-
#' @return The string "DuckDB"
151-
get_db_type = function() {
152-
"DuckDB"
153-
},
163+
# Create in-memory connection and register the data frame
164+
if (engine == "duckdb") {
165+
check_installed("duckdb")
154166

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-
},
167+
private$conn <- DBI::dbConnect(duckdb::duckdb(), dbdir = ":memory:")
165168

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)
169+
duckdb::duckdb_register(
170+
private$conn,
171+
table_name,
172+
df,
173+
experimental = FALSE
177174
)
178-
}
179-
DBI::dbGetQuery(private$conn, query)
180-
},
181175

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)
176+
DBI::dbExecute(
177+
private$conn,
178+
r"(
179+
-- extensions: lock down supply chain + auto behaviors
180+
SET allow_community_extensions = false;
181+
SET allow_unsigned_extensions = false;
182+
SET autoinstall_known_extensions = false;
183+
SET autoload_known_extensions = false;
184+
185+
-- external I/O: block file/database/network access from SQL
186+
SET enable_external_access = false;
187+
SET disabled_filesystems = 'LocalFileSystem';
188+
189+
-- freeze configuration so user SQL can't relax anything
190+
SET lock_configuration = true;
191+
)"
192+
)
193+
} else if (engine == "sqlite") {
194+
check_installed("RSQLite")
195+
private$conn <- DBI::dbConnect(RSQLite::SQLite(), ":memory:")
196+
DBI::dbWriteTable(private$conn, table_name, df)
214197
}
215-
invisible(NULL)
216198
}
217199
)
218200
)
@@ -390,6 +372,22 @@ is_data_source <- function(x) {
390372
}
391373

392374

375+
get_default_dataframe_engine <- function() {
376+
if (is_installed("duckdb")) {
377+
return("duckdb")
378+
}
379+
if (is_installed("RSQLite")) {
380+
return("sqlite")
381+
}
382+
cli::cli_abort(c(
383+
"No compatible database engine installed for DataFrameSource",
384+
"i" = "Install either {.pkg duckdb} or {.pkg RSQLite}:",
385+
" " = "{.run install.packages(\"duckdb\")}",
386+
" " = "{.run install.packages(\"RSQLite\")}"
387+
))
388+
}
389+
390+
393391
get_schema_impl <- function(conn, table_name, categorical_threshold = 20) {
394392
# Get column information
395393
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)