Skip to content

BE-921: Prepare typed entity patch writes - #10126

Draft
montekki wants to merge 1 commit into
fs/be-921-plan-entity-temporal-changesfrom
fs/be-921-prepare-entity-patch-writes
Draft

montekki wants to merge 1 commit into
fs/be-921-plan-entity-temporal-changesfrom
fs/be-921-prepare-entity-patch-writes

Conversation

@montekki

@montekki montekki commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

🌟 Purpose

Each patch mixes authorization, edition creation, draft transitions, and history persistence. Pass authorization inputs explicitly and collect the edition, draft, temporal, and provenance changes in a typed structure before writing them. This lets later layers resolve patches in memory before sending their writes to Postgres.

🔍 Changes

  1. Move draft creation, publication, edition-ID allocation, and temporal transition calculation from the persistence helpers in entity/patch.rs and entity/mod.rs into EntityPatchSet::apply in entity/batch/mod.rs. EntityPatchSet collects editions, draft rows, temporal history, first-publication provenance, and superseded edition IDs.
  2. Replace insert_entity_edition in entity/mod.rs with typed EntityEditionRow construction and insertion in write_entity_patch_set in entity/batch/write.rs. Allocate edition UUIDs in Rust before writing so pending changes can reference them.
  3. Move direct and inherited type-membership inserts and edition-cache population from insert_entity_edition into the shared writer. Move draft inserts, first-publication updates, and superseded-edition provenance updates from the draft/publication helpers into that writer as well.
  4. Pass the policy set and context explicitly to authorize_entity_patch. Extract reusable actor, role, and group data into PrincipalContext and load_principal_context in store/postgres/mod.rs, using the existing authorization context builder.
  5. Change persist_entity_patch to build a patch set for the prepared patch, apply its transition, and call write_entity_patch_set. The outer request still prepares, writes, and validates each patch separately in this frame.

The request still prepares, persists, and validates one patch at a time. Principal data is extracted inside the Postgres store through the existing context builder. Edition IDs are allocated before persistence, and temporal changes use the shared bulk writer.

Validation: 26 selected temporal, patch, draft, and read-only tests passed. Clippy, Rust formatting, embedded SQL formatting, and whitespace checks passed.

@montekki montekki self-assigned this Oct 20, 2026
@github-actions github-actions Bot added area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team labels Oct 20, 2026
@vercel

vercel Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
hash Ready Ready Preview Oct 20, 2026 6:08pm UTC
petrinaut Ready Ready Preview Oct 20, 2026 6:08pm UTC
petrinaut-docs Ready Ready Preview Oct 20, 2026 6:08pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
hashdotdesign-tokens Ignored Ignored Preview Oct 20, 2026 6:08pm UTC

Request Review

@codecov

codecov Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 216 lines in your changes missing coverage. Please review.
✅ Project coverage is 68.01%. Comparing base (b3c7bf4) to head (70a667c).

Files with missing lines Patch % Lines
...src/store/postgres/knowledge/entity/batch/write.rs 0.00% 105 Missing ⚠️
...e/src/store/postgres/knowledge/entity/batch/mod.rs 0.00% 88 Missing ⚠️
...cal/graph/postgres-store/src/store/postgres/mod.rs 0.00% 12 Missing ⚠️
...store/src/store/postgres/knowledge/entity/patch.rs 0.00% 11 Missing ⚠️
Additional details and impacted files
@@                            Coverage Diff                             @@
##           fs/be-921-plan-entity-temporal-changes   #10126      +/-   ##
==========================================================================
- Coverage                                   68.02%   68.01%   -0.02%     
==========================================================================
  Files                                        2008     2009       +1     
  Lines                                      206587   206637      +50     
  Branches                                     8801     8801              
==========================================================================
  Hits                                       140537   140537              
- Misses                                      64379    64429      +50     
  Partials                                     1671     1671              
Flag Coverage Δ
hash-graph 21.07% <ø> (ø)
hash-graph-api 39.82% <ø> (ø)
hash-graph-atlas 80.01% <ø> (ø)
hash-graph-postgres-store 32.05% <0.00%> (-0.08%) ⬇️
hashql-compiletest 28.71% <ø> (ø)
hashql-eval 81.55% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions

Copy link
Copy Markdown
Contributor

Benchmark results

hash-graph-benches – Integrations

