Skip to content

ClickHouse: map PostgreSQL regex-match operators (~, ~*, !~, !~*) to match()#5

Open
alexey-milovidov wants to merge 1 commit into
mainfrom
pg-regexp-clickhouse
Open

ClickHouse: map PostgreSQL regex-match operators (~, ~*, !~, !~*) to match()#5
alexey-milovidov wants to merge 1 commit into
mainfrom
pg-regexp-clickhouse

Conversation

@alexey-milovidov

@alexey-milovidov alexey-milovidov commented Jul 13, 2026

Copy link
Copy Markdown
Member

What

Maps PostgreSQL's regex-match operators to ClickHouse's match() when transpiling to the ClickHouse dialect:

PostgreSQL ClickHouse
x ~ 'p' match(x, 'p')
x ~* 'p' match(x, '(?i)p')
x !~ 'p' NOT (match(x, 'p'))
x !~* 'p' NOT (match(x, '(?i)p'))

The mapping lives on the RegexpLike/RegexpILike AST nodes, so MySQL RLIKE/REGEXP sources also transpile to match().

Why

ClickHouse has no REGEXP_LIKE function and no case-insensitive regex operator. Previously these operators were emitted as REGEXP_LIKE(...) (for ~/!~) or an invalid infix x REGEXP_ILIKE y (for ~*/!~*), both of which ClickHouse rejects:

Function with name `REGEXP_LIKE` does not exist

This surfaced while transpiling PostgreSQL's regression test suite to ClickHouse; the ~-family operators are common in real queries (e.g. WHERE name ~ '^A').

How

Adds a ClickHouse branch to generate_regexp_like and generate_regexp_i_like. Case-insensitivity uses an inline (?i) flag — inlined into a string-literal pattern for readable output, and wrapped as concat('(?i)', pattern) for a non-literal pattern so the flag still applies. !~/!~* are already represented as NOT(RegexpLike/RegexpILike), so they follow automatically.

Scope: only the unambiguous operators

Deliberately excluded are PostgreSQL's polymorphic operators — @>, <@, &&, @@, >>, <<. Their meaning is type-dependent (array vs jsonb vs range vs inet vs bit-string containment / overlap / shift), which a purely syntactic transpiler cannot resolve without operand type information. Mapping @> to hasAll would be right for arrays but wrong for jsonb/ranges, and PostgreSQL's >>/<< are bit-string/inet operators (not integer shifts), so bitShiftRight would be incorrect. These continue to pass through unchanged — surfacing as a ClickHouse parse error rather than silently-wrong results. The regex-match operators are unambiguous (text-only), which is why they are safe to map here.

Tests

Adds crates/polyglot-sql/tests/postgres_regexp_clickhouse.rs. Output semantics were verified against a real ClickHouse instance (match('ABC','(?i)abc') = 1, match('ABC','abc') = 0, NOT (match('abc','x')) = 1). The full polyglot-sql suite passes; the only failing test, tpch_query2_transpile_postgres_to_fabric, is pre-existing on main and unrelated to this change.


Upstream PR: tobilg#316

ClickHouse has no REGEXP_LIKE function and no case-insensitive regex
operator. The PostgreSQL regex-match operators were emitted as
REGEXP_LIKE(...) (for ~ and !~) or an invalid "x REGEXP_ILIKE y" infix
form (for ~* and !~*), both of which ClickHouse rejects at parse time.

Map them to ClickHouse's match(haystack, pattern):

    x ~   'p'  ->  match(x, 'p')
    x ~*  'p'  ->  match(x, '(?i)p')
    x !~  'p'  ->  NOT (match(x, 'p'))
    x !~* 'p'  ->  NOT (match(x, '(?i)p'))

Case-insensitive matching uses an inline (?i) flag: inlined into a
string-literal pattern for readable output, and wrapped with concat() for a
non-literal pattern expression so the flag still applies. The mapping is on
the RegexpLike/RegexpILike nodes, so MySQL RLIKE/REGEXP sources transpile to
match() as well.

The polymorphic operators (@>, <@, &&, @@, >>, <<) are intentionally left
untouched: their meaning is type-dependent and cannot be resolved without
operand type information.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant