Skip to content

BE-635: Replace composite-type inserts to support zero-downtime additive migrations#8959

Open
TimDiekmann wants to merge 5 commits into
mainfrom
t/be-635-replace-composite-type-inserts-to-support-zero-downtime
Open

BE-635: Replace composite-type inserts to support zero-downtime additive migrations#8959
TimDiekmann wants to merge 5 commits into
mainfrom
t/be-635-replace-composite-type-inserts-to-support-zero-downtime

Conversation

@TimDiekmann

@TimDiekmann TimDiekmann commented Jul 6, 2026

Copy link
Copy Markdown
Member

🌟 What is the purpose of this PR?

Schema changes must survive the rollout window (graph-migrate done, old binary still serving). Composite row types on the wire encode the table's column count, so any column change on a composite-inserted table broke every insert from the old binary — rolling migrations were impossible and every schema change required a maintenance window.

This PR replaces all composite-type inserts (live write paths and snapshot restore) with explicit column lists and one array parameter per column via unnest. An old binary simply doesn't mention a new nullable/defaulted column and keeps working, so additive migrations roll out with zero downtime.

🔗 Related links

  • BE-635 (internal)
  • BE-625 (internal) — unblocked by this PR
  • BE-634 (internal) — pre-existing restore bug found while validating

🔍 What does this change?

  • PostgresRow produces column–parameter pairs; InsertStatement derives statement, casts, and parameters from that single sequence, so they cannot fall out of order
  • InsertStatementOptions with table-name override, DISTINCT, and an extensible OnConflict enum; the <table>_tmp staging convention lives in the snapshot's insert_rows_batch helper
  • all 32 snapshot batch inserts converted; every composite ToSql derive removed
  • PostgresType becomes the single storage-type authority (built-in names sourced from postgres_types::Type); DatabaseColumn now carries the physical column facts, filter typing moves to the new FilterColumn trait
  • dead code removed along the way (unused nullable(), eight unused row structs)

Pre-Merge Checklist 🚀

🚢 Has this modified a publishable library?

This PR:

  • does not modify any publishable blocks or libraries, or modifications do not need publishing

📜 Does this require a change to the docs?

The changes in this PR:

  • are internal and do not require a docs change

🕸️ Does this require a change to the Turbo Graph?

The changes in this PR:

  • do not affect the execution graph

⚠️ Known issues

  • Column removals/renames still require a two-release drain — only additive changes become free

🐾 Next steps

  • information_schema conformance test for the hand-written column/type mappings
  • DatabaseTable trait linking table → columns → row, separating physical from computed columns
  • snapshot fixtures for teams/AI actors (the only two batch arms without test data)

🛡 What tests cover this?

  • transpile unit tests for the generated insert statements
  • hash-graph-postgres-store integration suite (277 tests) against live Postgres
  • hash-backend-integration snapshot-restore roundtrip (48 tests) and the graph HTTP suite
  • manual dump → wipe → restore → dump equivalence check (canonical JSON comparison)

❓ How to test this?

  1. cargo nextest run --package hash-graph-postgres-store --all-features (needs the compose Postgres + plain hash-graph-migrations)
  2. cd tests/hash-backend-integration && yarn vitest --run src/tests/subgraph (needs the graph with --embed-admin)

…ameters

Composite row types on the wire encode the table's column count, so any
additive migration broke inserts from binaries compiled against the old
schema during rollout. Live write paths now name every inserted column
explicitly and send one array parameter per column via unnest, letting
old binaries coexist with an additively migrated schema.

- rework PostgresRow to produce column-parameter pairs consumed by
  InsertStatement::compile_rows, deriving statement and parameters from
  a single sequence
- declare storage types per insertable column (InsertableColumn)
  instead of deriving them from filter parameter types
- rewrite the embedding upserts to scalar/array parameters and drop the
  local composite row structs
- verify affected row counts of the bulk inserts
- name inserted columns in the positional entity_is_of_type inserts

Snapshot restore still uses composite types and is left for a follow-up.
Replaces the 32 composite-type UNNEST inserts in the snapshot batch
writers with the InsertStatement machinery introduced for the live
write paths, removing the last composite row types on the wire and
all composite ToSql derives.

- extend InsertStatementOptions with a table-name override, DISTINCT,
  and an extensible OnConflict enum; the tmp-table staging convention
  moves into the snapshot's insert_rows_batch helper
