\unset ECHO CREATE SERVER regex_loopback FOREIGN DATA WRAPPER clickhouse_fdw OPTIONS(dbname 'regex_test', driver 'binary'); CREATE USER MAPPING FOR CURRENT_USER SERVER regex_loopback; SELECT clickhouse_raw_query('DROP DATABASE IF EXISTS regex_test'); SELECT clickhouse_raw_query('CREATE DATABASE regex_test'); SELECT clickhouse_raw_query($$ CREATE TABLE regex_test.strings(id Int32, val String) engine=TinyLog() $$); SELECT clickhouse_raw_query($$ INSERT INTO regex_test.strings VALUES (1, 'val1'), (2, 'val2'), (3, 'a,b,c'), (4, 'foo,bar,baz'), (5, 'sleep no more'), (6, 'aa-T-bb-t-cc'), (7, 'test@example.com, user@hi.example'), (8, 'line\ntarget') $$); CREATE FOREIGN TABLE strings (id int, val text) SERVER regex_loopback; \unset ECHO -- Use DO to test functions available in Postgres 15+ -- PG15+: trim_array → match() DO $_$ DECLARE rec record; tc jsonb; output JSONB; tests jsonb[] := ARRAY[ $${ "func": "regexp_like", "where": "regexp_like(val, '^val\\d')", "push": "(match(val, '^val\\\\d'))" }$$, $${ "func": "case-insensitive regexp_like", "where": "regexp_like(val, '^VAL\\d', 'i')", "push": "(match(val, concat('(?i)', '^VAL\\\\d')))" }$$, $${ "func": "regexp_like with si", "where": "regexp_like(val, '^VAL\\d', 'si')", "push": "(match(val, concat('(?is)', '^VAL\\\\d')))" }$$, $${ "func": "regexp_like with unsupported flag", "where": "regexp_like(val, '^VAL\\d', 'ix')" }$$, $${ "func": "regexp_like with t flag", "where": "regexp_like(val, '^VAL\\d', 'it')", "push": "(match(val, concat('(?i)', '^VAL\\\\d')))" }$$ ]; BEGIN IF current_setting('server_version_num')::int >= 150000 THEN FOREACH tc IN ARRAY tests LOOP EXECUTE format('EXPLAIN (VERBOSE, FORMAT JSON) SELECT val FROM strings WHERE %s', tc->>'where') INTO output; If tc ? 'push' THEN RAISE NOTICE '% PUSHED DOWN: %', tc->>'func', jsonb_path_query( output, '$[0].Plan' )->>'Remote SQL' = format('SELECT val FROM regex_test.strings WHERE %s', tc->>'push'); ELSE RAISE NOTICE '% NOT PUSHED DOWN: %', tc->>'func', jsonb_path_query( output, '$[0].Plan' )->>'Remote SQL' = 'SELECT val FROM regex_test.strings'; END IF; FOR rec IN EXECUTE format('SELECT val FROM strings WHERE %s', tc->>'where') LOOP RAISE NOTICE '%', rec; END LOOP; END LOOP; ELSE -- Fake it on earlier versions. RAISE NOTICE 'regexp_like PUSHED DOWN: t'; RAISE NOTICE '(val1)'; RAISE NOTICE '(val2)'; RAISE NOTICE 'case-insensitive regexp_like PUSHED DOWN: t'; RAISE NOTICE '(val1)'; RAISE NOTICE '(val2)'; RAISE NOTICE 'regexp_like with si PUSHED DOWN: t'; RAISE NOTICE '(val1)'; RAISE NOTICE '(val2)'; RAISE NOTICE 'regexp_like with unsupported flag NOT PUSHED DOWN: t'; RAISE NOTICE '(val1)'; RAISE NOTICE '(val2)'; RAISE NOTICE 'regexp_like with t flag PUSHED DOWN: t'; RAISE NOTICE '(val1)'; RAISE NOTICE '(val2)'; END IF; END; $_$; \set ECHO all -- Check regexp_split_to_array. EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM strings WHERE regexp_split_to_array(val, ',') = '{a,b,c}'::text[]; SELECT val FROM strings WHERE regexp_split_to_array(val, ',') = '{a,b,c}'::text[]; EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM strings WHERE regexp_split_to_array(val, '\s+') = '{sleep,no,more}'::text[]; SELECT val FROM strings WHERE regexp_split_to_array(val, '\s+') = '{sleep,no,more}'::text[]; EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM strings WHERE regexp_split_to_array(val, '-t-', 'i') = '{aa,bb,cc}'::text[]; SELECT val FROM strings WHERE regexp_split_to_array(val, '-t-', 'i') = '{aa,bb,cc}'::text[]; -- No pushdown when the regex is not a constant. EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM strings WHERE regexp_split_to_array(val, val) = '{}'::text[]; -- Check regexp_replace(). EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM strings WHERE regexp_replace(val, '[0,1]$', '_xyz') = 'val_xyz'; SELECT * FROM strings WHERE regexp_replace(val, '[0,1]$', '_xyz') = 'val_xyz'; -- No replace returns unmodified string. EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM strings WHERE regexp_replace(val, '^x', 'y') = 'val2'; SELECT * FROM strings WHERE regexp_replace(val, '^x', 'y') = 'val2'; -- Case-insensitive, refer to capture. EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM strings WHERE regexp_replace(val, 'VAL([0,1])$', 'x-\1', 'i') = 'x-1'; SELECT * FROM strings WHERE regexp_replace(val, 'VAL([0,1])$', 'x-\1', 'i') = 'x-1'; -- Case-insensitive. EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM strings WHERE regexp_replace(val, '[VL]', 'x', 'i') = 'xal1'; SELECT * FROM strings WHERE regexp_replace(val, '[VL]', 'x', 'i') = 'xal1'; -- Replace all case-insensitive. EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM strings WHERE regexp_replace(val, '[VL]', 'x', 'gig') = 'xax1'; SELECT * FROM strings WHERE regexp_replace(val, '[VL]', 'x', 'gig') = 'xax1'; -- Refer to full match. EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM strings WHERE regexp_replace(val, '^val', 'x-\0', 'i') = 'x-val1'; SELECT * FROM strings WHERE regexp_replace(val, '^val', 'x-\0', 'i') = 'x-val1'; -- No pushdown when the regex is not a constant. EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM strings WHERE regexp_replace(val, val, '') = ''; -- Check regexp_match(). EXPLAIN (VERBOSE, COSTS OFF) SELECT regexp_match(val, '[01]$') FROM strings WHERE regexp_match(val, '[01]$') = '{1}'::text[]; SELECT regexp_match(val, '[01]$') FROM strings WHERE regexp_match(val, '[01]$') = '{1}'::text[]; -- No match. EXPLAIN (VERBOSE, COSTS OFF) SELECT id, regexp_match(val, '[34]$') FROM strings WHERE regexp_match(val, '[34]$') = '{}'::text[]; SELECT id, regexp_match(val, '[34]$') FROM strings WHERE regexp_match(val, '[34]$') = '{}'::text[]; -- Multiple matches. EXPLAIN (VERBOSE, COSTS OFF) SELECT regexp_match(val, 'ba') FROM strings WHERE regexp_match(val, 'ba') = '{ba}'::text[]; SELECT regexp_match(val, 'ba') FROM strings WHERE regexp_match(val, 'ba') = '{ba}'::text[]; -- Capturing groups. EXPLAIN (VERBOSE, COSTS OFF) SELECT regexp_match(val, '([a-zA-Z0-9]+)@') FROM strings WHERE regexp_match(val, '([a-zA-Z0-9]+)@') = '{test}'::text[]; SELECT regexp_match(val, '([a-zA-Z0-9]+)@') FROM strings WHERE regexp_match(val, '([a-zA-Z0-9]+)@') = '{test}'::text[]; -- Case-insensitive. EXPLAIN (VERBOSE, COSTS OFF) SELECT regexp_match(val, 'VAL[01]$', 'i') FROM strings WHERE regexp_match(val, 'VAL[01]$', 'i') = '{val1}'::text[]; SELECT regexp_match(val, 'VAL[01]$', 'i') FROM strings WHERE regexp_match(val, 'VAL[01]$', 'i') = '{val1}'::text[]; -- No pushdown when the regex is not a constant. EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM strings WHERE regexp_match(val, val) = '{1}'::text[]; -- Compare consistency of `.` matching a newline. SELECT regexp_match(val, 'line.') FROM strings WHERE regexp_match(val, 'line.') = E'{"line\n"}'::text[]; SELECT regexp_match(val, 'line.', 's') FROM strings WHERE regexp_match(val, 'line.', 's') = E'{"line\n"}'::text[]; SELECT regexp_match(val, 'line.', 'm'), id FROM strings WHERE regexp_match(val, 'line.', 'm') = '{}'::text[] ORDER BY id; SELECT regexp_match(val, 'line.', 'n'), id FROM strings WHERE regexp_match(val, 'line.', 'n') = '{}'::text[] ORDER BY id; SELECT regexp_match(val, 'line.', 'p'), id FROM strings WHERE regexp_match(val, 'line.', 'p') = '{}'::text[] ORDER BY id; SELECT regexp_match(val, 'line.', 'w') FROM strings WHERE regexp_match(val, 'line.', 'w') = E'{"line\n"}'::text[]; -- Last flag overrides. SELECT regexp_match(val, 'line.', 'ms') FROM strings WHERE regexp_match(val, 'line.', 'ms') = E'{"line\n"}'::text[]; SELECT regexp_match(val, 'line.', 'sm'), id FROM strings WHERE regexp_match(val, 'line.', 'sm') = '{}'::text[] ORDER BY id; SELECT regexp_match(val, 'line.', 'mspw') FROM strings WHERE regexp_match(val, 'line.', 'mspw') = E'{"line\n"}'::text[]; -- Compare consistency of `[^x]` matching a newline. SELECT regexp_match(val, 'line[^x]') FROM strings WHERE regexp_match(val, 'line[^x]') = E'{"line\n"}'::text[]; SELECT regexp_match(val, 'line[^x]', 's') FROM strings WHERE regexp_match(val, 'line[^x]', 's') = E'{"line\n"}'::text[]; -- m, n match in CH but not PG SELECT regexp_match(val, 'line[^x]', 'm') FROM strings WHERE regexp_match(val, 'line[^x]', 'm') = E'{"line\n"}'::text[]; SELECT regexp_match(val, 'line[^x]', 'n') FROM strings WHERE regexp_match(val, 'line[^x]', 'n') = E'{"line\n"}'::text[]; SELECT regexp_match(val, 'line[^x]', 'p') FROM strings WHERE regexp_match(val, 'line[^x]', 'p') = E'{"line\n"}'::text[]; -- w matches in both. SELECT regexp_match(val, 'line[^x]', 'w') FROM strings WHERE regexp_match(val, 'line[^x]', 'w') = E'{"line\n"}'::text[]; -- Last flag overrides. SELECT regexp_match(val, 'line[^x]', 'msw') FROM strings WHERE regexp_match(val, 'line[^x]', 'msw') = E'{"line\n"}'::text[]; -- Compare consistency of `$` end of line. SELECT regexp_match(val, 'line$'), id FROM strings WHERE regexp_match(val, 'line$') = '{}'::text[] ORDER BY id; SELECT regexp_match(val, 'line$', 'm') FROM strings WHERE regexp_match(val, 'line$', 'm') = E'{line}'::text[]; SELECT regexp_match(val, 'line$', 'n') FROM strings WHERE regexp_match(val, 'line$', 'n') = E'{line}'::text[]; SELECT regexp_match(val, 'line$', 's'), id FROM strings WHERE regexp_match(val, 'line$', 's') = '{}'::text[] ORDER BY id; -- p matches in CH but not PG. SELECT regexp_match(val, 'line$', 'p'), id FROM strings WHERE regexp_match(val, 'line$', 'p') = '{}'::text[] ORDER BY id; -- w matches in both. SELECT regexp_match(val, 'line$', 'w') FROM strings WHERE regexp_match(val, 'line$', 'w') = E'{line}'::text[]; -- Last flag overrides. SELECT regexp_match(val, 'line$', 'ms'), id FROM strings WHERE regexp_match(val, 'line$', 'ms') = '{}'::text[] ORDER BY id; SELECT regexp_match(val, 'line$', 'sm') FROM strings WHERE regexp_match(val, 'line$', 'sm') = E'{line}'::text[]; SELECT regexp_match(val, 'line$', 'smw') FROM strings WHERE regexp_match(val, 'line$', 'smw') = E'{line}'::text[]; -- Compare consistency of `^` start of line. SELECT regexp_match(val, '^target'), id FROM strings WHERE regexp_match(val, '^target') = '{}'::text[] ORDER BY id; SELECT regexp_match(val, '^target', 'm') FROM strings WHERE regexp_match(val, '^target', 'm') = E'{target}'::text[]; SELECT regexp_match(val, '^target', 'n') FROM strings WHERE regexp_match(val, '^target', 'n') = E'{target}'::text[]; SELECT regexp_match(val, '^target', 's'), id FROM strings WHERE regexp_match(val, '^target', 's') = '{}'::text[] ORDER BY id; -- p matches in CH but not PG. SELECT regexp_match(val, '^target', 'p'), id FROM strings WHERE regexp_match(val, '^target', 'p') = '{}'::text[] ORDER BY id; -- w matches in both. SELECT regexp_match(val, '^target', 'w') FROM strings WHERE regexp_match(val, '^target', 'w') = E'{target}'::text[]; -- Last flag overrides. SELECT regexp_match(val, '^target', 'ms'), id FROM strings WHERE regexp_match(val, '^target', 'ms') = '{}'::text[] ORDER BY id; SELECT regexp_match(val, '^target', 'sm') FROM strings WHERE regexp_match(val, '^target', 'sm') = E'{target}'::text[]; SELECT regexp_match(val, '^target', 'msw') FROM strings WHERE regexp_match(val, '^target', 'msw') = E'{target}'::text[]; -- Ensure no pushdown when we disable it. SET pg_clickhouse.pushdown_regex = 'false'; EXPLAIN (VERBOSE, COSTS OFF) SELECT val FROM strings WHERE regexp_split_to_array(val, ',') = '{a,b,c}'::text[]; EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM strings WHERE regexp_replace(val, '^x', 'y') = 'val2'; EXPLAIN (VERBOSE, COSTS OFF) SELECT * FROM strings WHERE regexp_match(val, '.') = '{1}'::text[]; \unset ECHO -- Use DO to test functions available in Postgres 15+ -- PG15+: trim_array → match() DO $_$ DECLARE output JSONB; BEGIN IF current_setting('server_version_num')::int >= 150000 THEN EXECUTE format($$EXPLAIN (VERBOSE, FORMAT JSON) SELECT val FROM strings WHERE regexp_like(val, '^val\d')$$) INTO output; RAISE NOTICE 'regexp_like: %', jsonb_path_query(output, '$[0].Plan')->>'Remote SQL'; ELSE -- Fake it on earlier versions. RAISE NOTICE 'regexp_like: SELECT val FROM regex_test.strings'; END IF; END; $_$; \set ECHO all DROP USER MAPPING FOR CURRENT_USER SERVER regex_loopback; SELECT clickhouse_raw_query('DROP DATABASE regex_test'); DROP SERVER regex_loopback CASCADE;