From e1064b350b4af211e18557e3fb6730ec416ea68a Mon Sep 17 00:00:00 2001 From: Sam Albers Date: Tue, 23 Jun 2026 08:51:24 -0700 Subject: [PATCH 1/8] create appending mechanism --- R/utils-classes.R | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/R/utils-classes.R b/R/utils-classes.R index f873d54a..395c630c 100644 --- a/R/utils-classes.R +++ b/R/utils-classes.R @@ -328,14 +328,10 @@ filter.bcdc_promise <- function(.data, ...) { ## Change CQL query on the fly if geom is not GEOMETRY current_cql = specify_geom_name(.data$cols_df, current_cql) - # Add cql filter statement to any existing cql filter statements. - # ensure .data$query_list$CQL_FILTER is class sql even if NULL, so - # dispatches on sql class and dbplyr::c.sql method is used - .data$query_list$CQL_FILTER <- c( - dbplyr::sql(.data$query_list$CQL_FILTER), - current_cql, - drop_null = TRUE - ) + # Append the new clause to any existing CQL filter. + combined_cql <- c(unclass(.data$query_list$CQL_FILTER), unclass(current_cql)) + combined_cql <- combined_cql[nzchar(combined_cql)] + .data$query_list$CQL_FILTER <- dbplyr::sql(combined_cql) as.bcdc_promise(list( query_list = .data$query_list, From 9f4beadec5e58d178b2da975f3491e1e26419fb9 Mon Sep 17 00:00:00 2001 From: Sam Albers Date: Tue, 23 Jun 2026 08:51:29 -0700 Subject: [PATCH 2/8] add test --- tests/testthat/test-query-geodata-filter.R | 33 ++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/testthat/test-query-geodata-filter.R b/tests/testthat/test-query-geodata-filter.R index 1f57aeeb..6a33c634 100644 --- a/tests/testthat/test-query-geodata-filter.R +++ b/tests/testthat/test-query-geodata-filter.R @@ -173,6 +173,39 @@ test_that("Different combinations of predicates work", { ) }) +test_that("filter() does not leak a drop_null clause or double-wrap parens", { + # Exercises the CQL append path in filter.bcdc_promise offline. A minimal + # promise is sufficient because filter() only consults cols_df and query_list. + cols_df <- data.frame( + col_name = c("GEOMETRY", "BGC_LABEL"), + remote_col_type = c("gml:GeometryPropertyType", "xsd:string"), + stringsAsFactors = FALSE + ) + promise <- as.bcdc_promise(list( + query_list = list(typeNames = "test", CQL_FILTER = NULL), + cli = NULL, + record = NULL, + cols_df = cols_df + )) + + bbox <- st_as_sfc(st_bbox( + c(xmin = 0, ymin = 0, xmax = 1, ymax = 1), + crs = 3005 + )) + cql <- function(p) as.character(finalize_cql(p$query_list$CQL_FILTER)) + + # A single spatial clause: no TRUE AS "drop_null" artifact, and wrapped in + # exactly one set of parentheses rather than two. + single <- cql(filter(promise, INTERSECTS(bbox))) + expect_false(grepl("drop_null", single)) + expect_false(grepl("^\\(\\(", single)) + + # A chained second clause AND-joins cleanly, still without the artifact. + multi <- cql(filter(filter(promise, INTERSECTS(bbox)), BGC_LABEL != "ZZZ")) + expect_false(grepl("drop_null", multi)) + expect_match(multi, "AND \\(\"BGC_LABEL\" != 'ZZZ'\\)") +}) + test_that("subsetting works locally", { x <- c("a", "b") y <- data.frame(id = x, stringsAsFactors = FALSE) From 603125c2f3c1d9bf047d32282c74ddb16853b413 Mon Sep 17 00:00:00 2001 From: Sam Albers Date: Tue, 23 Jun 2026 08:51:56 -0700 Subject: [PATCH 3/8] update NEWS --- NEWS.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/NEWS.md b/NEWS.md index 86bb1a41..45b3f281 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,10 @@ # bcdata (development version) +* Fixed a bug where `filter()` calls using CQL geometry predicates (such as + `INTERSECTS()`) produced malformed CQL that the server rejected with an HTTP + 400 error. The CQL leaked a spurious `TRUE AS "drop_null"` clause and extra + parentheses following the removal of `c.sql()` in dbplyr 2.6.0 (#368). + # bcdata 0.5.2 * Removed dependency on `leaflet.extras`, using `leaflet::addControl()` instead From 4a6e777f7d0aaae0c6b1fab79c3bd2811388cd70 Mon Sep 17 00:00:00 2001 From: Sam Albers Date: Tue, 23 Jun 2026 09:19:51 -0700 Subject: [PATCH 4/8] ci: trigger R-CMD-check From 1a4a818f7f96490432229ddbfc6a67782cb64af3 Mon Sep 17 00:00:00 2001 From: Sam Albers Date: Tue, 23 Jun 2026 13:57:18 -0700 Subject: [PATCH 5/8] Update R/utils-classes.R Co-authored-by: Andy Teucher --- R/utils-classes.R | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/R/utils-classes.R b/R/utils-classes.R index 395c630c..9fafc6c8 100644 --- a/R/utils-classes.R +++ b/R/utils-classes.R @@ -329,9 +329,10 @@ filter.bcdc_promise <- function(.data, ...) { current_cql = specify_geom_name(.data$cols_df, current_cql) # Append the new clause to any existing CQL filter. - combined_cql <- c(unclass(.data$query_list$CQL_FILTER), unclass(current_cql)) - combined_cql <- combined_cql[nzchar(combined_cql)] - .data$query_list$CQL_FILTER <- dbplyr::sql(combined_cql) + .data$query_list$CQL_FILTER <- dbplyr::sql( + .data$query_list$CQL_FILTER, + current_cql + ) as.bcdc_promise(list( query_list = .data$query_list, From 102952e3f08991dc27592ab5197f236cfda4829b Mon Sep 17 00:00:00 2001 From: Sam Albers Date: Tue, 23 Jun 2026 14:00:40 -0700 Subject: [PATCH 6/8] use snapshot test --- tests/testthat/_snaps/query-geodata-filter.md | 14 +++++++++++ tests/testthat/test-query-geodata-filter.R | 25 +++++++++---------- 2 files changed, 26 insertions(+), 13 deletions(-) create mode 100644 tests/testthat/_snaps/query-geodata-filter.md diff --git a/tests/testthat/_snaps/query-geodata-filter.md b/tests/testthat/_snaps/query-geodata-filter.md new file mode 100644 index 00000000..1fec1e76 --- /dev/null +++ b/tests/testthat/_snaps/query-geodata-filter.md @@ -0,0 +1,14 @@ +# filter() builds clean CQL without a drop_null artifact (#368) + + Code + cql(filter(promise, INTERSECTS(bbox))) + Output + (INTERSECTS(GEOMETRY, POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))) + +--- + + Code + cql(filter(filter(promise, INTERSECTS(bbox)), BGC_LABEL != "ZZZ")) + Output + ((INTERSECTS(GEOMETRY, POLYGON ((0 0, 1 0, 1 1, 0 1, 0 0)))) AND ("BGC_LABEL" != 'ZZZ')) + diff --git a/tests/testthat/test-query-geodata-filter.R b/tests/testthat/test-query-geodata-filter.R index 6a33c634..66480090 100644 --- a/tests/testthat/test-query-geodata-filter.R +++ b/tests/testthat/test-query-geodata-filter.R @@ -173,9 +173,11 @@ test_that("Different combinations of predicates work", { ) }) -test_that("filter() does not leak a drop_null clause or double-wrap parens", { +test_that("filter() builds clean CQL without a drop_null artifact (#368)", { # Exercises the CQL append path in filter.bcdc_promise offline. A minimal # promise is sufficient because filter() only consults cols_df and query_list. + # The snapshots let us confirm at a glance that a single predicate is wrapped + # in exactly one set of parentheses and that no TRUE AS "drop_null" leaks in. cols_df <- data.frame( col_name = c("GEOMETRY", "BGC_LABEL"), remote_col_type = c("gml:GeometryPropertyType", "xsd:string"), @@ -192,18 +194,15 @@ test_that("filter() does not leak a drop_null clause or double-wrap parens", { c(xmin = 0, ymin = 0, xmax = 1, ymax = 1), crs = 3005 )) - cql <- function(p) as.character(finalize_cql(p$query_list$CQL_FILTER)) - - # A single spatial clause: no TRUE AS "drop_null" artifact, and wrapped in - # exactly one set of parentheses rather than two. - single <- cql(filter(promise, INTERSECTS(bbox))) - expect_false(grepl("drop_null", single)) - expect_false(grepl("^\\(\\(", single)) - - # A chained second clause AND-joins cleanly, still without the artifact. - multi <- cql(filter(filter(promise, INTERSECTS(bbox)), BGC_LABEL != "ZZZ")) - expect_false(grepl("drop_null", multi)) - expect_match(multi, "AND \\(\"BGC_LABEL\" != 'ZZZ'\\)") + cql <- function(p) finalize_cql(p$query_list$CQL_FILTER) + + # A single spatial clause. + expect_snapshot(cql(filter(promise, INTERSECTS(bbox)))) + + # A chained second clause AND-joined onto the first. + expect_snapshot( + cql(filter(filter(promise, INTERSECTS(bbox)), BGC_LABEL != "ZZZ")) + ) }) test_that("subsetting works locally", { From a28f176c2b71c43e6eff4a200c7540f2849db942 Mon Sep 17 00:00:00 2001 From: Sam Albers Date: Tue, 23 Jun 2026 14:03:46 -0700 Subject: [PATCH 7/8] document --- DESCRIPTION | 2 +- man/bcdata-package.Rd | 1 + man/bcdc_list_group_records.Rd | 10 +--------- man/bcdc_list_organization_records.Rd | 10 +--------- 4 files changed, 4 insertions(+), 19 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index c5d614f9..1520705c 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -60,7 +60,6 @@ VignetteBuilder: knitr Encoding: UTF-8 Roxygen: list(markdown = TRUE) -RoxygenNote: 7.3.3 Collate: 'bcdata-package.R' 'bcdc-get-citation.R' @@ -86,3 +85,4 @@ Collate: 'zzz.R' Config/testthat/edition: 3 Config/testthat/parallel: true +Config/roxygen2/version: 8.0.0 diff --git a/man/bcdata-package.Rd b/man/bcdata-package.Rd index f68e87c5..17629502 100644 --- a/man/bcdata-package.Rd +++ b/man/bcdata-package.Rd @@ -25,6 +25,7 @@ Useful links: Authors: \itemize{ + \item Andy Teucher \email{andy.teucher@gmail.com} (\href{https://orcid.org/0000-0002-7840-692X}{ORCID}) \item Sam Albers \email{sam.albers@gmail.com} (\href{https://orcid.org/0000-0002-9270-7884}{ORCID}) [contributor] \item Stephanie Hazlitt \email{stephhazlitt@gmail.com} (\href{https://orcid.org/0000-0002-3161-2304}{ORCID}) [contributor] } diff --git a/man/bcdc_list_group_records.Rd b/man/bcdc_list_group_records.Rd index 8ae8e8e7..1fc014aa 100644 --- a/man/bcdc_list_group_records.Rd +++ b/man/bcdc_list_group_records.Rd @@ -1,12 +1,9 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/bcdc_search.R -\name{bcdc_list_groups} -\alias{bcdc_list_groups} +\name{bcdc_list_group_records} \alias{bcdc_list_group_records} \title{Retrieve group information for B.C. Data Catalogue} \usage{ -bcdc_list_groups() - bcdc_list_group_records(group) } \arguments{ @@ -16,11 +13,6 @@ bcdc_list_group_records(group) Returns a tibble of groups or records. Groups can be viewed here: https://catalogue.data.gov.bc.ca/group or accessed directly from R using \code{bcdc_list_groups} } -\section{Functions}{ -\itemize{ -\item \code{bcdc_list_groups()}: - -}} \examples{ \donttest{ try( diff --git a/man/bcdc_list_organization_records.Rd b/man/bcdc_list_organization_records.Rd index 3dd8ab38..96cc865b 100644 --- a/man/bcdc_list_organization_records.Rd +++ b/man/bcdc_list_organization_records.Rd @@ -1,12 +1,9 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/bcdc_search.R -\name{bcdc_list_organizations} -\alias{bcdc_list_organizations} +\name{bcdc_list_organization_records} \alias{bcdc_list_organization_records} \title{Retrieve organization information for B.C. Data Catalogue} \usage{ -bcdc_list_organizations() - bcdc_list_organization_records(organization) } \arguments{ @@ -16,11 +13,6 @@ bcdc_list_organization_records(organization) Returns a tibble of organizations or records. Organizations can be viewed here: https://catalogue.data.gov.bc.ca/organizations or accessed directly from R using \code{bcdc_list_organizations} } -\section{Functions}{ -\itemize{ -\item \code{bcdc_list_organizations()}: - -}} \examples{ \donttest{ try( From a30e65abf41a569b504416590e26bfcf460bc20f Mon Sep 17 00:00:00 2001 From: Sam Albers Date: Tue, 23 Jun 2026 14:28:57 -0700 Subject: [PATCH 8/8] fix to 8.0.0 --- R/bcdc_search.R | 32 +++++++++++++++++++-------- man/bcdc_list_group_records.Rd | 10 ++++++++- man/bcdc_list_organization_records.Rd | 10 ++++++++- 3 files changed, 41 insertions(+), 11 deletions(-) diff --git a/R/bcdc_search.R b/R/bcdc_search.R index 3341ec7b..de83eb50 100644 --- a/R/bcdc_search.R +++ b/R/bcdc_search.R @@ -39,7 +39,9 @@ bcdc_search_facets <- function( "groups" ) ) { - if (!has_internet()) stop("No access to internet", call. = FALSE) # nocov + if (!has_internet()) { + stop("No access to internet", call. = FALSE) + } # nocov facet <- match.arg(facet, several.ok = TRUE) query <- paste0("\"", facet, "\"", collapse = ",") @@ -72,7 +74,7 @@ bcdc_search_facets <- function( } #' @export -#' @describeIn bcdc_list_group_records +#' @describeIn bcdc_list_group_records List the available groups in the B.C. Data Catalogue. #' bcdc_list_groups <- function() bcdc_search_facets("groups") @@ -91,7 +93,9 @@ bcdc_list_groups <- function() bcdc_search_facets("groups") #' } bcdc_list_group_records <- function(group) { - if (!has_internet()) stop("No access to internet", call. = FALSE) # nocov + if (!has_internet()) { + stop("No access to internet", call. = FALSE) + } # nocov cli <- bcdc_catalogue_client("action/group_package_show") @@ -116,7 +120,7 @@ bcdc_list_group_records <- function(group) { } #' @export -#' @describeIn bcdc_list_organization_records +#' @describeIn bcdc_list_organization_records List the available organizations in the B.C. Data Catalogue. #' bcdc_list_organizations <- function() bcdc_search_facets("organization") @@ -135,7 +139,9 @@ bcdc_list_organizations <- function() bcdc_search_facets("organization") #' } bcdc_list_organization_records <- function(organization) { - if (!has_internet()) stop("No access to internet", call. = FALSE) # nocov + if (!has_internet()) { + stop("No access to internet", call. = FALSE) + } # nocov option_package_limit <- getOption("bcdata.max_package_search_limit", 1000) @@ -175,7 +181,9 @@ bcdc_list_organization_records <- function(organization) { #' ) #' } bcdc_list <- function() { - if (!has_internet()) stop("No access to internet", call. = FALSE) # nocov + if (!has_internet()) { + stop("No access to internet", call. = FALSE) + } # nocov l_new_ret <- 1 ret <- character() @@ -240,7 +248,9 @@ bcdc_search <- function( groups = NULL, n = 100 ) { - if (!has_internet()) stop("No access to internet", call. = FALSE) # nocov + if (!has_internet()) { + stop("No access to internet", call. = FALSE) + } # nocov # TODO: allow terms to be passed as a vector, and allow use of | for OR terms <- process_search_terms(...) @@ -345,7 +355,9 @@ bcdc_search <- function( #' ) #' } bcdc_get_record <- function(id) { - if (!has_internet()) stop("No access to internet", call. = FALSE) # nocov + if (!has_internet()) { + stop("No access to internet", call. = FALSE) + } # nocov id <- slug_from_url(id) @@ -443,7 +455,9 @@ as.bcdc_organization <- function(x, description) { #' #' @export bcdc_tidy_resources <- function(record) { - if (!has_internet()) stop("No access to internet", call. = FALSE) # nocov + if (!has_internet()) { + stop("No access to internet", call. = FALSE) + } # nocov UseMethod("bcdc_tidy_resources") } diff --git a/man/bcdc_list_group_records.Rd b/man/bcdc_list_group_records.Rd index 1fc014aa..02f32901 100644 --- a/man/bcdc_list_group_records.Rd +++ b/man/bcdc_list_group_records.Rd @@ -1,9 +1,12 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/bcdc_search.R -\name{bcdc_list_group_records} +\name{bcdc_list_groups} +\alias{bcdc_list_groups} \alias{bcdc_list_group_records} \title{Retrieve group information for B.C. Data Catalogue} \usage{ +bcdc_list_groups() + bcdc_list_group_records(group) } \arguments{ @@ -13,6 +16,11 @@ bcdc_list_group_records(group) Returns a tibble of groups or records. Groups can be viewed here: https://catalogue.data.gov.bc.ca/group or accessed directly from R using \code{bcdc_list_groups} } +\section{Functions}{ +\itemize{ +\item \code{bcdc_list_groups()}: List the available groups in the B.C. Data Catalogue. + +}} \examples{ \donttest{ try( diff --git a/man/bcdc_list_organization_records.Rd b/man/bcdc_list_organization_records.Rd index 96cc865b..04fe1279 100644 --- a/man/bcdc_list_organization_records.Rd +++ b/man/bcdc_list_organization_records.Rd @@ -1,9 +1,12 @@ % Generated by roxygen2: do not edit by hand % Please edit documentation in R/bcdc_search.R -\name{bcdc_list_organization_records} +\name{bcdc_list_organizations} +\alias{bcdc_list_organizations} \alias{bcdc_list_organization_records} \title{Retrieve organization information for B.C. Data Catalogue} \usage{ +bcdc_list_organizations() + bcdc_list_organization_records(organization) } \arguments{ @@ -13,6 +16,11 @@ bcdc_list_organization_records(organization) Returns a tibble of organizations or records. Organizations can be viewed here: https://catalogue.data.gov.bc.ca/organizations or accessed directly from R using \code{bcdc_list_organizations} } +\section{Functions}{ +\itemize{ +\item \code{bcdc_list_organizations()}: List the available organizations in the B.C. Data Catalogue. + +}} \examples{ \donttest{ try(