Repository: ponder-sh/ponder (ts)

Issue:
GraphQL API filtering for nullable fields doesn't work
### Version

0.9.18

### Current behavior

using `null` as a rawValue in any filter doesn't work because the constructed drizzle query should be using `isNull` and `isNotNull` rather than `eq(column, null)`.

in particular for these two cases

https://github.com/ponder-sh/ponder/blob/main/packages/core/src/graphql/index.ts#L810

should be

```ts
          if (rawValue === null) {
            conditions.push(isNull(column));
          } else {
            conditions.push(eq(column, rawValue));
          }
```

https://github.com/ponder-sh/ponder/blob/main/packages/core/src/graphql/index.ts#L824

should be 

```ts
          if (rawValue === null) {
            conditions.push(isNotNull(column));
          } else {
            conditions.push(ne(column, rawValue));
          }
```

### Expected behavior

_No response_

### Steps to reproduce

_No response_

### Link to repository

_No response_

### Anything else?

_No response_

Pull request description:
fix: graphql null equality
Fixes #1545 

Patch:
diff --git a/.changeset/slow-ways-walk.md b/.changeset/slow-ways-walk.md
new file mode 100644
index 00000000..fe9110e6
--- /dev/null
+++ b/.changeset/slow-ways-walk.md
@@ -0,0 +1,5 @@
+---
+"ponder": patch
+---
+
+Fixed a bug where GraphQL queries filtering on `null` values did not work.
diff --git a/packages/core/src/graphql/index.ts b/packages/core/src/graphql/index.ts
index 5bc0d14f..314f43fe 100644
--- a/packages/core/src/graphql/index.ts
+++ b/packages/core/src/graphql/index.ts
@@ -26,6 +26,8 @@ import {
   gte,
   inArray,
   is,
+  isNotNull,
+  isNull,
   like,
   lt,
   lte,
@@ -807,7 +809,11 @@ function buildWhereConditions(
             ),
           );
         } else {
-          conditions.push(eq(column, rawValue));
+          if (rawValue === null) {
+            conditions.push(isNull(column));
+          } else {
+            conditions.push(eq(column, rawValue));
+          }
         }
         break;
       case "_not":
@@ -821,7 +827,11 @@ function buildWhereConditions(
             ),
           );
         } else {
-          conditions.push(ne(column, rawValue));
+          if (rawValue === null) {
+            conditions.push(isNotNull(column));
+          } else {
+            conditions.push(ne(column, rawValue));
+          }
         }
         break;
       case "_in":


Test patch:
diff --git a/packages/core/src/graphql/index.test.ts b/packages/core/src/graphql/index.test.ts
index 4b798e70..038e772c 100644
--- a/packages/core/src/graphql/index.test.ts
+++ b/packages/core/src/graphql/index.test.ts
@@ -969,6 +969,64 @@ test("filter universal", async (context) => {
   });
 });
 
+test("filter null equality", async (context) => {
+  const person = onchainTable("person", (t) => ({
+    id: t.bigint().primaryKey(),
+    nullable: t.text(),
+  }));
+  const schema = { person };
+
+  const { database, indexingStore } = await setupDatabaseServices(context, {
+    schemaBuild: { schema },
+  });
+  const contextValue = buildContextValue(database);
+  const query = (source: string) =>
+    execute({ schema: graphqlSchema, contextValue, document: parse(source) });
+
+  await indexingStore
+    .insert(schema.person)
+    .values([{ id: 1n, nullable: "a" }, { id: 2n, nullable: "b" }, { id: 3n }]);
+
+  const graphqlSchema = buildGraphQLSchema({ schema });
+
+  let result = await query(`
+    query {
+      persons(where: { nullable: null }) {
+        items {
+          id
+          nullable
+        }
+      }
+    }
+  `);
+
+  expect(result.errors?.[0]?.message).toBeUndefined();
+  expect(result.data).toMatchObject({
+    persons: { items: [{ id: "3", nullable: null }] },
+  });
+
+  result = await query(`
+    query {
+      persons(where: { nullable_not: null }) {
+        items {
+          id
+          nullable
+        }
+      }
+    }
+  `);
+
+  expect(result.errors?.[0]?.message).toBeUndefined();
+  expect(result.data).toMatchObject({
+    persons: {
+      items: [
+        { id: "1", nullable: "a" },
+        { id: "2", nullable: "b" },
+      ],
+    },
+  });
+});
+
 test("filter singular", async (context) => {
   const person = onchainTable("person", (t) => ({
     id: t.hex().primaryKey(),