Repository: ponder-sh/ponder
Pull request #1583 (2025-03-05 22:05:57)

fix: typos in documentation files
This pull request contains changes to improve clarity, correctness and structure.


Patch:
diff --git a/docs/pages/0_6/docs/query/api-functions.mdx b/docs/pages/0_6/docs/query/api-functions.mdx
index 6131bab3..e9f197d7 100644
--- a/docs/pages/0_6/docs/query/api-functions.mdx
+++ b/docs/pages/0_6/docs/query/api-functions.mdx
@@ -173,7 +173,7 @@ ponder.get("/account/:address", async (c) => {
   const account = await c.db
     .select()
     .from(Account)
-    .where(eq(Accout.address, address))
+    .where(eq(Account.address, address))
     .first();
 
   if (account) {
diff --git a/docs/pages/0_6/docs/utilities/replace-bigints.mdx b/docs/pages/0_6/docs/utilities/replace-bigints.mdx
index 7c200936..34191a7e 100644
--- a/docs/pages/0_6/docs/utilities/replace-bigints.mdx
+++ b/docs/pages/0_6/docs/utilities/replace-bigints.mdx
@@ -14,7 +14,7 @@ import { Callout } from "nextra/components";
 
 Replaces all `BigInt{:ts}` values in an object using the provided [replacer](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#replacer) function. The target can be a scalar, array, or object.
 
-The `ReplaceBigInt{:ts}` generic type implements the same funtionality at the type level.
+The `ReplaceBigInt{:ts}` generic type implements the same functionality at the type level.
 
 ## Usage
 
diff --git a/docs/pages/docs/indexing/read-contract-data.mdx b/docs/pages/docs/indexing/read-contract-data.mdx
index 7e060320..b70d9ff3 100644
--- a/docs/pages/docs/indexing/read-contract-data.mdx
+++ b/docs/pages/docs/indexing/read-contract-data.mdx
@@ -160,7 +160,7 @@ ponder.on("Blitmap:Mint", async ({ event, context }) => {
     abi: context.contracts.Blitmap.abi,
     address: context.contracts.Blitmap.address,
     functionName: "totalSupply",
-    // This is set automatically, no need to include it youself.
+    // This is set automatically, no need to include it yourself.
     // blockNumber: event.block.number,
   });
 });
diff --git a/packages/client/CHANGELOG.md b/packages/client/CHANGELOG.md
index 5bcb8ad6..8effabac 100644
--- a/packages/client/CHANGELOG.md
+++ b/packages/client/CHANGELOG.md
@@ -1,5 +1,7 @@
 # @ponder/client
 
+## 0.9.27
+
 ## 0.9.26
 
 ## 0.9.25
diff --git a/packages/client/package.json b/packages/client/package.json
index 79635f6f..e4e8d332 100644
--- a/packages/client/package.json
+++ b/packages/client/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ponder/client",
-  "version": "0.9.26",
+  "version": "0.9.27",
   "description": "",
   "license": "MIT",
   "type": "module",
diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md
index 1f68ec79..0e0f60f6 100644
--- a/packages/core/CHANGELOG.md
+++ b/packages/core/CHANGELOG.md
@@ -1,5 +1,11 @@
 # ponder
 
+## 0.9.27
+
+### Patch Changes
+
+- [#1582](https://github.com/ponder-sh/ponder/pull/1582) [`8dde38dfbb81777f1ab22fe65ea1166cce1b944f`](https://github.com/ponder-sh/ponder/commit/8dde38dfbb81777f1ab22fe65ea1166cce1b944f) Thanks [@kyscott18](https://github.com/kyscott18)! - Fixed a bug that would cause crash recovery and reorg handling to error for some apps.
+
 ## 0.9.26
 
 ### Patch Changes
diff --git a/packages/core/package.json b/packages/core/package.json
index d331b317..f8a80b28 100644
--- a/packages/core/package.json
+++ b/packages/core/package.json
@@ -1,6 +1,6 @@
 {
   "name": "ponder",
-  "version": "0.9.26",
+  "version": "0.9.27",
   "description": "An open-source framework for crypto application backends",
   "license": "MIT",
   "type": "module",
diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts
index f53bdc7a..23d119d9 100644
--- a/packages/core/src/database/index.ts
+++ b/packages/core/src/database/index.ts
@@ -30,7 +30,6 @@ import { wait } from "@/utils/wait.js";
 import type { PGlite } from "@electric-sql/pglite";
 import {
   type TableConfig,
-  and,
   eq,
   getTableColumns,
   getTableName,
@@ -1289,77 +1288,57 @@ FOR EACH ROW EXECUTE FUNCTION "${namespace}".${getTableNames(table).triggerFn};
       });
     },
     async revert({ checkpoint, tx }) {
-      await this.wrap({ method: "revert", includeTraceLogs: true }, () =>
+      await this.record({ method: "revert", includeTraceLogs: true }, () =>
         Promise.all(
           tables.map(async (table) => {
             const primaryKeyColumns = getPrimaryKeyColumns(table);
 
-            // @ts-ignore
-            const { rows } = await tx.execute<Schema>(
-              sql.raw(
-                `DELETE FROM "${namespace}"."${getTableName(getReorgTable(table))}" WHERE checkpoint > '${checkpoint}' RETURNING *`,
-              ),
-            );
-
-            const reversed = rows.sort(
-              // @ts-ignore
-              (a, b) => b.operation_id - a.operation_id,
+            const result = await tx.execute(
+              sql.raw(`
+WITH reverted1 AS (
+  DELETE FROM "${namespace}"."${getTableName(getReorgTable(table))}"
+  WHERE checkpoint > '${checkpoint}' RETURNING *
+), reverted2 AS (
+  SELECT ${primaryKeyColumns.map(({ sql }) => `"${sql}"`).join(", ")}, MIN(operation_id) AS operation_id FROM reverted1
+  GROUP BY ${primaryKeyColumns.map(({ sql }) => `"${sql}"`).join(", ")}
+), reverted3 AS (
+  SELECT ${Object.values(getTableColumns(table))
+    .map((column) => `reverted1."${getColumnCasing(column, "snake_case")}"`)
+    .join(", ")}, reverted1.operation FROM reverted2
+  INNER JOIN reverted1
+  ON ${primaryKeyColumns.map(({ sql }) => `reverted2."${sql}" = reverted1."${sql}"`).join("AND ")}
+  AND reverted2.operation_id = reverted1.operation_id
+), inserted AS (
+  DELETE FROM "${namespace}"."${getTableName(table)}" as t
+  WHERE EXISTS (
+    SELECT * FROM reverted3
+    WHERE ${primaryKeyColumns.map(({ sql }) => `t."${sql}" = reverted3."${sql}"`).join("AND ")}
+    AND OPERATION = 0
+  )
+  RETURNING *
+), updated_or_deleted AS (
+  INSERT INTO  "${namespace}"."${getTableName(table)}"
+  SELECT ${Object.values(getTableColumns(table))
+    .map((column) => `"${getColumnCasing(column, "snake_case")}"`)
+    .join(", ")} FROM reverted3
+  WHERE operation = 1 OR operation = 2
+  ON CONFLICT (${primaryKeyColumns.map(({ sql }) => `"${sql}"`).join(", ")})
+  DO UPDATE SET
+    ${Object.values(getTableColumns(table))
+      .map(
+        (column) =>
+          `"${getColumnCasing(column, "snake_case")}" = EXCLUDED."${getColumnCasing(column, "snake_case")}"`,
+      )
+      .join(", ")}
+  RETURNING *
+) SELECT COUNT(*) FROM reverted1 as count;
+`),
             );
 
-            // undo operation
-            for (const log of reversed) {
-              if (log.operation === 0) {
-                // create
-
-                await tx.delete(table).where(
-                  and(
-                    ...primaryKeyColumns.map(({ js, sql }) =>
-                      // @ts-ignore
-                      eq(table[js]!, log[sql]),
-                    ),
-                  ),
-                );
-              } else if (log.operation === 1) {
-                // update
-
-                // @ts-ignore
-                log.operation_id = undefined;
-                // @ts-ignore
-                log.checkpoint = undefined;
-                // @ts-ignore
-                log.operation = undefined;
-                await tx
-                  .update(table)
-                  .set(log)
-                  .where(
-                    and(
-                      ...primaryKeyColumns.map(({ js, sql }) =>
-                        // @ts-ignore
-                        eq(table[js]!, log[sql]),
-                      ),
-                    ),
-                  );
-              } else {
-                // delete
-
-                // @ts-ignore
-                log.operation_id = undefined;
-                // @ts-ignore
-                log.checkpoint = undefined;
-                // @ts-ignore
-                log.operation = undefined;
-                await tx
-                  .insert(table)
-                  .values(log)
-                  .onConflictDoNothing({
-                    target: primaryKeyColumns.map(({ js }) => table[js]!),
-                  });
-              }
-            }
-
             common.logger.info({
               service: "database",
-              msg: `Reverted ${rows.length} unfinalized operations from '${getTableName(table)}'`,
+              // @ts-ignore
+              msg: `Reverted ${result.rows[0]!.count} unfinalized operations from '${getTableName(table)}'`,
             });
           }),
         ),
diff --git a/packages/create-ponder/CHANGELOG.md b/packages/create-ponder/CHANGELOG.md
index ed52ab65..6d9c8e23 100644
--- a/packages/create-ponder/CHANGELOG.md
+++ b/packages/create-ponder/CHANGELOG.md
@@ -1,5 +1,7 @@
 # create-ponder
 
+## 0.9.27
+
 ## 0.9.26
 
 ## 0.9.25
diff --git a/packages/create-ponder/package.json b/packages/create-ponder/package.json
index a161cdaf..1c2a4436 100644
--- a/packages/create-ponder/package.json
+++ b/packages/create-ponder/package.json
@@ -1,6 +1,6 @@
 {
   "name": "create-ponder",
-  "version": "0.9.26",
+  "version": "0.9.27",
   "type": "module",
   "description": "A CLI tool to create Ponder apps",
   "license": "MIT",
diff --git a/packages/eslint-config-ponder/CHANGELOG.md b/packages/eslint-config-ponder/CHANGELOG.md
index 34bbf951..29a4c4d9 100644
--- a/packages/eslint-config-ponder/CHANGELOG.md
+++ b/packages/eslint-config-ponder/CHANGELOG.md
@@ -1,5 +1,7 @@
 # eslint-config-ponder
 
+## 0.9.27
+
 ## 0.9.26
 
 ## 0.9.25
diff --git a/packages/eslint-config-ponder/package.json b/packages/eslint-config-ponder/package.json
index f6ed95b4..9ea2a55b 100644
--- a/packages/eslint-config-ponder/package.json
+++ b/packages/eslint-config-ponder/package.json
@@ -1,6 +1,6 @@
 {
   "name": "eslint-config-ponder",
-  "version": "0.9.26",
+  "version": "0.9.27",
   "description": "ESLint config for Ponder apps",
   "license": "MIT",
   "main": "./index.js",
diff --git a/packages/react/CHANGELOG.md b/packages/react/CHANGELOG.md
index 498c91bd..0c2e912e 100644
--- a/packages/react/CHANGELOG.md
+++ b/packages/react/CHANGELOG.md
@@ -1,5 +1,12 @@
 # @ponder/react
 
+## 0.9.27
+
+### Patch Changes
+
+- Updated dependencies []:
+  - @ponder/client@0.9.27
+
 ## 0.9.26
 
 ### Patch Changes
diff --git a/packages/react/package.json b/packages/react/package.json
index fbfa9a61..b7f5142c 100644
--- a/packages/react/package.json
+++ b/packages/react/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@ponder/react",
-  "version": "0.9.26",
+  "version": "0.9.27",
   "description": "React hooks for Ponder",
   "license": "MIT",
   "repository": {
@@ -37,7 +37,7 @@
     "superjson": "^2.2.2"
   },
   "peerDependencies": {
-    "@ponder/client": ">=0.9.26",
+    "@ponder/client": ">=0.9.27",
     "@tanstack/react-query": ">=5.0.0",
     "react": ">=18",
     "typescript": ">=5.0.4"


Test patch:
diff --git a/packages/core/src/database/index.test.ts b/packages/core/src/database/index.test.ts
index c65e4b7b..2a6795ff 100644
--- a/packages/core/src/database/index.test.ts
+++ b/packages/core/src/database/index.test.ts
@@ -719,6 +719,13 @@ test("revert()", async (context) => {
     .insert(account)
     .values({ address: "0x0000000000000000000000000000000000000001" });
 
+  await database.complete({
+    checkpoint: createCheckpoint({ chainId: 1n, blockNumber: 10n }),
+    db: database.qb.drizzle,
+  });
+
+  await indexingStore.delete(account, { address: zeroAddress });
+
   await database.complete({
     checkpoint: createCheckpoint({ chainId: 1n, blockNumber: 11n }),
     db: database.qb.drizzle,
@@ -726,7 +733,7 @@ test("revert()", async (context) => {
 
   await database.qb.drizzle.transaction(async (tx) => {
     await database.revert({
-      checkpoint: createCheckpoint({ chainId: 1n, blockNumber: 10n }),
+      checkpoint: createCheckpoint({ chainId: 1n, blockNumber: 9n }),
       tx,
     });
   });
@@ -739,6 +746,72 @@ test("revert()", async (context) => {
   await context.common.shutdown.kill();
 });
 
+test("revert() with composite primary key", async (context) => {
+  const test = onchainTable(
+    "Test",
+    (p) => ({
+      a: p.integer("A").notNull(),
+      b: p.integer("B").notNull(),
+      c: p.integer("C"),
+    }),
+    (table) => ({
+      pk: primaryKey({ columns: [table.a, table.b] }),
+    }),
+  );
+
+  const database = await createDatabase({
+    common: context.common,
+    namespace: "public",
+    preBuild: {
+      databaseConfig: context.databaseConfig,
+    },
+    schemaBuild: {
+      schema: { test },
+      statements: buildSchema({ schema: { test } }).statements,
+    },
+  });
+
+  await database.migrate({ buildId: "abc" });
+
+  // setup tables, reorg tables, and metadata checkpoint
+
+  await database.createTriggers();
+
+  const indexingStore = createRealtimeIndexingStore({
+    common: context.common,
+    schemaBuild: { schema: { test } },
+    database,
+  });
+
+  await indexingStore.insert(test).values({ a: 1, b: 1 });
+
+  await database.complete({
+    checkpoint: createCheckpoint({ chainId: 1n, blockNumber: 11n }),
+    db: database.qb.drizzle,
+  });
+
+  await indexingStore.update(test, { a: 1, b: 1 }).set({ c: 1 });
+
+  await database.complete({
+    checkpoint: createCheckpoint({ chainId: 1n, blockNumber: 12n }),
+    db: database.qb.drizzle,
+  });
+
+  await database.qb.drizzle.transaction(async (tx) => {
+    await database.revert({
+      checkpoint: createCheckpoint({ chainId: 1n, blockNumber: 11n }),
+      tx,
+    });
+  });
+
+  const rows = await database.qb.drizzle.select().from(test);
+
+  expect(rows).toHaveLength(1);
+  expect(rows[0]).toStrictEqual({ a: 1, b: 1, c: null });
+
+  await context.common.shutdown.kill();
+});
+
 test("getStatus() empty", async (context) => {
   const database = await createDatabase({
     common: context.common,