Skip to content

fix(pg_list_tables): cast reltuples to float8 so estimated_rows is a number#17

Open
HighviewOne wants to merge 2 commits into
mainfrom
fix/estimated-rows-number-type
Open

fix(pg_list_tables): cast reltuples to float8 so estimated_rows is a number#17
HighviewOne wants to merge 2 commits into
mainfrom
fix/estimated-rows-number-type

Conversation

@HighviewOne

Copy link
Copy Markdown
Collaborator

What

pg_list_tables casts pg_class.reltuples to ::bigint. Node-pg parses int8 (OID 20) as a JavaScript string by default, so estimated_rows was returning "42" (a JSON string) instead of 42 (a JSON number) — contrary to the TypeScript annotation and caller expectations.

How

Cast to ::float8 instead. reltuples is already float4 (an approximate estimate), so float8 just widens the precision. Node-pg returns float8 as a JS number correctly, and the TypeScript type number stays valid.

This is consistent with how other numeric fields are handled in health.ts and explain.ts. The codebase's pattern for bigint counters is to cast to ::text (for lossless serialization) — reltuples doesn't need that since it's already approximate.

Test

Added a regression assertion to the integration test that checks typeof estimated_rows === "number" against a live database. The existing test only checked name and type, so a revert would have been silent. All 87 integration tests pass.

🤖 Generated with Claude Code

…number

pg_class.reltuples is float4. Casting it to ::bigint gives an int8 result
(OID 20), which node-pg parses as a string by default -- so estimated_rows
was returning "42" (a JSON string) instead of 42 (a JSON number), contrary
to the TypeScript annotation and every caller's expectation.

Cast to ::float8 instead: reltuples is already approximate, float8 just
widens the precision, and node-pg returns float8 as a JS number. Consistent
with the pattern used by other numeric fields in health.ts and explain.ts.

Adds a regression assertion to the integration test that verified
typeof estimated_rows === "number" against a live database.
@jeffyaw

jeffyaw commented Jun 11, 2026

Copy link
Copy Markdown
Member

Review punch list for this PR. The core fix is correct: the handler generic at src/tools/schemas.ts:60 already declared estimated_rows: number, so the old ::bigint cast made the declared type a lie at runtime (node-pg returns int8 as a string); ::float8 makes it true and the regression test pins it. Two minor items:

  1. src/tools/schemas.ts (the cast line) -- reltuples is a float4 sampling estimate, so ::float8 can now surface non-integral values (e.g. 123456.75) where ::bigint used to round; a fractional "row count" reads oddly. Fix: round(c.reltuples)::float8 keeps the value integral while staying a JS number.

  2. src/tools/schemas.ts:37 (tool description) -- "approximate - 0 until ANALYZE runs" is wrong on PG 14+: the never-vacuumed/analyzed sentinel is -1, which this PR now surfaces as a clean -1 number that callers will read as "minus one rows". Fix: update the description to mention -1 (on PG 14+) -- or map the sentinel to null in SQL, e.g. NULLIF(round(c.reltuples), -1)::float8 AS estimated_rows, so "no estimate yet" is unambiguous (note: on PG <= 13 the sentinel is 0 and can't be distinguished from a genuinely empty table, so the description should keep the 0 caveat either way).

🤖 Generated with Claude Code

…fix description

- NULLIF(round(c.reltuples), -1)::float8: rounds to integral row count
  (float4 sampling can produce 123456.75); maps PG 14+ never-analyzed
  sentinel (-1) to null so callers can distinguish "no estimate" from 0
- Description updated: "null = no ANALYZE yet on PG 14+; 0 may mean
  empty or unanalyzed on PG <= 13"
- Integration test updated to accept number|null and assert integrality

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@HighviewOne

Copy link
Copy Markdown
Collaborator Author

Addressed both review points:

  1. Cast — changed to NULLIF(round(c.reltuples), -1)::float8: rounds to an integral value (avoids fractional row counts from float4 sampling), then maps the PG 14+ never-analyzed sentinel (-1) to null.
  2. Description — updated to "null = no ANALYZE yet on PG 14+; 0 may mean empty or unanalyzed on PG <= 13".
  3. Integration test — updated to accept number | null and assert integrality when a number is returned.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants