Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/hash-and-trigram-name-indexes.md
Original file line number Diff line number Diff line change
@@ -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.
Comment thread
shrugs marked this conversation as resolved.
Outdated
85 changes: 84 additions & 1 deletion apps/ensindexer/src/lib/indexing-engines/ponder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand All @@ -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(),
Comment thread
shrugs marked this conversation as resolved.
Outdated
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();
});
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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<EventNames>,
event: {} as IndexingEngineEvent<EventNames>,
});

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",
);
Comment thread
shrugs marked this conversation as resolved.
Outdated
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<EventNames>,
event: {} as IndexingEngineEvent<EventNames>,
});
await getRegisteredCallback(1)({
context: { db: vi.fn() } as unknown as Context<EventNames>,
event: {} as IndexingEngineEvent<EventNames>,
});

expect(mockEnsDbExecute).toHaveBeenCalledTimes(1);
expect(handler1).toHaveBeenCalledTimes(1);
expect(handler2).toHaveBeenCalledTimes(1);
});
Comment thread
shrugs marked this conversation as resolved.
Outdated

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<EventNames>,
event: {} as IndexingEngineEvent<EventNames>,
}),
).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();
Expand Down
21 changes: 21 additions & 0 deletions apps/ensindexer/src/lib/indexing-engines/ponder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -146,6 +149,24 @@ async function initializeIndexingSetup(): Promise<void> {
* 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`);
Comment thread
shrugs marked this conversation as resolved.
Outdated
logger.info({
msg: "Ensured required Postgres extensions are installed",
module: "IndexingEngine",
});
Comment thread
shrugs marked this conversation as resolved.
Outdated
}

/**
Expand Down
10 changes: 7 additions & 3 deletions packages/ensdb-sdk/src/ensindexer-abstract/subgraph.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,13 @@ 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)
Comment thread
shrugs marked this conversation as resolved.
Comment thread
shrugs marked this conversation as resolved.
byExactName: index().using("hash", t.name),
Comment thread
shrugs marked this conversation as resolved.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm surprised no other code is updated in relation to us making the schema changes here? Ex: code in API handlers?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

none! postgres planner will use the indexes without specification

// 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")),
Comment thread
shrugs marked this conversation as resolved.
Outdated

byLabelhash: index().on(t.labelhash),
byParentId: index().on(t.parentId),
byOwnerId: index().on(t.ownerId),
Expand Down
Loading