Skip to content
5 changes: 5 additions & 0 deletions .changeset/chatty-monkeys-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@ensnode/ensdb-sdk": minor
---

Fixed `buildConcreteEnsIndexerSchema` function by replacing the incomplete cloning approach with working mutation approach.
Comment thread
tk-o marked this conversation as resolved.
Outdated
Comment thread
tk-o marked this conversation as resolved.
Outdated
Comment thread
tk-o marked this conversation as resolved.
Outdated

This file was deleted.

3 changes: 0 additions & 3 deletions packages/ensdb-sdk/src/ensindexer-abstract/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
* for ENSDb, which is then used to build the ENSDb Schema for a Drizzle client for ENSDb.
*/

Comment thread
tk-o marked this conversation as resolved.
// TODO: remove `ensnode-metadata.schema` export when database migrations
// for ENSNode Schema are executable.
export * from "./ensnode-metadata.schema";
export * from "./ensv2.schema";
export * from "./protocol-acceleration.schema";
export * from "./registrars.schema";
Expand Down
5 changes: 4 additions & 1 deletion packages/ensdb-sdk/src/lib/drizzle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,10 @@ describe("buildIndividualEnsDbSchemas", () => {
}
});

it("builds two concrete schemas with respective names, leaving abstract unaffected", () => {
// TODO: remove the skip once the `abstractEnsIndexerSchema` is
// no longer mutated by `buildConcreteEnsIndexerSchema`.
// https://github.com/namehash/ensnode/issues/1830
it.skip("builds two concrete schemas with respective names, leaving abstract unaffected", () => {
Comment thread
tk-o marked this conversation as resolved.
Outdated
Comment thread
tk-o marked this conversation as resolved.
Outdated
const schemaNameA = "ensindexer_alpha";
const schemaNameB = "ensindexer_beta";

Expand Down
73 changes: 18 additions & 55 deletions packages/ensdb-sdk/src/lib/drizzle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,36 +21,6 @@ import * as ensNodeSchema from "../ensnode";
*/
export type AbstractEnsIndexerSchema = typeof abstractEnsIndexerSchema;

/**
* Clone a Drizzle Table object with a new schema name.
*
* Drizzle tables store their identity (name, columns, schema) on
* Symbol-keyed properties. Cloning a table requires creating
* a new object with the same prototype, copying all properties,
* and updating the schema name.
*/
function cloneTableWithSchema<TableType extends Table>(
table: TableType,
schemaName: string,
): TableType {
const clone = Object.create(
Comment thread
tk-o marked this conversation as resolved.
Object.getPrototypeOf(table),
Object.getOwnPropertyDescriptors(table),
) as TableType;

// @ts-expect-error - Drizzle's Table type for the schema symbol is
// not typed in a way that allows us to set it directly,
// but we know it exists and can be set.
clone[Table.Symbol.Schema] = schemaName;

// Fail-fast if the clone lost the Drizzle sentinel.
if (!isTable(clone)) {
throw new Error(`Cloned table is no longer a valid Drizzle Table (schema: ${schemaName}).`);
}

return clone;
}

/**
* Build a "concrete" ENSIndexer Schema definition for ENSDb.
*
Expand All @@ -66,34 +36,27 @@ function cloneTableWithSchema<TableType extends Table>(
function buildConcreteEnsIndexerSchema<ConcreteEnsIndexerSchema extends AbstractEnsIndexerSchema>(
Comment thread
tk-o marked this conversation as resolved.
ensIndexerSchemaName: string,
): ConcreteEnsIndexerSchema {
const ensIndexerSchema = {} as ConcreteEnsIndexerSchema;

for (const [key, abstractSchemaObject] of Object.entries(abstractEnsIndexerSchema)) {
if (isTable(abstractSchemaObject)) {
(ensIndexerSchema as any)[key] = cloneTableWithSchema(
abstractSchemaObject,
ensIndexerSchemaName,
);
} else if (isPgEnum(abstractSchemaObject)) {
// Enums are functions; clone by copying properties onto a new function.
// Unlike tables, enums don't rely on prototype identity, so
// Object.assign is sufficient here.
const concreteSchemaObject = Object.assign(
(...args: any[]) => abstractSchemaObject(...args),
abstractSchemaObject,
);
// @ts-expect-error - Drizzle's PgEnum type for the schema symbol is
// typed as readonly, but we need to set it here so
// the output schema definition has the correct schema for
// all table and enum objects.
concreteSchemaObject.schema = ensIndexerSchemaName;
(ensIndexerSchema as any)[key] = concreteSchemaObject;
} else {
(ensIndexerSchema as any)[key] = abstractSchemaObject;
// TODO: Refactor this function to avoid mutating the "abstract" ENSIndexer Schema definition.
// https://github.com/namehash/ensnode/issues/1830
const concreteEnsIndexerSchema = abstractEnsIndexerSchema as ConcreteEnsIndexerSchema;

for (const dbObject of Object.values(abstractEnsIndexerSchema)) {
if (isTable(dbObject)) {
// Update Drizzle table definition to reference
// the specific `ensIndexerSchemaName` name of the ENSIndexer Schema.
// @ts-expect-error - Drizzle types don't define `Table.Symbol.Schema` type,
// but it's present at runtime.
dbObject[Table.Symbol.Schema] = ensIndexerSchemaName;
} else if (isPgEnum(dbObject)) {
// Update Drizzle enum definition to reference
// the specific `ensIndexerSchemaName` name of the ENSIndexer Schema.
// @ts-expect-error - Drizzle types consider `schema` to be
// a readonly property.
dbObject.schema = ensIndexerSchemaName;
Comment thread
lightwalker-eth marked this conversation as resolved.
}
}

return ensIndexerSchema;
return concreteEnsIndexerSchema;
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
}
Comment thread
tk-o marked this conversation as resolved.

/**
Expand Down
50 changes: 28 additions & 22 deletions packages/integration-test-env/src/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,35 +189,41 @@ async function pollIndexingStatus(
ensIndexerSchemaName: string,
timeoutMs: number,
): Promise<void> {
const client = new (await import("@ensnode/ensdb-sdk")).EnsDbReader(
ensDbUrl,
ensIndexerSchemaName,
);
const { EnsDbReader } = await import("@ensnode/ensdb-sdk");
const ensDbClient = new EnsDbReader(ensDbUrl, ensIndexerSchemaName);

const start = Date.now();
log("Polling indexing status...");

while (Date.now() - start < timeoutMs) {
checkAborted();
try {
const snapshot = await client.getIndexingStatusSnapshot();
if (snapshot !== undefined) {
const omnichainStatus = snapshot.omnichainSnapshot.omnichainStatus;
log(`Omnichain status: ${omnichainStatus}`);
if (
omnichainStatus === OmnichainIndexingStatusIds.Following ||
omnichainStatus === OmnichainIndexingStatusIds.Completed
) {
log("Indexing reached target status");
return;
try {
while (Date.now() - start < timeoutMs) {
checkAborted();
try {
const snapshot = await ensDbClient.getIndexingStatusSnapshot();
if (snapshot !== undefined) {
const omnichainStatus = snapshot.omnichainSnapshot.omnichainStatus;
log(`Omnichain status: ${omnichainStatus}`);
if (
omnichainStatus === OmnichainIndexingStatusIds.Following ||
omnichainStatus === OmnichainIndexingStatusIds.Completed
) {
log("Indexing reached target status");
return;
}
}
} catch {
// indexer may not be ready yet
Copy link
Copy Markdown
Contributor

@vercel vercel Bot Mar 26, 2026

Choose a reason for hiding this comment

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

Bare catch block and finally block error masking in the indexing status polling loop

Fix on Vercel

}
} catch {
// indexer may not be ready yet
await new Promise((r) => setTimeout(r, 3000));
}
await new Promise((r) => setTimeout(r, 3000));
throw new Error(`Indexing did not complete within ${timeoutMs / 1000}s`);
} finally {
console.log("Closing ENSDb client...");
// @ts-expect-error - DrizzleClient.$client is not typed to have an `end` method,
// but in practice it does (e.g. pg's Client does).
await ensDbClient.ensDb.$client.end();
Comment thread
tk-o marked this conversation as resolved.
Comment thread
tk-o marked this conversation as resolved.
console.log("ENSDb client closed");
Comment on lines +222 to +225
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

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

The cleanup logic reaches into Drizzle internals (ensDbClient.ensDb.$client.end()) and suppresses typing with @ts-expect-error. This is brittle (a Drizzle type update that adds end will break the build because @ts-expect-error expects an error, and a driver change could make $client.end absent/different). Prefer a small explicit shutdown helper (e.g. a close() method on EnsDbReader) or a runtime/typed guard that checks for an end() function before calling it, without @ts-expect-error.

Suggested change
// @ts-expect-error - DrizzleClient.$client is not typed to have an `end` method,
// but in practice it does (e.g. pg's Client does).
await ensDbClient.ensDb.$client.end();
console.log("ENSDb client closed");
const anyClient = ensDbClient as any;
try {
const rawEnsDb = anyClient?.ensDb;
const rawUnderlyingClient = rawEnsDb?.$client;
const endFn = rawUnderlyingClient?.end;
if (typeof endFn === "function") {
await endFn.call(rawUnderlyingClient);
console.log("ENSDb client closed");
} else {
console.log("ENSDb client has no 'end' method; skipping close");
}
} catch (closeErr) {
console.log("Failed to close ENSDb client cleanly:", closeErr);
}

Copilot uses AI. Check for mistakes.
Comment on lines +221 to +225
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
console.log("Closing ENSDb client...");
// @ts-expect-error - DrizzleClient.$client is not typed to have an `end` method,
// but in practice it does (e.g. pg's Client does).
await ensDbClient.ensDb.$client.end();
console.log("ENSDb client closed");
log("Closing ENSDb client...");
// @ts-expect-error - DrizzleClient.$client is not typed to have an `end` method,
// but in practice it does (e.g. pg's Client does).
await ensDbClient.ensDb.$client.end();
log("ENSDb client closed");

Finally block uses console.log() instead of the standard log() function, causing inconsistent logging formatting

Fix on Vercel

}
throw new Error(`Indexing did not complete within ${timeoutMs / 1000}s`);
}

function logVersions() {
Expand Down Expand Up @@ -312,7 +318,7 @@ async function main() {

// Phase 3: Start ENSIndexer
const ENSINDEXER_URL = `http://localhost:${ENSINDEXER_PORT}`;
const ENSINDEXER_SCHEMA_NAME = "public";
const ENSINDEXER_SCHEMA_NAME = "ensindexer_0";

log("Starting ENSIndexer...");
spawnService(
Expand Down
Loading