@@ -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# ' }
114126DataFrameSource <- 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+
393391get_schema_impl <- function (conn , table_name , categorical_threshold = 20 ) {
394392 # Get column information
395393 columns <- DBI :: dbListFields(conn , table_name )
0 commit comments