Skip to content

feat: materialize canonicality in the index, expose in omnigraph#2061

Open
shrugs wants to merge 8 commits intomainfrom
feat/materialize-canonicality
Open

feat: materialize canonicality in the index, expose in omnigraph#2061
shrugs wants to merge 8 commits intomainfrom
feat/materialize-canonicality

Conversation

@shrugs
Copy link
Copy Markdown
Member

@shrugs shrugs commented May 5, 2026

Reviewer Focus (Read This First)

  • bidirectional invariant maintenance in canonicality-db-helpers.tsRegistry.canonicalDomainIdDomain.canonicalSubregistryId, plus the cascade through registryDomains. this is where bugs live.
  • ENSv2Registry.ParentUpdated handler. it's emitted from the child registry and may arrive before the parent registry's LabelRegistered — handled via stub-insert + ensure-registry, but it's worth a second read.
  • bridged resolver attach/detach in handleBridgedResolverChange. detach has to recover the prior target from Domain.canonicalSubregistryId because protocol-acceleration's NewResolver/ResolverUpdated has already overwritten the DRR by the time we run.

Problem & Motivation

  • canonicality was computed at query time via recursive CTEs starting from getRootRegistryIds(). every canonical filter, every name walk, every canonical-path lookup paid the traversal cost.
  • the old registryCanonicalDomain table was a last-write-wins shim with no edge auth on the writer side; the API layer compensated with edge-auth joins everywhere it was consumed. brittle.
  • TODO(canonical-names) markers and a user-facing disclaimer in the enskit example flagged this as a known stopgap. moving canonicality into the index unblocks accurate Domain.canonical/Registry.canonical exposure on the omnigraph schema.

What Changed (Concrete)

  1. schema: add Registry.canonical, Registry.canonicalDomainId, Domain.canonical, Domain.canonicalSubregistryId. drop registryCanonicalDomain. add registryDomains (per-registry child list, single row per registry) so the cascade walks children without scanning domain.
  2. new apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts: setRegistryCanonicalDomain (writes both halves of the bidirectional edge, dislodges prior occupants, cascades), updateRegistryCanonicality (recursive flip over registryDomains), ensureDomainInRegistry/removeDomainFromRegistry, handleBridgedResolverChange.
  3. new apps/ensindexer/src/lib/ensv2/registry-db-helpers.ts: ensureRegistry upsert that seeds canonical = true only for the namespace's primary root registry.
  4. ENSv1 NewOwner switches to ensureRegistry + setRegistryCanonicalDomain + ensureDomainInRegistry. ENSv1 NewResolver now invokes handleBridgedResolverChange (event signature gains resolver).
  5. ENSv2 LabelRegistered switches to ensureRegistry + ensureDomainInRegistry. ENSv2 SubregistryUpdated keeps the raw subregistryId, ensures the registry row exists, leaves canonicality to ParentUpdated. new ENSv2 ParentUpdated handler — child registry claims its canonical parent, ensures parent rows exist (stub-insert if needed) before wiring the edge. new ENSv2 ResolverUpdated handler invokes handleBridgedResolverChange.
  6. omnigraph getCanonicalPath: short-circuits on domain.canonical = false, walks registry.canonicalDomainId upward with no root-set check and no edge-auth join.
  7. omnigraph find-domains: delete canonical-registries-cte.ts. filterByCanonical is now WHERE base.canonical = TRUE. base-domain-set derives parentId from registry.canonicalDomainId directly. filterByName walks via the materialized edge without edge-auth.
  8. omnigraph getDomainByInterpretedName: delete the BridgedResolver recursion branch — bridged attach is now an ordinary canonical edge that walkCanonicalNamegraph follows. ENSv1Resolver fallback retained.
  9. graphql: add canonical: Boolean on Domain and Registry interfaces (and all implementing types). regenerated schema.graphql and introspection.ts.
  10. isBridgedResolver signature is now (namespace, resolver, originatingNode) => BridgedResolverRegistry | null. result is a discriminated union (ENSv1VirtualRegistry / ENSv2Registry) carrying everything needed to upsert the bridged registry. callers in forward-resolution.ts and the Resolver.bridgesTo graphql resolver pass ENS_ROOT_NODE as a sentinel — only chainId, address are projected there.
  11. remove getRootRegistryIds from @ensnode/ensnode-sdk; no consumers remain. getRootRegistryId (singular, primary root) retained.
  12. update ENSv2 Registry ABI to latest (drop Approval event, rename anonymous tokenId output).
  13. enskit SearchView: drop the canonical-derivation disclaimer.

Design & Planning

  • Planning artifacts: none formal. the stopgap was inline-documented via TODO(canonical-names) markers and the enskit disclaimer. materialization was the obvious next step once the bidirectional invariant could be maintained at index time.
  • Reviewed / approved by: n/a — soliciting review now.

Self-Review

  • Bugs caught: setRegistryCanonicalDomain initially didn't dislodge a prior occupant on the new domain (i.e. Domain.canonicalSubregistryId already pointing at a different registry) — added the dislodge-and-cascade path. handleBridgedResolverChange initially read the prior target from the DRR but protocol-acceleration overwrites it first; switched to recovering from Domain.canonicalSubregistryId, which only the bridged path writes for v1 originators.
  • Logic simplified: getDomainByInterpretedName lost the BridgedResolver recursion branch and the shadow/non-shadow path.slice distinction. getCanonicalPath lost the root-set check and the Param-bound array binding. base-domain-set and filterByName lost their edge-auth joins.
  • Naming: registryCanonicalDomain table → registry.canonicalDomainId column reads more directly. BridgedResolverTargetBridgedResolverRegistry to reflect the richer return type.
  • Dead code removed: getRootRegistryIds, canonical-registries-cte.ts, the parentDomain alias, the registryCanonicalDomain table, two TODO(canonical-names) blocks in ENSv2Registry.

