├── .Rbuildignore ├── .github ├── .gitignore └── workflows │ └── check-standard.yaml ├── .gitignore ├── DESCRIPTION ├── LICENSE ├── LICENSE.md ├── NAMESPACE ├── NEWS.md ├── R ├── qt.R ├── queries.R ├── sql_helpers.R └── utils-pipe.R ├── README.Rmd ├── README.md ├── inst └── query_templates │ ├── basic.sql │ └── dimensional_rollup.sql ├── man ├── and_join.Rd ├── blank_if_null.Rd ├── comma_join.Rd ├── env_from_params.Rd ├── head.query_template.Rd ├── pipe.Rd ├── print.query_template.Rd ├── qt_basic.Rd ├── qt_rollup.Rd ├── queries_default_location.Rd ├── queries_list.Rd ├── query_as_function.Rd ├── query_create.Rd ├── query_from_string.Rd ├── query_load.Rd ├── query_set_default_location.Rd └── query_substitute.Rd └── tests ├── testthat.R └── testthat ├── _snaps └── qt.md ├── example_with_defaults.sql ├── test-io.R ├── test-qt.R ├── test-query_create.R ├── test-querying.R └── test-sql_helpers.R /.Rbuildignore: -------------------------------------------------------------------------------- 1 | ^.*\.Rproj$ 2 | ^\.Rproj\.user$ 3 | ^LICENSE\.md$ 4 | ^README\.Rmd$ 5 | ^\.github$ 6 | -------------------------------------------------------------------------------- /.github/.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | -------------------------------------------------------------------------------- /.github/workflows/check-standard.yaml: -------------------------------------------------------------------------------- 1 | # For help debugging build failures open an issue on the RStudio community with the 'github-actions' tag. 2 | # https://community.rstudio.com/new-topic?category=Package%20development&tags=github-actions 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | - dev 9 | pull_request: 10 | branches: 11 | - main 12 | - master 13 | 14 | name: R-CMD-check 15 | 16 | jobs: 17 | R-CMD-check: 18 | runs-on: ${{ matrix.config.os }} 19 | 20 | name: ${{ matrix.config.os }} (${{ matrix.config.r }}) 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | config: 26 | - {os: windows-latest, r: 'release'} 27 | - {os: macOS-latest, r: 'release'} 28 | - {os: ubuntu-20.04, r: 'release', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} 29 | - {os: ubuntu-20.04, r: 'devel', rspm: "https://packagemanager.rstudio.com/cran/__linux__/focal/latest"} 30 | 31 | env: 32 | R_REMOTES_NO_ERRORS_FROM_WARNINGS: true 33 | RSPM: ${{ matrix.config.rspm }} 34 | GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | steps: 37 | - uses: actions/checkout@v2 38 | 39 | - uses: r-lib/actions/setup-r@v1 40 | with: 41 | r-version: ${{ matrix.config.r }} 42 | 43 | - uses: r-lib/actions/setup-pandoc@v1 44 | 45 | - name: Query dependencies 46 | run: | 47 | install.packages('remotes') 48 | saveRDS(remotes::dev_package_deps(dependencies = TRUE), ".github/depends.Rds", version = 2) 49 | writeLines(sprintf("R-%i.%i", getRversion()$major, getRversion()$minor), ".github/R-version") 50 | shell: Rscript {0} 51 | 52 | - name: Restore R package cache 53 | if: runner.os != 'Windows' 54 | uses: actions/cache@v2 55 | with: 56 | path: ${{ env.R_LIBS_USER }} 57 | key: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1-${{ hashFiles('.github/depends.Rds') }} 58 | restore-keys: ${{ runner.os }}-${{ hashFiles('.github/R-version') }}-1- 59 | 60 | - name: Install system dependencies 61 | if: runner.os == 'Linux' 62 | run: | 63 | while read -r cmd 64 | do 65 | eval sudo $cmd 66 | done < <(Rscript -e 'writeLines(remotes::system_requirements("ubuntu", "20.04"))') 67 | 68 | - name: Install dependencies 69 | run: | 70 | remotes::install_deps(dependencies = TRUE) 71 | remotes::install_cran("rcmdcheck") 72 | shell: Rscript {0} 73 | 74 | - name: Check 75 | env: 76 | _R_CHECK_CRAN_INCOMING_REMOTE_: false 77 | run: | 78 | options(crayon.enabled = TRUE) 79 | rcmdcheck::rcmdcheck(args = c("--no-manual", "--as-cran"), error_on = "warning", check_dir = "check") 80 | shell: Rscript {0} 81 | 82 | - name: Upload check results 83 | if: failure() 84 | uses: actions/upload-artifact@main 85 | with: 86 | name: ${{ runner.os }}-r${{ matrix.config.r }}-results 87 | path: check 88 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .Rproj.user 2 | .Rhistory 3 | .RData 4 | .Ruserdata 5 | *.Rproj 6 | -------------------------------------------------------------------------------- /DESCRIPTION: -------------------------------------------------------------------------------- 1 | Package: queries 2 | Type: Package 3 | Title: Store and Manage Parameterized Queries 4 | Version: 0.3.4 5 | Author: Colin Fraser 6 | Maintainer: Colin Fraser 7 | Description: Maintain a set of parameterized queries with easy parameterization. 8 | License: MIT + file LICENSE 9 | Encoding: UTF-8 10 | LazyData: true 11 | Imports: 12 | fs (>= 1.3.1), 13 | purrr (>= 0.3.3), 14 | readr (>= 1.3.1), 15 | rmarkdown (>= 1.18), 16 | magrittr (>= 1.5), 17 | glue (>= 1.3.1), 18 | yaml (>= 2.2.0), 19 | rlang (>= 0.4.3), 20 | tibble (>= 2.1.3), 21 | rstudioapi (>= 0.10), 22 | stringr, 23 | dplyr 24 | RoxygenNote: 7.1.2 25 | Suggests: 26 | testthat (>= 2.1.0) 27 | URL: https://github.com/colin-fraser/queries 28 | BugReports: https://github.com/colin-fraser/queries/issues 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | YEAR: 2020 2 | COPYRIGHT HOLDER: Colin Fraser 3 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # MIT License 2 | 3 | Copyright (c) 2020 Colin Fraser 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NAMESPACE: -------------------------------------------------------------------------------- 1 | # Generated by roxygen2: do not edit by hand 2 | 3 | S3method(head,query_template) 4 | S3method(print,query_template) 5 | export("%>%") 6 | export(and_join) 7 | export(blank_if_null) 8 | export(comma_join) 9 | export(qt_basic) 10 | export(qt_rollup) 11 | export(queries_default_location) 12 | export(queries_list) 13 | export(query_as_function) 14 | export(query_create) 15 | export(query_from_string) 16 | export(query_load) 17 | export(query_set_default_location) 18 | export(query_substitute) 19 | importFrom(magrittr,"%>%") 20 | importFrom(rlang,.data) 21 | importFrom(utils,head) 22 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # queries 0.3.4 2 | - Added `qt_basic` and `qt_rollup` functions 3 | 4 | # queries 0.3.3 5 | - Added names_to_as argument to comma_join 6 | 7 | # queries 0.3.2 8 | - Added `and_join` helper function 9 | 10 | # queries 0.3.1 11 | - set up github actions and make tests pass on Windows 12 | 13 | # queries 0.3.0 14 | - Add query_create as a convenience function for starting new query files. 15 | 16 | # queries 0.2.1 17 | - Added query_as_function 18 | 19 | # queries 0.2.0 20 | - Improve logic for looking for files 21 | - Add default parameters to parameters 22 | 23 | # queries 0.1.1 24 | * Changed look for queries in /sql first 25 | * Parameters have descriptions now 26 | 27 | # queries 0.1.0.9000 28 | 29 | * Added a `NEWS.md` file to track changes to the package. 30 | -------------------------------------------------------------------------------- /R/qt.R: -------------------------------------------------------------------------------- 1 | # Query Templates 2 | # This includes built-in queries that are handy to have 3 | 4 | load_template <- function(filename) { 5 | query_load(system.file("query_templates", filename, package = "queries")) 6 | } 7 | 8 | qt_sub <- function(name, ...) { 9 | args <- list(...) 10 | query <- load_template(name) 11 | args[['qry']] <- query 12 | do.call(query_substitute, args) 13 | } 14 | 15 | #' The most basic query pattern 16 | #' 17 | #' @param select_cols vector of columns to select. 18 | #' Names will be used as identifiers, if supplied 19 | #' @param table_name table to select from 20 | #' @param where vector of constraints to be joined with 'and' 21 | #' @param group_by vector of columns to group by 22 | #' @param order_by vector of columns to order by 23 | #' @param limit number of rows to return 24 | #' 25 | #' @return a filled query 26 | #' @export 27 | #' 28 | #' @examples 29 | #' qt_basic( 30 | #' select_cols = c(Date = "saledate", "Country", Sales = "SUM(sales)"), 31 | #' table = "all_sales", 32 | #' where = c("country in ('US', 'CA')", "year = 2020"), 33 | #' group_by = c("saledate", "country"), 34 | #' order_by = "Date") 35 | qt_basic <- function(select_cols, table_name, where = NULL, group_by = NULL, 36 | order_by = NULL, limit = NULL) { 37 | qt_sub("basic.sql", select_cols = select_cols, table_name = table_name, 38 | where = where, group_by = group_by, order_by = order_by, limit = limit) 39 | } 40 | 41 | 42 | #' Dimensional rollup query pattern 43 | #' 44 | #' @description 45 | #' Produces a query that performs a rollup operation on a cube-like table. 46 | #' 47 | #' @param dimensions dimensions to roll up by 48 | #' @param metrics aggregations that will be performed on each dimension 49 | #' @param table_name table to query 50 | #' @param where constraints 51 | #' @param order_by order by 52 | #' 53 | #' @return a query 54 | #' @export 55 | #' 56 | #' @examples 57 | #' qt_rollup( 58 | #' c(order_date = "date", "country"), 59 | #' c(sales = "sum(sales)"), 60 | #' 'metrics_table' 61 | #' ) 62 | qt_rollup <- function(dimensions, metrics, table_name, where = NULL, 63 | order_by = dimensions) { 64 | qt_sub("dimensional_rollup.sql", dimensions = dimensions, metrics = metrics, 65 | table_name = table_name, where = where, order_by = order_by) 66 | } -------------------------------------------------------------------------------- /R/queries.R: -------------------------------------------------------------------------------- 1 | #' Returns the default query location 2 | #' 3 | #' @description First checks for a queries_location option. 4 | #' If that's not there, defualts to ~/.queries 5 | #' 6 | #' @return default query location as a character 7 | #' @export 8 | #' 9 | queries_default_location <- function() { 10 | if (!is.null(getOption("default_queries_location"))) { 11 | return(getOption("default_queries_location")) 12 | } 13 | 14 | default_fp <- fs::path(fs::path_home(), ".queries") 15 | if (fs::dir_exists(default_fp)) { 16 | return(default_fp) 17 | } else { 18 | stop( 19 | glue::glue("Default path {default_fp} does not exist. "), 20 | "Create it or choose a different path with\n", 21 | "options(queries_location='')." 22 | ) 23 | } 24 | } 25 | 26 | #' List the available queries 27 | #' 28 | #' @param location the directory to look in 29 | #' 30 | #' @return A tibble of queries 31 | #' @export 32 | #' 33 | queries_list <- function(location = queries_default_location()) { 34 | filepaths <- fs::dir_ls(location, glob = "*.sql") 35 | filepaths %>% 36 | purrr::map(~ list(path = .x, query = query_from_file(.x))) %>% 37 | purrr::map_df(~ tibble::tibble( 38 | name = stringr::str_extract(fs::path_file(.x$path), "(-|\\w)+"), 39 | path = .x$path, 40 | description = .x$query$description, 41 | params = list(.x$query$params), template = .x$query$template 42 | )) 43 | } 44 | 45 | #' Build a query object from a length-1 character vector 46 | #' 47 | #' @param s character containing query 48 | #' @param include_header if true, the header will be included in the sql query 49 | #' 50 | #' @return convert string into query object 51 | #' @export 52 | #' @importFrom rlang .data 53 | query_from_string <- function(s, include_header = FALSE) { 54 | parts <- separate_head_body(s) 55 | qry <- yaml::yaml.load(parts$head, as.named.list = T) 56 | if (include_header) { 57 | qry$template <- s 58 | } else { 59 | qry$template <- parts$body 60 | } 61 | qry$head <- parts$head 62 | class(qry) <- "query_template" 63 | 64 | qry 65 | } 66 | 67 | separate_head_body <- function(s) { 68 | query_lines <- unlist(strsplit(s, "\n")) 69 | header_end <- which(substr(query_lines, 1, 2) != "--")[1] 70 | 71 | head <- gsub("-- ", "", query_lines[1:(header_end - 1)]) %>% 72 | paste(collapse = "\n") 73 | body <- trimws(paste(query_lines[header_end:length(query_lines)], collapse = "\n"), 74 | whitespace = "\n" 75 | ) 76 | 77 | return(list(head = head, body = body)) 78 | } 79 | 80 | query_from_file <- function(file, include_header = FALSE) { 81 | query_from_string(readr::read_file(file), include_header) 82 | } 83 | 84 | 85 | #' Print query 86 | #' 87 | #' @param x a query of time query_template 88 | #' @param ... ignored 89 | #' 90 | #' @return NULL 91 | #' @export 92 | #' 93 | print.query_template <- function(x, ...) { 94 | cat(x$template) 95 | } 96 | 97 | #' Load a query from file 98 | #' 99 | #' @param query_name name of the query. This can also be a path, in which 100 | #' case query_location is ignored. 101 | #' @param query_location location of the query. 102 | #' @param include_header if true, the header will be included in the sql query 103 | #' 104 | #' @return loaded query 105 | #' @export 106 | #' 107 | query_load <- function(query_name, query_location = 'sql', 108 | include_header = FALSE) { 109 | if (fs::file_exists(query_name)) { 110 | return(query_from_file(query_name, include_header)) 111 | } 112 | if (fs::file_exists(fs::path("sql", query_name))) { 113 | return(query_from_file(fs::path("sql", query_name), include_header)) 114 | } 115 | if (fs::file_exists(fs::path("sql", query_name, ext = "sql"))) { 116 | return(query_from_file( 117 | fs::path("sql", query_name, ext = "sql"), 118 | include_header 119 | )) 120 | } 121 | 122 | 123 | file <- fs::path(query_location, paste0(query_name, ".sql")) 124 | query_from_file(file, include_header) 125 | } 126 | 127 | 128 | #' Print the header of a query 129 | #' 130 | #' @param x a query 131 | #' @param ... ignored 132 | #' 133 | #' @return silently returns the head 134 | #' @export 135 | #' @importFrom utils head 136 | #' 137 | head.query_template <- function(x, ...) { 138 | h <- x$head 139 | cat(h) 140 | invisible(h) 141 | } 142 | 143 | #' Sets the default queries location 144 | #' 145 | #' @param path file path to a directory with .sql files 146 | #' 147 | #' @export 148 | #' 149 | query_set_default_location <- function(path) { 150 | options(default_queries_location = path) 151 | } 152 | 153 | #' Replace parameters in query 154 | #' 155 | #' @param qry a query object or name of a query 156 | #' @param query_location the location of the query 157 | #' @param append_params should the parameter values be appended to the 158 | #' query in a comment? 159 | #' @param ... parameter values 160 | #' @param include_header if true, the header will be included in the sql query 161 | #' 162 | #' @return a formatted query 163 | #' @export 164 | #' 165 | query_substitute <- function(qry, ..., query_location = queries_default_location(), 166 | include_header = FALSE, append_params = FALSE) { 167 | if (class(qry) == "character") { 168 | query <- query_load(qry, query_location, include_header) 169 | } else if (class(qry) == "query_template") { 170 | query <- qry 171 | } 172 | else { 173 | stop("qry must be either a character indicating a saved query template or an object if type query_template") 174 | } 175 | 176 | glue_env <- env_from_params(query$params, ...) 177 | 178 | out <- glue::as_glue(stringr::str_trim(glue::glue(query$template, .envir = rlang::env(glue_env)))) 179 | if (append_params) { 180 | param_note <- paste("--", readr::read_lines(yaml::as.yaml(as.list(glue_env))), 181 | collapse = "\n" 182 | ) 183 | out <- paste(out, param_note, sep = "\n\n") 184 | } 185 | 186 | out 187 | } 188 | 189 | 190 | 191 | #' Creates the environment for using glue to replace 192 | #' 193 | #' @param params list of parameters 194 | #' @param ... filled parameter values 195 | #' 196 | #' @return an environment for gluing 197 | #' @keywords internal 198 | #' 199 | env_from_params <- function(params, ...) { 200 | param_list <- list(...) 201 | 202 | missing_with_no_default <- params %>% 203 | purrr::discard(~ !is.null(.x$default)) %>% 204 | purrr::discard(~ .x$name %in% names(param_list)) %>% 205 | purrr::map_chr("name") 206 | 207 | if (length(missing_with_no_default) > 0) { 208 | stop(paste0( 209 | "Missing params with no default:\n", 210 | paste0("- ", missing_with_no_default, collapse = "\n") 211 | )) 212 | } 213 | 214 | user_specified <- params %>% 215 | purrr::map_if(~ .x$name %in% names(param_list), ~ list(param_list[[.x$name]]) %>% rlang::set_names(.x$name)) %>% 216 | purrr::map_if(~ !is.null(.x$default), ~ list(.x$default) %>% rlang::set_names(.x$name)) %>% 217 | purrr::flatten() %>% 218 | rlang::as_environment(parent = rlang::env_parent()) 219 | 220 | return(user_specified) 221 | } 222 | 223 | 224 | #' Turn a parameterized query into a function 225 | #' 226 | #' @param x a query loaded with query_load 227 | #' @param include_header include the header in the query output 228 | #' @param append_params append parameter values in the query 229 | #' 230 | #' @return a function whose arguments are the query parameters 231 | #' @export 232 | #' 233 | query_as_function <- function(x, include_header = FALSE, append_params = FALSE) { 234 | fpar <- as.pairlist(x$params %>% purrr::map(~ list(.x$default) %>% rlang::set_names(.x$name)) %>% purrr::flatten()) 235 | f <- function() { 236 | args <- as.list(environment()) 237 | null_args <- args %>% 238 | purrr::keep(is.null) %>% 239 | names() 240 | if (length(null_args) > 0) { 241 | stop( 242 | "Missing params with no default:\n", 243 | paste0("- ", null_args, collapse = "\n") 244 | ) 245 | } 246 | do.call(query_substitute, c( 247 | list(qry = x), args, 248 | list( 249 | include_header = include_header, 250 | append_params = append_params 251 | ) 252 | )) 253 | } 254 | formals(f) <- fpar 255 | f 256 | } 257 | 258 | #' Create a parameterized query 259 | #' 260 | #' @param filename what do you want the query to be called 261 | #' @param query_name the name at the top of the query description 262 | #' @param param_names names of query parameters 263 | #' @param open if true, the saved file opens in rstudio 264 | #' 265 | #' @return invisibly 266 | #' @export 267 | #' 268 | query_create <- function(filename, query_name = "", 269 | param_names = NULL, 270 | open = TRUE) { 271 | q <- glue::glue( 272 | "-- name: {query_name}\n-- params:\n{paste('-- - name:', param_names, collapse = '\n')}" 273 | ) 274 | f <- readr::write_file(q, filename) 275 | if (rstudioapi::isAvailable() & open) { 276 | rstudioapi::navigateToFile(filename) 277 | } 278 | invisible(f) 279 | } 280 | -------------------------------------------------------------------------------- /R/sql_helpers.R: -------------------------------------------------------------------------------- 1 | #' Join a character vector together with commas 2 | #' 3 | #' @param s a character vector to be joined 4 | #' @param leading_comma should a leading comma be inserted? 5 | #' @param trailing_comma should a trailing comma be inserted? 6 | #' @param quote should the output be quoted? Default false. 7 | #' @param names_to_as if this is true and s is named, the names will 8 | #' be used as column identifiers. See examples. Defaults to true. 9 | #' 10 | #' @return a length-1 character vector 11 | #' @export 12 | #' 13 | #' @examples 14 | #' comma_join(letters) 15 | #' comma_join(letters, leading_comma = TRUE) 16 | #' comma_join(letters, quote = TRUE) 17 | #' 18 | #' # named input 19 | #' comma_join(c(avg_sales = "avg(sales)", "country"), names_to_as = TRUE) 20 | #' 21 | comma_join <- function(s, leading_comma = FALSE, trailing_comma = FALSE, 22 | quote = FALSE, names_to_as = TRUE) { 23 | if (is.null(s)) { 24 | return("") 25 | } 26 | if (names_to_as & !is.null(names(s))) { 27 | if (quote) { 28 | warning("Both quote and names_to_as are TRUE. You probably don't want this.") 29 | } 30 | s <- names_to_as(s) 31 | } 32 | if (quote) { 33 | s <- paste0("'", s, "'") 34 | } 35 | out <- paste(paste(s, collapse = ", ")) 36 | if (leading_comma) { 37 | out <- paste(",", out) 38 | } 39 | if (trailing_comma) { 40 | out <- paste0(out, ",") 41 | } 42 | return(out) 43 | } 44 | 45 | names_to_as <- function(s) { 46 | names <- names(s) 47 | ifelse(names != "", paste(s, 'as', names), s) 48 | } 49 | 50 | #' Insert blank character if a variable is null 51 | #' 52 | #' @param s String to insert, or blank if null 53 | #' @param output What to print if s is not null 54 | #' 55 | #' This is useful for specifying e.g. a column list that may or may not 56 | #' be part of a query. 57 | #' 58 | #' @return a character vector 59 | #' @export 60 | #' 61 | blank_if_null <- function(s, output = s) { 62 | if (is.null(s)) { 63 | return("") 64 | } 65 | 66 | output 67 | } 68 | 69 | 70 | #' Join a vector with 'AND' 71 | #' 72 | #' @param s a character vector 73 | #' @param leading_and logical indicating whether 'and' should be pre-pended 74 | #' @param trailing_and logical indicating whether 'and' should be post-pended 75 | #' 76 | #' @return a string with the elements of s joined by 'AND' 77 | #' @export 78 | #' 79 | #' @examples 80 | #' and_join("") 81 | #' and_join("country = 'US'") 82 | #' and_join(c("country = 'US'", "type = 1")) 83 | and_join <- function(s, leading_and = FALSE, trailing_and = FALSE) { 84 | if (is.null(s)) { 85 | return("") 86 | } 87 | prefix <- {if (leading_and) 'and\n' else ''} 88 | suffix <- {if (trailing_and) ' and\n' else ''} 89 | paste0(prefix, paste0(s, collapse = ' and\n'), suffix) 90 | } 91 | 92 | -------------------------------------------------------------------------------- /R/utils-pipe.R: -------------------------------------------------------------------------------- 1 | #' Pipe operator 2 | #' 3 | #' See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details. 4 | #' 5 | #' @name %>% 6 | #' @rdname pipe 7 | #' @keywords internal 8 | #' @export 9 | #' @importFrom magrittr %>% 10 | #' @usage lhs \%>\% rhs 11 | NULL 12 | -------------------------------------------------------------------------------- /README.Rmd: -------------------------------------------------------------------------------- 1 | --- 2 | output: github_document 3 | --- 4 | 5 | 6 | 7 | ```{r, include = FALSE} 8 | knitr::opts_chunk$set( 9 | collapse = TRUE, 10 | comment = "#>", 11 | fig.path = "man/figures/README-", 12 | out.width = "100%" 13 | ) 14 | ``` 15 | 16 | # queries 17 | 18 | 19 | 20 | 21 | I have a number of parameterized SQL queries that I run regularly. The purpose of this package is to keep canonical versions of those queries with standardized parameterization. It uses the `glue` package to replace parameters. 22 | 23 | ## Usage 24 | 25 | The package reads modified sql files that have metadata stored in yaml format in a comment block at the top. Here's a simple example: start with a file like `[tests/testthat/example_with_defaults.sql](tests/testthat/example_with_defaults.sql)`. 26 | 27 | ```{r} 28 | cat(readr::read_file("tests/testthat/example_with_defaults.sql")) 29 | ``` 30 | 31 | The header is written in `yaml` format. I can import this into R with 32 | 33 | ```{r} 34 | library(queries) 35 | query <- query_load("tests/testthat/example_with_defaults.sql") 36 | ``` 37 | 38 | and then plug in parameters with 39 | 40 | ```{r} 41 | query_substitute(query, metrics = c(total_sales = "SUM(Sales)", avg_sales = "AVG(Sales)")) %>% 42 | cat 43 | ``` 44 | 45 | Notice that vector names are converted to column identifiers. 46 | 47 | Defaults are applied as specified in the yaml header. You can also create a function that completes the query: 48 | 49 | ```{r} 50 | qf <- query_as_function(query) 51 | qf(dimensions = c("Country", "Segment", "Product"), metrics = c(Sales = "SUM(Sales)")) 52 | ``` 53 | 54 | The created function has arguments corresponding to the params as specified in the yaml header. 55 | 56 | ```{r} 57 | print(args(qf)) 58 | ``` 59 | 60 | RStudio will see these and autocomplete them, which is convenient. When I forget a parameter, it tells me. 61 | 62 | ```{r error=TRUE} 63 | qf(dimensions = "Segment") 64 | ``` 65 | 66 | 67 | ## Big picture usage 68 | 69 | ### Query Library 70 | 71 | I use this in two ways. First, I have a library of queries stored in a repository—say `~/projects/path/to/queries`. These are canonical versions of parameterized queries that my team uses. In my .Rprofile, I have a line that sets the default query location to that path 72 | 73 | `options(default_queries_location='~/projects/path/to/queries')` 74 | 75 | Which contains files 76 | 77 | - `sales_by_group.sql` 78 | - `customer_list.sql` 79 | - etc... 80 | 81 | Whenever I need to run one of these queries, I build the query with 82 | 83 | `q <- load_query('sales_by_group')` 84 | 85 | If I don't remember all the parameters, I can view the header with `head`: 86 | ```{r} 87 | head(query) 88 | ``` 89 | 90 | ### R Projects 91 | The other way that I use this is to store bespoke queries in a `/sql` directory in an R project. `load_query` will look for a `/sql` directory first, then check whether the `default_query_location` option is set up. 92 | 93 | ## Installation 94 | 95 | Install with devtools: 96 | 97 | ``` r 98 | devtools::install_github("colin-fraser/queries") 99 | ``` 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # queries 5 | 6 | 7 | 8 | 9 | I have a number of parameterized SQL queries that I run regularly. The 10 | purpose of this package is to keep canonical versions of those queries 11 | with standardized parameterization. It uses the `glue` package to 12 | replace parameters. 13 | 14 | ## Usage 15 | 16 | The package reads modified sql files that have metadata stored in yaml 17 | format in a comment block at the top. Here’s a simple example: start 18 | with a file like 19 | `[tests/testthat/example_with_defaults.sql](tests/testthat/example_with_defaults.sql)`. 20 | 21 | ``` r 22 | cat(readr::read_file("tests/testthat/example_with_defaults.sql")) 23 | #> -- name: Sales by group 24 | #> -- description: Computes metrics grouped by dimensions. If the description is 25 | #> -- really long you can just continue on the next line with a single-space 26 | #> -- indent. 27 | #> -- params: 28 | #> -- - name: dimensions 29 | #> -- description: dimensions to group by 30 | #> -- default: country 31 | #> -- - name: metrics 32 | #> -- description: metrics to aggregate by dimension 33 | #> 34 | #> SELECT {comma_join(metrics, trailing_comma = TRUE)} {comma_join(dimensions)} 35 | #> FROM Customers 36 | #> GROUP BY {comma_join(dimensions)} 37 | ``` 38 | 39 | The header is written in `yaml` format. I can import this into R with 40 | 41 | ``` r 42 | library(queries) 43 | query <- query_load("tests/testthat/example_with_defaults.sql") 44 | ``` 45 | 46 | and then plug in parameters with 47 | 48 | ``` r 49 | query_substitute(query, metrics = c(total_sales = "SUM(Sales)", avg_sales = "AVG(Sales)")) %>% 50 | cat 51 | #> SELECT SUM(Sales) as total_sales, AVG(Sales) as avg_sales, country 52 | #> FROM Customers 53 | #> GROUP BY country 54 | ``` 55 | 56 | Notice that vector names are converted to column identifiers. 57 | 58 | Defaults are applied as specified in the yaml header. You can also 59 | create a function that completes the query: 60 | 61 | ``` r 62 | qf <- query_as_function(query) 63 | qf(dimensions = c("Country", "Segment", "Product"), metrics = c(Sales = "SUM(Sales)")) 64 | #> SELECT SUM(Sales) as Sales, Country, Segment, Product 65 | #> FROM Customers 66 | #> GROUP BY Country, Segment, Product 67 | ``` 68 | 69 | The created function has arguments corresponding to the params as 70 | specified in the yaml header. 71 | 72 | ``` r 73 | print(args(qf)) 74 | #> function (dimensions = "country", metrics = NULL) 75 | #> NULL 76 | ``` 77 | 78 | RStudio will see these and autocomplete them, which is convenient. When 79 | I forget a parameter, it tells me. 80 | 81 | ``` r 82 | qf(dimensions = "Segment") 83 | #> Error in qf(dimensions = "Segment"): Missing params with no default: 84 | #> - metrics 85 | ``` 86 | 87 | ## Big picture usage 88 | 89 | ### Query Library 90 | 91 | I use this in two ways. First, I have a library of queries stored in a 92 | repository—say `~/projects/path/to/queries`. These are canonical 93 | versions of parameterized queries that my team uses. In my .Rprofile, I 94 | have a line that sets the default query location to that path 95 | 96 | `options(default_queries_location='~/projects/path/to/queries')` 97 | 98 | Which contains files 99 | 100 | - `sales_by_group.sql` 101 | - `customer_list.sql` 102 | - etc… 103 | 104 | Whenever I need to run one of these queries, I build the query with 105 | 106 | `q <- load_query('sales_by_group')` 107 | 108 | If I don’t remember all the parameters, I can view the header with 109 | `head`: 110 | 111 | ``` r 112 | head(query) 113 | #> name: Sales by group 114 | #> description: Computes metrics grouped by dimensions. If the description is 115 | #> really long you can just continue on the next line with a single-space 116 | #> indent. 117 | #> params: 118 | #> - name: dimensions 119 | #> description: dimensions to group by 120 | #> default: country 121 | #> - name: metrics 122 | #> description: metrics to aggregate by dimension 123 | ``` 124 | 125 | ### R Projects 126 | 127 | The other way that I use this is to store bespoke queries in a `/sql` 128 | directory in an R project. `load_query` will look for a `/sql` directory 129 | first, then check whether the `default_query_location` option is set up. 130 | 131 | ## Installation 132 | 133 | Install with devtools: 134 | 135 | ``` r 136 | devtools::install_github("colin-fraser/queries") 137 | ``` 138 | -------------------------------------------------------------------------------- /inst/query_templates/basic.sql: -------------------------------------------------------------------------------- 1 | -- name: Basic query 2 | -- params: 3 | -- - name: select_cols 4 | -- description: columns to select 5 | -- - name: table_name 6 | -- description: table to select from 7 | -- - name: where 8 | -- description: a vector of `where` constraints 9 | -- default: "NULL" 10 | -- - name: group_by 11 | -- description: columns to group by 12 | -- default: NULL 13 | -- - name: order_by 14 | -- description: columns to order by 15 | -- default: NULL 16 | -- - name: limit 17 | -- description: number of rows to return 18 | -- default: NULL 19 | 20 | SELECT {comma_join(select_cols)} 21 | FROM {table_name} 22 | {blank_if_null(where, paste("WHERE", and_join(where)))} 23 | {blank_if_null(group_by, paste("GROUP BY", and_join(group_by)))} 24 | {blank_if_null(order_by, paste("ORDER BY", and_join(order_by)))} 25 | {blank_if_null(limit, paste("LIMIT", limit))} 26 | -------------------------------------------------------------------------------- /inst/query_templates/dimensional_rollup.sql: -------------------------------------------------------------------------------- 1 | -- name: Dimensional Rollup 2 | -- description: This query is for rolling up on dimensional tables. 3 | -- params: 4 | -- - name: dimensions 5 | -- description: dimensions to include 6 | -- - name: metrics 7 | -- description: aggregate metrics 8 | -- - name: table_name 9 | -- description: table to select from 10 | -- - name: where 11 | -- description: constraints 12 | -- - name: order_by 13 | -- description: columns to order by 14 | 15 | SELECT 16 | {comma_join(dimensions)}, 17 | {comma_join(metrics)} 18 | FROM {table_name} 19 | {blank_if_null(where, paste("WHERE", and_join(where)))} 20 | GROUP BY {comma_join(dimensions, names_to_as = FALSE)} 21 | ORDER BY {comma_join(order_by, names_to_as = FALSE)} -------------------------------------------------------------------------------- /man/and_join.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sql_helpers.R 3 | \name{and_join} 4 | \alias{and_join} 5 | \title{Join a vector with 'AND'} 6 | \usage{ 7 | and_join(s, leading_and = FALSE, trailing_and = FALSE) 8 | } 9 | \arguments{ 10 | \item{s}{a character vector} 11 | 12 | \item{leading_and}{logical indicating whether 'and' should be pre-pended} 13 | 14 | \item{trailing_and}{logical indicating whether 'and' should be post-pended} 15 | } 16 | \value{ 17 | a string with the elements of s joined by 'AND' 18 | } 19 | \description{ 20 | Join a vector with 'AND' 21 | } 22 | \examples{ 23 | and_join("") 24 | and_join("country = 'US'") 25 | and_join(c("country = 'US'", "type = 1")) 26 | } 27 | -------------------------------------------------------------------------------- /man/blank_if_null.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sql_helpers.R 3 | \name{blank_if_null} 4 | \alias{blank_if_null} 5 | \title{Insert blank character if a variable is null} 6 | \usage{ 7 | blank_if_null(s, output = s) 8 | } 9 | \arguments{ 10 | \item{s}{String to insert, or blank if null} 11 | 12 | \item{output}{What to print if s is not null 13 | 14 | This is useful for specifying e.g. a column list that may or may not 15 | be part of a query.} 16 | } 17 | \value{ 18 | a character vector 19 | } 20 | \description{ 21 | Insert blank character if a variable is null 22 | } 23 | -------------------------------------------------------------------------------- /man/comma_join.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/sql_helpers.R 3 | \name{comma_join} 4 | \alias{comma_join} 5 | \title{Join a character vector together with commas} 6 | \usage{ 7 | comma_join( 8 | s, 9 | leading_comma = FALSE, 10 | trailing_comma = FALSE, 11 | quote = FALSE, 12 | names_to_as = TRUE 13 | ) 14 | } 15 | \arguments{ 16 | \item{s}{a character vector to be joined} 17 | 18 | \item{leading_comma}{should a leading comma be inserted?} 19 | 20 | \item{trailing_comma}{should a trailing comma be inserted?} 21 | 22 | \item{quote}{should the output be quoted? Default false.} 23 | 24 | \item{names_to_as}{if this is true and s is named, the names will 25 | be used as column identifiers. See examples. Defaults to true.} 26 | } 27 | \value{ 28 | a length-1 character vector 29 | } 30 | \description{ 31 | Join a character vector together with commas 32 | } 33 | \examples{ 34 | comma_join(letters) 35 | comma_join(letters, leading_comma = TRUE) 36 | comma_join(letters, quote = TRUE) 37 | 38 | # named input 39 | comma_join(c(avg_sales = "avg(sales)", "country"), names_to_as = TRUE) 40 | 41 | } 42 | -------------------------------------------------------------------------------- /man/env_from_params.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/queries.R 3 | \name{env_from_params} 4 | \alias{env_from_params} 5 | \title{Creates the environment for using glue to replace} 6 | \usage{ 7 | env_from_params(params, ...) 8 | } 9 | \arguments{ 10 | \item{params}{list of parameters} 11 | 12 | \item{...}{filled parameter values} 13 | } 14 | \value{ 15 | an environment for gluing 16 | } 17 | \description{ 18 | Creates the environment for using glue to replace 19 | } 20 | \keyword{internal} 21 | -------------------------------------------------------------------------------- /man/head.query_template.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/queries.R 3 | \name{head.query_template} 4 | \alias{head.query_template} 5 | \title{Print the header of a query} 6 | \usage{ 7 | \method{head}{query_template}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{a query} 11 | 12 | \item{...}{ignored} 13 | } 14 | \value{ 15 | silently returns the head 16 | } 17 | \description{ 18 | Print the header of a query 19 | } 20 | -------------------------------------------------------------------------------- /man/pipe.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/utils-pipe.R 3 | \name{\%>\%} 4 | \alias{\%>\%} 5 | \title{Pipe operator} 6 | \usage{ 7 | lhs \%>\% rhs 8 | } 9 | \description{ 10 | See \code{magrittr::\link[magrittr:pipe]{\%>\%}} for details. 11 | } 12 | \keyword{internal} 13 | -------------------------------------------------------------------------------- /man/print.query_template.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/queries.R 3 | \name{print.query_template} 4 | \alias{print.query_template} 5 | \title{Print query} 6 | \usage{ 7 | \method{print}{query_template}(x, ...) 8 | } 9 | \arguments{ 10 | \item{x}{a query of time query_template} 11 | 12 | \item{...}{ignored} 13 | } 14 | \description{ 15 | Print query 16 | } 17 | -------------------------------------------------------------------------------- /man/qt_basic.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/qt.R 3 | \name{qt_basic} 4 | \alias{qt_basic} 5 | \title{The most basic query pattern} 6 | \usage{ 7 | qt_basic( 8 | select_cols, 9 | table_name, 10 | where = NULL, 11 | group_by = NULL, 12 | order_by = NULL, 13 | limit = NULL 14 | ) 15 | } 16 | \arguments{ 17 | \item{select_cols}{vector of columns to select. 18 | Names will be used as identifiers, if supplied} 19 | 20 | \item{table_name}{table to select from} 21 | 22 | \item{where}{vector of constraints to be joined with 'and'} 23 | 24 | \item{group_by}{vector of columns to group by} 25 | 26 | \item{order_by}{vector of columns to order by} 27 | 28 | \item{limit}{number of rows to return} 29 | } 30 | \value{ 31 | a filled query 32 | } 33 | \description{ 34 | The most basic query pattern 35 | } 36 | \examples{ 37 | qt_basic( 38 | select_cols = c(Date = "saledate", "Country", Sales = "SUM(sales)"), 39 | table = "all_sales", 40 | where = c("country in ('US', 'CA')", "year = 2020"), 41 | group_by = c("saledate", "country"), 42 | order_by = "Date") 43 | } 44 | -------------------------------------------------------------------------------- /man/qt_rollup.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/qt.R 3 | \name{qt_rollup} 4 | \alias{qt_rollup} 5 | \title{Dimensional rollup query pattern} 6 | \usage{ 7 | qt_rollup(dimensions, metrics, table_name, where = NULL, order_by = dimensions) 8 | } 9 | \arguments{ 10 | \item{dimensions}{dimensions to roll up by} 11 | 12 | \item{metrics}{aggregations that will be performed on each dimension} 13 | 14 | \item{table_name}{table to query} 15 | 16 | \item{where}{constraints} 17 | 18 | \item{order_by}{order by} 19 | } 20 | \value{ 21 | a query 22 | } 23 | \description{ 24 | Produces a query that performs a rollup operation on a cube-like table. 25 | } 26 | \examples{ 27 | qt_rollup( 28 | c(order_date = "date", "country"), 29 | c(sales = "sum(sales)"), 30 | 'metrics_table' 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /man/queries_default_location.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/queries.R 3 | \name{queries_default_location} 4 | \alias{queries_default_location} 5 | \title{Returns the default query location} 6 | \usage{ 7 | queries_default_location() 8 | } 9 | \value{ 10 | default query location as a character 11 | } 12 | \description{ 13 | First checks for a queries_location option. 14 | If that's not there, defualts to ~/.queries 15 | } 16 | -------------------------------------------------------------------------------- /man/queries_list.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/queries.R 3 | \name{queries_list} 4 | \alias{queries_list} 5 | \title{List the available queries} 6 | \usage{ 7 | queries_list(location = queries_default_location()) 8 | } 9 | \arguments{ 10 | \item{location}{the directory to look in} 11 | } 12 | \value{ 13 | A tibble of queries 14 | } 15 | \description{ 16 | List the available queries 17 | } 18 | -------------------------------------------------------------------------------- /man/query_as_function.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/queries.R 3 | \name{query_as_function} 4 | \alias{query_as_function} 5 | \title{Turn a parameterized query into a function} 6 | \usage{ 7 | query_as_function(x, include_header = FALSE, append_params = FALSE) 8 | } 9 | \arguments{ 10 | \item{x}{a query loaded with query_load} 11 | 12 | \item{include_header}{include the header in the query output} 13 | 14 | \item{append_params}{append parameter values in the query} 15 | } 16 | \value{ 17 | a function whose arguments are the query parameters 18 | } 19 | \description{ 20 | Turn a parameterized query into a function 21 | } 22 | -------------------------------------------------------------------------------- /man/query_create.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/queries.R 3 | \name{query_create} 4 | \alias{query_create} 5 | \title{Create a parameterized query} 6 | \usage{ 7 | query_create(filename, query_name = "", param_names = NULL, open = TRUE) 8 | } 9 | \arguments{ 10 | \item{filename}{what do you want the query to be called} 11 | 12 | \item{query_name}{the name at the top of the query description} 13 | 14 | \item{param_names}{names of query parameters} 15 | 16 | \item{open}{if true, the saved file opens in rstudio} 17 | } 18 | \value{ 19 | invisibly 20 | } 21 | \description{ 22 | Create a parameterized query 23 | } 24 | -------------------------------------------------------------------------------- /man/query_from_string.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/queries.R 3 | \name{query_from_string} 4 | \alias{query_from_string} 5 | \title{Build a query object from a length-1 character vector} 6 | \usage{ 7 | query_from_string(s, include_header = FALSE) 8 | } 9 | \arguments{ 10 | \item{s}{character containing query} 11 | 12 | \item{include_header}{if true, the header will be included in the sql query} 13 | } 14 | \value{ 15 | convert string into query object 16 | } 17 | \description{ 18 | Build a query object from a length-1 character vector 19 | } 20 | -------------------------------------------------------------------------------- /man/query_load.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/queries.R 3 | \name{query_load} 4 | \alias{query_load} 5 | \title{Load a query from file} 6 | \usage{ 7 | query_load(query_name, query_location = "sql", include_header = FALSE) 8 | } 9 | \arguments{ 10 | \item{query_name}{name of the query. This can also be a path, in which 11 | case query_location is ignored.} 12 | 13 | \item{query_location}{location of the query.} 14 | 15 | \item{include_header}{if true, the header will be included in the sql query} 16 | } 17 | \value{ 18 | loaded query 19 | } 20 | \description{ 21 | Load a query from file 22 | } 23 | -------------------------------------------------------------------------------- /man/query_set_default_location.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/queries.R 3 | \name{query_set_default_location} 4 | \alias{query_set_default_location} 5 | \title{Sets the default queries location} 6 | \usage{ 7 | query_set_default_location(path) 8 | } 9 | \arguments{ 10 | \item{path}{file path to a directory with .sql files} 11 | } 12 | \description{ 13 | Sets the default queries location 14 | } 15 | -------------------------------------------------------------------------------- /man/query_substitute.Rd: -------------------------------------------------------------------------------- 1 | % Generated by roxygen2: do not edit by hand 2 | % Please edit documentation in R/queries.R 3 | \name{query_substitute} 4 | \alias{query_substitute} 5 | \title{Replace parameters in query} 6 | \usage{ 7 | query_substitute( 8 | qry, 9 | ..., 10 | query_location = queries_default_location(), 11 | include_header = FALSE, 12 | append_params = FALSE 13 | ) 14 | } 15 | \arguments{ 16 | \item{qry}{a query object or name of a query} 17 | 18 | \item{...}{parameter values} 19 | 20 | \item{query_location}{the location of the query} 21 | 22 | \item{include_header}{if true, the header will be included in the sql query} 23 | 24 | \item{append_params}{should the parameter values be appended to the 25 | query in a comment?} 26 | } 27 | \value{ 28 | a formatted query 29 | } 30 | \description{ 31 | Replace parameters in query 32 | } 33 | -------------------------------------------------------------------------------- /tests/testthat.R: -------------------------------------------------------------------------------- 1 | library(testthat) 2 | library(queries) 3 | 4 | test_check("queries") 5 | -------------------------------------------------------------------------------- /tests/testthat/_snaps/qt.md: -------------------------------------------------------------------------------- 1 | # qt_basic 2 | 3 | Code 4 | qt_basic(select_cols = c("date", sales = "avg(sales)"), table_name = "all_sales") 5 | Output 6 | SELECT date, avg(sales) as sales 7 | FROM all_sales 8 | 9 | --- 10 | 11 | Code 12 | qt_basic(select_cols = c("date", sales = "avg(sales)"), table_name = "all_sales", 13 | where = c("sales >= 20", "date >= '2022-01-01'"), group_by = "country", 14 | order_by = "country", limit = 200) 15 | Output 16 | SELECT date, avg(sales) as sales 17 | FROM all_sales 18 | WHERE sales >= 20 and 19 | date >= '2022-01-01' 20 | GROUP BY country 21 | ORDER BY country 22 | LIMIT 200 23 | 24 | # qt_rollup 25 | 26 | Code 27 | qt_rollup(c(order_date = "date", "country"), (sales <- "sum(sales)"), 28 | "sales_cube") 29 | Output 30 | SELECT 31 | date as order_date, country, 32 | sum(sales) 33 | FROM sales_cube 34 | 35 | GROUP BY date, country 36 | ORDER BY date, country 37 | 38 | --- 39 | 40 | Code 41 | qt_rollup(c(order_date = "date", "country"), c(sales = "sum(sales)", avg_sales = "avg(sales)"), 42 | "sales_cube", where = c("date>=2020", "country in ('US', 'CA')")) 43 | Output 44 | SELECT 45 | date as order_date, country, 46 | sum(sales) as sales, avg(sales) as avg_sales 47 | FROM sales_cube 48 | WHERE date>=2020 and 49 | country in ('US', 'CA') 50 | GROUP BY date, country 51 | ORDER BY date, country 52 | 53 | -------------------------------------------------------------------------------- /tests/testthat/example_with_defaults.sql: -------------------------------------------------------------------------------- 1 | -- name: Sales by group 2 | -- description: Computes metrics grouped by dimensions. If the description is 3 | -- really long you can just continue on the next line with a single-space 4 | -- indent. 5 | -- params: 6 | -- - name: dimensions 7 | -- description: dimensions to group by 8 | -- default: country 9 | -- - name: metrics 10 | -- description: metrics to aggregate by dimension 11 | 12 | SELECT {comma_join(metrics, trailing_comma = TRUE)} {comma_join(dimensions)} 13 | FROM Customers 14 | GROUP BY {comma_join(dimensions)} 15 | -------------------------------------------------------------------------------- /tests/testthat/test-io.R: -------------------------------------------------------------------------------- 1 | test_that("reading query", { 2 | expect_s3_class(query_load("example_with_defaults", "."), "query_template") 3 | expect_s3_class(query_load("example_with_defaults.sql"), "query_template") 4 | fs::dir_create("sql") 5 | fs::file_copy("example_with_defaults.sql", "sql/example_with_defaults_copy.sql") 6 | expect_s3_class(query_load("example_with_defaults_copy.sql"), "query_template") 7 | fs::dir_delete("sql") 8 | }) 9 | 10 | test_that("query_location works", { 11 | options(default_queries_location = "abc") 12 | expect_equal(queries_default_location(), "abc") 13 | options(default_queries_location = NULL) 14 | }) 15 | -------------------------------------------------------------------------------- /tests/testthat/test-qt.R: -------------------------------------------------------------------------------- 1 | local_edition(3) 2 | test_that("qt_basic", { 3 | expect_error(qt_basic()) 4 | expect_error(qt_basic(select_cols = "A")) 5 | expect_snapshot( 6 | qt_basic( 7 | select_cols = c("date", sales = "avg(sales)"), 8 | table_name = "all_sales" 9 | ) 10 | ) 11 | expect_snapshot( 12 | qt_basic( 13 | select_cols = c("date", sales = "avg(sales)"), 14 | table_name = "all_sales", 15 | where = c("sales >= 20", "date >= '2022-01-01'"), 16 | group_by = "country", order_by = "country", limit = 200 17 | ) 18 | ) 19 | }) 20 | 21 | test_that("qt_rollup", { 22 | expect_snapshot(qt_rollup( 23 | c(order_date = "date", "country"), 24 | (sales <- "sum(sales)"), "sales_cube" 25 | )) 26 | expect_snapshot(qt_rollup(c(order_date = "date", "country"), 27 | c(sales = "sum(sales)", avg_sales = "avg(sales)"), 28 | "sales_cube", 29 | where = c("date>=2020", "country in ('US', 'CA')") 30 | )) 31 | }) 32 | -------------------------------------------------------------------------------- /tests/testthat/test-query_create.R: -------------------------------------------------------------------------------- 1 | test_that("query_create works", { 2 | tf <- fs::file_temp() 3 | query_create(tf, "Hello!", param_names = c("A", "B"), open = F) 4 | readr::write_file("\n{A} {B}", tf, append=TRUE) # write something to the new query 5 | 6 | # file exists 7 | expect_true(fs::file_exists(tf)) 8 | 9 | # query sub works 10 | expect_equal(query_substitute(query_load(tf), A = "a", B = "b"), "a b") 11 | fs::file_delete(tf) 12 | }) 13 | 14 | -------------------------------------------------------------------------------- /tests/testthat/test-querying.R: -------------------------------------------------------------------------------- 1 | remove_linebreaks <- function(x) gsub("[\r\n]", "", x) 2 | 3 | test_that("missing defaults causes errors", { 4 | qry <- query_load("example_with_defaults.sql") 5 | expect_error(query_substitute(qry)) 6 | expect_error(query_substitute(qry, dimensions = 1)) 7 | }) 8 | 9 | test_that("inserting params works", { 10 | expected_q <- "-- name: Sales by group 11 | -- description: Computes metrics grouped by dimensions. If the description is 12 | -- really long you can just continue on the next line with a single-space 13 | -- indent. 14 | -- params: 15 | -- - name: dimensions 16 | -- description: dimensions to group by 17 | -- default: country 18 | -- - name: metrics 19 | -- description: metrics to aggregate by dimension 20 | 21 | SELECT A, B 22 | FROM Customers 23 | GROUP BY B" 24 | expect_equal( 25 | remove_linebreaks(query_substitute("example_with_defaults.sql", 26 | metrics = "A", 27 | dimensions = "B", include_header = TRUE, 28 | append_params = FALSE 29 | )), 30 | remove_linebreaks(expected_q) 31 | ) 32 | }) 33 | 34 | test_that("default params works", { 35 | expected_q <- "-- name: Sales by group 36 | -- description: Computes metrics grouped by dimensions. If the description is 37 | -- really long you can just continue on the next line with a single-space 38 | -- indent. 39 | -- params: 40 | -- - name: dimensions 41 | -- description: dimensions to group by 42 | -- default: country 43 | -- - name: metrics 44 | -- description: metrics to aggregate by dimension 45 | 46 | SELECT A, country 47 | FROM Customers 48 | GROUP BY country" 49 | expect_equal( 50 | remove_linebreaks(query_substitute("example_with_defaults.sql", 51 | metrics = "A", 52 | include_header = TRUE, append_params = FALSE 53 | )), 54 | remove_linebreaks(expected_q) 55 | ) 56 | }) 57 | 58 | test_that("append_params works", { 59 | expected_q <- "-- name: Sales by group 60 | -- description: Computes metrics grouped by dimensions. If the description is 61 | -- really long you can just continue on the next line with a single-space 62 | -- indent. 63 | -- params: 64 | -- - name: dimensions 65 | -- description: dimensions to group by 66 | -- default: country 67 | -- - name: metrics 68 | -- description: metrics to aggregate by dimension 69 | 70 | SELECT A, B 71 | FROM Customers 72 | GROUP BY B 73 | 74 | -- metrics: A 75 | -- dimensions: B" 76 | expect_equal( 77 | remove_linebreaks(query_substitute("example_with_defaults.sql", 78 | metrics = "A", 79 | dimensions = "B", include_header = TRUE, 80 | append_params = TRUE 81 | )), remove_linebreaks(expected_q) 82 | ) 83 | }) 84 | -------------------------------------------------------------------------------- /tests/testthat/test-sql_helpers.R: -------------------------------------------------------------------------------- 1 | test_that("comma_join works", { 2 | expect_equal(comma_join("a"), "a") 3 | expect_equal(comma_join(c("a", "b")), "a, b") 4 | expect_equal( 5 | comma_join(c("a", "b"), leading_comma = TRUE, trailing_comma = TRUE, quote = TRUE), 6 | ", 'a', 'b'," 7 | ) 8 | expect_equal(comma_join(NULL, TRUE, TRUE, TRUE), "") 9 | expect_equal(comma_join(c(avg_sales = 'AVG(sales)', 'country')), 10 | 'AVG(sales) as avg_sales, country') 11 | }) 12 | 13 | test_that("blank_if_null works", { 14 | expect_equal(blank_if_null("a"), "a") 15 | expect_equal(blank_if_null(NULL), "") 16 | }) 17 | 18 | test_that("names_to_as", { 19 | expect_equal(names_to_as(c(a = "A")), "A as a") 20 | expect_equal(names_to_as(c(a = 'A', 'B')), c("A as a", "B")) 21 | }) 22 | 23 | test_that("and_join works", { 24 | expect_equal(and_join(NULL), "") 25 | expect_equal(and_join("A"), "A") 26 | expect_equal(and_join(c("A", "B")), "A and\nB") 27 | expect_equal(and_join(c("A", "B"), leading_and = TRUE), 28 | "and\nA and\nB") 29 | expect_equal(and_join(c("A", "B"), trailing_and = TRUE), 30 | "A and\nB and\n") 31 | expect_equal(and_join(c("A", "B"), leading_and = TRUE, trailing_and = TRUE), 32 | "and\nA and\nB and\n") 33 | }) --------------------------------------------------------------------------------