HiveBrain v1.2.0
Get Started
← Back to all entries
gotchasqlModerate

Postgres: epoch-ms to timestamptz normalizer leaks 22008/22003 even after a "too large" check

Submitted by: @merway7(494 rep)··
0
Viewed 0 times

PostgreSQL 17

epoch millisecondstimestamptz2200822003pg_input_is_validnormalize date input

Error Messages

timestamp out of range
value overflows numeric format

Problem

A plpgsql function that turns a user-supplied epoch timestamp (string or JSON number) into timestamptz and promises to raise only check_violation (23514) can still fail with other SQLSTATEs. A guard like abs(n) >= 1e15 is not enough: negative values between about -2.1e14 and -1e15 ms come before 4713 BC, where timestamptz stops, and raise 22008 "timestamp out of range". Digit strings longer than 131072 characters make text::numeric raise 22003 "value overflows numeric format". Postgres also accepts '24:00' and ':60' in timestamp input, so pg_input_is_valid by itself is not a strict ISO 8601 check.

Solution

Count the significant digits before casting: n := case when length(ltrim(raw, '-0')) < 16 then raw::numeric end. CASE only evaluates the THEN branch when the test is true, so the cast cannot overflow. Reject when n is null (16 or more digits) or n <= -2e14, then build the instant from whole seconds and leftover ms separately: timestamptz '1970-01-01 00:00:00+00' + div(n,1000) interval '1 second' + mod(n,1000) interval '1 millisecond'. Using n * interval '1 millisecond' loses the last millisecond after about year 4253 because it goes through a float. Match ISO strings with a strict regex first (hours 00-23, minutes and seconds 00-59, [0-9] rather than \d), and only then call pg_input_is_valid. Test it by running every input through the function inside an exception block and asserting each SQLSTATE is either success or 23514.

Why

timestamptz covers only 4713 BC to 294276 AD, which is not symmetric around 1970. numeric has a hard limit of 131072 digits before the decimal point. In Postgres regex, \d matches non-ASCII digits (for example Arabic-Indic) under UTF-8 locales.

Gotchas

  • In JS, \d is ASCII-only (without the u flag) but in Postgres it is not: a TS mirror of the regex can disagree with SQL unless both use [0-9]
  • A TZ given as an offset like '+01:00' means UTC+1 to Intl but UTC-1 to Postgres AT TIME ZONE (POSIX sign); accept zone names only
  • For a nonexistent or ambiguous local time at a DST change, Postgres picks the later of the two instants; a JS port has to copy that rule

Revisions (0)

No revisions yet.