From 7c58b7fa0ed3df43d468c35d9da79588ca763c1d Mon Sep 17 00:00:00 2001 From: shrugs Date: Tue, 31 Mar 2026 15:40:45 -0500 Subject: [PATCH 01/14] fix: enable hash index on name --- .../ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts index e78386a54..9c5c456b1 100644 --- a/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts +++ b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts @@ -93,9 +93,9 @@ export const subgraph_domain = onchainTable( expiryDate: t.bigint(), }), (t) => ({ - // Temporarily disable the `byName` index to avoid index creation issues. - // For more details, see: https://github.com/namehash/ensnode/issues/1819 - // byName: index().on(t.name), + // uses a hash index because some name values exceed the btree max row size (8191 bytes) + byExactName: index().using("hash", t.name), + byLabelhash: index().on(t.labelhash), byParentId: index().on(t.parentId), byOwnerId: index().on(t.ownerId), From 5aa82080f1795174a18f284bb9feaa71330cc8d2 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 17 Apr 2026 10:46:44 -0500 Subject: [PATCH 02/14] feat: add pg_trgm extension setup + GIN trigram index on subgraph_domain.name Use the existing `initializeIndexingSetup` hook to install the `pg_trgm` extension before Ponder creates indexes, then re-introduce the GIN trigram index (`gin_trgm_ops`) on `subgraph_domain.name` to back the Subgraph GraphQL partial-match filters (`_contains`, `_starts_with`, `_ends_with`). The hash index added previously is retained for exact equality lookups; the trigram index covers partial matches. --- .changeset/hash-and-trigram-name-indexes.md | 6 ++ .../src/lib/indexing-engines/ponder.test.ts | 85 ++++++++++++++++++- .../src/lib/indexing-engines/ponder.ts | 21 +++++ .../ensindexer-abstract/subgraph.schema.ts | 4 + 4 files changed, 115 insertions(+), 1 deletion(-) create mode 100644 .changeset/hash-and-trigram-name-indexes.md diff --git a/.changeset/hash-and-trigram-name-indexes.md b/.changeset/hash-and-trigram-name-indexes.md new file mode 100644 index 000000000..450f5347f --- /dev/null +++ b/.changeset/hash-and-trigram-name-indexes.md @@ -0,0 +1,6 @@ +--- +"ensindexer": minor +"@ensnode/ensdb-sdk": minor +--- + +Re-enable `subgraph_domain.name` indexes (originally disabled in #1819) by pairing a hash index for exact-match lookups with a GIN trigram index (`gin_trgm_ops`) for partial-match filters (`_contains`, `_starts_with`, `_ends_with`). The hash index avoids the btree 8191-byte row size limit triggered by spam names. The trigram index requires the `pg_trgm` Postgres extension, which ENSIndexer now installs automatically in the setup hook before Ponder creates indexes. diff --git a/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts b/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts index 28d13674f..d1a4bd464 100644 --- a/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts +++ b/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts @@ -7,6 +7,8 @@ const { mockPonderOn } = vi.hoisted(() => ({ mockPonderOn: vi.fn() })); const mockWaitForEnsRainbow = vi.hoisted(() => vi.fn()); +const mockEnsDbExecute = vi.hoisted(() => vi.fn()); + vi.mock("ponder:registry", () => ({ ponder: { on: (...args: unknown[]) => mockPonderOn(...args), @@ -21,10 +23,28 @@ vi.mock("@/lib/ensrainbow/singleton", () => ({ waitForEnsRainbowToBeReady: mockWaitForEnsRainbow, })); +vi.mock("@/lib/ensdb/singleton", () => ({ + ensDbClient: { + ensDb: { + execute: (...args: unknown[]) => mockEnsDbExecute(...args), + }, + }, +})); + +vi.mock("@/lib/logger", () => ({ + logger: { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + }, +})); + describe("addOnchainEventListener", () => { beforeEach(async () => { vi.clearAllMocks(); mockWaitForEnsRainbow.mockResolvedValue(undefined); + mockEnsDbExecute.mockResolvedValue(undefined); // Reset module state to test idempotent behavior correctly vi.resetModules(); }); @@ -347,7 +367,7 @@ describe("addOnchainEventListener", () => { }); }); - describe("setup events (no preconditions)", () => { + describe("setup events (ENSRainbow wait skipped)", () => { it("skips ENSRainbow wait for :setup events", async () => { const { addOnchainEventListener } = await getPonderModule(); const handler = vi.fn().mockResolvedValue(undefined); @@ -387,6 +407,69 @@ describe("addOnchainEventListener", () => { }); }); + describe("Postgres extension preconditions (setup events)", () => { + it("installs the pg_trgm extension before the setup handler runs", async () => { + const { addOnchainEventListener } = await getPonderModule(); + const handler = vi.fn().mockResolvedValue(undefined); + + addOnchainEventListener("Registry:setup" as EventNames, handler); + await getRegisteredCallback()({ + context: { db: vi.fn() } as unknown as Context, + event: {} as IndexingEngineEvent, + }); + + expect(mockEnsDbExecute).toHaveBeenCalledTimes(1); + const sqlArg = mockEnsDbExecute.mock.calls[0]![0] as { + queryChunks: { value: string[] }[]; + }; + // Drizzle `sql` template produces a SQL object whose first queryChunk is a + // StringChunk with a `value: string[]` holding the raw static SQL fragments. + expect(sqlArg.queryChunks[0]!.value.join("")).toContain( + "CREATE EXTENSION IF NOT EXISTS pg_trgm", + ); + expect(handler).toHaveBeenCalled(); + }); + + it("runs the extension install only once across multiple setup events (idempotent)", async () => { + const { addOnchainEventListener } = await getPonderModule(); + const handler1 = vi.fn().mockResolvedValue(undefined); + const handler2 = vi.fn().mockResolvedValue(undefined); + + addOnchainEventListener("Registry:setup" as EventNames, handler1); + addOnchainEventListener("PublicResolver:setup" as EventNames, handler2); + + await getRegisteredCallback(0)({ + context: { db: vi.fn() } as unknown as Context, + event: {} as IndexingEngineEvent, + }); + await getRegisteredCallback(1)({ + context: { db: vi.fn() } as unknown as Context, + event: {} as IndexingEngineEvent, + }); + + expect(mockEnsDbExecute).toHaveBeenCalledTimes(1); + expect(handler1).toHaveBeenCalledTimes(1); + expect(handler2).toHaveBeenCalledTimes(1); + }); + + it("propagates errors from the extension install", async () => { + const { addOnchainEventListener } = await getPonderModule(); + mockEnsDbExecute.mockRejectedValueOnce(new Error("permission denied")); + const handler = vi.fn().mockResolvedValue(undefined); + + addOnchainEventListener("Registry:setup" as EventNames, handler); + + await expect( + getRegisteredCallback()({ + context: { db: vi.fn() } as unknown as Context, + event: {} as IndexingEngineEvent, + }), + ).rejects.toThrow("permission denied"); + + expect(handler).not.toHaveBeenCalled(); + }); + }); + describe("event type detection", () => { it("treats :setup suffix as setup event type", async () => { const { addOnchainEventListener } = await getPonderModule(); diff --git a/apps/ensindexer/src/lib/indexing-engines/ponder.ts b/apps/ensindexer/src/lib/indexing-engines/ponder.ts index 84996fdc6..1c31cdf34 100644 --- a/apps/ensindexer/src/lib/indexing-engines/ponder.ts +++ b/apps/ensindexer/src/lib/indexing-engines/ponder.ts @@ -14,8 +14,11 @@ import { type Event as PonderIndexingEvent, ponder, } from "ponder:registry"; +import { sql } from "drizzle-orm"; +import { ensDbClient } from "@/lib/ensdb/singleton"; import { waitForEnsRainbowToBeReady } from "@/lib/ensrainbow/singleton"; +import { logger } from "@/lib/logger"; /** * Context passed to event handlers registered with @@ -146,6 +149,24 @@ async function initializeIndexingSetup(): Promise { * ENSIndexer relies on these indexing metrics being immediately available on startup to build and * store the current Indexing Status in ENSDb. */ + + // Ensure all required Postgres extensions are installed before Ponder + // creates indexes that depend on them. `pg_trgm` provides the `gin_trgm_ops` + // operator class used by the GIN trigram index on `subgraph_domain.name`, + // which backs the Subgraph GraphQL partial-match filters + // (`_contains`, `_starts_with`, `_ends_with`). + // `CREATE EXTENSION IF NOT EXISTS` is idempotent and fast when the + // extension is already installed, so this satisfies the + // "no long-running preconditions" constraint documented above. + logger.debug({ + msg: "Ensuring required Postgres extensions are installed", + module: "IndexingEngine", + }); + await ensDbClient.ensDb.execute(sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`); + logger.info({ + msg: "Ensured required Postgres extensions are installed", + module: "IndexingEngine", + }); } /** diff --git a/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts index 9c5c456b1..bd19717ee 100644 --- a/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts +++ b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts @@ -95,6 +95,10 @@ export const subgraph_domain = onchainTable( (t) => ({ // uses a hash index because some name values exceed the btree max row size (8191 bytes) byExactName: index().using("hash", t.name), + // GIN trigram index for partial-match filters (_contains, _starts_with, _ends_with). + // Requires the `pg_trgm` extension, installed by the ENSIndexer setup hook + // (see `initializeIndexingSetup` in `apps/ensindexer/src/lib/indexing-engines/ponder.ts`). + byFuzzyName: index().using("gin", t.name.op("gin_trgm_ops")), byLabelhash: index().on(t.labelhash), byParentId: index().on(t.parentId), From 2006086a35421a2760ac0921dca6a550b790c017 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 17 Apr 2026 10:56:06 -0500 Subject: [PATCH 03/14] docs: note pg_trgm extension requirement for ENSDb Call out that any Postgres instance used as ENSDb must have the `pg_trgm` extension available for installation, since ENSIndexer runs `CREATE EXTENSION IF NOT EXISTS pg_trgm` at startup to back the partial-name search indexes on `subgraph_domain.name`. --- docs/ensnode.io/src/content/docs/docs/contributing/index.mdx | 2 +- docs/ensnode.io/src/content/docs/docs/deploying/docker.mdx | 4 ++++ docs/ensnode.io/src/content/docs/docs/deploying/terraform.mdx | 4 ++++ docs/ensnode.io/src/content/docs/docs/running/index.mdx | 4 ++++ 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/ensnode.io/src/content/docs/docs/contributing/index.mdx b/docs/ensnode.io/src/content/docs/docs/contributing/index.mdx index b9beadb5d..fce21e875 100644 --- a/docs/ensnode.io/src/content/docs/docs/contributing/index.mdx +++ b/docs/ensnode.io/src/content/docs/docs/contributing/index.mdx @@ -15,7 +15,7 @@ This guide covers running ENSNode locally for development and contributions. ### Prerequisites - [Git](https://git-scm.com/) -- [Postgres 17](https://www.postgresql.org/) +- [Postgres 17](https://www.postgresql.org/) with the [`pg_trgm`](https://www.postgresql.org/docs/current/pgtrgm.html) extension available for installation (ships with stock Postgres contrib; ENSIndexer runs `CREATE EXTENSION IF NOT EXISTS pg_trgm` at startup to back partial-name search indexes) - [Node.js](https://nodejs.org/) - It's recommended you install Node.js through [nvm](https://github.com/nvm-sh/nvm) or [asdf](https://asdf-vm.com/). - see `.nvmrc` and `.tool-versions` for the specific version of Node.js diff --git a/docs/ensnode.io/src/content/docs/docs/deploying/docker.mdx b/docs/ensnode.io/src/content/docs/docs/deploying/docker.mdx index bea25babb..a864d6149 100644 --- a/docs/ensnode.io/src/content/docs/docs/deploying/docker.mdx +++ b/docs/ensnode.io/src/content/docs/docs/deploying/docker.mdx @@ -11,6 +11,10 @@ import dockercompose from '@workspace/docker-compose.yml?raw'; The Docker images are the easiest way to run or deploy the ENSNode suite of services, both locally and in the cloud. +:::note[Postgres Requirement] +ENSIndexer runs `CREATE EXTENSION IF NOT EXISTS pg_trgm` at startup to back partial-name search indexes. If you're swapping out the bundled `postgres:17` image for a managed or custom Postgres, make sure the [`pg_trgm`](https://www.postgresql.org/docs/current/pgtrgm.html) extension is available for installation (it ships with stock Postgres contrib and is enabled by default on most managed providers). +::: + Date: Fri, 17 Apr 2026 11:01:53 -0500 Subject: [PATCH 04/14] test: drop redundant setup-precondition tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The idempotency and error-propagation tests for the pg_trgm install re-exercise the shared `eventHandlerPreconditions` promise-caching mechanism, which is already covered by the ENSRainbow onchain-path tests. The SQL-content assertion tested an implementation detail that a source-level review catches more cheaply. Keep the ensDb singleton and logger mocks — they're needed for the module to load cleanly under vitest, not for coverage. --- .../src/lib/indexing-engines/ponder.test.ts | 63 ------------------- 1 file changed, 63 deletions(-) diff --git a/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts b/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts index d1a4bd464..745e4e495 100644 --- a/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts +++ b/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts @@ -407,69 +407,6 @@ describe("addOnchainEventListener", () => { }); }); - describe("Postgres extension preconditions (setup events)", () => { - it("installs the pg_trgm extension before the setup handler runs", async () => { - const { addOnchainEventListener } = await getPonderModule(); - const handler = vi.fn().mockResolvedValue(undefined); - - addOnchainEventListener("Registry:setup" as EventNames, handler); - await getRegisteredCallback()({ - context: { db: vi.fn() } as unknown as Context, - event: {} as IndexingEngineEvent, - }); - - expect(mockEnsDbExecute).toHaveBeenCalledTimes(1); - const sqlArg = mockEnsDbExecute.mock.calls[0]![0] as { - queryChunks: { value: string[] }[]; - }; - // Drizzle `sql` template produces a SQL object whose first queryChunk is a - // StringChunk with a `value: string[]` holding the raw static SQL fragments. - expect(sqlArg.queryChunks[0]!.value.join("")).toContain( - "CREATE EXTENSION IF NOT EXISTS pg_trgm", - ); - expect(handler).toHaveBeenCalled(); - }); - - it("runs the extension install only once across multiple setup events (idempotent)", async () => { - const { addOnchainEventListener } = await getPonderModule(); - const handler1 = vi.fn().mockResolvedValue(undefined); - const handler2 = vi.fn().mockResolvedValue(undefined); - - addOnchainEventListener("Registry:setup" as EventNames, handler1); - addOnchainEventListener("PublicResolver:setup" as EventNames, handler2); - - await getRegisteredCallback(0)({ - context: { db: vi.fn() } as unknown as Context, - event: {} as IndexingEngineEvent, - }); - await getRegisteredCallback(1)({ - context: { db: vi.fn() } as unknown as Context, - event: {} as IndexingEngineEvent, - }); - - expect(mockEnsDbExecute).toHaveBeenCalledTimes(1); - expect(handler1).toHaveBeenCalledTimes(1); - expect(handler2).toHaveBeenCalledTimes(1); - }); - - it("propagates errors from the extension install", async () => { - const { addOnchainEventListener } = await getPonderModule(); - mockEnsDbExecute.mockRejectedValueOnce(new Error("permission denied")); - const handler = vi.fn().mockResolvedValue(undefined); - - addOnchainEventListener("Registry:setup" as EventNames, handler); - - await expect( - getRegisteredCallback()({ - context: { db: vi.fn() } as unknown as Context, - event: {} as IndexingEngineEvent, - }), - ).rejects.toThrow("permission denied"); - - expect(handler).not.toHaveBeenCalled(); - }); - }); - describe("event type detection", () => { it("treats :setup suffix as setup event type", async () => { const { addOnchainEventListener } = await getPonderModule(); From a15e0214bccfc52324a235c56c3a541b4556ac83 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 17 Apr 2026 11:45:36 -0500 Subject: [PATCH 05/14] fix: trim comments --- apps/ensindexer/src/lib/indexing-engines/ponder.ts | 9 ++------- .../ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts | 4 +--- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/apps/ensindexer/src/lib/indexing-engines/ponder.ts b/apps/ensindexer/src/lib/indexing-engines/ponder.ts index 1c31cdf34..8fd52dfdd 100644 --- a/apps/ensindexer/src/lib/indexing-engines/ponder.ts +++ b/apps/ensindexer/src/lib/indexing-engines/ponder.ts @@ -151,13 +151,8 @@ async function initializeIndexingSetup(): Promise { */ // Ensure all required Postgres extensions are installed before Ponder - // creates indexes that depend on them. `pg_trgm` provides the `gin_trgm_ops` - // operator class used by the GIN trigram index on `subgraph_domain.name`, - // which backs the Subgraph GraphQL partial-match filters - // (`_contains`, `_starts_with`, `_ends_with`). - // `CREATE EXTENSION IF NOT EXISTS` is idempotent and fast when the - // extension is already installed, so this satisfies the - // "no long-running preconditions" constraint documented above. + // creates indexes that depend on them. + // - `pg_trgm` necessary for GIN trigram indexes (partial string matching) logger.debug({ msg: "Ensuring required Postgres extensions are installed", module: "IndexingEngine", diff --git a/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts index bd19717ee..8f132a73c 100644 --- a/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts +++ b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts @@ -95,9 +95,7 @@ export const subgraph_domain = onchainTable( (t) => ({ // uses a hash index because some name values exceed the btree max row size (8191 bytes) byExactName: index().using("hash", t.name), - // GIN trigram index for partial-match filters (_contains, _starts_with, _ends_with). - // Requires the `pg_trgm` extension, installed by the ENSIndexer setup hook - // (see `initializeIndexingSetup` in `apps/ensindexer/src/lib/indexing-engines/ponder.ts`). + // GIN trigram index for partial-match filters (_contains, _starts_with, _ends_with) byFuzzyName: index().using("gin", t.name.op("gin_trgm_ops")), byLabelhash: index().on(t.labelhash), From 127c1de6ae61fd1d76dd6436877a8611044e8e4c Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 17 Apr 2026 11:59:34 -0500 Subject: [PATCH 06/14] fix: pin pg_trgm install to `public` schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without `WITH SCHEMA public`, Postgres installs the extension into the first writable schema on the current connection's search_path (often the ENSIndexer schema). When Ponder later issues an unqualified `CREATE INDEX ... USING gin (name gin_trgm_ops)`, the operator class must be resolvable via its own connection's search_path — pinning to `public` (which is on the default search_path) makes that reliable. --- .../ensindexer/src/lib/indexing-engines/ponder.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/apps/ensindexer/src/lib/indexing-engines/ponder.ts b/apps/ensindexer/src/lib/indexing-engines/ponder.ts index 8fd52dfdd..86c1287ba 100644 --- a/apps/ensindexer/src/lib/indexing-engines/ponder.ts +++ b/apps/ensindexer/src/lib/indexing-engines/ponder.ts @@ -152,12 +152,23 @@ async function initializeIndexingSetup(): Promise { // Ensure all required Postgres extensions are installed before Ponder // creates indexes that depend on them. - // - `pg_trgm` necessary for GIN trigram indexes (partial string matching) logger.debug({ msg: "Ensuring required Postgres extensions are installed", module: "IndexingEngine", }); - await ensDbClient.ensDb.execute(sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`); + + try { + // `pg_trgm` necessary for GIN trigram indexes (partial string matching). + // Install into `public` so `gin_trgm_ops` is on the default search_path + // when Ponder issues the unqualified `CREATE INDEX ... USING gin (name gin_trgm_ops)`. + await ensDbClient.ensDb.execute(sql`CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public`); + } catch (cause) { + throw new Error( + `Unable to create the pg_trgm extension in the connected Postgres: ensure the database user has permission to install extensions and that the 'pg_trgm' extension is available.`, + { cause }, + ); + } + logger.info({ msg: "Ensured required Postgres extensions are installed", module: "IndexingEngine", From a62a62a7758fff2a80519b5a97275337af42ceaa Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 17 Apr 2026 12:45:29 -0500 Subject: [PATCH 07/14] fix: emit gin_trgm_ops opclass via raw sql fragment Ponder's index-emission layer drops Drizzle's `.op("gin_trgm_ops")`, producing `CREATE INDEX ... USING gin (name)` with no opclass and failing at index-creation time with "text has no default operator class for access method gin". Passing the column + opclass as a raw `sql\`${t.name} gin_trgm_ops\`` fragment sidesteps the dropped opclass. --- .../ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts index 8f132a73c..352f9d353 100644 --- a/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts +++ b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts @@ -1,3 +1,4 @@ +import { sql } from "drizzle-orm"; import type { Address } from "enssdk"; import { index, onchainTable, relations } from "ponder"; @@ -95,8 +96,10 @@ export const subgraph_domain = onchainTable( (t) => ({ // uses a hash index because some name values exceed the btree max row size (8191 bytes) byExactName: index().using("hash", t.name), - // GIN trigram index for partial-match filters (_contains, _starts_with, _ends_with) - byFuzzyName: index().using("gin", t.name.op("gin_trgm_ops")), + // GIN trigram index for partial-match filters (_contains, _starts_with, _ends_with). + // Inline `gin_trgm_ops` via `sql` because passing it through `.op()` gets dropped + // by Ponder's index-emission layer, producing `USING gin (name)` with no opclass. + byFuzzyName: index().using("gin", sql`${t.name} gin_trgm_ops`), byLabelhash: index().on(t.labelhash), byParentId: index().on(t.parentId), From 427ff54c8c631f4d6e8e6a64da5f7fafb9cb6908 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 17 Apr 2026 14:34:50 -0500 Subject: [PATCH 08/14] fix(ensindexer): address PR feedback on pg_trgm setup - log underlying Postgres error in the pg_trgm install catch so ops can triage without walking the Error#cause chain (some log formatters don't surface `cause` by default) - swap inline logger mock in ponder.test.ts for the shared `@/lib/__test__/mockLogger` helper used across the rest of the suite --- .../src/lib/indexing-engines/ponder.test.ts | 11 ++--------- apps/ensindexer/src/lib/indexing-engines/ponder.ts | 7 +++++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts b/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts index 745e4e495..208496bcb 100644 --- a/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts +++ b/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts @@ -1,6 +1,8 @@ import type { Context, EventNames } from "ponder:registry"; import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest"; +import "@/lib/__test__/mockLogger"; + import type { IndexingEngineContext, IndexingEngineEvent } from "./ponder"; const { mockPonderOn } = vi.hoisted(() => ({ mockPonderOn: vi.fn() })); @@ -31,15 +33,6 @@ vi.mock("@/lib/ensdb/singleton", () => ({ }, })); -vi.mock("@/lib/logger", () => ({ - logger: { - debug: vi.fn(), - info: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - }, -})); - describe("addOnchainEventListener", () => { beforeEach(async () => { vi.clearAllMocks(); diff --git a/apps/ensindexer/src/lib/indexing-engines/ponder.ts b/apps/ensindexer/src/lib/indexing-engines/ponder.ts index 86c1287ba..1641fae47 100644 --- a/apps/ensindexer/src/lib/indexing-engines/ponder.ts +++ b/apps/ensindexer/src/lib/indexing-engines/ponder.ts @@ -163,6 +163,13 @@ async function initializeIndexingSetup(): Promise { // when Ponder issues the unqualified `CREATE INDEX ... USING gin (name gin_trgm_ops)`. await ensDbClient.ensDb.execute(sql`CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public`); } catch (cause) { + // Log the underlying Postgres error so ops can see it without walking the + // Error#cause chain (some log formatters don't surface `cause` by default). + logger.error({ + msg: "Failed to install the `pg_trgm` Postgres extension", + error: cause, + module: "IndexingEngine", + }); throw new Error( `Unable to create the pg_trgm extension in the connected Postgres: ensure the database user has permission to install extensions and that the 'pg_trgm' extension is available.`, { cause }, From 3d49f4f868d8ab49bc182cd51ec296c8abdc7878 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 17 Apr 2026 14:46:25 -0500 Subject: [PATCH 09/14] fix: qualify gin_trgm_ops with `public.` schema MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ponder pins its connection's search_path to only the ENSIndexer schema (`SET search_path = ""`), so unqualified `gin_trgm_ops` couldn't be resolved at `createIndexes` time — even though ENSIndexer had installed `pg_trgm` into `public` at startup. Qualifying the opclass as `public.gin_trgm_ops` in the index definition bypasses search_path entirely. Caught by the integration tests after backfill completed and Ponder proceeded to apply indexes. --- .../src/ensindexer-abstract/subgraph.schema.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts index 352f9d353..14150e8d9 100644 --- a/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts +++ b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts @@ -97,9 +97,13 @@ export const subgraph_domain = onchainTable( // uses a hash index because some name values exceed the btree max row size (8191 bytes) byExactName: index().using("hash", t.name), // GIN trigram index for partial-match filters (_contains, _starts_with, _ends_with). - // Inline `gin_trgm_ops` via `sql` because passing it through `.op()` gets dropped - // by Ponder's index-emission layer, producing `USING gin (name)` with no opclass. - byFuzzyName: index().using("gin", sql`${t.name} gin_trgm_ops`), + // - Inline `gin_trgm_ops` via `sql` because passing it through `.op()` gets dropped + // by Ponder's index-emission layer, producing `USING gin (name)` with no opclass. + // - Qualify with `public.` because Ponder pins its connection's search_path to the + // ENSIndexer schema only (`SET search_path = ""`), so unqualified + // `gin_trgm_ops` can't be resolved. ENSIndexer installs `pg_trgm` into `public` + // on startup (see `initializeIndexingSetup`), so the qualifier matches. + byFuzzyName: index().using("gin", sql`${t.name} public.gin_trgm_ops`), byLabelhash: index().on(t.labelhash), byParentId: index().on(t.parentId), From e5f770b53080f8cd938fb167225fbd5ca59b5234 Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 17 Apr 2026 14:58:20 -0500 Subject: [PATCH 10/14] fix: install extension + execute on concrete schema --- apps/ensindexer/src/lib/indexing-engines/ponder.ts | 6 ++---- .../src/ensindexer-abstract/subgraph.schema.ts | 10 +++------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/apps/ensindexer/src/lib/indexing-engines/ponder.ts b/apps/ensindexer/src/lib/indexing-engines/ponder.ts index 1641fae47..23e234717 100644 --- a/apps/ensindexer/src/lib/indexing-engines/ponder.ts +++ b/apps/ensindexer/src/lib/indexing-engines/ponder.ts @@ -158,10 +158,8 @@ async function initializeIndexingSetup(): Promise { }); try { - // `pg_trgm` necessary for GIN trigram indexes (partial string matching). - // Install into `public` so `gin_trgm_ops` is on the default search_path - // when Ponder issues the unqualified `CREATE INDEX ... USING gin (name gin_trgm_ops)`. - await ensDbClient.ensDb.execute(sql`CREATE EXTENSION IF NOT EXISTS pg_trgm WITH SCHEMA public`); + // `pg_trgm` necessary for GIN trigram indexes (partial string matching) + await ensDbClient.ensDb.execute(sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`); } catch (cause) { // Log the underlying Postgres error so ops can see it without walking the // Error#cause chain (some log formatters don't surface `cause` by default). diff --git a/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts index 14150e8d9..0fd82020b 100644 --- a/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts +++ b/packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts @@ -97,13 +97,9 @@ export const subgraph_domain = onchainTable( // uses a hash index because some name values exceed the btree max row size (8191 bytes) byExactName: index().using("hash", t.name), // GIN trigram index for partial-match filters (_contains, _starts_with, _ends_with). - // - Inline `gin_trgm_ops` via `sql` because passing it through `.op()` gets dropped - // by Ponder's index-emission layer, producing `USING gin (name)` with no opclass. - // - Qualify with `public.` because Ponder pins its connection's search_path to the - // ENSIndexer schema only (`SET search_path = ""`), so unqualified - // `gin_trgm_ops` can't be resolved. ENSIndexer installs `pg_trgm` into `public` - // on startup (see `initializeIndexingSetup`), so the qualifier matches. - byFuzzyName: index().using("gin", sql`${t.name} public.gin_trgm_ops`), + // (inline `gin_trgm_ops` via `sql` because passing it through `.op()` gets dropped by Ponder, + // producing `USING gin (name)` with no opclass) + byFuzzyName: index().using("gin", sql`${t.name} gin_trgm_ops`), byLabelhash: index().on(t.labelhash), byParentId: index().on(t.parentId), From 6168351923e7459aad53a6da3d0ae0293dd12f5a Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 17 Apr 2026 15:09:25 -0500 Subject: [PATCH 11/14] fix: use drizzlekit migration instead of hooking setup --- .../src/lib/indexing-engines/ponder.test.ts | 15 +---- .../src/lib/indexing-engines/ponder.ts | 32 ---------- .../migrations/0001_enable_ext_pg_trgm.sql | 3 + .../migrations/meta/0001_snapshot.json | 58 +++++++++++++++++++ .../ensdb-sdk/migrations/meta/_journal.json | 9 ++- 5 files changed, 70 insertions(+), 47 deletions(-) create mode 100644 packages/ensdb-sdk/migrations/0001_enable_ext_pg_trgm.sql create mode 100644 packages/ensdb-sdk/migrations/meta/0001_snapshot.json diff --git a/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts b/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts index 208496bcb..28d13674f 100644 --- a/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts +++ b/apps/ensindexer/src/lib/indexing-engines/ponder.test.ts @@ -1,16 +1,12 @@ import type { Context, EventNames } from "ponder:registry"; import { beforeEach, describe, expect, expectTypeOf, it, vi } from "vitest"; -import "@/lib/__test__/mockLogger"; - import type { IndexingEngineContext, IndexingEngineEvent } from "./ponder"; const { mockPonderOn } = vi.hoisted(() => ({ mockPonderOn: vi.fn() })); const mockWaitForEnsRainbow = vi.hoisted(() => vi.fn()); -const mockEnsDbExecute = vi.hoisted(() => vi.fn()); - vi.mock("ponder:registry", () => ({ ponder: { on: (...args: unknown[]) => mockPonderOn(...args), @@ -25,19 +21,10 @@ vi.mock("@/lib/ensrainbow/singleton", () => ({ waitForEnsRainbowToBeReady: mockWaitForEnsRainbow, })); -vi.mock("@/lib/ensdb/singleton", () => ({ - ensDbClient: { - ensDb: { - execute: (...args: unknown[]) => mockEnsDbExecute(...args), - }, - }, -})); - describe("addOnchainEventListener", () => { beforeEach(async () => { vi.clearAllMocks(); mockWaitForEnsRainbow.mockResolvedValue(undefined); - mockEnsDbExecute.mockResolvedValue(undefined); // Reset module state to test idempotent behavior correctly vi.resetModules(); }); @@ -360,7 +347,7 @@ describe("addOnchainEventListener", () => { }); }); - describe("setup events (ENSRainbow wait skipped)", () => { + describe("setup events (no preconditions)", () => { it("skips ENSRainbow wait for :setup events", async () => { const { addOnchainEventListener } = await getPonderModule(); const handler = vi.fn().mockResolvedValue(undefined); diff --git a/apps/ensindexer/src/lib/indexing-engines/ponder.ts b/apps/ensindexer/src/lib/indexing-engines/ponder.ts index 23e234717..84996fdc6 100644 --- a/apps/ensindexer/src/lib/indexing-engines/ponder.ts +++ b/apps/ensindexer/src/lib/indexing-engines/ponder.ts @@ -14,11 +14,8 @@ import { type Event as PonderIndexingEvent, ponder, } from "ponder:registry"; -import { sql } from "drizzle-orm"; -import { ensDbClient } from "@/lib/ensdb/singleton"; import { waitForEnsRainbowToBeReady } from "@/lib/ensrainbow/singleton"; -import { logger } from "@/lib/logger"; /** * Context passed to event handlers registered with @@ -149,35 +146,6 @@ async function initializeIndexingSetup(): Promise { * ENSIndexer relies on these indexing metrics being immediately available on startup to build and * store the current Indexing Status in ENSDb. */ - - // Ensure all required Postgres extensions are installed before Ponder - // creates indexes that depend on them. - logger.debug({ - msg: "Ensuring required Postgres extensions are installed", - module: "IndexingEngine", - }); - - try { - // `pg_trgm` necessary for GIN trigram indexes (partial string matching) - await ensDbClient.ensDb.execute(sql`CREATE EXTENSION IF NOT EXISTS pg_trgm`); - } catch (cause) { - // Log the underlying Postgres error so ops can see it without walking the - // Error#cause chain (some log formatters don't surface `cause` by default). - logger.error({ - msg: "Failed to install the `pg_trgm` Postgres extension", - error: cause, - module: "IndexingEngine", - }); - throw new Error( - `Unable to create the pg_trgm extension in the connected Postgres: ensure the database user has permission to install extensions and that the 'pg_trgm' extension is available.`, - { cause }, - ); - } - - logger.info({ - msg: "Ensured required Postgres extensions are installed", - module: "IndexingEngine", - }); } /** diff --git a/packages/ensdb-sdk/migrations/0001_enable_ext_pg_trgm.sql b/packages/ensdb-sdk/migrations/0001_enable_ext_pg_trgm.sql new file mode 100644 index 000000000..15c5b351b --- /dev/null +++ b/packages/ensdb-sdk/migrations/0001_enable_ext_pg_trgm.sql @@ -0,0 +1,3 @@ +-- This migration enables the pg_trgm extension, which is used for trigram-based indexing and +-- searching in PostgreSQL. +CREATE EXTENSION IF NOT EXISTS pg_trgm; diff --git a/packages/ensdb-sdk/migrations/meta/0001_snapshot.json b/packages/ensdb-sdk/migrations/meta/0001_snapshot.json new file mode 100644 index 000000000..610b0f653 --- /dev/null +++ b/packages/ensdb-sdk/migrations/meta/0001_snapshot.json @@ -0,0 +1,58 @@ +{ + "id": "7a9206c1-c22e-460d-8314-efa9b4cd64bc", + "prevId": "d661dcae-f64d-4ecd-a4da-3d5783e17e2c", + "version": "7", + "dialect": "postgresql", + "tables": { + "ensnode.metadata": { + "name": "metadata", + "schema": "ensnode", + "columns": { + "ens_indexer_schema_name": { + "name": "ens_indexer_schema_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "metadata_pkey": { + "name": "metadata_pkey", + "columns": [ + "ens_indexer_schema_name", + "key" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "views": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/ensdb-sdk/migrations/meta/_journal.json b/packages/ensdb-sdk/migrations/meta/_journal.json index 46d9e212a..52850f324 100644 --- a/packages/ensdb-sdk/migrations/meta/_journal.json +++ b/packages/ensdb-sdk/migrations/meta/_journal.json @@ -8,6 +8,13 @@ "when": 1773743108514, "tag": "0000_cultured_captain_cross", "breakpoints": true + }, + { + "idx": 1, + "version": "7", + "when": 1776456213289, + "tag": "0001_enable_ext_pg_trgm", + "breakpoints": true } ] -} +} \ No newline at end of file From fef01b5cbfa64e451025aac008a2be6622fbd46e Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 17 Apr 2026 15:12:07 -0500 Subject: [PATCH 12/14] docs(changeset): reflect migration-based pg_trgm install --- .changeset/hash-and-trigram-name-indexes.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/hash-and-trigram-name-indexes.md b/.changeset/hash-and-trigram-name-indexes.md index 450f5347f..e4a4a3123 100644 --- a/.changeset/hash-and-trigram-name-indexes.md +++ b/.changeset/hash-and-trigram-name-indexes.md @@ -3,4 +3,4 @@ "@ensnode/ensdb-sdk": minor --- -Re-enable `subgraph_domain.name` indexes (originally disabled in #1819) by pairing a hash index for exact-match lookups with a GIN trigram index (`gin_trgm_ops`) for partial-match filters (`_contains`, `_starts_with`, `_ends_with`). The hash index avoids the btree 8191-byte row size limit triggered by spam names. The trigram index requires the `pg_trgm` Postgres extension, which ENSIndexer now installs automatically in the setup hook before Ponder creates indexes. +Re-enable `subgraph_domain.name` indexes (originally disabled in #1819) by pairing a hash index for exact-match lookups with a GIN trigram index (`gin_trgm_ops`) for partial-match filters (`_contains`, `_starts_with`, `_ends_with`). The hash index avoids the btree 8191-byte row size limit triggered by spam names. The trigram index requires the `pg_trgm` Postgres extension, which ENSIndexer now installs automatically via a Drizzle migration (`0001_enable_ext_pg_trgm.sql`) that runs before Ponder starts. From 072fff2f0802b6134b6123907b40a30f93a4db4c Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 17 Apr 2026 15:24:52 -0500 Subject: [PATCH 13/14] fix: lint --- packages/ensdb-sdk/migrations/meta/0001_snapshot.json | 7 ++----- packages/ensdb-sdk/migrations/meta/_journal.json | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/ensdb-sdk/migrations/meta/0001_snapshot.json b/packages/ensdb-sdk/migrations/meta/0001_snapshot.json index 610b0f653..e50f4d057 100644 --- a/packages/ensdb-sdk/migrations/meta/0001_snapshot.json +++ b/packages/ensdb-sdk/migrations/meta/0001_snapshot.json @@ -32,10 +32,7 @@ "compositePrimaryKeys": { "metadata_pkey": { "name": "metadata_pkey", - "columns": [ - "ens_indexer_schema_name", - "key" - ] + "columns": ["ens_indexer_schema_name", "key"] } }, "uniqueConstraints": {}, @@ -55,4 +52,4 @@ "schemas": {}, "tables": {} } -} \ No newline at end of file +} diff --git a/packages/ensdb-sdk/migrations/meta/_journal.json b/packages/ensdb-sdk/migrations/meta/_journal.json index 52850f324..2ba16c788 100644 --- a/packages/ensdb-sdk/migrations/meta/_journal.json +++ b/packages/ensdb-sdk/migrations/meta/_journal.json @@ -17,4 +17,4 @@ "breakpoints": true } ] -} \ No newline at end of file +} From dec0101f1ddbbf539010ea14c45f0d40c035a53e Mon Sep 17 00:00:00 2001 From: shrugs Date: Fri, 17 Apr 2026 15:26:47 -0500 Subject: [PATCH 14/14] fix: address PR review nits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - import `sql` from ponder instead of drizzle-orm for consistency with other schema files in ensdb-sdk (ensv2.schema.ts does the same) - rephrase pg_trgm managed-provider note from "enabled by default" (inaccurate — pg_trgm is per-database `CREATE EXTENSION`) to "available for installation" to match the rest of the docs --- docs/ensnode.io/src/content/docs/docs/deploying/docker.mdx | 2 +- docs/ensnode.io/src/content/docs/docs/running/index.mdx | 2 +- packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/ensnode.io/src/content/docs/docs/deploying/docker.mdx b/docs/ensnode.io/src/content/docs/docs/deploying/docker.mdx index a864d6149..88f159aa3 100644 --- a/docs/ensnode.io/src/content/docs/docs/deploying/docker.mdx +++ b/docs/ensnode.io/src/content/docs/docs/deploying/docker.mdx @@ -12,7 +12,7 @@ import dockercompose from '@workspace/docker-compose.yml?raw'; The Docker images are the easiest way to run or deploy the ENSNode suite of services, both locally and in the cloud. :::note[Postgres Requirement] -ENSIndexer runs `CREATE EXTENSION IF NOT EXISTS pg_trgm` at startup to back partial-name search indexes. If you're swapping out the bundled `postgres:17` image for a managed or custom Postgres, make sure the [`pg_trgm`](https://www.postgresql.org/docs/current/pgtrgm.html) extension is available for installation (it ships with stock Postgres contrib and is enabled by default on most managed providers). +ENSIndexer runs `CREATE EXTENSION IF NOT EXISTS pg_trgm` at startup to back partial-name search indexes. If you're swapping out the bundled `postgres:17` image for a managed or custom Postgres, make sure the [`pg_trgm`](https://www.postgresql.org/docs/current/pgtrgm.html) extension is available for installation (it ships with stock Postgres contrib and is available for installation on most managed providers). :::