Repository: ponder-sh/ponder
Pull request #1517 (2025-02-13 17:07:33)

fix: graphql hex value case sensitivity for arguments to singular fields


Patch:
diff --git a/.changeset/flat-fans-compare.md b/.changeset/flat-fans-compare.md
new file mode 100644
index 00000000..84682be0
--- /dev/null
+++ b/.changeset/flat-fans-compare.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Fixed a bug where hex value arguments to singular query fields in GraphQL did not respect case-insensitive comparison.
diff --git a/packages/core/src/graphql/index.ts b/packages/core/src/graphql/index.ts
index 0482f846..5bc0d14f 100644
--- a/packages/core/src/graphql/index.ts
+++ b/packages/core/src/graphql/index.ts
@@ -1,5 +1,6 @@
 import type { Database } from "@/database/index.js";
 import type { OnchainTable } from "@/drizzle/onchain.js";
+import { normalizeColumn } from "@/indexing-store/cache.js";
 import type { Schema } from "@/internal/types.js";
 import type { Drizzle, ReadonlyDrizzle } from "@/types/db.js";
 import { never } from "@/utils/never.js";
@@ -743,7 +744,7 @@ const conditionSuffixesByLengthDesc = Object.values(conditionSuffixes)
 function buildWhereConditions(
   where: Record<string, any> | undefined,
   columns: Record<string, Column>,
-): (SQL | undefined)[] {
+) {
   const conditions: (SQL | undefined)[] = [];
 
   if (where === undefined) return conditions;
@@ -991,13 +992,37 @@ export function buildDataLoaderCache({
             limit: encodedIds.length,
           });
 
-          return decodedRowFragments.map((decodedRowFragment) => {
-            return rows.find((row) =>
-              Object.entries(decodedRowFragment).every(
-                ([col, val]) => row[col] === val,
-              ),
-            );
-          });
+          // Now, we need to order the rows coming out of the database to match
+          // the order of the IDs passed in. To accomplish this, we need to do
+          // a comparison of the decoded row PK fragments with the database rows.
+          // This is tricky because the decoded row PK fragments are not normalized,
+          // so some comparisons will fail (eg for our PgHex column type).
+          // To fix this, we need to normalize the values before doing the comparison.
+          return (
+            decodedRowFragments
+              // Normalize the decoded row fragments
+              .map((fragment) =>
+                Object.fromEntries(
+                  Object.entries(fragment).map(([col, val]) => {
+                    const column = table.columns[col];
+                    if (column === undefined) {
+                      throw new Error(
+                        `Unknown column '${table.tsName}.${col}' used in dataloader row ID fragment`,
+                      );
+                    }
+                    return [col, normalizeColumn(column, val, false)];
+                  }),
+                ),
+              )
+              // Find the database row corresponding to each normalized row fragment
+              .map((fragment) =>
+                rows.find((row) =>
+                  Object.entries(fragment).every(
+                    ([col, val]) => row[col] === val,
+                  ),
+                ),
+              )
+          );
         },
         { maxBatchSize: 1_000 },
       );


Test patch:
diff --git a/packages/core/src/graphql/index.test.ts b/packages/core/src/graphql/index.test.ts
index 8d3e48e4..4b798e70 100644
--- a/packages/core/src/graphql/index.test.ts
+++ b/packages/core/src/graphql/index.test.ts
@@ -2220,3 +2220,54 @@ test("snake case table and column names with where clause", async (context) => {
     },
   });
 });
+
+test("singular with hex primary key uses case insensitive where", async (context) => {
+  const account = onchainTable("account", (t) => ({
+    address: t.hex().primaryKey(),
+  }));
+
+  const schema = { account };
+
+  const { database, indexingStore } = await setupDatabaseServices(context, {
+    schemaBuild: { schema },
+  });
+  const contextValue = buildContextValue(database);
+  const query = (source: string) =>
+    execute({ schema: graphqlSchema, contextValue, document: parse(source) });
+
+  const CHECKSUM_ADDRESS = "0x67BD7c89B54Fa52826186A57363A9303DB3E7626";
+  const LOWERCASE_ADDRESS = "0x67bd7c89b54fa52826186a57363a9303db3e7626";
+
+  await indexingStore
+    .insert(schema.account)
+    .values({ address: CHECKSUM_ADDRESS });
+
+  const graphqlSchema = buildGraphQLSchema({ schema });
+
+  const result = await query(`
+    query {
+      account(address: "${CHECKSUM_ADDRESS}") {
+        address
+      }
+      accounts(where: { address: "${CHECKSUM_ADDRESS}" }) {
+        items {
+          address
+        }
+      }
+    }
+  `);
+
+  expect(result.errors?.[0]?.message).toBeUndefined();
+  expect(result.data).toMatchObject({
+    account: {
+      address: LOWERCASE_ADDRESS,
+    },
+    accounts: {
+      items: [
+        {
+          address: LOWERCASE_ADDRESS,
+        },
+      ],
+    },
+  });
+});