-- test/sql/multi_level_merge.sql -- -- Two 1.5.6 hardening behaviours: -- -- 1. Multi-level upper-tree pending merge. Before 1.5.6 the pending -- merge could read back only a SINGLE upper leaf; once enough -- distinct trigrams accumulated that VACUUM had to build a -- multi-LEVEL upper tree (an UPPER internal page, page_kind=2, -- fanning out to many UPPER leaves, page_kind=3), the next merge -- raised "pending-list merge on multi-level upper tree not yet -- supported" and demanded a REINDEX. 1.5.6 makes -- snapshot_existing_upper descend the whole upper subtree -- recursively, so a second batch of inserts merges cleanly. -- -- 2. pg_tre.compile_timeout_ms enforcement. The GUC was previously -- defined but never enforced; 1.5.6 arms a real wall-clock -- deadline around regex compilation (via a progress hook woven -- into TRE's NFA-build loops) and raises a cancel error when a -- pathological pattern blows the budget. -- -- Both behaviours are deterministic: the multi-level case is asserted -- via a page-kind histogram (an UPPER internal page must exist after -- the build, and a second batch + VACUUM must not error and must still -- agree with the heap), and the timeout case via a pattern whose -- bounded-repetition expansion reliably exceeds a 1 ms budget. CREATE EXTENSION IF NOT EXISTS pg_tre; psql:test/sql/multi_level_merge.sql:27: NOTICE: extension "pg_tre" already exists, skipping CREATE EXTENSION CREATE EXTENSION IF NOT EXISTS pageinspect; CREATE EXTENSION -- ------------------------------------------------------------------ -- Part 1: multi-level upper-tree pending merge -- ------------------------------------------------------------------ DROP TABLE IF EXISTS mlm CASCADE; psql:test/sql/multi_level_merge.sql:34: NOTICE: table "mlm" does not exist, skipping DROP TABLE CREATE TABLE mlm (id serial PRIMARY KEY, body text); CREATE TABLE -- 4000 distinct md5 hashes generate far more distinct trigrams than fit -- in a single upper leaf, forcing VACUUM to build a multi-LEVEL upper -- tree (>= 1 UPPER internal page). INSERT INTO mlm(body) SELECT md5(g::text) FROM generate_series(1, 4000) g; INSERT 0 4000 SET client_min_messages = 'warning'; SET CREATE INDEX mlm_idx ON mlm USING tre (body); CREATE INDEX RESET client_min_messages; RESET -- Drain the pending list into the posting/upper tree. VACUUM mlm; VACUUM -- Count UPPER internal pages (page_kind = PG_TRE_PAGE_UPPER = 2). The -- page-kind tag is the first byte of each page's special area, whose -- offset we read from the page header (robust to special-area size). CREATE OR REPLACE FUNCTION mlm_internal_pages() RETURNS bigint LANGUAGE sql AS $$ SELECT count(*) FROM ( SELECT get_byte(get_raw_page('mlm_idx', b), (page_header(get_raw_page('mlm_idx', b))).special) AS kind FROM generate_series(0, pg_relation_size('mlm_idx')/8192 - 1) b ) s WHERE kind = 2; $$; CREATE FUNCTION -- A multi-level tree exists: at least one UPPER internal page. SELECT mlm_internal_pages() > 0 AS has_multi_level_upper_tree; has_multi_level_upper_tree ---------------------------- t (1 row) CREATE OR REPLACE FUNCTION mlm_diff(pat text) RETURNS text LANGUAGE plpgsql AS $$ DECLARE seq_ids int[]; idx_ids int[]; BEGIN SET LOCAL enable_indexscan=off; SET LOCAL enable_bitmapscan=off; SET LOCAL enable_seqscan=on; EXECUTE format('SELECT array_agg(id ORDER BY id) FROM mlm ' 'WHERE tre_amatch(body, %L, 0)', pat) INTO seq_ids; SET LOCAL enable_seqscan=off; SET LOCAL enable_indexscan=on; SET LOCAL enable_bitmapscan=on; EXECUTE format('SELECT array_agg(id ORDER BY id) FROM mlm ' 'WHERE body %%~~ tre_pattern(%L, 0)', pat) INTO idx_ids; IF seq_ids IS NOT DISTINCT FROM idx_ids THEN RETURN 'OK ' || pat; END IF; RETURN format('BAD %s seq=%s idx=%s', pat, cardinality(seq_ids), cardinality(idx_ids)); END$$; CREATE FUNCTION -- Baseline agreement after the first build. SELECT mlm_diff('abc'); mlm_diff ---------- OK abc (1 row) SELECT mlm_diff('00'); mlm_diff ---------- OK 00 (1 row) -- Second batch. Before 1.5.6 this VACUUM raised: -- ERROR: pg_tre: pending-list merge on multi-level upper tree not yet -- supported (HINT: REINDEX ...) -- Now it must merge cleanly into the existing multi-level tree. INSERT INTO mlm(body) SELECT md5(g::text) FROM generate_series(4001, 8000) g; INSERT 0 4000 VACUUM mlm; VACUUM -- Still multi-level, and the index still agrees with the heap over the -- merged data set. SELECT mlm_internal_pages() > 0 AS still_multi_level; still_multi_level ------------------- t (1 row) SELECT mlm_diff('abc'); mlm_diff ---------- OK abc (1 row) SELECT mlm_diff('00'); mlm_diff ---------- OK 00 (1 row) SELECT mlm_diff('def'); mlm_diff ---------- OK def (1 row) DROP FUNCTION mlm_internal_pages(); DROP FUNCTION DROP FUNCTION mlm_diff(text); DROP FUNCTION DROP INDEX mlm_idx; DROP INDEX DROP TABLE mlm; DROP TABLE -- ------------------------------------------------------------------ -- Part 2: pg_tre.compile_timeout_ms enforcement -- ------------------------------------------------------------------ -- Raise the NFA-state cap out of the way so the timeout (not the state -- guard) is what trips, and confirm the GUC's visible default. SHOW pg_tre.compile_timeout_ms; pg_tre.compile_timeout_ms --------------------------- 1s (1 row) -- A pattern with deeply nested bounded repetitions whose AST expansion -- reliably takes far longer than 1 ms to compile. With the timeout -- armed the compile is pre-empted and a cancel error is raised. SET pg_tre.max_nfa_states = 1000000; SET SET pg_tre.compile_timeout_ms = 1; SET SELECT tre_amatch('x', 'a{80}{80}{80}', 0); psql:test/sql/multi_level_merge.sql:122: ERROR: pg_tre: regex compilation exceeded pg_tre.compile_timeout_ms = 1 ms HINT: Simplify the pattern (reduce nested bounded repetitions) or raise pg_tre.compile_timeout_ms for trusted callers. -- A trivial pattern compiles well within a generous budget (proving the -- deadline only fires for genuinely expensive compiles, and is disarmed -- cleanly afterwards). SET pg_tre.compile_timeout_ms = 5000; SET SELECT tre_amatch('hello', 'hello', 0); tre_amatch ------------ t (1 row) RESET ALL; RESET