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
46 changes: 46 additions & 0 deletions docs/DEBUGGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -731,3 +731,49 @@ out-of-memory, one reporting only 2.9 MB available, while a full pipeline run of
mine was hammering the same endpoint. Re-run alone, they failed differently, as
planning timeouts. The header of `query-matrix.mts` warns about this; it applies
to ad-hoc curl measurements just as much.

## 2026-09-20: Adding an industry filter broke a question that worked (FIXED)

Found while answering "what does this result really mean" about the York County
map. The unfiltered question, facilities within 5 km upstream from PFOS
detections, returns 418 facilities in 3s. It also returns dentists and schools,
because block A carries no industry filter and "facility" means anything with a
NAICS code. The obvious next step is to filter block A to PFAS source
categories. Doing that returned nothing: 429, `Operation timed out. Last
operation: Query planning`, on both projections.

**Root cause, one level up from the 2026-09-20 seed-side entry above.**
`sideIsConstrained` treated a region, an entity filter and an IRI pin as the
same thing. An industry filter made the anchor count as constrained, so the
reorder stopped firing, so the query seeded from block A, which with a filter
and no region means every facility in that industry across the country.

Isolated by varying one thing at a time, all at 5 km, York, detections only:

| Block A | Result |
| --- | --- |
| no filter, no region | works, 418 facilities, 3s |
| 1 code, no region | 429, query planning |
| 21 codes, region Maine | 429 Cartesian Product, then 500 at 728 MB |
| 21 codes, region York | 500 at 728 MB |
| 1 code, region York | works, 21 facilities, 13s |

The pattern is that block A had to be *small*, not merely filtered, and only a
region made it small.

**The fix** replaces the boolean with a rank: IRI pin 3, region 2, entity filter
1, nothing 0, lead with the higher, ties keep anchor-first. Only questions with
both sides constrained at different levels change plan. See
`docs/QUERY-MATRIX.md` Part 11 for the measurements.

**Long selections: measure before blaming the clause.** The first version of
this entry said selections beyond about ten codes still fail. Re-measured after
the ranking landed, that is wrong. 21 codes answers at 5 km (101 facilities,
13s; 84 samples, 23s) and only fails unbounded (500, 430 MB). Code count is a
cost on the closure, which a distance bound already caps.

The suspected culprit was cleared too. `fio:subcodeOf` is materialised
transitively, so `?ic fio:subcodeOf? ?sel` is an exact rewrite of
`FILTER(?ic = ?sel || EXISTS { ?ic fio:subcodeOf ?sel })`. Measured: 13s with
the FILTER, query-planning timeout with the path. The FILTER is the fast form
here; do not "optimise" it.
74 changes: 74 additions & 0 deletions docs/QUERY-MATRIX.md
Original file line number Diff line number Diff line change
Expand Up @@ -907,3 +907,77 @@ This is what ignoring it looks like in practice: a plausible number, in the
right direction, that attributes a cache effect to a code change. The pre-fix
engine was re-run warm into `2026-09-20-raw-e84a11f.csv`, and that file, not the
cold one, is the before-half of 10.2. The cold sweep carries a `.note` saying so.

## Part 11: Narrowness is a ranking, not a boolean (2026-09-20)

Part 10 made the distance bound work when block A is left open. Putting an
industry filter on block A broke it again, and for a reason one level up from
the seed side.

`sideIsConstrained` answered yes for a region, a filter or an IRI pin alike. An
industry filter therefore made the anchor "constrained", the reorder stopped
firing, and the query seeded from block A again. Only now block A meant every
facility in that industry nationwide rather than every facility in the graph,
which is smaller and still far too large to seed an aggregate from.

### 11.1 What it cost

"What facilities are within 5 km upstream from PFOS samples", York County,
detections only, block A carrying no region:

| Block A | Before | After |
| --- | --- | --- |
| no filter | 418 facilities, 3s | unchanged |
| 1 code, unbounded | 429, query planning | 35 facilities, 16s |
| 1 code, 5 km | 429, query planning | 22 facilities, 8s |
| 8 codes, 5 km | 429, query planning | **88 facilities, 3s** |
| 8 codes, unbounded | 429, query planning | 192 facilities, 15s |

Adding a filter to a working question made it fail. That is the shape of the
report: a user narrowing a result set that was too broad, and getting nothing.

### 11.2 The rank

3 an IRI pin. The executor chose this slice; re-seeding discards it.
2 a region. Bounded by geography at any level, and indexed for.
1 an entity filter only. Selective in kind, unbounded in extent.
0 nothing.

The body leads with the higher rank, and ties keep the anchor-first order. So
this only changes questions where both sides are constrained at *different*
levels; everything where one side was constrained and the other was not keeps
the plan it had. Of the 375 shapes in `scripts/__snapshots__/query-shapes.txt`,
2 move, both variants with a region on one side and a bare filter on the other.

