-- test/sql/tre_upstream_fixes.sql -- -- TRE matcher bugs fixed by patches/tre-upstream-fixes.patch (gburd/tre -- PRs #1 and #2), as seen from SQL. Each row was wrong before the patch: -- -- 1a backtracking matcher lost the multibyte read offset on backtrack -- (pos_add_next not saved), so match offsets were wrong and could even -- point past the end of the string; -- 1b a back reference's text was skipped with pointer arithmetic, leaving -- the previous-character state wrong (\> \< ^ right after \1 failed); -- 1c multibyte back references were compared one byte too early, so -- (éa)\1 never matched; -- 2 approximate matching could not insert before a failed $ / \> / \b -- (^caf$ was not within 1 edit of 'cafe'); -- 3 a character-class mismatch was not costed as a substitution in -- multibyte mode (^[[:alpha:]]$ vs '5' at k=1 failed). -- -- Offsets from tre_amatch_detail are byte offsets in the database encoding. -- Backreference rows only run in UTF-8 databases (the multibyte decoder); -- the expected file assumes UTF-8, as every other multibyte test here. CREATE EXTENSION IF NOT EXISTS pg_tre; psql:test/sql/tre_upstream_fixes.sql:22: NOTICE: extension "pg_tre" already exists, skipping CREATE EXTENSION \set VERBOSITY terse SET client_min_messages = warning; SET SELECT getdatabaseencoding() AS enc; enc ------ UTF8 (1 row) -- 1a/1b/1c: back references and offsets, exact matching (k=0). SELECT p.pat, p.subj, tre_amatch(p.subj, p.pat, 0) AS matched, d.match_start, d.match_end FROM (VALUES ('(éa)\1', 'éaéa'), -- 1c: was no match ('(é)\1', 'aéé'), -- 1a: was [4,7] (past the 5-byte end) ('(a)\1', 'a'), -- was a false match ('(-a)\1\>', '-a-a'), -- 1b: was no match ('(ab)\1é$', 'ababé'), ('b(c)\1', 'ébcc'), ('(é+)x\1', 'ééxéé') ) AS p(pat, subj) LEFT JOIN LATERAL tre_amatch_detail(p.subj, p.pat, 0) d ON true ORDER BY p.pat COLLATE "C"; pat | subj | matched | match_start | match_end ----------+-------+---------+-------------+----------- (-a)\1\> | -a-a | t | 0 | 4 (a)\1 | a | f | | (ab)\1é$ | ababé | t | 0 | 6 (é)\1 | aéé | t | 1 | 5 (é+)x\1 | ééxéé | t | 0 | 9 (éa)\1 | éaéa | t | 0 | 6 b(c)\1 | ébcc | t | 2 | 5 (7 rows) -- 1a without back references: a pattern that makes the backtracking -- matcher backtrack over a multibyte prefix must report correct offsets. SELECT d.match_start, d.match_end FROM tre_amatch_detail('éb', '(é|x)*b', 0) d; match_start | match_end -------------+----------- 0 | 3 (1 row) -- ^(foobar){~1}$ only allows an edit inside the group, so foobarx (one -- extra character after the group) stays a non-match through the index; -- with k=1 on the whole pattern it is cost 1 (above). -- 2: insertions before a positional assertion (k=1). SELECT p.pat, p.subj, tre_amatch_cost(p.subj, p.pat, 1) AS cost FROM (VALUES ('^caf$', 'cafe'), -- was no match ('caf$', 'cafe'), -- was no match ('^ca$', 'cac'), -- was no match ('ac\>', 'cacb'), -- was cost 2 ('^café$', 'cafée'), ('^(foobar){~1}$', 'foobarx') ) AS p(pat, subj) ORDER BY p.pat COLLATE "C"; pat | subj | cost ----------------+---------+------ ^(foobar){~1}$ | foobarx | 1 ^ca$ | cac | 1 ^caf$ | cafe | 1 ^café$ | cafée | 1 ac\> | cacb | 1 caf$ | cafe | 1 (6 rows) -- 3: a class mismatch is a substitution (k=1), multibyte decoder. Only -- collation-independent memberships here: classes follow the call's -- collation (is 'é' alpha? not under C), see tre_collation_classes. SELECT p.pat, p.subj, tre_amatch_cost(p.subj, p.pat, 1) AS cost FROM (VALUES ('^[[:alpha:]]$', '5'), ('^[[:digit:]]$', 'é'), ('^x[[:alpha:]]y$', 'x5y'), ('^[[:alpha:]]$', 'q') -- exact (cost 0) ) AS p(pat, subj) ORDER BY p.pat COLLATE "C", p.subj COLLATE "C"; pat | subj | cost -----------------+------+------ ^[[:alpha:]]$ | 5 | 1 ^[[:alpha:]]$ | q | 0 ^[[:digit:]]$ | é | 1 ^x[[:alpha:]]y$ | x5y | 1 (4 rows) -- Through the index: the same patterns via %~~ must agree with a seq scan. DROP TABLE IF EXISTS tuf; DROP TABLE CREATE TABLE tuf (id serial, s text); CREATE TABLE INSERT INTO tuf (s) VALUES ('éaéa'), ('aéé'), ('-a-a'), ('ababé'), ('cafe'), ('cafée'), ('foobarx'), ('x5y'), ('5'), ('é'), ('plain filler'), ('caf'); INSERT 0 12 INSERT INTO tuf (s) SELECT 'filler ' || g FROM generate_series(1, 300) g; INSERT 0 300 CREATE INDEX tuf_tre ON tuf USING tre (s); CREATE INDEX ANALYZE tuf; ANALYZE CREATE TEMP TABLE tuf_q (pat text, k int); CREATE TABLE INSERT INTO tuf_q VALUES ('(éa)\1', 0), ('(-a)\1\>', 0), ('(ab)\1é$', 0), ('^caf$', 1), ('^café$', 1), ('^(foobar){~1}$', 0), ('^x[[:alpha:]]y$', 1); INSERT 0 7 SET enable_seqscan = off; SET CREATE TEMP TABLE tuf_idx AS SELECT q.pat, q.k, array_agg(t.id ORDER BY t.id) AS ids FROM tuf_q q LEFT JOIN tuf t ON t.s %~~ tre_pattern(q.pat, q.k) GROUP BY q.pat, q.k; SELECT 7 RESET enable_seqscan; RESET SET enable_indexscan = off; SET enable_bitmapscan = off; SET SET CREATE TEMP TABLE tuf_seq AS SELECT q.pat, q.k, array_agg(t.id ORDER BY t.id) AS ids FROM tuf_q q LEFT JOIN tuf t ON t.s %~~ tre_pattern(q.pat, q.k) GROUP BY q.pat, q.k; SELECT 7 RESET enable_indexscan; RESET enable_bitmapscan; RESET RESET SELECT i.pat, i.k, i.ids AS idx_ids, i.ids IS NOT DISTINCT FROM s.ids AS idx_eq_seq FROM tuf_idx i JOIN tuf_seq s USING (pat, k) ORDER BY i.pat COLLATE "C"; pat | k | idx_ids | idx_eq_seq -----------------+---+----------+------------ (-a)\1\> | 0 | {3} | t (ab)\1é$ | 0 | {4} | t (éa)\1 | 0 | {1} | t ^(foobar){~1}$ | 0 | {NULL} | t ^caf$ | 1 | {5,12} | t ^café$ | 1 | {5,6,12} | t ^x[[:alpha:]]y$ | 1 | {8} | t (7 rows) DROP TABLE tuf; DROP TABLE