Skip to content

bootstrapDeclaredCapabilities seeds sys_capability one at a time too — the same per-item read + unconditional UPDATE #10946 removed from the two sibling loops #11096

Description

@os-warren

Found while implementing #10946 (batching the permission-set and position boot seeds). The sys_capability seeder next door has the identical shape and was not in that card's scope.

The shape

packages/plugins/plugin-security/src/bootstrap-declared-capabilities.ts:

  • line 210 — const existing = (await tryFind(ql, 'sys_capability', { name: cap.name }, 1))[0]; — a per-item existence SELECT, issued inside the loops at lines 136/138 (curated: every systemPermissions entry of every declared set) and line 290 (the derived half);
  • line 241 — if (await tryUpdate(ql, 'sys_capability', { id: existing.id, ...fields })) out.updated += 1; — an UPDATE on the seeder's own row that fires whether or not fields differ from what is stored.

So each declared capability costs its own sequential round trip pair on every kernel boot, exactly as permission sets and positions did. On a local file database this is invisible; on a remote libsql/Turso database — every hosted environment — each leg is its own HTTP request, awaited one after another.

Why it is worth a card of its own

Capability names come from systemPermissions on every declared permission set, so the count is not bounded by the number of sets — it is the union of every capability every package declares, which is typically larger than either axis #10946 measured. The cost competes for the same 20 s request budget (objectstack-ai/cloud#1555).

Not measured here

I did not re-run the hosted bootstrap-curve.mjs rig for this axis — that lives in objectstack-ai/cloud, outside the session that found this. What is established is the code shape above, and that it is the same shape whose cost was measured at slope 4.0000 / R² = 1.000000 on the two sibling loops in #10946. The capability axis' own slope is not measured.

Direction

The same two halves #10946 applied, and the machinery is already in the package:

  1. hoist one { name: { $in: [...] } } existence read out of each loop via buildExistingByName (packages/plugins/plugin-security/src/seed-name-lookup.ts, added by Boot seeds permission sets and positions one at a time: 4 sequential DB round trips each, 2 of them an unconditional UPDATE #10946) — it already carries the "a read that CANNOT ANSWER is not the answer 'none exist'" judgement that a batched read makes load-bearing;
  2. compare before writing, so an unchanged row costs nothing.

⚠️ Whatever lands must keep the reconciliation leg pinned: a row whose stored value differs still needs its UPDATE. An implementation that skipped all writes would show a perfect round-trip curve while silently reconciling nothing. Note also that this seeder has provenance branches (skippedUnowned, skippedForeign) and a curated/derived split that #10946's two loops do not — the refusal messages at lines 215-219 depend on whether a row was found, so the batched read has to preserve that distinction, not just the row.

Refs #10946 · objectstack-ai/cloud#1555 · #10979 and #10945 are the same boot-cost campaign in other packages.

Metadata

Metadata

Assignees

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions