Skip to content

claimSeedOwnership claims nothing at all on an object with more than 10k rows under one unowned predicate, because MAX_BULK_PER_ROW_HOOK_ROWS refuses the whole write #14719

Description

@os-sales

Follow-up recorded while implementing #14530 (PR #14718). Not fixed there: #14530's ruled scope is disposition 2 (turn the single-id loop into a predicate write), and paginating the claim is a different decision. Filing it so the boundary is on the record rather than only in a PR body.

The shape

claimSeedOwnership (packages/plugins/plugin-security/src/claim-seed-ownership.ts) now issues one predicate write per unowned shape:

await ql.update(name, { owner_id: adminUserId }, { where: { owner_id: null }, multi: true, context: SYSTEM_CTX });
await ql.update(name, { owner_id: adminUserId }, { where: { owner_id: 'usr_system' }, multi: true, context: SYSTEM_CTX });

A predicate write carries no limit, so the bound is now the engine's own per-row hook ceiling. MAX_BULK_PER_ROW_HOOK_ROWS is 10 000 (packages/spec/src/data/bulk-write-hook-conformance.ts), and D6 makes the refusal total — a predicate write matching more than that on an object carrying beforeUpdate/afterUpdate hooks is refused whole, never downgraded. Every object carries them in practice: objectql's own audit-stamp and fetch-previous builtins are registered on object: '*'.

Measured on a real ObjectQL engine (in-memory driver, 21 000 seeded rows, 10 500 per predicate):

WARN [security] claimSeedOwnership failed for crm_lead; those rows stay unowned and the next run will claim them
  Refusing the bulk write on 'crm_lead': it matches 10500 rows, and 'beforeUpdate' hooks are contracted to fire
  PER ROW on a predicate write (ADR-0058, bulk-write addendum), which is over the 10000-row ceiling for one write.
  Nothing was written. Narrow the predicate so the batch matches fewer rows (paginate the write), or remove the
  'beforeUpdate' hook from this object.

claimed: 0.

What it is and is not

Not a regression in reachable population. The pre-#14530 code scanned at limit: 10_000 per predicate, so it too could only ever claim 10 000 rows per predicate per run.

It is a change in what happens past that line. The old code claimed the first 10 000 rows silently and left the rest; the new code claims none of them and logs the engine's refusal. The old half-claimed state is also not self-healing: bootstrapPlatformAdmin short-circuits at already_have_admin (bootstrap-platform-admin.ts:412) before the claim on every later pass, so nothing re-runs the claim automatically once an admin exists. Loud-and-total was judged better than silent-and-partial for #14530's purpose, but neither one finishes the job for a large seeded dataset.

Why it is left open rather than solved

The obvious fix — page the claim — is the shape #14530 deliberately deleted, so it needs a decision rather than a reflex. Sketch of the options, unranked:

  1. Leave it. An app seeding more than 10 000 rows of one object with no owner_id is outside anything shipped; the operator now gets an actionable message naming the remedy.
  2. Page the predicate write — repeat the same predicate until it resolves 0 affected rows, with a bounded number of rounds. Each round is still one set-based write, so it keeps claimSeedOwnership writes up to 20k single-id system updates in a loop, so per-record sharing materialisation cannot batch them #14530's win; it re-introduces a loop, but over writes rather than over rows, and the predicate is self-consuming so the rounds terminate.
  3. Raise or exempt the ceiling for this path. Almost certainly wrong — the ceiling is a per-row hook fan-out bound the sharing hooks are exactly the reason for, and exempting a system writer from it is the shape 审批回写(系统身份)不触发共享规则物化,「批准后团队看不见」——平台只记一条日志、无补偿、无声明式手段 #13533 just closed on the other side.

Wants a maintainer or triage call, not an implementer's.

Where to look

  • packages/plugins/plugin-security/src/claim-seed-ownership.ts — the two predicate writes and the per-predicate warn.
  • packages/spec/src/data/bulk-write-hook-conformance.tsMAX_BULK_PER_ROW_HOOK_ROWS, D6, resolveBulkPerRowHookBudget.
  • packages/objectql/src/engine.tsassertBulkPerRowHookBudget, raised before the driver call so nothing is written.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions