diff --git a/crates/polyglot-sql/src/generator.rs b/crates/polyglot-sql/src/generator.rs index 2b354cc..c276dfc 100644 --- a/crates/polyglot-sql/src/generator.rs +++ b/crates/polyglot-sql/src/generator.rs @@ -20171,6 +20171,14 @@ impl Generator { self.generate_expression(&f.this)?; self.write(" ~ "); self.generate_expression(&f.pattern)?; + } else if matches!(self.config.dialect, Some(DialectType::ClickHouse)) && f.flags.is_none() + { + // ClickHouse has no REGEXP_LIKE; the regex-match operator is match(haystack, pattern). + self.write("match("); + self.generate_expression(&f.this)?; + self.write(", "); + self.generate_expression(&f.pattern)?; + self.write(")"); } else if matches!(self.config.dialect, Some(DialectType::Exasol)) && f.flags.is_none() { // Exasol uses REGEXP_LIKE as infix binary operator self.generate_expression(&f.this)?; @@ -34936,6 +34944,26 @@ impl Generator { self.generate_expression(&e.this)?; self.write(" ~* "); self.generate_expression(&e.expression)?; + } else if matches!(self.config.dialect, Some(DialectType::ClickHouse)) && e.flag.is_none() { + // ClickHouse has no case-insensitive regex operator; use match() with an inline + // (?i) flag. Inline it into a string-literal pattern for readable output, and fall + // back to concat() so the flag also applies to a non-literal pattern expression. + self.write("match("); + self.generate_expression(&e.this)?; + self.write(", "); + if let Expression::Literal(lit) = e.expression.as_ref() { + if let Literal::String(s) = lit.as_ref() { + let insensitive = + Expression::Literal(Box::new(Literal::String(format!("(?i){s}")))); + self.generate_expression(&insensitive)?; + self.write(")"); + return Ok(()); + } + } + self.write("concat('(?i)', "); + self.generate_expression(&e.expression)?; + self.write("))"); + return Ok(()); } else if matches!(self.config.dialect, Some(DialectType::Snowflake)) { // Snowflake uses REGEXP_LIKE(x, pattern, 'i') self.write_keyword("REGEXP_LIKE"); diff --git a/crates/polyglot-sql/tests/postgres_regexp_clickhouse.rs b/crates/polyglot-sql/tests/postgres_regexp_clickhouse.rs new file mode 100644 index 0000000..348720f --- /dev/null +++ b/crates/polyglot-sql/tests/postgres_regexp_clickhouse.rs @@ -0,0 +1,70 @@ +//! Regression tests: PostgreSQL regex-match operators transpiled to ClickHouse. +//! +//! ClickHouse has no `REGEXP_LIKE` function and no case-insensitive regex operator; the +//! regex-match primitive is `match(haystack, pattern)`. Previously the PostgreSQL operators +//! `~`, `~*`, `!~`, `!~*` were emitted as `REGEXP_LIKE(...)` / an invalid `x REGEXP_ILIKE y` +//! infix form, both of which ClickHouse rejects. They now map to `match`, with case-insensitive +//! matching expressed via an inline `(?i)` flag. + +use polyglot_sql::{transpile, DialectType}; + +fn pg_ch(sql: &str) -> String { + transpile(sql, DialectType::PostgreSQL, DialectType::ClickHouse) + .expect("PostgreSQL -> ClickHouse transpilation should succeed") + .join("; ") +} + +#[test] +fn regex_match_operator_maps_to_match() { + let cases = [ + ("SELECT x ~ 'ab.*' FROM t", "SELECT match(x, 'ab.*') FROM t"), + ( + "SELECT * FROM t WHERE name ~ '^A'", + "SELECT * FROM t WHERE match(name, '^A')", + ), + ]; + for (input, expected) in cases { + assert_eq!(pg_ch(input), expected, "input: {input}"); + } +} + +#[test] +fn case_insensitive_regex_operator_uses_inline_flag() { + // A string-literal pattern gets the (?i) flag inlined for readable output. + assert_eq!( + pg_ch("SELECT x ~* 'ab.*' FROM t"), + "SELECT match(x, '(?i)ab.*') FROM t" + ); + // A non-literal pattern falls back to concat() so the flag still applies. + assert_eq!( + pg_ch("SELECT x ~* y FROM t"), + "SELECT match(x, concat('(?i)', y)) FROM t" + ); +} + +#[test] +fn negated_regex_operators_map_to_not_match() { + assert_eq!( + pg_ch("SELECT x !~ 'ab.*' FROM t"), + "SELECT NOT (match(x, 'ab.*')) FROM t" + ); + assert_eq!( + pg_ch("SELECT x !~* 'ab.*' FROM t"), + "SELECT NOT (match(x, '(?i)ab.*')) FROM t" + ); +} + +#[test] +fn mysql_rlike_also_maps_to_clickhouse_match() { + // The mapping is on the RegexpLike node, so RLIKE/REGEXP sources benefit too. + assert_eq!( + transpile( + "SELECT x RLIKE 'a' FROM t", + DialectType::MySQL, + DialectType::ClickHouse + ) + .expect("MySQL -> ClickHouse transpilation should succeed") + .join("; "), + "SELECT match(x, 'a') FROM t" + ); +}