Cross-Codebase Alignment

  • Search terms used: registryCanonicalDomain, getRootRegistryIds, BridgedResolverTarget, isBridgedResolver, canonical_domain.
  • Reviewed but unchanged: ensapi find-domains order/limit layers (not affected), ensadmin (no consumers of removed helpers), ponder-subgraph (no canonicality references), ensapi resolution forward path (only the isBridgedResolver call site).
  • Deferred alignment: none identified.

Downstream & Consumer Impact

  • Public APIs affected: Domain.canonical: Boolean and Registry.canonical: Boolean — additive on the omnigraph schema, nullable, safe.
  • Docs updated: inline schema descriptions and code comments. no docs site changes.
  • Naming decisions worth calling out: BridgedResolverTargetBridgedResolverRegistry (internal sdk export). isBridgedResolver signature change is breaking for any in-tree caller; all updated. ensindexer schema changes — re-index required (implicit per ponder semantics).

Testing Evidence

  • Testing performed: existing domain.integration.test.ts for Domain.path still passes; the alias-collapse case now relies on the materialized edge and its old comment about edge-auth in the reverse walk was removed (no longer accurate). new Domain.canonical and Registry.canonical integration tests assert v2-rooted domains/registries are canonical and v1-only fixtures (e.g. addr.reverse, the v1 root registry) are non-canonical. full integration suite + manual omnigraph queries against a freshly-indexed mainnet to confirm canonical filtering matches forward-resolution preference.
  • Known gaps: per-package unit tests are not the convention for ensindexer behavior; cascade correctness is exercised end-to-end by the integration suite.
  • What reviewers have to reason about manually: ordering edge cases for ENSv2 ParentUpdated vs SubregistryUpdated and LabelRegistered. handlers are structured to be order-insensitive (ensure rows, then wire edge), but worth a second read.

Scope Reductions

  • Follow-ups: revisit registryDomains array representation if .eth registry scale makes append-rewrite painful (doubly-linked-list-per-edge is the alternative, documented inline); extend isBridgedResolver for ENSv2 bridges when defined (detach is wired and works today; attach is currently unreachable via the ENSv2 ResolverUpdated path).
  • Why they were deferred: keep this PR focused on the materialization itself.

Risk Analysis

  • Risk areas: cascade depth on large registries (e.g. .eth) — the cascade is recursive and synchronous, hot-path cost grows with subtree size. ordering of v2 ParentUpdated vs LabelRegistered — handler is order-insensitive but the stub-insert path is the load-bearing piece. assumption that the canonical namegraph is a tree (each registry has at most one canonical parent domain) — enforced by the bidirectional invariant + dislodge-on-conflict; no cycle guard in the cascade.
  • Mitigations or rollback options: rollback = revert + re-index. inline NOTE(child-list) documents the array-rewrite tradeoff. setRegistryCanonicalDomain invariants throw loudly on missing rows.
  • Named owner if this causes problems: @shrugs.

Pre-Review Checklist (Blocking)

  • I reviewed every line of this diff and understand it end-to-end
  • I'm prepared to defend this PR line-by-line in review
  • I'm comfortable being the on-call owner for this change
  • Relevant changesets are included (or explicitly not required)

Copilot AI review requested due to automatic review settings May 5, 2026 22:17
@shrugs shrugs requested a review from a team as a code owner May 5, 2026 22:17
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 5, 2026

🦋 Changeset detected

Latest commit: aad2f1c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 24 packages
Name Type
ensapi Major
ensindexer Major
ensadmin Major
ensrainbow Major
fallback-ensapi Major
enssdk Major
enscli Major
enskit Major
ensskills Major
@ensnode/datasources Major
@ensnode/ensrainbow-sdk Major
@ensnode/ensdb-sdk Major
@ensnode/ensnode-react Major
@ensnode/ensnode-sdk Major
@ensnode/integration-test-env Major
@ensnode/ponder-sdk Major
@ensnode/ponder-subgraph Major
@ensnode/shared-configs Major
@docs/ensnode Major
@docs/ensrainbow Major
@namehash/ens-referrals Major
@namehash/namehash-ui Major
@ensnode/ensindexer-perf-testing Major
@ensnode/enskit-react-example Patch

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

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 5, 2026

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

Project Deployment Actions Updated (UTC)
ensnode-enskit-react-example Ready Ready Preview, Comment May 6, 2026 2:13am
3 Skipped Deployments
Project Deployment Actions Updated (UTC)
admin.ensnode.io Skipped Skipped May 6, 2026 2:13am
ensnode.io Skipped Skipped May 6, 2026 2:13am
ensrainbow.io Skipped Skipped May 6, 2026 2:13am

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 5, 2026

Warning

Rate limit exceeded

@shrugs has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 57 minutes and 31 seconds before requesting another review.

To continue reviewing without waiting, purchase usage credits in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8bb2ab18-e664-403b-bd0a-a8f6544ddbf8

📥 Commits

Reviewing files that changed from the base of the PR and between 1426de2 and aad2f1c.

📒 Files selected for processing (9)
  • .changeset/canonical-fields-omnigraph.md
  • apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts
  • apps/ensapi/src/omnigraph-api/schema/account.integration.test.ts
  • apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts
  • apps/ensapi/src/omnigraph-api/schema/query.integration.test.ts
  • apps/ensapi/src/test/integration/devnet-names.ts
  • apps/ensindexer/ponder/src/register-handlers.ts
  • apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
