From b983beeceebed6ae02bd5f1af238ab71bef53bb2 Mon Sep 17 00:00:00 2001 From: Aaron Jacobs Date: Fri, 8 Dec 2023 16:50:34 -0500 Subject: [PATCH 1/5] Add a helper function for connecting to Snowflake. This commit introduces a new odbc::snowflake() function intended to help setting up connections to Snowflake and handling auth correctly on platforms that provide Snowflake-native OAuth credentials. Due to a naming clash, the Snowflake.Rd file has also been renamed driver-Snowflake.Rd. Because we want to smoke test arguments to the snowflake() function directly rather than a full connection string, this commit also exposes a new environment variable with the GitHub secret for the Snowflake password. Unit tests are included. Signed-off-by: Aaron Jacobs --- .github/workflows/db.yaml | 1 + NAMESPACE | 1 + R/driver-snowflake.R | 245 +++++++++++++++++++++- _pkgdown.yml | 1 + man/{Snowflake.Rd => driver-Snowflake.Rd} | 2 +- man/snowflake.Rd | 78 +++++++ tests/testthat/_snaps/driver-snowflake.md | 46 ++++ tests/testthat/test-driver-snowflake.R | 155 ++++++++++++++ 8 files changed, 526 insertions(+), 3 deletions(-) rename man/{Snowflake.Rd => driver-Snowflake.Rd} (96%) create mode 100644 man/snowflake.Rd create mode 100644 tests/testthat/_snaps/driver-snowflake.md diff --git a/.github/workflows/db.yaml b/.github/workflows/db.yaml index 4f126a16..39feaf8e 100644 --- a/.github/workflows/db.yaml +++ b/.github/workflows/db.yaml @@ -94,6 +94,7 @@ jobs: - name: Install Snowflake Driver run: | echo "ODBC_CS_SNOWFLAKE=dsn=Snowflake;pwd=${{ secrets.SNOWFLAKE_PWD }}" >> $GITHUB_ENV + echo "ODBC_PWD_SNOWFLAKE=${{ secrets.SNOWFLAKE_PWD }}" >> $GITHUB_ENV curl https://sfc-repo.snowflakecomputing.com/odbc/linux/3.2.0/snowflake_linux_x8664_odbc-3.2.0.tgz --output snowflake_linux_x8664_odbc-3.2.0.tgz gunzip snowflake_linux_x8664_odbc-3.2.0.tgz tar -xvf snowflake_linux_x8664_odbc-3.2.0.tar diff --git a/NAMESPACE b/NAMESPACE index 62093127..2ff4f662 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -20,6 +20,7 @@ export(odbcListObjects) export(odbcPreviewObject) export(odbcSetTransactionIsolationLevel) export(quote_value) +export(snowflake) exportClasses("DB2/AIX64") exportClasses("Microsoft SQL Server") exportClasses("Spark SQL") diff --git a/R/driver-snowflake.R b/R/driver-snowflake.R index 5045c9ef..3efc2c99 100644 --- a/R/driver-snowflake.R +++ b/R/driver-snowflake.R @@ -35,7 +35,7 @@ getCatalogSchema <- function(conn, catalog_name = NULL, schema_name = NULL) { #' to aid with performance, as the SQLColumns method is more performant #' when restricted to a particular DB/schema. #' @inheritParams DBI::dbListFields -#' @rdname Snowflake +#' @rdname driver-Snowflake #' @usage NULL setMethod("odbcConnectionColumns_", c("Snowflake", "character"), function(conn, @@ -52,7 +52,7 @@ setMethod("odbcConnectionColumns_", c("Snowflake", "character"), } ) -#' @rdname Snowflake +#' @rdname driver-Snowflake setMethod("dbExistsTableForWrite", c("Snowflake", "character"), function(conn, name, ..., catalog_name = NULL, schema_name = NULL) { @@ -87,3 +87,244 @@ setMethod("odbcDataType", "Snowflake", ) } ) + +#' Helper for connecting to Snowflake via ODBC +#' +#' @description +#' +#' Connect to a Snowflake account via the [Snowflake ODBC +#' driver](https://docs.snowflake.com/en/developer-guide/odbc/odbc). +#' +#' In particular, the custom `dbConnect()` method for the Snowflake ODBC driver +#' detects ambient OAuth credentials on platforms like Snowpark Container +#' Services or Posit Workbench. +#' +#' @inheritParams DBI::dbConnect +#' @param account A Snowflake [account +#' identifier](https://docs.snowflake.com/en/user-guide/admin-account-identifier), +#' e.g. `"testorg-test_account"`. +#' @param driver The name of the Snowflake ODBC driver, or `NULL` to use the +#' default name. +#' @param warehouse The name of a Snowflake compute warehouse, or `NULL` to use +#' the default. +#' @param database The name of a Snowflake database, or `NULL` to use the +#' default. +#' @param schema The name of a Snowflake database schema, or `NULL` to use the +#' default. +#' @param uid,pwd Manually specify a username and password for authentication. +#' Specifying these options will disable ambient credential discovery. +#' @param ... Further arguments passed on to [`dbConnect()`]. +#' +#' @returns An `OdbcConnection` object with an active connection to a Snowflake +#' account. +#' +#' @examples +#' \dontrun{ +#' # Use ambient credentials. +#' DBI::dbConnect(odbc::snowflake()) +#' +#' # Use browser-based SSO (if configured). Only works on desktop. +#' DBI::dbConnect( +#' odbc::snowflake(), +#' account = "testorg-test_account", +#' authenticator = "externalbrowser" +#' ) +#' +#' # Use a traditional username & password. +#' DBI::dbConnect( +#' odbc::snowflake(), +#' account = "testorg-test_account", +#' uid = "me", +#' pwd = rstudioapi::askForPassword() +#' ) +#' } +#' @export +snowflake <- function() { + new("Snowflake") +} + +#' @rdname snowflake +#' @export +setMethod( + "dbConnect", "Snowflake", + function(drv, + account = Sys.getenv("SNOWFLAKE_ACCOUNT"), + driver = NULL, + warehouse = NULL, + database = NULL, + schema = NULL, + uid = NULL, + pwd = NULL, + ...) { + call <- caller_env() + check_string(account, call = call) + check_string(driver, allow_null = TRUE, call = call) + check_string(warehouse, allow_null = TRUE, call = call) + check_string(database, allow_null = TRUE, call = call) + check_string(uid, allow_null = TRUE, call = call) + check_string(pwd, allow_null = TRUE, call = call) + args <- snowflake_args( + account = account, + driver = driver, + warehouse = warehouse, + database = database, + schema = schema, + uid = uid, + pwd = pwd, + ... + ) + inject(dbConnect(odbc(), !!!args)) + } +) + +snowflake_args <- function(account = Sys.getenv("SNOWFLAKE_ACCOUNT"), + driver = NULL, + ...) { + args <- list( + driver = driver %||% snowflake_default_driver(), + account = account, + server = snowflake_server(account), + # Connections to Snowflake are always over HTTPS. + port = 443 + ) + auth <- snowflake_auth_args(account, ...) + all <- utils::modifyList(c(args, auth), list(...)) + + # Respect the Snowflake Partner environment variable, if present. + if (is.null(all$application) && nchar(Sys.getenv("SF_PARTNER")) != 0) { + all$application <- Sys.getenv("SF_PARTNER") + } + + arg_names <- tolower(names(all)) + if (!"authenticator" %in% arg_names && !all(c("uid", "pwd") %in% arg_names)) { + abort( + c( + "x" = "Failed to detect ambient Snowflake credentials.", + "i" = "Supply `uid` and `pwd` to authenticate manually." + ), + call = quote(DBI::dbConnect()) + ) + } + + all +} + +# Returns a sensible driver name even if odbc.ini and odbcinst.ini do not +# contain an entry for the Snowflake ODBC driver. For Linux and macOS we +# default to known shared library paths used by the official installers. +# On Windows we use the official driver name. +snowflake_default_driver <- function() { + default_paths <- snowflake_default_driver_paths() + if (length(default_paths) > 0) { + return(default_paths[1]) + } + + fallbacks <- c("Snowflake", "SnowflakeDSIIDriver") + fallbacks <- intersect(fallbacks, odbcListDrivers()$name) + if (length(fallbacks) > 0) { + return(fallbacks[1]) + } + + abort( + c( + "Failed to automatically find Snowflake ODBC driver.", + i = "Set `driver` to known driver name or path." + ), + call = quote(DBI::dbConnect()) + ) +} + +snowflake_default_driver_paths <- function() { + if (Sys.info()["sysname"] == "Linux") { + paths <- c( + "/opt/rstudio-drivers/snowflake/bin/lib/libsnowflakeodbc_sb64.so", + "/usr/lib/snowflake/odbc/lib/libSnowflake.so" + ) + } else if (Sys.info()["sysname"] == "Darwin") { + paths <- "/opt/snowflake/snowflakeodbc/lib/universal/libSnowflake.dylib" + } else { + paths <- character() + } + paths[file.exists(paths)] +} + +snowflake_server <- function(account) { + if (nchar(account) == 0) { + abort( + c( + "No Snowflake account ID provided.", + i = "Either supply `account` argument or set env var `SNOWFLAKE_ACCOUNT`." + ), + call = quote(DBI::dbConnect()) + ) + } + paste0(account, ".snowflakecomputing.com") +} + +snowflake_auth_args <- function(account, + uid = NULL, + pwd = NULL, + authenticator = NULL, + ...) { + if (!is.null(uid) && !is.null(pwd)) { + return(list(uid = uid, pwd = pwd)) + } else if (xor(is.null(uid), is.null(pwd))) { + abort( + c( + "Both `uid` and `pwd` must be specified to authenticate.", + i = "Or leave both unset to use ambient Snowflake credentials." + ), + call = quote(DBI::dbConnect()) + ) + } + + # Manual override of the authentication method. + if (!is.null(authenticator)) { + return(list()) + } + + # Check for Workbench-provided credentials. + sf_home <- Sys.getenv("SNOWFLAKE_HOME") + if (grepl("posit-workbench", sf_home, fixed = TRUE)) { + token <- workbench_snowflake_token(account, sf_home) + if (!is.null(token)) { + return(list(authenticator = "oauth", token = token)) + } + } + + # Check for the default token mounted when running in Snowpark Container + # Services. + if (file.exists("/snowflake/session/token")) { + return(list( + authenticator = "oauth", + token = readLines("/snowflake/session/token", warn = FALSE) + )) + } + + list() +} + +# Reads Posit Workbench-managed Snowflake credentials from a +# $SNOWFLAKE_HOME/connections.toml file, as used by the Snowflake Connector for +# Python implementation. The file will look as follows: +# +# [workbench] +# account = "account-id" +# token = "token" +# authenticator = "oauth" +workbench_snowflake_token <- function(account, sf_home) { + cfg <- readLines(file.path(sf_home, "connections.toml")) + # We don't attempt a full parse of the TOML syntax, instead relying on the + # fact that this file will always contain only one section. + if (!any(grepl(account, cfg, fixed = TRUE))) { + # The configuration doesn't actually apply to this account. + return(NULL) + } + line <- grepl("token = ", cfg, fixed = TRUE) + token <- gsub("token = ", "", cfg[line]) + if (nchar(token) == 0) { + return(NULL) + } + # Drop enclosing quotes. + gsub("\"", "", token) +} diff --git a/_pkgdown.yml b/_pkgdown.yml index 2045bdf6..f0553df9 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -23,6 +23,7 @@ reference: - databricks - 'Microsoft SQL Server-class' - 'Oracle-class' + - snowflake - title: ODBC configuration contents: diff --git a/man/Snowflake.Rd b/man/driver-Snowflake.Rd similarity index 96% rename from man/Snowflake.Rd rename to man/driver-Snowflake.Rd index 7c9d472c..145e7051 100644 --- a/man/Snowflake.Rd +++ b/man/driver-Snowflake.Rd @@ -5,7 +5,7 @@ \alias{dbExistsTableForWrite,Snowflake,character-method} \title{Connecting to Snowflake via ODBC} \usage{ -\S4method{dbExistsTableForWrite}{Snowflake,character}(conn, name, ...) +\S4method{dbExistsTableForWrite}{Snowflake,character}(conn, name, ..., catalog_name = NULL, schema_name = NULL) } \arguments{ \item{conn}{A \linkS4class{DBIConnection} object, as returned by diff --git a/man/snowflake.Rd b/man/snowflake.Rd new file mode 100644 index 00000000..45b2713c --- /dev/null +++ b/man/snowflake.Rd @@ -0,0 +1,78 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/driver-snowflake.R +\name{snowflake} +\alias{snowflake} +\alias{dbConnect,Snowflake-method} +\title{Helper for connecting to Snowflake via ODBC} +\usage{ +snowflake() + +\S4method{dbConnect}{Snowflake}( + drv, + account = Sys.getenv("SNOWFLAKE_ACCOUNT"), + driver = NULL, + warehouse = NULL, + database = NULL, + schema = NULL, + uid = NULL, + pwd = NULL, + ... +) +} +\arguments{ +\item{drv}{an object that inherits from \linkS4class{DBIDriver}, +or an existing \linkS4class{DBIConnection} +object (in order to clone an existing connection).} + +\item{account}{A Snowflake \href{https://docs.snowflake.com/en/user-guide/admin-account-identifier}{account identifier}, +e.g. \code{"testorg-test_account"}.} + +\item{driver}{The name of the Snowflake ODBC driver, or \code{NULL} to use the +default name.} + +\item{warehouse}{The name of a Snowflake compute warehouse, or \code{NULL} to use +the default.} + +\item{database}{The name of a Snowflake database, or \code{NULL} to use the +default.} + +\item{schema}{The name of a Snowflake database schema, or \code{NULL} to use the +default.} + +\item{uid, pwd}{Manually specify a username and password for authentication. +Specifying these options will disable ambient credential discovery.} + +\item{...}{Further arguments passed on to \code{\link[=dbConnect]{dbConnect()}}.} +} +\value{ +An \code{OdbcConnection} object with an active connection to a Snowflake +account. +} +\description{ +Connect to a Snowflake account via the \href{https://docs.snowflake.com/en/developer-guide/odbc/odbc}{Snowflake ODBC driver}. + +In particular, the custom \code{dbConnect()} method for the Snowflake ODBC driver +detects ambient OAuth credentials on platforms like Snowpark Container +Services or Posit Workbench. +} +\examples{ +\dontrun{ +# Use ambient credentials. +DBI::dbConnect(odbc::snowflake()) + +# Use browser-based SSO (if configured). Only works on desktop. +DBI::dbConnect( + odbc::snowflake(), + account = "testorg-test_account", + authenticator = "externalbrowser" +) + +# Use a traditional username & password. +DBI::dbConnect( + odbc::snowflake(), + account = "testorg-test_account", + uid = "me", + pwd = rstudioapi::askForPassword() +) +} +} diff --git a/tests/testthat/_snaps/driver-snowflake.md b/tests/testthat/_snaps/driver-snowflake.md new file mode 100644 index 00000000..50822e5b --- /dev/null +++ b/tests/testthat/_snaps/driver-snowflake.md @@ -0,0 +1,46 @@ +# an account ID is required + + Code + snowflake_args(driver = "driver") + Condition + Error in `DBI::dbConnect()`: + ! No Snowflake account ID provided. + i Either supply `account` argument or set env var `SNOWFLAKE_ACCOUNT`. + +# both 'uid' and 'pwd' are required when present + + Code + snowflake_args(account = "testorg-test_account", driver = "driver", uid = "uid", + ) + Condition + Error in `DBI::dbConnect()`: + ! Both `uid` and `pwd` must be specified to authenticate. + i Or leave both unset to use ambient Snowflake credentials. + +# we error if we can't find ambient credentials + + Code + snowflake_args(account = "testorg-test_account", driver = "driver") + Condition + Error in `DBI::dbConnect()`: + ! x Failed to detect ambient Snowflake credentials. + i Supply `uid` and `pwd` to authenticate manually. + +# we error if we can't find the driver + + Code + snowflake_default_driver() + Condition + Error in `DBI::dbConnect()`: + ! Failed to automatically find Snowflake ODBC driver. + i Set `driver` to known driver name or path. + +# Workbench-managed credentials are ignored for other accounts + + Code + snowflake_args(account = "testorg-test_account", driver = "driver") + Condition + Error in `DBI::dbConnect()`: + ! x Failed to detect ambient Snowflake credentials. + i Supply `uid` and `pwd` to authenticate manually. + diff --git a/tests/testthat/test-driver-snowflake.R b/tests/testthat/test-driver-snowflake.R index d8b77b1e..23308ccc 100644 --- a/tests/testthat/test-driver-snowflake.R +++ b/tests/testthat/test-driver-snowflake.R @@ -1,3 +1,158 @@ test_that("can connect to snowflake", { con <- test_con("SNOWFLAKE") }) + +test_that("an account ID is required", { + withr::local_envvar(SNOWFLAKE_ACCOUNT = "") + expect_snapshot(snowflake_args(driver = "driver"), error = TRUE) +}) + +test_that("environment variables are handled correctly", { + withr::local_envvar( + SNOWFLAKE_ACCOUNT = "testorg-test_account", + SF_PARTNER = "test_partner" + ) + args <- snowflake_args( + pwd = "pwd", + uid = "user", + driver = "driver" + ) + expect_equal(args$account, "testorg-test_account") + expect_equal(args$server, "testorg-test_account.snowflakecomputing.com") + expect_equal(args$application, "test_partner") +}) + +test_that("environment variables can be overridden with parameters", { + withr::local_envvar( + SNOWFLAKE_ACCOUNT = "account", + SF_PARTNER = "test_partner" + ) + args <- snowflake_args( + account = "testorg-test_account", + pwd = "pwd", + uid = "user", + driver = "driver", + application = "myapp" + ) + expect_equal(args$account, "testorg-test_account") + expect_equal(args$server, "testorg-test_account.snowflakecomputing.com") + expect_equal(args$application, "myapp") +}) + +test_that("the 'uid' and 'pwd' arguments suppress automated auth", { + args <- snowflake_args( + account = "testorg-test_account", + driver = "driver", + uid = "uid", + pwd = "pwd" + ) + expect_equal(args$uid, "uid") + expect_equal(args$pwd, "pwd") + expect_equal(args$authenticator, NULL) +}) + +test_that("both 'uid' and 'pwd' are required when present", { + expect_snapshot( + snowflake_args( + account = "testorg-test_account", + driver = "driver", + uid = "uid", + ), + error = TRUE + ) +}) + +test_that("alternative authenticators are supported", { + args <- snowflake_args( + account = "testorg-test_account", + driver = "driver", + authenticator = "externalbrowser" + ) + expect_equal(args$authenticator, "externalbrowser") +}) + +test_that("we error if we can't find ambient credentials", { + withr::local_envvar(SF_PARTNER = "") + local_mocked_bindings( + snowflake_auth_args = function(...) list() + ) + expect_snapshot( + snowflake_args(account = "testorg-test_account", driver = "driver"), + error = TRUE + ) +}) + +test_that("the default driver falls back to a known driver name", { + local_mocked_bindings( + snowflake_default_driver_paths = function() character(), + odbcListDrivers = function() list(name = c("bar", "Snowflake")) + ) + expect_equal(snowflake_default_driver(), "Snowflake") +}) + +test_that("we error if we can't find the driver", { + local_mocked_bindings( + snowflake_default_driver_paths = function() character(), + odbcListDrivers = function() list() + ) + expect_snapshot(snowflake_default_driver(), error = TRUE) +}) + +test_that("Workbench-managed credentials are detected correctly", { + # Emulate the connections.toml file written by Workbench. + sf_home <- tempfile("posit-workbench") + dir.create(sf_home) + writeLines( + c( + '[workbench]', + 'account = "testorg-test_account"', + 'token = "token"', + 'authenticator = "oauth"' + ), + file.path(sf_home, "connections.toml") + ) + withr::local_envvar( + SNOWFLAKE_ACCOUNT = "testorg-test_account", + SNOWFLAKE_HOME = sf_home + ) + args <- snowflake_args(driver = "driver") + expect_equal(args$token, "token") + expect_equal(args$authenticator, "oauth") +}) + +test_that("Workbench-managed credentials are ignored for other accounts", { + # Emulate the connections.toml file written by Workbench. + sf_home <- tempfile("posit-workbench") + dir.create(sf_home) + writeLines( + c( + '[workbench]', + 'account = "nonmatching"', + 'token = "token"', + 'authenticator = "oauth"' + ), + file.path(sf_home, "connections.toml") + ) + withr::local_envvar( + SNOWFLAKE_HOME = sf_home, + SF_PARTNER = "test_partner" + ) + expect_snapshot( + snowflake_args(account = "testorg-test_account", driver = "driver"), + error = TRUE + ) +}) + +test_that("snowflake() works against a real account", { + pwd <- Sys.getenv("ODBC_PWD_SNOWFLAKE") + if (nchar(pwd) == 0) { + skip("Secret ODBC_PWD_SNOWFLAKE not available.") + } + dbConnect( + odbc::snowflake(), + account = "uab99020.us-east-1", + driver = "SnowflakeDSIIDriver", + uid = "odbcTestRunner", + pwd = pwd + ) +}) From 341ef92d95939b9fb8a4deb8cce0063d8ca743e9 Mon Sep 17 00:00:00 2001 From: Aaron Jacobs Date: Tue, 7 May 2024 11:43:22 -0400 Subject: [PATCH 2/5] Add missing param docs for Snowflake's odbcConnectionColumns(). Signed-off-by: Aaron Jacobs --- R/driver-snowflake.R | 1 + man/driver-Snowflake.Rd | 2 ++ 2 files changed, 3 insertions(+) diff --git a/R/driver-snowflake.R b/R/driver-snowflake.R index 3efc2c99..414877d3 100644 --- a/R/driver-snowflake.R +++ b/R/driver-snowflake.R @@ -35,6 +35,7 @@ getCatalogSchema <- function(conn, catalog_name = NULL, schema_name = NULL) { #' to aid with performance, as the SQLColumns method is more performant #' when restricted to a particular DB/schema. #' @inheritParams DBI::dbListFields +#' @param catalog_name,schema_name Catalog and schema names. #' @rdname driver-Snowflake #' @usage NULL setMethod("odbcConnectionColumns_", c("Snowflake", "character"), diff --git a/man/driver-Snowflake.Rd b/man/driver-Snowflake.Rd index 145e7051..d8a96be7 100644 --- a/man/driver-Snowflake.Rd +++ b/man/driver-Snowflake.Rd @@ -22,6 +22,8 @@ given verbatim, e.g. \code{SQL('"my_schema"."table_name"')} }} \item{...}{Other parameters passed on to methods.} + +\item{catalog_name, schema_name}{Catalog and schema names.} } \description{ \subsection{\code{odbcConnectionColumns()}}{ From 16ef1582208688c7a2325cf494e44ef4dc18e31d Mon Sep 17 00:00:00 2001 From: Aaron Jacobs Date: Wed, 15 May 2024 10:53:49 -0400 Subject: [PATCH 3/5] Error rather than warn uid/pwd are missing in odbc::databricks(). Signed-off-by: Aaron Jacobs --- R/driver-databricks.R | 2 +- tests/testthat/_snaps/driver-databricks.md | 6 +++--- tests/testthat/test-driver-databricks.R | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/R/driver-databricks.R b/R/driver-databricks.R index 7b946057..1cf43d57 100644 --- a/R/driver-databricks.R +++ b/R/driver-databricks.R @@ -109,7 +109,7 @@ databricks_args <- function(httpPath, arg_names <- tolower(names(all)) if (!"authmech" %in% arg_names && !all(c("uid", "pwd") %in% arg_names)) { - warn( + abort( c( "x" = "Failed to detect ambient Databricks credentials.", "i" = "Supply `uid` and `pwd` to authenticate manually." diff --git a/tests/testthat/_snaps/driver-databricks.md b/tests/testthat/_snaps/driver-databricks.md index 3883abd8..ecffdba3 100644 --- a/tests/testthat/_snaps/driver-databricks.md +++ b/tests/testthat/_snaps/driver-databricks.md @@ -16,13 +16,13 @@ ! No Databricks workspace URL provided. i Either supply `workspace` argument or set env var `DATABRICKS_HOST`. -# warns if auth fails +# errors if auth fails Code . <- databricks_args1() Condition - Warning in `DBI::dbConnect()`: - x Failed to detect ambient Databricks credentials. + Error in `DBI::dbConnect()`: + ! x Failed to detect ambient Databricks credentials. i Supply `uid` and `pwd` to authenticate manually. # must supply both uid and pwd diff --git a/tests/testthat/test-driver-databricks.R b/tests/testthat/test-driver-databricks.R index 8e35735f..651306b1 100644 --- a/tests/testthat/test-driver-databricks.R +++ b/tests/testthat/test-driver-databricks.R @@ -49,14 +49,14 @@ test_that("user agent respects envvar", { expect_equal(databricks_user_agent(), "my-odbc/1.0.0") }) -test_that("warns if auth fails", { +test_that("errors if auth fails", { withr::local_envvar(DATABRICKS_TOKEN = "") databricks_args1 <- function(...) { databricks_args("path", "host", driver = "driver", ...) } - expect_snapshot(. <- databricks_args1()) + expect_snapshot(. <- databricks_args1(), error = TRUE) expect_silent(databricks_args1(uid = "uid", pwd = "pwd")) expect_silent(databricks_args1(authMech = 10)) From 925003d1e1c1384c69e6b4ce306139d962c1595a Mon Sep 17 00:00:00 2001 From: simonpcouch Date: Tue, 14 May 2024 12:49:46 -0500 Subject: [PATCH 4/5] skip snowflake `test_con()` when repository secret not available --- tests/testthat/test-driver-snowflake.R | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/testthat/test-driver-snowflake.R b/tests/testthat/test-driver-snowflake.R index 23308ccc..93f58b47 100644 --- a/tests/testthat/test-driver-snowflake.R +++ b/tests/testthat/test-driver-snowflake.R @@ -1,4 +1,9 @@ test_that("can connect to snowflake", { + pwd <- Sys.getenv("ODBC_PWD_SNOWFLAKE") + if (nchar(pwd) == 0) { + skip("Secret ODBC_PWD_SNOWFLAKE not available.") + } + con <- test_con("SNOWFLAKE") }) From 81abe10796936becb25c63f7bce1026037213030 Mon Sep 17 00:00:00 2001 From: simonpcouch Date: Wed, 15 May 2024 10:48:24 -0500 Subject: [PATCH 5/5] note change in NEWS --- NEWS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/NEWS.md b/NEWS.md index 65c6e1cb..e6449d3f 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # odbc (development version) +* New `odbc::snowflake()` makes it easier to connect to Snowflake, + automatically handling authentication correctly on platforms that provide + Snowflake-native OAuth credentials (@atheriel, #662). + * Transitioned to the cli package for formatting most error messages (@simonpcouch, #781, #784, #785, #788).