shihao zhong <[email protected]> writes:
> Named composite types still have the problem. ALTER TYPE can add a
> timestamptz after the index is built.
I measured this on 18.6, 19rc1 and master, and it is broader than a mutable
attribute: a pure structural change does it too, with no mutable type and no
GUC. Adding a plain int attribute is enough.
CREATE TYPE c1 AS (x int);
CREATE TABLE t1 (v c1);
INSERT INTO t1 VALUES (row(1));
CREATE INDEX t1_i ON t1 ((json_array(v returning jsonb)));
ALTER TYPE c1 ADD ATTRIBUTE y int;
SELECT count(*) FROM t1
WHERE json_array(v returning jsonb) =
json_array(row(1, null)::c1 returning jsonb);
The index scan returns 0 and a seq scan returns 1; REINDEX restores 1, so the
entries are stale. 18 rejects the CREATE INDEX. RENAME ATTRIBUTE and DROP
ATTRIBUTE do the same, and so does ALTER TABLE ... RENAME COLUMN / ADD COLUMN
on a table's rowtype.
The reason is that the jsonb output carries the attribute names and their
number. The immutability verdict is computed once, at CREATE INDEX, from the
type's definition at that moment, and the catalog does not block a later
ADD / RENAME / DROP ATTRIBUTE (only ALTER ATTRIBUTE ... TYPE is rejected). So
the timestamptz case is one instance of a more general problem, and a recheck
that only looks for a mutable attribute would leave the structural changes
open.
The same expression misbehaves in the other places it can be used, all on
19rc1 and master and all rejected by 18:
- a unique index takes a second row with the same key, and a later REINDEX
fails with "could not create unique index";
- a partial index returns a row that no longer matches its predicate;
- a STORED generated column keeps [{"x": 1}] while a recompute gives
[{"z": 1}];
- a partition key stops pruning to the right partition, and the row ends up
violating its partition constraint.
It also reaches through a nested composite, a domain over a composite and an
array of a composite.
The full matrix, the minimal repro and the per-version output on
18.6 / 19rc1 / master are in the attached evidence.txt.
regards,
Manu
JSON_ARRAY / JSON_OBJECT over named composite types: stale indexes after ALTER
TYPE, no GUC
===========================================================================================
Context: shihao's report that named composite types still get a stale
expression index
after ALTER TYPE ... ADD ATTRIBUTE ts timestamptz. This sweep checks whether
that is the
only path, and whether create-time detection is complete.
Builds (all --enable-cassert --enable-debug, CFLAGS=-O2):
r18 18.6
r19rc1 19rc1 (0b8bff042a9)
master 20devel, source tree at f9ccc5e4f92
jian's v2-0001 was NOT measured in this run (see "Caveats").
Oracle (oracle.sql): the same predicate is counted twice, once with
enable_seqscan=off and
once with enable_indexscan/indexonlyscan/bitmapscan=off. EXPLAIN confirms the
first plan
uses the index. The row is always inserted BEFORE the ALTER. Then REINDEX and
count again.
Files: sweep.sql (all cases), oracle.sql, run.sh (one fresh cluster per build),
out/sweep.<build>.out (full psql output).
1. Verdict
----------
(a) Create-time detection works. A composite that already contains timestamptz
when the
index is built is rejected on 19rc1 and master, both directly (K0) and
nested (K0n).
json_check_mutability() does recurse into the attributes.
(b) shihao's case is not specific to timestamptz. The JSON value of a composite
contains
the attribute names and one entry per attribute, so any change to the
type's shape
changes the value. With an all-int composite and no GUC change, ADD
ATTRIBUTE, RENAME
ATTRIBUTE and DROP ATTRIBUTE each leave the index stale. So do ALTER TABLE
... RENAME
COLUMN / ADD COLUMN on a table whose row type is used as a column type.
(c) The same happens through a nested composite, a domain over a composite, an
array of a
composite, JSON_OBJECT, and RETURNING json (indexed through ::text). Beyond
plain btree
indexes it also affects UNIQUE indexes, partial-index predicates, STORED
generated
columns and partition keys.
(d) 18.6 rejects every CREATE in this sweep. 19rc1 and master give identical
results.
(e) The ALTER is not blocked. ADD / RENAME / DROP ATTRIBUTE go through on a
type used by
an indexed column. Only ALTER ATTRIBUTE ... TYPE is refused ('cannot alter
type "c9"
because column "t9.v" uses it'), on every build.
2. Matrix
---------
"rej" = CREATE rejected (must be marked IMMUTABLE). "i/s" = index count / seq
count.
"*" = index and seq disagree. "+REINDEX" = after REINDEX of that index.
case r18 r19rc1 master
+REINDEX (19/master)
S0 shihao: ADD ATTRIBUTE ts + TimeZone rej 0/1 * 0/1 * -
K0 composite with timestamptz at CREATE rej rej rej -
K0n nested, timestamptz inside, at CREATE rej rej rej -
C1 ADD ATTRIBUTE y int rej 0/1 * 0/1 * 1/1
C2 RENAME ATTRIBUTE rej 0/1 * 0/1 * 1/1
C3 DROP ATTRIBUTE rej 0/1 * 0/1 * 1/1
C4 table row type, ALTER TABLE RENAME COLUMN rej 0/1 * 0/1 * 1/1
C4b table row type, ALTER TABLE ADD COLUMN rej 0/1 * 0/1 * (not
run)
C5 nested composite, RENAME in inner type rej 0/1 * 0/1 * 1/1
C6 domain over composite, RENAME rej 0/1 * 0/1 * 1/1
C7 array of composite, RENAME rej 0/1 * 0/1 * 1/1
C8 JSON_OBJECT('k' VALUE v), RENAME rej 0/1 * 0/1 * (not
run)
C8b JSON_ARRAY(v RETURNING json)::text, RENAME rej 0/1 * 0/1 * (not
run)
C9 ALTER ATTRIBUTE TYPE on an indexed type refused on every build
U1 UNIQUE index, RENAME, insert same value rej dup dup REINDEX
fails
P1 partial index predicate, RENAME rej 1/0 * 1/0 * 0/0
G1 STORED generated column, RENAME rej stale stale -
K1 LIST partition key, RENAME rej 0 vs 1 0 vs 1 -
Details:
- C1 after the ALTER the row reads [{"x": 1, "y": null}]; C2 reads [{"z": 1}];
C5 reads
[{"a": {"z": 1}}].
- U1: the second INSERT of row(1) is accepted. The table then holds 2 rows with
1 distinct
key; the index finds 1, a seq scan 2. REINDEX fails with "could not create
unique index".
- P1: the index on (id) WHERE json_array(v) = '[{"x": 1}]' still returns the
row after
the RENAME, although the predicate is now false for it (seq scan: 0).
- G1: stored [{"x": 1}], recomputed [{"z": 1}].
- K1: count is 0 with enable_partition_pruning=on and 1 with it off. The row
stays in the
partition for '[{"x": 1}]' and now violates that partition's constraint.
3. Minimal repro (same shape as shihao's, no mutable type, no GUC)
------------------------------------------------------------------
CREATE TYPE c1 AS (x int);
CREATE TABLE t1 (v c1);
INSERT INTO t1 VALUES (row(1));
CREATE INDEX t1_i ON t1 ((json_array(v returning jsonb)));
ALTER TYPE c1 ADD ATTRIBUTE y int;
SELECT count(*) FROM t1
WHERE json_array(v returning jsonb) = json_array(row(1, null)::c1 returning
jsonb);
-- 19rc1 / master: index scan 0, seq scan 1; REINDEX gives 1. 18.6 rejects
the CREATE INDEX.
4. Mechanism (line numbers from master at f9ccc5e4f92)
------------------------------------------------------
contain_mutable_functions_walker() (optimizer/util/clauses.c:424) asks
to_json_is_immutable() / to_jsonb_is_immutable() about the type of each
argument of a
JsonConstructorExpr. Both call json_check_mutability()
(utils/adt/jsonfuncs.c:6078). For
TYPTYPE_COMPOSITE it reads the current tuple descriptor
(lookup_rowtype_tupdesc(typoid, -1),
jsonfuncs.c:6103) and recurses into each live attribute.
The answer is only valid for the type's shape at that moment, and nothing
records it:
- composite_to_json() (json.c:521) and composite_to_jsonb() (jsonb.c:978) emit
the
attribute names and one entry per live attribute, so the value depends on
catalog state
even when every attribute type is immutable. That is the same reason
record_out() is
STABLE.
- ALTER TYPE ... ADD / DROP / RENAME ATTRIBUTE and ALTER TABLE ... ADD / RENAME
COLUMN on
the row type are allowed while an index expression depends on the type
(measured: C1-C8).
find_composite_type_dependencies() (commands/tablecmds.c:6982) only stops
ALTER ATTRIBUTE
TYPE here (C9).
So the recursion is not missing. What is missing is a way to account for the
composite's
shape changing after the check. A recheck limited to "a mutable attribute was
added" would
leave C1-C8, U1, P1, G1 and K1 open.
5. Caveats
----------
- jian's v2-0001 was not measured in this run. From reading it, v2 changes only
record,
anyarray and anycompatiblearray; named composites still go through the
TYPTYPE_COMPOSITE
recursion, so these vectors are expected to remain. This is an inference, not
a result.
- No fix is measured here.
- The REINDEX step was not run for C4b, C8 and C8b; their divergence is shown
without it.
- Scalar types whose text output depends on a GUC (interval, bytea_output,
float,
money) were not part of this sweep.
- The exact commit of the master binary is not recorded separately from its
source tree.