- PostgresRow names its table via fn table() -> TableName, allowing
  tables outside the Table enum
- DatabaseColumn now carries the physical column facts with &self
  receivers and no Copy bound; parameter_type moves to the new
  FilterColumn trait; the unused nullable method and eight unused row
  structs are removed
- introduce PostgresType as the single storage-type authority with
  canonical Postgres type names, replacing the ad-hoc cast enum
- add column enums, PostgresRow impls, Table variants, and the
  principal_type/policy_effect types for all snapshot tables

Validated with the crate test suite, the hash-backend-integration
restore roundtrip, the graph HTTP suite, and manual dump/restore
equivalence checks. The latter surfaced a pre-existing bug where the
restore re-applies data type conversions to canonical property values
(BE-634).
Copilot AI review requested due to automatic review settings July 6, 2026 08:29
@vercel

vercel Bot commented Jul 6, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
hash Ready Ready Preview, Comment Jul 7, 2026 9:54am
2 Skipped Deployments
Project Deployment Actions Updated (UTC)
hashdotdesign-tokens Ignored Ignored Preview Jul 7, 2026 9:54am
petrinaut Skipped Skipped Jul 7, 2026 9:54am

@cursor

cursor Bot commented Jul 6, 2026

Copy link
Copy Markdown

PR Summary

High Risk
Large change to core entity, ontology, policy, and snapshot persistence paths; incorrect column/SQL generation could corrupt restores or fail writes under migration rollouts.

Overview
Replaces composite row-type bulk inserts with explicit column lists and one unnest array parameter per column, so an older binary can keep writing during additive schema rollouts without matching the table’s full composite wire shape.

PostgresRow now drives inserts: columnar_parameters builds parallel arrays, InsertStatement::compile_rows (with InsertStatementOptions for DISTINCT, OnConflict, and optional *_tmp table names) generates SQL, and snapshot restore funnels through insert_rows_batch. Snapshot batches for entities, ontology, policy, principals, and actions drop #[derive(ToSql)] on row structs in favor of per-table DatabaseColumn enums.

Live write paths (entity bulk insert, data-type conversion inserts) switch from InsertStatementBuilder + query to execute_raw and assert inserted row counts match input size. Entity and ontology embedding updates stop using temporary composite row types and use scalar/unnest parameters instead.

PostgresType moves to a dedicated module (names aligned with postgres_types::Type); composite RowExpansion insert expressions are removed.

Reviewed by Cursor Bugbot for commit e67d406. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions github-actions Bot added area/libs Relates to first-party libraries/crates/packages (area) type/eng > backend Owned by the @backend team area/tests New or updated tests labels Jul 6, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the Postgres write/query layer (and snapshot restore paths) to avoid composite row types on the wire by generating INSERT ... (col, ...) SELECT * FROM UNNEST($1::<type>[], ...) statements, enabling zero-downtime additive schema migrations where old binaries can keep inserting without referencing newly added nullable/defaulted columns.

Changes:

  • Replaced composite-type bulk inserts with explicit column lists + parallel array parameters via UNNEST, with configurable DISTINCT / ON CONFLICT behavior.
  • Centralized storage-level SQL type rendering in a new PostgresType (built-in names sourced from postgres_types::Type) and updated HashQL filter SQL snapshots accordingly.
  • Refactored snapshot restore batch insert code to use a common helper that targets <table>_tmp staging tables and uses the new insert statement compilation.

Reviewed changes

