diff --git a/DESCRIPTION b/DESCRIPTION index cf816e8..38cccf7 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -16,11 +16,11 @@ Imports: cli (>= 3.6.2), tidyselect (>= 1.2.1), purrr (>= 1.0.2), - RSQLite (>= 2.3.5), - rlang (>= 1.1.3) + RSQLite (>= 2.3.5) Suggests: devtools (>= 2.2.0), - testthat (>= 3.0.0) + testthat (>= 3.0.0), + withr (>= 3.0.0) License: MIT + file LICENSE Encoding: UTF-8 Roxygen: list(markdown = TRUE) diff --git a/NAMESPACE b/NAMESPACE index bef5d05..f6b2243 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -3,11 +3,13 @@ export(add_entry_tbl) export(check_notnull_fields) export(check_pkeys_fields) +export(delete_entry_tbl) export(detect_cens) export(get_tbl_fields_notnull) export(get_tbl_fields_pkey) export(get_tbl_info) export(init_con) +export(modify_entry_tbl) export(remove_cens) export(search_tbl) export(uncensored) diff --git a/R/db_con.R b/R/db_con.R index 1005d74..10765e7 100644 --- a/R/db_con.R +++ b/R/db_con.R @@ -9,5 +9,5 @@ init_con <- function(){ db <- file.path(db_path(), db_name()) stopifnot(db |> file.exists()) - DBI::dbConnect(RSQLite::SQLite(), db) + return(DBI::dbConnect(RSQLite::SQLite(), db)) } diff --git a/R/db_info_misc.R b/R/db_info_misc.R index 586d9ed..c5ca882 100644 --- a/R/db_info_misc.R +++ b/R/db_info_misc.R @@ -1,5 +1,6 @@ #' Check if pkeys presents #' +#' @param con connexion object returned by DBI::dbConnect() #' @param tbl a character name of the table #' @param fields a vector of column names in the specified table #' @@ -8,12 +9,12 @@ #' #' @examples #' \dontrun{ -#' check_pkeys_fields("species", fields = c("species_id")) +#' check_pkeys_fields(con, "species", fields = c("species_id")) #' } #' @export #' -check_pkeys_fields <- function(tbl, fields){ - pkeys <- get_tbl_fields_pkey(tbl) +check_pkeys_fields <- function(con, tbl, fields){ + pkeys <- get_tbl_fields_pkey(con, tbl = tbl) if(!all(pkeys %in% names(fields))){ missing_pkeys <- pkeys[which(!pkeys %in% fields)] |> @@ -24,6 +25,7 @@ check_pkeys_fields <- function(tbl, fields){ #' Check if not null fields present #' +#' @param con connexion object returned by DBI::dbConnect() #' @param tbl a character name of the table #' @param fields a vector of column names in the specified table #' @@ -32,14 +34,14 @@ check_pkeys_fields <- function(tbl, fields){ #' #' @examples #' \dontrun{ -#' check_notnull_fields("species", fields = c("species_id")) +#' check_notnull_fields(con, "species", fields = c("species_id")) #' } #' #' @export -check_notnull_fields <- function(tbl, fields){ - notnulls <- get_tbl_fields_notnull(tbl) +check_notnull_fields <- function(con, tbl, fields){ + notnulls <- get_tbl_fields_notnull(con, tbl = tbl) - if(!all(notnulls %in% fields)){ + if(!all(notnulls %in% names(fields))){ missing_fields <- notnulls[which(!notnulls %in% fields)] |> glue::glue_collapse(", ", last = "and") cli::cli_abort("{ missing_fields } cannot be null(s)") @@ -48,6 +50,7 @@ check_notnull_fields <- function(tbl, fields){ #' Get table fields properties (fkey, pkey, unique constraints etc.) #' +#' @param con connexion object returned by DBI::dbConnect() #' @param tbl a character name of the table #' #' @return @@ -64,15 +67,15 @@ check_notnull_fields <- function(tbl, fields){ #' get_tbl_info("species") #' } #' @export -get_tbl_info <- function(tbl) { - con <- init_con() - res <- DBI::dbGetQuery(con, glue::glue("SELECT * FROM pragma_table_info('{ tbl }');")) - on.exit(DBI::dbDisconnect(con)) +get_tbl_info <- function(con = NULL, tbl = NULL) { + q <- glue::glue_sql("SELECT * FROM pragma_table_info({tbl});", .con = con) + res <- DBI::dbGetQuery(con, q) return(res) } #' Get table primary key fields #' +#' @param con connexion object returned by DBI::dbConnect() #' @param tbl a character name of the table #' #' @return @@ -83,14 +86,15 @@ get_tbl_info <- function(tbl) { #' get_tbl_fields_pkey("species") #' } #' @export -get_tbl_fields_pkey <- function(tbl){ - get_tbl_info(tbl) |> - dplyr::filter(rlang::.data$pk == 1) |> - dplyr::pull(rlang::.data$name) +get_tbl_fields_pkey <- function(con, tbl){ + get_tbl_info(con, tbl = tbl) |> + dplyr::filter(pk == 1) |> + dplyr::pull(name) } #' Get table not null fields #' +#' @param con connexion object returned by DBI::dbConnect() #' @param tbl a character name of the table #' #' @return @@ -101,9 +105,9 @@ get_tbl_fields_pkey <- function(tbl){ #' get_tbl_fields_notnull("species") #' } #' @export -get_tbl_fields_notnull <- function(tbl){ - get_tbl_info(tbl) |> - dplyr::filter(rlang::.data$notnull == 1) |> - dplyr::pull(rlang::.data$name) +get_tbl_fields_notnull <- function(con, tbl){ + get_tbl_info(con, tbl = tbl) |> + dplyr::filter(notnull == 1) |> + dplyr::pull(name) } diff --git a/R/db_search.R b/R/db_search.R index 5d79c82..734c897 100644 --- a/R/db_search.R +++ b/R/db_search.R @@ -1,5 +1,6 @@ #' search_tbl #' +#' @param con connexion object returned by DBI::dbConnect() #' @param tbl a character name of the table #' @param ... table fields with search value #' @@ -8,11 +9,11 @@ #' #' @examples #' \dontrun{ -#' search_tbl("species", genus = "%Poo%") +#' search_tbl(con, "species", genus = "%Poo%") #' } #' @export #' -search_tbl <- function(tbl,...){ +search_tbl <- function(con = NULL, tbl = NULL,...){ fields <- list(...) tbl_fields <- DBI::dbListFields(con, tbl) @@ -22,18 +23,16 @@ search_tbl <- function(tbl,...){ stopifnot(all(names(fields) %in% tbl_fields)) search_criterias <- purrr::map2(names(fields), fields, \(n, p){ - if(length(p) == 1L){ - glue::glue("{n} LIKE ${n}") + if(length(p) > 1L){ + glue::glue_sql("{`n`} IN ({ p* })", .con = con) } else { - cli::cli_abort("{n} as more than one search criterias") + glue::glue_sql("{`n`} LIKE { p }", .con = con) } - }) |> glue::glue_collapse(" OR ") + }) |> glue::glue_sql_collapse(" OR ") - query <- glue::glue("SELECT * FROM {tbl} WHERE {search_criterias};") - - con <- init_con() + query <- glue::glue_sql("SELECT * FROM { tbl } WHERE { search_criterias };", .con = con) + res <- DBI::dbSendQuery(con, query) - DBI::dbBind(res, fields) entries <- list() while (!DBI::dbHasCompleted(res)) { @@ -41,10 +40,9 @@ search_tbl <- function(tbl,...){ } entries <- dplyr::bind_rows(entries) - cli::cli_alert_info("Found {nrow(entries)} entries") + cli::cli_alert_info("Found { nrow(entries) } entries") on.exit(DBI::dbClearResult(res)) - on.exit(DBI::dbDisconnect(con), add=TRUE, after = TRUE) return(entries) } diff --git a/R/db_tbl.R b/R/db_tbl.R index 4c72efb..1214c0f 100644 --- a/R/db_tbl.R +++ b/R/db_tbl.R @@ -1,42 +1,37 @@ -#' DB getters -#' -#' Retrieve specific information from our admin database. +#' Add entry within specified table #' +#' @param con connexion object returned by DBI::dbConnect() #' @param tbl a character name of the table #' @param ... a vector of column names in the specified table #' -#' -#' @describeIn get_column_elements Get species name list. #' @export #' -add_entry_tbl <- function(tbl, ...){ +add_entry_tbl <- function(con = NULL, tbl = NULL, ...){ fields <- list(...) - check_pkeys_fields(tbl, fields) - check_notnull_fields(tbl, fields) + check_pkeys_fields(con, tbl, fields) + check_notnull_fields(con, tbl, fields) - columns <- glue::glue_collapse(names(fields), ", ") - values <- glue::glue_collapse(fields, ", ") - - ddl <- glue::glue("INSERT INTO {tbl} ({columns}) VALUES ({glue::single_quote(values)});") - con <- init_con() + ddl <- glue::glue_sql("INSERT INTO { tbl } ({ names(fields)* }) VALUES ({ fields* });", .con = con) res <- DBI::dbSendStatement(con, ddl) if(DBI::dbHasCompleted(res)){ - cli::cli_alert_info("{ DBI::dbGetRowsAffected(res) } row inserted in {tbl}") + cli::cli_alert_info("{ DBI::dbGetRowsAffected(res) } row inserted in { tbl }") } on.exit(DBI::dbClearResult(res)) - on.exit(DBI::dbDisconnect(con), add=TRUE, after = TRUE) } -modify_entry_tbl <- function(tbl, ...){ +#' @describeIn add_entry_tbl Modify entry within specified table. +#' @export +#' +modify_entry_tbl <- function(con = NULL, tbl = NULL, ...){ fields <- list(...) - check_pkeys_fields(tbl, fields) + check_pkeys_fields(con, tbl, fields) - pkeys_tbl <- get_tbl_fields_pkey(tbl) - target_row <- do.call("search_tbl", list(tbl = tbl) |> append(fields[pkeys_tbl])) + pkeys_tbl <- get_tbl_fields_pkey(con, tbl) + target_row <- do.call("search_tbl", list(con = con, tbl = tbl) |> append(fields[pkeys_tbl])) if(nrow(target_row) > 1L){ cli::cli_abort("Error: More than one row found with { fields[pkeys_tbl] }") @@ -47,19 +42,18 @@ modify_entry_tbl <- function(tbl, ...){ update_entries <- purrr::map(names(update_values), \(n){ glue::glue("{n} = ${n}") - }) |> glue::glue_collapse(",") + }) |> glue::glue_sql_collapse(",") criterias <- purrr::map(names(pkeys_values), \(n){ glue::glue("{n} = ${n}") - }) |> glue::glue_collapse(" AND ") + }) |> glue::glue_sql_collapse(" AND ") - ddl <- glue::glue(" - UPDATE {tbl} + ddl <- glue::glue_sql(" + UPDATE { tbl } SET { update_entries } WHERE { criterias }; - ") + ", .con = con) - con <- init_con() res <- DBI::dbSendStatement(con, ddl) DBI::dbBind(res, fields) @@ -68,31 +62,32 @@ modify_entry_tbl <- function(tbl, ...){ } on.exit(DBI::dbClearResult(res)) - on.exit(DBI::dbDisconnect(con), add=TRUE, after = TRUE) } } -delete_entry_tbl <- function(tbl, ...){ +#' @describeIn add_entry_tbl Delete entry within specified table. +#' @export +#' +delete_entry_tbl <- function(con = NULL, tbl = NULL, ...){ fields <- list(...) - check_pkeys_fields(tbl, fields) - pkeys_tbl <- get_tbl_fields_pkey(tbl) - target_row <- do.call("search_tbl", list(tbl = tbl) |> append(fields[pkeys_tbl])) + check_pkeys_fields(con, tbl, fields) + pkeys_tbl <- get_tbl_fields_pkey(con, tbl) + target_row <- do.call("search_tbl", list(con = con, tbl = tbl) |> append(fields[pkeys_tbl])) if(nrow(target_row) > 1L){ cli::cli_abort("Error: More than one row found with { fields[pkeys_tbl] }") } else { criterias <- purrr::map(names(fields[pkeys_tbl]), \(n){ glue::glue("{n} = ${n}") - }) |> glue::glue_collapse(" AND ") + }) |> glue::glue_sql_collapse(" AND ") - ddl <- glue::glue(" + ddl <- glue::glue_sql(" DELETE - FROM {tbl} + FROM { tbl } WHERE { criterias }; - ") + ", .con = con) - con <- init_con() res <- DBI::dbSendStatement(con, ddl) DBI::dbBind(res, fields) @@ -101,12 +96,9 @@ delete_entry_tbl <- function(tbl, ...){ } on.exit(DBI::dbClearResult(res)) - on.exit(DBI::dbDisconnect(con), add=TRUE, after = TRUE) } } -get_tbl <- function(tbl) { - con <- init_con() +get_tbl <- function(con = NULL, tbl = NULL) { DBI::dbReadTable(con, tbl) - on.exit(DBI::dbDisconnect(con)) } diff --git a/man/add_entry_tbl.Rd b/man/add_entry_tbl.Rd new file mode 100644 index 0000000..4857e82 --- /dev/null +++ b/man/add_entry_tbl.Rd @@ -0,0 +1,31 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/db_tbl.R +\name{add_entry_tbl} +\alias{add_entry_tbl} +\alias{modify_entry_tbl} +\alias{delete_entry_tbl} +\title{Add entry within specified table} +\usage{ +add_entry_tbl(con = NULL, tbl = NULL, ...) + +modify_entry_tbl(con = NULL, tbl = NULL, ...) + +delete_entry_tbl(con = NULL, tbl = NULL, ...) +} +\arguments{ +\item{con}{connexion object returned by DBI::dbConnect()} + +\item{tbl}{a character name of the table} + +\item{...}{a vector of column names in the specified table} +} +\description{ +Add entry within specified table +} +\section{Functions}{ +\itemize{ +\item \code{modify_entry_tbl()}: Modify entry within specified table. + +\item \code{delete_entry_tbl()}: Delete entry within specified table. + +}} diff --git a/man/check_notnull_fields.Rd b/man/check_notnull_fields.Rd index b9560d6..eacc904 100644 --- a/man/check_notnull_fields.Rd +++ b/man/check_notnull_fields.Rd @@ -4,9 +4,11 @@ \alias{check_notnull_fields} \title{Check if not null fields present} \usage{ -check_notnull_fields(tbl, fields) +check_notnull_fields(con, tbl, fields) } \arguments{ +\item{con}{connexion object returned by DBI::dbConnect()} + \item{tbl}{a character name of the table} \item{fields}{a vector of column names in the specified table} @@ -19,7 +21,7 @@ Check if not null fields present } \examples{ \dontrun{ - check_notnull_fields("species", fields = c("species_id")) + check_notnull_fields(con, "species", fields = c("species_id")) } } diff --git a/man/check_pkeys_fields.Rd b/man/check_pkeys_fields.Rd index e0a66bd..7375b95 100644 --- a/man/check_pkeys_fields.Rd +++ b/man/check_pkeys_fields.Rd @@ -4,9 +4,11 @@ \alias{check_pkeys_fields} \title{Check if pkeys presents} \usage{ -check_pkeys_fields(tbl, fields) +check_pkeys_fields(con, tbl, fields) } \arguments{ +\item{con}{connexion object returned by DBI::dbConnect()} + \item{tbl}{a character name of the table} \item{fields}{a vector of column names in the specified table} @@ -19,6 +21,6 @@ Check if pkeys presents } \examples{ \dontrun{ - check_pkeys_fields("species", fields = c("species_id")) + check_pkeys_fields(con, "species", fields = c("species_id")) } } diff --git a/man/get_column_elements.Rd b/man/get_column_elements.Rd deleted file mode 100644 index 328dbe7..0000000 --- a/man/get_column_elements.Rd +++ /dev/null @@ -1,21 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/db_tbl.R -\name{add_entry_tbl} -\alias{add_entry_tbl} -\title{DB getters} -\usage{ -add_entry_tbl(tbl, ...) -} -\arguments{ -\item{tbl}{a character name of the table} - -\item{...}{a vector of column names in the specified table} -} -\description{ -Retrieve specific information from our admin database. -} -\section{Functions}{ -\itemize{ -\item \code{add_entry_tbl()}: Get species name list. - -}} diff --git a/man/get_tbl_fields_notnull.Rd b/man/get_tbl_fields_notnull.Rd index 66df0e0..b7707dd 100644 --- a/man/get_tbl_fields_notnull.Rd +++ b/man/get_tbl_fields_notnull.Rd @@ -4,9 +4,11 @@ \alias{get_tbl_fields_notnull} \title{Get table not null fields} \usage{ -get_tbl_fields_notnull(tbl) +get_tbl_fields_notnull(con, tbl) } \arguments{ +\item{con}{connexion object returned by DBI::dbConnect()} + \item{tbl}{a character name of the table} } \value{ diff --git a/man/get_tbl_fields_pkey.Rd b/man/get_tbl_fields_pkey.Rd index a08b5ff..ba5356f 100644 --- a/man/get_tbl_fields_pkey.Rd +++ b/man/get_tbl_fields_pkey.Rd @@ -4,9 +4,11 @@ \alias{get_tbl_fields_pkey} \title{Get table primary key fields} \usage{ -get_tbl_fields_pkey(tbl) +get_tbl_fields_pkey(con, tbl) } \arguments{ +\item{con}{connexion object returned by DBI::dbConnect()} + \item{tbl}{a character name of the table} } \value{ diff --git a/man/get_tbl_info.Rd b/man/get_tbl_info.Rd index 33ce33e..cbf4c22 100644 --- a/man/get_tbl_info.Rd +++ b/man/get_tbl_info.Rd @@ -4,9 +4,11 @@ \alias{get_tbl_info} \title{Get table fields properties (fkey, pkey, unique constraints etc.)} \usage{ -get_tbl_info(tbl) +get_tbl_info(con = NULL, tbl = NULL) } \arguments{ +\item{con}{connexion object returned by DBI::dbConnect()} + \item{tbl}{a character name of the table} } \value{ diff --git a/man/search_tbl.Rd b/man/search_tbl.Rd index 2fd4e21..d8a14ef 100644 --- a/man/search_tbl.Rd +++ b/man/search_tbl.Rd @@ -4,9 +4,11 @@ \alias{search_tbl} \title{search_tbl} \usage{ -search_tbl(tbl, ...) +search_tbl(con = NULL, tbl = NULL, ...) } \arguments{ +\item{con}{connexion object returned by DBI::dbConnect()} + \item{tbl}{a character name of the table} \item{...}{table fields with search value} @@ -19,6 +21,6 @@ search_tbl } \examples{ \dontrun{ - search_tbl("species", genus = "\%Poo\%") + search_tbl(con, "species", genus = "\%Poo\%") } } diff --git a/tests/testthat/helper.R b/tests/testthat/helper.R new file mode 100644 index 0000000..cfc07e9 --- /dev/null +++ b/tests/testthat/helper.R @@ -0,0 +1,21 @@ + +test_db <- function(mockData = FALSE, env = parent.frame()) { + con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:") + + DBI::dbExecute(con, "DROP TABLE IF EXISTS 'species';") + DBI::dbExecute(con, "CREATE TABLE 'species' ( + species_id TEXT, + genus TEXT, + species TEXT NOT NULL, + PRIMARY KEY (species_id) + );") + + if(mockData){ + DBI::dbExecute(con, "INSERT INTO 'species' (species_id, genus, species) VALUES ('TSN', 'Lupus', 'Lupus lupus');") + DBI::dbExecute(con, "INSERT INTO 'species' (species_id, genus, species) VALUES ('TSN2', 'Alces', 'Alces alces');") + DBI::dbExecute(con, "INSERT INTO 'species' (species_id, genus, species) VALUES ('TSN3', 'Castor', 'Castor canadensis');") + } + + withr::defer(DBI::dbDisconnect(con), env) + return(con) +} diff --git a/tests/testthat/test-db_search.R b/tests/testthat/test-db_search.R new file mode 100644 index 0000000..fc2788f --- /dev/null +++ b/tests/testthat/test-db_search.R @@ -0,0 +1,33 @@ +context("Search entries in table") + +test_that("search_tbl() success", { + + con <- test_db(mockData = TRUE) + + testthat::expect_equal( + search_tbl( + con = con, + tbl = "species", + species_id = c("TSN", "TSN2") + )$species_id, c("TSN", "TSN2") + ) + + testthat::expect_equal( + search_tbl( + con = con, + tbl = "species", + species_id = "%TS%" + )$species_id, c("TSN", "TSN2", "TSN3") + ) + + testthat::expect_equal( + search_tbl( + con = con, + tbl = "species", + genus = "Alces", + genus = "Castor" + )$species_id, c("TSN2", "TSN3") + ) + + withr::deferred_run() +}) diff --git a/tests/testthat/test-db_tbl.R b/tests/testthat/test-db_tbl.R new file mode 100644 index 0000000..6284cc0 --- /dev/null +++ b/tests/testthat/test-db_tbl.R @@ -0,0 +1,83 @@ +context("Add, delete, update entries in table") + +test_that("add_entry_tbl() success", { + + con <- test_db() + add_entry_tbl( + con = con, + tbl = "species", + species_id = "TSN", + genus = "Lupus", + species = "Lupus lupus" + ) + + rs <- DBI::dbGetQuery(con,"SELECT * FROM species;") + testthat::expect_true(nrow(rs) == 1L) + testthat::expect_true(rs$species_id == "TSN") + testthat::expect_true(rs$genus == "Lupus") + testthat::expect_true(rs$species == "Lupus lupus") + + withr::deferred_run() +}) + + +test_that("add_entry_tbl() with missing pkeys", { + con <- test_db() + + testthat::expect_error( + add_entry_tbl( + con = con, + tbl = "species", + genus = "Lupus", + species = "Lupus lupus" + ), "Primary key(s) species_id is/are missing", fixed = TRUE) + + withr::deferred_run() +}) + +test_that("add_entry_tbl() with not null contraint ", { + con <- test_db() + + testthat::expect_error( + add_entry_tbl( + con = con, + tbl = "species", + species_id = "TSN", + genus = "Lupus" + ), "species cannot be null(s)", fixed = TRUE) + + withr::deferred_run() +}) + + +test_that("delete_entry_tbl() success", { + con <- test_db(mockData = TRUE) + + delete_entry_tbl( + con = con, + tbl = "species", + species_id = "TSN" + ) + + rs <- DBI::dbGetQuery(con,"SELECT * FROM species WHERE species_id = 'TSN';") + testthat::expect_true(nrow(rs) == 0L) + + withr::deferred_run() +}) + +test_that("modify_entry_tbl() success", { + con <- test_db(mockData = TRUE) + + modify_entry_tbl( + con = con, + tbl = "species", + species_id = "TSN", + genus = "Hydra" + ) + + new_genus <- DBI::dbGetQuery(con,"SELECT genus FROM species WHERE species_id = 'TSN';")$genus + testthat::expect_true(new_genus == "Hydra") + + withr::deferred_run() +}) +