📝 Walkthrough
📝 Walkthrough
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: materialize canonicality in the index, expose in omnigraph' clearly and concisely summarizes the main change—moving canonical-name computations from query-time to materialized index data and exposing it in the API schema.
Description check ✅ Passed The description comprehensively follows the template structure, covering summary (concrete bullet points), motivation, what changed, testing evidence, risk analysis, and pre-review checklist, providing substantial detail appropriate for a large architectural refactor.
Docstring Coverage ✅ Passed Docstring coverage is 85.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/materialize-canonicality

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 moves ENS “canonicality” from query-time recursive traversal into index-time materialization, then exposes the resulting canonical flags and canonical parent/child edges through the Omnigraph GraphQL schema. This reduces query complexity/cost (no canonical registries CTE / edge-auth joins) and enables first-class Domain.canonical / Registry.canonical semantics.

Changes:

  • Materializes canonicality in the indexed DB (Registry.canonical/canonicalDomainId, Domain.canonical/canonicalSubregistryId) plus a per-registry child list (registryDomains) to support cascading canonicality updates.
  • Updates ENSv1/ENSv2 indexer handlers (incl. new ENSv2 ParentUpdated + ResolverUpdated) to maintain the bidirectional canonical edge invariant and bridged-resolver attachments.
  • Simplifies Omnigraph canonical path + domain search/name-walk logic to use the materialized edges/flags; adds canonical fields to GraphQL types and regenerates SDK schema artifacts.

Reviewed changes

Copilot reviewed 20 out of 22 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/enssdk/src/omnigraph/generated/schema.graphql Adds canonical: Boolean to Domain/Registry interfaces and implementing types.
packages/enssdk/src/omnigraph/generated/introspection.ts Regenerates introspection to include new canonical fields.
packages/ensnode-sdk/src/shared/root-registry.ts Removes getRootRegistryIds; clarifies getRootRegistryId as canonical root.
packages/ensnode-sdk/src/shared/protocol-acceleration/is-bridged-resolver.ts Changes bridged-resolver detection to return a richer discriminated union incl. registry id + metadata; requires originatingNode.
packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts Adds canonicality columns + introduces registryDomains table; removes registryCanonicalDomain.
packages/datasources/src/abis/ensv2/Registry.ts Updates ENSv2 Registry ABI (drops Approval event; renames an output).
examples/enskit-react-example/src/SearchView.tsx Updates example search UI copy and query to render only canonical domains.
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts Ensures registries/domains are tracked in registryDomains; adds ParentUpdated and ResolverUpdated canonicality/bridge wiring.
apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts Switches to ensureRegistry + canonicality helpers; wires bridged-resolver changes from ENSv1 NewResolver.
apps/ensindexer/src/lib/ensv2/registry-db-helpers.ts Adds ensureRegistry helper that seeds root registry as canonical.
apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Introduces core canonicality maintenance helpers (bidirectional edge + cascading flips + bridge attach/detach).
apps/ensapi/src/omnigraph-api/schema/resolver.ts Updates Resolver.bridgesTo to new isBridgedResolver signature (uses sentinel node).
apps/ensapi/src/omnigraph-api/schema/registry.ts Exposes Registry.canonical field.
apps/ensapi/src/omnigraph-api/schema/domain.ts Exposes Domain.canonical field.
apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts Updates test commentary to reflect new canonicality mechanics.
apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts Removes query-time bridged-resolver recursion; walks canonical namegraph using materialized edges/flags.
apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts Simplifies canonical path traversal to materialized edges + short-circuit on domain.canonical.
apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name.ts Updates upward traversal to use registry.canonicalDomainId instead of removed table.
apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-canonical.ts Filters via domain.canonical instead of canonical registries CTE.
apps/ensapi/src/omnigraph-api/lib/find-domains/layers/base-domain-set.ts Derives parentId from registry.canonicalDomainId and includes canonical in the base shape.
apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts Deletes no-longer-needed canonical registries recursive CTE.
apps/ensapi/src/lib/resolution/forward-resolution.ts Updates forward-resolution recursion to new isBridgedResolver signature/projection.
Comments suppressed due to low confidence (1)

packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts:290

  • Domain.canonical is now a frequently-used filter predicate (e.g. filterByCanonical becomes WHERE base.canonical = TRUE, and the enskit example queries canonical: true), but the schema doesn’t add an index/partial index on domains.canonical. On large datasets this can turn canonical-only queries into full scans.

