0

我正在构建一些实用程序函数来简化cast(statement as type)从 R 中更轻松地编写 SQL 查询的过程。

我这样做的方式是通过一个主力函数,as_type该函数由几个一次性函数(例如as_bigint)调用;至关重要的是,我还认为as_type直接调用是一个有效的用例。

代码的基本结构如下:

as_type = function(x, type) {
  if (is.na(match(type, known_types())))
    stop("Attempt to cast to unknown type ", type)
  sprintf('cast(%s as %s)', deparse(substitute(x, parent.frame())), type)
}

as_bigint = function(x) as_type(x, 'bigint')

known_types = function() 'bigint'

# more complicated than this but for works the purposes of this Q
query_encode = glue::glue

具有预期的用途,例如

query_encode("select {as_bigint('1')}")
query_encode("select {as_type('1', 'bigint')}")

as_(实际上,对于其他有效的 SQL 类型,还有几个有效的类型和函数;只有query_encode被导出)

不幸的是,直接调用as_type失败,因为如?substituteh/t Brodie G on Twitter)中所述:

如果 [a component of the parse tree] 不是 [the second argument to substitute]中的绑定符号env,则它不变

query_encode("select {as_bigint('1')}")
# select cast("1" as bigint)
query_encode("select {as_type('1', 'bigint')}")
# select cast(x as bigint)

我已经制定了以下解决方法,但它几乎感觉不到强大:

as_type = function(x, type) {
  if (is.na(match(type, known_types())))
    stop("Attempt to cast to unknown Presto type ", type)
  prev_call = as.character(tail(sys.calls(), 2L)[[1L]][[1L]])
  valid_parent_re = sprintf('^as_(?:%s)$', paste(known_type(), collapse = '|'))
  eval_env = 
    if (grepl(valid_parent_re, prev_call)) parent.frame() else environment()
  sprintf(
    'cast(%s as %s)',
    gsub('"', "'", deparse(substitute(x, eval_env)), fixed = TRUE),
    type
  )
}

即,检查sys.calls()并检查是否as_type从其中一个as_函数调用;将env参数设置substituteparent.frame()好像是,如果不是,则为当前环境。

这暂时有效:

query_encode("select {as_bigint('1')}")
# select cast("1" as bigint)
query_encode("select {as_type('1', 'bigint')}")
# select cast("1" as bigint)

问题是,这是解决这个问题的最佳方式吗?这样说来,这感觉像是一个基于意见的问题,但我的意思是——(1)这种方法是否像乍一看那样脆弱,(2)假设是这样,还有什么更强大的替代方案?

例如,值得注意的is.name(x)FALSEfrom as_type,但我不清楚如何使用它来继续。

4

2 回答 2

1

我相信你可能忽略了胶水变压器。从一个角色到另一个角色再到一个角色是一个你不需要走的大弯路。

Transformers 允许您在评估之前和之后将函数应用于胶水输入和输出,您可以在此处阅读有关它们的更多信息。保持您的格式,我们可以构建:

library(glue)

cast_transformer <- function(regex = "as_(.*?)\\((.*)\\)$", ...) {
  function(text, envir) {
    type <- sub(regex, "\\1", text)
    known_types <- "bigint"
    if(type %in% known_types)
    {
      val <- sub(regex, "\\2", text)
      glue("cast({val} as {type})")
    } else {
      eval(parse(text = text, keep.source = FALSE), envir)
    }
  }
}

glue("select {as_bigint('1')}",.transformer = cast_transformer())
#> select cast('1' as bigint)

因为我们现在正在解析表达式,所以没有 function as_bigint,如果对您方便,您仍然可以保留语法,但没有什么能阻止您将其简化为:

glue("select {bigint: '1'}",.transformer = cast_transformer("(\\D+): (.*)$"))
#> select cast('1' as bigint)

选择regex您喜欢的默认值并定义包装器query_encode <- function(query) glue(query, .transformer = cast_transformer()),您就可以开始了。

于 2019-08-20T16:42:18.930 回答
1

这是我在评论中提到的可能的替代方法:

.as_type <- function(x_sub, type) {
  if(!isTRUE(type %in% known_types()))
    stop("Attempt to cast to unknown type ", type)
  sprintf('cast(%s as %s)', deparse(paste0(x_sub, collapse='\n')), type)
}
as_bigint <- function(x) .as_type(substitute(x), 'bigint')
as_type <- function(x, type) .as_type(substitute(x), type)
known_types <- function() 'bigint'
query_encode <- glue::glue

然后

query_encode("select {as_bigint('1')}")
## select cast("1" as bigint)
query_encode("select {as_type('1', 'bigint')}")
## select cast("1" as bigint)

就你真正想做的事情而言,我认为我们对你正在做的事情感到困惑,我同意这感觉有点脏。这以不同的方式变脏,但不是那么脏,而且看起来它可能会起作用。唯一的脏​​点是需要调用每个函数substitute,但这没什么大不了的。

就脆弱性而言,在您不导出as_函数的范围内,似乎还可以,尽管不导出这些函数确实感觉很奇怪。我会导出它们,但如果你这样做,那么你需要更健壮的检查,因为人们可以重命名函数等。需要注意的一件事是编译器可能会弄乱帧数。确实不应该,但卢克蒂尔尼似乎比我更舒服。

于 2019-07-27T12:51:06.787 回答