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:

awaitql.update(name,{owner_id: adminUserId},{where: {owner_id: null},multi: true,context: SYSTEM_CTX});awaitql.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.

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

      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
       blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
      }
      } catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
      })();
      (function(){
      try {
      var __m = "github.com";
      var __re = new RegExp('^' + "github\\.com" + '
      
      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:

      awaitql.update(name,{owner_id: adminUserId},{where: {owner_id: null},multi: true,context: SYSTEM_CTX});awaitql.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.

      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

          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
          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:

          awaitql.update(name,{owner_id: adminUserId},{where: {owner_id: null},multi: true,context: SYSTEM_CTX});awaitql.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.

          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

              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
              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:

              awaitql.update(name,{owner_id: adminUserId},{where: {owner_id: null},multi: true,context: SYSTEM_CTX});awaitql.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.

              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

                  , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
                  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:

                  awaitql.update(name,{owner_id: adminUserId},{where: {owner_id: null},multi: true,context: SYSTEM_CTX});awaitql.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.

                  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

                      , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                      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:

                      awaitql.update(name,{owner_id: adminUserId},{where: {owner_id: null},multi: true,context: SYSTEM_CTX});awaitql.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.

                      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

                          , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
                          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:

                          awaitql.update(name,{owner_id: adminUserId},{where: {owner_id: null},multi: true,context: SYSTEM_CTX});awaitql.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.

                          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

                              , 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
                              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:

                              awaitql.update(name,{owner_id: adminUserId},{where: {owner_id: null},multi: true,context: SYSTEM_CTX});awaitql.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.

                              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