-- -- Phase 6 safety hardening tests -- -- Covers: -- 1. Pattern complexity guard (NFA state limits) -- 2. Per-index reloptions -- 3. Query cancellation during extraction (manual test only) -- -- Setup: ensure extension is loaded CREATE EXTENSION IF NOT EXISTS pg_tre; psql:test/sql/p6_safety.sql:11: NOTICE: extension "pg_tre" already exists, skipping CREATE EXTENSION SET client_min_messages = WARNING; SET -- -- Test 1: Reloptions parsing and application -- -- Create table for reloption tests CREATE TABLE relopts_test (id int, body text); CREATE TABLE INSERT INTO relopts_test VALUES (1, 'hello world'), (2, 'goodbye world'); INSERT 0 2 -- Create index with custom reloptions CREATE INDEX relopts_idx ON relopts_test USING tre (body) WITH (fastupdate=on, range_size_blocks=256); CREATE INDEX -- Verify index exists \d+ relopts_idx Index "public.relopts_idx" Column | Type | Key? | Definition | Storage | Stats target --------+------+------+------------+----------+-------------- body | text | yes | body | extended | tre, for table "public.relopts_test" Options: fastupdate=on, range_size_blocks=256 -- Test that index works with custom options SELECT id FROM relopts_test WHERE body %~~ tre_pattern('hello', 0) ORDER BY id; id ---- 1 (1 row) -- Cleanup DROP INDEX relopts_idx; DROP INDEX DROP TABLE relopts_test; DROP TABLE -- -- Test 2: Pattern complexity guard -- -- This pattern has moderate complexity (should pass) SELECT tre_amatch('test string for matching', 'test.*matching', 0); tre_amatch ------------ t (1 row) -- Test GUC for max_nfa_states SHOW pg_tre.max_nfa_states; pg_tre.max_nfa_states ----------------------- 10000 (1 row) -- Lower the limit temporarily to test rejection SET pg_tre.max_nfa_states = 100; SET -- This pattern should be simple enough even with the low limit SELECT tre_amatch('hello', 'hello', 0); tre_amatch ------------ t (1 row) -- Try a more complex pattern (may or may not exceed limit depending on TRE internals) -- We can't reliably trigger the limit without knowing TRE's exact NFA construction, -- so we just verify the GUC works SET pg_tre.max_nfa_states = 10000; -- restore default SET SHOW pg_tre.max_nfa_states; pg_tre.max_nfa_states ----------------------- 10000 (1 row) -- -- Test 3: Verify GUC defaults -- SHOW pg_tre.pending_list_limit; pg_tre.pending_list_limit --------------------------- 0 (1 row) SHOW pg_tre.range_size_blocks; pg_tre.range_size_blocks -------------------------- 128 (1 row) SHOW pg_tre.fastupdate; pg_tre.fastupdate ------------------- on (1 row) SHOW pg_tre.max_extraction_fanout; pg_tre.max_extraction_fanout ------------------------------ 4096 (1 row) SHOW pg_tre.compile_timeout_ms; pg_tre.compile_timeout_ms --------------------------- 1s (1 row) SHOW pg_tre.match_timeout_ms; pg_tre.match_timeout_ms ------------------------- 1s (1 row) -- -- Test 4: Reloptions validation -- CREATE TABLE relopts_validation_test (id int, body text); CREATE TABLE -- Valid reloptions CREATE INDEX relopts_valid_idx ON relopts_validation_test USING tre (body) WITH (range_size_blocks=64); -- valid value CREATE INDEX DROP INDEX relopts_valid_idx; DROP INDEX -- q must be 3 until Phase 8 CREATE INDEX relopts_q_idx ON relopts_validation_test USING tre (body) WITH (q=3); CREATE INDEX DROP INDEX relopts_q_idx; DROP INDEX -- Cleanup DROP TABLE relopts_validation_test; DROP TABLE -- -- Test 5: Multiple indexes with different options -- CREATE TABLE multi_opts_test (id int, body text); CREATE TABLE INSERT INTO multi_opts_test SELECT i, 'text' || i FROM generate_series(1, 100) i; INSERT 0 100 -- Index 1: default options CREATE INDEX multi_opts_idx1 ON multi_opts_test USING tre (body); CREATE INDEX -- Index 2: custom range size CREATE INDEX multi_opts_idx2 ON multi_opts_test USING tre (body) WITH (range_size_blocks=256); CREATE INDEX -- Index 3: pending list merged on INSERT past 128 kB CREATE INDEX multi_opts_idx3 ON multi_opts_test USING tre (body) WITH (pending_list_limit=128); CREATE INDEX -- All indexes should work; the planner can use one for a selective -- pattern. Version-robust: assert plan-node category via a single -- text token (no exact plan-shape dump, which differs across PG -- majors and across which of the three indexes the planner picks). CREATE FUNCTION multi_opts_uses_index(q text) RETURNS boolean LANGUAGE plpgsql AS $$ DECLARE r record; node text; BEGIN -- Silence the version-independent "reloptions not initialized" -- WARNING that planning these indexes emits, so the expected output -- is a single deterministic token. SET LOCAL client_min_messages = error; FOR r IN EXECUTE 'EXPLAIN (FORMAT JSON) ' || q LOOP node := r."QUERY PLAN"->0->'Plan'->>'Node Type'; END LOOP; RETURN node ILIKE '%Index%' OR node ILIKE '%Bitmap%'; END $$; CREATE FUNCTION SET enable_seqscan = off; SET SELECT CASE WHEN multi_opts_uses_index($$SELECT * FROM multi_opts_test WHERE body %~~ tre_pattern('text1', 0)$$) THEN 'multi_opts_uses_index' ELSE 'multi_opts_seq' END AS multi_opts_plan; multi_opts_plan ----------------------- multi_opts_uses_index (1 row) SET enable_seqscan = on; SET DROP FUNCTION multi_opts_uses_index(text); DROP FUNCTION -- Cleanup DROP TABLE multi_opts_test CASCADE; DROP TABLE -- -- Test 6: Pattern cache behavior under complexity guard -- -- Cache should work normally for simple patterns SELECT tre_amatch('test', 'test', 0); tre_amatch ------------ t (1 row) SELECT tre_amatch('test', 'test', 0); -- cache hit tre_amatch ------------ t (1 row) -- Different pattern SELECT tre_amatch('another', 'another', 0); tre_amatch ------------ t (1 row) -- -- Test 7: CHECK_FOR_INTERRUPTS (documentation only) -- -- Manual test procedure: -- 1. Run a complex query with a pattern that generates deep recursion: -- SELECT * FROM large_table WHERE body %~~ tre_pattern('(a+)+', 0); -- 2. Press Ctrl-C during execution -- 3. Verify the query is canceled within 1-2 seconds -- This cannot be tested in pg_regress without external tooling RESET ALL; RESET