Skip to content

Conversation

@shrugs
Copy link
Collaborator

@shrugs shrugs commented Jan 29, 2026

closes #1565

Reviewer Focus (Read This First)

  • recursive CTE correctness in find-domains.ts - the v1/v2 path traversal logic is the core of this PR
    • this was written by claude but seems functionally correct to me
  • isInterpetedLabel bug fix - previously fell through to isNormalizedLabel for encoded labelhashes

Problem & Motivation

  • materializing canonical names at index-time is impractical due to ponder cache semantics and retroactive label healing
  • need to support "autocomplete" / "search my domains" use case
  • query-time calculation via recursive CTEs is more correct and avoids indexing complexity

What Changed (Concrete)

  1. new findDomains() function with recursive CTEs for v1/v2 domain path traversal
  2. new registryCanonicalDomain schema table for ENSv2 canonical parent tracking
  3. split getCanonicalPath into getV1CanonicalPath and getV2CanonicalPath
  4. new indexes: v1Domain.byLabelHash, v2Domain.byLabelHash, label.byValue
  5. new SDK functions: parsePartialInterpretedName, constructSubInterpretedName, ensureInterpretedLabel, interpretedLabelsToLabelHashPath
  6. fix: isInterpetedLabel now returns early for valid encoded labelhashes
  7. new Query.domains endpoint with where: { name, owner } filter
  8. MAX_DEPTH (16) validation to prevent unbounded CTE recursion

Design & Planning

  • query-time approach chosen over index-time materialization due to ponder constraints
  • inverted CTE traversal (leaf→root) for efficiency vs traversing down from all domains
  • registryCanonicalDomain as interim solution until ENSv2 canonical names are implemented on-chain

Self-Review

  • changed innerJoin to leftJoin on schema.label so owner-only queries don't exclude unlabeled domains

  • added MAX_DEPTH validation after initial implementation

  • Bugs caught: isInterpetedLabel not returning early for encoded labelhashes

  • Logic simplified: n/a

  • Naming / terminology improved: renamed interpretedNameToLabelHashPathinterpretedLabelsToLabelHashPath

  • Dead or unnecessary code removed: n/a


Cross-Codebase Alignment

  • checked get-canonical-path.ts for MAX_DEPTH constant alignment
  • reviewed existing index definitions in schema before adding new ones

Downstream & Consumer Impact

  • new Query.domains(where: { name, owner }) endpoint available (dev-only for now)
  • existing APIs unchanged
  • new schema tables/indexes will require migration
  • Naming decisions worth calling out: concrete vs partial terminology for parsed name segments

Testing Evidence

  • unit tests for parsePartialInterpretedName (30 cases)
  • unit tests for constructSubInterpretedName (14 cases)
  • typecheck and lint pass
  • manual testing against devnet
  • Known gaps: no integration tests for recursive CTEs, no tests for findDomains itself

Scope Reductions

  • integration tests for findDomains

  • LIKE operator input escaping (marked with TODO)

  • production-ready Query.domains endpoint (currently dev-only)

  • Follow-ups: integration tests, input escaping, promote endpoint to production

  • Why they were deferred: focused on core functionality first


Risk Analysis

  • recursive CTE could be slow for deep hierarchies (mitigated by MAX_DEPTH=16)
  • new indexes increase write overhead during indexing
  • registryCanonicalDomain is an interim solution that will need migration

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 January 29, 2026 03:36
@changeset-bot
Copy link

changeset-bot bot commented Jan 29, 2026

🦋 Changeset detected

Latest commit: 65d6ef9

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

This PR includes changesets to release 18 packages
Name Type
ensapi Major
ensindexer Major
ensadmin Major
ensrainbow Major
fallback-ensapi Major
@ensnode/datasources Major
@ensnode/ensrainbow-sdk Major
@ensnode/ponder-metadata Major
@ensnode/ensnode-schema Major
@ensnode/ensnode-react Major
@ensnode/ponder-subgraph Major
@ensnode/ensnode-sdk Major
@ensnode/shared-configs Major
@docs/ensnode Major
@docs/ensrainbow Major
@docs/mintlify Major
@namehash/ens-referrals Major
@namehash/namehash-ui Major

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
Contributor

vercel bot commented Jan 29, 2026

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