Copilot reviewed 79 out of 79 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
libs/@local/hashql/eval/tests/ui/postgres/tuple-construction.stdout Updated expected SQL output for new ::bool casts / formatting.
libs/@local/hashql/eval/tests/ui/postgres/struct-construction.stdout Updated expected SQL output for new ::bool casts / formatting.
libs/@local/hashql/eval/tests/ui/postgres/opaque-passthrough.stdout Updated expected SQL output for new ::bool casts / formatting.
libs/@local/hashql/eval/tests/ui/postgres/nested-if-input-branches.stdout Updated expected SQL output for ::int4 / ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/multiple-filters.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/mixed-sources-filter.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/minimal-select-no-extra-joins.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/logical-and-inputs.stdout Updated expected SQL output for ::int4 / ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/list-construction.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/let-binding-propagation.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/input-parameter-load.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/input-parameter-exists.stdout Updated expected SQL output for ::int4 / ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/if-input-branches.stdout Updated expected SQL output for ::int4 / ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/filter/unary_not.snap Updated expected SQL output for ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/filter/unary_neg.snap Updated expected SQL output for ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/filter/unary_bitnot.snap Updated expected SQL output for ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/filter/temporal_decision_time_interval.snap Updated expected SQL output for int4[] array literal typing.
libs/@local/hashql/eval/tests/ui/postgres/filter/switch_int_many_branches.snap Updated expected SQL output for ::int4 / ::bool casts and formatting.
libs/@local/hashql/eval/tests/ui/postgres/filter/straight_line_goto_chain.snap Updated expected SQL output for ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/filter/property_mask.snap Updated expected SQL output for int4[] array literal typing.
libs/@local/hashql/eval/tests/ui/postgres/filter/property_field_equality.snap Updated expected SQL output for ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/filter/nested_property_access.snap Updated expected SQL output for ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/filter/left_entity_filter.snap Updated expected SQL output for ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/filter/island_exit_with_live_out.snap Updated expected SQL output for int4[] array literal typing.
libs/@local/hashql/eval/tests/ui/postgres/filter/island_exit_switch_int.snap Updated expected SQL output for ::int4 / ::bool casts and formatting.
libs/@local/hashql/eval/tests/ui/postgres/filter/island_exit_goto.snap Updated expected SQL output for int4[] array literal typing.
libs/@local/hashql/eval/tests/ui/postgres/filter/island_exit_empty_arrays.snap Updated expected SQL output for int4[] empty array typing.
libs/@local/hashql/eval/tests/ui/postgres/filter/field_index_projection.snap Updated expected SQL output for ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/filter/field_by_name_projection.snap Updated expected SQL output for ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/filter/dynamic_index_projection.snap Updated expected SQL output for ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/filter/diamond_cfg_merge.snap Updated expected SQL output for ::int4 / ::bool casts.
libs/@local/hashql/eval/tests/ui/postgres/filter/binary_sub_numeric_cast.snap Updated expected SQL output for ::bool casts and formatting.
libs/@local/hashql/eval/tests/ui/postgres/filter/binary_bitor_boolean_or.snap Updated expected SQL output for ::bool casts and formatting.
libs/@local/hashql/eval/tests/ui/postgres/filter/binary_bitor_bigint_cast.snap Updated expected SQL output for ::int8 / ::bool casts and formatting.
libs/@local/hashql/eval/tests/ui/postgres/filter/binary_bitand_boolean_and.snap Updated expected SQL output for ::bool casts and formatting.
libs/@local/hashql/eval/tests/ui/postgres/filter/binary_bitand_bigint_cast.snap Updated expected SQL output for ::int8 / ::bool casts and formatting.
libs/@local/hashql/eval/tests/ui/postgres/env-captured-variable.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/entity-web-id-equality.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/entity-uuid-equality.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/entity-type-ids-lateral.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/entity-archived-check.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/dict-construction.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/constant-true-filter.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/postgres/comparison-no-cast.stdout Updated expected SQL output for ::bool casts and line numbering.
libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-tuple.stdout Updated expected debug formatting for decimal significands.
libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-struct.stdout Updated expected debug formatting for decimal significands.
libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-or.stdout Updated expected debug formatting for decimal significands.
libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-and.stdout Updated expected debug formatting for decimal significands.
libs/@local/hashql/eval/src/postgres/parameters.rs Updated parameter kind → Postgres type mapping (Int8, Bool, TstzRange).
libs/@local/hashql/eval/src/postgres/filter/tests.rs Updated test documentation strings for new cast spellings (int8, bool).
libs/@local/hashql/eval/src/postgres/filter/mod.rs Updated filter SQL generation to use Bool/Int4/Int8 types.
libs/@local/graph/postgres-store/src/store/postgres/query/table.rs Refactored column traits and added many table/column storage type definitions.
libs/@local/graph/postgres-store/src/store/postgres/query/statement/mod.rs Re-exported new insert statement types.
libs/@local/graph/postgres-store/src/store/postgres/query/statement/insert.rs Replaced builder-based insert construction with UNNEST-based bulk insert compilation.
libs/@local/graph/postgres-store/src/store/postgres/query/rows.rs Replaced composite-row ToSql derives with columnar parameter generation per table.
libs/@local/graph/postgres-store/src/store/postgres/query/postgres_type.rs Added storage-level Postgres type enum and transpilation logic.
libs/@local/graph/postgres-store/src/store/postgres/query/mod.rs Wired in new postgres_type module and updated exports.
libs/@local/graph/postgres-store/src/store/postgres/query/expression/table_reference.rs Added TableName::as_str() helper used for staging table naming.
libs/@local/graph/postgres-store/src/store/postgres/query/expression/mod.rs Removed PostgresType export from expression module (now centralized).
libs/@local/graph/postgres-store/src/store/postgres/query/expression/conditional.rs Removed old PostgresType definition and row-expansion expression support.
libs/@local/graph/postgres-store/src/store/postgres/query/compile.rs Updated compiler to use new PostgresType and expanded unsupported table list for embeddings.
libs/@local/graph/postgres-store/src/store/postgres/ontology/read.rs Adjusted string handling for column names (to_owned()).
libs/@local/graph/postgres-store/src/store/postgres/ontology/property_type.rs Replaced composite UNNEST($1::<composite>[] ) embedding insert with explicit columns/params.
libs/@local/graph/postgres-store/src/store/postgres/ontology/entity_type.rs Replaced composite UNNEST($1::<composite>[] ) embedding insert with explicit columns/params.
libs/@local/graph/postgres-store/src/store/postgres/ontology/data_type.rs Replaced composite inserts with new InsertStatement + execute_raw and row-count checks; updated embedding insert.
libs/@local/graph/postgres-store/src/store/postgres/knowledge/entity/mod.rs Switched multiple inserts to InsertStatement + execute_raw; updated entity embeddings insert to columnar unnest.
libs/@local/graph/postgres-store/src/snapshot/principal/table.rs Replaced composite row ToSql derives with PostgresRow + per-column metadata for snapshot restore.
libs/@local/graph/postgres-store/src/snapshot/principal/channel.rs Updated comment to match new sender structure.
libs/@local/graph/postgres-store/src/snapshot/principal/batch.rs Migrated principal snapshot batch inserts to shared insert_rows_batch helper.
libs/@local/graph/postgres-store/src/snapshot/policy/table.rs Replaced composite row ToSql derives with PostgresRow + per-column metadata for snapshot restore.
libs/@local/graph/postgres-store/src/snapshot/policy/batch.rs Migrated policy snapshot batch inserts to shared insert_rows_batch helper.
libs/@local/graph/postgres-store/src/snapshot/ontology/property_type/batch.rs Migrated ontology property-type snapshot batch inserts to shared helper + new conflict options.
libs/@local/graph/postgres-store/src/snapshot/ontology/metadata/batch.rs Migrated ontology metadata snapshot batch inserts to shared helper + new conflict options.
libs/@local/graph/postgres-store/src/snapshot/ontology/entity_type/batch.rs Migrated ontology entity-type snapshot batch inserts to shared helper + new conflict options.
libs/@local/graph/postgres-store/src/snapshot/ontology/data_type/batch.rs Migrated ontology data-type snapshot batch inserts to shared helper + new conflict options.
libs/@local/graph/postgres-store/src/snapshot/mod.rs Added SnapshotInsertOptions and insert_rows_batch helper targeting <table>_tmp tables.
libs/@local/graph/postgres-store/src/snapshot/entity/batch.rs Migrated entity snapshot batch inserts to shared helper + new conflict options.
libs/@local/graph/postgres-store/src/snapshot/action/table.rs Replaced composite row ToSql derives with PostgresRow + per-column metadata for snapshot restore.
libs/@local/graph/postgres-store/src/snapshot/action/batch.rs Migrated action snapshot batch inserts to shared helper + new conflict options.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread libs/@local/graph/postgres-store/src/store/postgres/query/table.rs
Comment thread libs/@local/graph/postgres-store/src/store/postgres/query/table.rs
@codecov

