| From: | PG Bug reporting form <noreply(at)postgresql(dot)org> |
|---|---|
| To: | pgsql-bugs(at)lists(dot)postgresql(dot)org |
| Cc: | kehan5800(at)gmail(dot)com |
| Subject: | BUG #19750: tsquery output omits parentheses, so the text reparses to a different value |
| Date: | 2026-10-04 05:33:50 |
| Message-ID: | 19750-d4e6f6e6e93000ba@postgresql.org |
| Views: | Whole Thread | Raw Message | Download mbox | Resend email |
| Thread: | |
| Lists: | pgsql-bugs |
The following bug has been logged on the website:
Bug reference: 19750
Logged by: Ke
Email address: kehan5800(at)gmail(dot)com
PostgreSQL version: 18.6
Operating system: Ubuntu 22.04.2 x86_64
Description:
tsquery's output function, infix() in src/backend/utils/adt/tsquery.c,
parenthesises a binary operator's operand only when the operand's operator
has lower priority than the parent (or, for <->, when it is the right
operand). When the same operator, & or |, is nested on the right, no
parentheses are written. The parser is left-associative, so that text
reads back as a different tree, and tsquery equality compares trees:
=# select q, q::tsquery::text as printed,
(q::tsquery::text::tsquery = q::tsquery)::int as round_trips
from (values ('a & b & c'), ('a & (b & c)'), ('(a & b) & c'),
('a | (b | c)'), ('a & (b | c)'), ('a <-> (b <-> c)'))
v(q);
q | printed | round_trips
-----------------+-------------------------+-------------
a & b & c | 'a' & 'b' & 'c' | 1
a & (b & c) | 'a' & 'b' & 'c' | 0
(a & b) & c | 'a' & 'b' & 'c' | 1
a | (b | c) | 'a' | 'b' | 'c' | 0
a & (b | c) | 'a' & ( 'b' | 'c' ) | 1
a <-> (b <-> c) | 'a' <-> ( 'b' <-> 'c' ) | 1
(6 rows)
Two different values print as 'a' & 'b' & 'c':
=# select count(distinct q)
from (values ('a & (b & c)'::tsquery), ('a & b & c'::tsquery))
v(q);
count
-------
2
=# select count(distinct q::text)
from (values ('a & (b & c)'::tsquery), ('a & b & c'::tsquery))
v(q);
count
-------
1
You get this shape without writing parentheses. The && and || operators,
which applications use to build a query from parts, produce it:
=# select ('a'::tsquery && ('b & c')::tsquery)::text as printed,
(('a'::tsquery && ('b & c')::tsquery)::text::tsquery
= ('a'::tsquery && ('b & c')::tsquery))::int as round_trips;
printed | round_trips
-----------------+-------------
'a' & 'b' & 'c' | 0
A text dump/restore, which goes through tsqueryout/tsqueryin, therefore
changes the stored value:
=# create table tq (q tsquery);
=# insert into tq values ('a'::tsquery && ('b & c')::tsquery);
=# select (select q from tq) = (select q::text::tsquery from tq);
?column?
----------
f
Expected: either the output is 'a' & ( 'b' & 'c' ), which reads back to the
same value (as is already done for <->), or the two values compare equal.
round_trips should be 1 in every row above.
The two trees match the same documents, since & and | are associative, so
@@ results do not change. What changes is the value itself. Equality,
ordering, GROUP BY, DISTINCT and a unique index on a tsquery column all
treat
the restored value differently from the original.
Suggested fix: in infix(), also parenthesise the right operand of & and |
when it has the same priority as the parent. That is already done for
OP_PHRASE through the rightPhraseOp argument ("phrase operator depends on
order"). Generalising that, so a right operand is parenthesised whenever
its priority is <= the parent's (not only for OP_PHRASE), would do it. The
cost is extra parentheses in the output for right-nested & and | only;
left-nested and flat chains print the same as today.
Reproduced on master 20devel @ 45277ca0d1cb, 18.6 and 17.11.
| From | Date | Subject | |
|---|---|---|---|
| Next Message | David Rowley | 2026-10-04 05:37:29 | Re: BUG #19742: `INTERSECT` under a `UNION ALL` with an empty arm fails with "could not find pathkey item t" |
| Previous Message | shihao zhong | 2026-10-04 05:23:29 | Re: BUG #19742: `INTERSECT` under a `UNION ALL` with an empty arm fails with "could not find pathkey item t" |