Project Deployment Actions Updated (UTC)
admin.ensnode.io Ready Ready Preview, Comment Feb 1, 2026 1:35am
ensnode.io Ready Ready Preview, Comment Feb 1, 2026 1:35am
ensrainbow.io Ready Ready Preview, Comment Feb 1, 2026 1:35am

@coderabbitai
Copy link

coderabbitai bot commented Jan 29, 2026

📝 Walkthrough

Walkthrough

Adds partial-name domain search across ENSv1/v2, separate v1/v2 canonical-path computation and per-request canonical-path dataloaders; exposes Domain.name/path and domains query filters; introduces interpretation utilities for partial names, schema/indexer/index changes, and registry→canonical-domain mappings.

Changes

Cohort / File(s) Summary
Domain search & canonical path
apps/ensapi/src/graphql-api/lib/find-domains.ts, apps/ensapi/src/graphql-api/lib/get-canonical-path.ts
New findDomains({name, owner}) that unionizes v1/v2 recursive-CTE traversals; splits canonical-path logic into getV1CanonicalPath and getV2CanonicalPath; renames ENSv2 root constant to ENSv2_ROOT_REGISTRY_ID.
GraphQL schema & resolvers
apps/ensapi/src/graphql-api/schema/domain.ts, apps/ensapi/src/graphql-api/schema/query.ts, apps/ensapi/src/graphql-api/schema/account.ts, apps/ensapi/src/graphql-api/schema/permissions.ts, apps/ensapi/src/graphql-api/schema/registration.ts, apps/ensapi/src/graphql-api/schema/registry.ts, apps/ensapi/src/graphql-api/schema/resolver.ts
Adds Domain.name and Domain.path resolvers, DomainsWhereInput/AccountDomainsWhereInput, dev-only domains query wired to findDomains; Account.domains accepts where; refactors many where predicates to inline conditional before/after expressions; bridged resolver now uses config-derived namespace.
Interpretation utilities & FQDN pipeline
packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts, apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts, packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.test.ts
Renames interpretedNameToLabelHashPathinterpretedLabelsToLabelHashPath (now takes InterpretedLabel[]); adds parsePartialInterpretedName, ensureInterpretedLabel, constructSubInterpretedName; updates FQDN pipeline and expands tests for partial/encoded-label cases.
Database schema & indexes
packages/ensnode-schema/src/schemas/ensv2.schema.ts
Adds registry_canonical_domains table; adds byLabelHash indexes to v1Domain/v2Domain; adds byValue index on label.
ENSv2 indexer: bridging awareness
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts
Adds bridging detection for L2 ETH registries; computes bridged domain IDs and upserts/deletes registryCanonicalDomain entries (last-write-wins heuristic) on NameRegistered and SubregistryUpdated.
Protocol-acceleration/resolver lookup
apps/ensapi/src/lib/protocol-acceleration/find-resolver.ts
Makes inclusion of RegistryOld branch conditional on registry type instead of unconditional inclusion (changes resolver lookup predicates).
Context, Yoga, and loaders
apps/ensapi/src/graphql-api/context.ts, apps/ensapi/src/graphql-api/yoga.ts, apps/ensapi/src/graphql-api/builder.ts
Adds per-request DataLoaders for v1/v2 canonical-paths and a context() factory; wires imported context into Yoga; adjusts SchemaBuilder Context generic to ReturnType<typeof context>.
Tests/config/deps
packages/datasources/src/ens-test-env.ts, pnpm-workspace.yaml
Updates ENSv1 test contract addresses and bumps ponder from 0.16.1→0.16.2.
Misc schema resolvers
various schema files listed above
Refactors multiple where-clause constructions to use inline conditional expressions for optional before/after pagination predicates (semantic parity).

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Query as GraphQL Query
    participant FindDomains as findDomains()
    participant DB as Database
    participant Loader as Dataloader

    Client->>Query: domains(where: {name?, owner?})
    Query->>FindDomains: findDomains({name, owner})
    FindDomains->>FindDomains: parsePartialInterpretedName → interpretedLabelsToLabelHashPath
    FindDomains->>DB: execute v1 recursive CTE (labelHashPath)
    DB-->>FindDomains: v1 domain IDs
    FindDomains->>DB: execute v2 recursive CTE (registryCanonicalDomain)
    DB-->>FindDomains: v2 domain IDs
    FindDomains->>FindDomains: unionAll(v1, v2) + filters + pagination
    FindDomains-->>Query: paginated Domain IDs
    Query->>Loader: load DomainInterfaceRef by IDs
    Loader->>DB: fetch domain records
    DB-->>Loader: domain data
    Loader-->>Query: DomainInterfaceRef[]
    Query-->>Client: domains connection