### 11.3 What it does not fix

**Long industry selections fail only without a bound.** An earlier draft of
this part said 21 codes fails outright. Re-measured after the ranking landed,
that is wrong: the code count only matters when the trace is unbounded.

| Codes | 5 km | Unbounded |
| ---: | --- | --- |
| 1 | 22 facilities, 8s | 35 facilities, 16s |
| 8 | 88 facilities, 3s | 192 facilities, 15s |
| 21 | 101 facilities, 13s | 500, out of memory at 430 MB |

Which is what a bound is for: the unbounded form walks the whole closure from
every matched facility, so more codes means more seeds and more work, while the
bounded form caps it. A long selection is a reason to set a distance, not a
reason to shorten the list.

**A rewrite of the industry clause was tried and rejected.** The obvious suspect
was `FILTER(?ic = ?sel || EXISTS { ?ic fio:subcodeOf ?sel })`, evaluated per
selected code and duplicated inside the bounded aggregate. `fio:subcodeOf` is
materialised transitively (`325110` is a direct subcode of `32`, `325` and
`3251`), so `?ic fio:subcodeOf? ?sel` says exactly the same thing as a
zero-or-one path. It is slower, not faster: the 21-code bounded query answers in
13s with the FILTER and times out in query planning with the path. Leave the
clause alone.

**One dashboard question changes plan.** `samples-downstream-waste-indiana`
scopes samples to Indiana and filters block C to NAICS 5622 with no region, so
its anchor is nationwide and its target is one state. Both orders were measured
before accepting the change: identical answers, 136 samples and 415 facilities
either way, and the reordered form is no slower at 13s and 2s against 16s and
3s.
13 changes: 13 additions & 0 deletions docs/changelog/2026-W38.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,16 @@
**Known limit, recorded so it is not discovered later:** the snapshot drives everything through `planPipeline`, so it covers shapes a user can ask for and is blind to builder arguments the planner never produces. A reordering of the anchor-seeded `upstream` branch of `boundedTrace` passes all seven checks. The guarantee is "no reachable shape moved unintentionally", not "no emitted SPARQL moved".

*Files:* `scripts/query-matrix.mts`, `scripts/check-query-snapshots.mts` (new), `scripts/__snapshots__/query-shapes.txt` (new), `package.json`, `.github/workflows/checks.yml`

17. **Fixed: Narrowing a Question by Industry Made It Fail** (2026-09-20)
Found immediately after entry 15, by doing the obvious next thing with the map it fixed. "What facilities are within 5 km upstream from PFOS samples in York County" answers in 3s and returns 418 facilities, of which 261 distinct industries include dentists, schools and hospitals, because block A carries no industry filter and "facility" means anything with a NAICS code. Filtering block A to PFAS source categories returned nothing at all: a 30s timeout in query planning, on both projections.

The cause sits one level above entry 15. `sideIsConstrained` answered yes for a region, an entity filter or an IRI pin alike, so an industry filter made the anchor count as constrained, the reorder stopped firing, and the query seeded from block A again. With a filter and no region that means every facility in that industry nationwide. The comment two lines above the predicate already read "narrowed is not small"; the predicate did not act on its own warning.

It now returns a rank instead: an IRI pin 3, a region 2, an entity filter 1, nothing 0. The body leads with the higher, and ties keep the anchor-first order, so only questions with both sides constrained at *different* levels change plan. Of the 375 shapes in the query snapshot, 2 move.

Measured with block A carrying no region: one code goes from a planning timeout to **35 facilities in 16s** unbounded and **22 in 8s** at 5 km; eight codes go from a planning timeout to **88 facilities in 3s** at 5 km. The filtered map is the useful one, 88 candidates rather than 418, with the dentists gone and sewage treatment, dry cleaners, boatyards, airports and petroleum terminals at the top.

**One limit, and one that turned out not to exist.** A long industry selection fails only without a distance bound: 21 codes answers at 5 km (101 facilities, 13s) and runs out of memory unbounded, because the unbounded form walks the whole closure from every matched facility. That is a reason to set a distance, not to shorten the list. The clause first blamed for it, `FILTER(?ic = ?sel || EXISTS { ?ic fio:subcodeOf ?sel })`, was cleared by measurement: `fio:subcodeOf` is materialised transitively, so a zero-or-one path is an exact rewrite, and it is slower, 13s against a planning timeout. And one dashboard question changes plan, `samples-downstream-waste-indiana`, whose anchor is NAICS 5622 with no region against samples scoped to Indiana. Both orders were run before accepting it: identical answers, 136 samples and 415 facilities either way, reordered no slower.

*Files:* `src/engine/templates/fusedQueries.ts`, `scripts/check-query-joins.mts` (30 new assertions), `scripts/__snapshots__/query-shapes.txt`, `docs/QUERY-MATRIX.md` (Part 11), `docs/DEBUGGING.md`
Loading
Loading