Consider adding a partial index like (canonical) WHERE canonical = true (or a composite index that matches your common query patterns) to keep canonical filtering performant.

    // Whether this Domain is part of the canonical namegraph. Mirrors the parent Registry's flag.
    canonical: t.boolean().notNull().default(false),

    // The Subregistry of this Domain that participates in the canonical namegraph (i.e. the
    // Registry whose `canonicalDomainId` points back to this Domain). May differ from
    // `subregistryId` when a Bridged Resolver attaches a different Registry under this Domain.
    canonicalSubregistryId: t.text().$type<RegistryId>(),

    // NOTE: Domain-Resolver Relations tracked via Protocol Acceleration plugin
  }),
  (t) => ({
    byType: index().on(t.type),
    byRegistry: index().on(t.registryId),
    bySubregistry: index().on(t.subregistryId).where(sql`${t.subregistryId} IS NOT NULL`),
    byOwner: index().on(t.ownerId),
    byLabelHash: index().on(t.labelHash),
  }),

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

Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR replaces runtime recursive CTE traversal from getRootRegistryIds() with materialized canonical flags and bidirectional edges (Registry.canonicalDomainIdDomain.canonicalSubregistryId) maintained at index time, and exposes Domain.canonical / Registry.canonical on the omnigraph GraphQL schema.

  • Schema: adds Registry.canonical, Registry.canonicalDomainId, Domain.canonical, Domain.canonicalSubregistryId, and registryDomains (per-registry child list for cascade walks); removes registryCanonicalDomain.
  • Indexer: introduces canonicality-db-helpers.ts with setRegistryCanonicalDomain, updateRegistryCanonicality, ensureDomainInRegistry, and handleBridgedResolverChange; a new ParentUpdated handler on ENSv2Registry claims canonical parentage with order-insensitive stub-insert; bridged-resolver attach/detach is wired in handleBridgedResolverChange via a guaranteed pre-ProtocolAcceleration execution order.
  • Query layer: getCanonicalPath, filterByCanonical, base-domain-set, and filterByName all drop edge-auth joins in favour of the materialized edge; getDomainByInterpretedName drops the BridgedResolver recursion branch since bridged registries now appear as ordinary canonical edges.

Confidence Score: 5/5

Safe to merge; bidirectional invariant maintenance is thorough, the handler registration order is structurally enforced, and the query-layer simplification is a strict improvement. Re-index required per Ponder semantics.

setRegistryCanonicalDomain correctly maintains both halves of the bidirectional edge, ensureDomainInRegistry seeds domain.canonical from the parent registry at insertion time, and updateRegistryCanonicality propagates flips through the registryDomains child list. The ParentUpdated stub-insert handles out-of-order event delivery without stale state. No logic gaps were found after tracing LabelRegistered, ParentUpdated, SubregistryUpdated, and the bridged-resolver attach/detach paths.

canonicality-db-helpers.ts is the highest-risk file — the dislodge path in setRegistryCanonicalDomain is load-bearing and the unconditional cascade of already-non-canonical dislodged registries is a minor inefficiency worth monitoring as registry sizes grow.

Important Files Changed

Filename Overview
apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts New file — core bidirectional invariant maintenance; logic is correct but updateRegistryCanonicality unconditionally cascades the dislodged registry even when it is already non-canonical.
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts Adds ParentUpdated handler with order-insensitive stub-insert pattern and a new ResolverUpdated handler for bridged-resolver detach; SubregistryUpdated correctly defers canonicality to ParentUpdated.
apps/ensindexer/ponder/src/register-handlers.ts Enforces the critical NodeMigration → ENSv2 → ProtocolAcceleration ordering with a clear comment; correctly gates NodeMigration on ProtocolAcceleration since ENSv2 requires it.
apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts Simplified significantly: materialized domain.canonical short-circuit, CTE walks registry.canonicalDomainId without root-set check, post-CTE depth guard throws loudly on invariant violations.
packages/ensnode-sdk/src/shared/protocol-acceleration/is-bridged-resolver.ts Signature gains originatingNode; returns BridgedResolverRegistry discriminated union with full upsert payload; callers pass ENS_ROOT_NODE as a sentinel with clear comments.
packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts Adds canonical/canonicalDomainId to Registry, canonical/canonicalSubregistryId to Domain, replaces registryCanonicalDomain with registryDomains (DomainId[] per registry).
apps/ensindexer/src/lib/ensv2/registry-db-helpers.ts New file — simple ensureRegistry upsert that seeds canonical=true only for the namespace's primary root registry; straightforward and correct.
apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts Switches to ensureRegistry + setRegistryCanonicalDomain + ensureDomainInRegistry; NewResolver gains the resolver arg and calls handleBridgedResolverChange before Protocol Acceleration can overwrite the DRR.
apps/ensindexer/src/plugins/protocol-acceleration/handlers/node-migration.ts Extracts migrateNode listener from ProtocolAcceleration into a standalone module registered first, ensuring nodeIsMigrated is populated before ENSv2 or PA handlers read it.

Sequence Diagram

sequenceDiagram
    participant EV as On-chain Event
    participant PA as Protocol Acceleration
    participant V2 as ENSv2 Plugin
    participant NM as NodeMigration
    participant CH as canonicality-db-helpers
    participant DB as Database

    Note over NM,V2: Registration order: NodeMigration → ENSv2 → ProtocolAcceleration

    EV->>NM: NewOwner (ENSv1Registry)
    NM->>DB: migrateNode(parentNode, label)

    EV->>V2: LabelRegistered (ENSv2Registry)
    V2->>DB: ensureRegistry(registryId, canonical=isRoot)
    V2->>DB: insert domain (onConflictDoUpdate tokenId)
    V2->>CH: ensureDomainInRegistry(registryId, domainId)
    CH->>DB: read registry.canonical → set domain.canonical

    EV->>V2: ParentUpdated (child ENSv2Registry)
    V2->>DB: ensureRegistry(thisRegistry, stub)
    V2->>DB: ensureRegistry(parentRegistry, stub)
    V2->>DB: insert parentDomain stub (onConflictDoNothing)
    V2->>CH: ensureDomainInRegistry(parentRegistry, parentDomain)
    V2->>CH: setRegistryCanonicalDomain(thisRegistry, parentDomain)
    CH->>DB: clear prevCanonicalDomain.canonicalSubregistryId
    CH->>DB: dislodge prevRegistryUnderNewDomain (if any)
    CH->>DB: registry.canonicalDomainId = parentDomain
    CH->>DB: domain.canonicalSubregistryId = thisRegistry
    CH->>CH: updateRegistryCanonicality(thisRegistry, newCanonical)
    CH->>DB: cascade canonical flag to subtree

    EV->>V2: NewResolver (ENSv1Registry, pre-PA)
    V2->>CH: handleBridgedResolverChange(registry, domainId, node, resolver)
    CH->>DB: read prevDRR (before PA overwrites)
    CH->>CH: isBridgedResolver(prevResolver) → detach if needed
    CH->>CH: isBridgedResolver(newResolver) → attach if known bridge
    EV->>PA: NewResolver (overwrites DRR)
    PA->>DB: ensureDomainResolverRelation(...)
