PERF GUARD: enforce bounded list queries and signed keyset contracts (#663) #792

Open
opened 2026-10-02 13:10:52 +00:00 by kayg · 0 comments
Owner

PERF GUARD: enforce bounded list queries and signed keyset contracts (#663)

Finding and context

origin/dev crates/plugins/notes/src/lib.rs:3343–3385 returns at most 100 rows
but uses LIMIT/OFFSET and an unsigned numeric cursor. Tasks repeats this at
tasks_api.rs:779,810. Round 7a still has it at lib.rs:3722.
Photos routes.rs:650–652 has offset paging and defaults to 200 tiles.
Files listing.rs:283 already has keyset LIMIT queries. Page position can
increase query work; a LIMIT alone does not prove bounded response bytes.
Product corrections already belong to #703/#682/#680. This issue owns guards.

Exact detection

Parse SQLite statements assembled by registered list handlers and their local
helpers. Resolve literals, raw strings, concat and finite format fragments.
Unknown SQL construction fails as uncheckable unless precisely excepted.
Reject an OFFSET clause on any interactive list path. Match SQL syntax, not
Rust identifiers (TZOFFSETFROM and byte offsets are valid). Reject each
outer row-producing SELECT without a positive finite LIMIT bound. Check each
UNION arm where it can materialize unbounded input. Scalar COUNT/EXISTS,
fetch_one and fixed singleton lookups are classified separately, not blanket
exemptions. A LIMIT in a nested subquery does not bound its parent.

For bound LIMIT values require a registry mapping to a clamped page parameter:
1..100 response rows, at most 101 read rows for lookahead. Unknown bound dataflow
requires an exception and contract test. Query checks cover registered SQL
only; in-memory, filesystem and search-engine list paths need the same response
tests. Do not assume .take(100) bounds work before collection.

For every list, run an in-process endpoint contract test with 10k synthetic
records, equal sort keys and long fields. Test omitted, 0, 1, 100, 101 and extreme
limits: successful responses have <=100 items and <= the registered serialized
UTF-8 response-byte cap (before compression). The implementation selects and
documents that finite cap per representation; missing cap fails. A partial
page needs a continuation; a too-large single item needs a defined error or
bounded summary, never silent data loss. Verify pages contain all IDs exactly
once in an unchanged fixture, use a stable ID tiebreaker, and handle inserts
before the boundary without repeating the previous page. Reject a modified,
wrong-User, wrong-filter, wrong-sort and expired signed cursor. Error bodies
must also be bounded. Background retention DELETE OFFSET is out of scope.

Tests and gates

Positive fixtures use Files-style keyset SQL. Negative fixtures put OFFSET in
raw/formatted SQL, LIMIT only in a subquery, negative LIMIT and an uncapped bind.
Comments, timezone strings and mutation retention queries do not fail. Endpoint
contracts fail on 101 returned rows, missing next cursor and an oversized title.
Run source detection in perf-lint; endpoint tests in the affected crate tests.
A query-plan test may assert intended index/search structure on its fixture;
do not assert machine-specific wall time or exact SQLite plan text.

Scope and exception contract

Specification only, from #663 perf-guards. Implement in a later job. Reuse the
shared guard registry and ledger described in the coverage issue. Until it
exists, use this exact contract: an exception has rule ID, repo-relative file,
symbol or route/surface ID, normalized syntax hash (or exact build metric),
owner issue, reason, replacement test and UTC expiry date. No directory globs,
line-number-only entries or ignore all comments. Fail on expired, changed,
duplicate and unused exceptions. Existing debt needs an explicit entry and a
non-growing limit. Print exceptions in CI. An exception does not waive access,
session clearing or accessibility tests. Generated and test fixtures are
excluded by explicit source class, not substring matching. Tests must prove
both violation detection and valid exceptions. No product fix belongs here.

The static/contract command exits 1 for violations, 2 for invalid configuration
or parse/build errors, and 0 only for complete passing coverage. Output includes
rule ID, file:line, symbol/route, actual value and required value. No User content.
Measured host timing stays periodic under CLAUDE.md. Deterministic guards fail
required CI; the dedicated perf-budget runner reports measured budget failures
with nonzero status but is not a required merge check.

# PERF GUARD: enforce bounded list queries and signed keyset contracts (#663) ## Finding and context origin/dev `crates/plugins/notes/src/lib.rs:3343–3385` returns at most 100 rows but uses LIMIT/OFFSET and an unsigned numeric cursor. Tasks repeats this at `tasks_api.rs:779,810`. Round 7a still has it at `lib.rs:3722`. Photos `routes.rs:650–652` has offset paging and defaults to 200 tiles. Files `listing.rs:283` already has keyset LIMIT queries. Page position can increase query work; a LIMIT alone does not prove bounded response bytes. Product corrections already belong to #703/#682/#680. This issue owns guards. ## Exact detection Parse SQLite statements assembled by registered list handlers and their local helpers. Resolve literals, raw strings, concat and finite format fragments. Unknown SQL construction fails as uncheckable unless precisely excepted. Reject an OFFSET clause on any interactive list path. Match SQL syntax, not Rust identifiers (`TZOFFSETFROM` and byte offsets are valid). Reject each outer row-producing SELECT without a positive finite LIMIT bound. Check each UNION arm where it can materialize unbounded input. Scalar COUNT/EXISTS, fetch_one and fixed singleton lookups are classified separately, not blanket exemptions. A LIMIT in a nested subquery does not bound its parent. For bound LIMIT values require a registry mapping to a clamped page parameter: 1..100 response rows, at most 101 read rows for lookahead. Unknown bound dataflow requires an exception and contract test. Query checks cover registered SQL only; in-memory, filesystem and search-engine list paths need the same response tests. Do not assume `.take(100)` bounds work before collection. For every list, run an in-process endpoint contract test with 10k synthetic records, equal sort keys and long fields. Test omitted, 0, 1, 100, 101 and extreme limits: successful responses have <=100 items and <= the registered serialized UTF-8 response-byte cap (before compression). The implementation selects and documents that finite cap per representation; missing cap fails. A partial page needs a continuation; a too-large single item needs a defined error or bounded summary, never silent data loss. Verify pages contain all IDs exactly once in an unchanged fixture, use a stable ID tiebreaker, and handle inserts before the boundary without repeating the previous page. Reject a modified, wrong-User, wrong-filter, wrong-sort and expired signed cursor. Error bodies must also be bounded. Background retention DELETE OFFSET is out of scope. ## Tests and gates Positive fixtures use Files-style keyset SQL. Negative fixtures put OFFSET in raw/formatted SQL, LIMIT only in a subquery, negative LIMIT and an uncapped bind. Comments, timezone strings and mutation retention queries do not fail. Endpoint contracts fail on 101 returned rows, missing next cursor and an oversized title. Run source detection in perf-lint; endpoint tests in the affected crate tests. A query-plan test may assert intended index/search structure on its fixture; do not assert machine-specific wall time or exact SQLite plan text. ## Scope and exception contract Specification only, from #663 perf-guards. Implement in a later job. Reuse the shared guard registry and ledger described in the coverage issue. Until it exists, use this exact contract: an exception has rule ID, repo-relative file, symbol or route/surface ID, normalized syntax hash (or exact build metric), owner issue, reason, replacement test and UTC expiry date. No directory globs, line-number-only entries or `ignore all` comments. Fail on expired, changed, duplicate and unused exceptions. Existing debt needs an explicit entry and a non-growing limit. Print exceptions in CI. An exception does not waive access, session clearing or accessibility tests. Generated and test fixtures are excluded by explicit source class, not substring matching. Tests must prove both violation detection and valid exceptions. No product fix belongs here. The static/contract command exits 1 for violations, 2 for invalid configuration or parse/build errors, and 0 only for complete passing coverage. Output includes rule ID, file:line, symbol/route, actual value and required value. No User content. Measured host timing stays periodic under CLAUDE.md. Deterministic guards fail required CI; the dedicated perf-budget runner reports measured budget failures with nonzero status but is not a required merge check.
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#792
No description provided.