BE-2: Use the Postgres clock for writing timestamps and resolving temporal axes - #9111
BE-2: Use the Postgres clock for writing timestamps and resolving temporal axes#9111claude[bot] wants to merge 10 commits into
Conversation
…poral axes Temporal axes were resolved with the graph host's clock while ontology writes stamp rows with the Postgres clock, so any drift between the two made archive/unarchive-then-read return stale results. The database clock is now the single time authority: - `determine_actor` gains a sibling `determine_actor_with_timestamp` which returns `statement_timestamp()` from the same statement, and `PolicyComponents` carries that reading so every operation gets a database clock value without an extra round trip. - All production `QueryTemporalAxesUnresolved::resolve()` call sites now use `resolve_with` with the operation's database timestamp; the parameterless `resolve()` is removed so the host clock can no longer leak into query resolution. - Entity writes source their transaction time from the database: `create_entities` from its first in-transaction statement, `patch_entity` from the locking statement (which now filters on and returns `statement_timestamp()`), and entity deletion from a `current_timestamp` read on the write transaction. - Adds an archive/unarchive round-trip integration test and a test asserting resolved axes come from the database clock.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
2 Skipped Deployments
|
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #9111 +/- ##
==========================================
- Coverage 59.55% 59.35% -0.21%
==========================================
Files 1408 1407 -1
Lines 137734 136311 -1423
Branches 6418 6382 -36
==========================================
- Hits 82028 80904 -1124
+ Misses 54711 54416 -295
+ Partials 995 991 -4
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
`yarn install --immutable` rejects the install when a package.json declares a dependency the lockfile does not record. Also import `Cow` from `alloc` to match the integration-test crate's convention.
- add the `timestamp` field to the remaining `PolicyComponents` test initializer - drop the `EntityTypeStore` trait import which is no longer used now that the entity read paths call the inherent `get_closed_multi_entity_types_impl` - expect `clippy::too_many_lines` on `execute_entity_deletion`
Clippy's `missing_const_for_fn` flags `set_timestamp`, and making it const makes `with_timestamp` const-eligible as well.
Ontology create/archive/unarchive statements stamped rows with inline SQL `now()`, which is frozen for the lifetime of a transaction. Inside a shared transaction — such as the integration-test harness's rollback transaction — archiving a type created earlier in the same transaction therefore produced an empty interval, which cannot be decoded when the statement returns it. The statements now take the operation's database clock reading as a parameter, like the entity write paths: archive/unarchive reuse the reading carried by their `PolicyComponents`, while create/update fetch one at the start of their transaction. All statements of one operation keep sharing a single value, and separate operations get distinct stamps even within one transaction.
Benchmark results
|
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2002 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 1002 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 3314 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 1527 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 2078 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 1033 | Flame Graph |
policy_resolution_medium
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 102 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 269 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 108 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 133 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 63 | Flame Graph |
policy_resolution_none
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 8 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 3 | Flame Graph |
policy_resolution_small
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 26 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 94 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 27 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 66 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 29 | Flame Graph |
read_scaling_complete
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id;one_depth | 1 entities | Flame Graph | |
| entity_by_id;one_depth | 10 entities | Flame Graph | |
| entity_by_id;one_depth | 25 entities | Flame Graph | |
| entity_by_id;one_depth | 5 entities | Flame Graph | |
| entity_by_id;one_depth | 50 entities | Flame Graph | |
| entity_by_id;two_depth | 1 entities | Flame Graph | |
| entity_by_id;two_depth | 10 entities | Flame Graph | |
| entity_by_id;two_depth | 25 entities | Flame Graph | |
| entity_by_id;two_depth | 5 entities | Flame Graph | |
| entity_by_id;two_depth | 50 entities | Flame Graph | |
| entity_by_id;zero_depth | 1 entities | Flame Graph | |
| entity_by_id;zero_depth | 10 entities | Flame Graph | |
| entity_by_id;zero_depth | 25 entities | Flame Graph | |
| entity_by_id;zero_depth | 5 entities | Flame Graph | |
| entity_by_id;zero_depth | 50 entities | Flame Graph |
read_scaling_linkless
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | 1 entities | Flame Graph | |
| entity_by_id | 10 entities | Flame Graph | |
| entity_by_id | 100 entities | Flame Graph | |
| entity_by_id | 1000 entities | Flame Graph | |
| entity_by_id | 10000 entities | 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
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1
|
Flame Graph |
representative_read_entity_type
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| get_entity_type_by_id | Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba
|
Flame Graph |
representative_read_multiple_entities
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_property | traversal_paths=0 | 0 | |
| entity_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=0 | 0 | |
| link_by_source_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true |
scenarios
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| full_test | query-limited | Flame Graph | |
| full_test | query-unlimited | Flame Graph | |
| linked_queries | query-limited | Flame Graph | |
| linked_queries | query-unlimited | Flame Graph |
Route the entities-table read path's snapshot instants through the operation's database clock reading instead of the process clock, and update the newly added compiler tests to resolve their temporal axes explicitly.
PR SummaryHigh Risk Overview Authorization / policy context: Temporal resolution: Postgres store: Entity and ontology reads, permission checks, validation Tests: Ontology archive/unarchive round-trip and a test that subgraph resolved pinned time is bracketed by two DB clock readings. Reviewed by Cursor Bugbot for commit b9bddb8. Bugbot is set up for automated code reviews on this repo. Configure here. |
… components has_permission_for_entities_impl accepted the surrounding operation's clock reading but only used it to resolve the temporal axes. For an already-resolved AuthenticatedActor::Id the policy components builder therefore found no timestamp and issued a standalone SELECT statement_timestamp(), whose result was then discarded. Pass the reading to the builder instead, so the entity read path with include_permissions performs no extra clock round trip.
TimDiekmann
left a comment
There was a problem hiding this comment.
We should add this to the missing endpoints as well as you highlighted in slack.
|
|
||
| let temporal_axes = params | ||
| .temporal_axes | ||
| .resolve_with(timestamp.unwrap_or_else(|| policy_components.timestamp())); |
There was a problem hiding this comment.
This implies that the timestamp is always resolved in the policy components, but it may not be needed?
There was a problem hiding this comment.
It is always resolved: PolicyComponents::timestamp is Timestamp<()>, not Option (policies/components.rs:69), so every build ends with a reading whether or not the operation goes on to use one. The cost is not uniform though — for an unresolved actor it rides along on the lookup statement and is free; only an already-resolved actor with no supplied reading pays a statement of its own.
This call site always needs one regardless: the temporal axes are resolved with it two lines down (postgres/knowledge/entity/mod.rs:1146-1148). The unwrap_or_else you flagged is gone — the caller's reading is forwarded into the builder instead, so the permission check runs on the caller's instant rather than a second one.
Making it lazy would mean timestamp() becoming async and fallible, and a reading taken later than the components' other statements would break the "all timestamps in one operation agree" property this PR is after. So I kept it eager and stopped the docs implying it is free: policies/components.rs:93 and :374-378.
Generated by Claude Code
There was a problem hiding this comment.
My concern was why we pass in a timestamp when policy components always have one. Getting a timestamp should always happen inside of a transaction, so they should never differ as well, no?
There was a problem hiding this comment.
You're right for now()/transaction_timestamp(), which is frozen for the whole transaction — two reads inside one transaction cannot differ, and the parameter would be dead weight. But the store's clock is statement_timestamp(), not now(): postgres/mod.rs:1313 and the statement_timestamp() column in determine_actor_with_timestamp at postgres/mod.rs:1293. That advances per statement, so two reads in one transaction do differ.
That matters here because one request builds PolicyComponents twice, not once: query_entities_impl builds for ViewEntity at entity/mod.rs:757, then has_permission_for_entities_impl builds a second one for UpdateEntity at entity/mod.rs:1133 (same shape in query_entity_subgraph_impl at :834 and :1010). Both do run inside the one REPEATABLE READ, READ ONLY transaction opened in query_entities at :1862, so under now() they'd agree by construction. Under statement_timestamp() they don't, and the entities would be read at one instant while permissions resolved at another — hence threading the first reading into the second build.
The only reason we're on statement_timestamp() is the test harness: tests/graph/integration/postgres/lib.rs:222 runs each test inside a single transaction that is rolled back, so a frozen now() gives a create and a subsequent archive in one test the identical stamp, and Postgres canonicalises tstzrange(T, T, '[)') to empty. That's the constraint recorded in TODO(BE-688) at postgres/mod.rs:3404.
So the real choice is yours: keep statement_timestamp() and thread the reading, or move to now() and delete the parameter — self-consistent and clearly nicer, but it can't land until the harness uses per-test databases (BE-688). I'd keep the threading for now and drop the parameter as part of BE-688, since that's the point at which the simplification is actually free. One caveat either way: the ontology archive/unarchive paths don't open an explicit transaction, so each statement is its own — the reading still has to be passed explicitly there whichever clock function we pick.
Generated by Claude Code
There was a problem hiding this comment.
The only reason we're on statement_timestamp() is the test harness
This contradicts the reason why we're doing this in the first place. We must not change the code expectation to meet a test, that's reversed. If we read different timestamps in a single call. I'd say we block this on BE-688. Not a fan, but honest.
There was a problem hiding this comment.
Agreed — the harness shouldn't be setting the semantics, and blocking on BE-688 is the honest call.
End state: now() as the single clock, Option<Timestamp<()>> off has_permission_for_entities_impl, with_timestamp/set_timestamp off the builder, and PolicyComponents resolving one reading per transaction.
One caveat on reaching it, because it isn't uniform across the write paths. Create and update already build their components inside the transaction — data_type.rs:511 opens it, :528 builds on &transaction, :736 commits; update is :956/:1033/:1192 — so now() gives those one reading per operation and the parameter simply goes away. Archive and unarchive do not: archive_data_type builds on self at data_type.rs:1222 and unarchive_data_type at :1268, with no begin_transaction anywhere in either, so each statement is its own transaction and now() in the policy lookup and now() in the write are different values. Same shape in property_type.rs:1011/:1059 and entity_type.rs:1813/:1861. Threading would survive the switch on those six unless they're wrapped first.
The wrapping itself is mechanical — it's the create/update shape, and PolicyComponents::builder(&transaction) is already the established pattern — but it moves the policy read inside the write transaction, so I'd treat it as its own small change with its own review rather than something that rides along with the now() switch.
Which would you prefer: hold this PR as-is and do the now() switch once BE-688 lands, or make the switch now and accept the ontology integration tests being red until it does?
Generated by Claude Code
Each arm of the actor match yields a concrete clock reading, so the builder no longer carries an `Option` past the lookup. Behaviour is unchanged: a supplied reading is taken as-is, an unresolved actor captures one on its lookup statement, and an already-resolved actor without one reads the clock on its own. The entities-table page reads its closed entity types at the operation's instant instead of at a clock reading of their own, saving a round trip and keeping the type chips consistent with the rows they describe.
Audited the entities-table endpoint from #9094 end to end. Findings, with the one gap fixed in e927367: Gap found and fixed. Clean, verified by reading:
The three sites left out of the earlier pass are all test-only — Repo-wide sweep: no production host-clock reads remain under Left for you to decide: the same trait method still resolves its own instant for its two other callers — If you meant a different endpoint by "missing endpoints", name it and I'll take it in this PR. Generated by Claude Code |
The archive statement matched any edition whose transaction time contained the supplied reading. Since that reading is now captured while building the policy components rather than by the update statement itself, a concurrent archive holding an older reading could match an edition another archive had already closed and rewind its upper bound and provenance. Restrict the update to the edition which is still open. The containment check stays so a reading taken before the edition opened cannot produce an inverted range. Also describe when the plain actor lookup is appropriate rather than listing its call sites.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 43e58c6. Configure here.
The chips were resolved at a clock reading taken during the request. On a continuation page the rows come from the cursor's pinned instants, which are older, so a type edition archived or superseded in between was visible to the row query but absent from the chip query. The lookup then missed an entry it had already resolved from a row and hit the expect in `get_closed_multi_entity_types_impl`. Pass the page's own temporal axes, which are the cursor's instants on a continuation page and the operation's reading on the first one.

