Skip to content

Harden the feed download against paging, empty and pinless responses - #95

Merged
baz8080 merged 5 commits into
mainfrom
claude/trusting-dirac-17nbln-ingest
Sep 24, 2026
Merged

baz8080 merged 5 commits into
mainfrom
claude/trusting-dirac-17nbln-ingest

Conversation

@baz8080

@baz8080 baz8080 commented Sep 24, 2026 •

Copy link
Copy Markdown
Owner

Ingest fixes from the retroactive review (PR F of the set). This PR changes no published number on the current data.

Changes (src/uisce/pipeline.py)

  • Paging by key. download_cases pages with OBJECTID > <last seen> instead of resultOffset. With offsets, two things went wrong, and both stayed inside the 1% count tolerance:

    • one case deleted during the download pushed a live case off the next page, so that case was stamped vanished;
    • a server maxRecordCount below 1,000 silently dropped the difference on every page.

    Every page must be strictly ascending from the last id, or the build fails with the page named. That also covers a server that ignores the filter and re-serves a page, and a feature with no OBJECTID.

  • Features with no pin. ArcGIS leaves out geometry for a null shape, or writes an empty point as "NaN", and one such feature used to crash every build. The coordinates are NOT NULL and key the geocode cache, so allowing NULL would mean a non-additive schema change. Instead, a new function, restore_pins, handles these cases:

    • a case the database already knows keeps the pin it last had;
    • a case that has never been pinned is left out, with a ::warning:: on the Actions run, until the feed pins it.
  • Empty download. check_download_complete refuses an empty download while the database holds rows that aren't vanished. The vanish stamp touches every such row, closed ones included: 4,306 on the release, of which 498 are open. A count of 0 alongside a full download still builds.

  • backfill_reduced_pressure now uses COALESCE(reduced_pressure, 0) = 0. None of the 7 rows with a NULL flag matches today.

  • Read-only database access. It goes through one context manager that always closes the connection, probes the columns once, and builds the URI with as_uri().

Review

First round, high effort, 8 findings.

  • Two reproduced crashes:
    • a "NaN" point;
    • a database that has no cases table yet.
  • An empty-feed guard that counted only open rows.
  • Page order. Paging trusted the server's order without checking, and the tests couldn't tell if orderByFields was removed.
  • Never-pinned cases were only a log line.

Second round, medium effort, 10 findings.

  • Connections left open when a query raised.
  • The refusal was keyed on the count instead of the download.
  • Missing tests:
    • both directions of the refusal;
    • a server ignoring the filter (without the check, that loops forever);
    • a missing OBJECTID.
  • Smaller fixes: a doubled column probe and URI escaping.
  • CLAUDE.md rows needed amending and adding.

All are addressed.

Notes

  • notes/data-quality.md: a dated paragraph in the vanished-cases section, and "A feature with no pin (2026-09-24)".
  • CLAUDE.md: the vanished_at row is amended, and there is a new row for features with no pin.

Tests

  • New tests in tests/test_pipeline.py, each failing before its fix:
    • LiveFeed fakes a server that deletes a row mid-download, one that caps pages, one that ignores orderByFields, one with unsorted storage, and one that re-serves the first page (it fails fast instead of looping).
    • Both directions of the empty-download refusal.
    • A feature with no geometry, and a "NaN" point.
    • TestRestorePins, including a database with no cases table.
    • A NULL pressure flag.
  • uv run ruff check and uv run pytest pass: 702 tests, merged with current main.

🤖 Generated with Claude Code

https://claude.ai/code/session_0168hLW2X3mkV3Jhm26LJSwQ

Four fixes from a review of pipeline.py, each with a regression test that
failed first.

download_cases pages by key, OBJECTID > <last seen>, instead of by
resultOffset. By offset, one case deleted mid-download shifted a live
case out of the next page and stamped it vanished for that build, and a
server maxRecordCount of 995 lost 5 rows a page (20 of 4,300), both
under FEED_COUNT_TOLERANCE.

check_download_complete refuses an empty feed while the DB holds Open
cases not yet vanished. 0 of 0 passed the tolerance and would have
stamped all 498 open cases on the 2026-09-23 release vanished. A partial
purge is still stamped, as data-quality.md says it should be.

map_cases no longer KeyErrors on a feature with no geometry, which
ArcGIS omits for a null shape and which crashed every build. The
coordinates are REAL NOT NULL and key the geocode cache, and dropping
the constraint is not an additive migration, so restore_pins gives such
a case the pin the DB last stored for it; a case never pinned is set
aside and printed until the feed pins it. The DB therefore never holds
NULL coordinates and geocoding and the site are unchanged.

backfill_reduced_pressure reads a NULL feed flag as 0. On the release
DB the 7 NULL-flag rows do not match the pattern, so no published
number moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168hLW2X3mkV3Jhm26LJSwQ
From a code review of this PR:

- An empty point written as "NaN" passed the None check, became a nan
  coordinate and crashed the build on a NOT NULL column. Coordinates
  that are not finite numbers now count as missing.
- restore_pins assumed a cases table whenever the DB file existed;
  geocode_all can create the file first. Both readers go through one
  helper that answers "no table yet".
- The empty-feed guard counted open rows, but the vanish stamp touches
  every row not yet vanished, closed ones included (4,306 on the
  release, not 498). It now counts those, and its dead `not features`
  branch and misleading message are gone.
- Key paging trusted the server's order. A page that is not ascending
  now fails the build, and a fake feed stored out of order makes
  dropping orderByFields fail a test.
- A case the feed has never pinned is surfaced as a ::warning:: on the
  run rather than a log line, and the decision is in data-quality.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168hLW2X3mkV3Jhm26LJSwQ
From a second code review of the review-fix commit:

- Read-only connections are closed on every path (a context manager
  over the cases table replaces `with conn:`, which never closes), the
  column probe runs once, and the DB URI is built with as_uri().
- The empty-feed refusal keys on an empty download, which is what the
  vanish stamp acts on; a 0 count with a full download now builds.
- The page check is one strictly-increasing test from the last id, so
  a server ignoring `OBJECTID > n` is refused rather than looped over,
  and a feature with no OBJECTID fails with the page named.
- Tests for both refusal directions and the ignored filter; the test's
  own sqlite connection is closed. CLAUDE.md's vanished_at row is
  amended and gains a row for features with no pin.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0168hLW2X3mkV3Jhm26LJSwQ
@baz8080
baz8080 merged commit 899c667 into main Sep 24, 2026
2 checks passed
@baz8080
baz8080 deleted the claude/trusting-dirac-17nbln-ingest branch September 24, 2026 11:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants