Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OdbcResult: Ability to explicitely describe parameters #863

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

detule
Copy link
Collaborator

@detule detule commented Nov 30, 2024

Hi @simonpcouch @hadley

This is a draft/RFC of a feature that would allow (power) users to explicitly describe parameter attributes when submitting parametrized queries.

The details are in #518 and this PR would address the issue there. In summary:

  • We do a very good job of inferring the parameter metadata, but at the end of the day it's only an informed guess.
  • In some cases the user may wish to override those inferred values. Linked issue above is one of those.
  • Offer them the ability to do that. This type of feature is not uncommon in other ODBC programming interfaces.

Some notes about the PR:

  • Added an ODBC_TYPE helper method that would help with the user not having to guess what integer corresponds to SQL_WVARCHAR, for example.
  • The example is in ?OdbcResult.
  • I have a few items below as TODOs, including offering the usage of this parameter more broadly.

TODO:

  • Rename ODBC_TYPE to SQL_TYPE
  • Permeate parameter_description to dbSendQuery, dbGetQuery
  • Add unit tests

@detule detule marked this pull request as draft December 7, 2024 02:14
@detule detule requested review from hadley and simonpcouch December 7, 2024 02:14
Copy link
Collaborator

@simonpcouch simonpcouch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems very much useful for power users! I don't have much to offer in terms of critique on the src/ side, but left some notes on the R interface.

Comment on lines +140 to +153
#' @examples
#' \dontrun{
#' library(odbc)
#' # Writing UNICODE into a VARCHAR
#' # column with SQL server
#' DBI::dbRemoveTable(conn, "#tmp")
#' dbExecute(conn, "CREATE TABLE #tmp (col1 VARCHAR(50) COLLATE Latin1_General_100_CI_AI_SC_UTF8);")
#' res <- dbSendQuery(conn, "INSERT INTO #tmp SELECT ? COLLATE Latin1_General_100_CI_AI_SC_UTF8")
#' description <- data.frame(param_index = 1, data_type = ODBC_TYPE("WVARCHAR"),
#' column_size = 5, decimal_digits = NA_integer_)
#' dbBind(res, params = list("\u2915"), param_description = description)
#' dbClearResult(res)
#' DBI::dbReadTable(conn, "#tmp")
#' }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#' @examples
#' \dontrun{
#' library(odbc)
#' # Writing UNICODE into a VARCHAR
#' # column with SQL server
#' DBI::dbRemoveTable(conn, "#tmp")
#' dbExecute(conn, "CREATE TABLE #tmp (col1 VARCHAR(50) COLLATE Latin1_General_100_CI_AI_SC_UTF8);")
#' res <- dbSendQuery(conn, "INSERT INTO #tmp SELECT ? COLLATE Latin1_General_100_CI_AI_SC_UTF8")
#' description <- data.frame(param_index = 1, data_type = ODBC_TYPE("WVARCHAR"),
#' column_size = 5, decimal_digits = NA_integer_)
#' dbBind(res, params = list("\u2915"), param_description = description)
#' dbClearResult(res)
#' DBI::dbReadTable(conn, "#tmp")
#' }
#' @examplesIf FALSE
#' # Writing UNICODE into a VARCHAR column with SQL server
#' dbRemoveTable(conn, "#tmp")
#'
#' dbExecute(
#' conn,
#' "CREATE TABLE #tmp (col1 VARCHAR(50) COLLATE Latin1_General_100_CI_AI_SC_UTF8);"
#' )
#'
#' res <- dbSendQuery(
#' conn,
#' "INSERT INTO #tmp SELECT ? COLLATE Latin1_General_100_CI_AI_SC_UTF8"
#' )
#'
#' description <- data.frame(
#' param_index = 1,
#' data_type = ODBC_TYPE("WVARCHAR"),
#' column_size = 5,
#' decimal_digits = NA_integer_
#' )
#'
#' dbBind(res, params = list("\u2915"), param_description = description)
#' dbClearResult(res)
#'
#' dbReadTable(conn, "#tmp")

Some style edits I'd make:

  • examples with \dontrun{} -> examplesIf FALSE so that users don't see ## Not run tags and CRAN definitely won't try to run the examples
  • I believe the DBI:: namespacing here may not be needed?
  • Line-breaking more liberally via codegrip

Comment on lines +129 to +136
#' @param param_description A data.frame with per-parameter attribute
#' overrides. Argument is optional; if used it must have columns:
#' * param_index Index of parameter in query ( beginning with 1 ).
#' * data_type Integer corresponding to the parameter SQL Data Type.
#' See \code{\link{ODBC_TYPE}}.
#' * column_size Size of parameter.
#' * decimal_digits Either precision or the scale of the parameter
#' depending on type.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
#' @param param_description A data.frame with per-parameter attribute
#' overrides. Argument is optional; if used it must have columns:
#' * param_index Index of parameter in query ( beginning with 1 ).
#' * data_type Integer corresponding to the parameter SQL Data Type.
#' See \code{\link{ODBC_TYPE}}.
#' * column_size Size of parameter.
#' * decimal_digits Either precision or the scale of the parameter
#' depending on type.
#' @param param_description A data frame containing per-parameter attribute overrides.
#' Optional argument that, if provided, must contain the following columns:
#' \describe{
#' \item{param_index}{Index of parameter in query (beginning with 1).}
#' \item{data_type}{Integer corresponding to the parameter SQL Data Type.
#' See [ODBC_TYPE()].
#' \item{column_size}{Size of parameter.}
#' \item{decimal_digits}{Either precision or the scale of the parameter
#' depending on type.}
#' }
  • Transitions to \describe for data frame columns
  • Links to ODBC_TYPE() with roxygen shorthand
  • Refers to "data frame" rather than data.frame since tibbles and other data.frame subclasses are fine

#' @rdname OdbcResult
#' @inheritParams DBI::dbBind
#' @inheritParams DBI-tables
#' @export
setMethod("dbBind", "OdbcResult",
function(res, params, ..., batch_rows = getOption("odbc.batch_rows", NA)) {
function(res, params, ...,
param_description = data.frame(),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
param_description = data.frame(),
param_description = NULL,

NULL indicates a bit more clearly to me that "this is the default that won't actually be used if you don't supply anything"

params <- as.list(params)
if (length(params) == 0) {
return(invisible(res))
}

if (nrow(param_description)) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (nrow(param_description)) {
if (!is.null(param_description)) {

Aligning with above.

Comment on lines +168 to +173
if (!all(c("param_index", "data_type", "column_size", "decimal_digits")
%in% colnames(param_description))) {
cli::cli_abort(
"param_description data.frame does not have necessary columns."
)
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if (!all(c("param_index", "data_type", "column_size", "decimal_digits")
%in% colnames(param_description))) {
cli::cli_abort(
"param_description data.frame does not have necessary columns."
)
}
check_data_frame(param_description)
needed_columns <- c("param_index", "data_type", "column_size", "decimal_digits")
if (!all(needed_columns %in% colnames(param_description))) {
cli::cli_abort(
"{.arg param_description} must have columns {.field {needed_columns}}, but
doesn't have column{?s}
{.field {.or {needed_columns[needed_columns %in% colnames(param_description)]}}}."
)
}

check_data_frame() will first ensure that the thing is a data frame and provide an informative message if not before we do the finer-grain check for needed columns.

#' ODBC_TYPE("LONGVARCHAR")
#' }
#' @export
ODBC_TYPE <- function(type) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think, just to situate more comfily in this package's namespace, this may be better formatted as:

Suggested change
ODBC_TYPE <- function(type) {
odbcType <- function(type) {

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants