From 6acbea09b2170892e86eb579a0765ec1051ec5db Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 12 Mar 2026 06:24:19 +0100 Subject: [PATCH 01/12] Create three database schemas for ENSDb 1) Ponder Schema - describing Ponder runtime state. 2) ENSIndexer Schema - describing ENSIndexer runtime state. 3) ENSNode Schema - describing runtime state across ENSNode services. --- packages/ensnode-schema/package.json | 44 ++++- .../ensv2.subschema.ts} | 0 .../index.ts} | 0 .../protocol-acceleration.subschema.ts} | 0 .../registrars.subschema.ts} | 0 .../subgraph.subschema.ts} | 0 .../tokenscope.subschema.ts} | 0 .../src/ensnode-schema/index.ts | 66 +++++++ .../src/ensnode-schema/schema.ts | 5 + packages/ensnode-schema/src/index.ts | 2 + .../ensnode-schema/src/ponder-schema/index.ts | 186 ++++++++++++++++++ .../src/ponder-schema/schema.ts | 5 + .../src/schemas/ensnode-metadata.schema.ts | 37 ---- packages/ensnode-schema/tsconfig.json | 5 +- packages/ensnode-schema/tsup.config.ts | 8 +- pnpm-lock.yaml | 16 +- 16 files changed, 319 insertions(+), 55 deletions(-) rename packages/ensnode-schema/src/{schemas/ensv2.schema.ts => ensindexer-schema/ensv2.subschema.ts} (100%) rename packages/ensnode-schema/src/{ponder.schema.ts => ensindexer-schema/index.ts} (100%) rename packages/ensnode-schema/src/{schemas/protocol-acceleration.schema.ts => ensindexer-schema/protocol-acceleration.subschema.ts} (100%) rename packages/ensnode-schema/src/{schemas/registrars.schema.ts => ensindexer-schema/registrars.subschema.ts} (100%) rename packages/ensnode-schema/src/{schemas/subgraph.schema.ts => ensindexer-schema/subgraph.subschema.ts} (100%) rename packages/ensnode-schema/src/{schemas/tokenscope.schema.ts => ensindexer-schema/tokenscope.subschema.ts} (100%) create mode 100644 packages/ensnode-schema/src/ensnode-schema/index.ts create mode 100644 packages/ensnode-schema/src/ensnode-schema/schema.ts create mode 100644 packages/ensnode-schema/src/index.ts create mode 100644 packages/ensnode-schema/src/ponder-schema/index.ts create mode 100644 packages/ensnode-schema/src/ponder-schema/schema.ts delete mode 100644 packages/ensnode-schema/src/schemas/ensnode-metadata.schema.ts diff --git a/packages/ensnode-schema/package.json b/packages/ensnode-schema/package.json index 94cd3393a..29a4100e8 100644 --- a/packages/ensnode-schema/package.json +++ b/packages/ensnode-schema/package.json @@ -16,7 +16,10 @@ "Ponder" ], "exports": { - ".": "./src/ponder.schema.ts" + ".": "./src/index.ts", + "./ponder": "./src/ponder-schema/index.ts", + "./ensindexer": "./src/ensindexer-schema/index.ts", + "./ensnode": "./src/ensnode-schema/index.ts" }, "files": [ "dist" @@ -24,26 +27,49 @@ "publishConfig": { "access": "public", "exports": { - "types": "./dist/ponder.schema.d.ts", - "default": "./dist/ponder.schema.js" - }, - "main": "./dist/ponder.schema.js", - "module": "./dist/ponder.schema.mjs", - "types": "./dist/ponder.schema.d.ts" + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./ponder": { + "import": { + "types": "./dist/ponder-schema.d.ts", + "default": "./dist/ponder-schema.js" + } + }, + "./ensindexer": { + "import": { + "types": "./dist/ensindexer-schema.d.ts", + "default": "./dist/ensindexer-schema.js" + } + }, + "./ensnode": { + "import": { + "types": "./dist/ensnode-schema.d.ts", + "default": "./dist/ensnode-schema.js" + } + } + } }, "scripts": { "prepublish": "tsup", "lint": "biome check --write .", "lint:ci": "biome ci" }, - "dependencies": { + "peerDependencies": { + "drizzle-orm": "catalog:", "ponder": "catalog:", "viem": "catalog:" }, "devDependencies": { "@ensnode/ensnode-sdk": "workspace:", "@ensnode/shared-configs": "workspace:*", + "drizzle-orm": "catalog:", + "ponder": "catalog:", "tsup": "catalog:", - "typescript": "catalog:" + "typescript": "catalog:", + "viem": "catalog:" } } diff --git a/packages/ensnode-schema/src/schemas/ensv2.schema.ts b/packages/ensnode-schema/src/ensindexer-schema/ensv2.subschema.ts similarity index 100% rename from packages/ensnode-schema/src/schemas/ensv2.schema.ts rename to packages/ensnode-schema/src/ensindexer-schema/ensv2.subschema.ts diff --git a/packages/ensnode-schema/src/ponder.schema.ts b/packages/ensnode-schema/src/ensindexer-schema/index.ts similarity index 100% rename from packages/ensnode-schema/src/ponder.schema.ts rename to packages/ensnode-schema/src/ensindexer-schema/index.ts diff --git a/packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts b/packages/ensnode-schema/src/ensindexer-schema/protocol-acceleration.subschema.ts similarity index 100% rename from packages/ensnode-schema/src/schemas/protocol-acceleration.schema.ts rename to packages/ensnode-schema/src/ensindexer-schema/protocol-acceleration.subschema.ts diff --git a/packages/ensnode-schema/src/schemas/registrars.schema.ts b/packages/ensnode-schema/src/ensindexer-schema/registrars.subschema.ts similarity index 100% rename from packages/ensnode-schema/src/schemas/registrars.schema.ts rename to packages/ensnode-schema/src/ensindexer-schema/registrars.subschema.ts diff --git a/packages/ensnode-schema/src/schemas/subgraph.schema.ts b/packages/ensnode-schema/src/ensindexer-schema/subgraph.subschema.ts similarity index 100% rename from packages/ensnode-schema/src/schemas/subgraph.schema.ts rename to packages/ensnode-schema/src/ensindexer-schema/subgraph.subschema.ts diff --git a/packages/ensnode-schema/src/schemas/tokenscope.schema.ts b/packages/ensnode-schema/src/ensindexer-schema/tokenscope.subschema.ts similarity index 100% rename from packages/ensnode-schema/src/schemas/tokenscope.schema.ts rename to packages/ensnode-schema/src/ensindexer-schema/tokenscope.subschema.ts diff --git a/packages/ensnode-schema/src/ensnode-schema/index.ts b/packages/ensnode-schema/src/ensnode-schema/index.ts new file mode 100644 index 000000000..2f92b82fa --- /dev/null +++ b/packages/ensnode-schema/src/ensnode-schema/index.ts @@ -0,0 +1,66 @@ +/** + * ENSNode Schema + * + * Defines the database objects describing the ENSNode services state. + */ + +import { primaryKey } from "drizzle-orm/pg-core"; + +import { ENSNODE_SCHEMA } from "./schema"; + +export { ENSNODE_SCHEMA_NAME } from "./schema"; + +/** + * ENSNode Metadata + * + * Possible key value pairs are defined by 'EnsNodeMetadata' type: + * - `EnsNodeMetadataEnsDbVersion` + * - `EnsNodeMetadataEnsIndexerPublicConfig` + * - `EnsNodeMetadataEnsIndexerIndexingStatus` + */ +export const ensNodeMetadata = ENSNODE_SCHEMA.table( + "ensnode_metadata", + (t) => ({ + /** + * ENSIndexer Reference + * + * References the ENSIndexer instance by a unique ENSIndexer schema name + * that a metadata record is associated with. This allows us to support + * multiple ENSIndexer instances using the same database, while ensuring + * that their metadata records do not conflict with each other. + */ + ensIndexerRef: t.text().notNull(), + + /** + * Key + * + * Allowed keys: + * - `EnsNodeMetadataEnsDbVersion['key']` + * - `EnsNodeMetadataEnsIndexerPublicConfig['key']` + * - `EnsNodeMetadataEnsIndexerIndexingStatus['key']` + */ + key: t.text().notNull(), + + /** + * Value + * + * Allowed values: + * - `EnsNodeMetadataEnsDbVersion['value']` + * - `EnsNodeMetadataEnsIndexerPublicConfig['value']` + * - `EnsNodeMetadataEnsIndexerIndexingStatus['value']` + * + * Guaranteed to be a serialized representation of JSON object. + */ + value: t.jsonb().notNull(), + }), + (table) => [ + /** + * Primary key constraint on 'ensIndexerRef' and 'key' columns, + * to ensure that there is only one record for each key per ENSIndexer instance. + */ + primaryKey({ + name: "ensnode_metadata_pkey", + columns: [table.ensIndexerRef, table.key], + }), + ], +); diff --git a/packages/ensnode-schema/src/ensnode-schema/schema.ts b/packages/ensnode-schema/src/ensnode-schema/schema.ts new file mode 100644 index 000000000..1cc926c6b --- /dev/null +++ b/packages/ensnode-schema/src/ensnode-schema/schema.ts @@ -0,0 +1,5 @@ +import { pgSchema } from "drizzle-orm/pg-core"; + +export const ENSNODE_SCHEMA_NAME = "ensnode"; + +export const ENSNODE_SCHEMA = pgSchema(ENSNODE_SCHEMA_NAME); diff --git a/packages/ensnode-schema/src/index.ts b/packages/ensnode-schema/src/index.ts new file mode 100644 index 000000000..49b2d5e31 --- /dev/null +++ b/packages/ensnode-schema/src/index.ts @@ -0,0 +1,2 @@ +// Re-export relevant schema definitions for backward compatibility. +export * from "./ensindexer-schema"; diff --git a/packages/ensnode-schema/src/ponder-schema/index.ts b/packages/ensnode-schema/src/ponder-schema/index.ts new file mode 100644 index 000000000..35af4870f --- /dev/null +++ b/packages/ensnode-schema/src/ponder-schema/index.ts @@ -0,0 +1,186 @@ +// This file defines tables for RPC request cache schema. +// Relevant codebase was copied from Ponder Sync schema defined in the Ponder repository: +// https://github.com/ponder-sh/ponder/blob/ponder%400.16.2/packages/core/src/sync-store/schema.ts + +/** + * RPC Request Cache Schema + * + * Defines the database objects for caching RPC requests and their results. + * + * This file defines tables for RPC request cache schema. + * Relevant codebase was copied from Ponder Sync schema defined in the Ponder repository. + * @see https://github.com/ponder-sh/ponder/blob/ponder%400.16.2/packages/core/src/sync-store/schema.ts + */ + +import { customType, index, primaryKey } from "drizzle-orm/pg-core"; +import type { Address, Hash, Hex } from "viem"; + +import { PONDER_SCHEMA } from "./schema"; + +export { PONDER_SCHEMA_NAME } from "./schema"; + +const numeric78 = customType<{ data: bigint; driverData: string }>({ + dataType() { + return "numeric(78,0)"; + }, + fromDriver(value: string) { + return BigInt(value); + }, +}); + +export const blocks = PONDER_SCHEMA.table( + "blocks", + (t) => ({ + chainId: t.bigint({ mode: "bigint" }).notNull(), + number: t.bigint({ mode: "bigint" }).notNull(), + timestamp: t.bigint({ mode: "bigint" }).notNull(), + hash: t.varchar({ length: 66 }).notNull().$type(), + parentHash: t.varchar({ length: 66 }).notNull().$type(), + logsBloom: t.varchar({ length: 514 }).notNull().$type(), + miner: t.varchar({ length: 42 }).notNull().$type
(), + gasUsed: numeric78().notNull(), + gasLimit: numeric78().notNull(), + baseFeePerGas: numeric78(), + nonce: t.varchar({ length: 18 }).$type(), + mixHash: t.varchar({ length: 66 }).$type(), + stateRoot: t.varchar({ length: 66 }).notNull().$type(), + receiptsRoot: t.varchar({ length: 66 }).notNull().$type(), + transactionsRoot: t.varchar({ length: 66 }).notNull().$type(), + sha3Uncles: t.varchar({ length: 66 }).$type(), + size: numeric78().notNull(), + difficulty: numeric78().notNull(), + totalDifficulty: numeric78(), + extraData: t.text().notNull().$type(), + }), + (table) => [ + primaryKey({ + name: "blocks_pkey", + columns: [table.chainId, table.number], + }), + ], +); + +export const transactions = PONDER_SCHEMA.table( + "transactions", + (t) => ({ + chainId: t.bigint({ mode: "bigint" }).notNull(), + blockNumber: t.bigint({ mode: "bigint" }).notNull(), + transactionIndex: t.integer().notNull(), + hash: t.varchar({ length: 66 }).notNull().$type(), + blockHash: t.varchar({ length: 66 }).notNull().$type(), + from: t.varchar({ length: 42 }).notNull().$type
(), + to: t.varchar({ length: 42 }).$type
(), + input: t.text().notNull().$type(), + value: numeric78().notNull(), + nonce: t.integer().notNull(), + r: t.varchar({ length: 66 }).$type(), + s: t.varchar({ length: 66 }).$type(), + v: numeric78(), + type: t.text().notNull().$type(), + gas: numeric78().notNull(), + gasPrice: numeric78(), + maxFeePerGas: numeric78(), + maxPriorityFeePerGas: numeric78(), + accessList: t.text(), + }), + (table) => [ + primaryKey({ + name: "transactions_pkey", + columns: [table.chainId, table.blockNumber, table.transactionIndex], + }), + ], +); + +export const transactionReceipts = PONDER_SCHEMA.table( + "transaction_receipts", + (t) => ({ + chainId: t.bigint({ mode: "bigint" }).notNull(), + blockNumber: t.bigint({ mode: "bigint" }).notNull(), + transactionIndex: t.integer().notNull(), + transactionHash: t.varchar({ length: 66 }).notNull().$type(), + blockHash: t.varchar({ length: 66 }).notNull().$type(), + from: t.varchar({ length: 42 }).notNull().$type
(), + to: t.varchar({ length: 42 }).$type
(), + contractAddress: t.varchar({ length: 42 }).$type
(), // Note: incorrect + logsBloom: t.varchar({ length: 514 }).notNull().$type(), + gasUsed: numeric78().notNull(), + cumulativeGasUsed: numeric78().notNull(), + effectiveGasPrice: numeric78().notNull(), + status: t.text().notNull().$type(), + type: t.text().notNull().$type(), + }), + (table) => [ + primaryKey({ + name: "transaction_receipts_pkey", + columns: [table.chainId, table.blockNumber, table.transactionIndex], + }), + ], +); + +export const logs = PONDER_SCHEMA.table( + "logs", + (t) => ({ + chainId: t.bigint({ mode: "bigint" }).notNull(), + blockNumber: t.bigint({ mode: "bigint" }).notNull(), + logIndex: t.integer().notNull(), + transactionIndex: t.integer().notNull(), + blockHash: t.varchar({ length: 66 }).notNull().$type(), + transactionHash: t.varchar({ length: 66 }).notNull().$type(), + address: t.varchar({ length: 42 }).notNull().$type
(), + topic0: t.varchar({ length: 66 }).$type(), + topic1: t.varchar({ length: 66 }).$type(), + topic2: t.varchar({ length: 66 }).$type(), + topic3: t.varchar({ length: 66 }).$type(), + data: t.text().notNull().$type(), + }), + (table) => [ + primaryKey({ + name: "logs_pkey", + columns: [table.chainId, table.blockNumber, table.logIndex], + }), + ], +); + +export const traces = PONDER_SCHEMA.table( + "traces", + (t) => ({ + chainId: t.bigint({ mode: "bigint" }).notNull(), + blockNumber: t.bigint({ mode: "bigint" }).notNull(), + transactionIndex: t.integer().notNull(), + traceIndex: t.integer().notNull(), + from: t.varchar({ length: 42 }).notNull().$type
(), + to: t.varchar({ length: 42 }).$type
(), + input: t.text().notNull().$type(), + output: t.text().$type(), + value: numeric78(), + type: t.text().notNull(), + gas: numeric78().notNull(), + gasUsed: numeric78().notNull(), + error: t.text(), + revertReason: t.text(), + subcalls: t.integer().notNull(), + }), + (table) => [ + primaryKey({ + name: "traces_pkey", + columns: [table.chainId, table.blockNumber, table.transactionIndex, table.traceIndex], + }), + ], +); + +export const rpcRequestResults = PONDER_SCHEMA.table( + "rpc_request_results", + (t) => ({ + requestHash: t.text().notNull(), + chainId: t.bigint({ mode: "bigint" }).notNull(), + blockNumber: t.bigint({ mode: "bigint" }), + result: t.text().notNull(), + }), + (table) => [ + primaryKey({ + name: "rpc_request_results_pkey", + columns: [table.chainId, table.requestHash], + }), + index("rpc_request_results_chain_id_block_number_index").on(table.chainId, table.blockNumber), + ], +); diff --git a/packages/ensnode-schema/src/ponder-schema/schema.ts b/packages/ensnode-schema/src/ponder-schema/schema.ts new file mode 100644 index 000000000..459507c66 --- /dev/null +++ b/packages/ensnode-schema/src/ponder-schema/schema.ts @@ -0,0 +1,5 @@ +import { pgSchema } from "drizzle-orm/pg-core"; + +export const PONDER_SCHEMA_NAME = "ponder_sync"; + +export const PONDER_SCHEMA = pgSchema(PONDER_SCHEMA_NAME); diff --git a/packages/ensnode-schema/src/schemas/ensnode-metadata.schema.ts b/packages/ensnode-schema/src/schemas/ensnode-metadata.schema.ts deleted file mode 100644 index bac75fd62..000000000 --- a/packages/ensnode-schema/src/schemas/ensnode-metadata.schema.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Schema Definitions that hold metadata about the ENSNode instance. - */ - -import { onchainTable } from "ponder"; - -/** - * ENSNode Metadata - * - * Possible key value pairs are defined by 'EnsNodeMetadata' type: - * - `EnsNodeMetadataEnsDbVersion` - * - `EnsNodeMetadataEnsIndexerPublicConfig` - * - `EnsNodeMetadataEnsIndexerIndexingStatus` - */ -export const ensNodeMetadata = onchainTable("ensnode_metadata", (t) => ({ - /** - * Key - * - * Allowed keys: - * - `EnsNodeMetadataEnsDbVersion['key']` - * - `EnsNodeMetadataEnsIndexerPublicConfig['key']` - * - `EnsNodeMetadataEnsIndexerIndexingStatus['key']` - */ - key: t.text().primaryKey(), - - /** - * Value - * - * Allowed values: - * - `EnsNodeMetadataEnsDbVersion['value']` - * - `EnsNodeMetadataEnsIndexerPublicConfig['value']` - * - `EnsNodeMetadataEnsIndexerIndexingStatus['value']` - * - * Guaranteed to be a serialized representation of JSON object. - */ - value: t.jsonb().notNull(), -})); diff --git a/packages/ensnode-schema/tsconfig.json b/packages/ensnode-schema/tsconfig.json index d34e39a61..d4861e993 100644 --- a/packages/ensnode-schema/tsconfig.json +++ b/packages/ensnode-schema/tsconfig.json @@ -1,5 +1,8 @@ { - "extends": "@ensnode/shared-configs/tsconfig.ponder.json", + "extends": "@ensnode/shared-configs/tsconfig.lib.json", + "compilerOptions": { + "rootDir": "." // necessary for 'The project root is ambiguous' + }, "include": ["./**/*.ts"], "exclude": ["node_modules"] } diff --git a/packages/ensnode-schema/tsup.config.ts b/packages/ensnode-schema/tsup.config.ts index afadde0d3..de83ab0d2 100644 --- a/packages/ensnode-schema/tsup.config.ts +++ b/packages/ensnode-schema/tsup.config.ts @@ -1,13 +1,19 @@ import { defineConfig } from "tsup"; export default defineConfig({ - entry: ["src/ponder.schema.ts"], + entry: [ + "src/index.ts", + "src/ponder-schema/index.ts", + "src/ensindexer-schema/index.ts", + "src/ensnode-schema/index.ts", + ], platform: "neutral", format: ["esm"], target: "es2022", bundle: true, splitting: false, sourcemap: true, + dts: true, clean: true, external: ["viem", "ponder", "drizzle-orm", "drizzle-orm/pg-core"], outDir: "./dist", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 120d2868c..1e308a215 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -862,13 +862,6 @@ importers: version: 4.0.5(@types/debug@4.1.12)(@types/node@24.10.9)(jiti@2.6.1)(jsdom@27.0.1(postcss@8.5.6))(lightningcss@1.30.2)(tsx@4.20.6)(yaml@2.8.1) packages/ensnode-schema: - dependencies: - ponder: - specifier: 'catalog:' - version: 0.16.3(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.5)(lightningcss@1.30.2)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(zod@3.25.76) - viem: - specifier: 'catalog:' - version: 2.38.5(typescript@5.9.3)(zod@3.25.76) devDependencies: '@ensnode/ensnode-sdk': specifier: 'workspace:' @@ -876,12 +869,21 @@ importers: '@ensnode/shared-configs': specifier: workspace:* version: link:../shared-configs + drizzle-orm: + specifier: 'catalog:' + version: 0.41.0(@electric-sql/pglite@0.2.13)(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/pg@8.16.0)(kysely@0.26.3)(pg@8.16.3) + ponder: + specifier: 'catalog:' + version: 0.16.3(@opentelemetry/api@1.9.0(patch_hash=4b2adeefaf7c22f9987d0a125d69cab900719bec7ed7636648bea6947107033a))(@types/node@24.10.9)(@types/pg@8.16.0)(hono@4.12.5)(lightningcss@1.30.2)(typescript@5.9.3)(viem@2.38.5(typescript@5.9.3)(zod@3.25.76))(zod@3.25.76) tsup: specifier: 'catalog:' version: 8.5.0(jiti@2.6.1)(postcss@8.5.6)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: 'catalog:' version: 5.9.3 + viem: + specifier: 'catalog:' + version: 2.38.5(typescript@5.9.3)(zod@3.25.76) packages/ensnode-sdk: dependencies: From eed4800da0893635edf414e106990d76cfda722e Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 12 Mar 2026 06:24:35 +0100 Subject: [PATCH 02/12] Update import paths --- .../ensnode-schema/src/ensindexer-schema/index.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/ensnode-schema/src/ensindexer-schema/index.ts b/packages/ensnode-schema/src/ensindexer-schema/index.ts index 58508341e..10ce6e45d 100644 --- a/packages/ensnode-schema/src/ensindexer-schema/index.ts +++ b/packages/ensnode-schema/src/ensindexer-schema/index.ts @@ -1,10 +1,9 @@ /** - * Merge the various sub-schemas into a single ponder (drizzle) schema. + * Merge the various sub-schemas into ENSIndexer Schema. */ -export * from "./schemas/ensnode-metadata.schema"; -export * from "./schemas/ensv2.schema"; -export * from "./schemas/protocol-acceleration.schema"; -export * from "./schemas/registrars.schema"; -export * from "./schemas/subgraph.schema"; -export * from "./schemas/tokenscope.schema"; +export * from "./ensv2.subschema"; +export * from "./protocol-acceleration.subschema"; +export * from "./registrars.subschema"; +export * from "./subgraph.subschema"; +export * from "./tokenscope.subschema"; From 16c55842fb3030ba1e4ad0bf3669fc0fcf289461 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 12 Mar 2026 06:30:05 +0100 Subject: [PATCH 03/12] Enforce read-only connection with ENSDb for ENSApi runtime Replaces `makeDrizzle` DB connection helper with `makeReadOnlyDrizzle` one that is guaranteed to connect in read-only mode. --- apps/ensapi/src/lib/db.ts | 7 +++++-- apps/ensapi/src/lib/handlers/drizzle.ts | 11 ++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/apps/ensapi/src/lib/db.ts b/apps/ensapi/src/lib/db.ts index 0148d37ec..71a2f5e78 100644 --- a/apps/ensapi/src/lib/db.ts +++ b/apps/ensapi/src/lib/db.ts @@ -2,9 +2,12 @@ import config from "@/config"; import * as schema from "@ensnode/ensnode-schema"; -import { makeDrizzle } from "@/lib/handlers/drizzle"; +import { makeReadOnlyDrizzle } from "@/lib/handlers/drizzle"; -export const db = makeDrizzle({ +/** + * Read-only Drizzle instance for ENSDb queries to ENSIndexer Schema + */ +export const db = makeReadOnlyDrizzle({ databaseUrl: config.databaseUrl, databaseSchema: config.databaseSchemaName, schema, diff --git a/apps/ensapi/src/lib/handlers/drizzle.ts b/apps/ensapi/src/lib/handlers/drizzle.ts index a5d8f36da..6847f04b3 100644 --- a/apps/ensapi/src/lib/handlers/drizzle.ts +++ b/apps/ensapi/src/lib/handlers/drizzle.ts @@ -1,5 +1,6 @@ import { setDatabaseSchema } from "@ponder/client"; import { drizzle } from "drizzle-orm/node-postgres"; +import { parseIntoClientConfig } from "pg-connection-string"; import { makeLogger } from "@/lib/logger"; @@ -8,9 +9,9 @@ type Schema = { [name: string]: unknown }; const logger = makeLogger("drizzle"); /** - * Makes a Drizzle DB object. + * Makes a read-only Drizzle DB object. */ -export const makeDrizzle = ({ +export const makeReadOnlyDrizzle = ({ schema, databaseUrl, databaseSchema, @@ -22,7 +23,11 @@ export const makeDrizzle = ({ // monkeypatch schema onto tables setDatabaseSchema(schema, databaseSchema); - return drizzle(databaseUrl, { + return drizzle({ + connection: { + ...parseIntoClientConfig(databaseUrl), + options: "-c default_transaction_read_only=on", + }, schema, casing: "snake_case", logger: { From 92afddfa46ef366d1152ff4f0e5da231b389b3da Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 12 Mar 2026 06:53:02 +0100 Subject: [PATCH 04/12] Update `EnsDbClient` for ENSIndexer to handle updated DB schema definitions. Handle multi-tenancy in ENSNode Metadata table. --- .../src/lib/ensdb-client/ensdb-client.test.ts | 11 +++-- .../src/lib/ensdb-client/ensdb-client.ts | 44 ++++++++++--------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts index 0462b7788..15f28f3f4 100644 --- a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts +++ b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts @@ -1,6 +1,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { ensNodeMetadata } from "@ensnode/ensnode-schema"; +import * as ensNodeSchema from "@ensnode/ensnode-schema/ensnode"; import { deserializeCrossChainIndexingStatusSnapshot, EnsNodeMetadataKeys, @@ -59,7 +59,7 @@ describe("EnsDbClient", () => { await expect(client.getEnsDbVersion()).resolves.toBeUndefined(); expect(selectMock).toHaveBeenCalledTimes(1); - expect(fromMock).toHaveBeenCalledWith(ensNodeMetadata); + expect(fromMock).toHaveBeenCalledWith(ensNodeSchema.ensNodeMetadata); }); it("returns value when one record exists", async () => { @@ -150,13 +150,14 @@ describe("EnsDbClient", () => { await client.upsertEnsDbVersion("0.2.0"); // assert - expect(insertMock).toHaveBeenCalledWith(ensNodeMetadata); + expect(insertMock).toHaveBeenCalledWith(ensNodeSchema.ensNodeMetadata); expect(valuesMock).toHaveBeenCalledWith({ + ensIndexerRef: ensDbClientMock.databaseSchemaName, key: EnsNodeMetadataKeys.EnsDbVersion, value: "0.2.0", }); expect(onConflictDoUpdateMock).toHaveBeenCalledWith({ - target: ensNodeMetadata.key, + target: [ensNodeSchema.ensNodeMetadata.ensIndexerRef, ensNodeSchema.ensNodeMetadata.key], set: { value: "0.2.0" }, }); }); @@ -176,6 +177,7 @@ describe("EnsDbClient", () => { // assert expect(valuesMock).toHaveBeenCalledWith({ + ensIndexerRef: ensDbClientMock.databaseSchemaName, key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, value: expectedValue, }); @@ -199,6 +201,7 @@ describe("EnsDbClient", () => { // assert expect(valuesMock).toHaveBeenCalledWith({ + ensIndexerRef: ensDbClientMock.databaseSchemaName, key: EnsNodeMetadataKeys.EnsIndexerIndexingStatus, value: expectedValue, }); diff --git a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts index 3af5de58f..83a23f617 100644 --- a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts +++ b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts @@ -1,7 +1,7 @@ import type { NodePgDatabase } from "drizzle-orm/node-postgres"; -import { eq, sql } from "drizzle-orm/sql"; +import { and, eq, sql } from "drizzle-orm/sql"; -import { ensNodeMetadata } from "@ensnode/ensnode-schema"; +import * as ensNodeSchema from "@ensnode/ensnode-schema/ensnode"; import { type CrossChainIndexingStatusSnapshot, deserializeCrossChainIndexingStatusSnapshot, @@ -20,21 +20,12 @@ import { import { makeDrizzle } from "./drizzle"; -/** - * ENSDb Client Schema - * - * Includes schema definitions for {@link EnsDbClient} queries and mutations. - */ -const schema = { - ensNodeMetadata, -}; - /** * Drizzle database * * Allows interacting with Postgres database for ENSDb, using Drizzle ORM. */ -interface DrizzleDb extends NodePgDatabase {} +interface DrizzleDb extends NodePgDatabase {} /** * ENSDb Client @@ -53,16 +44,23 @@ export class EnsDbClient implements EnsDbClientQuery, EnsDbClientMutation { */ private db: DrizzleDb; + /** + * ENSIndexer reference string for multi-tenancy in ENSDb. + */ + private ensIndexerRef: string; + /** * @param databaseUrl connection string for ENSDb Postgres database - * @param databaseSchemaName Postgres schema name for ENSDb tables + * @param ensIndexerRef reference string for ENSIndexer instance (used for multi-tenancy in ENSDb) */ - constructor(databaseUrl: string, databaseSchemaName: string) { + constructor(databaseUrl: string, ensIndexerRef: string) { this.db = makeDrizzle({ - databaseSchema: databaseSchemaName, + databaseSchema: ensNodeSchema.ENSNODE_SCHEMA_NAME, databaseUrl, - schema, + schema: ensNodeSchema, }); + + this.ensIndexerRef = ensIndexerRef; } /** @@ -154,8 +152,13 @@ export class EnsDbClient implements EnsDbClientQuery, EnsDbClientMutation { ): Promise { const result = await this.db .select() - .from(ensNodeMetadata) - .where(eq(ensNodeMetadata.key, metadata.key)); + .from(ensNodeSchema.ensNodeMetadata) + .where( + and( + eq(ensNodeSchema.ensNodeMetadata.ensIndexerRef, this.ensIndexerRef), + eq(ensNodeSchema.ensNodeMetadata.key, metadata.key), + ), + ); if (result.length === 0) { return undefined; @@ -186,13 +189,14 @@ export class EnsDbClient implements EnsDbClientQuery, EnsDbClientMutation { ); await tx - .insert(ensNodeMetadata) + .insert(ensNodeSchema.ensNodeMetadata) .values({ + ensIndexerRef: this.ensIndexerRef, key: metadata.key, value: metadata.value, }) .onConflictDoUpdate({ - target: ensNodeMetadata.key, + target: [ensNodeSchema.ensNodeMetadata.ensIndexerRef, ensNodeSchema.ensNodeMetadata.key], set: { value: metadata.value }, }); }); From b5bd5ca8681268c21ef72eb1a076f9fa5dc20204 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 12 Mar 2026 11:57:16 +0100 Subject: [PATCH 05/12] Replace Ponder Schema definitions with link to Ponder repository. --- .../ensnode-schema/src/ponder-schema/index.ts | 186 +----------------- .../src/ponder-schema/schema.ts | 5 - 2 files changed, 4 insertions(+), 187 deletions(-) delete mode 100644 packages/ensnode-schema/src/ponder-schema/schema.ts diff --git a/packages/ensnode-schema/src/ponder-schema/index.ts b/packages/ensnode-schema/src/ponder-schema/index.ts index 35af4870f..bd2514647 100644 --- a/packages/ensnode-schema/src/ponder-schema/index.ts +++ b/packages/ensnode-schema/src/ponder-schema/index.ts @@ -1,186 +1,8 @@ -// This file defines tables for RPC request cache schema. -// Relevant codebase was copied from Ponder Sync schema defined in the Ponder repository: -// https://github.com/ponder-sh/ponder/blob/ponder%400.16.2/packages/core/src/sync-store/schema.ts - /** - * RPC Request Cache Schema - * - * Defines the database objects for caching RPC requests and their results. + * Ponder Schema * - * This file defines tables for RPC request cache schema. - * Relevant codebase was copied from Ponder Sync schema defined in the Ponder repository. - * @see https://github.com/ponder-sh/ponder/blob/ponder%400.16.2/packages/core/src/sync-store/schema.ts + * Definition of the Ponder Schema can be found in the Ponder repository. + * @see https://github.com/ponder-sh/ponder/blob/ponder%400.16.3/packages/core/src/sync-store/schema.ts */ -import { customType, index, primaryKey } from "drizzle-orm/pg-core"; -import type { Address, Hash, Hex } from "viem"; - -import { PONDER_SCHEMA } from "./schema"; - -export { PONDER_SCHEMA_NAME } from "./schema"; - -const numeric78 = customType<{ data: bigint; driverData: string }>({ - dataType() { - return "numeric(78,0)"; - }, - fromDriver(value: string) { - return BigInt(value); - }, -}); - -export const blocks = PONDER_SCHEMA.table( - "blocks", - (t) => ({ - chainId: t.bigint({ mode: "bigint" }).notNull(), - number: t.bigint({ mode: "bigint" }).notNull(), - timestamp: t.bigint({ mode: "bigint" }).notNull(), - hash: t.varchar({ length: 66 }).notNull().$type(), - parentHash: t.varchar({ length: 66 }).notNull().$type(), - logsBloom: t.varchar({ length: 514 }).notNull().$type(), - miner: t.varchar({ length: 42 }).notNull().$type
(), - gasUsed: numeric78().notNull(), - gasLimit: numeric78().notNull(), - baseFeePerGas: numeric78(), - nonce: t.varchar({ length: 18 }).$type(), - mixHash: t.varchar({ length: 66 }).$type(), - stateRoot: t.varchar({ length: 66 }).notNull().$type(), - receiptsRoot: t.varchar({ length: 66 }).notNull().$type(), - transactionsRoot: t.varchar({ length: 66 }).notNull().$type(), - sha3Uncles: t.varchar({ length: 66 }).$type(), - size: numeric78().notNull(), - difficulty: numeric78().notNull(), - totalDifficulty: numeric78(), - extraData: t.text().notNull().$type(), - }), - (table) => [ - primaryKey({ - name: "blocks_pkey", - columns: [table.chainId, table.number], - }), - ], -); - -export const transactions = PONDER_SCHEMA.table( - "transactions", - (t) => ({ - chainId: t.bigint({ mode: "bigint" }).notNull(), - blockNumber: t.bigint({ mode: "bigint" }).notNull(), - transactionIndex: t.integer().notNull(), - hash: t.varchar({ length: 66 }).notNull().$type(), - blockHash: t.varchar({ length: 66 }).notNull().$type(), - from: t.varchar({ length: 42 }).notNull().$type
(), - to: t.varchar({ length: 42 }).$type
(), - input: t.text().notNull().$type(), - value: numeric78().notNull(), - nonce: t.integer().notNull(), - r: t.varchar({ length: 66 }).$type(), - s: t.varchar({ length: 66 }).$type(), - v: numeric78(), - type: t.text().notNull().$type(), - gas: numeric78().notNull(), - gasPrice: numeric78(), - maxFeePerGas: numeric78(), - maxPriorityFeePerGas: numeric78(), - accessList: t.text(), - }), - (table) => [ - primaryKey({ - name: "transactions_pkey", - columns: [table.chainId, table.blockNumber, table.transactionIndex], - }), - ], -); - -export const transactionReceipts = PONDER_SCHEMA.table( - "transaction_receipts", - (t) => ({ - chainId: t.bigint({ mode: "bigint" }).notNull(), - blockNumber: t.bigint({ mode: "bigint" }).notNull(), - transactionIndex: t.integer().notNull(), - transactionHash: t.varchar({ length: 66 }).notNull().$type(), - blockHash: t.varchar({ length: 66 }).notNull().$type(), - from: t.varchar({ length: 42 }).notNull().$type
(), - to: t.varchar({ length: 42 }).$type
(), - contractAddress: t.varchar({ length: 42 }).$type
(), // Note: incorrect - logsBloom: t.varchar({ length: 514 }).notNull().$type(), - gasUsed: numeric78().notNull(), - cumulativeGasUsed: numeric78().notNull(), - effectiveGasPrice: numeric78().notNull(), - status: t.text().notNull().$type(), - type: t.text().notNull().$type(), - }), - (table) => [ - primaryKey({ - name: "transaction_receipts_pkey", - columns: [table.chainId, table.blockNumber, table.transactionIndex], - }), - ], -); - -export const logs = PONDER_SCHEMA.table( - "logs", - (t) => ({ - chainId: t.bigint({ mode: "bigint" }).notNull(), - blockNumber: t.bigint({ mode: "bigint" }).notNull(), - logIndex: t.integer().notNull(), - transactionIndex: t.integer().notNull(), - blockHash: t.varchar({ length: 66 }).notNull().$type(), - transactionHash: t.varchar({ length: 66 }).notNull().$type(), - address: t.varchar({ length: 42 }).notNull().$type
(), - topic0: t.varchar({ length: 66 }).$type(), - topic1: t.varchar({ length: 66 }).$type(), - topic2: t.varchar({ length: 66 }).$type(), - topic3: t.varchar({ length: 66 }).$type(), - data: t.text().notNull().$type(), - }), - (table) => [ - primaryKey({ - name: "logs_pkey", - columns: [table.chainId, table.blockNumber, table.logIndex], - }), - ], -); - -export const traces = PONDER_SCHEMA.table( - "traces", - (t) => ({ - chainId: t.bigint({ mode: "bigint" }).notNull(), - blockNumber: t.bigint({ mode: "bigint" }).notNull(), - transactionIndex: t.integer().notNull(), - traceIndex: t.integer().notNull(), - from: t.varchar({ length: 42 }).notNull().$type
(), - to: t.varchar({ length: 42 }).$type
(), - input: t.text().notNull().$type(), - output: t.text().$type(), - value: numeric78(), - type: t.text().notNull(), - gas: numeric78().notNull(), - gasUsed: numeric78().notNull(), - error: t.text(), - revertReason: t.text(), - subcalls: t.integer().notNull(), - }), - (table) => [ - primaryKey({ - name: "traces_pkey", - columns: [table.chainId, table.blockNumber, table.transactionIndex, table.traceIndex], - }), - ], -); - -export const rpcRequestResults = PONDER_SCHEMA.table( - "rpc_request_results", - (t) => ({ - requestHash: t.text().notNull(), - chainId: t.bigint({ mode: "bigint" }).notNull(), - blockNumber: t.bigint({ mode: "bigint" }), - result: t.text().notNull(), - }), - (table) => [ - primaryKey({ - name: "rpc_request_results_pkey", - columns: [table.chainId, table.requestHash], - }), - index("rpc_request_results_chain_id_block_number_index").on(table.chainId, table.blockNumber), - ], -); +export const PONDER_SCHEMA_NAME = "ponder_sync"; diff --git a/packages/ensnode-schema/src/ponder-schema/schema.ts b/packages/ensnode-schema/src/ponder-schema/schema.ts deleted file mode 100644 index 459507c66..000000000 --- a/packages/ensnode-schema/src/ponder-schema/schema.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { pgSchema } from "drizzle-orm/pg-core"; - -export const PONDER_SCHEMA_NAME = "ponder_sync"; - -export const PONDER_SCHEMA = pgSchema(PONDER_SCHEMA_NAME); From 3f7f0e8332c9444cd342d14cf58ba1ef0d762a65 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 12 Mar 2026 11:57:29 +0100 Subject: [PATCH 06/12] Introduce docs for the schema package --- packages/ensnode-schema/src/README.md | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 packages/ensnode-schema/src/README.md diff --git a/packages/ensnode-schema/src/README.md b/packages/ensnode-schema/src/README.md new file mode 100644 index 000000000..022addd1a --- /dev/null +++ b/packages/ensnode-schema/src/README.md @@ -0,0 +1,28 @@ +# ENSDb Schemas + +Package defining database schemas used in ENSDb. + +## Ponder Schema + +- Owned by Ponder app. +- Shared across ENSIndexer instances. +- Includes responses to RPC requests made by cached public clients. +- Usually large in size, depending on selected ENS Namespace. +- Takes a long time to build. +- Backups highly recommended for sharing RPC cache across different ENSNode environments. + - For example, pulling production backup into local environment in order to test production workflows in isolation. + +## ENSIndexer Schema + +- Owned by an ENSIndexer instance and also influenced by Ponder implementation details, as ENSIndexer is a Ponder app. +- Isolated for specific ENSIndexer instance. +- Includes indexed data based on event handlers logic from active ENSIndexer plugins. +- May be large in size, depending on selected ENS Namespace and active plugins. +- May take a long time to build. Must be re-built from scratch in case of indexing logic changes (i.e. event handler code change, active plugins change). + +## ENSNode Schema + +- Owned by ENSNode services. +- Includes metadata describing configuration and state of various ENSNode services. +- Tiny in size. +- Takes virtually no time to be built. From c1964a20445ff68997553328aa9c438c02b56f34 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 12 Mar 2026 12:04:24 +0100 Subject: [PATCH 07/12] Apply AI PR feedback --- .../src/lib/ensdb-client/ensdb-client.mock.ts | 4 ++ .../src/lib/ensdb-client/ensdb-client.test.ts | 51 +++++-------------- .../src/lib/ensdb-client/singleton.ts | 6 ++- packages/ensnode-schema/package.json | 12 ++--- 4 files changed, 27 insertions(+), 46 deletions(-) diff --git a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.mock.ts b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.mock.ts index 7387cc19f..91f7cf273 100644 --- a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.mock.ts +++ b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.mock.ts @@ -23,6 +23,10 @@ export const databaseUrl = "postgres://user:pass@localhost:5432/ensdb"; export const databaseSchemaName = "public"; +// This is the same as the default value of config.databaseSchemaName, +// which is used as the ensIndexerRef for multi-tenancy in ENSDb. +export const ensIndexerRef = databaseSchemaName; + export const publicConfig = { databaseSchemaName, ensRainbowPublicConfig: { diff --git a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts index 15f28f3f4..480bc8a74 100644 --- a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts +++ b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.test.ts @@ -50,10 +50,7 @@ describe("EnsDbClient", () => { describe("getEnsDbVersion", () => { it("returns undefined when no record exists", async () => { // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); // act & assert await expect(client.getEnsDbVersion()).resolves.toBeUndefined(); @@ -66,10 +63,7 @@ describe("EnsDbClient", () => { // arrange selectResult.current = [{ value: "0.1.0" }]; - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); // act & assert await expect(client.getEnsDbVersion()).resolves.toBe("0.1.0"); @@ -81,10 +75,7 @@ describe("EnsDbClient", () => { // arrange selectResult.current = [{ value: "0.1.0" }, { value: "0.1.1" }]; - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); // act & assert await expect(client.getEnsDbVersion()).rejects.toThrowError(/ensdb_version/i); @@ -94,10 +85,7 @@ describe("EnsDbClient", () => { describe("getEnsIndexerPublicConfig", () => { it("returns undefined when no record exists", async () => { // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); // act & assert await expect(client.getEnsIndexerPublicConfig()).resolves.toBeUndefined(); @@ -108,10 +96,7 @@ describe("EnsDbClient", () => { const serializedConfig = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig); selectResult.current = [{ value: serializedConfig }]; - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); // act & assert await expect(client.getEnsIndexerPublicConfig()).resolves.toStrictEqual( @@ -125,10 +110,7 @@ describe("EnsDbClient", () => { // arrange selectResult.current = [{ value: ensDbClientMock.serializedSnapshot }]; - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); const expected = deserializeCrossChainIndexingStatusSnapshot( ensDbClientMock.serializedSnapshot, ); @@ -141,10 +123,7 @@ describe("EnsDbClient", () => { describe("upsertEnsDbVersion", () => { it("writes the database version metadata", async () => { // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); // act await client.upsertEnsDbVersion("0.2.0"); @@ -152,7 +131,7 @@ describe("EnsDbClient", () => { // assert expect(insertMock).toHaveBeenCalledWith(ensNodeSchema.ensNodeMetadata); expect(valuesMock).toHaveBeenCalledWith({ - ensIndexerRef: ensDbClientMock.databaseSchemaName, + ensIndexerRef: ensDbClientMock.ensIndexerRef, key: EnsNodeMetadataKeys.EnsDbVersion, value: "0.2.0", }); @@ -166,10 +145,7 @@ describe("EnsDbClient", () => { describe("upsertEnsIndexerPublicConfig", () => { it("serializes and writes the public config", async () => { // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); const expectedValue = serializeEnsIndexerPublicConfig(ensDbClientMock.publicConfig); // act @@ -177,7 +153,7 @@ describe("EnsDbClient", () => { // assert expect(valuesMock).toHaveBeenCalledWith({ - ensIndexerRef: ensDbClientMock.databaseSchemaName, + ensIndexerRef: ensDbClientMock.ensIndexerRef, key: EnsNodeMetadataKeys.EnsIndexerPublicConfig, value: expectedValue, }); @@ -187,10 +163,7 @@ describe("EnsDbClient", () => { describe("upsertIndexingStatusSnapshot", () => { it("serializes and writes the indexing status snapshot", async () => { // arrange - const client = new EnsDbClient( - ensDbClientMock.databaseUrl, - ensDbClientMock.databaseSchemaName, - ); + const client = new EnsDbClient(ensDbClientMock.databaseUrl, ensDbClientMock.ensIndexerRef); const snapshot = deserializeCrossChainIndexingStatusSnapshot( ensDbClientMock.serializedSnapshot, ); @@ -201,7 +174,7 @@ describe("EnsDbClient", () => { // assert expect(valuesMock).toHaveBeenCalledWith({ - ensIndexerRef: ensDbClientMock.databaseSchemaName, + ensIndexerRef: ensDbClientMock.ensIndexerRef, key: EnsNodeMetadataKeys.EnsIndexerIndexingStatus, value: expectedValue, }); diff --git a/apps/ensindexer/src/lib/ensdb-client/singleton.ts b/apps/ensindexer/src/lib/ensdb-client/singleton.ts index 3ab2225fd..a47ef727c 100644 --- a/apps/ensindexer/src/lib/ensdb-client/singleton.ts +++ b/apps/ensindexer/src/lib/ensdb-client/singleton.ts @@ -2,7 +2,11 @@ import config from "@/config"; import { EnsDbClient } from "./ensdb-client"; +// config.databaseSchemaName is unique per ENSIndexer instance and is used as the ensIndexerRef +// tenant key in the shared ENSNode schema (ensnode.*). +const ensIndexerRef = config.databaseSchemaName; + /** * Singleton instance of {@link EnsDbClient} for use in ENSIndexer. */ -export const ensDbClient = new EnsDbClient(config.databaseUrl, config.databaseSchemaName); +export const ensDbClient = new EnsDbClient(config.databaseUrl, ensIndexerRef); diff --git a/packages/ensnode-schema/package.json b/packages/ensnode-schema/package.json index 29a4100e8..7e4726696 100644 --- a/packages/ensnode-schema/package.json +++ b/packages/ensnode-schema/package.json @@ -35,20 +35,20 @@ }, "./ponder": { "import": { - "types": "./dist/ponder-schema.d.ts", - "default": "./dist/ponder-schema.js" + "types": "./dist/ponder-schema/index.d.ts", + "default": "./dist/ponder-schema/index.js" } }, "./ensindexer": { "import": { - "types": "./dist/ensindexer-schema.d.ts", - "default": "./dist/ensindexer-schema.js" + "types": "./dist/ensindexer-schema/index.d.ts", + "default": "./dist/ensindexer-schema/index.js" } }, "./ensnode": { "import": { - "types": "./dist/ensnode-schema.d.ts", - "default": "./dist/ensnode-schema.js" + "types": "./dist/ensnode-schema/index.d.ts", + "default": "./dist/ensnode-schema/index.js" } } } From b4fb135dc7d4bcc22d7db340e4e3d293c87015d3 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 12 Mar 2026 12:08:12 +0100 Subject: [PATCH 08/12] docs(changeset): Split database schemas into Ponder Schema, ENSIndexer Schema, and ENSNode Schema. --- .changeset/petite-peaches-watch.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/petite-peaches-watch.md diff --git a/.changeset/petite-peaches-watch.md b/.changeset/petite-peaches-watch.md new file mode 100644 index 000000000..2fb43b5ae --- /dev/null +++ b/.changeset/petite-peaches-watch.md @@ -0,0 +1,5 @@ +--- +"@ensnode/ensnode-schema": minor +--- + +Split database schemas into Ponder Schema, ENSIndexer Schema, and ENSNode Schema. From 6b6f805a110e3cf05c5b7b04e97e367a418e7c79 Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 12 Mar 2026 12:08:51 +0100 Subject: [PATCH 09/12] docs(changeset): Updated ENSDb connections to be always read-only. --- .changeset/vast-comics-burn.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/vast-comics-burn.md diff --git a/.changeset/vast-comics-burn.md b/.changeset/vast-comics-burn.md new file mode 100644 index 000000000..98e82272e --- /dev/null +++ b/.changeset/vast-comics-burn.md @@ -0,0 +1,5 @@ +--- +"ensapi": minor +--- + +Updated ENSDb connections to be always read-only. From 9c05883502d389a4d4dd8a4dd5a38b9a37faa4fd Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 12 Mar 2026 12:36:45 +0100 Subject: [PATCH 10/12] Update make drizzle helpers to only apply schema "monkeypatching" when needed for ENSIndexer schemas --- apps/ensapi/src/lib/handlers/drizzle.ts | 17 ++++++++++++----- apps/ensindexer/src/lib/ensdb-client/drizzle.ts | 6 ------ .../src/lib/ensdb-client/ensdb-client.ts | 4 ++-- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/apps/ensapi/src/lib/handlers/drizzle.ts b/apps/ensapi/src/lib/handlers/drizzle.ts index 6847f04b3..173252c90 100644 --- a/apps/ensapi/src/lib/handlers/drizzle.ts +++ b/apps/ensapi/src/lib/handlers/drizzle.ts @@ -18,15 +18,22 @@ export const makeReadOnlyDrizzle = ({ }: { schema: SCHEMA; databaseUrl: string; - databaseSchema: string; + databaseSchema?: string; }) => { - // monkeypatch schema onto tables - setDatabaseSchema(schema, databaseSchema); + // monkeypatch schema onto tables if databaseSchema is provided + if (databaseSchema) { + setDatabaseSchema(schema, databaseSchema); + } + + const parsedConfig = parseIntoClientConfig(databaseUrl); + const existingOptions = parsedConfig.options || ""; + const readOnlyOption = "-c default_transaction_read_only=on"; return drizzle({ connection: { - ...parseIntoClientConfig(databaseUrl), - options: "-c default_transaction_read_only=on", + ...parsedConfig, + // Combine existing options from URL with read-only requirement + options: existingOptions ? `${existingOptions} ${readOnlyOption}` : readOnlyOption, }, schema, casing: "snake_case", diff --git a/apps/ensindexer/src/lib/ensdb-client/drizzle.ts b/apps/ensindexer/src/lib/ensdb-client/drizzle.ts index b5cc9b5c7..079e18c58 100644 --- a/apps/ensindexer/src/lib/ensdb-client/drizzle.ts +++ b/apps/ensindexer/src/lib/ensdb-client/drizzle.ts @@ -2,7 +2,6 @@ // We currently duplicate the makeDrizzle function, as we don't have // a shared package for backend code yet. When we do, we can move // this function to the shared package and import it in both places. -import { setDatabaseSchema } from "@ponder/client"; import { drizzle } from "drizzle-orm/node-postgres"; type Schema = { [name: string]: unknown }; @@ -13,14 +12,9 @@ type Schema = { [name: string]: unknown }; export const makeDrizzle = ({ schema, databaseUrl, - databaseSchema, }: { schema: SCHEMA; databaseUrl: string; - databaseSchema: string; }) => { - // monkeypatch schema onto tables - setDatabaseSchema(schema, databaseSchema); - return drizzle(databaseUrl, { schema, casing: "snake_case" }); }; diff --git a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts index 83a23f617..e6769a692 100644 --- a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts +++ b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts @@ -55,7 +55,6 @@ export class EnsDbClient implements EnsDbClientQuery, EnsDbClientMutation { */ constructor(databaseUrl: string, ensIndexerRef: string) { this.db = makeDrizzle({ - databaseSchema: ensNodeSchema.ENSNODE_SCHEMA_NAME, databaseUrl, schema: ensNodeSchema, }); @@ -145,7 +144,8 @@ export class EnsDbClient implements EnsDbClientQuery, EnsDbClientMutation { * * @returns selected record in ENSDb. * @throws when more than one matching metadata record is found - * (should be impossible given the PK constraint on 'key') + * (should be impossible given the composite PK constraint on + * 'ensIndexerRef' and 'key'). */ private async getEnsNodeMetadata( metadata: Pick, From f13b6afd133bce946ab19a55aa163f2c6d9bc2af Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 12 Mar 2026 13:03:02 +0100 Subject: [PATCH 11/12] Remove db write workaround We do not write into ENSIndexer Schema anymore, so no need for extra queries to make writes to work. --- .../src/lib/ensdb-client/ensdb-client.ts | 32 +++++++------------ 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts index e6769a692..d743f3590 100644 --- a/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts +++ b/apps/ensindexer/src/lib/ensdb-client/ensdb-client.ts @@ -179,26 +179,16 @@ export class EnsDbClient implements EnsDbClientQuery, EnsDbClientMutation { private async upsertEnsNodeMetadata< EnsNodeMetadataType extends SerializedEnsNodeMetadata = SerializedEnsNodeMetadata, >(metadata: EnsNodeMetadataType): Promise { - await this.db.transaction(async (tx) => { - // Ponder live-query triggers insert into live_query_tables. - // Because this worker writes outside the Ponder runtime connection pool, - // the temp table must be ensured to exist on this connection. Without this, - // the upsert would fail with "relation 'live_query_tables' does not exist" error. - await tx.execute( - sql`CREATE TEMP TABLE IF NOT EXISTS live_query_tables (table_name TEXT PRIMARY KEY)`, - ); - - await tx - .insert(ensNodeSchema.ensNodeMetadata) - .values({ - ensIndexerRef: this.ensIndexerRef, - key: metadata.key, - value: metadata.value, - }) - .onConflictDoUpdate({ - target: [ensNodeSchema.ensNodeMetadata.ensIndexerRef, ensNodeSchema.ensNodeMetadata.key], - set: { value: metadata.value }, - }); - }); + await this.db + .insert(ensNodeSchema.ensNodeMetadata) + .values({ + ensIndexerRef: this.ensIndexerRef, + key: metadata.key, + value: metadata.value, + }) + .onConflictDoUpdate({ + target: [ensNodeSchema.ensNodeMetadata.ensIndexerRef, ensNodeSchema.ensNodeMetadata.key], + set: { value: metadata.value }, + }); } } From 1791217da8d4f1f83f96584762158844eb307a0e Mon Sep 17 00:00:00 2001 From: Tomasz Kopacki Date: Thu, 12 Mar 2026 13:06:39 +0100 Subject: [PATCH 12/12] Update ENSIndexer Schema references to be explicit --- apps/ensapi/src/lib/db.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/ensapi/src/lib/db.ts b/apps/ensapi/src/lib/db.ts index 71a2f5e78..db88e5e2c 100644 --- a/apps/ensapi/src/lib/db.ts +++ b/apps/ensapi/src/lib/db.ts @@ -1,6 +1,6 @@ import config from "@/config"; -import * as schema from "@ensnode/ensnode-schema"; +import * as ensIndexerSchema from "@ensnode/ensnode-schema/ensindexer"; import { makeReadOnlyDrizzle } from "@/lib/handlers/drizzle"; @@ -10,5 +10,5 @@ import { makeReadOnlyDrizzle } from "@/lib/handlers/drizzle"; export const db = makeReadOnlyDrizzle({ databaseUrl: config.databaseUrl, databaseSchema: config.databaseSchemaName, - schema, + schema: ensIndexerSchema, });