Skip to content

fix(db): add a rowid tiebreak to signal_snapshots' latest-row reads - #4665

Merged
JSONbored merged 1 commit into
mainfrom
fix/signal-snapshots-rowid-tiebreak
Jul 10, 2026
Merged

fix(db): add a rowid tiebreak to signal_snapshots' latest-row reads#4665
JSONbored merged 1 commit into
mainfrom
fix/signal-snapshots-rowid-tiebreak

Conversation

@JSONbored

Copy link
Copy Markdown
Owner

Summary

  • listSignalSnapshots ordered by generatedAt (millisecond precision) alone, and listLatestSignalSnapshotsForTargets used a secondary id DESC tiebreak — id is a crypto.randomUUID(), with no relationship to insertion order. Two writes for the same (signalType, targetKey) within one millisecond (a realistic case: an API-record write immediately after a seeded/prior one) tie on generatedAt.
  • Researched whether this is a real risk before fixing it: SQLite's own docs make no guarantee about tie order ("the order ... is undefined", sqlite.org/lang_select.html). Verified Cloudflare D1 (GA) currently routes every read for a database to a single active instance, so there's no live multi-replica divergence risk today — but Cloudflare's own roadmap (a Sessions API with read replicas, described in their "Building D1" post) would reintroduce exactly that risk, and SQLite's own docs separately warn that an "insertion order" appearance is incidental (can shift as a table grows, an index gets used, or the engine version changes), independent of replicas entirely.
  • Fixes both reads to use rowid (SQLite's actual monotonic per-insert counter) instead, matching this table's own documented invariant elsewhere in the codebase (retention.ts's dedupeSignalSnapshots: "'Latest' is the highest rowid per key ... rowid, unlike generated_at, can never tie") and the same pattern already used for orb_enrollments' own most-recently-inserted read (orb/relay.ts).
  • A regression test with deliberately adversarial ids (chosen to sort alphabetically backwards from insertion order) confirms listLatestSignalSnapshotsForTargets' prior id DESC tiebreak was a real, reproducible bug — reverting the fix makes that specific test fail. The listSignalSnapshots side couldn't be reproduced as a concrete bug against the local D1 test shim (its own untiebroken fallback happens to already match insertion order there), so its tests instead pin the fix's mechanism (genuine rowid-based order, not an id string comparison) as protection against a future regression.

Scope

  • The PR title follows type(scope): short summary Conventional Commit format.
  • This PR is focused and does not mix unrelated backend, UI, MCP, docs, dependency, and deploy changes.
  • This follows CONTRIBUTING.md and does not reintroduce GitHub Pages, VitePress, site/, or CNAME.
  • This is an owner-authored maintenance fix (no separate tracked issue — a correctness fix discovered during an unrelated investigation).

Validation

  • git diff --check
  • npm run actionlint
  • npm run typecheck
  • npm run test:coverage locally — neither change introduces a new JS-level branch (the rowid additions are a plain function-chain argument and a raw-SQL-string edit, not a conditional), confirmed via precise lcov line/branch cross-referencing against the diff; both changed statements are covered by thousands of pre-existing calls plus the new regression tests
  • npm run test:workers
  • npm run build:mcp
  • npm run test:mcp-pack
  • npm run ui:openapi:check
  • npm run ui:lint
  • npm run ui:typecheck
  • npm run ui:build
  • npm audit --audit-level=moderate — 0 vulnerabilities
  • New or changed behavior has unit/integration tests for new branches, fallback paths, and sanitizer boundaries — 4 new tests: adversarial-id ordering for both listSignalSnapshots and listLatestSignalSnapshotsForTargets, a 3-row insertion-order regression, and a "genuinely later generatedAt still wins" sanity check. Bug-injection verified: temporarily reverted the fix and confirmed the listLatestSignalSnapshotsForTargets test fails for the expected reason (picks the wrong "latest" row) before restoring it.

Safety

  • No secrets, wallet details, hotkeys, coldkeys, user PATs, private keys, raw trust scores, private rankings, or private maintainer evidence are exposed.
  • Public GitHub text stays sanitized, low-noise, and does not imply compensation guarantees or optimization tactics.
  • Auth, cookie, CORS, GitHub App, Cloudflare, or session changes include negative-path tests. — N/A, no auth/session surface touched.
  • API/OpenAPI/MCP behavior is updated and tested where needed. — N/A, no public API surface changed (internal query ordering only).
  • UI changes use live API data or real empty/error/loading states, not production mock/demo fallbacks. — N/A, backend-only change.
  • Visible UI changes include a UI Evidence section. — N/A, backend-only change, no visible UI.
  • Public docs/changelogs are updated where needed. — N/A, internal correctness fix, no public-facing behavior change.

listSignalSnapshots ordered by generatedAt (millisecond precision) alone,
and listLatestSignalSnapshotsForTargets used a secondary `id DESC`
tiebreak -- id is a crypto.randomUUID(), with no relationship to
insertion order. Two writes for the same (signalType, targetKey) within
one millisecond (a realistic case: an API-record write immediately after
a seeded/prior one) tie on generatedAt, and SQLite's own docs make no
guarantee about tie order ("the order ... is undefined",
sqlite.org/lang_select.html) -- so which row an unbroken ORDER BY tie
returns is an accident of the current query plan, not a contract, and can
change as the table grows or the engine version changes. Verified
Cloudflare D1 (GA) currently routes every read for a database to a single
active instance, so there's no live multi-replica divergence risk today --
but Cloudflare's own roadmap (a Sessions API with read replicas) would
reintroduce exactly that risk, and SQLite's "insertion order" appearance
is documented as incidental, not guaranteed, independent of replicas.

Fixes both reads to use rowid (SQLite's actual monotonic per-insert
counter) instead, matching this table's own documented invariant
elsewhere in the codebase (retention.ts's dedupeSignalSnapshots: "'Latest'
is the highest rowid per key ... rowid, unlike generated_at, can never
tie") and the same pattern already used for orb_enrollments' own
most-recently-inserted read (orb/relay.ts).

A regression test with deliberately adversarial ids (chosen to sort
alphabetically BACKWARDS from insertion order) confirms
listLatestSignalSnapshotsForTargets' prior `id DESC` tiebreak was a real,
reproducible bug -- reverting the fix makes that specific test fail. The
listSignalSnapshots side couldn't be reproduced as a concrete bug against
the local D1 test shim (its own untiebroken fallback happens to already
match insertion order there), so its tests instead pin the fix's
mechanism (genuine rowid-based order, not an id string comparison) as
protection against a future regression, rather than proving today's local
behavior is wrong.
@superagent-security

Copy link
Copy Markdown
Contributor

Superagent didn't find any vulnerabilities or security issues in this PR.

@JSONbored JSONbored self-assigned this Jul 10, 2026
@JSONbored
JSONbored merged commit 5ae3c59 into main Jul 10, 2026
7 checks passed
@JSONbored
JSONbored deleted the fix/signal-snapshots-rowid-tiebreak branch July 10, 2026 10:54
@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.13%. Comparing base (eecc228) to head (28d8718).
⚠️ Report is 3 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4665   +/-   ##
=======================================
  Coverage   94.13%   94.13%           
=======================================
  Files         434      434           
  Lines       38554    38554           
  Branches    14053    14053           
=======================================
  Hits        36291    36291           
  Misses       1604     1604           
  Partials      659      659           
Files with missing lines Coverage Δ
src/db/repositories.ts 96.62% <ø> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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.

1 participant