| From: | BHUVAN CHILUKUTI <c(dot)bhuvan(at)iitg(dot)ac(dot)in> |
|---|---|
| To: | "pgsql-bugs(at)lists(dot)postgresql(dot)org" <pgsql-bugs(at)lists(dot)postgresql(dot)org> |
| Subject: | Incorrect NOT IN results with implicit casts returning NULL after anti-join optimization |
| Date: | 2026-10-23 18:04:29 |
| Message-ID: | PN0P287MB090375874C6D5433282EC9DFA7642@PN0P287MB0903.INDP287.PROD.OUTLOOK.COM |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-bugs |
Hi,
I am reporting a query correctness issue in PostgreSQL involving the optimization of NOT IN subqueries into anti-joins when an implicit cast on the inner comparison operand can return SQL NULL for a non-NULL input.
Summary
The issue occurs when PostgreSQL converts a NOT IN (SELECT ...) predicate into an anti-join without preserving the NULL semantics of the inner comparison after an implicit cast.
The investigation attributes the introduction of this behavior to commit 383eb21ebffe9ddd97dca03e529fa363580e7ccf, dated 2026-03-12. I would appreciate confirmation from the PostgreSQL maintainers.
Steps to reproduce
On a fresh test database, using a sufficiently privileged role, run:
CREATE FUNCTION i2t(int) RETURNS text
IMMUTABLE STRICT LANGUAGE sql
AS $$ SELECT CASE WHEN $1 <> 0 THEN format('%s', $1) END $$;
CREATE CAST (int AS text)
WITH FUNCTION i2t(int) AS IMPLICIT;
CREATE TABLE o (t text NOT NULL);
INSERT INTO o VALUES ('1');
CREATE TABLE s (i int NOT NULL);
INSERT INTO s VALUES (0);
SELECT t, t NOT IN (SELECT i FROM s) AS not_in FROM o;
SELECT t FROM o WHERE t NOT IN (SELECT i FROM s);
The function returns SQL NULL for input 0. Therefore, the NOT IN predicate must not evaluate to TRUE for the outer row.
Expected behavior
The scalar expression should evaluate to NULL, and the query with the predicate in the WHERE clause should return zero rows.
Actual behavior
On the affected builds tested, the WHERE query incorrectly returns the outer row containing '1'. The investigation also found that control queries retaining the SubPlan return the expected result.
Tested versions
The issue was reproduced on the tested PostgreSQL 19 RC1 build, the tested REL_19_STABLE tip (015f4c9fc35f91a1ab46bafab05a3686cfe09d46), and the tested master tip (896246ea10cb9d20f69d0442223ce5fa9b365636). The tested PostgreSQL 18.6 build was unaffected.
These results describe the specific builds tested; they are not a claim about every build of these branches.
Proposed fix and testing
I have prepared a proposed patch that prevents the unsafe anti-join conversion when the inner comparison operand is not a direct PARAM_SUBLINK, allowing for a surrounding RelabelType. The patch includes regression tests.
On patched master, the recorded results were:
*
Core regression suite: 239/239 passed.
*
Isolation tests: 134/134 passed.
*
Contrib tests: 191 successful ok lines, with no not ok lines.
The patch has not been fully built and tested on REL_19_STABLE; there, only patch applicability was checked. Additional checks, including check-world/TAP-enabled testing, a non-assert production-style build, pgindent, and dedicated performance measurements, remain outstanding.
The fix may cause some queries involving non-binary casts to use a SubPlan instead of an anti-join. This is a potential performance trade-off that should be considered during review.
Attachments
I am including the proposed source fix and regression tests as a separate patch. I can provide the full investigation report, reproduction scripts, and captured logs if further evidence is useful.
I would appreciate your review of the reported behavior and the proposed fix. Please let me know if additional reproduction details or testing are required.
Regards,
Bhuvan Chilukuti
| Attachment | Content-Type | Size |
|---|---|---|
| AUDIT-F1-fix-with-test.diff | application/octet-stream | 9.1 KB |
| report_blockA.sql | application/octet-stream | 591 bytes |
| report_blockB.sql | application/octet-stream | 823 bytes |
| From | Date | Subject | |
|---|---|---|---|
| Next Message | PG Bug reporting form | 2026-10-23 18:53:54 | BUG #19793: Range containment with empty/unbounded constant range returns true/false for NULL element |
| Previous Message | Tom Lane | 2026-10-23 16:09:58 | Re: BUG #19784: "no relation entry for relid 1" when costing = ANY (<array expr over max(col)>) in a MinMaxAggPath |