Skip to content

test(postgrest): add int8/bigint precision e2e over PostgREST#2461

Open
Maliik-B wants to merge 3 commits into
supabase:masterfrom
Maliik-B:test/postgrest-js-bigint-precision
Open

test(postgrest): add int8/bigint precision e2e over PostgREST#2461
Maliik-B wants to merge 3 commits into
supabase:masterfrom
Maliik-B:test/postgrest-js-bigint-precision

Conversation

@Maliik-B

Copy link
Copy Markdown

What

Adds an end-to-end test (test/bigint.test.ts) and a fixture table (bigint_precision) showing how int8/bigint values behave over PostgREST around Number.MAX_SAFE_INTEGER (2^53 - 1), on read (with and without a ::text cast) and on write.

Why

This came out of the review on supabase/postgres-meta#1083 (a bigint_as typegen option). @avallete asked to demonstrate at runtime that the type adjustment matches PostgREST behavior, including on update, rather than asserting it at the type level. This test does that against the postgrest-js stack.

What it shows

Against a seeded value of 9223372036854775807 (2^63 - 1):

  • Read, no cast returns the JS number 9223372036854776000. The exact value is already lost, because PostgREST emits int8 as a JSON number and JSON.parse rounds it to the nearest double.
  • Read, cast to text (select('big_value::text')) returns the exact string "9223372036854775807". PostgREST returns a JSON string, which survives JSON.parse.
  • Write above 2^53 as a string round-trips losslessly: updating with "9007199254740993" and reading back with a cast returns "9007199254740993". The precision loss is a client-side concern, not a PostgREST one, so a string (or a BigInt, which postgrest-js already serializes to a string) goes in intact.
  • Write above 2^53 as a JS number loses precision before the request is sent: Number("9007199254740993") is already 9007199254740992 in JS, and that is what gets stored.

The string-write case carries a @ts-expect-error: the generated big_value is number, so the lossless string form is a type error today. That is the gap the postgres-meta bigint_as=string option closes, by generating the Insert/Update column as string.

Notes

  • test/types.generated.ts gains the bigint_precision table. I added that entry in place to keep the diff focused. A full codegen:postgrest run also surfaces unrelated pre-existing drift in that file (a couple of functions added/removed in the schema since it was last generated), which I left out of this change. Happy to do a full regen instead if you would prefer.
  • Test-only change, no src/ change.

Thanks @avallete for the review direction on postgres-meta#1083.

Adds test/bigint.test.ts and a bigint_precision fixture demonstrating int8
behavior around Number.MAX_SAFE_INTEGER on read (with and without a ::text
cast) and on write.

Refs: supabase/postgres-meta#1083

@avallete avallete left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @Maliik-B,

Thanks for putting together the runtime demonstration that's exactly the evidence we needed to settle the type direction.

I think the cleanest shape is to make bigint_as accept the full set of representations rather than a single scalar choice:

number · bigint · string · number | bigint · number | string · …

That way users pick the tradeoff that fits their app, and we lean on defaults that preserve today's behavior:

  • Read (SELECT) defaults to number. PostgREST emits int8 as a JSON number, so precision is already lost by the time JSON.parse runs — typing it otherwise would misrepresent what comes over the wire. Users who need the exact value cast to ::text and get a string back (and the type inference properly infer that the query result for a casted column will match the cast as demonstrated by your test).
  • Write (INSERT/UPDATE) — number | bigint is the value I'd recommend. bigint is the lossless channel (postgrest-js serializes it to a JSON string), and number keeps the ergonomic path for the common small-value case. It's pure input widening, so it's backward compatible.

The one thing to document loudly: any value that includes string trades away compile-time validation an arbitrary non-numeric string passes the type check and only fails at runtime in PostgREST. Totally fine as a deliberate opt-in, as long as users know what they're choosing.

What do you think?

@pkg-pr-new

pkg-pr-new Bot commented Jun 18, 2026

Copy link
Copy Markdown

Open in StackBlitz

@supabase/auth-js

npm i https://pkg.pr.new/@supabase/auth-js@2461

@supabase/functions-js

npm i https://pkg.pr.new/@supabase/functions-js@2461

@supabase/postgrest-js

npm i https://pkg.pr.new/@supabase/postgrest-js@2461

@supabase/realtime-js

npm i https://pkg.pr.new/@supabase/realtime-js@2461