policy_resolution_large

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2002 $$27.6 \mathrm{ms} \pm 233 \mathrm{μs}\left({\color{gray}-0.923 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.96 \mathrm{ms} \pm 31.1 \mathrm{μs}\left({\color{gray}1.22 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 1002 $$12.4 \mathrm{ms} \pm 131 \mathrm{μs}\left({\color{gray}-1.451 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 3314 $$39.8 \mathrm{ms} \pm 501 \mathrm{μs}\left({\color{gray}-2.596 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$12.1 \mathrm{ms} \pm 140 \mathrm{μs}\left({\color{gray}-3.625 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 1527 $$22.4 \mathrm{ms} \pm 268 \mathrm{μs}\left({\color{gray}-1.579 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 2078 $$28.5 \mathrm{ms} \pm 453 \mathrm{μs}\left({\color{gray}-0.127 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$3.20 \mathrm{ms} \pm 25.2 \mathrm{μs}\left({\color{gray}0.427 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 1033 $$13.6 \mathrm{ms} \pm 161 \mathrm{μs}\left({\color{gray}1.39 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_medium

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 102 $$3.26 \mathrm{ms} \pm 27.8 \mathrm{μs}\left({\color{gray}-2.237 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.51 \mathrm{ms} \pm 23.1 \mathrm{μs}\left({\color{gray}0.881 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 52 $$2.84 \mathrm{ms} \pm 22.8 \mathrm{μs}\left({\color{gray}-0.922 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 269 $$4.62 \mathrm{ms} \pm 47.5 \mathrm{μs}\left({\color{gray}0.337 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$3.00 \mathrm{ms} \pm 19.6 \mathrm{μs}\left({\color{gray}-0.445 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 108 $$3.58 \mathrm{ms} \pm 28.6 \mathrm{μs}\left({\color{gray}0.228 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 133 $$3.92 \mathrm{ms} \pm 39.8 \mathrm{μs}\left({\color{gray}0.730 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.87 \mathrm{ms} \pm 20.3 \mathrm{μs}\left({\color{gray}-2.964 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 63 $$3.50 \mathrm{ms} \pm 30.3 \mathrm{μs}\left({\color{gray}0.030 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_none

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2 $$2.22 \mathrm{ms} \pm 16.0 \mathrm{μs}\left({\color{gray}-0.758 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.23 \mathrm{ms} \pm 18.4 \mathrm{μs}\left({\color{gray}2.23 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 2 $$2.30 \mathrm{ms} \pm 15.0 \mathrm{μs}\left({\color{gray}-0.244 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 8 $$2.53 \mathrm{ms} \pm 23.0 \mathrm{μs}\left({\color{gray}1.74 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.37 \mathrm{ms} \pm 16.8 \mathrm{μs}\left({\color{gray}0.323 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 3 $$2.57 \mathrm{ms} \pm 21.1 \mathrm{μs}\left({\color{gray}-0.291 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_small

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 52 $$2.58 \mathrm{ms} \pm 16.1 \mathrm{μs}\left({\color{gray}-4.643 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.27 \mathrm{ms} \pm 21.8 \mathrm{μs}\left({\color{gray}-2.346 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 26 $$2.44 \mathrm{ms} \pm 18.2 \mathrm{μs}\left({\color{gray}-1.145 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 94 $$2.95 \mathrm{ms} \pm 32.9 \mathrm{μs}\left({\color{gray}-0.789 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$2.52 \mathrm{ms} \pm 20.8 \mathrm{μs}\left({\color{gray}0.198 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 27 $$2.73 \mathrm{ms} \pm 27.1 \mathrm{μs}\left({\color{gray}-0.721 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 66 $$2.89 \mathrm{ms} \pm 23.7 \mathrm{μs}\left({\color{gray}-0.157 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.51 \mathrm{ms} \pm 16.3 \mathrm{μs}\left({\color{gray}0.469 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 29 $$2.77 \mathrm{ms} \pm 22.9 \mathrm{μs}\left({\color{gray}-0.752 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_complete

Function Value Mean Flame graphs
entity_by_id;one_depth 1 entities $$28.3 \mathrm{ms} \pm 285 \mathrm{μs}\left({\color{gray}-0.523 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 10 entities $$67.8 \mathrm{ms} \pm 502 \mathrm{μs}\left({\color{gray}-1.775 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 25 entities $$31.9 \mathrm{ms} \pm 313 \mathrm{μs}\left({\color{gray}0.244 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 5 entities $$36.6 \mathrm{ms} \pm 334 \mathrm{μs}\left({\color{gray}1.26 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 50 entities $$37.4 \mathrm{ms} \pm 273 \mathrm{μs}\left({\color{gray}-1.116 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 1 entities $$30.2 \mathrm{ms} \pm 233 \mathrm{μs}\left({\color{gray}-0.921 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 10 entities $$423 \mathrm{ms} \pm 1.72 \mathrm{ms}\left({\color{gray}-0.061 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 25 entities $$84.7 \mathrm{ms} \pm 903 \mathrm{μs}\left({\color{gray}0.823 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 5 entities $$83.3 \mathrm{ms} \pm 740 \mathrm{μs}\left({\color{gray}0.963 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 50 entities $$254 \mathrm{ms} \pm 1.57 \mathrm{ms}\left({\color{gray}0.719 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 1 entities $$9.42 \mathrm{ms} \pm 73.4 \mathrm{μs}\left({\color{gray}2.55 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 10 entities $$9.40 \mathrm{ms} \pm 71.9 \mathrm{μs}\left({\color{gray}2.86 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 25 entities $$9.44 \mathrm{ms} \pm 50.8 \mathrm{μs}\left({\color{gray}3.18 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 5 entities $$9.35 \mathrm{ms} \pm 65.6 \mathrm{μs}\left({\color{gray}2.61 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 50 entities $$9.23 \mathrm{ms} \pm 62.2 \mathrm{μs}\left({\color{gray}0.759 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_linkless

Function Value Mean Flame graphs
entity_by_id 1 entities $$9.03 \mathrm{ms} \pm 54.1 \mathrm{μs}\left({\color{gray}0.565 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10 entities $$9.07 \mathrm{ms} \pm 69.1 \mathrm{μs}\left({\color{gray}-0.557 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 100 entities $$9.09 \mathrm{ms} \pm 84.1 \mathrm{μs}\left({\color{gray}0.256 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 1000 entities $$9.41 \mathrm{ms} \pm 67.4 \mathrm{μs}\left({\color{gray}0.431 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10000 entities $$9.69 \mathrm{ms} \pm 73.7 \mathrm{μs}\left({\color{gray}1.70 \mathrm{\%}}\right) $$ Flame Graph

representative_read_entity

Function Value Mean Flame graphs
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1 $$9.51 \mathrm{ms} \pm 70.2 \mathrm{μs}\left({\color{gray}-0.214 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1 $$9.48 \mathrm{ms} \pm 55.1 \mathrm{μs}\left({\color{gray}-1.065 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1 $$9.46 \mathrm{ms} \pm 60.9 \mathrm{μs}\left({\color{gray}-0.637 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1 $$9.47 \mathrm{ms} \pm 60.2 \mathrm{μs}\left({\color{gray}-1.606 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2 $$9.51 \mathrm{ms} \pm 60.8 \mathrm{μs}\left({\color{gray}0.049 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1 $$9.73 \mathrm{ms} \pm 92.4 \mathrm{μs}\left({\color{gray}2.69 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1 $$9.46 \mathrm{ms} \pm 70.7 \mathrm{μs}\left({\color{gray}-1.712 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1 $$9.55 \mathrm{ms} \pm 65.7 \mathrm{μs}\left({\color{gray}0.811 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1 $$9.47 \mathrm{ms} \pm 69.5 \mathrm{μs}\left({\color{gray}-0.056 \mathrm{\%}}\right) $$ Flame Graph

representative_read_entity_type

Function Value Mean Flame graphs
get_entity_type_by_id Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba $$6.76 \mathrm{ms} \pm 48.6 \mathrm{μs}\left({\color{gray}-1.417 \mathrm{\%}}\right) $$ Flame Graph

representative_read_multiple_entities

Function Value Mean Flame graphs
entity_by_property traversal_paths=0 0 $$55.9 \mathrm{ms} \pm 450 \mathrm{μs}\left({\color{gray}-2.380 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=255 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true $$101 \mathrm{ms} \pm 466 \mathrm{μs}\left({\color{gray}-2.429 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false $$61.2 \mathrm{ms} \pm 603 \mathrm{μs}\left({\color{lightgreen}-5.917 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true $$69.0 \mathrm{ms} \pm 540 \mathrm{μs}\left({\color{gray}-2.425 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true $$76.4 \mathrm{ms} \pm 740 \mathrm{μs}\left({\color{gray}-2.333 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true $$83.0 \mathrm{ms} \pm 642 \mathrm{μs}\left({\color{gray}-1.596 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=0 0 $$45.1 \mathrm{ms} \pm 243 \mathrm{μs}\left({\color{gray}-1.142 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=255 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true $$68.3 \mathrm{ms} \pm 493 \mathrm{μs}\left({\color{gray}-0.419 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false $$50.2 \mathrm{ms} \pm 436 \mathrm{μs}\left({\color{gray}-1.391 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true $$57.7 \mathrm{ms} \pm 456 \mathrm{μs}\left({\color{gray}-0.235 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true $$59.4 \mathrm{ms} \pm 393 \mathrm{μs}\left({\color{gray}-0.887 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true $$59.4 \mathrm{ms} \pm 557 \mathrm{μs}\left({\color{gray}-1.425 \mathrm{\%}}\right) $$

scenarios

Function Value Mean Flame graphs
full_test query-limited $$106 \mathrm{ms} \pm 800 \mathrm{μs}\left({\color{gray}-4.223 \mathrm{\%}}\right) $$ Flame Graph
full_test query-unlimited $$115 \mathrm{ms} \pm 741 \mathrm{μs}\left({\color{lightgreen}-6.541 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-limited $$21.0 \mathrm{ms} \pm 136 \mathrm{μs}\left({\color{gray}0.732 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-unlimited $$481 \mathrm{ms} \pm 1.58 \mathrm{ms}\left({\color{gray}-0.268 \mathrm{\%}}\right) $$ Flame Graph

This branch was successfully deployed

3 active deployments
Preview – petrinaut-docs — 70a667c8 Deployed Oct 20, 2026 by vercel[bot]
Preview – hash — 70a667c8 Deployed Oct 20, 2026 by vercel[bot]
Preview – petrinaut — 70a667c8 Deployed Oct 20, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team

Development

Successfully merging this pull request may close these issues.

1 participant