BUG #19750: tsquery output omits parentheses, so the text reparses to a different value - Mailing list pgsql-bugs

From PG Bug reporting form
Subject BUG #19750: tsquery output omits parentheses, so the text reparses to a different value
Date
Msg-id 19750-d4e6f6e6e93000ba@postgresql.org
Whole thread
Responses Re: BUG #19750: tsquery output omits parentheses, so the text reparses to a different value
List pgsql-bugs
The following bug has been logged on the website:

Bug reference:      19750
Logged by:          Ke
Email address:      kehan5800@gmail.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.





pgsql-bugs by date:

Previous
From: shihao zhong
Date:
Subject: Re: BUG #19742: `INTERSECT` under a `UNION ALL` with an empty arm fails with "could not find pathkey item t"
Next
From: David Rowley
Date:
Subject: Re: BUG #19742: `INTERSECT` under a `UNION ALL` with an empty arm fails with "could not find pathkey item t"