Loading

Reviews (5): Last reviewed commit: "fix: re-add MAX_DEPTH guard to getCanoni..." | Re-trigger Greptile

Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
Comment thread apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts (1)

130-148: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Silent fall-through when the deepest resolver is an ENSv2Resolver.

The // TODO: ENSv2Resolver branch on Line 144 means that when deepestResolver is set but is not the ENSv1 fallback resolver, control silently flows into the return exact ? deepest.domainId : null; line. For a non-exact match with an ENSv2Resolver in the middle of the path, callers will get null with no log or error to attribute the miss — making this hard to debug post-merge. Consider at least a logger.debug({ deepestResolver }) or a typed error to aid triage until the ENSv2Resolver hop is implemented.

Want me to open a tracking issue for the ENSv2Resolver hop and add a debug log here in the meantime?

🤖 Prompt for 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.

In `@apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts` around
lines 130 - 148, The deepestResolver branch currently falls through silently for
ENSv2 resolvers; update the TODO: ENSv2Resolver branch to log the resolver
before returning so non-exact misses are debuggable — e.g., call logger.debug({
deepestResolver, path, depth }) (or logger.warn) when deepestResolver exists and
isn't equal to ENSv1Resolver (found via maybeGetDatasourceContract) before the
final return; keep the existing ENSv1Resolver fallback logic
(resolveCanonicalDomainId/getENSv1RootRegistryId) and then return exact ?
deepest.domainId : null as before.
🤖 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 `@apps/ensapi/src/omnigraph-api/schema/domain.ts`:
- Around line 109-113: The Domain.canonical and Registry.canonical GraphQL field
definitions are missing explicit nullable declarations and default to nullable:
true; update the t.field calls for the Domain.canonical resolver (symbol:
canonical in apps/ensapi/src/omnigraph-api/schema/domain.ts) and the
Registry.canonical resolver (symbol: canonical in
apps/ensapi/src/omnigraph-api/schema/registry.ts) to include nullable: false so
the schema reflects the database not-null boolean semantics and matches file
conventions.

In `@apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts`:
- Around line 173-190: handleBridgedResolverChange currently treats
domain.canonicalSubregistryId (prev) as bridge-owned and unconditionally calls
setRegistryCanonicalDomain(prev, null) when a bridged resolver disappears;
instead only detach a canonicalSubregistryId that was created by the bridge for
this originatingNode. Modify handleBridgedResolverChange to determine the set of
bridge-derived registry IDs for the originatingNode (use isBridgedResolver logic
or the same provenance mechanism that creates those registries) and only call
setRegistryCanonicalDomain(context, prev, null) when prev is non-null and
matches one of those bridge-derived IDs; do not clear canonicalSubregistryId for
registries that are normal (non-bridge) parent/child links. Use function
names/vars: handleBridgedResolverChange, canonicalSubregistryId,
setRegistryCanonicalDomain, isBridgedResolver, originatingNode, prev to locate
and implement this conditional check or tracking of bridge provenance.

In `@packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts`:
- Around line 616-619: The registry_domains schema uses a hot-path text[] column
(registryDomains.domainIds) causing full-row rewrites on
ensureDomainInRegistry/removeDomainFromRegistry and expensive reads in
canonicality-db-helpers.ts; replace this with a normalized onchainTable that
stores one row per (registryId, domainId) with a composite primary key, or if
immediate migration is not possible open a follow-up to implement that
normalized table and add instrumentation around cascade write latency and read
hotspots (measure operations in ensureDomainInRegistry/removeDomainFromRegistry
and the cascade walks in canonicality-db-helpers.ts) so we can detect
regressions early.

---

Outside diff comments:
In `@apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts`:
- Around line 130-148: The deepestResolver branch currently falls through
silently for ENSv2 resolvers; update the TODO: ENSv2Resolver branch to log the
resolver before returning so non-exact misses are debuggable — e.g., call
logger.debug({ deepestResolver, path, depth }) (or logger.warn) when
deepestResolver exists and isn't equal to ENSv1Resolver (found via
maybeGetDatasourceContract) before the final return; keep the existing
ENSv1Resolver fallback logic (resolveCanonicalDomainId/getENSv1RootRegistryId)
and then return exact ? deepest.domainId : null as before.
🪄 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: efd22769-1097-48d0-b83a-3042c36ab0d9

📥 Commits

Reviewing files that changed from the base of the PR and between ca39fbe and 3b8eeca.

