Repository: ponder-sh/ponder
Pull request #1247 (2024-11-15 18:57:34)

fix: more `onchainSchema()`
Fixed more errors with `onchainSchema()` specifically with pglite. Also fixed an issue with `build_id`, causing it to be incorrect because it was including the `instance_id`


Patch:
diff --git a/.changeset/silver-owls-buy.md b/.changeset/silver-owls-buy.md
new file mode 100644
index 00000000..7b4e1a18
--- /dev/null
+++ b/.changeset/silver-owls-buy.md
@@ -0,0 +1,5 @@
+---
+"@ponder/core": patch
+---
+
+Fixed `onchainSchema()` with pglite.
diff --git a/.changeset/swift-oranges-wink.md b/.changeset/swift-oranges-wink.md
new file mode 100644
index 00000000..67013bcc
--- /dev/null
+++ b/.changeset/swift-oranges-wink.md
@@ -0,0 +1,5 @@
+---
+"@ponder/core": patch
+---
+
+Fixed crash recovery, specifically build ID generation.
diff --git a/packages/core/src/build/service.ts b/packages/core/src/build/service.ts
index 9c3e2698..933d4975 100644
--- a/packages/core/src/build/service.ts
+++ b/packages/core/src/build/service.ts
@@ -538,7 +538,8 @@ const executeConfig = async (
 const executeSchema = async (
   buildService: Service,
 ): Promise<
-  { status: "success"; schema: Schema } | { status: "error"; error: Error }
+  | { status: "success"; schema: Schema; contentHash: string }
+  | { status: "error"; error: Error }
 > => {
   const executeResult = await executeFile(buildService, {
     file: buildService.common.options.schemaFile,
@@ -556,7 +557,15 @@ const executeSchema = async (
 
   const schema = executeResult.exports;
 
-  return { status: "success", schema };
+  const contents = fs.readFileSync(
+    buildService.common.options.schemaFile,
+    "utf-8",
+  );
+  return {
+    status: "success",
+    schema,
+    contentHash: createHash("sha256").update(contents).digest("hex"),
+  };
 };
 
 const executeIndexingFunctions = async (
@@ -665,7 +674,7 @@ const executeApiRoutes = async (
 const validateAndBuild = async (
   { common }: Pick<Service, "common">,
   config: { config: Config; contentHash: string },
-  schema: { schema: Schema },
+  schema: { schema: Schema; contentHash: string },
   indexingFunctions: {
     indexingFunctions: RawIndexingFunctions;
     contentHash: string;
@@ -713,11 +722,7 @@ const validateAndBuild = async (
   const buildId = createHash("sha256")
     .update(BUILD_ID_VERSION)
     .update(config.contentHash)
-    .update(
-      createHash("sha256")
-        .update(serialize(buildSchemaResult.statements))
-        .digest("hex"),
-    )
+    .update(schema.contentHash)
     .update(indexingFunctions.contentHash)
     .digest("hex")
     .slice(0, 10);
diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts
index 293bd56c..12888606 100644
--- a/packages/core/src/database/index.ts
+++ b/packages/core/src/database/index.ts
@@ -169,6 +169,7 @@ export const createDatabase = (args: {
             });
           }
         },
+        plugins: [new WithSchemaPlugin(args.namespace)],
       }),
       user: new HeadlessKysely({
         name: "user",
@@ -181,6 +182,7 @@ export const createDatabase = (args: {
             });
           }
         },
+        plugins: [new WithSchemaPlugin(args.namespace)],
       }),
       readonly: new HeadlessKysely({
         name: "readonly",
@@ -193,6 +195,7 @@ export const createDatabase = (args: {
             });
           }
         },
+        plugins: [new WithSchemaPlugin(args.namespace)],
       }),
       sync: new HeadlessKysely<PonderSyncSchema>({
         name: "sync",
@@ -496,11 +499,13 @@ export const createDatabase = (args: {
           // @ts-ignore
           .selectFrom("information_schema.tables")
           // @ts-ignore
-          .select("table_name")
+          .select(["table_name", "table_schema"])
           // @ts-ignore
           .where("table_name", "=", "namespace_lock")
+          // @ts-ignore
+          .where("table_schema", "=", "ponder")
           .executeTakeFirst()
-          .then((table) => table?.table_name === "namespace_lock");
+          .then((table) => table !== undefined);
 
         if (hasNamespaceLockTable) {
           await qb.internal.wrap({ method: "migrate" }, async () => {
@@ -564,11 +569,13 @@ export const createDatabase = (args: {
         // @ts-ignore
         .selectFrom("information_schema.tables")
         // @ts-ignore
-        .select("table_name")
+        .select(["table_name", "table_schema"])
         // @ts-ignore
         .where("table_name", "=", "_ponder_meta")
+        // @ts-ignore
+        .where("table_schema", "=", args.namespace)
         .executeTakeFirst()
-        .then((table) => table?.table_name === "_ponder_meta");
+        .then((table) => table !== undefined);
 
       if (hasPonderMetaTable) {
         await qb.internal.wrap({ method: "migrate" }, () =>
@@ -1157,13 +1164,13 @@ CREATE OR REPLACE FUNCTION ${tableName.triggerFn}
 RETURNS TRIGGER AS $$
 BEGIN
   IF TG_OP = 'INSERT' THEN
-    INSERT INTO "${tableName.reorg}" (${columnNames.join(",")}, operation, checkpoint)
+    INSERT INTO "${args.namespace}"."${tableName.reorg}" (${columnNames.join(",")}, operation, checkpoint)
     VALUES (${columnNames.map((name) => `NEW.${name}`).join(",")}, 0, '${encodeCheckpoint(maxCheckpoint)}');
   ELSIF TG_OP = 'UPDATE' THEN
-    INSERT INTO "${tableName.reorg}" (${columnNames.join(",")}, operation, checkpoint)
+    INSERT INTO "${args.namespace}"."${tableName.reorg}" (${columnNames.join(",")}, operation, checkpoint)
     VALUES (${columnNames.map((name) => `OLD.${name}`).join(",")}, 1, '${encodeCheckpoint(maxCheckpoint)}');
   ELSIF TG_OP = 'DELETE' THEN
-    INSERT INTO "${tableName.reorg}" (${columnNames.join(",")}, operation, checkpoint)
+    INSERT INTO "${args.namespace}"."${tableName.reorg}" (${columnNames.join(",")}, operation, checkpoint)
     VALUES (${columnNames.map((name) => `OLD.${name}`).join(",")}, 2, '${encodeCheckpoint(maxCheckpoint)}');
   END IF;
   RETURN NULL;


Test patch:
diff --git a/packages/core/src/database/index.test.ts b/packages/core/src/database/index.test.ts
index 934a57d0..93c2917b 100644
--- a/packages/core/src/database/index.test.ts
+++ b/packages/core/src/database/index.test.ts
@@ -1,6 +1,11 @@
 import { setupCommon, setupIsolatedDatabase } from "@/_test/setup.js";
 import { buildSchema } from "@/build/schema.js";
-import { onchainEnum, onchainTable, primaryKey } from "@/drizzle/index.js";
+import {
+  onchainEnum,
+  onchainSchema,
+  onchainTable,
+  primaryKey,
+} from "@/drizzle/index.js";
 import { createRealtimeIndexingStore } from "@/indexing-store/realtime.js";
 import {
   encodeCheckpoint,
@@ -107,6 +112,36 @@ test("setup() creates tables", async (context) => {
   await database.kill();
 });
 
+test("setup() with onchainSchema", async (context) => {
+  const schema = onchainSchema("multichain");
+  const account = schema.table("account", (t) => ({
+    address: t.hex().primaryKey(),
+    balance: t.bigint(),
+  }));
+
+  const database = createDatabase({
+    common: context.common,
+    schema: { schema, account },
+    databaseConfig: context.databaseConfig,
+    instanceId: "1234",
+    buildId: "abc",
+    ...buildSchema({
+      schema: { schema, account },
+      instanceId: "1234",
+    }),
+  });
+
+  await database.setup();
+
+  const tableNames = await getUserTableNames(database, "multichain");
+  expect(tableNames).toContain("1234__account");
+  expect(tableNames).toContain("1234_reorg__account");
+  expect(tableNames).toContain("_ponder_meta");
+
+  await database.unlock();
+  await database.kill();
+});
+
 test("setup() succeeds with a prior app in the same namespace", async (context) => {
   const database = createDatabase({
     common: context.common,