Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Package: fresh
Title: Freshwater Referenced Spatial Hydrology
Version: 0.31.0
Version: 0.32.0
Authors@R: c(
person("Allan", "Irvine", , "al@newgraphenvironment.com", role = c("aut", "cre"),
comment = c(ORCID = "0000-0002-3495-2128")),
Expand Down
1 change: 1 addition & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export(frs_waterbody_network)
export(frs_watershed_at_measure)
export(frs_watershed_split)
export(frs_wetland_fetch)
export(frs_wsg_drainage)
export(frs_wsg_species)
importFrom(DBI,dbConnect)
importFrom(DBI,dbDisconnect)
Expand Down
11 changes: 11 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
# fresh 0.32.0

Closes [#211](https://github.com/NewGraphEnvironment/fresh/issues/211). Adds `frs_wsg_drainage()` — a pure FWA-topology primitive that returns the drainage closure of a focal set of watershed groups (focal + every WSG they flow through), ordered downstream-first by outlet ltree depth.

- Promotes the inline closure query from `NewGraphEnvironment/link@v0.40.5` `data-raw/study_area_wsgs.R` into a reusable, tested function. Pure FWA topology — no species / bundle / overrides knowledge — so other fresh consumers (vignettes, ad-hoc R sessions, future provincial drivers) can use it without going through link.
- Closure predicate `f.outlet <@ w.outlet` against `public.wsg_outlet` (ltree-based FWA WSG outlet table). Order `nlevel(outlet) ASC, wsg ASC` — most downstream WSGs first, alphabetical within a depth. Running per-WSG work in this order persists downstream barriers before upstream WSGs read them, which is what link's study-area runner relies on for cross-WSG access flags to settle within a single pass.
- Signature `frs_wsg_drainage(conn, watershed_group_code, table = "public.wsg_outlet")` follows fresh vocabulary (`watershed_group_code`, matches `frs_wsg_species` + `frs_stream_fetch`). Table identifier validated via `.frs_validate_identifier`; focal codes upper-cased internally and quoted via `DBI::dbQuoteLiteral` (no injection surface). Unmatched focal codes warn rather than silently drop; all-unmatched errors.
- First consumer: `link::lnk_wsg_resolve` ([NewGraphEnvironment/link#207](https://github.com/NewGraphEnvironment/link/issues/207)) — the link wrapper composes this primitive with the bundle's species-presence filter (#157) to drive `data-raw/study_area_wsgs.R` (and replaces the inline query there).
- New `@family wsg` family. `frs_wsg_species` retag deferred to a separate concern.
- 11 tests / 14 expectations: 6 arg-validation (no DB), 5 live-DB (gated on `PG_DB_SHARE`) covering the 15-WSG PARS+BULK regression baseline, focal-order invariance, case-folding, unmatched-focal warning, and all-unmatched error. Live-validated against fwapg.

# fresh 0.31.0

Closes [#207](https://github.com/NewGraphEnvironment/fresh/issues/207). Adds `frs_candidates_pick()` — fourth primitive in the point-handling family (alongside `frs_point_snap`, `frs_network_features`, `frs_point_match`). Given a candidates table where multiple rows can share the same key value, optionally compute a per-row score from a caller-supplied SQL expression, optionally filter via a caller-supplied WHERE clause, then keep one row per key via `DISTINCT ON (col_key) ORDER BY ...`.
Expand Down
91 changes: 91 additions & 0 deletions R/frs_wsg_drainage.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
#' WSG Drainage Closure (FWA Topology)
#'
#' Given a focal set of FWA watershed groups, return the **drainage closure**
#' — every WSG that any focal WSG drains through — ordered downstream-first
#' by outlet ltree depth (`nlevel(outlet) ASC`). Pure FWA topology; no
#' species or bundle knowledge.
#'
#' Sources the FWA WSG outlet table (default `public.wsg_outlet` in fwapg).
#' The closure predicate is `f.outlet <@ w.outlet` — i.e. every WSG whose
#' outlet ltree is an ancestor of (or equal to) any focal WSG's outlet.
#'
#' Order: `depth ASC, wsg ASC` — so the most downstream WSGs come first and
#' ties within a depth are alphabetical. Running per-WSG work in this order
#' persists downstream barriers before upstream WSGs read them, which makes
#' cross-WSG accessibility flags settle correctly within a single pass.
#'
#' @param conn A [DBI::DBIConnection-class] (e.g. from [frs_db_conn()]).
#' @param watershed_group_code Character vector of focal WSG codes (e.g.
#' `c("BULK", "MORR")`). Must be non-empty and free of `NA`. Codes are
#' upper-cased internally. Focal codes that don't exist in `table`
#' trigger a warning and are dropped from the result; if no codes match,
#' the function errors.
#' @param table Character scalar. Fully-qualified table name holding the
#' WSG outlet ltree topology. Default `"public.wsg_outlet"`.
#'
#' @return Character vector of WSG codes — focal plus every WSG they flow
#' through — ordered downstream-first.
#'
#' @family wsg
#'
#' @export
#'
#' @examples
#' \dontrun{
#' conn <- frs_db_conn()
#'
#' # Skeena + Peace focal — returns the 15-WSG drainage closure DS-first
#' frs_wsg_drainage(conn, c("PARS", "BULK"))
#' #> [1] "KISP" "KLUM" "LKEL" "LSKE" "MSKE" "USKE" "BULK" "FINA"
#' #> "LBTN" "LPCE" "MORR" "PARA" "PCEA" "UPCE" "PARS"
#'
#' DBI::dbDisconnect(conn)
#' }
frs_wsg_drainage <- function(
conn,
watershed_group_code,
table = "public.wsg_outlet"
) {
stopifnot(
is.character(watershed_group_code),
length(watershed_group_code) > 0,
!anyNA(watershed_group_code),
all(nzchar(watershed_group_code)),
is.character(table),
length(table) == 1L
)
.frs_validate_identifier(table, "table")
focal <- toupper(watershed_group_code)

focal_lit <- paste(
DBI::dbQuoteLiteral(conn, focal),
collapse = ", "
)
sql <- sprintf(
paste0(
"SELECT DISTINCT w.wsg, nlevel(w.outlet) AS depth\n",
"FROM %s w\n",
"JOIN %s f ON f.wsg IN (%s)\n",
"WHERE f.outlet <@ w.outlet\n",
"ORDER BY depth ASC, w.wsg ASC"
),
table, table, focal_lit
)
res <- DBI::dbGetQuery(conn, sql)
if (nrow(res) == 0L) {
stop(
"No drainage closure found — are the focal WSG codes present in `",
table, "`?",
call. = FALSE
)
}
missing <- setdiff(focal, res$wsg)
if (length(missing) > 0L) {
warning(
"Focal WSG code(s) not found in `", table, "` (dropped from closure): ",
paste(missing, collapse = ", "),
call. = FALSE
)
}
res$wsg
}
53 changes: 53 additions & 0 deletions man/frs_wsg_drainage.Rd

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Empty file.
11 changes: 11 additions & 0 deletions planning/archive/2026-05-issue-211-frs-wsg-drainage/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Issue #211 — frs_wsg_drainage

## Outcome

Added `frs_wsg_drainage(conn, watershed_group_code, table = "public.wsg_outlet")` as a new exported primitive in the `@family wsg` family. Given a focal set of FWA watershed groups, returns the drainage closure (focal + every WSG they drain through) ordered downstream-first by `nlevel(outlet) ASC`. Pure FWA topology — no bundle / species / overrides knowledge. SQL ported verbatim from the validated query in `NewGraphEnvironment/link@v0.40.5 data-raw/study_area_wsgs.R:44-50`.

Beyond the literal SQL port, the function adds: scalar `table` arg check (`.frs_validate_identifier` is vectorized internally so a vector value would silently slip through and produce malformed SQL — caught by code-check Round 1); internal upper-casing of focal codes; `warning()` on unmatched focals so `c("BULK","TYPO")` no longer returns silently partial closure (also Round 1); test assertion tightened to match the validator's actual error message rather than a generic `class = "error"` (Round 2). 11 test_that blocks / 14 expectations (6 arg-validation + 5 live-DB gated on `PG_DB_SHARE`). Released as **v0.32.0**.

First consumer: `link::lnk_wsg_resolve` ([NewGraphEnvironment/link#207](https://github.com/NewGraphEnvironment/link/issues/207)) — the link wrapper composes this primitive with the bundle's species-presence filter to drive `data-raw/study_area_wsgs.R` (replacing the inline query there).

Closed by: commits `808458e` (Phase 1 verification), `157d03e` (function), `e9fa8b9` (tests), `b686598` (Release v0.32.0). PR forthcoming via `/gh-pr-push`.
97 changes: 97 additions & 0 deletions planning/archive/2026-05-issue-211-frs-wsg-drainage/findings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Findings — frs_wsg_drainage (#211)

## Issue context

### Problem

`link` currently computes WSG drainage closure (every WSG a focal set drains through, downstream-first) inline in `data-raw/study_area_wsgs.R` — see `NewGraphEnvironment/link@v0.40.5`. The query reads `public.wsg_outlet`, joins focal → closure via `f.outlet <@ w.outlet`, sorts by `nlevel(outlet) ASC`.

This is FWA-topology — it belongs in `fresh`, not `link`. No bundle/species/overrides knowledge involved; pure network shape.

### Proposed

```r
frs_wsg_drainage(conn, wsgs)
```

- `wsgs`: character vector of WSG codes (the focal set)
- Returns: character vector covering the drainage closure (focal + every WSG they flow through), ordered downstream-first (`nlevel(outlet) ASC`)
- Pure FWA topology — no bundle / species / overrides logic

### Where it lives

`R/frs_wsg_drainage.R`. Sits alongside `frs_wsg_species()` as part of a `frs_wsg_*` family — the WSG-topology surface of fresh.

### Why fresh, not link

`wsg_outlet` is FWA infrastructure. Drainage closure is a network-shape question with no link/bundle knowledge. Other fresh consumers (vignettes, ad-hoc analyses, future provincial drivers in other packages) want this without going through link.

### Acceptance

- [ ] `frs_wsg_drainage(conn, c("PARS","BULK"))` returns the 15-WSG Skeena+Peace closure: `KISP, KLUM, LKEL, LSKE, MSKE, USKE, BULK, FINA, LBTN, LPCE, MORR, PARA, PCEA, UPCE, PARS` — matches `NewGraphEnvironment/link@v0.40.5` `study_area_wsgs.R` output
- [ ] DS-first ordering preserved
- [ ] Closure invariant to focal ordering
- [ ] Runnable `@example`
- [ ] Test against a small live focal set

### Replaces

Inline closure query in `NewGraphEnvironment/link@v0.40.5` `data-raw/study_area_wsgs.R` (lines 44-50 computing `wsg_outlet`-based closure).

### Composes with

`link::lnk_wsg_resolve` (`NewGraphEnvironment/link#207`) — the link wrapper adds the bundle's species-presence filter (#157).

### Naming considered

`frs_wsg_drainage` (chosen) is `frs_wsg_*`-family consistent. `wsg` is FWA-specific terminology — honest to fresh's current FWA-only scope. If fresh ever grows a second network (NHD/HUC), a `network = "fwa"` param can be introduced then (YAGNI today).

## Codebase exploration

### Family conventions

- One existing `frs_wsg_*` function: `R/frs_wsg_species.R:39` — `frs_wsg_species(watershed_group_code)`. Loads a bundled CSV; no DB connection. Validation via `stopifnot()`. Returns data frame.
- `@family` tags in use: `parameters` (frs_wsg_species), `fetch` (frs_stream_fetch), `traverse` (frs_network_downstream), `index` (frs_point_snap), `network` (frs_network_features, frs_point_match, frs_candidates_pick), `database` (frs_db_conn). No existing `wsg` family.

### DB-issuing function pattern (frs_stream_fetch)

`R/frs_stream_fetch.R:42-84` — signature is `function(conn, <filters>, <controls>, table = "...", cols = c(...), limit = NULL)`. Validates identifiers via `.frs_validate_identifier()`. Composes SQL with `paste0` + `sprintf`. Calls `frs_db_query(conn, sql)` to execute.

### SQL idiom

- `sprintf()` + `paste0()` dominate fresh; no `glue`. Helpers in `R/utils.R`: `.frs_quote_string` (line 39, manual escape), `.frs_validate_identifier` (line 54), `.frs_sql_num` (line 71).
- `DBI::dbQuoteLiteral` is the safest path for user-supplied string vectors — link uses it in `study_area_wsgs.R:44`.

### Test pattern

DB-gated tests skip on missing `PG_DB_SHARE`:
```r
skip_if(Sys.getenv("PG_DB_SHARE") == "", "PG_DB_SHARE not set")
conn <- frs_db_conn()
on.exit(DBI::dbDisconnect(conn))
```
Helper: `frs_db_conn()` (`R/frs_db_conn.R:24-44`) reads `PG_*_SHARE` env vars. Mocking via `mockery::local_mocked_bindings()` to capture SQL without a DB.

### Example convention

`\dontrun{}` for DB-requiring examples (per `R/frs_stream_fetch.R:30-41`); inline for CSV-loaded functions (per `R/frs_wsg_species.R:29-38`).

### Existing `wsg_outlet` usage

Zero references in fresh codebase (R + SQL + tests). This function is the first consumer.

### NEWS.md style

`# fresh X.Y.Z` section header, lead with bold one-liner + issue link, then bullets. Reference: `NEWS.md:1-10` (v0.31.0).

### Source SQL to port (from `link@v0.40.5 data-raw/study_area_wsgs.R:44-50`)

```sql
SELECT DISTINCT w.wsg, nlevel(w.outlet) AS depth
FROM public.wsg_outlet w
JOIN public.wsg_outlet f ON f.wsg IN (<focal-lit>)
WHERE f.outlet <@ w.outlet
ORDER BY depth ASC, w.wsg ASC
```

Column name `wsg` to be confirmed in Phase 1.
12 changes: 12 additions & 0 deletions planning/archive/2026-05-issue-211-frs-wsg-drainage/progress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Progress — frs_wsg_drainage (#211)

## Session 2026-05-27

- Plan-mode exploration — phases approved by user
- Created branch `211-frs-wsg-drainage-fwa-wsg-drainage-closur` off main (off `90af475`)
- Scaffolded PWF baseline from issue #211 with approved phases (commit `1dc1145`)
- **Phase 1 complete:** `public.wsg_outlet` column confirmed as `wsg varchar(4)` (with `outlet ltree`, `lvl integer`); regression baseline run — PARS+BULK → 15 WSGs, exact match: `KISP, KLUM, LKEL, LSKE, MSKE, USKE, BULK, FINA, LBTN, LPCE, MORR, PARA, PCEA, UPCE, PARS` (DS-first, depths 1×6, 2×8, 3×1). Commit `808458e`.
- **Phase 2 complete:** Wrote `R/frs_wsg_drainage.R` (signature `frs_wsg_drainage(conn, watershed_group_code, table = "public.wsg_outlet")`); `devtools::document()` regenerated NAMESPACE + Rd; live-validated end-to-end (happy path: 15-WSG exact match; lowercase input normalized; TYPO triggers warning + drops from result; vector `table` rejected). `/code-check` Round 1 caught 2 fragility issues (scalar `table` check, silent unmatched focals), both fixed; Round 2 Clean. Commit `157d03e`.
- **Phase 3 complete:** Wrote `tests/testthat/test-frs_wsg_drainage.R` — 11 test_that blocks / 14 expectations (6 arg-validation + 5 live-DB). Live tests skip on missing `PG_DB_SHARE`; locally validated by inline-overriding env + `--no-environ` to bypass the dead `:63333` tunnel (per `m1-link-db-env` memory). All pass. `/code-check` Round 1 caught 1 too-broad `class = "error"` assertion (would pass on any error, not just identifier validation) — fixed to match `.frs_validate_identifier`'s actual message `"table contains invalid characters"`; Round 2 Clean. Commit `e9fa8b9`.
- **Phase 4 release-prep:** NEWS.md `# fresh 0.32.0` section added (one-line summary + 6 bullets matching v0.31.0 style); DESCRIPTION bumped `0.31.0 → 0.32.0` (no Date — matches fresh convention). lintr skipped (fresh hasn't adopted lintr; not in Suggests). /code-check skipped on this docs-only Release commit (R code already cleared in Phases 2 + 3).
- Next: commit Release v0.32.0, then `/planning-archive` + `/gh-pr-push`
Loading
Loading