codecov Bot commented Jul 6, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 15.36831% with 1597 lines in your changes missing coverage. Please review.
✅ Project coverage is 59.37%. Comparing base (ea86a33) to head (e67d406).
⚠️ Report is 9 commits behind head on main.

Files with missing lines Patch % Lines
...ph/postgres-store/src/store/postgres/query/rows.rs 12.43% 500 Missing ⚠️
...h/postgres-store/src/store/postgres/query/table.rs 27.66% 332 Missing ⚠️
...aph/postgres-store/src/snapshot/principal/table.rs 0.00% 191 Missing ⚠️
.../graph/postgres-store/src/snapshot/policy/table.rs 0.00% 121 Missing ⚠️
.../graph/postgres-store/src/snapshot/entity/batch.rs 0.00% 77 Missing ⚠️
...aph/postgres-store/src/snapshot/principal/batch.rs 0.00% 77 Missing ⚠️
.../graph/postgres-store/src/snapshot/action/table.rs 0.00% 59 Missing ⚠️
...gres-store/src/snapshot/ontology/metadata/batch.rs 0.00% 44 Missing ⚠️
...store/src/snapshot/ontology/property_type/batch.rs 0.00% 44 Missing ⚠️
...res-store/src/snapshot/ontology/data_type/batch.rs 0.00% 33 Missing ⚠️
... and 10 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #8959      +/-   ##
==========================================
- Coverage   59.63%   59.37%   -0.27%     
==========================================
  Files        1354     1355       +1     
  Lines      131373   132272     +899     
  Branches     6021     6024       +3     
