From 322521c9a971c26b8d440ce3dab087fc93a684b5 Mon Sep 17 00:00:00 2001 From: Brancen Gregory Date: Thu, 14 Aug 2025 18:10:19 -0500 Subject: [PATCH 01/11] Auth (#191) * Overhaul ojo_auth * Update wordlist * Add db_config to pkgdown index * Move ssl_mode argument before other ssl arguments --- DESCRIPTION | 3 +- NAMESPACE | 1 + R/ojo_auth.R | 438 +++++++++++++++++++++++---------- _pkgdown.yml | 1 + inst/WORDLIST | 2 + inst/example_config.yaml | 23 ++ man/db_config.Rd | 77 ++++++ man/ojo_auth.Rd | 80 +++--- tests/testthat/test-ojo_auth.R | 147 +++++++++++ 9 files changed, 611 insertions(+), 161 deletions(-) create mode 100644 inst/example_config.yaml create mode 100644 man/db_config.Rd create mode 100644 tests/testthat/test-ojo_auth.R diff --git a/DESCRIPTION b/DESCRIPTION index 86ca4d03..2d181377 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -49,7 +49,8 @@ Imports: tibble, tidyr, utils, - withr + withr, + yaml Suggests: arrow, attachment, diff --git a/NAMESPACE b/NAMESPACE index 680249b0..60d780bb 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,6 @@ # Generated by roxygen2: do not edit by hand +export(db_config) export(ojo_add_counts) export(ojo_add_issues) export(ojo_add_minutes) diff --git a/R/ojo_auth.R b/R/ojo_auth.R index 8ac78b0e..98f4ed43 100644 --- a/R/ojo_auth.R +++ b/R/ojo_auth.R @@ -1,152 +1,340 @@ -#' @title Create configuration for OJO database connection +#' @title Create database configuration object #' -#' @description Configure credentials for the Open Justice Oklahoma database +#' @description Creates a configuration object for OJO database connections with flexible configuration sources. +#' +#' @param host Database host name +#' @param port Database port number +#' @param username Database username +#' @param password Database password +#' @param ssl_mode SSL connection mode +#' @param ssl_root_cert Path to SSL root certificate file +#' @param ssl_cert Path to SSL client certificate file +#' @param ssl_key Path to SSL private key file +#' @param config_file Path to YAML configuration file #' #' @details -#' Assists the user in populating a .Renviron file with the necessary environment variables to connect to the Open Justice Oklahoma database. +#' This function creates a database configuration object by reading from multiple sources +#' with the following precedence (highest to lowest): +#' 1. Function arguments +#' 2. YAML configuration file +#' 3. Environment variables (.Renviron and system) +#' +#' @return A db_config object (list) containing database connection parameters +#' @export +#' +#' @examples +#' \dontrun{ +#' # Create config with explicit parameters +#' config <- db_config( +#' host = "localhost", +#' port = "5432", +#' username = "user", +#' password = "pass", +#' ssl_mode = "require" +#' ) #' -#' @param host The host name of the database server -#' @param port The port number of the database server -#' @param username The username to use to connect to the database -#' @param password The password to use to connect to the database -#' @param ... Placeholder for additional arguments -#' @param .admin A logical value indicating whether to connect to the database as an administrator -#' @param .overwrite A logical value indicating whether to overwrite the existing .Renviron file -#' @param .install A logical value indicating whether to install the database connection or use it only for the current session +#' # For local testing where SSL is not configured +#' local_config <- db_config( +#' host = "localhost", +#' port = "5432", +#' username = "postgres", +#' password = "password", +#' ssl_mode = "disable" +#' ) #' +#' # Create config from YAML file +#' config <- db_config(config_file = "~/.ojo_config.yaml") +#' +#' } +db_config <- function( + host = NULL, + port = NULL, + username = NULL, + password = NULL, + ssl_mode = NULL, + ssl_root_cert = NULL, + ssl_cert = NULL, + ssl_key = NULL, + config_file = NULL +) { + # Initialize default configuration + config <- list( + host = NULL, + port = NULL, + username = NULL, + password = NULL, + ssl_mode = NULL, + ssl_root_cert = NULL, + ssl_cert = NULL, + ssl_key = NULL + ) + + # Read from environment variables first + if (Sys.getenv("OJO_HOST") != "") { + config$host <- Sys.getenv("OJO_HOST") + } + + if (Sys.getenv("OJO_PORT") != "") { + config$port <- Sys.getenv("OJO_PORT") + } + + if (Sys.getenv("OJO_USER") != "") { + config$username <- Sys.getenv("OJO_USER") + } + + if (Sys.getenv("OJO_PASS") != "") { + config$password <- Sys.getenv("OJO_PASS") + } + + if (Sys.getenv("OJO_SSL_MODE") != "") { + config$ssl_mode <- Sys.getenv("OJO_SSL_MODE") + } + + if (Sys.getenv("OJO_SSL_ROOT_CERT") != "") { + config$ssl_root_cert <- Sys.getenv("OJO_SSL_ROOT_CERT") + } + + if (Sys.getenv("OJO_SSL_CERT") != "") { + config$ssl_cert <- Sys.getenv("OJO_SSL_CERT") + } + + if (Sys.getenv("OJO_SSL_KEY") != "") { + config$ssl_key <- Sys.getenv("OJO_SSL_KEY") + } + + # Override with config file if provided + if (!is.null(config_file) && file.exists(config_file)) { + file_config <- yaml::read_yaml(config_file) + # Support both flat and nested config structures + if (!is.null(file_config$ojodb)) { + file_config <- file_config$ojodb + } + + for (name in names(config)) { + if (!is.null(file_config[[name]])) { + config[[name]] <- file_config[[name]] + } + } + } + + # Override with function arguments (highest precedence) + if (!is.null(host)) { + config$host <- host + } + + if (!is.null(port)) { + config$port <- port + } + + if (!is.null(username)) { + config$username <- username + } + + if (!is.null(password)) { + config$password <- password + } + + if (!is.null(ssl_mode)) { + config$ssl_mode <- ssl_mode + } + + if (!is.null(ssl_root_cert)) { + config$ssl_root_cert <- ssl_root_cert + } + + if (!is.null(ssl_cert)) { + config$ssl_cert <- ssl_cert + } + + if (!is.null(ssl_key)) { + config$ssl_key <- ssl_key + } + + # Add class for method dispatch + class(config) <- c("db_config", "list") + config +} + +#' @title Configure OJO database authentication +#' +#' @description Sets up authentication for the Open Justice Oklahoma database using a db_config object. +#' +#' @param ... Placeholder for future arguments +#' @param db_config A db_config object created by `db_config()` +#' @param .install Logical indicating whether to save configuration to .Renviron (TRUE) or use for current session only (FALSE) +#' @param .overwrite Logical indicating whether to overwrite existing .Renviron entries +#' +#' @details +#' This function takes a db_config object and sets up the database authentication. +#' When .install = FALSE (default), credentials are only set for the current R session. +#' When .install = TRUE, credentials are saved to .Renviron for persistent use. +#' +#' @return Invisible NULL #' @export -#' @returns Nothing #' #' @examples #' \dontrun{ -#' ojo_auth() +#' # Create config and authenticate for a remote server +#' config <- db_config( +#' host = "remote.db.com", +#' port = "5432", +#' username = "user", +#' password = "pass", +#' ssl_mode = "require" +#' ) +#' ojo_auth(db_config = config) +#' +#' # One-liner with explicit parameters +#' ojo_auth(db_config = db_config( +#' host = "remote.db.com", +#' port = "5432", +#' username = "user", +#' password = "pass", +#' ssl_mode = "require" +#' )) +#' +#' # From config file +#' ojo_auth(db_config = db_config(config_file = "~/.ojo_config.yaml")) +#' +#' # Permanent configuration +#' ojo_auth( +#' db_config = db_config( +#' host = "remote.db.com", +#' port = "5432", +#' username = "user", +#' password = "pass", +#' ssl_mode = "require" +#' ), +#' .install = TRUE +#' ) #' } -#' @section Side Effects: -#' The first time this function is run, it will prompt the user for a username, password, and host name. -#' It will then store these credentials in the user's .Renviron file. -#' If the .Renviron file already exists, it will be backed up and the new credentials will be appended to the end of the file. -#' If the .Renviron file does not exist, it will be created and the credentials will be stored there. -#' -ojo_auth <- function(host, port, username, password, ..., .admin = F, .overwrite = T, .install = T) { - home <- Sys.getenv("HOME") - renv <- fs::path(home, ".Renviron") - rootcert <- fs::path(home, ".postgresql/ojodb/server-ca.pem") - clientcert <- fs::path(home, ".postgresql/ojodb/client-cert.pem") - clientkey <- fs::path(home, ".postgresql/ojodb/client-key.pem") - - # Check if SSL certs are in correct location; if not, throw error - if (!fs::file_exists(rootcert) | - !fs::file_exists(clientcert) | - !fs::file_exists(clientkey)) { - rlang::abort( - paste0( - "It looks like your SSL certs are not in the correct location (", - fs::path(home, ".postgresql/ojodb/..."), - ").\nPlease check that you have all three (server-ca.pem, client-cert.pem, and client-key.pem)." - ) +#' +ojo_auth <- function(..., db_config, .install = FALSE, .overwrite = FALSE) { + # Validate input + if (!inherits(db_config, "db_config")) { + stop( + "db_config must be a db_config object created by db_config()", + call. = FALSE ) } - if (.install) { + # Check required parameters + required <- c("host", "port", "username", "password", "ssl_mode") + missing <- required[sapply(required, function(x) is.null(db_config[[x]]))] + if (length(missing) > 0) { + stop( + "Missing required configuration: ", + paste(missing, collapse = ", "), + call. = FALSE + ) + } + + # Validate SSL certificates exist if using verify modes + if (!is.null(db_config$ssl_mode) && db_config$ssl_mode %in% c("verify-ca", "verify-full")) { + missing_certs <- c() + ssl_files <- c("ssl_root_cert", "ssl_cert", "ssl_key") + for (cert_type in ssl_files) { + cert_path <- db_config[[cert_type]] + if (!is.null(cert_path) && !file.exists(cert_path)) { + missing_certs <- c( + missing_certs, + paste0(cert_type, " (", cert_path, ")") + ) + } + } - # Check if .Renviron exists. If it does, make a backup... - if (fs::file_exists(renv)) { - # Backup original .Renviron before doing anything else here. - fs::file_copy(renv, fs::path(home, ".Renviron_backup"), - overwrite = TRUE) + if (length(missing_certs) > 0) { + warning( + "SSL certificate files not found:\n", + paste(missing_certs, collapse = "\n"), + "\nConnection may fail. Consider using ssl_mode = 'require' to try a secure connection without certs or, more likely, provide valid certificate paths.", + call. = FALSE + ) } + } - # ...if not, create a fresh one. - if (!fs::file_exists(renv)) { - fs::file_create(renv) - # Filling out the .Renviron file + if (.install) { + home <- path.expand("~") + renv <- file.path(home, ".Renviron") + + # Backup existing .Renviron + if (file.exists(renv)) { + file.copy(renv, file.path(home, ".Renviron_backup"), overwrite = TRUE) } else { + file.create(renv) + } - # If we want to overwrite the old config: - if (isTRUE(.overwrite)) { - cli::cli_alert_info("Your original .Renviron will be backed up and stored in your R HOME directory if needed.") - - # Saving the original .Renviron file - oldenv <- utils::read.table(renv, stringsAsFactors = FALSE) - # Creating the new .Renviron file (not filled out yet, all OJO variables removed) - newenv <- oldenv |> - dplyr::as_tibble() |> - dplyr::filter(!stringi::stri_detect_regex(.data$V1, "(OJO_HOST)|(OJO_PORT)|(OJO_DRIVER)|(OJO_SSL)")) - if (.admin == T) { - newenv <- newenv |> - dplyr::filter(!stringi::stri_detect_regex(.data$V1, "(OJO_ADMIN_USER)|(OJO_ADMIN_PASS)")) |> - as.data.frame() - } else { - newenv <- newenv |> - dplyr::filter(!stringi::stri_detect_regex(.data$V1, "(OJO_DEFAULT_USER)|(OJO_DEFAULT_PASS)")) |> - as.data.frame() - } - - # Save new .Renviron file with OJO variables removed - utils::write.table(newenv, renv, - quote = FALSE, sep = "\n", - col.names = FALSE, row.names = FALSE - ) + # Clean existing OJO variables if overwriting + if (.overwrite && file.exists(renv)) { + lines <- readLines(renv) + # Remove all OJO-related lines + clean_lines <- lines[!grepl("^OJO_", lines)] - # If a config already exists, and we don't want to overwrite it: - } else { - tv <- readLines(renv) - if (.admin) { - if (any(grepl("OJO_ADMIN_USER", tv))) { - stop("An OJO_ADMIN_USER already exists. You can overwrite it with the argument `.overwrite = TRUE`", call. = F) - } - } else { - if (any(grepl("OJO_DEFAULT_USER", tv))) { - stop("An OJO_DEFAULT_USER already exists. You can overwrite it with the argument `.overwrite = TRUE`", call. = FALSE) - } - } + # Write new configuration + new_vars <- c( + clean_lines, + paste0("OJO_HOST='", db_config$host, "'"), + paste0("OJO_PORT='", db_config$port, "'"), + paste0("OJO_USER='", db_config$username, "'"), + paste0("OJO_PASS='", db_config$password, "'"), + paste0("OJO_SSL_MODE='", db_config$ssl_mode, "'"), + paste0("OJO_SSL_ROOT_CERT='", db_config$ssl_root_cert, "'"), + paste0("OJO_SSL_CERT='", db_config$ssl_cert, "'"), + paste0("OJO_SSL_KEY='", db_config$ssl_key, "'") + ) + writeLines(new_vars, renv) + } else if (!.overwrite && file.exists(renv)) { + # Check for conflicts + lines <- readLines(renv) + if (any(grepl("OJO_USER", lines))) { + stop( + "Configuration already exists. Use .overwrite = TRUE to replace it.", + call. = FALSE + ) } - } - # Fill out .Renviron with new arguments - hostconcat <- paste0("OJO_HOST='", host, "'") - portconcat <- paste0("OJO_PORT='", port, "'") - if (.admin) { - userconcat <- paste0("OJO_ADMIN_USER='", username, "'") - passconcat <- paste0("OJO_ADMIN_PASS='", password, "'") + # Append new configuration + new_vars <- c( + paste0("OJO_HOST='", db_config$host, "'"), + paste0("OJO_PORT='", db_config$port, "'"), + paste0("OJO_USER='", db_config$username, "'"), + paste0("OJO_PASS='", db_config$password, "'"), + paste0("OJO_SSL_MODE='", db_config$ssl_mode, "'"), + paste0("OJO_SSL_ROOT_CERT='", db_config$ssl_root_cert, "'"), + paste0("OJO_SSL_CERT='", db_config$ssl_cert, "'"), + paste0("OJO_SSL_KEY='", db_config$ssl_key, "'") + ) + cat(new_vars, file = renv, sep = "\n", append = TRUE) } else { - userconcat <- paste0("OJO_DEFAULT_USER='", username, "'") - passconcat <- paste0("OJO_DEFAULT_PASS='", password, "'") + # New .Renviron file + new_vars <- c( + paste0("OJO_HOST='", db_config$host, "'"), + paste0("OJO_PORT='", db_config$port, "'"), + paste0("OJO_USER='", db_config$username, "'"), + paste0("OJO_PASS='", db_config$password, "'"), + paste0("OJO_SSL_MODE='", db_config$ssl_mode, "'"), + paste0("OJO_SSL_ROOT_CERT='", db_config$ssl_root_cert, "'"), + paste0("OJO_SSL_CERT='", db_config$ssl_cert, "'"), + paste0("OJO_SSL_KEY='", db_config$ssl_key, "'") + ) + writeLines(new_vars, renv) } - sslmodeconcat <- paste0("OJO_SSL_MODE='verify-ca'") - rootcertconcat <- paste0("OJO_SSL_ROOT_CERT='", rootcert, "'") - clientcertconcat <- paste0("OJO_SSL_CERT='", clientcert, "'") - clientkeyconcat <- paste0("OJO_SSL_KEY='", clientkey, "'") - write(hostconcat, renv, sep = "\n", append = TRUE) - write(portconcat, renv, sep = "\n", append = TRUE) - write(userconcat, renv, sep = "\n", append = TRUE) - write(passconcat, renv, sep = "\n", append = TRUE) - write(sslmodeconcat, renv, sep = "\n", append = TRUE) - write(rootcertconcat, renv, sep = "\n", append = TRUE) - write(clientcertconcat, renv, sep = "\n", append = TRUE) - write(clientkeyconcat, renv, sep = "\n", append = TRUE) - cli::cli_alert_success('Your configuration has been stored in your .Renviron. - To use now, restart R or run `readRenviron("~/.Renviron")`') - invisible() - - # If .install = FALSE... + cat("Configuration saved to .Renviron\n") + cat("To use now, restart R or run readRenviron('~/.Renviron')\n") } else { - cli::cli_alert_info("To install your configuration for use in future sessions, run this function with `.install = TRUE`.") - - # ...set up local environment, but don't save to .Renviron - Sys.setenv(OJO_HOST = host) - Sys.setenv(OJO_PORT = port) - if (.admin == T) { - Sys.setenv(OJO_ADMIN_USER = username) - Sys.setenv(OJO_ADMIN_PASS = password) - } else { - Sys.setenv(OJO_DEFAULT_USER = username) - Sys.setenv(OJO_DEFAULT_PASS = password) - } - Sys.setenv(OJO_SSL_MODE = "verify-ca") - Sys.setenv(OJO_SSL_ROOT_CERT = rootcert) - Sys.setenv(OJO_SSL_CERT = clientcert) - Sys.setenv(OJO_SSL_KEY = clientkey) + # Set environment variables for current session only + Sys.setenv(OJO_HOST = db_config$host) + Sys.setenv(OJO_PORT = db_config$port) + Sys.setenv(OJO_USER = db_config$username) + Sys.setenv(OJO_PASS = db_config$password) + Sys.setenv(OJO_SSL_MODE = db_config$ssl_mode) + Sys.setenv(OJO_SSL_ROOT_CERT = db_config$ssl_root_cert) + Sys.setenv(OJO_SSL_CERT = db_config$ssl_cert) + Sys.setenv(OJO_SSL_KEY = db_config$ssl_key) + + cat("Configuration set for current session only\n") + cat("To persist configuration, run with .install = TRUE\n") } + invisible() } diff --git a/_pkgdown.yml b/_pkgdown.yml index c6f16fce..2182316f 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -40,6 +40,7 @@ reference: - ojo_query - ojo_check_ssl - ojo_auth + - db_config - ojo_env - ojo_version diff --git a/inst/WORDLIST b/inst/WORDLIST index 5537deab..f278daa8 100644 --- a/inst/WORDLIST +++ b/inst/WORDLIST @@ -16,9 +16,11 @@ Renviron SSL Stackoverflow Tibble +YAML auth casenum civ +config crim dbConnect dbPool diff --git a/inst/example_config.yaml b/inst/example_config.yaml new file mode 100644 index 00000000..f29055b4 --- /dev/null +++ b/inst/example_config.yaml @@ -0,0 +1,23 @@ +# Example OJO database configuration file +# Save as ~/.ojo_config.yaml or any path and use with db_config(config_file = "path") + +host: "your-database-host.com" +port: "5432" +username: "your_username" +password: "your_password" +ssl_mode: "verify-ca" +ssl_root_cert: "/path/to/server-ca.pem" +ssl_cert: "/path/to/client-cert.pem" +ssl_key: "/path/to/client-key.pem" + +# Alternative nested format also supported: +# ojodb: +# host: "your-database-host.com" +# port: "5432" +# username: "your_username" +# password: "your_password" +# ssl_mode: "verify-ca" +# ssl_root_cert: "/path/to/server-ca.pem" +# ssl_cert: "/path/to/client-cert.pem" +# ssl_key: "/path/to/client-key.pem" + diff --git a/man/db_config.Rd b/man/db_config.Rd new file mode 100644 index 00000000..b46f2fd9 --- /dev/null +++ b/man/db_config.Rd @@ -0,0 +1,77 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ojo_auth.R +\name{db_config} +\alias{db_config} +\title{Create database configuration object} +\usage{ +db_config( + host = NULL, + port = NULL, + username = NULL, + password = NULL, + ssl_mode = NULL, + ssl_root_cert = NULL, + ssl_cert = NULL, + ssl_key = NULL, + config_file = NULL +) +} +\arguments{ +\item{host}{Database host name} + +\item{port}{Database port number} + +\item{username}{Database username} + +\item{password}{Database password} + +\item{ssl_mode}{SSL connection mode} + +\item{ssl_root_cert}{Path to SSL root certificate file} + +\item{ssl_cert}{Path to SSL client certificate file} + +\item{ssl_key}{Path to SSL private key file} + +\item{config_file}{Path to YAML configuration file} +} +\value{ +A db_config object (list) containing database connection parameters +} +\description{ +Creates a configuration object for OJO database connections with flexible configuration sources. +} +\details{ +This function creates a database configuration object by reading from multiple sources +with the following precedence (highest to lowest): +\enumerate{ +\item Function arguments +\item YAML configuration file +\item Environment variables (.Renviron and system) +} +} +\examples{ +\dontrun{ +# Create config with explicit parameters +config <- db_config( + host = "localhost", + port = "5432", + username = "user", + password = "pass", + ssl_mode = "require" +) + +# For local testing where SSL is not configured +local_config <- db_config( + host = "localhost", + port = "5432", + username = "postgres", + password = "password", + ssl_mode = "disable" +) + +# Create config from YAML file +config <- db_config(config_file = "~/.ojo_config.yaml") + +} +} diff --git a/man/ojo_auth.Rd b/man/ojo_auth.Rd index 3adbc7c7..94dec1c9 100644 --- a/man/ojo_auth.Rd +++ b/man/ojo_auth.Rd @@ -2,55 +2,65 @@ % Please edit documentation in R/ojo_auth.R \name{ojo_auth} \alias{ojo_auth} -\title{Create configuration for OJO database connection} +\title{Configure OJO database authentication} \usage{ -ojo_auth( - host, - port, - username, - password, - ..., - .admin = F, - .overwrite = T, - .install = T -) +ojo_auth(..., db_config, .install = FALSE, .overwrite = FALSE) } \arguments{ -\item{host}{The host name of the database server} - -\item{port}{The port number of the database server} - -\item{username}{The username to use to connect to the database} +\item{...}{Placeholder for future arguments} -\item{password}{The password to use to connect to the database} +\item{db_config}{A db_config object created by \code{db_config()}} -\item{...}{Placeholder for additional arguments} +\item{.install}{Logical indicating whether to save configuration to .Renviron (TRUE) or use for current session only (FALSE)} -\item{.admin}{A logical value indicating whether to connect to the database as an administrator} - -\item{.overwrite}{A logical value indicating whether to overwrite the existing .Renviron file} - -\item{.install}{A logical value indicating whether to install the database connection or use it only for the current session} +\item{.overwrite}{Logical indicating whether to overwrite existing .Renviron entries} } \value{ -Nothing +Invisible NULL } \description{ -Configure credentials for the Open Justice Oklahoma database +Sets up authentication for the Open Justice Oklahoma database using a db_config object. } \details{ -Assists the user in populating a .Renviron file with the necessary environment variables to connect to the Open Justice Oklahoma database. -} -\section{Side Effects}{ - -The first time this function is run, it will prompt the user for a username, password, and host name. -It will then store these credentials in the user's .Renviron file. -If the .Renviron file already exists, it will be backed up and the new credentials will be appended to the end of the file. -If the .Renviron file does not exist, it will be created and the credentials will be stored there. +This function takes a db_config object and sets up the database authentication. +When .install = FALSE (default), credentials are only set for the current R session. +When .install = TRUE, credentials are saved to .Renviron for persistent use. } - \examples{ \dontrun{ -ojo_auth() +# Create config and authenticate for a remote server +config <- db_config( + host = "remote.db.com", + port = "5432", + username = "user", + password = "pass", + ssl_mode = "require" +) +ojo_auth(db_config = config) + +# One-liner with explicit parameters +ojo_auth(db_config = db_config( + host = "remote.db.com", + port = "5432", + username = "user", + password = "pass", + ssl_mode = "require" +)) + +# From config file +ojo_auth(db_config = db_config(config_file = "~/.ojo_config.yaml")) + +# Permanent configuration +ojo_auth( + db_config = db_config( + host = "remote.db.com", + port = "5432", + username = "user", + password = "pass", + ssl_mode = "require" + ), + .install = TRUE +) } + } diff --git a/tests/testthat/test-ojo_auth.R b/tests/testthat/test-ojo_auth.R new file mode 100644 index 00000000..2823bde6 --- /dev/null +++ b/tests/testthat/test-ojo_auth.R @@ -0,0 +1,147 @@ +test_that("db_config creates basic configuration correctly", { + config <- db_config( + host = "localhost", + port = "5432", + username = "testuser", + password = "testpass", + ssl_mode = "require" + ) + + expect_s3_class(config, "db_config") + expect_equal(config$host, "localhost") + expect_equal(config$port, "5432") + expect_equal(config$username, "testuser") + expect_equal(config$password, "testpass") + expect_equal(config$ssl_mode, "require") +}) + +test_that("db_config reads from environment variables", { + # withr::with_envvar() sets environment variables only for the code + # inside the block, then automatically restores the previous state. + withr::with_envvar( + new = c( + OJO_HOST = "env_host", + OJO_PORT = "env_port", + OJO_USER = "env_user", + OJO_PASS = "env_pass", + OJO_SSL_MODE = "env_ssl_mode" + ), + { + config <- db_config() + + expect_equal(config$host, "env_host") + expect_equal(config$port, "env_port") + expect_equal(config$username, "env_user") + expect_equal(config$password, "env_pass") + expect_equal(config$ssl_mode, "env_ssl_mode") + } + ) +}) + +test_that("db_config function arguments override environment", { + withr::with_envvar( + new = c(OJO_HOST = "env_host", OJO_PORT = "env_port"), + { + # Function arguments should override the environment variables + config <- db_config( + host = "arg_host", + port = "arg_port", + username = "arg_user", + password = "arg_pass", + ssl_mode = "arg_ssl_mode" + ) + + expect_equal(config$host, "arg_host") + expect_equal(config$port, "arg_port") + expect_equal(config$username, "arg_user") + expect_equal(config$password, "arg_pass") + expect_equal(config$ssl_mode, "arg_ssl_mode") + } + ) +}) + +test_that("ojo_auth validates db_config input", { + expect_error( + ojo_auth(db_config = list(host = "test")), + "db_config must be a db_config object" + ) + + incomplete_config <- structure( + list(host = "test"), + class = c("db_config", "list") + ) + + expect_error( + ojo_auth(db_config = incomplete_config), + "Missing required configuration" + ) +}) + +test_that("ojo_auth works with session-only mode", { + # withr::local_envvar() will restore the environment variables to their + # original state after the test block finishes, even if it errors. + # This is ideal for testing functions that have side effects. + withr::local_envvar(.local_envir = teardown_env()) + + config <- db_config( + host = "localhost", + port = "5432", + username = "testuser", + password = "testpass", + ssl_mode = "require" + ) + + capture.output({ + ojo_auth(db_config = config, .install = FALSE) + }) + + # Verify environment variables were set correctly + expect_equal(Sys.getenv("OJO_HOST"), "localhost") + expect_equal(Sys.getenv("OJO_PORT"), "5432") + expect_equal(Sys.getenv("OJO_USER"), "testuser") + expect_equal(Sys.getenv("OJO_PASS"), "testpass") + expect_equal(Sys.getenv("OJO_SSL_MODE"), "require") +}) + +test_that("db_config reads from YAML file", { + temp_yaml <- withr::local_tempfile(fileext = ".yaml") + + yaml_content <- " +host: yaml_host +port: yaml_port +username: yaml_user +password: yaml_pass +ssl_mode: require +" + writeLines(yaml_content, temp_yaml) + + config <- db_config(config_file = temp_yaml) + + expect_equal(config$host, "yaml_host") + expect_equal(config$port, "yaml_port") + expect_equal(config$username, "yaml_user") + expect_equal(config$password, "yaml_pass") + expect_equal(config$ssl_mode, "require") +}) + +test_that("db_config handles nested YAML format", { + temp_yaml <- withr::local_tempfile(fileext = ".yaml") + + yaml_content <- " +ojodb: + host: nested_host + port: nested_port + username: nested_user + password: nested_pass + ssl_mode: nested_ssl_mode +" + writeLines(yaml_content, temp_yaml) + + config <- db_config(config_file = temp_yaml) + + expect_equal(config$host, "nested_host") + expect_equal(config$port, "nested_port") + expect_equal(config$username, "nested_user") + expect_equal(config$password, "nested_pass") + expect_equal(config$ssl_mode, "nested_ssl_mode") +}) From 7076d1d83545264f634a0856e060cb6b53d9994d Mon Sep 17 00:00:00 2001 From: Brancen Gregory Date: Thu, 14 Aug 2025 20:31:38 -0500 Subject: [PATCH 02/11] Connection arguments (#194) * Add driver and database db_config args; add function to create database connection strings * Add note for todo --- R/ojo_auth.R | 46 +++++++++++++- R/ojo_connection_string.R | 129 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 R/ojo_connection_string.R diff --git a/R/ojo_auth.R b/R/ojo_auth.R index 98f4ed43..0d9018ad 100644 --- a/R/ojo_auth.R +++ b/R/ojo_auth.R @@ -2,6 +2,9 @@ #' #' @description Creates a configuration object for OJO database connections with flexible configuration sources. #' +#' @param ... Placeholder for future args +#' @param driver Database driver name +#' @param database Database name #' @param host Database host name #' @param port Database port number #' @param username Database username @@ -26,6 +29,8 @@ #' \dontrun{ #' # Create config with explicit parameters #' config <- db_config( +#' driver = "RPostgres", +#' database = "ojodb", #' host = "localhost", #' port = "5432", #' username = "user", @@ -35,6 +40,8 @@ #' #' # For local testing where SSL is not configured #' local_config <- db_config( +#' driver = "RPostgres", +#' database = "ojodb", #' host = "localhost", #' port = "5432", #' username = "postgres", @@ -47,6 +54,9 @@ #' #' } db_config <- function( + ..., + driver = NULL, + database = NULL, host = NULL, port = NULL, username = NULL, @@ -59,6 +69,8 @@ db_config <- function( ) { # Initialize default configuration config <- list( + driver = NULL, + database = NULL, host = NULL, port = NULL, username = NULL, @@ -70,6 +82,14 @@ db_config <- function( ) # Read from environment variables first + if (Sys.getenv("OJO_DRIVER") != "") { + config$driver <- Sys.getenv("OJO_DRIVER") + } + + if (Sys.getenv("OJO_DATABASE") != "") { + config$host <- Sys.getenv("OJO_DATABASE") + } + if (Sys.getenv("OJO_HOST") != "") { config$host <- Sys.getenv("OJO_HOST") } @@ -118,6 +138,14 @@ db_config <- function( } # Override with function arguments (highest precedence) + if (!is.null(driver)) { + config$driver <- driver + } + + if (!is.null(database)) { + config$database <- database + } + if (!is.null(host)) { config$host <- host } @@ -176,6 +204,8 @@ db_config <- function( #' \dontrun{ #' # Create config and authenticate for a remote server #' config <- db_config( +#' driver = "RPostgres", +#' database = "ojodb", #' host = "remote.db.com", #' port = "5432", #' username = "user", @@ -186,6 +216,8 @@ db_config <- function( #' #' # One-liner with explicit parameters #' ojo_auth(db_config = db_config( +#' driver = "RPostgres", +#' database = "ojodb", #' host = "remote.db.com", #' port = "5432", #' username = "user", @@ -199,6 +231,8 @@ db_config <- function( #' # Permanent configuration #' ojo_auth( #' db_config = db_config( +#' driver = "RPostgres", +#' database = "ojodb", #' host = "remote.db.com", #' port = "5432", #' username = "user", @@ -219,7 +253,7 @@ ojo_auth <- function(..., db_config, .install = FALSE, .overwrite = FALSE) { } # Check required parameters - required <- c("host", "port", "username", "password", "ssl_mode") + required <- c("driver", "host", "port", "username", "password", "ssl_mode") missing <- required[sapply(required, function(x) is.null(db_config[[x]]))] if (length(missing) > 0) { stop( @@ -229,6 +263,8 @@ ojo_auth <- function(..., db_config, .install = FALSE, .overwrite = FALSE) { ) } + # TODO: Make required params depend on value of driver + # Validate SSL certificates exist if using verify modes if (!is.null(db_config$ssl_mode) && db_config$ssl_mode %in% c("verify-ca", "verify-full")) { missing_certs <- c() @@ -273,6 +309,8 @@ ojo_auth <- function(..., db_config, .install = FALSE, .overwrite = FALSE) { # Write new configuration new_vars <- c( clean_lines, + paste0("OJO_DRIVER='", db_config$driver, "'"), + paste0("OJO_DATABASE='", db_config$database, "'"), paste0("OJO_HOST='", db_config$host, "'"), paste0("OJO_PORT='", db_config$port, "'"), paste0("OJO_USER='", db_config$username, "'"), @@ -295,6 +333,8 @@ ojo_auth <- function(..., db_config, .install = FALSE, .overwrite = FALSE) { # Append new configuration new_vars <- c( + paste0("OJO_DRIVER='", db_config$driver, "'"), + paste0("OJO_DATABASE='", db_config$database, "'"), paste0("OJO_HOST='", db_config$host, "'"), paste0("OJO_PORT='", db_config$port, "'"), paste0("OJO_USER='", db_config$username, "'"), @@ -308,6 +348,8 @@ ojo_auth <- function(..., db_config, .install = FALSE, .overwrite = FALSE) { } else { # New .Renviron file new_vars <- c( + paste0("OJO_DRIVER='", db_config$driver, "'"), + paste0("OJO_DATABASE='", db_config$database, "'"), paste0("OJO_HOST='", db_config$host, "'"), paste0("OJO_PORT='", db_config$port, "'"), paste0("OJO_USER='", db_config$username, "'"), @@ -323,6 +365,8 @@ ojo_auth <- function(..., db_config, .install = FALSE, .overwrite = FALSE) { cat("To use now, restart R or run readRenviron('~/.Renviron')\n") } else { # Set environment variables for current session only + Sys.setenv(OJO_DRIVER = db_config$driver) + Sys.setenv(OJO_DATABASE = db_config$database) Sys.setenv(OJO_HOST = db_config$host) Sys.setenv(OJO_PORT = db_config$port) Sys.setenv(OJO_USER = db_config$username) diff --git a/R/ojo_connection_string.R b/R/ojo_connection_string.R new file mode 100644 index 00000000..38ac4927 --- /dev/null +++ b/R/ojo_connection_string.R @@ -0,0 +1,129 @@ +#' @title Create a Database Connection String +#' +#' @description Generates a standard connection string URI for various database +#' backends from an `ojo_db` configuration object. +#' +#' @details This function is useful for applications or libraries that require a +#' connection URI. It intelligently constructs the appropriate string based on +#' the driver specified in the configuration. +#' +#' - For **PostgreSQL** (`RPostgres`), it creates a `postgresql://` URI. +#' - For **DuckDB** (`duckdb`), it creates a `duckdb://` URI, treating the +#' `host` field in the config as the path to the database file. +#' +#' @param config A `db_config` object created by `db_config()`. If `NULL` (the +#' default), a default configuration is created by calling `db_config()`, +#' which will pull from environment variables. +#' +#' @return A single character string representing the database connection URI. +#' @export +#' +#' @examples +#' \dontrun{ +#' # Create a PostgreSQL connection string from environment variables +#' ojo_connection_string(config = db_config(driver = "RPostgres")) +#' +#' # Create a connection string for a local SQLite database file +#' duckdb_config <- db_config(driver = "duckdb", host = "/path/to/db.duckdb") +#' ojo_connection_string(config = duckdb_config) +#' } +ojo_connection_string <- function(config = NULL) { + # If no config is provided, create a default one. + if (is.null(config)) { + config <- db_config() + } + + # Ensure the config object is valid + if (!inherits(config, "db_config")) { + rlang::abort("`config` must be a `db_config` object created by `db_config()`.") + } + + # Dispatch to the appropriate internal function based on the driver + connection_string_builder <- switch( + config$driver, + "RPostgres" = .create_postgres_string, + "RSQLite" = .create_sqlite_string, + "duckdb" = .create_duckdb_string, + rlang::abort(glue::glue("Connection string generation is not supported for the driver: '{config$driver}'.")) + ) + + # TODO: Return invisibly unless arg switch flipped + connection_string_builder(config) +} + +#' @description Creates a PostgreSQL connection string. +#' @keywords internal +.create_postgres_string <- function(config) { + # Check for required parameters + required <- c("host", "port", "username", "password") + missing_params <- setdiff(required, names(config)) + if (length(missing_params) > 0) { + rlang::abort( + c("PostgreSQL config is missing required parameters to build a connection string.", + "i" = glue::glue("Missing: {paste(missing_params, collapse = ', ')}") + ) + ) + } + + # Build the base URI + base_uri <- glue::glue( + "postgresql://{config$username}:{config$password}@{config$host}:{config$port}/ojodb" + ) + + # Identify and collect query parameters (e.g., for SSL) + param_keys <- c("sslmode", "sslrootcert", "sslcert", "sslkey") + + # Rename keys from db_config to match connection string params + names(config)[names(config) == "ssl_mode"] <- "sslmode" + names(config)[names(config) == "ssl_root_cert"] <- "sslrootcert" + names(config)[names(config) == "ssl_cert"] <- "sslcert" + names(config)[names(config) == "ssl_key"] <- "sslkey" + + query_params <- config[names(config) %in% param_keys] + + # Filter out any NULL or empty parameters + query_params <- query_params[!sapply(query_params, is.null)] + query_params <- query_params[query_params != ""] + + # If there are any parameters, format them into a query string + if (length(query_params) > 0) { + query_string <- paste( + names(query_params), query_params, + sep = "=", collapse = "&" + ) + return(paste0(base_uri, "?", query_string)) + } else { + return(base_uri) + } +} + +#' @description Creates a DuckDB connection string. +#' @keywords internal +.create_duckdb_string <- function(config) { + # For DuckDB, the connection string is simply the path to the database file. + # An empty path signifies an in-memory database. + if (is.null(config$host)) { + rlang::abort( + c("DuckDB config is missing the database file path.", + "i" = "Please provide the path in the `host` argument of `db_config()`. For an in-memory database, use an empty string `''`.") + ) + } + + # For DuckDB, the connection string is just the path. + return(config$host) +} + +#' @description Creates a SQLite connection string. +#' @keywords internal +.create_sqlite_string <- function(config) { + # For SQLite, the 'host' is treated as the file path. + if (is.null(config$host) || config$host == "") { + rlang::abort( + c("SQLite config is missing the database file path.", + "i" = "Please provide the path in the `host` argument of `db_config()`.") + ) + } + + # The connection string for SQLite is the protocol followed by the path. + glue::glue("sqlite://{config$host}") +} From e8b2a69bf0bbe16feba33ab4df6e080157717ccb Mon Sep 17 00:00:00 2001 From: Brancen Gregory Date: Fri, 15 Aug 2025 00:14:26 -0500 Subject: [PATCH 03/11] Overhaul connection management (#199) * Add driver and database db_config args; add function to create database connection strings * Add note for todo * Overhauls connection management and updates tests accordingly --- NAMESPACE | 2 + R/ojo_auth.R | 2 +- R/ojo_collect.R | 2 +- R/ojo_connect.R | 261 +++++++++++++--------- R/ojo_connection_string.R | 3 + R/ojo_list_schemas.R | 2 +- R/ojo_list_vars.R | 2 +- R/ojo_pool.R | 111 +++++++++ R/ojo_query.R | 43 ++-- R/ojo_tbl.R | 139 +++--------- man/db_config.Rd | 13 ++ man/dot-connect_duckdb.Rd | 12 + man/dot-connect_postgres.Rd | 12 + man/dot-connect_sqlite.Rd | 12 + man/dot-create_duckdb_string.Rd | 12 + man/dot-create_postgres_string.Rd | 12 + man/dot-create_sqlite_string.Rd | 12 + man/get_connection_object.Rd | 15 -- man/ojo_auth.Rd | 6 + man/ojo_connect.Rd | 59 +++-- man/ojo_connection_string.Rd | 40 ++++ man/ojo_default_connection.Rd | 33 +++ man/ojo_pool.Rd | 67 ++++++ man/ojo_query.Rd | 27 ++- man/ojo_tbl.Rd | 41 ++-- man/tbl_from_gcs_arrow.Rd | 22 -- man/tbl_from_gcs_duckdb.Rd | 20 -- man/tbl_from_rpostgres.Rd | 22 -- tests/testthat/_snaps/ojo_list_schemas.md | 4 +- tests/testthat/test-ojo_connect.R | 82 +++++-- 30 files changed, 699 insertions(+), 391 deletions(-) create mode 100644 R/ojo_pool.R create mode 100644 man/dot-connect_duckdb.Rd create mode 100644 man/dot-connect_postgres.Rd create mode 100644 man/dot-connect_sqlite.Rd create mode 100644 man/dot-create_duckdb_string.Rd create mode 100644 man/dot-create_postgres_string.Rd create mode 100644 man/dot-create_sqlite_string.Rd delete mode 100644 man/get_connection_object.Rd create mode 100644 man/ojo_connection_string.Rd create mode 100644 man/ojo_default_connection.Rd create mode 100644 man/ojo_pool.Rd delete mode 100644 man/tbl_from_gcs_arrow.Rd delete mode 100644 man/tbl_from_gcs_duckdb.Rd delete mode 100644 man/tbl_from_rpostgres.Rd diff --git a/NAMESPACE b/NAMESPACE index 60d780bb..c2e0275f 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -12,6 +12,7 @@ export(ojo_civ_cases) export(ojo_collect) export(ojo_color) export(ojo_connect) +export(ojo_connection_string) export(ojo_county_population) export(ojo_crim_cases) export(ojo_env) @@ -22,6 +23,7 @@ export(ojo_list_schemas) export(ojo_list_tables) export(ojo_list_vars) export(ojo_pal) +export(ojo_pool) export(ojo_query) export(ojo_search_minutes) export(ojo_show_row) diff --git a/R/ojo_auth.R b/R/ojo_auth.R index 0d9018ad..137234b1 100644 --- a/R/ojo_auth.R +++ b/R/ojo_auth.R @@ -87,7 +87,7 @@ db_config <- function( } if (Sys.getenv("OJO_DATABASE") != "") { - config$host <- Sys.getenv("OJO_DATABASE") + config$database <- Sys.getenv("OJO_DATABASE") } if (Sys.getenv("OJO_HOST") != "") { diff --git a/R/ojo_collect.R b/R/ojo_collect.R index 2ffbdc7d..34f54dae 100644 --- a/R/ojo_collect.R +++ b/R/ojo_collect.R @@ -91,7 +91,7 @@ ojo_collect <- function(.data, ..., .silent = !rlang::is_interactive()) { estimate_results <- function(.data, .silent) { - if ("n" %in% names(.data)) { + if ("n" %in% colnames(.data)) { rlang::warn("The tbl you are requesting has a variable named `n`. This might cause issues with progress bar rendering.", .frequency = "once", .frequency_id = "ojo_collect_n_warning") } diff --git a/R/ojo_connect.R b/R/ojo_connect.R index 699907bf..fc59d0b5 100644 --- a/R/ojo_connect.R +++ b/R/ojo_connect.R @@ -1,139 +1,200 @@ -#' @title OJO Connect +#' @title Connect to an OJO Database Backend #' -#' @description Connect to the Open Justice Oklahoma database +#' @description Establishes a single connection to a database, such as the Open +#' Justice Oklahoma Postgres database or a local DuckDB instance. #' -#' @details -#' Opens a connection to the Open Justice Oklahoma database using credentials stored in the .Renviron file. -#' If no credentials exist, prompts for user, password, and host name and provides instructions to store them for future sessions. +#' @details This function serves as the primary way to create individual database +#' connection objects. It is designed to be side-effect-free, returning the +#' connection object directly to the user for manual management. For creating +#' a managed pool of connections suitable for applications, use `ojo_pool()`. +#' +#' @param db_config A `db_config` object created by `db_config()`. If `NULL` (the +#' default), a default configuration is created by calling `db_config()` with +#' no arguments, which will pull from environment variables. +#' @param ... Placeholder for future arguments. #' -#' @param .admin A logical value indicating whether to connect to the database as an administrator. -#' @param ... Placeholder. -#' @param .driver The driver to use for the connection. Default is "RPostgres". "duckdb" is also supported. -#' @param .global Deprecated. A connection will always be created in the specified environment, or in the package environment by default. -#' @param .env The environment in which you want the connection stored. -#' @param .pool A logical value indicating whether to use a connection pool from the `{pool}` package, or not. +#' @return A database connection object (e.g., a `PqConnection` or `duckdb_connection`). #' #' @export -#' @returns A database connection object created with `RPostgres::Postgres()` and either `pool::dbPool` or `DBI::dbConnect` #' #' @examples #' \dontrun{ -#' ojo_connect() -#' } -#' @section Side Effects: -#' A connection object (named `ojo_con` or `ojo_pool` depending on the `.pool` argument) is created in the package environment. +#' # --- Manual Connection Management --- #' -#' @seealso ojo_auth() +#' # 1. Create a configuration +#' my_config <- db_config(.admin = TRUE) #' -ojo_connect <- function(..., .admin = FALSE, .driver = "RPostgres", .global = lifecycle::deprecated(), .env = ojo_env(), .pool = FALSE) { - - if (lifecycle::is_present(.global)) { - lifecycle::deprecate_warn( - when = "2.8.0", - what = "ojo_connect(.global)" - ) +#' # 2. Create a connection object +#' con <- ojo_connect(db_config = my_config) +#' +#' # 3. Use the connection with dplyr or DBI +#' dplyr::tbl(con, "case") +#' DBI::dbListTables(con) +#' +#' # 4. Close the connection when finished +#' DBI::dbDisconnect(con) +#' } +#' @seealso [db_config()] for creating configuration objects, and [ojo_pool()] +#' for creating a connection pool. +ojo_connect <- function(db_config = NULL, ...) { + # If no config is provided, create a default one. + if (is.null(db_config)) { + db_config <- db_config() } - user_type <- if (.admin) "ADMIN" else "DEFAULT" + # Ensure the config object is valid + if (!inherits(db_config, "db_config")) { + rlang::abort("`db_config` must be a `db_config` object created by `db_config()`.") + } - if (Sys.getenv("OJO_HOST") == "" && .driver == "RPostgres") { + if (is.null(db_config$driver) || db_config$driver == "") { rlang::abort( - "No {tolower(user_type)} configuration for the OJO database was found. Please create one now using `ojo_auth`, or manually, by adding the necessary environment variables with `usethis::edit_r_environ`.", - use_cli_format = TRUE + c("Database configuration is missing required parameters.", + "i" = glue::glue("Missing: {paste(missing_params, collapse = ', ')}"), + "*" = "Please set them with `ojo_auth()` or in your `db_config()` call." + ) ) } - connection_type <- if (.pool) "ojo_pool" else "ojo_con" - connection_key <- paste0(connection_type, "_", .driver) + # Dispatch to the appropriate backend-specific connection function + connection_function <- switch( + db_config$driver, + "RPostgres" = .connect_postgres, + "duckdb" = .connect_duckdb, + "RSQLite" = .connect_sqlite, + rlang::abort(glue::glue("The driver '{db_config$driver}' is not supported.")) + ) + + # Call the selected connection function + connection_function(db_config, ...) +} - # Check if a valid connection object already exists in the environment - existing_conn <- get_connection_object(.env, connection_key) - if (!is.null(existing_conn) && DBI::dbIsValid(existing_conn)) { - return(existing_conn) +#' Internal function to connect to Postgres +#' @keywords internal +.connect_postgres <- function(db_config, ...) { + # Check for required parameters + required <- c("database", "host", "port", "username", "password", "ssl_mode") + missing_params <- setdiff(required, names(db_config)) + if (length(missing_params) > 0) { + rlang::abort( + c("Postgres connection is missing required configuration parameters.", + "i" = glue::glue("Missing: {paste(missing_params, collapse = ', ')}"), + "*" = "Please set them with `ojo_auth()` or in your `db_config()` call." + ) + ) } - # Set the driver and connection arguments - conn_args <- switch( - .driver, - "RPostgres" = list( - drv = RPostgres::Postgres(), - dbname = "ojodb", - host = Sys.getenv("OJO_HOST"), - port = Sys.getenv("OJO_PORT"), - user = Sys.getenv(glue::glue("OJO_{user_type}_USER")), - password = Sys.getenv(glue::glue("OJO_{user_type}_PASS")), - sslmode = Sys.getenv("OJO_SSL_MODE"), - sslrootcert = Sys.getenv("OJO_SSL_ROOT_CERT"), - sslcert = Sys.getenv("OJO_SSL_CERT"), - sslkey = Sys.getenv("OJO_SSL_KEY"), - bigint = "integer", - check_interrupts = TRUE, - ... - ), - "duckdb" = list( - drv = duckdb::duckdb(), - ... - ), - rlang::abort("Unsupported driver: {.driver}") + # Assemble the arguments for the connection function + conn_args <- list( + drv = RPostgres::Postgres(), + dbname = db_config$database, + host = db_config$host, + port = as.integer(db_config$port), + user = db_config$username, + password = db_config$password, + sslmode = db_config$ssl_mode, + sslrootcert = db_config$ssl_root_cert, + sslcert = db_config$ssl_cert, + sslkey = db_config$ssl_key, + bigint = "integer", + check_interrupts = TRUE ) - conn_fn <- switch( - connection_type, - ojo_pool = pool::dbPool, - ojo_con = DBI::dbConnect - ) + # Create the connection + rlang::exec(DBI::dbConnect, !!!conn_args) +} + +#' Internal function to connect to DuckDB +#' @keywords internal +.connect_duckdb <- function(db_config, ...) { + # Establish the connection (in-memory by default) + con <- DBI::dbConnect(duckdb::duckdb(), dbdir = ":memory:") + + # Install and load necessary extensions for accessing remote data + tryCatch({ + DBI::dbExecute(con, "INSTALL httpfs; LOAD httpfs;") + DBI::dbExecute(con, "SET s3_endpoint='storage.googleapis.com';") + }, error = function(e) { + rlang::warn(c("Failed to install or configure DuckDB extensions.", + "i" = "Accessing remote data (e.g., from GCS) may not work.", + "x" = e$message)) + }) - new_conn <- rlang::exec(conn_fn, !!!conn_args) - assign(connection_key, new_conn, envir = .env) + con +} - # Make sure duckdb instance has needed features - if (.driver == "duckdb") { - DBI::dbExecute( - new_conn, - stringr::str_glue("INSTALL httpfs; LOAD httpfs; SET s3_endpoint='storage.googleapis.com';") +#' Internal function to connect to SQLite +#' @keywords internal +.connect_sqlite <- function(db_config, ...) { + # For SQLite, the 'host' is treated as the file path. + if (is.null(db_config$host) || db_config$host == "") { + rlang::abort( + c("SQLite config is missing the database file path.", + "i" = "Please provide the path in the `host` argument of `db_config()`.") ) } - withr::defer({ - if (exists(connection_key, envir = .env)) { - connection_object <- get(connection_key, envir = .env, inherits = FALSE) - if (.pool) { - pool::poolClose(connection_object) - } else { - DBI::dbDisconnect(connection_object) - } - rm(list = connection_key, envir = .env) - } - }, envir = .env) - - return(new_conn) + DBI::dbConnect(RSQLite::SQLite(), dbname = db_config$host) } -#' @title Get Connection Object +#' @title Get or create the default OJO database connection +#' +#' @description This internal function manages a single, default connection object +#' stored in the package's private environment (`.ojo_env`). It is the +#' cornerstone of the interactive user experience, ensuring all default +#' database operations for a given backend share the same connection. #' -#' @description -#' Gets the connection object from the environment specified by the `.env` argument. +#' @details +#' This function accepts arguments (...) that are passed to `ojo_connect()`, +#' allowing it to manage distinct default connections for different backends. #' -#' @param env The environment to search for the connection object. +#' The first time this function is called with a unique configuration, it will: +#' 1. Call `ojo_connect()` with the provided arguments. +#' 2. Store the connection in the `.ojo_env` environment under a unique key. +#' 3. Use `withr::defer()` to register a cleanup handler for that connection. #' -#' @keywords internal +#' @param ... Arguments to pass to `ojo_connect()`, primarily a `db_config` object +#' to specify the backend (e.g., `db_config = db_config(.driver = "duckdb")`). #' -get_connection_object <- function(env, key) { - connection_object_exists <- exists( - key, - envir = env, - inherits = FALSE - ) +#' @return A valid database connection object. +#' @keywords internal +ojo_default_connection <- function(...) { + # Capture the arguments to create a unique fingerprint for the connection type. + args <- list(...) + connection_key <- paste0("default_con_", digest::digest(args)) - if (!connection_object_exists) { - return(NULL) + # Check if a valid connection for this specific configuration already exists + if (exists(connection_key, envir = .ojo_env)) { + con <- get(connection_key, envir = .ojo_env) + if (DBI::dbIsValid(con)) { + return(con) + } } - connection_object <- get( - key, - envir = env, - inherits = FALSE + # If no valid connection exists, create a new one. + cli::cli_inform(c("i" = "Creating a new default connection to the OJO database.", + "*" = "This connection will be closed automatically when your R session ends.")) + + new_con <- rlang::exec(ojo_connect, !!!args) + + # Store the new connection in the package environment under its unique key. + assign(connection_key, new_con, envir = .ojo_env) + + # Register a deferred event in the global environment to ensure cleanup + # happens when the user's session ends. + withr::defer( + { + if (exists(connection_key, envir = .ojo_env)) { + con_to_close <- get(connection_key, envir = .ojo_env) + if (DBI::dbIsValid(con_to_close)) { + # This handles single connections only now + DBI::dbDisconnect(con_to_close) + } + rm(list = connection_key, envir = .ojo_env) + } + }, + envir = globalenv() ) - return(connection_object) + return(new_con) } diff --git a/R/ojo_connection_string.R b/R/ojo_connection_string.R index 38ac4927..e6a8ea59 100644 --- a/R/ojo_connection_string.R +++ b/R/ojo_connection_string.R @@ -51,6 +51,7 @@ ojo_connection_string <- function(config = NULL) { connection_string_builder(config) } +#' @title Create Postgres Connection String #' @description Creates a PostgreSQL connection string. #' @keywords internal .create_postgres_string <- function(config) { @@ -97,6 +98,7 @@ ojo_connection_string <- function(config = NULL) { } } +#' @title Create DuckDB Connection String #' @description Creates a DuckDB connection string. #' @keywords internal .create_duckdb_string <- function(config) { @@ -113,6 +115,7 @@ ojo_connection_string <- function(config = NULL) { return(config$host) } +#' @title Create SQLite Connection String #' @description Creates a SQLite connection string. #' @keywords internal .create_sqlite_string <- function(config) { diff --git a/R/ojo_list_schemas.R b/R/ojo_list_schemas.R index 039489b9..48f7fed9 100644 --- a/R/ojo_list_schemas.R +++ b/R/ojo_list_schemas.R @@ -19,7 +19,7 @@ ojo_list_schemas <- function(..., .con = NULL) { ojo_query( "SELECT schema_name FROM information_schema.schemata", - .con = .con + con = .con ) |> dplyr::rename(schema = schema_name) |> dplyr::filter(!.data$schema %in% c("pg_catalog", "information_schema")) |> diff --git a/R/ojo_list_vars.R b/R/ojo_list_vars.R index 1bb7282e..a2bbb57b 100644 --- a/R/ojo_list_vars.R +++ b/R/ojo_list_vars.R @@ -21,7 +21,7 @@ ojo_list_vars <- function(table, schema = "public", ..., .con = NULL) { ojo_tbl( table = "columns", schema = "information_schema", - .con = .con + con = .con ) |> dplyr::filter( .data$table_schema == schema, diff --git a/R/ojo_pool.R b/R/ojo_pool.R new file mode 100644 index 00000000..b19d1471 --- /dev/null +++ b/R/ojo_pool.R @@ -0,0 +1,111 @@ +#' @title Create a Database Connection Pool +#' +#' @description Creates a managed pool of database connections using the {pool} +#' package. This is the recommended way to connect to the database for +#' applications with concurrent users, such as Shiny apps. +#' +#' @details A connection pool is more efficient than managing individual +#' connections in a concurrent environment. Instead of creating and tearing +#' down a new connection for each user or session, the pool maintains a set of +#' active connections that are "checked out" as needed and returned to the +#' pool when the operation is complete. +#' +#' This function is a dedicated wrapper around `ojo_connect()` that forces the +#' creation of a pool. +#' +#' **Important:** Remember to close the pool with `pool::poolClose(pool)` when +#' your application shuts down to release all database connections. +#' +#' @param config A `db_config` object created by `db_config()`. If `NULL` (the +#' default), a default configuration is created, which will pull from +#' environment variables. +#' @title Create a Database Connection Pool +#' +#' @description Creates a managed pool of database connections using the {pool} +#' package. This is the recommended way to connect to the database for +#' applications with concurrent users, such as Shiny apps. +#' +#' @details A connection pool is more efficient than managing individual +#' connections in a concurrent environment. Instead of creating and tearing +#' down a new connection for each user or session, the pool maintains a set of +#' active connections that are "checked out" as needed and returned to the +#' pool when the operation is complete. +#' +#' **Important:** Remember to close the pool with `pool::poolClose(pool)` when +#' your application shuts down to release all database connections. +#' +#' @param config A `db_config` object created by `db_config()`. If `NULL` (the +#' default), a default configuration is created, which will pull from +#' environment variables. +#' @param ... Additional arguments passed on to the underlying `pool::dbPool()` +#' function, such as `minSize`, `maxSize`, or `idleTimeout`. +#' +#' @return A `Pool` object. +#' @export +#' +#' @examples +#' \dontrun{ +#' # Create a connection pool using default settings +#' pool <- ojo_pool() +#' +#' # Use the pool with ojo_tbl or dplyr +#' ojo_tbl("case", .con = pool) +#' +#' # When your application stops, close the pool +#' pool::poolClose(pool) +#' +#' # Create a pool for a specific backend (e.g., SQLite) +#' sqlite_config <- db_config(.driver = "RSQLite", host = "local.db") +#' sqlite_pool <- ojo_pool(config = sqlite_config) +#' pool::poolClose(sqlite_pool) +#' } +ojo_pool <- function(config = NULL, ...) { + # If no config is provided, create a default one. + if (is.null(config)) { + config <- db_config() + } + + # Ensure the config object is valid + if (!inherits(config, "db_config")) { + rlang::abort("`config` must be a `db_config` object created by `db_config()`.") + } + + # Check if the selected driver supports connection pooling + supported_drivers <- c("RPostgres", "RSQLite") + if (!config$driver %in% supported_drivers) { + rlang::abort( + glue::glue("Connection pooling is not supported for the '{config$driver}' driver."), + "i" = glue::glue("Supported drivers are: {paste(supported_drivers, collapse = ', ')}.") + ) + } + + # Assemble the arguments for the pool::dbPool function based on the driver. + # This logic is similar to ojo_connect's internals but targets pool::dbPool. + conn_args <- switch( + config$driver, + "RPostgres" = list( + drv = RPostgres::Postgres(), + dbname = config$database, + host = config$host, + port = as.integer(config$port), + user = config$username, + password = config$password, + sslmode = config$ssl_mode, + sslrootcert = config$ssl_root_cert, + sslcert = config$ssl_cert, + sslkey = config$ssl_key, + bigint = "integer" + ), + "RSQLite" = list( + drv = RSQLite::SQLite(), + # TODO: Decide whether host or database should be the file path + dbname = config$host # For SQLite, 'host' is the file path + ) + ) + + # Combine the generated args with any additional args passed via ... + final_args <- c(conn_args, list(...)) + + # Create the connection pool using the assembled arguments + rlang::exec(pool::dbPool, !!!final_args) +} diff --git a/R/ojo_query.R b/R/ojo_query.R index debf6510..03482a91 100644 --- a/R/ojo_query.R +++ b/R/ojo_query.R @@ -1,27 +1,36 @@ -#' @title OJO Query +#' @title Send a raw SQL query to the database #' -#' @description Query the Open Justice Oklahoma database +#' @description Executes a raw SQL query against a database connection. This +#' function is backend-agnostic and will send the query to any valid DBI +#' connection. #' -#' @param ... Arguments to pass to glue::glue_sql -#' @param .con The ojodb connection to use -#' @param query The query to send to ojodb +#' @param query The SQL query to send to the database. The user is responsible +#' for ensuring the syntax is correct for the target database backend. +#' @param con The database connection to use. If `NULL`, a default connection +#' to the primary Postgres database will be created and used. +#' +#' @export +#' @returns A lazy tibble with the `ojo_tbl` class. #' -#' @export ojo_query -#' @returns data, a lazy tibble containing the results of the query #' @examples #' \dontrun{ -#' ojo_query("SELECT * FROM \"case\" LIMIT 10") -#' ojo_query("SELECT * FROM iic.inmate LIMIT 10") -#' } +#' # Run a query against the default Postgres database +#' ojo_query("SELECT * FROM case LIMIT 10") #' -ojo_query <- function(query, ..., .con = NULL) { - if (is.null(.con)) { - .con <- ojo_connect(...) +#' # Run a query against a manual DuckDB connection +#' duck_con <- ojo_connect(db_config = db_config(.driver = "duckdb")) +#' ojo_query("SELECT * FROM 'my_duck_db_file.parquet' LIMIT 5", con = duck_con) +#' } +ojo_query <- function(query, con = NULL) { + # If no connection is provided, get the default singleton connection. + if (is.null(con)) { + con <- ojo_default_connection() } - if (!inherits(.con, "PqConnection")) { - rlang::abort("Direct SQL querying is currently only supported for Postgres backends. Make sure your connection is using `ojo_connect(.driver = 'RPostgres')`") - } + # Create a lazy tibble from the raw SQL query + data <- dplyr::tbl(con, dbplyr::sql(query)) - dplyr::tbl(.con, sql(query)) + # Add the ojo_tbl class for consistency with ojo_tbl() + class(data) <- c("ojo_tbl", class(data)) + return(data) } diff --git a/R/ojo_tbl.R b/R/ojo_tbl.R index 7d9e61d9..0cb58425 100644 --- a/R/ojo_tbl.R +++ b/R/ojo_tbl.R @@ -1,126 +1,41 @@ -#' Identify a table from the OJO database +#' @title Get a table from a database connection #' -#' Identifies a table in the OJO database from which to query data. Remember to run `connect_ojo()` to establish a connection before attempting to query and to close the connection afterwards with `disconnect_ojo()`. +#' @description Creates a lazy tibble from a table in a database. This is the +#' primary way to begin a database query with {ojodb}. #' -#' @aliases ojo_tbl +#' @details For interactive use, if no connection is provided, this function will +#' automatically create and manage a default connection to the primary OJO +#' Postgres database. To connect to other backends (like DuckDB or SQLite), +#' you must first create a connection object with `ojo_connect()` and pass it +#' via the `con` argument. #' -#' @param table The name of a table in the OJO database. To get a list of tables, run `ojo_list_tables()` -#' @param schema The name of a schema in the OJO database. To get a list of schemas, run `ojo_list_schemas()` -#' @param ... Placeholder -#' @param .con The ojodb connection to use -#' @param .source `r lifecycle::badge("experimental")` The source of the table. Options are 'postgres', 'gcs_duckdb', and 'gcs_arrow'. Default is 'postgres'. +#' @param table The name of the table to query. +#' @param schema The name of the schema where the table resides. +#' @param con A database connection object. If `NULL` (the default), a managed +#' default connection will be created and used automatically. +#' +#' @export +#' @return A lazy tibble with the `ojo_tbl` class. #' -#' @export ojo_tbl -#' @return A pointer to a table that can be passed to dplyr functions and/or pulled into a dataframe using `ojo_collect()` #' @examples #' \dontrun{ -#' # Identifies the table -#' ojo_tbl("case") +#' # Interactively connect to the 'case' table in the default Postgres DB +#' case_tbl <- ojo_tbl("case") #' -#' # Pulls down case information data for every Tulsa felony filed in 2020 into a dataframe d -#' d <- ojo_tbl("case") %>% -#' filter(district == "TULSA", case_type == "CF", year == 2020) %>% -#' collect() +#' # Connect to a table on a manually created DuckDB connection +#' my_duckdb_con <- ojo_connect(db_config = db_config(.driver = "duckdb")) +#' my_duckdb_tbl <- ojo_tbl("some_table", con = my_duckdb_con) #' } -#' @seealso ojo_list_tables(), ojo_list_vars(), ojo_list_schemas() -#' -ojo_tbl <- function( - table, - schema = "public", - ..., - .con = NULL, - .source = "postgres" -) { - if (.source == "database") { - lifecycle::deprecate_warn("2.9.0", I(".source = 'database' is deprecated. Use '.source = 'postgres' instead.")) - .source <- "postgres" +ojo_tbl <- function(table, schema = "public", con = NULL) { + # If no connection is provided, get the default singleton connection. + # This defaults to the primary Postgres database. + if (is.null(con)) { + con <- ojo_default_connection() } - data <- switch( - .source, - "postgres" = { - if (is.null(.con)) { - .con <- ojo_connect( - ..., - .driver = "RPostgres" - ) - } - tbl_from_rpostgres(.con, schema, table) - }, - "gcs_duckdb" = { - if (!rlang::is_installed("duckdb")) { - rlang::abort(".source == \"gcs_duckdb\" requires {duckdb}.") - } - if (is.null(.con)) { - .con <- ojo_connect(..., .driver = "duckdb", .source = .source) - } - if (schema == "public") { - schema <- "oscn" - } - tbl_from_gcs_duckdb(.con, schema, table) - }, - "gcs_arrow" = { - if (!rlang::is_installed("arrow")) { - rlang::abort(".source == \"gcs_arrow\" requires {arrow} with GCS support.") - } - gcs_available <- arrow::arrow_with_gcs() - if (!gcs_available) { - rlang::abort("Arrow wasn't compiled with GCS support.") - } - if (schema == "public") { - schema <- "oscn" - } - tbl_from_gcs_arrow(schema, table) - }, - rlang::abort("Invalid source specified. Please choose one of: 'postgres', 'gcs_duckdb', or 'gcs_arrow'.") - ) + # Create a lazy tibble from the connection + data <- dplyr::tbl(con, DBI::Id(schema = schema, table = table)) class(data) <- c("ojo_tbl", class(data)) - return(data) } - -#' Fetch data from a database -#' -#' @param con A database connection object. -#' @param schema The schema name in the database. -#' @param table The table name to fetch. -#' -#' @return A dplyr tbl object connected to the specified table. -#' -#' @keywords internal -#' -tbl_from_rpostgres <- function(con, schema, table) { - dplyr::tbl(con, DBI::Id(schema = schema, table = table)) -} - -#' Fetch data from Google Cloud Storage -#' -#' @param schema The schema (directory) name in Google Cloud Storage. -#' @param table The table (file) name to fetch. -#' @param anonymous Logical, whether to access GCS anonymously (default: TRUE). -#' -#' @return A dataset object from the specified GCS path. -#' -#' @keywords internal -#' -tbl_from_gcs_arrow <- function(schema, table, anonymous = TRUE) { - bucket_path <- stringr::str_glue("{schema}/{table}") - bucket <- arrow::gs_bucket(bucket_path, anonymous = anonymous) - arrow::open_dataset(bucket, format = "parquet") -} - -#' Fetch data from Google Cloud Storage using {duckdb} -#' -#' @param schema The schema (directory) name in Google Cloud Storage. -#' @param table The table (file) name to fetch. -#' -#' @return A lazy {duckdb} result -#' -#' @keywords internal -#' -tbl_from_gcs_duckdb <- function(con, schema, table) { - bucket_path <- stringr::str_glue("gs://ojo-data-warehouse/raw-data/{schema}/{table}/*.parquet") - DBI::dbExecute(con, stringr::str_glue("CREATE OR REPLACE VIEW \'{table}\' AS SELECT * FROM read_parquet('{bucket_path}')")) - tbl(con, table) -} diff --git a/man/db_config.Rd b/man/db_config.Rd index b46f2fd9..d5f4483e 100644 --- a/man/db_config.Rd +++ b/man/db_config.Rd @@ -5,6 +5,9 @@ \title{Create database configuration object} \usage{ db_config( + ..., + driver = NULL, + database = NULL, host = NULL, port = NULL, username = NULL, @@ -17,6 +20,12 @@ db_config( ) } \arguments{ +\item{...}{Placeholder for future args} + +\item{driver}{Database driver name} + +\item{database}{Database name} + \item{host}{Database host name} \item{port}{Database port number} @@ -54,6 +63,8 @@ with the following precedence (highest to lowest): \dontrun{ # Create config with explicit parameters config <- db_config( + driver = "RPostgres", + database = "ojodb", host = "localhost", port = "5432", username = "user", @@ -63,6 +74,8 @@ config <- db_config( # For local testing where SSL is not configured local_config <- db_config( + driver = "RPostgres", + database = "ojodb", host = "localhost", port = "5432", username = "postgres", diff --git a/man/dot-connect_duckdb.Rd b/man/dot-connect_duckdb.Rd new file mode 100644 index 00000000..4c0c5a93 --- /dev/null +++ b/man/dot-connect_duckdb.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ojo_connect.R +\name{.connect_duckdb} +\alias{.connect_duckdb} +\title{Internal function to connect to DuckDB} +\usage{ +.connect_duckdb(db_config, ...) +} +\description{ +Internal function to connect to DuckDB +} +\keyword{internal} diff --git a/man/dot-connect_postgres.Rd b/man/dot-connect_postgres.Rd new file mode 100644 index 00000000..6367c116 --- /dev/null +++ b/man/dot-connect_postgres.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ojo_connect.R +\name{.connect_postgres} +\alias{.connect_postgres} +\title{Internal function to connect to Postgres} +\usage{ +.connect_postgres(db_config, ...) +} +\description{ +Internal function to connect to Postgres +} +\keyword{internal} diff --git a/man/dot-connect_sqlite.Rd b/man/dot-connect_sqlite.Rd new file mode 100644 index 00000000..a5ebf712 --- /dev/null +++ b/man/dot-connect_sqlite.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ojo_connect.R +\name{.connect_sqlite} +\alias{.connect_sqlite} +\title{Internal function to connect to SQLite} +\usage{ +.connect_sqlite(db_config, ...) +} +\description{ +Internal function to connect to SQLite +} +\keyword{internal} diff --git a/man/dot-create_duckdb_string.Rd b/man/dot-create_duckdb_string.Rd new file mode 100644 index 00000000..d7047fb8 --- /dev/null +++ b/man/dot-create_duckdb_string.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ojo_connection_string.R +\name{.create_duckdb_string} +\alias{.create_duckdb_string} +\title{Create DuckDB Connection String} +\usage{ +.create_duckdb_string(config) +} +\description{ +Creates a DuckDB connection string. +} +\keyword{internal} diff --git a/man/dot-create_postgres_string.Rd b/man/dot-create_postgres_string.Rd new file mode 100644 index 00000000..b84ebdfb --- /dev/null +++ b/man/dot-create_postgres_string.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ojo_connection_string.R +\name{.create_postgres_string} +\alias{.create_postgres_string} +\title{Create Postgres Connection String} +\usage{ +.create_postgres_string(config) +} +\description{ +Creates a PostgreSQL connection string. +} +\keyword{internal} diff --git a/man/dot-create_sqlite_string.Rd b/man/dot-create_sqlite_string.Rd new file mode 100644 index 00000000..27e6b745 --- /dev/null +++ b/man/dot-create_sqlite_string.Rd @@ -0,0 +1,12 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ojo_connection_string.R +\name{.create_sqlite_string} +\alias{.create_sqlite_string} +\title{Create SQLite Connection String} +\usage{ +.create_sqlite_string(config) +} +\description{ +Creates a SQLite connection string. +} +\keyword{internal} diff --git a/man/get_connection_object.Rd b/man/get_connection_object.Rd deleted file mode 100644 index 31c9e92c..00000000 --- a/man/get_connection_object.Rd +++ /dev/null @@ -1,15 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ojo_connect.R -\name{get_connection_object} -\alias{get_connection_object} -\title{Get Connection Object} -\usage{ -get_connection_object(env, key) -} -\arguments{ -\item{env}{The environment to search for the connection object.} -} -\description{ -Gets the connection object from the environment specified by the \code{.env} argument. -} -\keyword{internal} diff --git a/man/ojo_auth.Rd b/man/ojo_auth.Rd index 94dec1c9..f8c2639e 100644 --- a/man/ojo_auth.Rd +++ b/man/ojo_auth.Rd @@ -30,6 +30,8 @@ When .install = TRUE, credentials are saved to .Renviron for persistent use. \dontrun{ # Create config and authenticate for a remote server config <- db_config( + driver = "RPostgres", + database = "ojodb", host = "remote.db.com", port = "5432", username = "user", @@ -40,6 +42,8 @@ ojo_auth(db_config = config) # One-liner with explicit parameters ojo_auth(db_config = db_config( + driver = "RPostgres", + database = "ojodb", host = "remote.db.com", port = "5432", username = "user", @@ -53,6 +57,8 @@ ojo_auth(db_config = db_config(config_file = "~/.ojo_config.yaml")) # Permanent configuration ojo_auth( db_config = db_config( + driver = "RPostgres", + database = "ojodb", host = "remote.db.com", port = "5432", username = "user", diff --git a/man/ojo_connect.Rd b/man/ojo_connect.Rd index 13c684bf..324ddaed 100644 --- a/man/ojo_connect.Rd +++ b/man/ojo_connect.Rd @@ -2,50 +2,49 @@ % Please edit documentation in R/ojo_connect.R \name{ojo_connect} \alias{ojo_connect} -\title{OJO Connect} +\title{Connect to an OJO Database Backend} \usage{ -ojo_connect( - ..., - .admin = FALSE, - .driver = "RPostgres", - .global = lifecycle::deprecated(), - .env = ojo_env(), - .pool = FALSE -) +ojo_connect(db_config = NULL, ...) } \arguments{ -\item{...}{Placeholder.} +\item{db_config}{A \code{db_config} object created by \code{db_config()}. If \code{NULL} (the +default), a default configuration is created by calling \code{db_config()} with +no arguments, which will pull from environment variables.} -\item{.admin}{A logical value indicating whether to connect to the database as an administrator.} - -\item{.driver}{The driver to use for the connection. Default is "RPostgres". "duckdb" is also supported.} - -\item{.global}{Deprecated. A connection will always be created in the specified environment, or in the package environment by default.} - -\item{.env}{The environment in which you want the connection stored.} - -\item{.pool}{A logical value indicating whether to use a connection pool from the \code{{pool}} package, or not.} +\item{...}{Placeholder for future arguments.} } \value{ -A database connection object created with \code{RPostgres::Postgres()} and either \code{pool::dbPool} or \code{DBI::dbConnect} +A database connection object (e.g., a \code{PqConnection} or \code{duckdb_connection}). } \description{ -Connect to the Open Justice Oklahoma database +Establishes a single connection to a database, such as the Open +Justice Oklahoma Postgres database or a local DuckDB instance. } \details{ -Opens a connection to the Open Justice Oklahoma database using credentials stored in the .Renviron file. -If no credentials exist, prompts for user, password, and host name and provides instructions to store them for future sessions. -} -\section{Side Effects}{ - -A connection object (named \code{ojo_con} or \code{ojo_pool} depending on the \code{.pool} argument) is created in the package environment. +This function serves as the primary way to create individual database +connection objects. It is designed to be side-effect-free, returning the +connection object directly to the user for manual management. For creating +a managed pool of connections suitable for applications, use \code{ojo_pool()}. } - \examples{ \dontrun{ -ojo_connect() +# --- Manual Connection Management --- + +# 1. Create a configuration +my_config <- db_config(.admin = TRUE) + +# 2. Create a connection object +con <- ojo_connect(db_config = my_config) + +# 3. Use the connection with dplyr or DBI +dplyr::tbl(con, "case") +DBI::dbListTables(con) + +# 4. Close the connection when finished +DBI::dbDisconnect(con) } } \seealso{ -ojo_auth() +\code{\link[=db_config]{db_config()}} for creating configuration objects, and \code{\link[=ojo_pool]{ojo_pool()}} +for creating a connection pool. } diff --git a/man/ojo_connection_string.Rd b/man/ojo_connection_string.Rd new file mode 100644 index 00000000..9f070c32 --- /dev/null +++ b/man/ojo_connection_string.Rd @@ -0,0 +1,40 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ojo_connection_string.R +\name{ojo_connection_string} +\alias{ojo_connection_string} +\title{Create a Database Connection String} +\usage{ +ojo_connection_string(config = NULL) +} +\arguments{ +\item{config}{A \code{db_config} object created by \code{db_config()}. If \code{NULL} (the +default), a default configuration is created by calling \code{db_config()}, +which will pull from environment variables.} +} +\value{ +A single character string representing the database connection URI. +} +\description{ +Generates a standard connection string URI for various database +backends from an \code{ojo_db} configuration object. +} +\details{ +This function is useful for applications or libraries that require a +connection URI. It intelligently constructs the appropriate string based on +the driver specified in the configuration. +\itemize{ +\item For \strong{PostgreSQL} (\code{RPostgres}), it creates a \verb{postgresql://} URI. +\item For \strong{DuckDB} (\code{duckdb}), it creates a \verb{duckdb://} URI, treating the +\code{host} field in the config as the path to the database file. +} +} +\examples{ +\dontrun{ +# Create a PostgreSQL connection string from environment variables +ojo_connection_string(config = db_config(driver = "RPostgres")) + +# Create a connection string for a local SQLite database file +duckdb_config <- db_config(driver = "duckdb", host = "/path/to/db.duckdb") +ojo_connection_string(config = duckdb_config) +} +} diff --git a/man/ojo_default_connection.Rd b/man/ojo_default_connection.Rd new file mode 100644 index 00000000..86502fc1 --- /dev/null +++ b/man/ojo_default_connection.Rd @@ -0,0 +1,33 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ojo_connect.R +\name{ojo_default_connection} +\alias{ojo_default_connection} +\title{Get or create the default OJO database connection} +\usage{ +ojo_default_connection(...) +} +\arguments{ +\item{...}{Arguments to pass to \code{ojo_connect()}, primarily a \code{db_config} object +to specify the backend (e.g., \code{db_config = db_config(.driver = "duckdb")}).} +} +\value{ +A valid database connection object. +} +\description{ +This internal function manages a single, default connection object +stored in the package's private environment (\code{.ojo_env}). It is the +cornerstone of the interactive user experience, ensuring all default +database operations for a given backend share the same connection. +} +\details{ +This function accepts arguments (...) that are passed to \code{ojo_connect()}, +allowing it to manage distinct default connections for different backends. + +The first time this function is called with a unique configuration, it will: +\enumerate{ +\item Call \code{ojo_connect()} with the provided arguments. +\item Store the connection in the \code{.ojo_env} environment under a unique key. +\item Use \code{withr::defer()} to register a cleanup handler for that connection. +} +} +\keyword{internal} diff --git a/man/ojo_pool.Rd b/man/ojo_pool.Rd new file mode 100644 index 00000000..00ef0d66 --- /dev/null +++ b/man/ojo_pool.Rd @@ -0,0 +1,67 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/ojo_pool.R +\name{ojo_pool} +\alias{ojo_pool} +\title{Create a Database Connection Pool} +\usage{ +ojo_pool(config = NULL, ...) +} +\arguments{ +\item{config}{A \code{db_config} object created by \code{db_config()}. If \code{NULL} (the +default), a default configuration is created, which will pull from +environment variables.} + +\item{...}{Additional arguments passed on to the underlying \code{pool::dbPool()} +function, such as \code{minSize}, \code{maxSize}, or \code{idleTimeout}.} +} +\value{ +A \code{Pool} object. +} +\description{ +Creates a managed pool of database connections using the {pool} +package. This is the recommended way to connect to the database for +applications with concurrent users, such as Shiny apps. + +Creates a managed pool of database connections using the {pool} +package. This is the recommended way to connect to the database for +applications with concurrent users, such as Shiny apps. +} +\details{ +A connection pool is more efficient than managing individual +connections in a concurrent environment. Instead of creating and tearing +down a new connection for each user or session, the pool maintains a set of +active connections that are "checked out" as needed and returned to the +pool when the operation is complete. + +This function is a dedicated wrapper around \code{ojo_connect()} that forces the +creation of a pool. + +\strong{Important:} Remember to close the pool with \code{pool::poolClose(pool)} when +your application shuts down to release all database connections. + +A connection pool is more efficient than managing individual +connections in a concurrent environment. Instead of creating and tearing +down a new connection for each user or session, the pool maintains a set of +active connections that are "checked out" as needed and returned to the +pool when the operation is complete. + +\strong{Important:} Remember to close the pool with \code{pool::poolClose(pool)} when +your application shuts down to release all database connections. +} +\examples{ +\dontrun{ +# Create a connection pool using default settings +pool <- ojo_pool() + +# Use the pool with ojo_tbl or dplyr +ojo_tbl("case", .con = pool) + +# When your application stops, close the pool +pool::poolClose(pool) + +# Create a pool for a specific backend (e.g., SQLite) +sqlite_config <- db_config(.driver = "RSQLite", host = "local.db") +sqlite_pool <- ojo_pool(config = sqlite_config) +pool::poolClose(sqlite_pool) +} +} diff --git a/man/ojo_query.Rd b/man/ojo_query.Rd index 6f99e210..f73491d1 100644 --- a/man/ojo_query.Rd +++ b/man/ojo_query.Rd @@ -2,27 +2,32 @@ % Please edit documentation in R/ojo_query.R \name{ojo_query} \alias{ojo_query} -\title{OJO Query} +\title{Send a raw SQL query to the database} \usage{ -ojo_query(query, ..., .con = NULL) +ojo_query(query, con = NULL) } \arguments{ -\item{query}{The query to send to ojodb} +\item{query}{The SQL query to send to the database. The user is responsible +for ensuring the syntax is correct for the target database backend.} -\item{...}{Arguments to pass to glue::glue_sql} - -\item{.con}{The ojodb connection to use} +\item{con}{The database connection to use. If \code{NULL}, a default connection +to the primary Postgres database will be created and used.} } \value{ -data, a lazy tibble containing the results of the query +A lazy tibble with the \code{ojo_tbl} class. } \description{ -Query the Open Justice Oklahoma database +Executes a raw SQL query against a database connection. This +function is backend-agnostic and will send the query to any valid DBI +connection. } \examples{ \dontrun{ -ojo_query("SELECT * FROM \"case\" LIMIT 10") -ojo_query("SELECT * FROM iic.inmate LIMIT 10") -} +# Run a query against the default Postgres database +ojo_query("SELECT * FROM case LIMIT 10") +# Run a query against a manual DuckDB connection +duck_con <- ojo_connect(db_config = db_config(.driver = "duckdb")) +ojo_query("SELECT * FROM 'my_duck_db_file.parquet' LIMIT 5", con = duck_con) +} } diff --git a/man/ojo_tbl.Rd b/man/ojo_tbl.Rd index 09d50836..795af5f4 100644 --- a/man/ojo_tbl.Rd +++ b/man/ojo_tbl.Rd @@ -2,38 +2,39 @@ % Please edit documentation in R/ojo_tbl.R \name{ojo_tbl} \alias{ojo_tbl} -\title{Identify a table from the OJO database} +\title{Get a table from a database connection} \usage{ -ojo_tbl(table, schema = "public", ..., .con = NULL, .source = "postgres") +ojo_tbl(table, schema = "public", con = NULL) } \arguments{ -\item{table}{The name of a table in the OJO database. To get a list of tables, run \code{ojo_list_tables()}} +\item{table}{The name of the table to query.} -\item{schema}{The name of a schema in the OJO database. To get a list of schemas, run \code{ojo_list_schemas()}} +\item{schema}{The name of the schema where the table resides.} -\item{...}{Placeholder} - -\item{.con}{The ojodb connection to use} - -\item{.source}{\ifelse{html}{\href{https://lifecycle.r-lib.org/articles/stages.html#experimental}{\figure{lifecycle-experimental.svg}{options: alt='[Experimental]'}}}{\strong{[Experimental]}} The source of the table. Options are 'postgres', 'gcs_duckdb', and 'gcs_arrow'. Default is 'postgres'.} +\item{con}{A database connection object. If \code{NULL} (the default), a managed +default connection will be created and used automatically.} } \value{ -A pointer to a table that can be passed to dplyr functions and/or pulled into a dataframe using \code{ojo_collect()} +A lazy tibble with the \code{ojo_tbl} class. } \description{ -Identifies a table in the OJO database from which to query data. Remember to run \code{connect_ojo()} to establish a connection before attempting to query and to close the connection afterwards with \code{disconnect_ojo()}. +Creates a lazy tibble from a table in a database. This is the +primary way to begin a database query with {ojodb}. +} +\details{ +For interactive use, if no connection is provided, this function will +automatically create and manage a default connection to the primary OJO +Postgres database. To connect to other backends (like DuckDB or SQLite), +you must first create a connection object with \code{ojo_connect()} and pass it +via the \code{con} argument. } \examples{ \dontrun{ -# Identifies the table -ojo_tbl("case") +# Interactively connect to the 'case' table in the default Postgres DB +case_tbl <- ojo_tbl("case") -# Pulls down case information data for every Tulsa felony filed in 2020 into a dataframe d -d <- ojo_tbl("case") \%>\% - filter(district == "TULSA", case_type == "CF", year == 2020) \%>\% - collect() -} +# Connect to a table on a manually created DuckDB connection +my_duckdb_con <- ojo_connect(db_config = db_config(.driver = "duckdb")) +my_duckdb_tbl <- ojo_tbl("some_table", con = my_duckdb_con) } -\seealso{ -ojo_list_tables(), ojo_list_vars(), ojo_list_schemas() } diff --git a/man/tbl_from_gcs_arrow.Rd b/man/tbl_from_gcs_arrow.Rd deleted file mode 100644 index d3a91ade..00000000 --- a/man/tbl_from_gcs_arrow.Rd +++ /dev/null @@ -1,22 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ojo_tbl.R -\name{tbl_from_gcs_arrow} -\alias{tbl_from_gcs_arrow} -\title{Fetch data from Google Cloud Storage} -\usage{ -tbl_from_gcs_arrow(schema, table, anonymous = TRUE) -} -\arguments{ -\item{schema}{The schema (directory) name in Google Cloud Storage.} - -\item{table}{The table (file) name to fetch.} - -\item{anonymous}{Logical, whether to access GCS anonymously (default: TRUE).} -} -\value{ -A dataset object from the specified GCS path. -} -\description{ -Fetch data from Google Cloud Storage -} -\keyword{internal} diff --git a/man/tbl_from_gcs_duckdb.Rd b/man/tbl_from_gcs_duckdb.Rd deleted file mode 100644 index 2f2df6dc..00000000 --- a/man/tbl_from_gcs_duckdb.Rd +++ /dev/null @@ -1,20 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ojo_tbl.R -\name{tbl_from_gcs_duckdb} -\alias{tbl_from_gcs_duckdb} -\title{Fetch data from Google Cloud Storage using {duckdb}} -\usage{ -tbl_from_gcs_duckdb(con, schema, table) -} -\arguments{ -\item{schema}{The schema (directory) name in Google Cloud Storage.} - -\item{table}{The table (file) name to fetch.} -} -\value{ -A lazy {duckdb} result -} -\description{ -Fetch data from Google Cloud Storage using {duckdb} -} -\keyword{internal} diff --git a/man/tbl_from_rpostgres.Rd b/man/tbl_from_rpostgres.Rd deleted file mode 100644 index 5fdc4a53..00000000 --- a/man/tbl_from_rpostgres.Rd +++ /dev/null @@ -1,22 +0,0 @@ -% Generated by roxygen2: do not edit by hand -% Please edit documentation in R/ojo_tbl.R -\name{tbl_from_rpostgres} -\alias{tbl_from_rpostgres} -\title{Fetch data from a database} -\usage{ -tbl_from_rpostgres(con, schema, table) -} -\arguments{ -\item{con}{A database connection object.} - -\item{schema}{The schema name in the database.} - -\item{table}{The table name to fetch.} -} -\value{ -A dplyr tbl object connected to the specified table. -} -\description{ -Fetch data from a database -} -\keyword{internal} diff --git a/tests/testthat/_snaps/ojo_list_schemas.md b/tests/testthat/_snaps/ojo_list_schemas.md index 18ba0d13..70d9108b 100644 --- a/tests/testthat/_snaps/ojo_list_schemas.md +++ b/tests/testthat/_snaps/ojo_list_schemas.md @@ -2,6 +2,6 @@ structure(list(schema = c("archive", "doc_tracker", "eviction_addresses", "eviction_dashboard", "iic", "ocdc", "ocdc_new", "odoc", "oscn", - "oscn_test", "ppb", "public")), class = c("tbl_df", "tbl", "data.frame" - ), row.names = c(NA, -12L)) + "oscn_test", "ppb", "public", "tulsa_county_jail")), class = c("tbl_df", + "tbl", "data.frame"), row.names = c(NA, -13L)) diff --git a/tests/testthat/test-ojo_connect.R b/tests/testthat/test-ojo_connect.R index d1231e38..003baab5 100644 --- a/tests/testthat/test-ojo_connect.R +++ b/tests/testthat/test-ojo_connect.R @@ -1,34 +1,84 @@ -test_that("ojo_connect creates a new connection", { +# Helper to ensure a clean slate for each test +# This is crucial for testing the singleton pattern reliably. +with_clean_ojo_env <- function(code) { + # Store the current state of the environment + old_env <- as.list(.ojo_env) + # Clear the environment for the test + rm(list = ls(envir = .ojo_env), envir = .ojo_env) + + # Ensure the environment is restored even if the test fails + on.exit({ + rm(list = ls(envir = .ojo_env), envir = .ojo_env) + rlang::env_bind(.ojo_env, !!!old_env) + }) + + # Run the test code in the cleaned environment + force(code) +} + +test_that("ojo_connect creates a side-effect-free connection", { skip_on_runiverse() con <- ojo_connect() + # Defer disconnection to ensure it happens even if tests fail + withr::defer(DBI::dbDisconnect(con)) + expect_true(DBI::dbIsValid(con), "Connection should be valid") - # Clean up: Close the connection after testing - withr::deferred_run(envir = ojo_env()) + # The key test: the package environment should be empty because + # ojo_connect() does not have side effects. + expect_equal(length(ls(envir = .ojo_env)), 0) }) -test_that("ojo_connect reuses existing connection", { +test_that("ojo_default_connection creates and caches a connection", { skip_on_runiverse() - con1 <- ojo_connect() - con2 <- ojo_connect() + with_clean_ojo_env({ + # 1. First call: creates a new connection + con1 <- ojo_default_connection() + expect_true(DBI::dbIsValid(con1)) - # Check if both connection objects point to the same connection - expect_identical(con1, con2, "Should reuse the same connection object") + # 2. Second call: should return the exact same object + con2 <- ojo_default_connection() + expect_identical(con1, con2, "Should reuse the cached singleton connection") - # Clean up: Close the connection after testing - withr::deferred_run(envir = ojo_env()) + # 3. Manually created connection should be different + manual_con <- ojo_connect() + withr::defer(DBI::dbDisconnect(manual_con)) # Clean up manual connection + expect_false( + identical(con1, manual_con), + "Default and manual connections should be different objects" + ) + + # Clean up the default connection at the end of the test + withr::deferred_run() + }) }) -test_that("ojo_connect handles connection pooling correctly", { +test_that("ojo_default_connection handles different backends separately", { skip_on_runiverse() - pool_con <- ojo_connect(.pool = TRUE) - expect_true(DBI::dbIsValid(pool_con), "Pooled connection should be valid") + with_clean_ojo_env({ + # Create and cache a default Postgres connection + pg_con <- ojo_default_connection() + expect_s4_class(pg_con, "PqConnection") + + # Create and cache a default DuckDB connection + duckdb_config <- db_config(driver = "duckdb") + duck_con <- ojo_default_connection(db_config = duckdb_config) + expect_s4_class(duck_con, "duckdb_connection") + + # Ensure they are not the same object + expect_false(identical(pg_con, duck_con)) - # Further checks can be added to validate the pooling behavior + # Verify that calling again returns the correct cached versions + expect_identical(pg_con, ojo_default_connection()) + expect_identical( + duck_con, + ojo_default_connection(db_config = duckdb_config) + ) - # Clean up: Close the pool after testing - withr::deferred_run(envir = ojo_env()) + # Clean up all default connections + withr::deferred_run() + }) }) From f6244adac160c78af72b5bcb95e444cc0bbff204 Mon Sep 17 00:00:00 2001 From: Brancen Gregory Date: Fri, 15 Aug 2025 00:33:21 -0500 Subject: [PATCH 04/11] Remove ojo_colors and ojo_theme --- R/ojo_colors.R | 72 -------------------------------------------------- R/ojo_theme.R | 28 -------------------- 2 files changed, 100 deletions(-) delete mode 100644 R/ojo_colors.R delete mode 100644 R/ojo_theme.R diff --git a/R/ojo_colors.R b/R/ojo_colors.R deleted file mode 100644 index 8e11f3ce..00000000 --- a/R/ojo_colors.R +++ /dev/null @@ -1,72 +0,0 @@ -#' Add OJO styling to a ggplot -#' -#' Add OJO styling to a ggplot -#' -#' @export -#' -#' @examples -#' \dontrun{ -#' ggplot(ojo_example, aes(file_year, n_cases, color = court)) + -#' geom_line(size = 1.5) + -#' theme_ojo() + -#' ojo_color() + -#' scale_x_continuous( -#' breaks = 2010:2019, -#' limits = c(NA, 2019) -#' ) -#' } -#' -ojo_pal <- c( - "#F8D64E", "black", "#0D0887FF", "#6A00A8FF", - "#B12A90FF", "#E16462FF", "#FCA636FF", "#F0F921FF" -) - - -#' Add OJO styling to a ggplot -#' -#' Add OJO styling to a ggplot -#' -#' @param numbers A vector of numbers to use for the color palette -#' -#' @export -#' -#' @examples -#' \dontrun{ -#' ggplot(ojo_example, aes(file_year, n_cases, color = court)) + -#' geom_line(size = 1.5) + -#' theme_ojo() + -#' ojo_color() + -#' scale_x_continuous( -#' breaks = 2010:2019, -#' limits = c(NA, 2019) -#' ) -#' } -#' -ojo_color <- function(numbers = 1:8) { - ggplot2::scale_color_manual(values = ojo_pal[numbers]) -} - - -#' Add OJO styling to a ggplot -#' -#' Add OJO styling to a ggplot -#' -#' @param numbers A vector of numbers to use for the color palette -#' -#' @export -#' -#' @examples -#' \dontrun{ -#' ggplot(ojo_example, aes(file_year, n_cases, color = court)) + -#' geom_line(size = 1.5) + -#' theme_ojo() + -#' ojo_color() + -#' scale_x_continuous( -#' breaks = 2010:2019, -#' limits = c(NA, 2019) -#' ) -#' } -#' -ojo_fill <- function(numbers = 1:8) { - ggplot2::scale_fill_manual(values = ojo_pal[numbers]) -} diff --git a/R/ojo_theme.R b/R/ojo_theme.R deleted file mode 100644 index cfb6b518..00000000 --- a/R/ojo_theme.R +++ /dev/null @@ -1,28 +0,0 @@ -#' Style a ggplot in the OJO style -#' -#' Add OJO styling to a ggplot -#' -#' @export ojo_theme -#' -#' @importFrom ggplot2 %+replace% -#' -#' @examples -#' \dontrun{ -#' ggplot(ojo_example, aes(file_year, n_cases, color = court)) + -#' geom_line(size = 1.5) + -#' ojo_theme() + -#' ojo_colors() + -#' scale_x_continuous( -#' breaks = 2010:2019, -#' limits = c(NA, 2019) -#' ) -#' } -ojo_theme <- function() { - ggplot2::theme_bw(base_size = 14, base_family = "Menlo") %+replace% - ggplot2::theme( - panel.background = ggplot2::element_blank(), - plot.background = ggplot2::element_blank(), - legend.background = ggplot2::element_rect(fill = "transparent", colour = NA), - legend.key = ggplot2::element_rect(fill = "transparent", colour = NA) - ) -} From 660bc630df58a8c21e83e0af5144d16cab2602a5 Mon Sep 17 00:00:00 2001 From: Brancen Gregory Date: Fri, 15 Aug 2025 00:46:41 -0500 Subject: [PATCH 05/11] Update pkgdown --- NAMESPACE | 5 - _pkgdown.yml | 6 +- docs/404.html | 76 ++-- docs/LICENSE.html | 53 +-- docs/articles/index.html | 51 +-- docs/articles/vignette-connecting-to-ojo.html | 80 ++-- docs/articles/vignette-pulling-data.html | 395 +++++++++--------- docs/authors.html | 83 ++-- docs/deps/data-deps.txt | 13 +- docs/index.html | 99 +++-- docs/news/index.html | 149 +++++-- docs/pkgdown.js | 16 +- docs/pkgdown.yml | 7 +- docs/reference/index.html | 279 +++++++------ docs/reference/ojo_add_counts.html | 80 ++-- docs/reference/ojo_add_issues.html | 69 ++- docs/reference/ojo_add_minutes.html | 67 ++- docs/reference/ojo_add_party_details.html | 69 ++- docs/reference/ojo_auth.html | 160 ++++--- docs/reference/ojo_case_types.html | 61 ++- docs/reference/ojo_check_ssl.html | 67 ++- docs/reference/ojo_civ_cases.html | 77 ++-- docs/reference/ojo_collect.html | 79 ++-- docs/reference/ojo_connect.html | 126 +++--- docs/reference/ojo_county_population.html | 65 ++- docs/reference/ojo_crim_cases.html | 77 ++-- docs/reference/ojo_env.html | 57 ++- docs/reference/ojo_fiscal_year.html | 65 ++- docs/reference/ojo_list_schemas.html | 71 ++-- docs/reference/ojo_list_tables.html | 73 ++-- docs/reference/ojo_list_vars.html | 71 ++-- docs/reference/ojo_query.html | 92 ++-- docs/reference/ojo_search_minutes.html | 77 ++-- docs/reference/ojo_show_row.html | 69 ++- docs/reference/ojo_tbl.html | 106 +++-- docs/reference/ojo_version.html | 63 ++- docs/reference/skip_if_no_db.html | 53 +-- docs/search.json | 2 +- docs/sitemap.xml | 170 +++----- man/ojo_color.Rd | 27 -- man/ojo_fill.Rd | 27 -- man/ojo_pal.Rd | 29 -- man/ojo_theme.Rd | 23 - 43 files changed, 1547 insertions(+), 1837 deletions(-) mode change 100755 => 100644 docs/pkgdown.js delete mode 100644 man/ojo_color.Rd delete mode 100644 man/ojo_fill.Rd delete mode 100644 man/ojo_pal.Rd delete mode 100644 man/ojo_theme.Rd diff --git a/NAMESPACE b/NAMESPACE index c2e0275f..6db7f3c8 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -10,25 +10,21 @@ export(ojo_case_types) export(ojo_check_ssl) export(ojo_civ_cases) export(ojo_collect) -export(ojo_color) export(ojo_connect) export(ojo_connection_string) export(ojo_county_population) export(ojo_crim_cases) export(ojo_env) export(ojo_eviction_cases) -export(ojo_fill) export(ojo_fiscal_year) export(ojo_list_schemas) export(ojo_list_tables) export(ojo_list_vars) -export(ojo_pal) export(ojo_pool) export(ojo_query) export(ojo_search_minutes) export(ojo_show_row) export(ojo_tbl) -export(ojo_theme) export(ojo_version) import(dplyr) importFrom(dplyr,case_when) @@ -36,7 +32,6 @@ importFrom(dplyr,filter) importFrom(dplyr,left_join) importFrom(dplyr,mutate) importFrom(dplyr,select) -importFrom(ggplot2,"%+replace%") importFrom(lubridate,as_date) importFrom(lubridate,floor_date) importFrom(stringr,str_detect) diff --git a/_pkgdown.yml b/_pkgdown.yml index 2182316f..db4d1f59 100644 --- a/_pkgdown.yml +++ b/_pkgdown.yml @@ -18,10 +18,6 @@ reference: - title: "Functions to help aid with analysis" desc: "These include some other helpful functions you might use in your analysis." contents: - - ojo_color - - ojo_fill - - ojo_pal - - ojo_theme - ojo_county_population - ojo_fiscal_year - title: "Database management functions" @@ -36,6 +32,8 @@ reference: desc: "These are the most basic / fundamental functions in the package. Generally, we shouldn't need to use them directly." contents: - ojo_connect + - ojo_pool + - ojo_connection_string - ojo_tbl - ojo_query - ojo_check_ssl diff --git a/docs/404.html b/docs/404.html index c7f6ca4b..1f99c568 100644 --- a/docs/404.html +++ b/docs/404.html @@ -6,68 +6,60 @@ Page not found (404) • ojodb - - - - - - + + + + + - - - - + + + + - Skip to contents - -