fix(search): soft-fail on per-row excluded type and malformed regex name
R-CMD-check / check (push) Successful in 1m37s
R-CMD-check / check (pull_request) Successful in 1m37s

Cross-task review found two edge cases that violated the basket-mode
soft-fail contract:

- Per-row excluded type (e.g. type = c(NA, "special_district")) hit
  .coerce_type()'s abort inside the per-row resolver, killing the
  whole basket call. Now treated as no_match in the sidecar.
- Malformed regex in the substring fallback (e.g. name = "San(Diego")
  propagated DuckDB engine errors. .escape_regex() now backslash-
  escapes meta characters before the regexp_matches call. Utility-
  mode regex behavior is unchanged.

Plus a new public-surface test for the all-no-match case.
This commit is contained in:
2026-04-28 14:16:37 -04:00
parent 24e67791a0
commit efc0bd16b1
3 changed files with 86 additions and 2 deletions
+4 -1
View File
@@ -39,9 +39,12 @@
}
# Convert a type input (integer-like or label) into the canonical label
# string used in the sidecar query_type column.
# string used in the sidecar query_type column. Excluded types (4/5 /
# special_district / school_district) are returned as-is so the sidecar
# records what the user passed without calling .coerce_type() (which aborts).
#' @noRd
.type_to_label <- function(type) {
if (.is_excluded_type(type)) return(as.character(type))
int_type <- .coerce_type(type)
unname(c("0" = "state", "1" = "county", "2" = "city", "3" = "township")[[as.character(int_type)]])
}
+22 -1
View File
@@ -132,6 +132,14 @@ cog_gov_search <- function(name = NULL, state = NULL, type = NULL) {
)
}
#' @noRd
.escape_regex <- function(x) {
# Backslash-escape POSIX regex metacharacters so `name` is treated as a
# literal substring in the DuckDB regexp_matches call (substring fallback
# only; utility-mode intentionally preserves regex behavior).
gsub("([\\^$.|?*+(){}\\[\\]])", "\\\\\\1", x, perl = TRUE)
}
#' @noRd
.is_excluded_type <- function(type) {
excluded <- c("4", "5", "special_district", "school_district")
@@ -245,6 +253,19 @@ cog_gov_search <- function(name = NULL, state = NULL, type = NULL) {
))
}
# Short-circuit: excluded type (4/5 / special_district / school_district)
# -> no_match without SQL, preserving soft-fail contract.
if (!is.na(type) && .is_excluded_type(type)) {
empty <- .empty_xwalk_tibble()
return(list(
status = "no_match",
match_method = NA_character_,
n_candidates = 0L,
row = empty,
candidates = empty
))
}
preds <- character(0)
if (!is.na(state)) {
st_fips <- .coerce_state_to_fips(state)
@@ -282,7 +303,7 @@ cog_gov_search <- function(name = NULL, state = NULL, type = NULL) {
"SELECT * FROM canonical_fips_xwalk",
base_where,
conj,
sprintf("regexp_matches(gov_name, %s, 'i')", .sql_lit_chr(name))
sprintf("regexp_matches(gov_name, %s, 'i')", .sql_lit_chr(.escape_regex(name)))
)
sub <- tibble::as_tibble(DBI::dbGetQuery(con, sub_sql))