Doctrine

Unbounded queries must paginate or scope — never cap with an arbitrary limit

code-qualityedge-functionssupabasesilent-failure

Unbounded queries must paginate or scope — never cap with an arbitrary limit

Rule

When a query intends to return all rows of a data set whose cardinality is not structurally bounded, the query MUST either:

1. Paginate to exhaustion — fetch in fixed-size pages until a short page signals the end, OR 2. Scope with a WHERE clause that bounds the result set semantically (e.g., WHERE project_id = ?)

Using .limit(N) as a substitute for "all" is forbidden. It silently truncates — the function works until the data outgrows the cap, then degrades without any error signal.

What counts as "structurally bounded"

A data set is structurally bounded when its maximum cardinality is guaranteed by the schema or business domain. Examples:

  • Bounded: conversation spans for one interaction (capped by call duration), projects for one user (tens), config rows (single digits). .limit() is fine as a safety net here.
  • Unbounded: all contacts in the system, all aliases, all interactions, all receipts. These grow with business activity. .limit(N) on these is the bug.

Safe patterns

Pattern A — Paginate to exhaustion (preferred for full-table scans)

const PAGE_SIZE = 1000;
const MAX_PAGES = 25;
const rows = [];

for (let page = 0; page < MAX_PAGES; page++) {
  const from = page * PAGE_SIZE;
  const { data, error } = await db
    .from("contacts")
    .select("name,company")
    .range(from, from + PAGE_SIZE - 1);

  if (error) throw new Error(error.message);
  rows.push(...(data || []));

  if ((data || []).length < PAGE_SIZE) break;
}

if (rows.length >= PAGE_SIZE * MAX_PAGES) {
  console.warn(`contacts lookup hit page cap at ${PAGE_SIZE * MAX_PAGES} rows`);
}

Pattern B — Scope with WHERE (preferred when a natural key exists)

const { data } = await db
  .from("journal_claims")
  .select("*")
  .eq("call_id", interactionId);

Anti-pattern — arbitrary cap masquerading as "all"

const { data } = await db
  .from("contacts")
  .select("name,company")
  .limit(1000);  // WRONG: silently drops contact 1001+

When .limit() IS correct

  • Existence checks: .limit(1) — you only need to know if a row exists
  • Display queries: .limit(10) — you're showing "recent 10" to a user
  • Top-N queries: .limit(50).order(...) — you want the top N by some criteria
  • Safety nets on bounded sets: .limit(100) on a set you know has <50 rows but want to guard against schema drift

The distinguishing test: does the function's correctness depend on seeing every row? If yes, .limit(N) is wrong.

Known instances in camber (as of 2026-04-17)

FileLimitRiskStatus
generate-summary contacts.limit(1000)Silent contact truncationFIXED (paginated in HCP-0005, commit a72f81b9)
alias-scout.limit(500)Silent alias truncationOPEN — audit needed
embed-facts.limit(500)Silent fact truncationOPEN — audit needed
gmail-context-lookup.limit(2000)Silent context truncationOPEN — audit needed

Enforcement

  • Code review: any .limit(N) where N > 1 on a query that lacks a scoping WHERE clause should trigger a "is this set bounded?" question
  • Edge function authors: when writing a new query, ask "will this table have more than N rows in 12 months?" If the answer isn't confidently "no," paginate