Skip to content

Commit 37cb113

Browse files
NiallJoeMaherclaude
andcommitted
fix(db): stop /feed.xml crashing on "cannot infer relation posts.tags"
The schema barrel re-exports four tables under camelCase aliases for app ergonomics (post_votes as postVotes, etc.). Passing the whole module namespace to drizzle put two keys per table into its tableNamesMap; the collision made drizzle attach each join table's relations to the alias key and leave the canonical key with none. Relational queries that walk a many-relation on those tables (with: { tags }, with: { votes }) then threw "There is not enough information to infer relation posts.tags" — which is exactly what /feed.xml does, so the RSS feed 500'd. Strip the aliases from the schema object handed to drizzle so each table is registered once; app code still imports the aliases from the barrel unchanged. Adds a regression test that builds the /feed.xml query. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent dd621f2 commit 37cb113

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

server/db/index.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,20 @@ import { type Logger } from "drizzle-orm/logger";
44
import postgres from "postgres";
55

66
import { env } from "@/config/env";
7-
import * as schema from "@/server/db/schema";
7+
import * as schemaExports from "@/server/db/schema";
8+
9+
// The schema barrel re-exports four tables under camelCase aliases for app-code
10+
// ergonomics (`post_votes as postVotes`, etc.). Those alias keys point at the
11+
// SAME table object as their snake_case originals, so handing the whole module
12+
// namespace to drizzle puts two keys per table into its tableNamesMap. The
13+
// collision makes drizzle attach each join table's relations to the alias key
14+
// and leave the canonical key with none, silently breaking relational queries
15+
// like `with: { tags }` / `with: { votes }` ("not enough information to infer
16+
// relation posts.tags"). Strip the aliases so each table reaches drizzle once;
17+
// app code still imports them from the schema barrel unchanged.
18+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
19+
const { postVotes, commentVotes, postTags, feedSources, ...schema } =
20+
schemaExports;
821

922
/**
1023
* Cache the database connection in development. This avoids creating a new connection on every HMR

server/db/relations.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { describe, it, expect, beforeAll } from "vitest";
2+
3+
// Regression guard for the tableNamesMap collision: the schema barrel re-exports
4+
// a few tables under camelCase aliases (postVotes/postTags/...), and feeding both
5+
// keys to drizzle dropped the join tables' relations — which broke `with: { tags }`
6+
// / `with: { votes }` on /feed.xml ("not enough information to infer relation
7+
// posts.tags"). server/db/index.ts strips the aliases before handing the schema
8+
// to drizzle; these tests fail if that regresses.
9+
describe("posts relational config", () => {
10+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11+
let db: any;
12+
13+
beforeAll(async () => {
14+
process.env.SKIP_ENV_VALIDATION = "1";
15+
process.env.DATABASE_URL ??= "postgres://user:pass@localhost:5432/test";
16+
// Dynamic import so the env stubs above land before the module reads them.
17+
({ db } = await import("@/server/db"));
18+
});
19+
20+
it("registers relations on the join tables (no alias collision)", () => {
21+
const schema = db._.schema;
22+
expect(Object.keys(schema.post_tags.relations)).toContain("post");
23+
expect(Object.keys(schema.post_tags.relations)).toContain("tag");
24+
expect(Object.keys(schema.post_votes.relations)).toContain("post");
25+
});
26+
27+
it("builds the nested relational query used by /feed.xml", () => {
28+
expect(() =>
29+
db.query.posts
30+
.findMany({
31+
columns: { title: true },
32+
with: {
33+
author: { columns: { username: true } },
34+
tags: { with: { tag: true } },
35+
},
36+
limit: 1,
37+
})
38+
.toSQL(),
39+
).not.toThrow();
40+
});
41+
});

0 commit comments

Comments
 (0)