Wrong results from self-join elimination with different restriction operators
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:t254150psql -h localhost -U postgresBuilt from patchset v1 (message #1), October 14, 2026 at 06:18 AM.
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 t254150_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 t254150_1 && git checkout t254150_1Patchset v1 (message #1) is on t254150_1
Hi,
After finding the SJE bug involving inheritance scans and ONLY, I
asked an LLM to generate fuzz tests to look for further issues in
self-join elimination.
This uncovered another wrong-result case involving restriction clauses
with different equality operators.
How to reproduce:
...
postgres=# CREATE FUNCTION text_ci_eq(text, text) RETURNS boolean
LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE
AS $$
BEGIN
RETURN lower($1) = lower($2);
END $$;
CREATE FUNCTION text_ci_cmp(text, text) RETURNS int
LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE
AS $$SELECT CASE WHEN lower($1) < lower($2) THEN -1
WHEN lower($1) > lower($2) THEN 1 ELSE 0 END$$;
CREATE OPERATOR === (LEFTARG=text, RIGHTARG=text, PROCEDURE=text_ci_eq,
COMMUTATOR='===', RESTRICT=eqsel, JOIN=eqjoinsel, MERGES);
CREATE OPERATOR FAMILY ci_text_fam USING btree;
CREATE OPERATOR CLASS ci_text_ops FOR TYPE text USING btree FAMILY
ci_text_fam AS
OPERATOR 1 <, OPERATOR 2 <=, OPERATOR 3 ===, OPERATOR 4 >=, OPERATOR 5 >,
FUNCTION 1 text_ci_cmp;
CREATE TABLE ci_t(id int, a text, b int);
CREATE UNIQUE INDEX ci_t_a ON ci_t(a); -- deterministic (standard)
unique index
INSERT INTO ci_t VALUES (1,'a',1),(2,'A',1),(3,'b',2);
ANALYZE ci_t;
SET enable_self_join_elimination = on;
SELECT p1.id, p1.a, p2.id, p2.a
FROM ci_t p1 JOIN ci_t p2 ON p1.b = p2.b
WHERE p1.a === 'a' AND p2.a = 'a'
ORDER BY 1, 3;
id | a | id | a
----+---+----+---
1 | a | 1 | a
(1 row)
SET enable_self_join_elimination = off;
SELECT p1.id, p1.a, p2.id, p2.a
FROM ci_t p1 JOIN ci_t p2 ON p1.b = p2.b
WHERE p1.a === 'a' AND p2.a = 'a'
ORDER BY 1, 3;
id | a | id | a
----+---+----+---
1 | a | 1 | a
2 | A | 1 | a
(2 rows)
...
SJE incorrectly considers the two restrictions equivalent and
eliminates the join.
The unique index and p2.a = 'a' establish uniqueness on the kept side.
match_unique_clauses() then checks for a corresponding restriction on
the removed side, but compares only the operands, ignoring the
operator and input collation.
It therefore treats p1.a === 'a' as matching p2.a = 'a', even though
they select different rows.
The attached patch requires both the operator and input collation to
match, in addition to the existing operand checks.
It includes a regression test for the different-operator case and
verifies that identical restrictions still allow self-join
elimination.
The full reproducer, including the custom operator definitions, is in
the regression test.
--
Thanks,
Tender Wang
Tender Wang <tndrwang@gmail.com> writes:
After finding the SJE bug involving inheritance scans and ONLY, I
asked an LLM to generate fuzz tests to look for further issues in
self-join elimination.
This uncovered another wrong-result case involving restriction clauses
with different equality operators.
If we're going to refuse that, I wonder if we couldn't simplify
match_unique_clauses a great deal: just insist that the two OpExpr
clauses be equal(), after the ChangeVarNodes substitution on the outer
clause. Your proposal already breaks commuted-clause cases, which
seem like the only justification for all the futzing around with left
and right operands. I doubt that detecting that is of any real-world
value anyway.
regards, tom lane