⛔ Files ignored due to path filters (2)
  • packages/enssdk/src/omnigraph/generated/introspection.ts is excluded by !**/generated/**
  • packages/enssdk/src/omnigraph/generated/schema.graphql is excluded by !**/generated/**
📒 Files selected for processing (20)
  • apps/ensapi/src/lib/resolution/forward-resolution.ts
  • apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts
  • apps/ensapi/src/omnigraph-api/lib/find-domains/layers/base-domain-set.ts
  • apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-canonical.ts
  • apps/ensapi/src/omnigraph-api/lib/find-domains/layers/filter-by-name.ts
  • apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts
  • apps/ensapi/src/omnigraph-api/lib/get-domain-by-interpreted-name.ts
  • apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts
  • apps/ensapi/src/omnigraph-api/schema/domain.ts
  • apps/ensapi/src/omnigraph-api/schema/registry.ts
  • apps/ensapi/src/omnigraph-api/schema/resolver.ts
  • apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
  • apps/ensindexer/src/lib/ensv2/registry-db-helpers.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
  • examples/enskit-react-example/src/SearchView.tsx
  • packages/datasources/src/abis/ensv2/Registry.ts
  • packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts
  • packages/ensnode-sdk/src/shared/protocol-acceleration/is-bridged-resolver.ts
  • packages/ensnode-sdk/src/shared/root-registry.ts
💤 Files with no reviewable changes (2)
  • apps/ensapi/src/omnigraph-api/lib/find-domains/canonical-registries-cte.ts
  • apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts

Comment thread apps/ensapi/src/omnigraph-api/schema/domain.ts
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
Comment thread packages/ensdb-sdk/src/ensindexer-abstract/ensv2.schema.ts
Add integration tests covering Domain.canonical and Registry.canonical:
canonical=true for v2-rooted entities, canonical=false for ENSv1 addr.reverse
and the v1 root registry (which is non-canonical in ens-test-env where the
v2 root is the namespace's canonical root). Add changeset for the omnigraph
schema additions.
vercel[bot]

This comment was marked as resolved.

- Reorder handler registration: NodeMigration -> ENSv2 -> ProtocolAcceleration.
  Lets handleBridgedResolverChange read the previous Domain-Resolver Relation
  before ProtocolAcceleration overwrites it, removing the brittle
  canonicalSubregistryId-as-provenance hack that detached normal canonical
  edges on non-bridged resolver updates.
- Extract node migration into its own handlers/node-migration.ts so it can be
  registered ahead of both plugins.
- handleBridgedResolverChange now takes the registry AccountId and reads prev
  via DRR PK lookup; isBridgedResolver determines bridge provenance.
- Drop unused removeDomainFromRegistry.
- ensureDomainInRegistry throws when registry row is missing (invariant).
- updateRegistryCanonicality: read child once, add MAX_CASCADE_DEPTH=16 guard.
- getCanonicalPath: throw on impossible zero-row result for defense in depth.
- Domain.canonical and Registry.canonical are nullable: false on the omnigraph
  schema (matches notNull DB columns). Regenerate schema.graphql + introspection.
- ParentUpdated: add TODO comment about whether it should also be a domain event.
@shrugs
Copy link
Copy Markdown
Member Author

shrugs commented May 6, 2026

@greptile review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 25 out of 27 changed files in this pull request and generated 4 comments.


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

Comment thread apps/ensindexer/ponder/src/register-handlers.ts Outdated
Comment thread .changeset/canonical-fields-omnigraph.md Outdated
Comment thread apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

♻️ Duplicate comments (1)
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts (1)

374-380: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

ParentUpdated event still not linked to domain history.

The TODO at line 377 acknowledges this gap, and the event row is created at line 378 but never connected to either the child or the parent domain history via ensureDomainEvent. If you intend to associate ParentUpdated with the child domain (the registry that emitted it), the most natural place is thisRegistry's root domain (or the domain whose canonical parent just changed). Otherwise, please convert the TODO into an explicit decision in code so reviewers don't continue flagging it.

🤖 Prompt for 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.

In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts` around
lines 374 - 380, The ParentUpdated event is created via ensureEvent but not
linked to domain history; either attach it to the affected domain or make the
TODO an explicit decision. Fix by calling ensureDomainEvent for the domain whose
canonical parent changed (use the child/domain identifier from the ParentUpdated
payload, e.g. event.node or the registry root for thisRegistry) after
ensureEvent so the event is recorded in domain history; alternatively replace
the TODO with a clear comment/flag that documents the intentional choice not to
link ParentUpdated. Ensure you reference ParentUpdated, ensureEvent and
ensureDomainEvent (and thisRegistry or event.node as the domain identifier) when
making the change.
🤖 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 @.changeset/canonical-fields-omnigraph.md:
- Line 5: The changeset text incorrectly states that Domain.canonical and
Registry.canonical are nullable; update the release note in .changeset to
reflect that both fields are non-nullable (GraphQL type Boolean!), e.g., change
wording to state they are exposed as non-nullable Boolean! fields and clearly
indicate they always return a boolean value, referencing the symbols
Domain.canonical and Registry.canonical so readers know which schema fields were
changed.

In `@apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts`:
- Around line 38-50: The recursive CTE "upward" currently stops at MAX_DEPTH
(16) and can silently return a truncated canonical path; modify the logic around
the "upward" CTE and the final path/name assembly to detect when recursion hit
MAX_DEPTH (i.e., any row with upward.depth >= MAX_DEPTH) and surface an error
instead of returning partial results. Concretely, either add a SQL guard after
the recursion (e.g., an existence check on upward where depth >= MAX_DEPTH that
causes the query to fail) or perform an explicit check in the calling function
(the get-canonical-path flow) after the query and throw a clear error when
MAX_DEPTH was reached; adjust both the upward CTE and the later assembly used
around the same block (also apply to the similar block at the 59-64 region) so
deep ENS names don't get silently truncated.

In `@apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts`:
- Around line 24-29: The MAX_CASCADE_DEPTH constant is set too low and will
falsely abort valid ENS trees because updateRegistryCanonicality() recurses once
per canonical subregistry hop; replace this brittle depth cap with explicit
cycle detection (track visited registry/node identifiers in
updateRegistryCanonicality and bail with a clear error if a repeated id is
encountered) or, if a cap must remain, raise MAX_CASCADE_DEPTH to the protocol's
actual maximum nesting and document it; update any other uses (e.g., the second
occurrence at the other location) to use the new cycle-detection logic or the
revised constant.
- Around line 41-48: The current read-modify-write (existing/domainIds) can lose
concurrent updates; replace it with a single atomic upsert that appends the new
domainId at the DB level instead of reading the full row first. Modify the
insert on ensIndexerSchema.registryDomains via context.ensDb to use
onConflictDoUpdate that sets domainIds =
dedup(array_cat(registry_domains.domainIds, ARRAY[<domainId>]::text[])) (or the
equivalent array_cat/array_append + DISTINCT expression your SQL driver
supports) so the DB merges the new domainId into the existing array atomically;
reference the same registryId conflict key and avoid the prior existing
check/merge in canonicality-db-helpers.ts (remove the existing find/if block and
the client-side domainIds merge). Ensure the expression uses EXCLUDED or the
table column names your query builder exposes to perform the concat+distinct in
the UPDATE clause.
- Around line 41-58: The code inserts/updates registryDomains before confirming
the parent registry exists, which can leave an orphaned child-list on error;
change the order in the function that handles registry/domain linking (the block
using registryId, domainId, context.ensDb and collections
ensIndexerSchema.registryDomains, ensIndexerSchema.registry,
ensIndexerSchema.domain) so you first fetch/validate the parent registry (await
context.ensDb.find(ensIndexerSchema.registry, { id: registryId })) and throw if
missing, and only after that perform the registryDomains insert/update and the
domain update (onConflictDoUpdate for registryDomains and .update(...).set({
canonical: reg.canonical })) to prevent creating the child list when the parent
is absent.

In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts`:
- Around line 389-407: The handler in ENSv2Registry uses `tokenId` typed as
`bigint` and force-casts it with `as never`; update the event arg typing to use
the branded TokenId (e.g. EventWithArgs<{ tokenId: TokenId; resolver:
NormalizedAddress }>) and remove the `as never` casts when calling makeStorageId
and interpretTokenIdAsNode so you pass the TokenId directly; ensure the TokenId
type is imported/available and keep the rest of the logic calling
handleBridgedResolverChange(context, registry, domainId, originatingNode,
resolver) unchanged.

---

Duplicate comments:
In `@apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts`:
- Around line 374-380: The ParentUpdated event is created via ensureEvent but
not linked to domain history; either attach it to the affected domain or make
the TODO an explicit decision. Fix by calling ensureDomainEvent for the domain
whose canonical parent changed (use the child/domain identifier from the
ParentUpdated payload, e.g. event.node or the registry root for thisRegistry)
after ensureEvent so the event is recorded in domain history; alternatively
replace the TODO with a clear comment/flag that documents the intentional choice
not to link ParentUpdated. Ensure you reference ParentUpdated, ensureEvent and
ensureDomainEvent (and thisRegistry or event.node as the domain identifier) when
making the change.
🪄 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: 809c939b-d98f-4942-a4ae-f4edaba89cc8

📥 Commits

Reviewing files that changed from the base of the PR and between 3b8eeca and 1426de2.

⛔ Files ignored due to path filters (2)
  • packages/enssdk/src/omnigraph/generated/introspection.ts is excluded by !**/generated/**
  • packages/enssdk/src/omnigraph/generated/schema.graphql is excluded by !**/generated/**
📒 Files selected for processing (12)
  • .changeset/canonical-fields-omnigraph.md
  • apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts
  • apps/ensapi/src/omnigraph-api/schema/domain.integration.test.ts
  • apps/ensapi/src/omnigraph-api/schema/domain.ts
  • apps/ensapi/src/omnigraph-api/schema/registry.integration.test.ts
  • apps/ensapi/src/omnigraph-api/schema/registry.ts
  • apps/ensindexer/ponder/src/register-handlers.ts
  • apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv1/ENSv1Registry.ts
  • apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
  • apps/ensindexer/src/plugins/protocol-acceleration/handlers/ENSv1Registry.ts
  • apps/ensindexer/src/plugins/protocol-acceleration/handlers/node-migration.ts

Comment thread .changeset/canonical-fields-omnigraph.md Outdated
Comment thread apps/ensapi/src/omnigraph-api/lib/get-canonical-path.ts Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts
Comment thread apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
The wallet Registry's ParentUpdated claims sub1.sub2.parent.eth as its
canonical parent; linked.parent.eth.subregistry was re-pointed to the same
Registry without a corresponding ParentUpdated, so wallet.linked.parent.eth
is now a non-canonical alias that does not resolve via the canonical-name
walk. Update Domain.path tests and DEVNET_NAMES to reflect this.

Also: v1 'eth' Domain has a null canonical name in ens-test-env (v2 root is
the namespace's canonical root), so update Query.domains > sees .eth domain
to assert name: null on the v1 entity. Account.domains expected list flips
wallet.linked.parent.eth → wallet.sub1.sub2.parent.eth.
@vercel vercel Bot temporarily deployed to Preview – ensnode.io May 6, 2026 01:30 Inactive
@vercel vercel Bot temporarily deployed to Preview – ensrainbow.io May 6, 2026 01:30 Inactive
@vercel vercel Bot temporarily deployed to Preview – admin.ensnode.io May 6, 2026 01:30 Inactive
Comment thread apps/ensindexer/src/lib/ensv2/canonicality-db-helpers.ts Outdated
Comment thread .changeset/canonical-fields-omnigraph.md Outdated
/**
* Idempotently link `domainId` into `registryId`'s child list and inherit `canonical` from the
* Registry. If the Domain is already linked, no-op (the cascade in
* {@link updateRegistryCanonicality} keeps existing children's `canonical` consistent).
Copy link
Copy Markdown
Contributor

@vercel vercel Bot May 6, 2026

Choose a reason for hiding this comment

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

Concurrent event processing causes lost updates when adding children to a registry's domain list due to non-atomic read-modify-write pattern in Ponder's in-memory Store API

Fix on Vercel

- Changeset wording: "non-null Boolean!" instead of "nullable Boolean".
- domain.integration.test.ts: tighten Domain.canonical test type to boolean.
- ENSv2Registry.ts: replace `tokenId as never` with TokenId-typed event arg
  and direct usage, matching the rest of the file.
- register-handlers.ts: gate node-migration on ProtocolAcceleration only;
  add comment noting that ProtocolAcceleration is a hard requirement of
  the ENSv2 plugin so the OR was redundant.
- get-canonical-path.ts and updateRegistryCanonicality: drop the fixed
  depth caps. ENS names have no formal depth limit, so a fixed cap would
  silently truncate or abort indexing on legitimately deep namegraphs.
  Termination now relies on the canonical-namegraph-is-a-tree invariant;
  if that invariant is violated, both call sites would loop indefinitely
  — accepted trade-off, called out in inline comments.
ensapi-layer code keeps depth caps so the API can fail loudly. The cap is
detected by allowing the CTE to emit one row beyond MAX_DEPTH and throwing
when that row appears, rather than silently truncating.
@vercel vercel Bot temporarily deployed to Preview – ensrainbow.io May 6, 2026 02:12 Inactive
@vercel vercel Bot temporarily deployed to Preview – admin.ensnode.io May 6, 2026 02:12 Inactive
@vercel vercel Bot temporarily deployed to Preview – ensnode.io May 6, 2026 02:12 Inactive
@shrugs
Copy link
Copy Markdown
Member Author

shrugs commented May 6, 2026

@greptile review

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 28 out of 30 changed files in this pull request and generated 4 comments.


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

Comment on lines +34 to +41
const existing = await context.ensDb.find(ensIndexerSchema.registryDomains, { registryId });
if (existing?.domainIds.includes(domainId)) return;

const domainIds = existing ? [...existing.domainIds, domainId] : [domainId];
await context.ensDb
.insert(ensIndexerSchema.registryDomains)
.values({ registryId, domainIds })
.onConflictDoUpdate({ domainIds });
Comment on lines +123 to +151
* Recursively flip `canonical` on `registryId` and every Domain in its child list (and their
* canonical subtrees).
*
* The recursion is unbounded by design. ENS names have no formal depth limit, so a fixed cap
* would abort indexing on legitimately deep namegraphs. Termination relies on the canonical
* namegraph being a tree (each Registry has at most one canonical parent Domain, enforced by
* the bidirectional invariant `Registry.canonicalDomainId` ↔ `Domain.canonicalSubregistryId`).
* If that invariant is ever violated and a cycle is introduced, this function could recurse
* indefinitely — that is an accepted trade-off for correctness on legitimately deep names.
*/
export async function updateRegistryCanonicality(
context: IndexingEngineContext,
registryId: RegistryId,
canonical: boolean,
): Promise<void> {
await context.ensDb.update(ensIndexerSchema.registry, { id: registryId }).set({ canonical });

const children = await context.ensDb.find(ensIndexerSchema.registryDomains, { registryId });
if (!children) return;

for (const domainId of children.domainIds) {
// Read child once to capture its `canonicalSubregistryId` for the recursion (the field is
// independent of the `canonical` flag we're about to write, so a single PK read suffices).
const child = await context.ensDb.find(ensIndexerSchema.domain, { id: domainId });
await context.ensDb.update(ensIndexerSchema.domain, { id: domainId }).set({ canonical });

const childSubregistry = child?.canonicalSubregistryId ?? null;
if (childSubregistry) {
await updateRegistryCanonicality(context, childSubregistry, canonical);
Comment on lines 13 to 55
@@ -37,21 +41,20 @@ export async function getCanonicalPath(domainId: DomainId): Promise<CanonicalPat

UNION ALL

-- Step upward: domain -> current registry's canonical domain (parent).
-- 1. Recursion stops as soon as we reach a Root Registry or there is no parent to traverse.
-- 2. MAX_DEPTH guards against corrupted state.
-- 3. The pd.subregistry_id = upward.registry_id clause performs edge authentication.
-- Step upward: domain current registry's canonical parent domain.
-- The bidirectional invariant guarantees consistency, so no edge-auth is needed.
-- We allow recursion to one row beyond MAX_DEPTH so we can detect (and throw on) a
-- legitimate path that exceeds the cap, rather than silently truncating it.
SELECT
pd.id AS domain_id,
pd.registry_id,
upward.depth + 1
FROM upward
JOIN ${ensIndexerSchema.registryCanonicalDomain} rcd
ON rcd.registry_id = upward.registry_id
JOIN ${ensIndexerSchema.registry} r
ON r.id = upward.registry_id
JOIN ${ensIndexerSchema.domain} pd
@@ -0,0 +1,5 @@
---
"ensapi": minor
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants