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 5 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
15 changes: 11 additions & 4 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 @@ -208,12 +208,18 @@ validateObjectName <- function(table, 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)
cli::cli_abort(
simonpcouch marked this conversation as resolved.
Show resolved Hide resolved
"{.arg table} and {.arg view} can not both be used.",
call = call
)
}

# 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)
cli::cli_abort(
"{.arg table} and {.arg view} can not both be {.code NULL}.",
call = call
)
}

table %||% view
Expand All @@ -231,9 +237,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("NA values are not supported in {.arg x}.")
simonpcouch marked this conversation as resolved.
Show resolved Hide resolved
}
if (nzchar(conn@quote)) {
x <- gsub(conn@quote, paste0(conn@quote, conn@quote), x, fixed = TRUE)
Expand Down
7 changes: 5 additions & 2 deletions R/dbi-driver.R
Original file line number Diff line number Diff line change
Expand Up @@ -255,9 +255,12 @@ odbc_data_type_df <- function(dbObj, obj, ...) {
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)
cli::cli_abort(
"Column {nms[[i]]} is of unsupported type {.val object_type(obj[[i]])}.",
call = call2("odbcDataType")
)
} else {
stop(e)
cli::cli_abort(conditionMessage(e), call = call2("odbcDataType"))
simonpcouch marked this conversation as resolved.
Show resolved Hide resolved
}
}
)
Expand Down
2 changes: 1 addition & 1 deletion R/dbi-result.R
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,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
13 changes: 8 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,11 @@ 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 returned an invalid {.code dbms.name}.",
"i" = "Please provide one manually with {.arg dbms.name}."),
simonpcouch marked this conversation as resolved.
Show resolved Hide resolved
call = call
)
}

class <- getClassDef(info$dbms.name, inherits = FALSE)
Expand Down
37 changes: 22 additions & 15 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, call = caller_env()) {
simonpcouch marked this conversation as resolved.
Show resolved Hide resolved
check_number_whole(x, min = 1, call = call)
as.numeric(x)
}

id_field <- function(id,
Expand Down Expand Up @@ -81,11 +73,9 @@ 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)
check_n <- function(n, call = caller_env()) {
check_number_whole(n, min = -1, allow_infinite = TRUE, call = call)
simonpcouch marked this conversation as resolved.
Show resolved Hide resolved
if (is.infinite(n)) n <- -1
if (trunc(n) != n) stop("`n` must be a whole number", call. = FALSE)
n
}

Expand Down Expand Up @@ -193,6 +183,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()`:
! NA values are not supported in `x`.

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

Code
dbWriteTable(con, "df", df)
Condition
Error in `odbcDataType()`:
! Column foo is of unsupported type "object_type(obj[[i]])".
simonpcouch marked this conversation as resolved.
Show resolved Hide resolved

# 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()`:
! `table` and `view` can not both be used.

---

Code
odbcListColumns(con)
Condition
Error in `odbcListColumns()`:
! `table` and `view` can not both be `NULL`.

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:
! `x` 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
39 changes: 26 additions & 13 deletions tests/testthat/test-utils.R
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
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_error(parse_size("foo"), "must be a whole number")
expect_error(parse_size(TRUE), "must be a whole number")
expect_error(parse_size(0), "must be a whole number")
expect_error(parse_size(-1), "must be a whole number")
expect_error(parse_size(-.1), "must be a whole number")
expect_error(parse_size(.1), "must be a whole number")
expect_error(parse_size("0"), "must be a whole number")
expect_error(parse_size("1"), "must be a whole number")
expect_error(parse_size(Inf), "must be a whole number")
expect_error(parse_size(NULL), "must be a whole number")
expect_error(parse_size(1:2), "must be a whole number")
expect_error(parse_size(numeric()), "must be a whole number")
expect_error(parse_size(integer()), "must be a whole number")
simonpcouch marked this conversation as resolved.
Show resolved Hide resolved

expect_identical(parse_size(1L), 1)
expect_identical(parse_size(1), 1)
Expand Down Expand Up @@ -73,6 +74,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