Repository: ponder-sh/ponder (ts)

Issue:
[bug] stale tables don't get cleaned up
Ponder Version: 0.7.8
Database: Postgres

When I quit my Ponder indexer via `cmd + c`, the `_ponder_meta` app/instance id remains locked

Manually updating `is_locked` to 0 for the old builds do clean up the stale tables though

Pull request description:
fix: cleanup old tables
Closes #1267 

Patch:
diff --git a/.changeset/clean-scissors-deliver.md b/.changeset/clean-scissors-deliver.md
new file mode 100644
index 00000000..a6b52808
--- /dev/null
+++ b/.changeset/clean-scissors-deliver.md
@@ -0,0 +1,5 @@
+---
+"@ponder/core": patch
+---
+
+Fixed a bug where `one` relations were sometimes incorrectly marked as non-null in the GraphQL schema, which caused "Cannot return null for non-nullable field" errors when the related entity was not found.
diff --git a/.changeset/gentle-donkeys-hammer.md b/.changeset/gentle-donkeys-hammer.md
new file mode 100644
index 00000000..1faeb1e2
--- /dev/null
+++ b/.changeset/gentle-donkeys-hammer.md
@@ -0,0 +1,5 @@
+---
+"@ponder/core": patch
+---
+
+Fixed tables not being cleaned up.
diff --git a/examples/reference-erc20/ponder.schema.ts b/examples/reference-erc20/ponder.schema.ts
index 9ce5a110..10013b06 100644
--- a/examples/reference-erc20/ponder.schema.ts
+++ b/examples/reference-erc20/ponder.schema.ts
@@ -1,4 +1,4 @@
-import { index, onchainTable, primaryKey } from "@ponder/core";
+import { index, onchainTable, primaryKey, relations } from "@ponder/core";
 
 export const account = onchainTable("account", (t) => ({
   address: t.hex().primaryKey(),
@@ -32,6 +32,13 @@ export const transferEvent = onchainTable(
   }),
 );
 
+export const transferEventRelations = relations(transferEvent, ({ one }) => ({
+  fromAccount: one(account, {
+    fields: [transferEvent.from],
+    references: [account.address],
+  }),
+}));
+
 export const approvalEvent = onchainTable("approval_event", (t) => ({
   id: t.text().primaryKey(),
   amount: t.bigint().notNull(),
diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts
index d895e87f..27b35716 100644
--- a/packages/core/src/database/index.ts
+++ b/packages/core/src/database/index.ts
@@ -1009,18 +1009,28 @@ export const createDatabase = (args: {
           .execute()
           .then((rows) => rows.map(({ value }) => value as PonderApp));
 
-        const removedApps = apps
-          .filter((app) =>
-            app.is_dev === 1
-              ? app.is_locked === 0
-              : app.is_locked === 0 ||
+        const removedDevApps = apps.filter(
+          (app) =>
+            app.is_dev === 1 &&
+            (app.is_locked === 0 ||
+              app.heartbeat_at + args.common.options.databaseHeartbeatTimeout <
+                Date.now()),
+        );
+
+        const removedStartApps = apps
+          .filter(
+            (app) =>
+              app.is_dev === 0 &&
+              (app.is_locked === 0 ||
                 app.heartbeat_at +
                   args.common.options.databaseHeartbeatTimeout <
-                  Date.now(),
+                  Date.now()),
           )
           .sort((a, b) => (a.heartbeat_at > b.heartbeat_at ? -1 : 1))
           .slice(2);
 
+        const removedApps = [...removedDevApps, ...removedStartApps];
+
         for (const app of removedApps) {
           for (const table of app.table_names) {
             await qb.internal.schema
diff --git a/packages/core/src/graphql/index.ts b/packages/core/src/graphql/index.ts
index ae425a40..fe5bc456 100644
--- a/packages/core/src/graphql/index.ts
+++ b/packages/core/src/graphql/index.ts
@@ -228,10 +228,10 @@ export function buildGraphQLSchema(schema: Schema): GraphQLSchema {
             }
 
             fieldConfigMap[relationName] = {
-              // Note: This naming is backwards (Drizzle bug).
-              type: relation.isNullable
-                ? new GraphQLNonNull(referencedEntityType)
-                : referencedEntityType,
+              // Note: There is a `relation.isNullable` field here but it appears
+              // to be internal / incorrect. Until we have support for foriegn
+              // key constraints, all `one` relations must be nullable.
+              type: referencedEntityType,
               resolve: (parent, _args, context) => {
                 const loader = context.getDataLoader({
                   table: referencedTable,


Test patch:
diff --git a/packages/core/src/graphql/index.test.ts b/packages/core/src/graphql/index.test.ts
index 31cea81c..f26ff019 100644
--- a/packages/core/src/graphql/index.test.ts
+++ b/packages/core/src/graphql/index.test.ts
@@ -417,14 +417,16 @@ test("singular with one relation", async (context) => {
 
   const pet = onchainTable("pet", (t) => ({
     id: t.text().primaryKey(),
-    ownerId: t.text("ownooor_id"),
-    ownerIdNotNull: t.text().notNull(),
+    ownerId: t.text().notNull(),
+    ownerIdNullable: t.text(),
   }));
 
+  // Note that regardless of whether the `fields` column(s) are nullable,
+  // the field type corresponding to the `one` relation must be nullable.
   const petRelations = relations(pet, ({ one }) => ({
     owner: one(person, { fields: [pet.ownerId], references: [person.id] }),
-    ownerNotNull: one(person, {
-      fields: [pet.ownerIdNotNull],
+    ownerNullable: one(person, {
+      fields: [pet.ownerIdNullable],
       references: [person.id],
     }),
   }));
@@ -443,7 +445,7 @@ test("singular with one relation", async (context) => {
   ]);
   await indexingStore
     .insert(schema.pet)
-    .values({ id: "dog1", ownerIdNotNull: "jake" });
+    .values({ id: "dog1", ownerId: "jake" });
 
   const graphqlSchema = buildGraphQLSchema(schema);
 
@@ -452,10 +454,10 @@ test("singular with one relation", async (context) => {
       pet(id: "dog1") {
         owner {
           id
+          name
         }
-        ownerNotNull {
+        ownerNullable {
           id
-          name
         }
       }
     }
@@ -464,11 +466,11 @@ test("singular with one relation", async (context) => {
   expect(result.errors?.[0]?.message).toBeUndefined();
   expect(result.data).toMatchObject({
     pet: {
-      owner: null,
-      ownerNotNull: {
+      owner: {
         id: "jake",
         name: "jake",
       },
+      ownerNullable: null,
     },
   });