Loading
sequenceDiagram
    participant Resolver as Domain.name Resolver
    participant CanonicalPath as getV1CanonicalPath / getV2CanonicalPath
    participant DB as Database
    participant Loader as Dataloader
    participant Interpreter as interpretedLabelsToInterpretedName

    Resolver->>CanonicalPath: request canonical path for domainId
    CanonicalPath->>DB: recursive CTE (v1 via parent_id, v2 via registryCanonicalDomain)
    DB-->>CanonicalPath: ordered domain IDs (leaf→root)
    CanonicalPath-->>Resolver: CanonicalPath[]
    Resolver->>Loader: load domains for path IDs
    Loader->>DB: fetch domain rows
    DB-->>Loader: domain rows
    Loader-->>Resolver: Domain objects
    Resolver->>Interpreter: interpretedLabelsToInterpretedName(labels)
    Interpreter-->>Resolver: Name
    Resolver-->>Client: domain.name
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

ensnode-sdk

Poem

🐰 I hop through labels, hashes, and root,
Stitching v1 and v2 paths, one careful route,
I parse a partial name, chase canonical light,
Union the tracks, and bind day to night,
A rabbit cheers: canonical names take root!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feat/canonical name heuristic' is directly related to the PR's main objective of implementing canonical name materialization using a query-time heuristic approach.
Description check ✅ Passed The PR description comprehensively covers the problem, changes, design decisions, testing, and known gaps, following the repository template with detailed sections on motivation, concrete changes, and risk analysis.
Linked Issues check ✅ Passed The PR fully implements the requirements from issue #1565: canonical name materialization using last-write-wins heuristic, surfaced via Query.domains and Account.domains search endpoints for autocomplete/search use cases.
Out of Scope Changes check ✅ Passed All changes are scoped to implementing the canonical name heuristic feature. Supporting changes (schema updates, SDK utilities, context management, resolver refactoring) are all necessary to enable the core functionality and query-time resolution.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/canonical-name-heuristic

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
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 implements a canonical name heuristic for ENS domains to support domain search and autocomplete functionality. Since ENSv2 canonical names are not yet fully implemented by the ENS team, this PR introduces a "last-write-wins" heuristic for tracking canonical domain references.

Changes:

  • Updates ponder dependency from 0.16.1 to 0.16.2
  • Adds registryCanonicalDomain schema table to track registry-to-canonical-domain mappings using a heuristic approach
  • Implements separate canonical path resolution for ENSv1 (tree-based) and ENSv2 (graph-based with canonical domain tracking)
  • Adds Domain.name and Domain.path GraphQL fields to expose canonical names
  • Introduces findDomains function for domain search/filtering with partial name support (though implementation is incomplete)
  • Refactors SDK functions for better separation of concerns (e.g., interpretedLabelsToLabelHashPath)

Reviewed changes

Copilot reviewed 11 out of 12 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
pnpm-workspace.yaml Updates ponder catalog version to 0.16.2
pnpm-lock.yaml Updates ponder lock entries to version 0.16.2 across all packages and snapshots
packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts Refactors interpretedLabelsToLabelHashPath to accept labels instead of name; adds utility functions for constructing interpreted names, ensuring interpreted labels, and parsing partial interpreted names
packages/ensnode-schema/src/schemas/ensv2.schema.ts Adds registryCanonicalDomain table to track temporary canonical domain heuristic until ENS team implements proper canonical names
packages/datasources/src/ens-test-env.ts Updates test environment contract addresses for ENSv1RegistryOld and ENSv1Registry
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts Implements last-write-wins heuristic for canonical domains in SubregistryUpdated handler; removes debug console.log
apps/ensapi/src/graphql-api/schema/query.ts Adds development-only Query.domains connection for testing domain search functionality
apps/ensapi/src/graphql-api/schema/domain.ts Replaces commented-out canonical name fields with implemented Domain.name and Domain.path fields that use canonical path resolution
apps/ensapi/src/graphql-api/schema/account.ts Refactors Account.domains to use new findDomains helper instead of inline query logic; removes unused unionAll import
apps/ensapi/src/graphql-api/lib/get-domain-by-fqdn.ts Updates function call from interpretedNameToLabelHashPath to interpretedLabelsToLabelHashPath with proper parameter conversion
apps/ensapi/src/graphql-api/lib/get-canonical-path.ts Splits canonical path logic into getV1CanonicalPath (tree-based) and getV2CanonicalPath (uses registry_canonical_domains table); adds ROOT_NODE import
apps/ensapi/src/graphql-api/lib/find-domains.ts Adds new domain filtering/search function supporting name-based and owner-based queries (though name filtering for v2 and partial label matching remain unimplemented)
Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Copy link

