Context
src/selfhost/pg-adapter.ts and src/selfhost/d1-adapter.ts both implement a Statement.first(colName)
method with the same documented contract: Promise<T | null>. The D1 adapter honors this correctly:
// src/selfhost/d1-adapter.ts:57-61
return ((colName != null ? row[colName] : row) ?? null) as T | null;
The ?? coalesce guarantees null is returned whenever the row exists but the requested column is
SQL NULL or the key is absent from the row object.
The Postgres adapter has no equivalent coalesce:
// src/selfhost/pg-adapter.ts:38-43
return (colName ? row[colName] : row) as T;
When row[colName] is undefined (column absent) or the driver represents a SQL NULL as
undefined rather than null, this returns undefined cast as T — breaking the documented
T | null contract and diverging from the D1 sibling for callers that treat the two backends
interchangeably (the whole point of the adapter abstraction in backend-contracts.ts).
This is a known bug class in this repo — see the "D1 double .first() = undefined" pattern already
fixed elsewhere — but this specific sibling divergence between pg-adapter.ts and d1-adapter.ts
hasn't been addressed.
Existing test asymmetry confirms the gap: test/unit/selfhost-d1-adapter.test.ts:67 has a test
titled "first(colName) returns null when the row exists but the column value is NULL". The analogous
test in test/unit/selfhost-pg-adapter.test.ts:44 never exercises a NULL/absent-column case at all.
Requirements
PgStatement.first(colName) in src/selfhost/pg-adapter.ts must coalesce to null exactly like
d1-adapter.ts's implementation: return ((colName != null ? row[colName] : row) ?? null) as T | null;
(or the Postgres-appropriate equivalent — check the actual current signature before writing the
patch, don't assume this snippet compiles verbatim against pg-adapter.ts's row type).
- Do not change
d1-adapter.ts — it is already correct and is the reference implementation to match.
- Do not change any other method on
PgStatement — this issue is scoped to first() only.
Deliverables
Test Coverage Requirements
This repo's Codecov patch gate is 99%+ of changed lines and branches (src/** is covered). The new
?? null branch added to pg-adapter.ts must be exercised by the new test above, covering both the
present-value case (already covered) and the NULL/absent case (the new test).
Expected Outcome
PgStatement.first() and Statement.first() (D1) behave identically for the NULL/absent-column case,
both honoring the Promise<T | null> contract. Any code that branches on === null (rather than
== null/truthiness) behaves the same regardless of which backend is configured.
Links & Resources
src/selfhost/pg-adapter.ts:38-43 — the method to fix
src/selfhost/d1-adapter.ts:57-61 — the reference implementation to match
test/unit/selfhost-d1-adapter.test.ts:67 — the test to mirror
test/unit/selfhost-pg-adapter.test.ts:44 — the existing test to extend
Context
src/selfhost/pg-adapter.tsandsrc/selfhost/d1-adapter.tsboth implement aStatement.first(colName)method with the same documented contract:
Promise<T | null>. The D1 adapter honors this correctly:The
??coalesce guaranteesnullis returned whenever the row exists but the requested column isSQL
NULLor the key is absent from the row object.The Postgres adapter has no equivalent coalesce:
When
row[colName]isundefined(column absent) or the driver represents a SQLNULLasundefinedrather thannull, this returnsundefinedcast asT— breaking the documentedT | nullcontract and diverging from the D1 sibling for callers that treat the two backendsinterchangeably (the whole point of the adapter abstraction in
backend-contracts.ts).This is a known bug class in this repo — see the "D1 double
.first()= undefined" pattern alreadyfixed elsewhere — but this specific sibling divergence between
pg-adapter.tsandd1-adapter.tshasn't been addressed.
Existing test asymmetry confirms the gap:
test/unit/selfhost-d1-adapter.test.ts:67has a testtitled "first(colName) returns null when the row exists but the column value is NULL". The analogous
test in
test/unit/selfhost-pg-adapter.test.ts:44never exercises a NULL/absent-column case at all.Requirements
PgStatement.first(colName)insrc/selfhost/pg-adapter.tsmust coalesce tonullexactly liked1-adapter.ts's implementation:return ((colName != null ? row[colName] : row) ?? null) as T | null;(or the Postgres-appropriate equivalent — check the actual current signature before writing the
patch, don't assume this snippet compiles verbatim against pg-adapter.ts's row type).
d1-adapter.ts— it is already correct and is the reference implementation to match.PgStatement— this issue is scoped tofirst()only.Deliverables
PgStatement.first()insrc/selfhost/pg-adapter.tsreturnsnull(neverundefined) when therow exists but the requested column is SQL
NULLor absent.test/unit/selfhost-pg-adapter.test.tsgains a test mirroringtest/unit/selfhost-d1-adapter.test.ts:67— "first(colName) returns null when the row exists butthe column value is NULL" — for the Postgres adapter.
Test Coverage Requirements
This repo's Codecov patch gate is 99%+ of changed lines and branches (
src/**is covered). The new?? nullbranch added topg-adapter.tsmust be exercised by the new test above, covering both thepresent-value case (already covered) and the NULL/absent case (the new test).
Expected Outcome
PgStatement.first()andStatement.first()(D1) behave identically for the NULL/absent-column case,both honoring the
Promise<T | null>contract. Any code that branches on=== null(rather than== null/truthiness) behaves the same regardless of which backend is configured.Links & Resources
src/selfhost/pg-adapter.ts:38-43— the method to fixsrc/selfhost/d1-adapter.ts:57-61— the reference implementation to matchtest/unit/selfhost-d1-adapter.test.ts:67— the test to mirrortest/unit/selfhost-pg-adapter.test.ts:44— the existing test to extend