diff --git a/DESCRIPTION b/DESCRIPTION index 005809ce9..44c20963b 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -54,6 +54,7 @@ Roxygen: list(markdown = TRUE) RoxygenNote: 7.2.1 Collate: 'DBItest.R' + 'compat-purrr.R' 'context.R' 'dbi.R' 'expectations.R' @@ -127,6 +128,7 @@ Collate: 'test-compliance.R' 'test-stress.R' 'tweaks.R' + 'use.R' 'utf8.R' 'utils.R' 'zzz.R' diff --git a/NAMESPACE b/NAMESPACE index fd9bafdf4..2f62208ae 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,21 +3,54 @@ S3method("$",DBItest_tweaks) S3method(format,DBItest_tweaks) S3method(print,DBItest_tweaks) +export(check_df) +export(connect) +export(dbi_generics) +export(expect_all_args_have_default_values) +export(expect_ellipsis_in_formals) +export(expect_equal_df) +export(expect_has_class_method) +export(expect_invisible_true) export(get_default_context) +export(get_key_methods) +export(get_penguins) +export(get_pkg_path) +export(get_placeholder_funs) +export(get_texts) +export(has_utf8_or_ascii_encoding) +export(local_connection) +export(local_remove_test_table) +export(local_result) export(make_context) +export(new_bind_tester_extra) +export(package_name) +export(random_table_name) +export(s4_methods) export(set_default_context) +export(sql_union) export(test_all) export(test_compliance) export(test_connection) +export(test_data_type) export(test_driver) export(test_getting_started) export(test_meta) export(test_result) +export(test_select) +export(test_select_bind) +export(test_select_with_null) export(test_some) export(test_sql) export(test_stress) +export(test_table_roundtrip) +export(test_table_roundtrip_one) export(test_transaction) +export(trivial_df) +export(trivial_query) +export(trivial_values) +export(try_silent) export(tweaks) +export(unrowname) import(testthat) importFrom(DBI,Id) importFrom(DBI,SQL) @@ -61,7 +94,6 @@ importFrom(DBI,dbUnquoteIdentifier) importFrom(DBI,dbWithTransaction) importFrom(DBI,dbWriteTable) importFrom(callr,r) -importFrom(desc,desc_get_deps) importFrom(lubridate,with_tz) importFrom(methods,extends) importFrom(methods,findMethod) @@ -70,8 +102,10 @@ importFrom(methods,getClasses) importFrom(methods,hasMethod) importFrom(methods,is) importFrom(methods,new) +importFrom(rlang,"%||%") importFrom(rlang,":=") importFrom(rlang,abort) +importFrom(rlang,as_function) importFrom(rlang,enexpr) importFrom(rlang,enquo) importFrom(rlang,enquos) @@ -80,7 +114,10 @@ importFrom(rlang,expr) importFrom(rlang,has_length) importFrom(rlang,is_interactive) importFrom(rlang,list2) +importFrom(rlang,local_options) importFrom(rlang,quo) +importFrom(rlang,seq2) +importFrom(rlang,set_names) importFrom(stats,setNames) importFrom(utils,head) importFrom(withr,with_output_sink) diff --git a/R/compat-purrr.R b/R/compat-purrr.R new file mode 100644 index 000000000..d1380efbf --- /dev/null +++ b/R/compat-purrr.R @@ -0,0 +1,222 @@ +# nocov start - compat-purrr.R +# Latest version: https://github.com/r-lib/rlang/blob/main/R/compat-purrr.R + +# This file provides a minimal shim to provide a purrr-like API on top of +# base R functions. They are not drop-in replacements but allow a similar style +# of programming. +# +# Changelog: +# +# 2022-06-07: +# * `transpose()` is now more consistent with purrr when inner names +# are not congruent (#1346). +# +# 2021-12-15: +# * `transpose()` now supports empty lists. +# +# 2021-05-21: +# * Fixed "object `x` not found" error in `imap()` (@mgirlich) +# +# 2020-04-14: +# * Removed `pluck*()` functions +# * Removed `*_cpl()` functions +# * Used `as_function()` to allow use of `~` +# * Used `.` prefix for helpers + +map <- function(.x, .f, ...) { + .f <- as_function(.f, env = global_env()) + lapply(.x, .f, ...) +} +walk <- function(.x, .f, ...) { + map(.x, .f, ...) + invisible(.x) +} + +map_lgl <- function(.x, .f, ...) { + .rlang_purrr_map_mold(.x, .f, logical(1), ...) +} +map_int <- function(.x, .f, ...) { + .rlang_purrr_map_mold(.x, .f, integer(1), ...) +} +map_dbl <- function(.x, .f, ...) { + .rlang_purrr_map_mold(.x, .f, double(1), ...) +} +map_chr <- function(.x, .f, ...) { + .rlang_purrr_map_mold(.x, .f, character(1), ...) +} +.rlang_purrr_map_mold <- function(.x, .f, .mold, ...) { + .f <- as_function(.f, env = global_env()) + out <- vapply(.x, .f, .mold, ..., USE.NAMES = FALSE) + names(out) <- names(.x) + out +} + +map2 <- function(.x, .y, .f, ...) { + .f <- as_function(.f, env = global_env()) + out <- mapply(.f, .x, .y, MoreArgs = list(...), SIMPLIFY = FALSE) + if (length(out) == length(.x)) { + set_names(out, names(.x)) + } else { + set_names(out, NULL) + } +} +map2_lgl <- function(.x, .y, .f, ...) { + as.vector(map2(.x, .y, .f, ...), "logical") +} +map2_int <- function(.x, .y, .f, ...) { + as.vector(map2(.x, .y, .f, ...), "integer") +} +map2_dbl <- function(.x, .y, .f, ...) { + as.vector(map2(.x, .y, .f, ...), "double") +} +map2_chr <- function(.x, .y, .f, ...) { + as.vector(map2(.x, .y, .f, ...), "character") +} +imap <- function(.x, .f, ...) { + map2(.x, names(.x) %||% seq_along(.x), .f, ...) +} + +pmap <- function(.l, .f, ...) { + .f <- as.function(.f) + args <- .rlang_purrr_args_recycle(.l) + do.call("mapply", c( + FUN = list(quote(.f)), + args, MoreArgs = quote(list(...)), + SIMPLIFY = FALSE, USE.NAMES = FALSE + )) +} +.rlang_purrr_args_recycle <- function(args) { + lengths <- map_int(args, length) + n <- max(lengths) + + stopifnot(all(lengths == 1L | lengths == n)) + to_recycle <- lengths == 1L + args[to_recycle] <- map(args[to_recycle], function(x) rep.int(x, n)) + + args +} + +keep <- function(.x, .f, ...) { + .x[.rlang_purrr_probe(.x, .f, ...)] +} +discard <- function(.x, .p, ...) { + sel <- .rlang_purrr_probe(.x, .p, ...) + .x[is.na(sel) | !sel] +} +map_if <- function(.x, .p, .f, ...) { + matches <- .rlang_purrr_probe(.x, .p) + .x[matches] <- map(.x[matches], .f, ...) + .x +} +.rlang_purrr_probe <- function(.x, .p, ...) { + if (is_logical(.p)) { + stopifnot(length(.p) == length(.x)) + .p + } else { + .p <- as_function(.p, env = global_env()) + map_lgl(.x, .p, ...) + } +} + +compact <- function(.x) { + Filter(length, .x) +} + +transpose <- function(.l) { + if (!length(.l)) { + return(.l) + } + + inner_names <- names(.l[[1]]) + + if (is.null(inner_names)) { + fields <- seq_along(.l[[1]]) + } else { + fields <- set_names(inner_names) + .l <- map(.l, function(x) { + if (is.null(names(x))) { + set_names(x, inner_names) + } else { + x + } + }) + } + + # This way missing fields are subsetted as `NULL` instead of causing + # an error + .l <- map(.l, as.list) + + map(fields, function(i) { + map(.l, .subset2, i) + }) +} + +every <- function(.x, .p, ...) { + .p <- as_function(.p, env = global_env()) + + for (i in seq_along(.x)) { + if (!rlang::is_true(.p(.x[[i]], ...))) return(FALSE) + } + TRUE +} +some <- function(.x, .p, ...) { + .p <- as_function(.p, env = global_env()) + + for (i in seq_along(.x)) { + if (rlang::is_true(.p(.x[[i]], ...))) return(TRUE) + } + FALSE +} +negate <- function(.p) { + .p <- as_function(.p, env = global_env()) + function(...) !.p(...) +} + +reduce <- function(.x, .f, ..., .init) { + f <- function(x, y) .f(x, y, ...) + Reduce(f, .x, init = .init) +} +reduce_right <- function(.x, .f, ..., .init) { + f <- function(x, y) .f(y, x, ...) + Reduce(f, .x, init = .init, right = TRUE) +} +accumulate <- function(.x, .f, ..., .init) { + f <- function(x, y) .f(x, y, ...) + Reduce(f, .x, init = .init, accumulate = TRUE) +} +accumulate_right <- function(.x, .f, ..., .init) { + f <- function(x, y) .f(y, x, ...) + Reduce(f, .x, init = .init, right = TRUE, accumulate = TRUE) +} + +detect <- function(.x, .f, ..., .right = FALSE, .p = is_true) { + .p <- as_function(.p, env = global_env()) + .f <- as_function(.f, env = global_env()) + + for (i in .rlang_purrr_index(.x, .right)) { + if (.p(.f(.x[[i]], ...))) { + return(.x[[i]]) + } + } + NULL +} +detect_index <- function(.x, .f, ..., .right = FALSE, .p = is_true) { + .p <- as_function(.p, env = global_env()) + .f <- as_function(.f, env = global_env()) + + for (i in .rlang_purrr_index(.x, .right)) { + if (.p(.f(.x[[i]], ...))) { + return(i) + } + } + 0L +} +.rlang_purrr_index <- function(x, right = FALSE) { + idx <- seq_along(x) + if (right) { + idx <- rev(idx) + } + idx +} + +# nocov end diff --git a/R/context.R b/R/context.R index cba52b6a0..2b0be7a0d 100644 --- a/R/context.R +++ b/R/context.R @@ -78,10 +78,12 @@ get_default_context <- function() { .ctx_env$default_context } +#' @export package_name <- function(ctx) { attr(class(ctx$drv), "package") } +#' @export connect <- function(ctx, ...) { quos <- enquos(...) eval_tidy(quo(dbConnect(ctx$cnr, !!!quos))) diff --git a/R/dbi.R b/R/dbi.R index 399689061..66936a115 100644 --- a/R/dbi.R +++ b/R/dbi.R @@ -29,6 +29,7 @@ create_generics <- function() { writeLines(text, "R/generics.R") } +#' @export dbi_generics <- function(version) { version <- as.package_version(version) diff --git a/R/expectations.R b/R/expectations.R index dfaed0758..c2ef8e73c 100644 --- a/R/expectations.R +++ b/R/expectations.R @@ -9,6 +9,7 @@ expect_arglist_is_empty <- function(object) { invisible(act$val) } +#' @export expect_all_args_have_default_values <- function(object) { act <- quasi_label(rlang::enquo(object), arg = "object") act$args <- formals(act$val) @@ -38,6 +39,7 @@ expect_visible <- function(code) { ret$value } +#' @export expect_invisible_true <- function(code) { ret <- withVisible(code) expect_true(ret$value) @@ -46,6 +48,7 @@ expect_invisible_true <- function(code) { invisible(ret$value) } +#' @export expect_equal_df <- function(actual, expected) { factor_cols <- vapply(expected, is.factor, logical(1L)) expected[factor_cols] <- lapply(expected[factor_cols], as.character) diff --git a/R/import-testthat.R b/R/import-testthat.R index 2d5b2c264..808ef86b0 100644 --- a/R/import-testthat.R +++ b/R/import-testthat.R @@ -1,6 +1,7 @@ #' @import testthat #' @importFrom rlang quo enquo enquos expr enexpr eval_tidy list2 has_length := -#' @importFrom rlang abort is_interactive +#' @importFrom rlang abort is_interactive as_function local_options seq2 set_names +#' @importFrom rlang %||% NULL #' @importFrom methods findMethod getClasses getClass extends diff --git a/R/s4.R b/R/s4.R index 6e0d16677..25d9eced4 100644 --- a/R/s4.R +++ b/R/s4.R @@ -1,4 +1,5 @@ # http://stackoverflow.com/a/39880324/946850 +#' @export s4_methods <- function(env, pkg_fun = NULL) { generics <- methods::getGenerics(env) diff --git a/R/spec-all.R b/R/spec-all.R index 69f482b05..d5534c38f 100644 --- a/R/spec-all.R +++ b/R/spec-all.R @@ -7,5 +7,21 @@ spec_all <- c( spec_meta, spec_transaction, spec_compliance, - spec_stress + spec_stress, + # + NULL +) + +nested_spec_all <- list( + getting_started = spec_getting_started, + driver = spec_driver, + connection = spec_connection, + result = spec_result, + sql = spec_sql, + meta = spec_meta, + transaction = spec_transaction, + compliance = spec_compliance, + stress = spec_stress, + # + NULL ) diff --git a/R/spec-compliance-methods.R b/R/spec-compliance-methods.R index 7cfb4b17d..f42bdfb2e 100644 --- a/R/spec-compliance-methods.R +++ b/R/spec-compliance-methods.R @@ -8,6 +8,8 @@ spec_compliance_methods <- list( compliance = function(ctx) { #' A backend defines three classes, + key_methods <- get_key_methods() + #' which are subclasses of expect_identical( names(key_methods), @@ -93,6 +95,7 @@ spec_compliance_methods <- list( # Helpers ----------------------------------------------------------------- #' @importFrom methods hasMethod +#' @export expect_has_class_method <- function(name, class, args, driver_package) { full_args <- c(class, args) eval(bquote( @@ -100,6 +103,7 @@ expect_has_class_method <- function(name, class, args, driver_package) { )) } +#' @export expect_ellipsis_in_formals <- function(method, name) { sym <- as.name(name) eval(bquote({ @@ -108,37 +112,40 @@ expect_ellipsis_in_formals <- function(method, name) { })) } -key_methods <- list( - Driver = list( - "dbConnect" = NULL, - "dbDataType" = NULL - ), - Connection = list( - "dbDisconnect" = NULL, - "dbGetInfo" = NULL, - "dbSendQuery" = "character", - "dbListFields" = "character", - "dbListTables" = NULL, - "dbReadTable" = "character", - "dbWriteTable" = c("character", "data.frame"), - "dbExistsTable" = "character", - "dbRemoveTable" = "character", - "dbBegin" = NULL, - "dbCommit" = NULL, - "dbRollback" = NULL, - "dbIsValid" = NULL, - "dbQuoteString" = "character", - "dbQuoteIdentifier" = "character" - ), - Result = list( - "dbIsValid" = NULL, - "dbFetch" = NULL, - "dbClearResult" = NULL, - "dbColumnInfo" = NULL, - "dbGetRowsAffected" = NULL, - "dbGetRowCount" = NULL, - "dbHasCompleted" = NULL, - "dbGetStatement" = NULL, - "dbBind" = NULL +#' @export +get_key_methods <- function() { + list( + Driver = list( + "dbConnect" = NULL, + "dbDataType" = NULL + ), + Connection = list( + "dbDisconnect" = NULL, + "dbGetInfo" = NULL, + "dbSendQuery" = "character", + "dbListFields" = "character", + "dbListTables" = NULL, + "dbReadTable" = "character", + "dbWriteTable" = c("character", "data.frame"), + "dbExistsTable" = "character", + "dbRemoveTable" = "character", + "dbBegin" = NULL, + "dbCommit" = NULL, + "dbRollback" = NULL, + "dbIsValid" = NULL, + "dbQuoteString" = "character", + "dbQuoteIdentifier" = "character" + ), + Result = list( + "dbIsValid" = NULL, + "dbFetch" = NULL, + "dbClearResult" = NULL, + "dbColumnInfo" = NULL, + "dbGetRowsAffected" = NULL, + "dbGetRowCount" = NULL, + "dbHasCompleted" = NULL, + "dbGetStatement" = NULL, + "dbBind" = NULL + ) ) -) +} diff --git a/R/spec-driver-data-type.R b/R/spec-driver-data-type.R index 2271f6167..40e63b1f8 100644 --- a/R/spec-driver-data-type.R +++ b/R/spec-driver-data-type.R @@ -20,6 +20,7 @@ spec_driver_data_type <- list( #' test_data_type #' @param ctx,dbObj Arguments to internal test function #' @keywords internal +#' @export test_data_type <- function(ctx, dbObj) { #' @return #' `dbDataType()` returns the SQL type that corresponds to the `obj` argument diff --git a/R/spec-getting-started.R b/R/spec-getting-started.R index 355c31b20..c1b080acb 100644 --- a/R/spec-getting-started.R +++ b/R/spec-getting-started.R @@ -2,7 +2,6 @@ #' @family getting specifications #' @usage NULL #' @format NULL -#' @importFrom desc desc_get_deps #' @keywords NULL #' @section Definition: spec_getting_started <- list( @@ -10,7 +9,7 @@ spec_getting_started <- list( #' A DBI backend is an R package pkg_path <- get_pkg_path(ctx) - pkg_deps_df <- desc_get_deps(pkg_path) + pkg_deps_df <- desc::desc_get_deps(pkg_path) pkg_imports <- pkg_deps_df$package[pkg_deps_df$type == "Imports"] #' which imports the \pkg{DBI} diff --git a/R/spec-meta-bind-.R b/R/spec-meta-bind-.R index 218c2d8c5..2005f1408 100644 --- a/R/spec-meta-bind-.R +++ b/R/spec-meta-bind-.R @@ -1,5 +1,6 @@ # Helpers ----------------------------------------------------------------- +#' @export test_select_bind <- function(con, ctx, ...) { lapply( get_placeholder_funs(ctx), @@ -10,6 +11,7 @@ test_select_bind <- function(con, ctx, ...) { ) } +#' @export get_placeholder_funs <- function(ctx) { placeholder_fun <- ctx$tweaks$placeholder_pattern if (is.character(placeholder_fun)) { @@ -93,7 +95,7 @@ BindTester <- R6::R6Class( ret_values <- trivial_values(2) placeholder <- placeholder_fun(length(values)) is_na <- vapply(values, is_na_or_null, logical(1)) - placeholder_values <- vapply(values, function(x) quote_literal(con, x[1]), character(1)) + placeholder_values <- vapply(values, function(x) DBI::dbQuoteLiteral(con, x[1]), character(1)) result_names <- letters[seq_along(values)] query <- paste0( diff --git a/R/spec-meta-bind-tester-extra.R b/R/spec-meta-bind-tester-extra.R index d8ee21dde..444a3c990 100644 --- a/R/spec-meta-bind-tester-extra.R +++ b/R/spec-meta-bind-tester-extra.R @@ -13,6 +13,7 @@ BindTesterExtra <- R6::R6Class( ) ) +#' @export new_bind_tester_extra <- function(...) { R6::R6Class( inherit = BindTesterExtra, diff --git a/R/spec-meta-bind.R b/R/spec-meta-bind.R index a362e8000..f57694ddd 100644 --- a/R/spec-meta-bind.R +++ b/R/spec-meta-bind.R @@ -243,7 +243,7 @@ spec_meta_bind <- list( bind_character = function(ctx, con) { #' - [character] - test_select_bind(con, ctx, c(texts, NA)) + test_select_bind(con, ctx, c(get_texts(), NA)) }, bind_character_escape = function(ctx, con) { @@ -258,7 +258,7 @@ spec_meta_bind <- list( test_select_bind( con, ctx, - lapply(c(texts, NA_character_), factor) + lapply(c(get_texts(), NA_character_), factor) ) )) }, diff --git a/R/spec-meta-get-row-count.R b/R/spec-meta-get-row-count.R index 426cdb2ad..4e70691c6 100644 --- a/R/spec-meta-get-row-count.R +++ b/R/spec-meta-get-row-count.R @@ -27,7 +27,7 @@ spec_meta_get_row_count <- list( }, # row_count_query_limited = function(ctx, con) { - query <- union(.ctx = ctx, trivial_query(), "SELECT 2", "SELECT 3") + query <- sql_union(.ctx = ctx, trivial_query(), "SELECT 2", "SELECT 3") res <- local_result(dbSendQuery(con, query)) rc1 <- dbGetRowCount(res) expect_equal(rc1, 0L) @@ -44,7 +44,7 @@ spec_meta_get_row_count <- list( # row_count_query_empty = function(ctx, con) { #' For queries with an empty result set, - query <- union( + query <- sql_union( .ctx = ctx, "SELECT * FROM (SELECT 1 as a) a WHERE (0 = 1)" ) res <- local_result(dbSendQuery(con, query)) diff --git a/R/spec-result-fetch.R b/R/spec-result-fetch.R index d04830244..8699f7bbe 100644 --- a/R/spec-result-fetch.R +++ b/R/spec-result-fetch.R @@ -96,7 +96,7 @@ spec_result_fetch <- list( fetch_multi_row_multi_column = function(ctx, con) { #' or more columns by default returns the entire result. - query <- union( + query <- sql_union( .ctx = ctx, paste("SELECT", 1:5 + 0.5, "AS a,", 4:0 + 0.5, "AS b"), .order_by = "a" ) diff --git a/R/spec-result-get-query.R b/R/spec-result-get-query.R index b8ecab917..510976af3 100644 --- a/R/spec-result-get-query.R +++ b/R/spec-result-get-query.R @@ -120,7 +120,7 @@ spec_result_get_query <- list( get_query_multi_row_multi_column = function(ctx, con) { #' or more columns returns the entire result. - query <- union( + query <- sql_union( .ctx = ctx, paste("SELECT", 1:5 + 0.5, "AS a,", 4:0 + 0.5, "AS b"), .order_by = "a" ) diff --git a/R/spec-result-roundtrip.R b/R/spec-result-roundtrip.R index d47d196f8..7868d6392 100644 --- a/R/spec-result-roundtrip.R +++ b/R/spec-result-roundtrip.R @@ -31,7 +31,7 @@ spec_result_roundtrip <- list( data_character = function(ctx, con) { #' - [character] for text, - values <- texts + values <- get_texts() test_funs <- rep(list(has_utf8_or_ascii_encoding), length(values)) sql_names <- as.character(dbQuoteString(con, values)) @@ -46,8 +46,12 @@ spec_result_roundtrip <- list( skip("tweak: omit_blob_tests") } + is_raw_list <- function(x) { + is.list(x) && is.raw(x[[1L]]) + } + values <- list(is_raw_list) - sql_names <- ctx$tweaks$blob_cast(quote_literal(con, list(raw(1)))) + sql_names <- ctx$tweaks$blob_cast(DBI::dbQuoteLiteral(con, list(raw(1)))) #' with [NULL] entries for SQL NULL values test_select_with_null(.ctx = ctx, con, .dots = setNames(values, sql_names)) @@ -55,6 +59,12 @@ spec_result_roundtrip <- list( data_date = function(ctx, con) { #' - coercible using [as.Date()] for dates, + as_date_equals_to <- function(x) { + lapply(x, function(xx) { + function(value) as.Date(value) == xx + }) + } + char_values <- paste0("2015-01-", sprintf("%.2d", 1:12)) values <- as_date_equals_to(as.Date(char_values)) sql_names <- ctx$tweaks$date_cast(char_values) @@ -73,6 +83,12 @@ spec_result_roundtrip <- list( data_time = function(ctx, con) { #' - coercible using [hms::as_hms()] for times, + as_hms_equals_to <- function(x) { + lapply(x, function(xx) { + function(value) hms::as_hms(value) == xx + }) + } + char_values <- c("00:00:00", "12:34:56") time_values <- as_hms_equals_to(hms::as_hms(char_values)) sql_names <- ctx$tweaks$time_cast(char_values) @@ -91,6 +107,11 @@ spec_result_roundtrip <- list( data_timestamp = function(ctx, con) { #' - coercible using [as.POSIXct()] for timestamps, + coercible_to_timestamp <- function(x) { + x_timestamp <- try_silent(as.POSIXct(x)) + !is.null(x_timestamp) && all(is.na(x) == is.na(x_timestamp)) + } + char_values <- c("2015-10-11 00:00:00", "2015-10-11 12:34:56") time_values <- rep(list(coercible_to_timestamp), 2L) sql_names <- ctx$tweaks$timestamp_cast(char_values) @@ -101,6 +122,15 @@ spec_result_roundtrip <- list( data_timestamp_current = function(ctx, con) { #' (also applies to the return value of the SQL function `current_timestamp`) + coercible_to_timestamp <- function(x) { + x_timestamp <- try_silent(as.POSIXct(x)) + !is.null(x_timestamp) && all(is.na(x) == is.na(x_timestamp)) + } + + is_roughly_current_timestamp <- function(x) { + coercible_to_timestamp(x) && (Sys.time() - as.POSIXct(x, tz = "UTC") <= hms::hms(2)) + } + test_select_with_null( .ctx = ctx, con, "current_timestamp" ~ is_roughly_current_timestamp @@ -170,6 +200,12 @@ spec_result_roundtrip <- list( #' - Coercion to numeric always returns a number that is as close as possible #' to the true value data_64_bit_numeric = function(ctx, con) { + as_numeric_identical_to <- function(x) { + lapply(x, function(xx) { + function(value) as.numeric(value) == xx + }) + } + char_values <- c("10000000000", "-10000000000") test_values <- as_numeric_identical_to(as.numeric(char_values)) @@ -178,6 +214,12 @@ spec_result_roundtrip <- list( #' - Loss of precision when converting to numeric gives a warning data_64_bit_numeric_warning = function(ctx, con) { + as_numeric_equals_to <- function(x) { + lapply(x, function(xx) { + function(value) isTRUE(all.equal(as.numeric(value), xx)) + }) + } + char_values <- c(" 1234567890123456789", "-1234567890123456789") num_values <- as.numeric(char_values) test_values <- as_numeric_equals_to(num_values) @@ -202,6 +244,12 @@ spec_result_roundtrip <- list( #' - Conversion to character always returns a lossless decimal representation #' of the data data_64_bit_lossless = function(ctx, con) { + as_character_equals_to <- function(x) { + lapply(x, function(xx) { + function(value) as.character(value) == xx + }) + } + char_values <- c("1234567890123456789", "-1234567890123456789") test_values <- as_character_equals_to(char_values) @@ -212,12 +260,14 @@ spec_result_roundtrip <- list( ) +#' @export test_select_with_null <- function(...) { test_select(..., .add_null = "none") test_select(..., .add_null = "above") test_select(..., .add_null = "below") } +#' @export test_select <- function(con, ..., .dots = NULL, .add_null = "none", .ctx, .envir = parent.frame()) { values <- c(list(...), .dots) @@ -260,7 +310,7 @@ test_select <- function(con, ..., .dots = NULL, .add_null = "none", query <- rev(query) } query <- paste0(query, ", ", 1:2, " as id") - query <- union(.ctx = .ctx, query) + query <- sql_union(.ctx = .ctx, query) } rows <- check_df(dbGetQuery(con, query)) @@ -307,6 +357,7 @@ all_have_utf8_or_ascii_encoding <- function(x) { all(vapply(x, has_utf8_or_ascii_encoding, logical(1L))) } +#' @export has_utf8_or_ascii_encoding <- function(x) { if (Encoding(x) == "UTF-8") { TRUE @@ -322,21 +373,11 @@ has_utf8_or_ascii_encoding <- function(x) { } } -is_raw_list <- function(x) { - is.list(x) && is.raw(x[[1L]]) -} - coercible_to_date <- function(x) { x_date <- try_silent(as.Date(x)) !is.null(x_date) && all(is.na(x) == is.na(x_date)) } -as_date_equals_to <- function(x) { - lapply(x, function(xx) { - function(value) as.Date(value) == xx - }) -} - is_roughly_current_date <- function(x) { coercible_to_date(x) && (abs(Sys.Date() - as.Date(x)) <= 1) } @@ -346,45 +387,12 @@ coercible_to_time <- function(x) { !is.null(x_hms) && all(is.na(x) == is.na(x_hms)) } -as_hms_equals_to <- function(x) { - lapply(x, function(xx) { - function(value) hms::as_hms(value) == xx - }) -} - -coercible_to_timestamp <- function(x) { - x_timestamp <- try_silent(as.POSIXct(x)) - !is.null(x_timestamp) && all(is.na(x) == is.na(x_timestamp)) -} - as_timestamp_equals_to <- function(x) { lapply(x, function(xx) { function(value) as.POSIXct(value) == xx }) } -as_numeric_identical_to <- function(x) { - lapply(x, function(xx) { - function(value) as.numeric(value) == xx - }) -} - -as_numeric_equals_to <- function(x) { - lapply(x, function(xx) { - function(value) isTRUE(all.equal(as.numeric(value), xx)) - }) -} - -as_character_equals_to <- function(x) { - lapply(x, function(xx) { - function(value) as.character(value) == xx - }) -} - -is_roughly_current_timestamp <- function(x) { - coercible_to_timestamp(x) && (Sys.time() - as.POSIXct(x, tz = "UTC") <= hms::hms(2)) -} - is_date <- function(x) { inherits(x, "Date") } @@ -405,7 +413,3 @@ as_numeric_date <- function(d) { d <- as.Date(d) structure(as.numeric(unclass(d)), class = class(d)) } - -quote_literal <- function(con, x) { - DBI::dbQuoteLiteral(con, x) -} diff --git a/R/spec-result.R b/R/spec-result.R index 1d12f29e9..2236f447d 100644 --- a/R/spec-result.R +++ b/R/spec-result.R @@ -15,7 +15,8 @@ spec_result <- c( # Helpers ----------------------------------------------------------------- -union <- function(..., .order_by = NULL, .ctx) { +#' @export +sql_union <- function(..., .order_by = NULL, .ctx) { query <- .ctx$tweaks$union(c(...)) if (!missing(.order_by)) { @@ -28,12 +29,13 @@ trivial_statement <- function(ctx, table_name) { ctx$tweaks$create_table_as(table_name, trivial_query()) } +#' @export trivial_query <- function(n = 1L, column = "a", .order_by = NULL, .ctx = NULL) { value <- trivial_values(n) if (length(column) == n) { query <- paste0("SELECT ", paste0(value, " AS ", column, collapse = ", ")) } else { - query <- union( + query <- sql_union( paste0("SELECT ", value, " AS ", column), .order_by = .order_by, .ctx = .ctx @@ -43,10 +45,12 @@ trivial_query <- function(n = 1L, column = "a", .order_by = NULL, .ctx = NULL) { query } +#' @export trivial_values <- function(n = 1L) { seq_len(n) + 0.5 } +#' @export trivial_df <- function(n = 1L, column = "a") { values <- trivial_values(n) if (length(column) == 1) { diff --git a/R/spec-sql-append-table.R b/R/spec-sql-append-table.R index 58ebdebdc..0cf0b592b 100644 --- a/R/spec-sql-append-table.R +++ b/R/spec-sql-append-table.R @@ -230,8 +230,8 @@ spec_sql_append_table <- list( append_roundtrip_character = function(con) { #' - character (in both UTF-8 tbl_in <- data.frame( - id = seq_along(texts), - a = c(texts), + id = seq_along(get_texts()), + a = get_texts(), stringsAsFactors = FALSE ) test_table_roundtrip(use_append = TRUE, con, tbl_in) @@ -240,7 +240,7 @@ spec_sql_append_table <- list( append_roundtrip_character_native = function(con) { #' and native encodings), tbl_in <- data.frame( - a = c(enc2native(texts)), + a = c(enc2native(get_texts())), stringsAsFactors = FALSE ) test_table_roundtrip(use_append = TRUE, con, tbl_in) @@ -267,7 +267,7 @@ spec_sql_append_table <- list( append_roundtrip_factor = function(con) { #' - factor (returned as character, tbl_in <- data.frame( - a = factor(c(texts)) + a = factor(get_texts()) ) tbl_exp <- tbl_in tbl_exp$a <- as.character(tbl_exp$a) diff --git a/R/spec-sql-quote-string.R b/R/spec-sql-quote-string.R index 63f6c9b4c..38c092c66 100644 --- a/R/spec-sql-quote-string.R +++ b/R/spec-sql-quote-string.R @@ -67,6 +67,11 @@ spec_sql_quote_string <- list( expect_identical(unlist(unname(x_out)), x) } + expand_char <- function(...) { + df <- expand.grid(..., stringsAsFactors = FALSE) + do.call(paste0, df) + } + test_chars <- c( #' even if `x` contains "", diff --git a/R/spec-sql-write-table.R b/R/spec-sql-write-table.R index ab45a2eb1..d34107886 100644 --- a/R/spec-sql-write-table.R +++ b/R/spec-sql-write-table.R @@ -443,8 +443,8 @@ spec_sql_write_table <- list( roundtrip_character = function(ctx, con) { #' - character (in both UTF-8 tbl_in <- data.frame( - id = seq_along(texts), - a = c(texts), + id = seq_along(get_texts()), + a = get_texts(), stringsAsFactors = FALSE ) test_table_roundtrip(con, tbl_in) @@ -453,7 +453,7 @@ spec_sql_write_table <- list( roundtrip_character_native = function(ctx, con) { #' and native encodings), tbl_in <- data.frame( - a = c(enc2native(texts)), + a = c(enc2native(get_texts())), stringsAsFactors = FALSE ) test_table_roundtrip(con, tbl_in) @@ -480,7 +480,7 @@ spec_sql_write_table <- list( roundtrip_factor = function(ctx, con) { #' - factor (returned as character) tbl_in <- data.frame( - a = factor(c(texts)) + a = factor(get_texts()) ) tbl_exp <- tbl_in tbl_exp$a <- as.character(tbl_exp$a) @@ -822,12 +822,14 @@ spec_sql_write_table <- list( NULL ) +#' @export test_table_roundtrip <- function(...) { test_table_roundtrip_one(..., .add_na = "none") test_table_roundtrip_one(..., .add_na = "above") test_table_roundtrip_one(..., .add_na = "below") } +#' @export test_table_roundtrip_one <- function(con, tbl_in, tbl_expected = tbl_in, transform = identity, name = NULL, field.types = NULL, use_append = FALSE, .add_na = "none") { force(tbl_expected) diff --git a/R/use.R b/R/use.R new file mode 100644 index 000000000..761a7b3ec --- /dev/null +++ b/R/use.R @@ -0,0 +1,112 @@ +use_dbitest <- function(path) { + usethis::local_project(path) + local_options(usethis.overwrite = TRUE) + usethis::use_testthat(edition = 2) + usethis::use_package_doc(open = FALSE) + usethis::use_import_from("rlang", c( + "quo", + "enquo", + "enquos", + "expr", + "enexpr", + "eval_tidy", + "list2", + "has_length", + ":=", + "abort", + "is_interactive", + "as_function", + "local_options", + "seq2", + "set_names", + "%||%", + NULL + )) + usethis::use_import_from("DBItest", c( + "get_default_context", + "connect", + "local_connection", + "local_result", + "local_remove_test_table", + "try_silent", + "package_name", + "get_pkg_path", + "s4_methods", + "expect_invisible_true", + "expect_equal_df", + "expect_all_args_have_default_values", + "expect_ellipsis_in_formals", + "expect_has_class_method", + "get_key_methods", + "dbi_generics", + "check_df", + "random_table_name", + "get_placeholder_funs", + "has_utf8_or_ascii_encoding", + # + "sql_union", + "trivial_query", + "trivial_df", + "trivial_values", + "get_penguins", + # + "get_texts", + "unrowname", + # + "test_data_type", + "test_select_with_null", + "test_select", + "test_select_bind", + "new_bind_tester_extra", + "test_table_roundtrip", + "test_table_roundtrip_one", + NULL + )) + + invisible(map2(nested_spec_all, names(nested_spec_all), use_dbitest_spec)) +} + +use_dbitest_spec <- function(spec, name) { + if (name == "") { + return() + } + + path <- file.path("tests/testthat", paste0("test-dbitest-", name, ".R")) + header <- c( + "# Created by DBItest::use_dbitest(), do not edit by hand", + "ctx <- get_default_context()", + "con <- local_connection(ctx)", + NULL + ) + + tests <- compact(map2(spec, names(spec), get_dbitest_test)) + tests_flat <- map_chr(tests, paste, collapse = "\n") + tests_all <- paste(c(header, tests_flat), collapse = "\n\n") + + usethis::write_over(path, tests_all) +} + +get_dbitest_test <- function(fun, name) { + if (is.null(fun)) { + return(NULL) + } + + # FIXME: make more inclusive + if (!all(names(formals(fun)) %in% c("ctx", "con"))) { + return(NULL) + } + + body_code <- format_body(fun) + + c( + paste0('test_that("', name, '", {'), + body_code, + "})" + ) +} + +format_body <- function(fun) { + construct <- constructive::construct(fun, check = TRUE, ignore_function_env = TRUE) + flat <- unlist(strsplit(format(construct$code), "\n", fixed = TRUE)) + trimws(flat[seq2(2, length(flat) - 1)], "right") +} diff --git a/R/utf8.R b/R/utf8.R index 8a11111a5..76910c34c 100644 --- a/R/utf8.R +++ b/R/utf8.R @@ -8,4 +8,7 @@ text_chinese <- "\u6211\u662f\u8c01" text_ascii <- iconv("ASCII", to = "ASCII") -texts <- c(text_cyrillic, text_latin, text_latin_encoded, text_chinese, text_ascii) +#' @export +get_texts <- function() { + c(text_cyrillic, text_latin, text_latin_encoded, text_chinese, text_ascii) +} diff --git a/R/utils.R b/R/utils.R index 65a8f6b2e..8bd409d6a 100644 --- a/R/utils.R +++ b/R/utils.R @@ -1,5 +1,4 @@ -`%||%` <- function(a, b) if (is.null(a)) b else a - +#' @export get_pkg_path <- function(ctx) { pkg_name <- package_name(ctx) expect_type(pkg_name, "character") @@ -11,6 +10,7 @@ get_pkg_path <- function(ctx) { utils::globalVariables("con") utils::globalVariables("con2") +#' @export local_connection <- function(ctx, ..., .local_envir = parent.frame()) { con <- connect(ctx, ...) withr::local_db_connection(con, .local_envir = .local_envir) @@ -29,6 +29,7 @@ local_invalid_connection <- function(ctx, ...) { } # Calls `dbClearResult()` on `query` after exiting `frame`. +#' @export local_result <- function(query, frame = rlang::caller_env()) { res <- query withr::defer( @@ -41,6 +42,7 @@ local_result <- function(query, frame = rlang::caller_env()) { } # Calls `try_silent(dbRemoveTable())` after exiting `frame`. +#' @export local_remove_test_table <- function(con, name, frame = rlang::caller_env()) { table_name <- dbQuoteIdentifier(con, name) withr::defer( @@ -51,6 +53,7 @@ local_remove_test_table <- function(con, name, frame = rlang::caller_env()) { ) } +#' @export get_penguins <- function(ctx) { datasets_penguins <- unrowname(palmerpenguins::penguins[c(1, 153, 277), ]) if (!isTRUE(ctx$tweaks$strict_identifier)) { @@ -59,11 +62,13 @@ get_penguins <- function(ctx) { as.data.frame(datasets_penguins) } +#' @export unrowname <- function(x) { rownames(x) <- NULL x } +#' @export random_table_name <- function(n = 10) { # FIXME: Use parallel-safe sequence of numbers paste0("dbit", paste(sample(letters, n, replace = TRUE), collapse = "")) @@ -73,11 +78,8 @@ compact <- function(x) { x[!vapply(x, is.null, logical(1L))] } -expand_char <- function(...) { - df <- expand.grid(..., stringsAsFactors = FALSE) - do.call(paste0, df) -} +#' @export try_silent <- function(code) { tryCatch( code, @@ -85,6 +87,7 @@ try_silent <- function(code) { ) } +#' @export check_df <- function(df) { expect_s3_class(df, "data.frame") if (length(df) >= 1L) { diff --git a/tests/testthat/test-consistency.R b/tests/testthat/test-spec-all.R similarity index 88% rename from tests/testthat/test-consistency.R rename to tests/testthat/test-spec-all.R index ec41d6a10..81aa7c291 100644 --- a/tests/testthat/test-consistency.R +++ b/tests/testthat/test-spec-all.R @@ -37,3 +37,10 @@ test_that("all specs used", { new_names <- setdiff(defined_spec_names, names(spec_all)) expect_equal(new_names, rep("", length(new_names))) }) + +test_that("nested is the same as flat", { + expect_equal( + gsub("^[^.]+[.]", "", names(compact(unlist(nested_spec_all)))), + names(compact(spec_all)) + ) +})