@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 (3)
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts (1)

192-206: Clean up stale canonical mappings when subregistries change or are removed.

If a subregistry is cleared or replaced, the previous registryCanonicalDomain row remains, so canonical-path resolution can keep treating the old registry as canonical. Capture the previous subregistryId and delete its mapping before updating/clearing.

🛠️ Suggested fix (remove old mapping before update)
       const registryAccountId = getThisAccountId(context, event);
       const canonicalId = getCanonicalId(tokenId);
       const domainId = makeENSv2DomainId(registryAccountId, canonicalId);
+      const existing = await context.db.find(schema.v2Domain, { id: domainId });
+      const previousSubregistryId = existing?.subregistryId ?? null;

       // update domain's subregistry
       if (subregistry === null) {
+        if (previousSubregistryId) {
+          await context.db.delete(schema.registryCanonicalDomain, {
+            registryId: previousSubregistryId,
+          });
+        }
         await context.db.update(schema.v2Domain, { id: domainId }).set({ subregistryId: null });
       } else {
         const subregistryAccountId: AccountId = { chainId: context.chain.id, address: subregistry };
         const subregistryId = makeRegistryId(subregistryAccountId);

+        if (previousSubregistryId && previousSubregistryId !== subregistryId) {
+          await context.db.delete(schema.registryCanonicalDomain, {
+            registryId: previousSubregistryId,
+          });
+        }
         await context.db.update(schema.v2Domain, { id: domainId }).set({ subregistryId });

         // TODO(canonical-names): this implements last-write-wins heuristic for a Registry's canonical name,
         // replace with real logic once ENS Team implements Canonical Names
         await context.db
packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts (1)

108-128: Update TypeScript target to ES2023 or use a compatible alternative.

The code uses Array.prototype.toReversed() (ES2023), but tsconfig.lib.json declares target: "ES2022". While the project's minimum Node version (24.13.0) natively supports ES2023, this creates a configuration inconsistency. Either update the TypeScript target to ES2023 or replace toReversed() with .reverse() to maintain ES2022 compatibility.

🔧 Alternative for ES2022 compatibility
-    .toReversed();
+    .reverse();
apps/ensapi/src/graphql-api/lib/get-canonical-path.ts (1)

61-63: Update invariant labels to the new helper names.

The thrown error still references getCanonicalPath, which no longer exists, making logs misleading.

🛠️ Proposed fix
-  if (rows.length === 0) {
-    throw new Error(`Invariant(getCanonicalPath): DomainId '${domainId}' did not exist.`);
-  }
+  if (rows.length === 0) {
+    throw new Error(`Invariant(getV2CanonicalPath): DomainId '${domainId}' did not exist.`);
+  }
@@
-  if (rows.length === 0) {
-    throw new Error(`Invariant(getCanonicalPath): DomainId '${domainId}' did not exist.`);
-  }
+  if (rows.length === 0) {
+    throw new Error(`Invariant(getV1CanonicalPath): DomainId '${domainId}' did not exist.`);
+  }

Also applies to: 110-112

🤖 Fix all issues with AI agents
In `@apps/ensapi/src/graphql-api/lib/find-domains.ts`:
- Around line 164-168: findV2Domains currently ignores the DomainFilter.name
(and canonical path), causing unbounded v2 results; update the function to apply
name/canonicalPath filtering (or return no rows when name is provided until
implemented). Specifically, in findV2Domains (which queries schema.v2Domain),
include an additional where clause that filters by eq(schema.v2Domain.name,
name) or eq(schema.v2Domain.canonicalPath, name) when DomainFilter.name is
present, combining with the existing owner filter (e.g., using and(...) or
building an array of conditions) so the query is constrained; alternatively, if
you prefer to fail closed until canonical-path logic exists, have the function
return an empty selection when name is provided.
- Around line 108-160: The function findV1DomainsByName must handle the case
where parsePartialInterpretedName(name) yields an empty concrete array; add an
early-return when concrete.length === 0 that returns all v1 domains (so
owner-only queries still return domains) instead of building the CTE. In
practice, inside findV1DomainsByName, check if concrete.length === 0 and
immediately return a simple query against schema.v1Domain (similar to the
commented-out snippet that selects schema.v1Domain.id), before computing
labelHashPath/rawLabelHashPathArray/pathLength or constructing the recursive
CTE.

In `@apps/ensapi/src/graphql-api/schema/domain.ts`:
- Around line 124-129: The invariant error message inside the canonicalPath
mapping is still labeled "Domain.canonicalName"; update that message to
reference "Domain.name" instead to match the current model. Locate the mapping
over canonicalPath where domains.find((d) => d.id === domainId) is used and
change the thrown Error text (`Invariant(Domain.canonicalName): ...`) to
`Invariant(Domain.name): ...`, preserving the existing Path and DomainId
details.

Copilot AI review requested due to automatic review settings January 30, 2026 02:28
@vercel vercel bot temporarily deployed to Preview – ensnode.io January 30, 2026 02:28 Inactive
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io January 30, 2026 02:28 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io January 30, 2026 02:28 Inactive
Copy link
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 11 out of 12 changed files in this pull request and generated 11 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Copy link

@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

🤖 Fix all issues with AI agents
In `@apps/ensapi/src/graphql-api/lib/find-domains.ts`:
- Around line 121-130: The Promise.all block that runs
db.select().from(v1DomainsByName) and db.select().from(v2DomainsByName) (and
logs v1Domains.toSQL()/v2Domains.toSQL() and full results) should be removed or
gated behind a development/debug flag and protected with error handling; update
the code around the Promise.all(...) block so it only executes when a debug/dev
mode (e.g., process.env.NODE_ENV !== 'production' or a dedicated isDebug flag)
is true, await the queries inside a try/catch (or add .catch) to avoid unhandled
rejections, and avoid logging full result sets—log only the SQL
(v1Domains.toSQL().sql, v2Domains.toSQL().sql) or a small summary instead.
- Around line 93-119: The owner-only queries exclude domains without label rows
because schema.label is inner-joined unconditionally in the v1Domains and
v2Domains query builders; change the join on schema.label to a leftJoin (or
perform the join only when partial is truthy) so that owner-only searches still
return domains with no label row, while keeping the existing where clause using
partial and like(schema.label.value, `${partial}%`) when partial is provided;
update both v1Domains and v2Domains to reference schema.label via leftJoin (or
wrap the join in a conditional based on the partial variable) so unlabeled
domains are not filtered out.

In `@apps/ensapi/src/graphql-api/schema/query.ts`:
- Around line 38-45: The GraphQL input DomainsWhereInput currently uses isOneOf
which enforces exactly one of name or owner, conflicting with the resolver
findDomains that accepts both; update the schema by removing isOneOf from the
builder.inputType call (i.e., adjust the DomainsWhereInput definition so both
name and owner can be provided), or if you need "at least one" semantics add
runtime validation via Pothos Validation plugin in the same DomainsWhereInput to
enforce at-least-one instead of using isOneOf.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Jan 30, 2026

Greptile Overview

Greptile Summary

This PR implements query-time canonical name resolution for ENSv1/v2 domains using recursive CTEs, avoiding the complexity of index-time materialization. The core changes include a new findDomains() function with upward path traversal (leaf→root), a new registryCanonicalDomain schema table to track ENSv2 canonical parents, and several new SDK utilities for parsing partial interpreted names.

Key Changes:

  • New recursive CTE queries in find-domains.ts for v1 (parent_id traversal) and v2 (registry→canonical domain traversal)
  • Fixed isInterpretedLabel bug that incorrectly fell through to isNormalizedLabel for valid encoded labelhashes
  • New Query.domains GraphQL endpoint with name/owner filtering (dev-only)
  • Added indexes on v1Domain.labelHash, v2Domain.labelHash, and label.value for query performance
  • Implemented registryCanonicalDomain table with last-write-wins heuristic for SubregistryUpdated events

Issues Found:

  • V2 CTE uses INNER JOIN on registryCanonicalDomain, which will silently drop domains whose registries lack canonical parent entries
  • LIKE operator input escaping remains unimplemented (already noted in previous threads)
  • When partial is provided with leftJoin on label, NULL labels will be filtered out

Confidence Score: 3.5/5

  • Safe to merge with caveats - core logic is sound but v2 canonical domain resolution may silently exclude valid domains
  • The recursive CTE logic is well-structured and the isInterpretedLabel fix is correct. However, the v2DomainsByLabelHashPath function uses INNER JOINs on registryCanonicalDomain which will silently drop domains without canonical parent entries, potentially causing incomplete search results. The TODO items (LIKE escaping, integration tests) are acknowledged but don't block merge for a dev-only endpoint.
  • Pay close attention to apps/ensapi/src/graphql-api/lib/find-domains.ts (v2 CTE join logic) and verify registryCanonicalDomain is properly populated for all expected registries

Important Files Changed

Filename Overview
apps/ensapi/src/graphql-api/lib/find-domains.ts New recursive CTE-based domain search with v1/v2 path traversal, LIKE input escaping TODOs remain
packages/ensnode-sdk/src/shared/interpretation/interpreted-names-and-labels.ts Fixed isInterpretedLabel bug for encoded labelhashes, added new parsing/construction utilities
packages/ensnode-schema/src/schemas/ensv2.schema.ts Added registryCanonicalDomain table and indexes for labelHash and label.value lookups
apps/ensapi/src/graphql-api/lib/get-canonical-path.ts Split into getV1/V2CanonicalPath with recursive CTEs, consistent MAX_DEPTH validation
apps/ensindexer/src/plugins/ensv2/handlers/ensv2/ENSv2Registry.ts Implemented registryCanonicalDomain population with last-write-wins heuristic for SubregistryUpdated

Sequence Diagram

sequenceDiagram
    participant Client
    participant GraphQL as Query.domains
    participant FindDomains as findDomains()
    participant Parser as parsePartialInterpretedName()
    participant V1CTE as v1DomainsByLabelHashPath()
    participant V2CTE as v2DomainsByLabelHashPath()
    participant DB as PostgreSQL Database

    Client->>GraphQL: query domains(where: {name, owner})
    GraphQL->>FindDomains: findDomains({name, owner})
    
    FindDomains->>Parser: parsePartialInterpretedName(name)
    Parser-->>FindDomains: {concrete: ["sub1", "sub2"], partial: "paren"}
    
    FindDomains->>FindDomains: validate depth <= MAX_DEPTH (16)
    FindDomains->>FindDomains: validate name or owner provided
    FindDomains->>FindDomains: interpretedLabelsToLabelHashPath(concrete)
    
    par V1 and V2 Domain Queries
        FindDomains->>V1CTE: v1DomainsByLabelHashPath(labelHashPath)
        V1CTE->>DB: Recursive CTE (leaf→parent via parent_id)
        DB-->>V1CTE: {leafId, headId} pairs
        
        FindDomains->>V2CTE: v2DomainsByLabelHashPath(labelHashPath)
        V2CTE->>DB: Recursive CTE (leaf→parent via registryCanonicalDomain)
        DB-->>V2CTE: {leafId, headId} pairs
    end
    
    FindDomains->>DB: JOIN v1Domains on leafId<br/>+ filter by owner on leaf<br/>+ filter by partial LIKE on head.label
    FindDomains->>DB: JOIN v2Domains on leafId<br/>+ filter by owner on leaf<br/>+ filter by partial LIKE on head.label
    
    FindDomains->>DB: UNION ALL v1Domains + v2Domains
    DB-->>FindDomains: Combined domain IDs
    
    FindDomains-->>GraphQL: domains CTE
    GraphQL->>DB: SELECT with pagination (before/after/limit)
    DB-->>GraphQL: Domain results
    GraphQL->>GraphQL: Load full Domain entities via dataloader
    GraphQL-->>Client: Connection with Domain nodes
Loading

Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

6 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Copy link
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 17 out of 18 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Copy link

@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: 1

🤖 Fix all issues with AI agents
In `@apps/ensapi/src/graphql-api/lib/find-domains.ts`:
- Around line 169-171: The code uses a non-existent drizzle API sql.param() when
building rawLabelHashPathArray and pathLength; replace the
sql.param(labelHashPath) usage by directly interpolating the value into the sql
template (use sql`${labelHashPath}::text[]` for rawLabelHashPathArray and keep
using sql`${rawLabelHashPathArray}` for pathLength) so Drizzle will
auto-parameterize—also apply the same replacement for the other occurrences
referenced around the pathLength duplicate (the second
rawLabelHashPathArray/pathLength pair).

@shrugs shrugs marked this pull request as ready for review January 30, 2026 06:52
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

5 files reviewed, 6 comments

Edit Code Review Agent Settings | Greptile

Copilot AI review requested due to automatic review settings February 1, 2026 01:16
@vercel vercel bot temporarily deployed to Preview – ensrainbow.io February 1, 2026 01:16 Inactive
@vercel vercel bot temporarily deployed to Preview – ensnode.io February 1, 2026 01:16 Inactive
@vercel vercel bot temporarily deployed to Preview – admin.ensnode.io February 1, 2026 01:16 Inactive
Copy link
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 21 out of 22 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • pnpm-lock.yaml: Language not supported

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

Copy link

@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: 5

🤖 Fix all issues with AI agents
In @.changeset/eight-beans-behave.md:
- Line 8: Fix the typo in the example for Account.domains by adding the missing
closing quotation mark to the name value; update `Account.domains(where?: {
name: "example.et })` to have a matching quote for the `name` string so it reads
`Account.domains(where?: { name: "example.et" })`, ensuring the
`Account.domains` example is valid.
- Line 5: Replace the misspelled word "experiemental" in the changeset sentence
that mentions the ENSv2 API (the phrase starting "The experiemental ENSv2
API...") with the correct spelling "experimental" so the line reads "The
experimental ENSv2 API now supports the following Domain filters, namely
matching indexed Domains by name prefix."

In `@apps/ensapi/src/graphql-api/context.ts`:
- Around line 8-16: The two DataLoader factories createV1CanonicalPathLoader and
createV2CanonicalPathLoader currently call getV1CanonicalPath/getV2CanonicalPath
per id, causing N separate recursive CTE queries; change them to call a new
batched function (e.g., batchGetV1CanonicalPaths and batchGetV2CanonicalPaths)
that accepts the full domainIds array and performs a single SQL query that
computes canonical paths for all requested ids in one go (using a recursive CTE
with an IN (...) filter, join to a temp table, or a VALUES list), then return
results in the same order as input, mapping missing rows to null so DataLoader
receives an array aligned with domainIds. Ensure the new batch functions are
used inside DataLoader constructors and preserve existing types (CanonicalPath |
null) and error-handling.
- Around line 8-16: The batch loaders createV1CanonicalPathLoader and
createV2CanonicalPathLoader currently use Promise.all over
getV1CanonicalPath/getV2CanonicalPath so a single thrown error rejects the whole
batch; change each batch function to map domainIds to individual async calls
wrapped in try/catch and return either the CanonicalPath value or an Error
instance for that key (preserving input order) so DataLoader receives per-key
results/errors; ensure you return an array of (CanonicalPath | Error | null)
matching domainIds for both createV1CanonicalPathLoader and
createV2CanonicalPathLoader.

In `@apps/ensapi/src/graphql-api/schema/domain.ts`:
- Around line 110-133: The Domain.name resolver currently assumes every loaded
domain has a label and will throw when found.label or found.label.value is
missing; change the labels mapping in the resolve function (the block using
isENSv1Domain, context.loaders.v1CanonicalPath/v2CanonicalPath,
DomainInterfaceRef.getDataloader, and interpretedLabelsToInterpretedName) to
guard against missing labels by checking found and found.label.value for each
domainId and returning null (or a chosen fallback) from the resolver immediately
if any label is absent instead of throwing an Error.

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.

ENSv2 Canonical Name Heuristic

2 participants