@supabase/storage-js

npm i https://pkg.pr.new/@supabase/storage-js@2461

@supabase/supabase-js

npm i https://pkg.pr.new/@supabase/supabase-js@2461

commit: d936dcc

@Maliik-B

Copy link
Copy Markdown
Author

This shape works, and it matches what the test shows. Let me confirm I have the model right before I reshape the generator:

  • Row (read) stays number. The un-cast wire value is a lossy number, so that is the honest type, and ::text already infers string on the casted column (the existing relationships test and the new one both rely on that). So bigint_as would not touch the Row type.
  • Insert/Update (write) is where bigint_as widens the column. Default number for back-compat, with number | bigint as the recommended option, since postgrest-js serializes a BigInt to a JSON string.

Two things to pin down so I build the right thing:

  1. The exact accepted set. Is it a fixed list along the lines of number, bigint, string, number | bigint, number | string, number | bigint | string? And does string stay in with the runtime-validation caveat documented, or would you rather limit it to the number / bigint combinations?
  2. How a union is spelled on the flag/env side, for example bigint_as=number|bigint as a single string.

Once you confirm those, I will update #1083 to apply bigint_as to the Insert/Update types only, leave Row as number, and document the string tradeoff.

Copy link
Copy Markdown
Member
  1. I would recommend getting string out, but I could understand why someone would like to have it eg:

    const entity = supabase.from('table').select('id::text, value');
    const updated = supabase.from('table').updated({ value: 'updated' }).where({ id: entity.id })
    // If not set to string the user will need to explicitly cast this into bigint because it first
    // receive a string like so:
    const updated = supabase.from('table').updated({ value: 'updated' }).where({ id: Bigint(entity.id) })
    
  2. I think we can pass it as a single string, we can parse and split over the | to reconstruct the union properly.


    What would be nice is to demonstrate how those types actually would work by overriding them in the tests so we can ensure the DX makes sense for those objects and catch potential issues.

@Maliik-B

Copy link
Copy Markdown
Author

Sounds good, that all works for me. And thanks for the prompt replies.

  • Dropping string: agreed. number | bigint for Insert/Update, with the Bigint(entity.id) round-trip on the read-cast path as the documented pattern. Keeps the type honest and avoids the arbitrary-string hole.
  • Single string split on |: agreed, that keeps the option simple (bigint_as=number|bigint).
  • Demonstrating the types: will do. I will override bigint_precision's Insert/Update to number | bigint in test(postgrest): add int8/bigint precision e2e over PostgREST #2461 and add cases that exercise the DX (a BigInt write, a plain number write, and a string write that should be rejected), so we can see how it actually feels and catch issues before the generator change.

I will push the type-override demo to #2461 first so the DX is concrete, then reshape the bigint_as option in postgres-meta#1083 to emit the |-split union on Insert/Update only (Row left as number), defaulting to number. I will link both back here.

Maliik-B added a commit to Maliik-B/postgres-meta that referenced this pull request Jun 18, 2026
Per the read/write model worked out on supabase/supabase-js#2461: the Row stays number (an un-cast int8 is a lossy JSON number, and ::text already infers string on the casted column), and only Insert/Update widen to the bigint_as union. The option is now a pipe-split string accepting number and bigint; string is dropped, since an arbitrary non-numeric value would pass the type check and fail only at runtime in PostgREST.
@Maliik-B

Copy link
Copy Markdown
Author

The type-override demo is up in this PR (test/bigint.test-d.ts), and postgres-meta#1083 is reshaped to match.

The demo overrides bigint_precision's Insert/Update to number | bigint and checks the DX directly:

  • the Row big_value stays number on a plain select, and ::text still infers string,
  • a BigInt write type-checks (the lossless channel for values above 2^53),
  • a plain number write type-checks (the ergonomic common case),
  • a raw string write is a compile error, which is the concrete reason string is left out of the set.

postgres-meta#1083 now applies bigint_as per direction to match:

  • Row (read) is left as number.
  • Insert/Update widen to the bigint_as union: default number, accepted tokens number and bigint, passed as a single |-split string (bigint_as=number|bigint).
  • Function returns and arguments are left as number too; the option is scoped to table/view writes.

Generator change plus tests (default number, number|bigint, and bigint): supabase/postgres-meta#1083. Ready for another look whenever you have a moment, and happy to adjust the scope or the accepted set from here.

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