Skip to content

filter(INTERSECTS(...)) produces malformed CQL #368

Description

@achubaty

filter(INTERSECTS(...)) produces malformed CQL — extra parens + TRUE AS "drop_null" artifacts cause WFS 400 on openmaps.gov.bc.ca

Summary

bcdata 0.5.2 generates CQL that GeoServer rejects with HTTP 400 / <ows:ExceptionReport exceptionCode="NoApplicableCode">, surfacing as the generic "There was an issue sending this WFS request" message from catch_wfs_error(). Affects any WFS query built via bcdc_query_geodata() |> filter(...).

Two distinct problems in the generated CQL:

1. Extra outer parentheses. A single-clause filter produces:

CQL_FILTER=((INTERSECTS(GEOMETRY, POLYGON ((...))))

instead of:

CQL_FILTER=INTERSECTS(GEOMETRY, POLYGON ((...)))

The server's CQL parser rejects the wrapped form.

2. TRUE AS "drop_null" SQL artifacts. Multi-clause filters leak a SQL-side TRUE AS "drop_null" placeholder into the CQL (CQL has no AS aliasing — that's SQL):

CQL_FILTER=((INTERSECTS(...)) AND TRUE AS "drop_null" AND ("BGC_LABEL" != 'ZZZ') AND TRUE AS "drop_null")

Minimal reproducer

library(bcdata); library(sf); library(dplyr)

## Any sf polygon over BC works; using a small bbox here for brevity.
geom <- st_as_sfc(st_bbox(c(xmin = 800000, ymin = 1200000, xmax = 850000, ymax = 1250000),
                          crs = st_crs(3005)))

## (1) Single filter clause -- fails because of extra outer parens
bcdc_query_geodata("f358a53b-ffde-4830-a325-a5a03ff672c3") |>
  filter(INTERSECTS(geom)) |>
  collect()
##> Error: There was an issue sending this WFS request

## (2) Same query via direct httr POST (no extra parens) returns 200:
httr::POST("https://openmaps.gov.bc.ca/geo/pub/wfs",
  body = list(
    SERVICE = "WFS", VERSION = "2.0.0", REQUEST = "GetFeature",
    outputFormat = "application/json",
    typeNames = "WHSE_FOREST_VEGETATION.BEC_BIOGEOCLIMATIC_POLY",
    SRSNAME = "EPSG:3005",
    CQL_FILTER = sprintf("INTERSECTS(GEOMETRY, %s)", st_as_text(geom[[1]]))
  ),
  encode = "form"
) |> httr::status_code()
##> [1] 200

Server response (decoded)

catch_wfs_error() short-circuits on status ≥ 300 before parsing the response body, hiding the underlying diagnostic. Tracing bcdata:::bcdc_number_wfs_records() exposes the actual exception:

<?xml version="1.0" encoding="UTF-8"?>
<ows:ExceptionReport ... version="2.0.0">
  <ows:Exception exceptionCode="NoApplicableCode">
    <ows:ExceptionText></ows:ExceptionText>
  </ows:Exception>
</ows:ExceptionReport>

The <ExceptionText> is empty, but the server's pre-exception log includes a Parsing : ((INTERSECTS(... line that pinpoints the outer wrapping as the trigger.

Suggested fix directions

  • Drop the outermost (...) wrap when there's only one CQL clause (or unconditionally — single-clause expressions don't need it).
  • In the dplyr → CQL translator, strip columns of the form TRUE AS "drop_null" before serialising. These look like dbplyr's "guarantee non-null projection" placeholders that aren't valid CQL.

Suggested UX improvement

catch_wfs_error() could surface the response body (or at least an <ows:ExceptionText> extract) in its error message — the current generic "There was an issue sending this WFS request" made the actual server diagnostic impossible to recover without trace()-ing internals.

Environment

bcdata 0.5.2  (CRAN, current)
R 4.6.0
Ubuntu 24.04 noble / 25.04 resolute
WFS endpoint: https://openmaps.gov.bc.ca/geo/pub/wfs  (GeoServer-based)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions