Wrong result for whole-row IS NOT NULL with dropped columns
Hackorum builds and tests every patch posted to the lists, not only commitfest submissions. This is Hackorum's own CI rather than the PostgreSQL project's, and it is still under testing - please report anything that looks wrong.
You can run a PostgreSQL built from this patch straight from Docker, with no checkout and no build:
docker run --rm -p 5432:5432 ghcr.io/hackorum-dev/postgres-patch:t254290psql -h localhost -U postgresBuilt from patchset v1 (message #1), October 22, 2026 at 03:50 PM.
Every patchset is also pushed to a branch of our PostgreSQL fork, so you can check out the same tree CI built. Without a PostgreSQL checkout:
git clone --branch t254290_1 https://github.com/hackorum-dev/postgres.gitIn a checkout you already have, add the fork once:
git remote add hackorum https://github.com/hackorum-dev/postgres.gitthen, for this patchset and every later one:
git fetch hackorum t254290_1 && git checkout t254290_1Patchset v1 (message #1) is on t254290_1
Hi,
While working on another thread related with virtual generated columns I
found that a whole-row IS NOT NULL test returns false for a row with no
null fields if the table also has a dropped column:
CREATE TABLE t (a int, b int GENERATED ALWAYS AS (a * 2) VIRTUAL, dropme int);
ALTER TABLE t DROP COLUMN dropme;
INSERT INTO t VALUES (1);
SELECT t, t IS NOT NULL AS is_not_null FROM t;
t | is_not_null
-------+-------------
(1,2) | f
(1 row)
The same happens with a CHECK constraint, which can't be added because
no row can satisfy it, and when it's added as NOT VALID every new row
is rejected:
ALTER TABLE t ADD CONSTRAINT wr CHECK (t IS NOT NULL);
ERROR: check constraint "wr" of relation "t" is violated by some row
With a stored generated column, or without the dropped column, it
works as expected.
EXPLAIN shows that the planner has folded the whole test to a
constant:
EXPLAIN (VERBOSE, COSTS OFF) SELECT t IS NOT NULL FROM t;
QUERY PLAN
--------------------------
Seq Scan on public.t
Output: false
(2 rows)
IIUC what happens is that to expand the virtual column the whole-row
Var is turned into a RowExpr, and since this is a named rowtype
expandRTE() includes a null int4 Const as placeholder for the dropped
column. Then eval_const_expressions() breaks ROW(...) IS [NOT] NULL
into tests on each field, sees the null constant and refutes the
IS NOT NULL. The executor doesn't have this problem because
ExecEvalRowNullInt() skips dropped attributes, so the result depends on
whether the test was simplified at plan time or not.
Virtual generated columns are just the easiest way to reach this. The
same placeholder is added by coerce_record_to_complex() when casting a
row to a named rowtype, so this is also wrong on master without any
generated column involved:
CREATE TABLE ts (a int, dropme int, b int);
ALTER TABLE ts DROP COLUMN dropme;
EXPLAIN (VERBOSE, COSTS OFF) SELECT ROW(a, b)::ts IS NOT NULL FROM ts;
QUERY PLAN
-----------------------
Seq Scan on public.ts
Output: false
(2 rows)
The attached patch fixes this by looking up the rowtype's tuple
descriptor when simplifying a NullTest on a RowExpr of a named
composite type and skipping the fields that correspond to dropped
columns, which matches what the executor does. With the patch the
EXPLAIN above shows ((a IS NOT NULL) AND (b IS NOT NULL)). I've added
regression tests on generated_virtual.sql and rowtypes.sql.
The folding code is old, so the ROW()::type case is not new. I've
checked that the EXPLAIN above also shows "Output: false" on PG 14, so I
think that all supported branches are affected and this should be
backpatched. The virtual generated column case only applies to 18 and
master, so perhaps for 17 and older we need a different fix.
Thoughts?
--
Matheus Alcantara
EDB: https://www.enterprisedb.com
On Wed, Oct 21, 2026 at 12:05 AM Matheus Alcantara <matheusssilv97@gmail.com>
wrote:
The attached patch fixes this by looking up the rowtype's tuple
descriptor when simplifying a NullTest on a RowExpr of a named
composite type and skipping the fields that correspond to dropped
columns, which matches what the executor does.
Hi Matheus,
I reviewed v1 with assertions enabled,
and found no blocking issue in the changes.
Thanks,
Sho Ito
--
=======================================
*Sho Ito (Sean)*
E-mail: i.sho.0628@gmail.com
LinkedIn: *https://www.linkedin.com/in/sho-ito-0628/
<https://www.linkedin.com/in/sho-ito-0628/>*
GitHub: https://github.com/sean0628/
=======================================