diff --git a/pkg-r/NEWS.md b/pkg-r/NEWS.md index e5f5213f..02d1e891 100644 --- a/pkg-r/NEWS.md +++ b/pkg-r/NEWS.md @@ -1,5 +1,7 @@ # commons (development version) +* A Snowflake semantic view whose base tables are named with quoted identifiers, such as `"orders"`, now scopes to those tables. The quotes were kept as part of each name, so `data_source()` could not find them (#446). + * A definition that adds `NULL` to a date, such as `created + NULL`, now compiles to SQL that Snowflake accepts. It compiles to a typed `NULL` timestamp (#441). * A definition that divides by a compound expression, such as `amount / (ratio * 2)`, now compiles to the correct Snowflake and Databricks SQL. The parentheses around the divisor were dropped, so the query divided by `ratio` alone (#439). diff --git a/pkg-r/R/catalog-snowflake.R b/pkg-r/R/catalog-snowflake.R index a52b89f0..3f2a44bf 100644 --- a/pkg-r/R/catalog-snowflake.R +++ b/pkg-r/R/catalog-snowflake.R @@ -680,9 +680,12 @@ snowflake_semantic_dependencies <- function(id, specification) { complete <- TRUE for (table in semantic_spec_entries(specification$tables)) { base <- table$base_table %||% list() - table_name <- base$table - catalog <- base$database %||% base$catalog %||% model[["catalog"]] - schema <- base$schema %||% model[["schema"]] + # A name the YAML gives follows SQL identifier rules; one it leaves out + # comes from the view's own id, which is already exact. + table_name <- snowflake_yaml_identifier(base$table) + catalog <- snowflake_yaml_identifier(base$database %||% base$catalog) %||% + model[["catalog"]] + schema <- snowflake_yaml_identifier(base$schema) %||% model[["schema"]] if ( !rlang::is_string(catalog) || !rlang::is_string(schema) || @@ -706,6 +709,19 @@ snowflake_semantic_dependencies <- function(id, specification) { list(ids = ids[!duplicated(keys)], complete = complete && length(ids) > 0L) } +# A name in a semantic view's YAML follows SQL identifier rules: a quoted one +# is exact, with a doubled quote standing for one, and an unquoted one folds to +# upper case. +snowflake_yaml_identifier <- function(name) { + if (!rlang::is_string(name)) { + return(NULL) + } + if (grepl('^".*"$', name)) { + return(gsub('""', '"', substr(name, 2L, nchar(name) - 1L), fixed = TRUE)) + } + toupper(name) +} + snowflake_semantic_context <- function( specification, dimensions, diff --git a/pkg-r/tests/testthat/test-catalog-snowflake.R b/pkg-r/tests/testthat/test-catalog-snowflake.R index cdb8456f..fb2184be 100644 --- a/pkg-r/tests/testthat/test-catalog-snowflake.R +++ b/pkg-r/tests/testthat/test-catalog-snowflake.R @@ -93,6 +93,36 @@ test_that("Snowflake semantic YAML imports only public dimensions and metrics", expect_equal(model$metrics[[1]]$parent, "orders") }) +test_that("Snowflake semantic dependencies read base tables as SQL identifiers", { + # The YAML keeps a quoted name's quotes, and folds an unquoted one to upper + # case the way the SQL that created it would. + specification <- list(tables = list( + list(base_table = list( + database = '"Commons"', + schema = "live", + table = '"order ""lines"""' + )) + )) + id <- DBI::Id(catalog = "COMMONS", schema = "LIVE", table = "MODEL") + + model <- snowflake_semantic_model_from_spec(id, specification) + + expect_identical( + model$dependencies[[1]], + DBI::Id(catalog = "Commons", schema = "LIVE", table = 'order "lines"') + ) + + # A namespace the YAML leaves out is the view's own, which is exact. + inherited <- snowflake_semantic_model_from_spec( + DBI::Id(catalog = "MyDb", schema = "MySchema", table = "MODEL"), + list(tables = list(list(base_table = list(table = "orders")))) + ) + expect_identical( + inherited$dependencies[[1]], + DBI::Id(catalog = "MyDb", schema = "MySchema", table = "ORDERS") + ) +}) + test_that("Snowflake semantic scope retains dependencies and public context", { specification <- list( custom_instructions = "Use booked revenue for finance questions.",