Skip to content

Commit eff6fa4

Browse files
RealDiligentRealDiligent
andauthored
fix(selfhost): make PgStatement.first() coalesce to null like the D1 adapter (#8411)
PgStatement.first(colName) returned row[colName] uncoalesced, so an absent column (or a driver representing SQL NULL as undefined) leaked undefined through the documented Promise<T | null> contract, diverging from d1-adapter.ts's first() -- the reference implementation callers rely on when treating the two backends interchangeably via backend-contracts.ts. Applies the same ?? null coalesce the D1 sibling already uses. Adds the NULL-value and absent-column parity tests the pg suite was missing (the D1 suite has had the equivalent NULL test all along). Closes #8361 Co-authored-by: RealDiligent <nft.gold.eth@gmail.com>
1 parent cad1763 commit eff6fa4

2 files changed

Lines changed: 18 additions & 1 deletion

File tree

src/selfhost/pg-adapter.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,10 @@ class PgStatement implements SelfHostD1PreparedStatement {
3939
const { rows } = await this.exec();
4040
const row = rows[0];
4141
if (!row) return null;
42-
return (colName ? row[colName] : row) as T;
42+
// Coalesce to null so a SQL NULL (or an absent key) never leaks `undefined` through the documented
43+
// Promise<T | null> contract -- matches d1-adapter.ts's first(), the reference implementation callers
44+
// rely on when treating the two backends interchangeably (backend-contracts.ts).
45+
return ((colName != null ? row[colName] : row) ?? null) as T | null;
4346
}
4447

4548
async run(): Promise<{ success: true; meta: Record<string, unknown> }> {

test/unit/selfhost-pg-adapter.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,20 @@ describe("createPgAdapter (#977 self-host D1-over-Postgres)", () => {
4646
expect(await db.prepare("SELECT name FROM t").first("name")).toBe("a");
4747
});
4848

49+
it("first(colName) returns null when the row exists but the column value is NULL (#8361 D1 parity)", async () => {
50+
// pg represents a SQL NULL as null; the adapter must surface null, never undefined, per its
51+
// documented Promise<T | null> contract -- the same guarantee d1-adapter.ts's first() already makes.
52+
const db = createPgAdapter(makeMockPool([{ name: null }]));
53+
expect(await db.prepare("SELECT name FROM t").first("name")).toBeNull();
54+
});
55+
56+
it("first(colName) returns null when the column is absent from the row entirely (#8361 D1 parity)", async () => {
57+
// A driver/query shape that omits the key would previously return `undefined` cast as T, breaking the
58+
// contract for callers that treat the Postgres and D1 backends interchangeably.
59+
const db = createPgAdapter(makeMockPool([{ other: "a" }]));
60+
expect(await db.prepare("SELECT name FROM t").first("name")).toBeNull();
61+
});
62+
4963
it("run() reports success:true and a meta object carrying the row count", async () => {
5064
const db = createPgAdapter(makeMockPool([]));
5165
const result = await db.prepare("INSERT INTO t (name) VALUES (?)").bind("x").run();

0 commit comments

Comments
 (0)