Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

transition to cli errors #785

Merged
merged 6 commits into from
Apr 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions R/connection-pane.R
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ computeDisplayName <- function(connection) {
display_name
}

validateObjectName <- function(table, view, ...) {
validateObjectName <- function(table, view, ..., call = caller_env()) {

# Handle view look-alike object types
# ( e.g. PostgreSQL/"matview" )
Expand All @@ -207,14 +207,7 @@ validateObjectName <- function(table, view, ...) {
view <- Reduce(`%||%`, args[viewlike], view)

# Error if both table and view are passed
if (!is.null(table) && !is.null(view)) {
stop("`table` and `view` can not both be used", call. = FALSE)
}

# Error if neither table and view are passed
if (is.null(table) && is.null(view)) {
stop("`table` and `view` can not both be `NULL`", call. = FALSE)
}
check_exclusive(table, view, .frame = call)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:chef kiss:

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUCH A SATISFYING DIFF


table %||% view
}
Expand All @@ -231,9 +224,10 @@ odbcListColumns.OdbcConnection <- function(connection,
check_string(catalog, allow_null = TRUE)
check_string(schema, allow_null = TRUE)

name <- validateObjectName(table, view, ...)
# specify schema or catalog if given
cols <- odbcConnectionColumns_(connection,
name = validateObjectName(table, view, ...),
name = name,
catalog_name = catalog,
schema_name = schema
)
Expand Down
2 changes: 1 addition & 1 deletion R/dbi-connection.R
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ setMethod("dbQuoteIdentifier", c("OdbcConnection", "character"),
return(DBI::SQL(character()))
}
if (any(is.na(x))) {
stop("Cannot pass NA to dbQuoteIdentifier()", call. = FALSE)
cli::cli_abort("{.arg x} can't be {.code NA}.")
}
if (nzchar(conn@quote)) {
x <- gsub(conn@quote, paste0(conn@quote, conn@quote), x, fixed = TRUE)
Expand Down
10 changes: 3 additions & 7 deletions R/dbi-driver.R
Original file line number Diff line number Diff line change
Expand Up @@ -251,14 +251,10 @@ odbc_data_type_df <- function(dbObj, obj, ...) {
res <- character(NCOL(obj))
nms <- names(obj)
for (i in seq_along(obj)) {
tryCatch(
withCallingHandlers(
res[[i]] <- odbcDataType(con = dbObj, obj[[i]]),
error = function(e) {
if (conditionMessage(e) == "Unsupported type") {
stop("Column '", nms[[i]], "' is of unsupported type: '", object_type(obj[[i]]), "'", call. = FALSE)
} else {
stop(e)
}
error = function(err) {
cli::cli_abort("Can't determine type for column {nms[[i]]}.", parent = err, call = quote(odbcDataType()))
}
)
}
Expand Down
5 changes: 3 additions & 2 deletions R/dbi-result.R
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ setMethod("dbClearResult", "OdbcResult",
#' @export
setMethod("dbFetch", "OdbcResult",
function(res, n = -1, ...) {
n <- check_n(n)
check_number_whole(n, min = -1, allow_infinite = TRUE)
if (is.infinite(n)) n <- -1
result_fetch(res@ptr, n)
}
)
Expand Down Expand Up @@ -92,7 +93,7 @@ setMethod("dbIsValid", "OdbcResult",
setMethod("dbGetStatement", "OdbcResult",
function(res, ...) {
if (!dbIsValid(res)) {
stop("Result already cleared", call. = FALSE)
cli::cli_abort("Result already cleared.")
}
res@statement
}
Expand Down
2 changes: 1 addition & 1 deletion R/dbi-table.R
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ setMethod("dbExistsTable", c("OdbcConnection", "SQL"),
#' @export
setMethod("dbExistsTable", c("OdbcConnection", "character"),
function(conn, name, ...) {
stopifnot(length(name) == 1)
check_string(name)
df <- odbcConnectionTables(conn, name = name, ..., exact = TRUE)
NROW(df) > 0
}
Expand Down
2 changes: 1 addition & 1 deletion R/driver-sql-server.R
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ setMethod("isTempTable", c("Microsoft SQL Server", "SQL"),
#' @usage NULL
setMethod("dbExistsTable", c("Microsoft SQL Server", "character"),
function(conn, name, ...) {
stopifnot(length(name) == 1)
check_string(name)
if (isTempTable(conn, name, ...)) {
query <- paste0("SELECT OBJECT_ID('tempdb..", name, "')")
!is.na(dbGetQuery(conn, query)[[1]])
Expand Down
15 changes: 10 additions & 5 deletions R/odbc-connection.R
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,10 @@ OdbcConnection <- function(
timeout = Inf,
dbms.name = NULL,
attributes = NULL,
.connection_string = NULL
.connection_string = NULL,
call = caller_env(2)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using caller_env(2) as caller_env() shows an unhelpful ".local()" frame:

test_con("SQLITE", attributes = list(boop = "bop"))
#> Error in `.local()`:
#> ! `attributes` does not support the connection attribute "boop".
#> ℹ Allowed connection attribute is "azure_token".

with:

 3.   └─odbc::dbConnect(...)
 4.     └─odbc (local) .local(drv, ...)
 5.       └─odbc:::OdbcConnection(...) at odbc/R/dbi-driver.R:184:5

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FWIW, S4 adds the intermediate.local wrapper when the arguments of the generic and the method differ in some way. I think it happens here because our method adds the dsn argument before the docs. (No need to change anything, I just thought you might want to know where this was coming from)

) {

stopifnot(all(has_names(attributes)))
stopifnot(all(names(attributes) %in% SUPPORTED_CONNECTION_ATTRIBUTES))
check_attributes(attributes, call = call)

args <- compact(list(...))
check_args(args)
Expand Down Expand Up @@ -51,7 +50,13 @@ OdbcConnection <- function(
info$dbms.name <- dbms.name
}
if (!nzchar(info$dbms.name)) {
stop("The ODBC driver returned an invalid `dbms.name`. Please provide one manually with the `dbms.name` parameter.", call. = FALSE)
cli::cli_abort(
c(
"!" = "The ODBC driver failed to generate a DBMS name.",
"i" = "Please provide one manually with {.arg dbms.name}."
),
call = call
)
}

class <- getClassDef(info$dbms.name, inherits = FALSE)
Expand Down
39 changes: 20 additions & 19 deletions R/utils.R
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,9 @@ lengths <- function(x) {
}

# A 'size' must be an integer greater than 1, returned as a double so we have a larger range
parse_size <- function(x) {
nme <- substitute(x) %||% "NULL"

if (rlang::is_scalar_integerish(x) && !is.na(x) && !is.infinite(x) && x > 0) {
return(as.numeric(x))
}

stop(
sprintf("`%s` is not a valid size:\n Must be a positive integer.", as.character(nme)),
call. = FALSE
)
parse_size <- function(x, arg = caller_arg(x), call = caller_env()) {
check_number_whole(x, min = 1, arg = arg, call = call)
as.numeric(x)
}

id_field <- function(id,
Expand Down Expand Up @@ -81,14 +73,6 @@ id_field <- function(id,
}
}

check_n <- function(n) {
if (length(n) != 1) stop("`n` must be scalar", call. = FALSE)
if (n < -1) stop("`n` must be nonnegative or -1", call. = FALSE)
if (is.infinite(n)) n <- -1
if (trunc(n) != n) stop("`n` must be a whole number", call. = FALSE)
n
}

isPatternValue <- function(val) {
grepl("[%|_]", val)
}
Expand Down Expand Up @@ -193,6 +177,23 @@ check_field.types <- function(field.types, call = caller_env()) {
}
}

check_attributes <- function(attributes, call = caller_env()) {
if (!all(has_names(attributes))) {
cli::cli_abort("All elements of {.arg attributes} must be named.", call = call)
}

attributes_supported <- names(attributes) %in% SUPPORTED_CONNECTION_ATTRIBUTES
if (!all(attributes_supported)) {
cli::cli_abort(
c("!" = "{.arg attributes} does not support the connection attribute{?s} \\
{.val {names(attributes)[!attributes_supported]}}.",
"i" = "Allowed connection attribute{?s} {?is/are} \\
{.val {SUPPORTED_CONNECTION_ATTRIBUTES}}."),
call = call
)
}
}

# apple + spark drive config (#651) --------------------------------------------
configure_spark <- function(call = caller_env()) {
if (!is_macos()) {
Expand Down
8 changes: 8 additions & 0 deletions tests/testthat/_snaps/dbi-connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,11 @@
Output
<OdbcConnection>

# dbQuoteIdentifier() errors informatively

Code
dbQuoteIdentifier(con, NA_character_)
Condition
Error in `dbQuoteIdentifier()`:
! `x` can't be `NA`.

10 changes: 10 additions & 0 deletions tests/testthat/_snaps/driver-sqlite.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# unsupported types gives informative error

Code
dbWriteTable(con, "df", df)
Condition
Error in `odbcDataType()`:
! Can't determine type for column foo.
Caused by error:
! Unsupported type

# dbWriteTable() with `field.types` with `append = TRUE`

Code
Expand Down
16 changes: 16 additions & 0 deletions tests/testthat/_snaps/odbc-connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,19 @@
Error in `quote_value()`:
! Don't know how to escape a value with both single and double quotes.

# validateObjectName() errors informatively

Code
odbcListColumns(con, table = "boop", view = "bop")
Condition
Error in `odbcListColumns()`:
! Exactly one of `table` or `view` must be supplied.

---

Code
odbcListColumns(con)
Condition
Error in `odbcListColumns()`:
! One of `table` or `view` must be supplied.

26 changes: 26 additions & 0 deletions tests/testthat/_snaps/utils.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
# parse_size works

Code
parse_size("foo")
Condition
Error:
! `"foo"` must be a whole number, not the string "foo".

# id_field checks inputs

Code
Expand Down Expand Up @@ -27,6 +35,24 @@
Error in `dbWriteTable()`:
! `field.types` must be `NULL` or a named vector of field types, not a string.

# check_attributes()

Code
con <- test_con("SQLITE", attributes = list(boop = "bop"))
Condition
Error in `dbConnect()`:
! `attributes` does not support the connection attribute "boop".
i Allowed connection attribute is "azure_token".

---

Code
con <- test_con("SQLITE", attributes = list(boop = "bop", beep = "boop"))
Condition
Error in `dbConnect()`:
! `attributes` does not support the connection attributes "boop" and "beep".
i Allowed connection attribute is "azure_token".

# configure_spark() errors informatively on failure to install unixODBC

Code
Expand Down
9 changes: 9 additions & 0 deletions tests/testthat/test-dbi-connection.R
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,12 @@ test_that("show method does not print server if it is not available", {
)
expect_snapshot(con)
})

test_that("dbQuoteIdentifier() errors informatively", {
con <- test_con("SQLITE")

expect_snapshot(
error = TRUE,
dbQuoteIdentifier(con, NA_character_)
)
})
5 changes: 4 additions & 1 deletion tests/testthat/test-driver-sqlite.R
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,10 @@ test_that("unsupported types gives informative error", {
con <- test_con("SQLITE")

df <- data.frame(foo = complex(1))
expect_error(dbWriteTable(con, "df", df), "Column 'foo' is of unsupported type: 'complex'")
expect_snapshot(
error = TRUE,
dbWriteTable(con, "df", df)
)
})

test_that("odbcPreviewObject works", {
Expand Down
15 changes: 15 additions & 0 deletions tests/testthat/test-odbc-connection.R
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,21 @@ test_that("automatically picks correct quote type", {
expect_snapshot(quote_value("'\""), error = TRUE)
})

# connections pane -------------------------------------------------------------
test_that("validateObjectName() errors informatively", {
con <- test_con("SQLITE")

expect_snapshot(
error = TRUE,
odbcListColumns(con, table = "boop", view = "bop")
)

expect_snapshot(
error = TRUE,
odbcListColumns(con)
)
})

# odbcConnectionColumns deprecation --------------------------------------

test_that("odbcConnectionColumns warns on usage (#699)", {
Expand Down
26 changes: 13 additions & 13 deletions tests/testthat/test-utils.R
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
test_that("parse_size works", {
expect_error(parse_size("foo"), "Must be a positive integer")
expect_error(parse_size(TRUE), "Must be a positive integer")
expect_error(parse_size(0), "Must be a positive integer")
expect_error(parse_size(-1), "Must be a positive integer")
expect_error(parse_size(-.1), "Must be a positive integer")
expect_error(parse_size(.1), "Must be a positive integer")
expect_error(parse_size("0"), "Must be a positive integer")
expect_error(parse_size("1"), "Must be a positive integer")
expect_error(parse_size(Inf), "Must be a positive integer")
expect_error(parse_size(NULL), "Must be a positive integer")
expect_error(parse_size(1:2), "Must be a positive integer")
expect_error(parse_size(numeric()), "Must be a positive integer")
expect_error(parse_size(integer()), "Must be a positive integer")
expect_snapshot(error = TRUE, parse_size("foo"))

expect_identical(parse_size(1L), 1)
expect_identical(parse_size(1), 1)
Expand Down Expand Up @@ -73,6 +61,18 @@ test_that("check_field.types()", {
)
})

test_that("check_attributes()", {
expect_snapshot(
error = TRUE,
con <- test_con("SQLITE", attributes = list(boop = "bop"))
)

expect_snapshot(
error = TRUE,
con <- test_con("SQLITE", attributes = list(boop = "bop", beep = "boop"))
)
})

test_that("configure_spark() returns early on windows", {
local_mocked_bindings(is_macos = function() {FALSE})

Expand Down
Loading