==========================================
+ Hits        78344    78535     +191     
- Misses      52099    52807     +708     
  Partials      930      930              
Flag Coverage Δ
apps.hash-ai-worker-ts 1.39% <ø> (ø)
apps.hash-api 6.39% <ø> (ø)
local.hash-backend-utils 2.81% <ø> (ø)
local.hash-graph-sdk 10.00% <ø> (ø)
local.hash-isomorphic-utils 0.18% <ø> (ø)
rust.hash-graph-api 8.23% <ø> (ø)
rust.hash-graph-postgres-store 28.99% <15.02%> (-0.39%) ⬇️
rust.hashql-compiletest 28.40% <ø> (ø)
rust.hashql-eval 79.47% <80.00%> (ø)

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.

…osite-type-inserts-to-support-zero-downtime

# Conflicts:
#	libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-and.stdout
#	libs/@local/hashql/eval/tests/ui/graph/read/entity/arithmetic-comparisons-or.stdout
#	libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-struct.stdout
#	libs/@local/hashql/eval/tests/ui/graph/read/entity/field-access-tuple.stdout
Comment thread libs/@local/graph/postgres-store/src/snapshot/action/table.rs Outdated

@thehabbos007 thehabbos007 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Using unnest should also have slightly better performance in some situations as far as I know. I suggest we also await Bilal's review

@TimDiekmann TimDiekmann enabled auto-merge July 7, 2026 10:10

@indietyp indietyp left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm uncomfortable with the size of the unverified, hand-authored surface here: the 33 columnar_parameters impls are ~950 lines of manual AoS-to-SoA transposition over ~300 hand-written column/type pairs, none of it const-, macro-, or otherwise generated, with virtually 0 coverage reported by codecov over these files. While I appreciate the fact that a future PR may create conformance tests over information schemas, such tests would not catch issues in e.g. columnar_parameters column attribution during transposition.

The benchmarks have also shown a major regression; is this just noise or something more substantial here?

}

fn columnar_parameters<'r>(
rows: &'r [Self],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We started using descriptive names for lifetimes, so I reckon this should be 'rows instead.

pub transaction_time: LeftClosedTemporalInterval<TransactionTime>,
}

use crate::store::postgres::query::{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please move the use statement to the top


/// Transposes `rows` into one array parameter per column.
///
/// Implementations destructure `Self` exhaustively so a new field is a compile error

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would promote this section to a proper # Implementation Note instead of just a description. The conversion into columnar_properties may lead to data corruption if not done carefully, such as properties missing or misalignment (as the description itself states). Additionally, a small declarative macro may help reduce repetitive code throughout this change, which should also reduce the potential hazard of a wrong implementation in the future.

pub role_id: Uuid,
}