Requested by Tim Diekmann · Slack thread
Blocked on BE-688. The design intent is transaction-frozen
now()as the single clock, with no timestamp threaded through operations. That cannot be expressed while the integration harness runs each test in one transaction, where a frozennow()makes a create-then-archive produce an emptytstzrange. This branch therefore usesstatement_timestamp()and passes the operation's reading explicitly. Per-test databases (BE-688) unblock the switch.🌟 What is the purpose of this PR?
Before: the Graph resolved temporal axes (the "now" a query runs against) with the host's clock, while ontology writes stamp rows with the Postgres clock. With any drift between the two (~50 ms is enough), a client could archive an entity type and still get it back from the next read, or unarchive it and have it stay invisible — the read's "now" lands on the wrong side of the interval bound the write just recorded. Entity writes were host-clock-stamped too, so DB-
now()-based lookups (e.g. user-by-email) had the mirror-image race.After: the Postgres clock is the only clock for query-relevant timestamps. Reads resolve their temporal axes with a timestamp read from the database, and all writes — entity and ontology alike — take their transaction time from a database clock reading fetched once per operation and passed to every statement as a parameter. Archive-then-read and unarchive-then-read are consistent regardless of host clock drift, and all timestamps within one operation share a single value.
How (zero extra round trips on the hot paths): every operation already starts by building
PolicyComponents, whose first statement is thedetermine_actoractor lookup. That statement now additionally returnsstatement_timestamp(), andPolicyComponentscarries the reading so allresolve_with(...)calls and write stamps of the operation reuse it.statement_timestamp()(rather thannow()) is used for all database clock reads: it aligns with theREPEATABLE READsnapshot (taken at the transaction's first statement, not atBEGIN) and it still advances per statement inside a shared transaction — the integration-test harness runs each test in one rollback transaction, where a frozennow()gives distinct operations identical timestamps (and empty, undecodable intervals for archive-after-create).🔗 Related links
🔍 What does this change?
hash-graph-authorization:PrincipalStoregainsdetermine_actor_with_timestamp(actor lookup +statement_timestamp()in one statement; for the public actor only the clock is read) andcurrent_timestamp(standalone clock read, used as a fallback).PolicyComponentscarries the database clock reading, exposed viaPolicyComponents::timestamp(); the builder acceptswith_timestamp(...)for callers that already captured the operation's reading.hash-graph-store: the parameterlessQueryTemporalAxesUnresolved::resolve()is removed — callers must supply a timestamp viaresolve_with(...), so the host clock can no longer leak into query resolution.hash-graph-postgres-store, read paths:resolve_with(...)with the operation's database timestamp: entity reads (query_entities,query_entity_subgraph,summarize_entities,get_entity_by_id), ontology reads (query/count/subgraph for data/property/entity types),has_permission_for_*, and theStoreProvidervalidation lookups (which now carry the surrounding operation's timestamp).query_entities/query_entity_subgraphpreviously resolved twice per request (query pass and permission pass) with two different host-clock values; both passes now share the one database reading.hash-graph-postgres-store, write paths:create_entitiestakes its transaction time from its first in-transaction statement (the actor lookup) instead of the host clock before the transaction;remove_nanosecond()is dropped there since Postgres timestamps are µs-precision by construction.patch_entitytakes its transaction time fromlock_entity_edition, whose SQL now filters onCOALESCE($n, statement_timestamp())and returns the reading (LockedEntityEdition::locked_at); a client-supplied decision time is still honoured in the lock'sWHERE.Timestamp::now().create_ontology_temporal_metadata,archive_ontology_type,unarchive_ontology_type, and theircreate_ontology_metadata/update_owned_ontology_idwrappers) no longer stamp with inline SQLnow(): they take the operation's database clock reading as a parameter — archive/unarchive reuse thePolicyComponentsreading, create/update fetch one at the start of their transaction. This gives distinct per-operation stamps even when operations share a transaction, while all statements of one operation still share a single value.Round-trip accounting: authenticated reads and entity writes get the timestamp for free from the existing actor-lookup statement. A standalone
SELECT statement_timestamp()only happens on paths that never ran the lookup first: ontology create/update transactions, public-actor requests,PolicyComponentsbuilt from an already-resolvedActorIdwithout a supplied timestamp (create_web, policy CRUD internals), entity deletion, snapshot restore, and theget_closed_multi_entity_typesREST endpoint. No hot read path pays an extra round trip.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
policy_editionetc.) still stamp with inline SQLnow(). They are not resolved throughQueryTemporalAxesand are read with SQL-sidenow()comparisons on the same database clock, so they are unaffected by the drift this PR fixes; moving them onto the parameterized mechanism would be a mechanical follow-up.[patch.crates-io]git pins (libp2p et al.) are unreachable from the sandbox this was authored in, so localcargo check/clippy/nextestcould not run at all — rustfmt passes and the diff was reviewed carefully, but compile and test verification is deferred to CI. TheCargo.lockchange is the single hand-added dependency edge forhash-graph-authorization→hash-graph-temporal-versioning(workspace members carry no checksums);misewas likewise unavailable, so the generatedpackage.jsonwiring was mirrored by hand exactly like sibling crates.🐾 Next steps
disallowed-methodsguardrail forTimestamp::now()/OffsetDateTime::now_utc()in the store crates could enforce this statically; left out to avoid noise from the legitimate test-only uses.PolicyComponentsbuilding and should keep returning the clock reading.Interval::from_sqlstill panics (unimplemented!) on empty ranges. No write path can produce one anymore (per-operation stamps are strictly increasing within a transaction), but decoding one defensively as an error would be safer.🛡 What tests cover this?
archive_unarchive_round_tripandresolved_temporal_axes_use_database_clockintests/graph/integration/postgres/data_type.rs. The round-trip test also guards the ontology write stamps: with transaction-frozennow()stamps it fails inside the rollback-transaction harness (archive of a type created in the same test produced an empty, undecodable interval).query/compile/tests.rscovers the compiler against explicitly resolved axes.❓ How to test this?
cargo nextest run --package hash-graph-integration) against a local Postgres.