perf(ensapi): index-accelerate registration-ordered domain queries#2259
Conversation
Materialize the latest registration's start/expiry onto Domain as NOT NULL, sentinel-backed sort columns with (registry_id, col, id) composites, turning REGISTRATION_TIMESTAMP / REGISTRATION_EXPIRY ordering from a join + full-sort (~55s on .eth subdomains) into an index-ordered scan. Hydrate find-domains results directly via a relational query (skip the dataloader round-trip), and add a "most recently registered subdomains" Omnigraph example. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
🦋 Changeset detectedLatest commit: aae4cd9 The changes in this PR will be included in the next version bump. This PR includes changesets to release 24 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Warning Review limit reached
More reviews will be available in 49 minutes and 30 seconds. Learn how PR review limits work. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more reviews become available, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available. Please see our Fair Usage Limits Policy for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (6)
📝 WalkthroughWalkthroughAdds sentinel-backed mirrored registration start/expiry on Domain with composite indexes, maintains mirrors from registration events, refactors find-domains to use mirrored columns (adjusting NULLs behavior), extends GraphQL schema/types/examples, and updates pagination tests and docs. ChangesRegistration Timestamp/Expiry Ordering Optimization
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR eliminates a ~55 s sort-and-join query on REGISTRATION_TIMESTAMP/REGISTRATION_EXPIRY-ordered domain connections by materializing
Confidence Score: 5/5Safe to merge after the required re-index; the sentinel design is sound, all five expiry-mutation sites are correctly wired to the new helper, and the preminted-name domain-absent guard is in place. All five expiry-mutation sites (BaseRegistrar NameRenewed, NameWrapper NameUnwrapped/ExpiryExtended, ENSv2Registry LabelUnregistered/ExpiryUpdated) now route through updateLatestRegistrationExpiry, keeping the domain mirror consistent. The materializeDomainLatestRegistration find-guard correctly handles preminted names whose Domain row does not exist at BaseRegistrar registration time. The sentinel value fits in numeric(78,0), the cursor cast is hardened to that type to avoid the previous bigint overflow, and the relational findMany returns all scalar fields needed by the GraphQL resolvers. Integration tests cover every REGISTRATION pagination permutation in both directions. No files require special attention; the five expiry-mutation handlers in the indexer and the resolver helper are the most logic-dense, but all verified correct. Important Files Changed
Reviews (8): Last reviewed commit: "docs(ensapi): clarify REGISTRATION_TIMES..." | Re-trigger Greptile |
There was a problem hiding this comment.
Pull request overview
This PR accelerates find-domains-backed GraphQL connections when ordering by latest registration start/expiry by materializing those values onto the domains row (sentinel-backed NOT NULL) and switching the resolver to order/paginate directly on those columns (avoiding the latest_registration_indexes → registrations join + sort for registry-scoped queries like .eth subdomains).
Changes:
- Materialize latest registration start/expiry onto
Domain(__latestRegistrationStart/__latestRegistrationExpiry) using a +∞ sentinel and add composite indexes to support ordered scans. - Update ensindexer registration helpers + relevant expiry-mutation handlers to keep the materialized columns correct.
- Update ensapi
find-domainsresolver/helpers/tests and refresh Omnigraph schema docs + examples (including a “recently registered subdomains” example).
Reviewed changes
Copilot reviewed 21 out of 22 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/ensskills/skills/omnigraph/SKILL.md | Adds a skill example query for “recently registered subdomains”. |
| packages/enssdk/src/omnigraph/generated/schema.graphql | Updates schema descriptions for ordering semantics and enum value docs. |
| packages/ensnode-sdk/src/omnigraph-api/example-queries.ts | Adds an example query for registration-timestamp-ordered subdomains. |
| packages/ensdb-sdk/src/ensindexer-abstract/unigraph.schema.ts | Adds sentinel constant, materialized domain columns, and registry+registration composites. |
| docs/ensnode.io/src/data/omnigraph-examples/snapshot.json | Refreshes snapshot metadata (commit/date). |
| docs/ensnode.io/src/data/omnigraph-examples/schema.graphql | Refreshes the vendored schema snapshot. |
| docs/ensnode.io/src/data/omnigraph-examples/responses.json | Adds response fixture for the new example query. |
| docs/ensnode.io/src/data/omnigraph-examples/meta.ts | Adds curated meta entry for the new example. |
| docs/ensnode.io/src/data/omnigraph-examples/examples.json | Adds the new example to the vendored examples snapshot. |
| apps/ensindexer/src/plugins/unigraph/handlers/ensv2/ENSv2Registry.ts | Routes expiry updates through helper to maintain domain materialization. |
| apps/ensindexer/src/plugins/unigraph/handlers/ensv1/NameWrapper.ts | Routes expiry updates through helper to maintain domain materialization. |
| apps/ensindexer/src/plugins/unigraph/handlers/ensv1/BaseRegistrar.ts | Routes renewal expiry updates through helper to maintain domain materialization. |
| apps/ensindexer/src/lib/ensv2/registration-db-helpers.ts | Materializes latest registration values onto Domain + adds expiry-update helper. |
| apps/ensapi/src/test/integration/find-domains/test-domain-pagination.ts | Updates ordering assertions for registration-ordering null placement semantics. |
| apps/ensapi/src/omnigraph-api/schema/registry.ts | Appends shared ordering semantics to connection description; removes context param usage. |
| apps/ensapi/src/omnigraph-api/schema/query.ts | Appends shared ordering semantics to connection description; removes context param usage. |
| apps/ensapi/src/omnigraph-api/schema/domain.ts | Appends shared ordering semantics to connection description; removes context param usage. |
| apps/ensapi/src/omnigraph-api/schema/domain-inputs.ts | Adds enum value descriptions and introduces shared ordering description constant. |
| apps/ensapi/src/omnigraph-api/schema/account.ts | Appends shared ordering semantics to connection description; removes context param usage. |
| apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.ts | Switches to relational hydration + orders by materialized registration columns. |
| apps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver-helpers.ts | Updates order/cursor logic to use materialized columns and numeric casting. |
| .changeset/recently-registered-subdomains-index.md | Changeset describing the perf improvements and sentinel design. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
shrugs
left a comment
There was a problem hiding this comment.
also update the ensdb schema documentation in the docs/ensnode.io project for these new fields and discuss how to query them in an example for the unigraph sql documentation in that same project
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/ensnode.io/src/data/omnigraph-examples/examples.json`:
- Around line 34-40: The examples.json contains example IDs
(account-primary-names, domain-profile, domain-records, domain-registration,
hello-world, namegraph, resolver-by-address, subdomains-pagination) that are not
present in meta.ts and responses.json; update the fixtures to match by adding
corresponding entries for each of these IDs in meta.ts and responses.json (use
the same id strings as in examples.json and populate minimal matching metadata
and mock response objects), or alternatively modify the loader function that
reads meta.ts/responses.json to tolerate missing IDs by skipping or logging them
instead of throwing—ensure the chosen approach keeps id parity between
examples.json and the metadata/response sources and reference the IDs above when
making changes.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: b7e8a4f6-deec-46b9-8675-a1fa5bae8c07
⛔ Files ignored due to path filters (1)
packages/enssdk/src/omnigraph/generated/schema.graphqlis excluded by!**/generated/**
📒 Files selected for processing (21)
.changeset/recently-registered-subdomains-index.mdapps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver-helpers.tsapps/ensapi/src/omnigraph-api/lib/find-domains/find-domains-resolver.tsapps/ensapi/src/omnigraph-api/schema/account.tsapps/ensapi/src/omnigraph-api/schema/domain-inputs.tsapps/ensapi/src/omnigraph-api/schema/domain.tsapps/ensapi/src/omnigraph-api/schema/query.tsapps/ensapi/src/omnigraph-api/schema/registry.tsapps/ensapi/src/test/integration/find-domains/test-domain-pagination.tsapps/ensindexer/src/lib/ensv2/registration-db-helpers.tsapps/ensindexer/src/plugins/unigraph/handlers/ensv1/BaseRegistrar.tsapps/ensindexer/src/plugins/unigraph/handlers/ensv1/NameWrapper.tsapps/ensindexer/src/plugins/unigraph/handlers/ensv2/ENSv2Registry.tsdocs/ensnode.io/src/data/omnigraph-examples/examples.jsondocs/ensnode.io/src/data/omnigraph-examples/meta.tsdocs/ensnode.io/src/data/omnigraph-examples/responses.jsondocs/ensnode.io/src/data/omnigraph-examples/schema.graphqldocs/ensnode.io/src/data/omnigraph-examples/snapshot.jsonpackages/ensdb-sdk/src/ensindexer-abstract/unigraph.schema.tspackages/ensnode-sdk/src/omnigraph-api/example-queries.tspackages/ensskills/skills/omnigraph/SKILL.md
- rename isRegistrationOrdering -> shouldUseNullsLast (inverted), shared with the pagination integration test - drop the eager toSQL() debug log on the find-domains hot path - use query.execute() in the connection span callback (matches codebase convention) - trim resolver/helper comments and rename finalQuery/loadedDomains -> query/domains - drop ensapi from the changeset Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@greptile review |
…preminted names A preminted name (BaseRegistrar registerOnly on Basenames/Lineanames) has a Registration before its Domain row exists, so the unconditional Domain update in insertLatestRegistration/updateLatestRegistrationExpiry would throw. Guard the update behind a find, and materialize the sort keys from the latest Registration when the preminted name's Domain is created (ENSv1Registry NewOwner). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@greptile review |
tk-o
left a comment
There was a problem hiding this comment.
I've tested the update on my local ENSNode instance. The improvement is massive!
…antics in ordering description
|
@greptile review |
Reviewer Focus (Read This First)
find-domains-resolver-helpers.ts(cursorFilter/orderFindDomains) — the trickiest part. registration sort columns are now NOT NULL with a +∞ sentinel, which is what lets a single plain composite serve both directions with no null special-casing.findMany(skipping the Domain dataloader) is acceptable.Problem & Motivation
Domain.subdomains(order: { by: REGISTRATION_TIMESTAMP })on.ethran ~55s vs ~0.3s for the default NAME order.registrations(reached via join throughlatest_registration_indexes) while the filter (registry_id) is ondomains— no index can serve both, so the planner fetched all ~3M.ethsubdomains, joined, sorted, and took 10.REGISTRATION_TIMESTAMPandREGISTRATION_EXPIRY, acrossQuery.domains,Account.domains,Registry.domains,Domain.subdomains.What Changed (Concrete)
Domainas__latestRegistrationStart/__latestRegistrationExpiry, NOT NULL with a +∞ sentinel (REGISTRATION_SORT_SENTINEL = 2^256-1) for absent values (no registration / never expires).(registry_id, __latestRegistrationStart, id)and(registry_id, __latestRegistrationExpiry, id).insertLatestRegistration+ a newupdateLatestRegistrationExpiryhelper wired through all 5 latest-registration expiry-mutation sites (BaseRegistrar, unigraph NameWrapper ×2, ENSv2Registry ×2).findMany(withlabel), dropping the dataloader round-trip; remove the now-unusedcontextparam fromresolveFindDomainsand its 4 call sites.::numeric(78,0)(ponderbigintisnumeric, not int8 —::bigintoverflowed the uint64-max "never expires" sentinel).DomainsOrderByenum values and each find-domains field.Design & Planning
lightweight, iterative; no upfront design doc.
alternatives considered and rejected: (a) 4 indexes (separate ASC + DESC-NULLS-LAST per column) — keeps nullable columns but doubles index cost on the largest table; (b) default NULL placement on nullable columns — breaks index-efficient keyset pagination over the null tail (O(offset) scans + forward/backward inconsistency); (c) exclude no-registration domains — simplest but changes the result set. landed on the NOT-NULL sentinel as the cleanest: same set, index-seekable both directions, no null special-casing.
Planning artifacts: none
Reviewed / approved by: n/a (solo)
Self-Review
::bigintcursor cast overflowed on the uint64-max never-expires expiry (→::numeric(78,0)); a debugconsole.log(JSON.stringify(toSQL()))threw on BigInt params and masked the real error behind yoga's "Unexpected error" (removed); forward/backward keyset inconsistency over the null tail (resolved by the sentinel).usesDefaultNullPlacement→isRegistrationOrdering.needsRegistrationJoin, the two leftJoins,registrationValueById) plus the orphanedcontextparam andDomainInterfaceRef/rejectAnyErrorsimports.Cross-Codebase Alignment
insertLatestRegistration,updateLatestRegistrationExpiry,.set({ expiry,resolveFindDomains,getOrderColumn,latestRegistrationIndexcanonicalName/canonicalDepthare nullable and share the same DESC-index-miss + null-keyset behavior, but it's pre-existing and out of scope here.Downstream & Consumer Impact
REGISTRATION_TIMESTAMP/REGISTRATION_EXPIRYordering onQuery.domains,Account.domains,Registry.domains,Domain.subdomainsis now fast. result set is unchanged — no-registration domains are still included, sorted last for ASC and first for DESC via the sentinel.DomainsOrderByenum value descriptions, find-domains field descriptions, new Omnigraph example.__latestRegistration*(double-underscore marks an internal materialized mirror, not protocol surface; the canonical value stays on the Registration entity).Testing Evidence
EXPLAINon 200k / 2M-row repros confirming index(-only) scans in both directions including keyset pages and the uint64-max sentinel; full integration CI (pnpm test:integration:ci) green — 351 passed, 0 failures, including every REGISTRATION pagination permutation across the 4 fields; ensapi (288) + ensindexer (224) unit; root typecheck + lint.EXPLAINto be meaningful, hence the standalone repro.getLatestRegistration).Scope Reductions
left(canonical_name, 256)expression index has no natural string sentinel).Risk Analysis
findManyskipping the dataloader loses request-scoped cache priming for these connection nodes.Pre-Review Checklist (Blocking)