use crate::store::postgres::query::{

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please move these at the top (I will stop to mention them all now, but there are a bunch of others)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems odd to me that we're deferring snapshot tests for this, even though we explicitly add the implementation here. Feels like a potential footgun.

fn parameter_type(self) -> ParameterType;
fn nullable(self) -> bool;
fn as_str(self) -> &'static str;
/// A physical column of a database table: its SQL name and storage type.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is incorrect; it is also implemented for, e.g. DataTypeEmbeddings::Distance, which is not a physical column.

fn as_str(self) -> &'static str;
/// A physical column of a database table: its SQL name and storage type.
pub trait DatabaseColumn {
fn as_str(&self) -> &str;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did this degrade to &str instead of &static str? The idea behind using &'static str was that we could ensure that the value is well... static and has been properly escaped. We cannot do that in this case anymore.

rows: &'r [R],
options: InsertStatementOptions,
) -> (String, Vec<Box<dyn ToSql + Send + Sync + 'r>>) {
let (columns, parameters): (Vec<_>, Vec<_>) =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may also want to add a debug assert to ensure that no column is named twice and that the number of columns matches the expected count.

// TODO: Use a builder which knows `values` at compile time
let InsertValueItem::Values(values) = &mut self.statement.values else {
unreachable!()
pub fn compile_rows_with<'r, R: PostgresRow>(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't there a potential footgun here with arrays as arguments and UNNEST? Nothing prevents the user from supplying a PostgresType::Array, which is then unnested.

let mut web_ids = Vec::with_capacity(rows.len());
let mut entity_uuids = Vec::with_capacity(rows.len());
let mut draft_ids = Vec::with_capacity(rows.len());
let mut propertys = Vec::with_capacity(rows.len());

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

properties 😉

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should stop using sed 😂

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Benchmark results

@rust/hash-graph-benches – Integrations

policy_resolution_large

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2002 $$26.6 \mathrm{ms} \pm 180 \mathrm{μs}\left({\color{gray}0.193 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$3.49 \mathrm{ms} \pm 19.3 \mathrm{μs}\left({\color{gray}-0.290 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 1002 $$13.4 \mathrm{ms} \pm 84.9 \mathrm{μs}\left({\color{gray}-3.460 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 3314 $$44.3 \mathrm{ms} \pm 343 \mathrm{μs}\left({\color{gray}-3.295 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$14.8 \mathrm{ms} \pm 125 \mathrm{μs}\left({\color{lightgreen}-7.633 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 1527 $$25.3 \mathrm{ms} \pm 243 \mathrm{μs}\left({\color{gray}-3.382 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 2078 $$27.5 \mathrm{ms} \pm 206 \mathrm{μs}\left({\color{gray}-2.743 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$3.86 \mathrm{ms} \pm 24.9 \mathrm{μs}\left({\color{gray}-1.178 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 1033 $$14.6 \mathrm{ms} \pm 79.5 \mathrm{μs}\left({\color{lightgreen}-5.796 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_medium

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 102 $$3.78 \mathrm{ms} \pm 23.8 \mathrm{μs}\left({\color{gray}-3.515 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$3.02 \mathrm{ms} \pm 15.9 \mathrm{μs}\left({\color{gray}-3.722 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 52 $$3.40 \mathrm{ms} \pm 25.2 \mathrm{μs}\left({\color{lightgreen}-5.754 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 269 $$5.14 \mathrm{ms} \pm 27.1 \mathrm{μs}\left({\color{gray}-4.017 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$3.61 \mathrm{ms} \pm 18.3 \mathrm{μs}\left({\color{lightgreen}-5.255 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 108 $$4.22 \mathrm{ms} \pm 38.7 \mathrm{μs}\left({\color{gray}-4.521 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 133 $$4.42 \mathrm{ms} \pm 30.1 \mathrm{μs}\left({\color{gray}-0.157 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$3.55 \mathrm{ms} \pm 21.5 \mathrm{μs}\left({\color{gray}-3.768 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 63 $$4.17 \mathrm{ms} \pm 36.3 \mathrm{μs}\left({\color{gray}-2.596 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_none

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 2 $$2.71 \mathrm{ms} \pm 14.1 \mathrm{μs}\left({\color{gray}-2.595 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.51 \mathrm{ms} \pm 16.5 \mathrm{μs}\left({\color{lightgreen}-5.439 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 2 $$2.70 \mathrm{ms} \pm 18.3 \mathrm{μs}\left({\color{gray}-1.937 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 8 $$2.95 \mathrm{ms} \pm 12.9 \mathrm{μs}\left({\color{lightgreen}-6.617 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$2.73 \mathrm{ms} \pm 13.0 \mathrm{μs}\left({\color{gray}-4.451 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 3 $$2.97 \mathrm{ms} \pm 18.1 \mathrm{μs}\left({\color{lightgreen}-5.694 \mathrm{\%}}\right) $$ Flame Graph

policy_resolution_small

Function Value Mean Flame graphs
resolve_policies_for_actor user: empty, selectivity: high, policies: 52 $$3.07 \mathrm{ms} \pm 15.4 \mathrm{μs}\left({\color{gray}-1.346 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: low, policies: 1 $$2.81 \mathrm{ms} \pm 17.3 \mathrm{μs}\left({\color{gray}-1.412 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: empty, selectivity: medium, policies: 26 $$2.95 \mathrm{ms} \pm 15.8 \mathrm{μs}\left({\color{gray}-4.296 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: high, policies: 94 $$3.50 \mathrm{ms} \pm 21.6 \mathrm{μs}\left({\color{gray}-1.665 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: low, policies: 1 $$3.06 \mathrm{ms} \pm 14.7 \mathrm{μs}\left({\color{gray}-0.745 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: seeded, selectivity: medium, policies: 27 $$3.32 \mathrm{ms} \pm 20.4 \mathrm{μs}\left({\color{gray}-3.429 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: high, policies: 66 $$3.30 \mathrm{ms} \pm 22.0 \mathrm{μs}\left({\color{lightgreen}-5.702 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: low, policies: 1 $$3.03 \mathrm{ms} \pm 20.9 \mathrm{μs}\left({\color{gray}-2.348 \mathrm{\%}}\right) $$ Flame Graph
resolve_policies_for_actor user: system, selectivity: medium, policies: 29 $$3.24 \mathrm{ms} \pm 17.8 \mathrm{μs}\left({\color{lightgreen}-5.819 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_complete

Function Value Mean Flame graphs
entity_by_id;one_depth 1 entities $$42.3 \mathrm{ms} \pm 234 \mathrm{μs}\left({\color{gray}-4.141 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 10 entities $$33.4 \mathrm{ms} \pm 195 \mathrm{μs}\left({\color{gray}-2.084 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 25 entities $$36.4 \mathrm{ms} \pm 201 \mathrm{μs}\left({\color{gray}-1.017 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 5 entities $$32.4 \mathrm{ms} \pm 234 \mathrm{μs}\left({\color{gray}-0.126 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;one_depth 50 entities $$43.6 \mathrm{ms} \pm 219 \mathrm{μs}\left({\color{gray}0.221 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 1 entities $$49.7 \mathrm{ms} \pm 211 \mathrm{μs}\left({\color{gray}-2.256 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 10 entities $$41.0 \mathrm{ms} \pm 232 \mathrm{μs}\left({\color{gray}1.61 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 25 entities $$93.9 \mathrm{ms} \pm 515 \mathrm{μs}\left({\color{gray}-0.823 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 5 entities $$34.0 \mathrm{ms} \pm 235 \mathrm{μs}\left({\color{gray}-1.857 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;two_depth 50 entities $$314 \mathrm{ms} \pm 939 \mathrm{μs}\left({\color{red}11.0 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 1 entities $$10.5 \mathrm{ms} \pm 68.2 \mathrm{μs}\left({\color{gray}-2.287 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 10 entities $$10.6 \mathrm{ms} \pm 56.0 \mathrm{μs}\left({\color{gray}-4.901 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 25 entities $$10.4 \mathrm{ms} \pm 52.8 \mathrm{μs}\left({\color{lightgreen}-6.830 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 5 entities $$10.4 \mathrm{ms} \pm 65.3 \mathrm{μs}\left({\color{gray}-4.455 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id;zero_depth 50 entities $$10.5 \mathrm{ms} \pm 64.6 \mathrm{μs}\left({\color{gray}-0.713 \mathrm{\%}}\right) $$ Flame Graph

read_scaling_linkless

Function Value Mean Flame graphs
entity_by_id 1 entities $$10.6 \mathrm{ms} \pm 77.1 \mathrm{μs}\left({\color{gray}1.99 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10 entities $$10.5 \mathrm{ms} \pm 66.7 \mathrm{μs}\left({\color{gray}0.251 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 100 entities $$10.8 \mathrm{ms} \pm 58.9 \mathrm{μs}\left({\color{gray}2.26 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 1000 entities $$11.1 \mathrm{ms} \pm 73.2 \mathrm{μs}\left({\color{red}6.25 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id 10000 entities $$11.4 \mathrm{ms} \pm 76.7 \mathrm{μs}\left({\color{red}6.42 \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 $$11.1 \mathrm{ms} \pm 62.1 \mathrm{μs}\left({\color{gray}-1.545 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1 $$10.8 \mathrm{ms} \pm 62.0 \mathrm{μs}\left({\color{gray}-4.167 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1 $$10.9 \mathrm{ms} \pm 67.8 \mathrm{μs}\left({\color{gray}-0.203 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1 $$11.1 \mathrm{ms} \pm 68.1 \mathrm{μs}\left({\color{gray}1.24 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2 $$11.1 \mathrm{ms} \pm 66.0 \mathrm{μs}\left({\color{gray}-0.123 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1 $$11.2 \mathrm{ms} \pm 73.1 \mathrm{μs}\left({\color{gray}0.473 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1 $$10.9 \mathrm{ms} \pm 54.5 \mathrm{μs}\left({\color{gray}-0.489 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1 $$11.0 \mathrm{ms} \pm 58.9 \mathrm{μs}\left({\color{gray}0.368 \mathrm{\%}}\right) $$ Flame Graph
entity_by_id entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1 $$11.1 \mathrm{ms} \pm 68.4 \mathrm{μs}\left({\color{gray}2.31 \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 $$8.59 \mathrm{ms} \pm 53.2 \mathrm{μs}\left({\color{gray}0.691 \mathrm{\%}}\right) $$ Flame Graph

representative_read_multiple_entities

Function Value Mean Flame graphs
entity_by_property traversal_paths=0 0 $$62.6 \mathrm{ms} \pm 541 \mathrm{μs}\left({\color{red}5.59 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=255 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true $$114 \mathrm{ms} \pm 538 \mathrm{μs}\left({\color{gray}1.97 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false $$67.2 \mathrm{ms} \pm 372 \mathrm{μs}\left({\color{gray}4.05 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true $$77.6 \mathrm{ms} \pm 472 \mathrm{μs}\left({\color{gray}4.45 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true $$87.0 \mathrm{ms} \pm 517 \mathrm{μs}\left({\color{gray}3.42 \mathrm{\%}}\right) $$
entity_by_property traversal_paths=2 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true $$94.2 \mathrm{ms} \pm 789 \mathrm{μs}\left({\color{gray}3.04 \mathrm{\%}}\right) $$
link_by_source_by_property traversal_paths=0 0 $$46.1 \mathrm{ms} \pm 204 \mathrm{μs}\left({\color{gray}-2.219 \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 $$79.9 \mathrm{ms} \pm 1.04 \mathrm{ms}\left({\color{gray}0.465 \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 $$52.4 \mathrm{ms} \pm 377 \mathrm{μs}\left({\color{gray}-3.331 \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 $$62.6 \mathrm{ms} \pm 359 \mathrm{μs}\left({\color{gray}-0.096 \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 $$65.9 \mathrm{ms} \pm 478 \mathrm{μs}\left({\color{gray}-0.622 \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 $$65.5 \mathrm{ms} \pm 319 \mathrm{μs}\left({\color{gray}-3.613 \mathrm{\%}}\right) $$

scenarios

Function Value Mean Flame graphs
full_test query-limited $$133 \mathrm{ms} \pm 598 \mathrm{μs}\left({\color{red}9.48 \mathrm{\%}}\right) $$ Flame Graph
full_test query-unlimited $$143 \mathrm{ms} \pm 420 \mathrm{μs}\left({\color{red}7.49 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-limited $$19.4 \mathrm{ms} \pm 237 \mathrm{μs}\left({\color{lightgreen}-5.131 \mathrm{\%}}\right) $$ Flame Graph
linked_queries query-unlimited $$533 \mathrm{ms} \pm 936 \mathrm{μs}\left({\color{gray}0.792 \mathrm{\%}}\right) $$ Flame Graph

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) area/tests New or updated tests type/eng > backend Owned by the @backend team

Development

Successfully merging this pull request may close these issues.

5 participants