forked from duckdb/duckdb-r
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathDriver.R
More file actions
497 lines (460 loc) · 18.9 KB
/
Copy pathDriver.R
File metadata and controls
497 lines (460 loc) · 18.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
# The driver and its `?duckdb` reference page.
# Explained in handbook/usage/connections/README.md, and for the extension
# section of that page, handbook/usage/extensions/README.md.
DBDIR_MEMORY <- ":memory:"
# `call` names the frame the flag was passed in, not this check: rlang's
# `abort()` would otherwise report `check_flag()` to someone who called
# `dbConnect()`. The base fallback ignores it and suppresses the call entirely.
check_flag <- function(x, call = parent.frame()) {
if (is.null(x) || length(x) != 1 || is.na(x) || !is.logical(x)) {
abort("flags need to be scalar logicals", call = call)
}
}
extptr_str <- function(e, n = 5) {
x <- rethrow_rapi_ptr_to_str(e)
substr(x, nchar(x) - n + 1, nchar(x))
}
drv_to_string <- function(drv, call = parent.frame()) {
if (!is(drv, "duckdb_driver")) {
abort("pass a duckdb_driver object", call = call)
}
sprintf(
"<duckdb_driver dbdir='%s' read_only=%s bigint=%s>",
drv@dbdir,
drv@read_only,
drv@convert_opts$bigint
)
}
driver_registry <- new.env(parent = emptyenv())
#' @description
#' `duckdb()` creates or reuses a database instance.
#'
#' @param home Root directory for DuckDB's downloaded extensions and stored secrets.
#' `NULL` (the default) resolves the location as described in [duckdb_storage]:
#' an existing `~/.duckdb`, else a per-session temporary directory
#' (with an offer to create `~/.duckdb` in interactive sessions).
#' Pass a path to use it as the root explicitly, creating it if needed.
#' Cannot be combined with `shared_home`.
#' Applied only when the database instance is created;
#' see the \sQuote{Database instances and driver reuse} section.
#' @param shared_home Opt in or out of the shared `~/.duckdb` location,
#' overriding the automatic resolution.
#' One of:
#' * `NULL` (the default) -- resolve automatically (see [duckdb_storage]).
#' This is the safe default.
#' * `TRUE` -- store extensions and secrets under `~/.duckdb`, **creating that
#' directory if it does not exist**.
#' This is a good setting for permanent deployments (Posit Connect, Shiny, APIs).
#' Do not use on CRAN or on other infrastructure where you don't own `~/.duckdb`.
#'
#' The setting is a durable, machine-level side effect that is *not* scoped to the current session:
#' the directory persists after R exits, is reused by every future R session
#' (and by the DuckDB CLI, Python and other clients that share `~/.duckdb`),
#' and any secrets written there outlive this process.
#' Applying this setting repeatedly is a fast no-op.
#' * `FALSE` -- use a per-session temporary directory even if `~/.duckdb`
#' already exists. Nothing persists beyond the session.
#'
#' Cannot be combined with `home`.
#' Applied only when the database instance is created;
#' see the \sQuote{Database instances and driver reuse} section.
#' @param allow_extensions `r lifecycle::badge("experimental")`
#' Whether this driver may load DuckDB extensions (`INSTALL` / `LOAD`).
#' One of:
#' * `NULL` (the default) -- decide automatically.
#' Extensions are enabled,
#' except on an affected Linux build (one not compiled with `libstdc++`),
#' where they are disabled and a throttled advisory message is shown.
#' See the \sQuote{DuckDB extensions on Linux} section.
#' * `TRUE` -- force-enable extensions,
#' attempting to load them even on an affected build (which may crash R).
#' No message.
#' * `FALSE` -- disable extensions and silence the advisory message.
#'
#' The argument takes precedence over the `duckdb.allow_extensions` option (a scalar logical)
#' and the `DUCKDB_R_ALLOW_EXTENSIONS` environment variable
#' (a value R reads as `TRUE` enables extensions and `FALSE` disables them;
#' unset, empty, or any other value is undecided).
#' Applied only when the database instance is created;
#' see the \sQuote{Database instances and driver reuse} section.
#' @param environment_scan Set to `TRUE` to treat
#' data frames from the calling environment as tables.
#' If a database table with the same name exists, it takes precedence.
#' The default of this setting may change in a future version.
#'
#' @return `duckdb()` returns an object of class [duckdb_driver-class].
#'
#' @section Database instances and driver reuse:
#'
#' `duckdb()` returns a driver object that owns a DuckDB *database instance*.
#' `dbConnect()` opens connections to that instance,
#' and many connections can share one instance.
#'
#' For a file-based `dbdir`, the instance is cached, keyed by the (normalized) path:
#' calling `duckdb()` again with the same `dbdir` returns the same driver and instance
#' while it is still alive.
#' This is deliberate.
#' DuckDB allows only a single read-write handle to a database file at a time,
#' so opening a second instance of the same file would fail with a lock error.
#' Reusing one instance instead lets any number of `dbConnect(duckdb(dbdir = "my.db"))` calls share it.
#' An in-memory database (`:memory:`, the default) has no file to lock and is never cached:
#' every `duckdb()` call creates a fresh, isolated instance.
#'
#' Because the instance is created once per database file,
#' `config`, `read_only`, `home`, and `shared_home` take effect only at creation.
#' A call that reuses an existing instance cannot apply them, and fails rather than dropping them.
#' Passing `dbdir` to `dbConnect()` fails too when the driver owns a database file of its own,
#' because the connection would go to `dbdir` while the driver kept its own database open.
#' To apply different values to a file-based database --
#' for example to reopen it read-only, or to send extensions and secrets elsewhere --
#' first release the instance with [duckdb_shutdown()], which also drops it from the cache,
#' then create it again.
#' [dbDisconnect()] only closes a connection,
#' it does not release the instance, and its `shutdown` argument is unused.
#' Instances are shut down automatically when the driver is garbage-collected or the session ends.
#'
#' @section DuckDB extensions on Linux:
#'
#' DuckDB's prebuilt extensions for Linux are compiled with the GNU C++ standard library (`libstdc++`).
#' Loading one into a `duckdb` package that was itself built with a *different* C++ standard library --
#' most commonly `libc++` (clang's `-stdlib=libc++`) --
#' is an ABI mismatch that crashes R (<https://github.com/duckdb/duckdb-r/issues/1107>).
#' Almost all Linux builds (CRAN binaries and most source installs) use `libstdc++` and are unaffected;
#' macOS and Windows are unaffected.
#'
#' Each `duckdb()` call decides whether the driver it returns may load extensions,
#' via the `allow_extensions` argument, the `duckdb.allow_extensions` option,
#' the `DUCKDB_R_ALLOW_EXTENSIONS` environment variable, or automatic detection.
#' On the automatic path a build that was not compiled with `libstdc++` on Linux disables extensions:
#' `INSTALL` / `LOAD` raise a clear error instead of crashing,
#' automatic extension install/load is turned off,
#' and a throttled advisory message is shown when `duckdb()` is called.
#' Pass `allow_extensions = FALSE` to disable extensions and silence that message,
#' or `allow_extensions = TRUE` to attempt loading anyway (which may still crash R).
#'
#' The decision is carried on the returned driver as the experimental `allow_extensions` slot
#' (see [duckdb_driver-class]).
#'
#' @import methods DBI
#' @export
duckdb <- function(
dbdir = DBDIR_MEMORY,
read_only = FALSE,
bigint = "numeric",
config = list(),
...,
home = NULL,
shared_home = NULL,
allow_extensions = NULL,
environment_scan = FALSE
) {
check_flag(read_only)
if (...length() > 0) {
abort("... must be empty")
}
if (
!is.null(shared_home) &&
!(is.logical(shared_home) &&
length(shared_home) == 1L &&
!is.na(shared_home))
) {
abort("`shared_home` must be TRUE, FALSE, or NULL.")
}
if (!is.null(home) && !is.null(shared_home)) {
abort("Pass either `home` or `shared_home`, not both.")
}
convert_opts <- duckdb_convert_opts(bigint = bigint)
dbdir <- path_normalize(dbdir)
if (dbdir != DBDIR_MEMORY) {
drv <- driver_registry[[dbdir]]
# We reuse an existing driver object if the database is still alive.
# If not, we fall back to creating a new driver object with a new database.
if (!is.null(drv) && rethrow_rapi_lock(drv@database_ref)) {
# Settings that bind at creation are dropped here. Saying so is
# duckdb/duckdb-r#2560; the bigint setting is not one of them, because
# dbConnect() picks it up, so we update it.
warn_instance_settings_ignored(
drv,
read_only = read_only,
config = config,
supplied = c(
if (!missing(home)) "home",
if (!missing(shared_home)) "shared_home",
if (!missing(allow_extensions)) "allow_extensions",
if (!missing(environment_scan)) "environment_scan"
)
)
drv@convert_opts <- convert_opts
drv@bigint <- convert_opts$bigint
return(drv)
}
}
# Decide once, past the driver-cache reuse above, whether this driver may load
# DuckDB extensions (argument > `duckdb.allow_extensions` option >
# `DUCKDB_R_ALLOW_EXTENSIONS` env var > auto). The resolved flag is plumbed
# into the engine via rapi_startup() and exposed as the driver's
# `allow_extensions` slot; on the auto path it also drives the advisory message
# below. Placed here so an argument a reused driver would ignore does not take
# effect.
ax <- resolve_allow_extensions(allow_extensions)
# Choose CRAN-safe locations for the engine's writable state unless the user
# set them explicitly. Extensions and secrets share a "home" directory
# resolved fresh on every call (an existing ~/.duckdb, else a temporary
# directory; see `?duckdb_storage`); the temp/spill directory is redirected
# for in-memory databases.
need_extension <- !("extension_directory" %in% names(config))
need_secret <- !("secret_directory" %in% names(config))
if (need_extension || need_secret) {
# An explicit `home`/`shared_home` means the user knows the storage
# settings; remember it so later auto-resolved calls this session stay
# quiet. Set here, past the driver-cache reuse above, so an argument that a
# reused driver would ignore does not silence future messages.
if (!is.null(home) || !is.null(shared_home)) {
mark_storage_choice_made()
}
resolved_home <- resolve_storage_home(home, shared_home)
if (need_extension) {
config[["extension_directory"]] <- home_subdir(
resolved_home$root,
"extensions"
)
}
if (need_secret) {
config[["secret_directory"]] <- home_subdir(
resolved_home$root,
"stored_secrets"
)
}
# Report where storage resolved (once), unless the caller chose the location
# explicitly with `home` or `shared_home` (or a `duckdb.home` option /
# `DUCKDB_R_HOME` variable, which yield sources "option"/"env"). A tempdir
# ("session") is announced in both modes -- non-interactively, and
# interactively when the user opted out of creating ~/.duckdb; an existing
# ~/.duckdb ("shared") is announced only non-interactively (interactively it
# is the user's own directory, used without a prompt). Once the user has
# made any explicit `home`/`shared_home` choice this session they have seen
# the settings, so we stay quiet from then on.
announce <- is.null(home) &&
is.null(shared_home) &&
!storage_choice_made() &&
(identical(resolved_home$source, "session") ||
(!is_interactive() && identical(resolved_home$source, "shared")))
if (announce) {
maybe_storage_location_message(resolved_home)
}
}
# When extensions are disallowed for this driver, also turn off automatic
# extension install/load so a query cannot implicitly pull in a prebuilt
# (libstdc++) extension and crash R (duckdb/duckdb-r#1107). Automatic loading
# is a separate engine mechanism the C++ INSTALL/LOAD guard does not
# intercept, so it must be disabled here. Explicit INSTALL/LOAD is refused in
# the engine glue (see rapi_prepare()). An explicit user setting wins.
if (!ax$allow) {
if (!("autoinstall_known_extensions" %in% names(config))) {
config[["autoinstall_known_extensions"]] <- "FALSE"
}
if (!("autoload_known_extensions" %in% names(config))) {
config[["autoload_known_extensions"]] <- "FALSE"
}
}
# Announce the disabled state, but only on the auto path (NULL argument and no
# option/env override) where extensions came out disabled -- an explicit
# argument/option/env silences it. Throttled like the storage message, and
# independent of the storage announce above.
if (ax$announce) {
maybe_extensions_message()
}
# Temporary storage stays on by default, with the CLI's semantics: an
# on-disk database keeps the engine's own `<dbdir>.tmp` default, and an
# in-memory database gets a per-instance directory under the session tempdir
# (the engine's own default, `.tmp` in the working directory, is not a place
# an R package should write). The resolved value goes into the startup
# config only, not into the driver's `config` slot: that slot seeds the
# instance re-created when `dbConnect()` is called with a different `dbdir`
# (see dbConnect__duckdb_driver), which must re-resolve for the new `dbdir`
# -- an on-disk database opened as `dbConnect(duckdb(), dbdir = ...)` would
# otherwise inherit the in-memory driver's spill path and never use
# `<dbdir>.tmp` (the duckdb/duckdb-r#1604 family). An explicit
# `temp_directory` in `config` is stored and honored as-is.
startup_config <- config
if (!("temp_directory" %in% names(config))) {
temp_directory <- resolve_temp_directory(dbdir)$directory
if (!is.null(temp_directory)) {
startup_config[["temp_directory"]] <- temp_directory
}
}
# Always create new database for in-memory,
# allows isolation and mixing different configs
drv <- new(
"duckdb_driver",
config = config,
database_ref = rethrow_rapi_startup(
dbdir,
read_only,
startup_config,
environment_scan,
ax$allow
),
dbdir = dbdir,
read_only = read_only,
convert_opts = convert_opts,
bigint = convert_opts$bigint,
allow_extensions = ax$allow
)
if (dbdir != DBDIR_MEMORY) {
driver_registry[[dbdir]] <- drv
}
reg.finalizer(drv@database_ref, onexit = TRUE, rapi_shutdown)
drv
}
#' @description
#' `duckdb_shutdown()` shuts down a database instance.
#'
#' @return `dbDisconnect()` and `duckdb_shutdown()` are called for their
#' side effect.
#' @rdname duckdb
#' @export
duckdb_shutdown <- function(drv) {
if (!is(drv, "duckdb_driver")) {
abort("pass a duckdb_driver object")
}
if (!dbIsValid(drv)) {
warning("invalid driver object, already closed?")
invisible(FALSE)
}
rethrow_rapi_shutdown(drv@database_ref)
if (drv@dbdir != DBDIR_MEMORY) {
rm(list = drv@dbdir, envir = driver_registry)
}
invisible(TRUE)
}
#' @description
#' Return an [adbcdrivermanager::adbc_driver()] for use with Arrow Database
#' Connectivity via the adbcdrivermanager package.
#'
#' @return An object of class "adbc_driver"
#' @rdname duckdb
#' @export
#' @examplesIf simulate_duckdb()$env$examples_enabled() && requireNamespace("adbcdrivermanager", quietly = TRUE)
#' library(adbcdrivermanager)
#' with_adbc(db <- adbc_database_init(duckdb_adbc()), {
#' as.data.frame(read_adbc(db, "SELECT 1 as one;"))
#' })
duckdb_adbc <- function() {
init_func <- structure(
rethrow_rapi_adbc_init_func(),
class = "adbc_driver_init_func"
)
adbcdrivermanager::adbc_driver(init_func, subclass = "duckdb_driver_adbc")
}
# Registered in zzz.R
adbc_database_init.duckdb_driver_adbc <- function(driver, ...) {
adbcdrivermanager::adbc_database_init_default(
driver,
list(...),
subclass = "duckdb_database_adbc"
)
}
adbc_connection_init.duckdb_database_adbc <- function(database, ...) {
adbcdrivermanager::adbc_connection_init_default(
database,
list(...),
subclass = "duckdb_connection_adbc"
)
}
adbc_statement_init.duckdb_connection_adbc <- function(connection, ...) {
adbcdrivermanager::adbc_statement_init_default(
connection,
list(...),
subclass = "duckdb_statement_adbc"
)
}
is_installed <- function(pkg) {
as.logical(requireNamespace(pkg, quietly = TRUE)) == TRUE
}
check_tz <- function(timezone) {
if (!is.null(timezone) && timezone == "") {
return("")
}
if (is.null(timezone) || !timezone %in% OlsonNames()) {
warning(
"Invalid time zone '",
timezone,
"', ",
"falling back to UTC.\n",
"Set the `timezone_out` argument to a valid time zone.\n",
call. = FALSE
)
return("UTC")
}
timezone
}
# `config`, `read_only` and the storage arguments describe the database
# *instance*, so a call that finds one in the registry cannot apply them. They
# are compared rather than merely counted as supplied: `dbConnect()` forwards
# the driver's own values, and repeating a setting the instance already has is
# not a collision. Explained in handbook/usage/connections/README.md;
# duckdb/duckdb-r#2560 asks for the noise, duckdb/duckdb-r#126 for the removal.
warn_instance_settings_ignored <- function(
drv,
read_only,
config,
supplied,
call = parent.frame()
) {
ignored <- supplied
if (!identical(read_only, drv@read_only)) {
ignored <- c(ignored, "read_only")
}
differs <- !vapply(
names(config),
function(name) identical(config[[name]], drv@config[[name]]),
logical(1)
)
if (any(differs)) {
ignored <- c(ignored, paste0("config$", names(config)[differs]))
}
if (length(ignored) == 0) {
return(invisible())
}
abort(
c(
paste0(
paste0("`", ignored, "`", collapse = ", "),
" can't be applied to the database instance for `",
drv@dbdir,
"`, which already exists."
),
"These settings take effect only when the instance is created.",
"Release it with `duckdb_shutdown()` first, or pass them to the `duckdb()` call that creates it."
),
call = call
)
}
# A `dbdir` an extension answers rather than the filesystem -- `md:` for
# MotherDuck, `ducklake:` for DuckLake -- is not a path. Normalizing one turns
# it into a local file name that no extension will ever be asked about.
#
# The rule is the engine's, from `ExtensionHelper::ExtractExtensionPrefixFromPath()`:
# at least two alphanumeric-or-underscore characters before the first colon,
# which keeps `C:\db` a Windows path, and `://` after them means a URL scheme
# rather than a prefix, which keeps `s3://` out.
has_extension_prefix <- function(path) {
grepl("^[[:alnum:]_]{2,}:(?!//)", path, perl = TRUE)
}
path_normalize <- function(path) {
if (path == "" || path == DBDIR_MEMORY) {
return(DBDIR_MEMORY)
}
if (has_extension_prefix(path)) {
return(path)
}
out <- normalizePath(path, mustWork = FALSE)
# Stable results are only guaranteed if the file exists
if (!file.exists(out)) {
on.exit(unlink(out))
writeLines(character(), out)
out <- normalizePath(out, mustWork = TRUE)
}
out
}