Skip to content

Parse NULL elements in arrays as null - #1258

Open
OsamaAnsar wants to merge 1 commit into
porsager:masterfrom
OsamaAnsar:fix-null-in-array-parsing
Open

OsamaAnsar wants to merge 1 commit into
porsager:masterfrom
OsamaAnsar:fix-null-in-array-parsing

Conversation

@OsamaAnsar

Copy link
Copy Markdown

Fixes #1049, fixes #1124.

An unquoted NULL element in array output was passed to the element parser like any other value. Depending on the element type that gave:

query before after
array['a', null, 'b']::text[] ['a', 'NULL', 'b'] ['a', null, 'b']
array[1, null]::int2[] [1, NaN] [1, null]
array[1.5, null]::numeric[] ['1.5', 'NULL'] ['1.5', null]
array[true, null]::bool[] [true, false] [true, null]
array['{"a":1}'::json, null] throws "NULL" is not valid JSON [{ a: 1 }, null]
array[1, null]::bigint[] ['1', 'NULL'] ['1', null]
array['2020-01-01 00:00:00+00'::timestamptz, null] [Date, Invalid Date] [Date, null]
array_agg(y) over 'a', null, 'b' ['a', 'NULL', 'b'] ['a', null, 'b']
array[[1, null], [null, 4]]::int[] [[1, NaN], [NaN, 4]] [[1, null], [null, 4]]

This is the same behaviour as a NULL column value, which already comes back as null without going through the parser, and it matches what sql.array([1, null, 3]) already sends on the write side.

Postgres always quotes a string that reads NULL (in any case) in array output, so the unquoted token is unambiguous. The three places in arrayParserLoop that push an unquoted slice now go through a small arrayElement helper that returns null for the bare NULL token and otherwise behaves as before. Quoted elements are untouched, so array['NULL', null]::text[] gives ['NULL', null].

Notes:

  • The element parser (built-in or a custom one from types) is no longer called for a NULL element, so a custom parser will not see the string 'NULL' any more.
  • Behaviour for a value starting with a lower-bound prefix such as [0:2]={...} is unchanged apart from the NULL elements.

Tests

Added 16 tests to tests/index.js: end to end queries for text, int2/int4, bigint, numeric, bool, timestamptz, json, array_agg, nested arrays, an array of only nulls, a real 'NULL' string (plain, nested and round tripped through sql.array), a custom type parser that must not receive NULL, and direct arrayParser calls (including the ; delimited box type). Without the src/types.js change every one of them fails, and with it they all pass.

What I ran

  • Node v24.19.0: the 16 new tests, run against an in-process Postgres (PGlite served over TCP) since I have no local Postgres server. Not the CI matrix of Postgres 12 to 17.
  • The rest of tests/index.js was not run, because it needs a real server with psql/createdb, so I can't say anything about the other tests here.
  • eslint src tests passes (eslint 8 with the repo's .eslintrc.json).
  • Bun 1.4.2: ran the same end to end queries against PGlite with the same output as Node. I did not run the repo's test runner under Bun.
  • Deno and Cloudflare Workers: not run. I only checked that transpile.cjs, transpile.deno.js and transpile.cf.js succeed on a copy of the tree and that the transpiled test file for cjs passes node --check. I did not commit the cjs/, deno/ or cf/ output, since those look to be rebuilt in separate build commits.

Several open PRs touch src/types.js (e.g. #1247, #1162, #392), but none of their diffs touch arrayParserLoop, so I don't expect conflicts.

An unquoted NULL in array output was handed to the element parser like any
other value, so it came out as the string 'NULL' for text, NaN for int2/int4,
false for bool and Invalid Date for dates, and made json/jsonb arrays throw.

Postgres quotes a string that reads NULL, so an unquoted NULL is always a null
element and is now returned as null without calling the element parser.
@tcurdt

tcurdt commented Oct 15, 2026

Copy link
Copy Markdown

Any reason this does not get merged?
We ran into the same problem.

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.

nulls in result of array_agg are parsed to the string literal 'NULL' rather than the JS null NULL in smallint array mapped to NaN instead of null

2 participants